| // Copyright 2021 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package codifier |
| |
| import ( |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "os" |
| ) |
| |
| // Proc is the primary processing unit for Codifier. Notes: |
| // - Procs are designed to be operated on serially like p1().p2().p3(), |
| // allowing easy composability and simplifying the mental model required to |
| // create large bulk updates. |
| // - To enable serial composition, each operator returns the proc after |
| // modification by the operator. |
| // - When errors are found, the error is directly logged to the user and the |
| // operator returns nil. By returning nil on error, the operator chain is |
| // immediately stopped, making errors in long chains easy to spot and |
| // correct. |
| // - This error discipline also increases developer confidence in successfully |
| // applying large bulk updates. |
| type Proc struct { |
| // The source string that will be replaced. It should not be changed after |
| // initialization. |
| original string |
| |
| // The string that will replace the original string. It is initialized as a |
| // copy of the original string. It is the working string during processing and |
| // can be changed as needed to create the final replacement. |
| replacement string |
| |
| // The store is a key/value store used to hold data in the Proc. It holds |
| // strings or slices of strings. |
| // TODO(gboone): Don't export. Add accessors. |
| Store map[string]interface{} |
| |
| // If there is a parent, it will use this Proc's replacement whereever this |
| // Proc's original occurs in the parent's replacement. In this way, changes |
| // can be accumulated up to the original parent. |
| parent *Proc |
| |
| // The Fuschia directory, defaulted to "~/fuchsia" |
| fuchsiaDir string |
| |
| // Has a build been run and succeeded? |
| buildSuccess bool |
| |
| // Has a test been run and succeeded? |
| testSuccess bool |
| |
| // The name of the file if initialized from a file. |
| // It will be blank if initialized from a string,. |
| filename string |
| |
| // The list of files changed by this processor. |
| changedFiles orderedStrings |
| } |
| |
| // newProc creates a new, empty Proc. |
| func newProc() *Proc { |
| return NewProcFromString("") |
| } |
| |
| // NewProcFromString initializes a Proc with the given string. |
| func NewProcFromString(source string) *Proc { |
| return &Proc{ |
| Store: make(map[string]interface{}), |
| original: source, |
| replacement: source, |
| fuchsiaDir: "~/fuchsia", |
| } |
| } |
| |
| // NewProcFromFile initializes a Proc from a file. |
| func NewProcFromFile(filename string) *Proc { |
| return newProc().readFromFile(filename) |
| } |
| |
| // readFromFile is an operator that initializes the original and replacement |
| // fields of the Proc by reading the contents of the given file. The filename |
| // can be absolute or preceded by ~ or //. |
| func (p *Proc) readFromFile(filename string) *Proc { |
| if p == nil { |
| log.Println(" readFromFile() error: nil receiver") |
| return nil |
| } |
| filename, err := resolveGnPath(p.fuchsiaDir, filename) |
| if err != nil { |
| log.Printf(" readFromFile() path err: %v", err) |
| return nil |
| } |
| handle, err := os.Open(filename) |
| if err != nil { |
| log.Printf(" readFromFile() open file %q err: %v", filename, err) |
| return nil |
| } |
| defer handle.Close() |
| |
| p = p.setFilename(filename) |
| if p == nil { |
| log.Println(" readFromFile() could not set filename") |
| return nil |
| } |
| |
| return p.read(handle) |
| } |
| |
| // Read loads the original and replacement contents using the given reader. |
| func (p *Proc) read(reader io.Reader) *Proc { |
| if p == nil { |
| log.Println(" read() error: nil receiver") |
| return nil |
| } |
| if reader == nil { |
| log.Println(" read() error: nil reader") |
| return nil |
| } |
| content, err := ioutil.ReadAll(reader) |
| if err != nil { |
| log.Printf(" read() read err: %v", err) |
| return nil |
| } |
| p.original = string(content) |
| p.replacement = string(content) |
| return p |
| } |
| |
| // setFuchsiaDir is an operator that sets the Fuchsia repo directory, |
| // allowing filenames to be prefixed with "//". |
| func (p *Proc) setFuchsiaDir(path string) *Proc { |
| if p == nil { |
| log.Println(" setFuchsiaDir() error: nil receiver") |
| return nil |
| } |
| p.fuchsiaDir = path |
| return p |
| } |
| |
| // setFilename is an operator that sets the filename that will be used to write |
| // the modified Proc contents. |
| func (p *Proc) setFilename(filename string) *Proc { |
| if p == nil { |
| log.Println(" setFilename() error: nil receiver") |
| return nil |
| } |
| p.filename = filename |
| return p |
| } |
| |
| // OriginalContents returns the original source contents.. |
| func (p *Proc) OriginalContents() string { |
| if p == nil { |
| log.Println(" OriginalContents() error: nil receiver") |
| return "" |
| } |
| return p.original |
| } |
| |
| // ChangedFilesList returns the list of changed files in this processor. |
| func (p *Proc) ChangedFilesList() (orderedStrings, error) { |
| if p == nil { |
| return nil, errors.New(" ChangedFilesList() error: nil receiver") |
| } |
| return p.changedFiles, nil |
| } |
| |
| // WriteFile is an operator that writes the current p.replacement to the file |
| // named in p.filename. |
| func (p *Proc) WriteFile() *Proc { |
| if p == nil { |
| log.Println(" WriteFile() error: nil receiver") |
| return nil |
| } |
| if p.replacement == p.original { |
| log.Println("WriteFile(): no changes to write") |
| return p |
| } |
| if p.filename == "" { |
| log.Println(" WriteFile() error: filename not set [use SetFilename()?]") |
| return nil |
| } |
| f, err := os.OpenFile(p.filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) |
| if err != nil { |
| log.Printf(" WriteFile(): error writing file: %v", err) |
| return nil |
| } |
| defer f.Close() |
| |
| return p.write(f) |
| } |
| |
| // Writes the replacement string to the given io.writer. |
| func (p *Proc) write(writer io.Writer) *Proc { |
| if p == nil { |
| log.Println(" write() error: nil receiver") |
| return nil |
| } |
| if writer == nil { |
| log.Println(" write() error: nil writer") |
| return nil |
| } |
| fmt.Fprint(writer, p.replacement) |
| p.changedFiles.Add(p.filename) |
| return p |
| } |
| |
| // Prints items of varying types. |
| func printItem(k string, v interface{}, length int) string { |
| var line string |
| switch t := v.(type) { |
| case string: |
| line = flattenString(t, length) |
| log.Printf(" store: %s -> %v\n", k, line) |
| case []string: |
| for i, s := range t { |
| line = flattenString(s, length) |
| log.Printf(" store: %s[%d] -> %v\n", k, i, line) |
| } |
| default: |
| line = fmt.Sprintf(" [unknown type %T]", t) |
| log.Printf(" store: %s -> %v\n", k, line) |
| } |
| return line |
| } |
| |
| // print is an operator that logs the state of the processor. The lines are |
| // clipped to the given line length. If the line length is < 0, do not shorten. |
| func (p *Proc) print(lineLength int) *Proc { |
| if p == nil { |
| log.Println(" print() error: nil receiver") |
| return nil |
| } |
| log.Println(" Processing Unit state:") |
| |
| log.Printf(" original: %s", flattenString(p.original, lineLength)) |
| log.Printf(" replacement: %s", flattenString(p.replacement, lineLength)) |
| log.Printf(" fuchsiaDir: %s", flattenString(p.fuchsiaDir, lineLength)) |
| log.Printf(" filename: %s", flattenString(p.filename, lineLength)) |
| log.Printf(" buildSuccess: %v", p.buildSuccess) |
| log.Printf(" testSuccess: %v", p.testSuccess) |
| |
| if p.parent == nil { |
| log.Println(" parent: [nil]") |
| } else { |
| log.Printf(" parent: filename = %s", flattenString(p.parent.filename, lineLength)) |
| log.Printf(" parent: replacement = %s", flattenString(p.parent.replacement, lineLength)) |
| } |
| |
| if len(p.changedFiles) == 0 { |
| log.Println(" changedFiles: [empty]") |
| } else { |
| for i, f := range p.changedFiles { |
| log.Printf(" changedFiles, #%2d: %s", i, flattenString(f, lineLength)) |
| } |
| } |
| |
| if len(p.Store) == 0 { |
| log.Println(" store: [empty]") |
| } else { |
| for k, v := range p.Store { |
| printItem(k, v, lineLength) |
| } |
| } |
| return p |
| } |
| |
| // copy returns a ~copy of p. The new changedFiles is nil. |
| func (p *Proc) copy() *Proc { |
| if p == nil { |
| log.Println(" copy() error: nil receiver") |
| return nil |
| } |
| q := *p |
| q.changedFiles = nil |
| q.Store = make(map[string]interface{}, len(p.Store)) |
| return q.copyStore(p) |
| } |