blob: 561065a7fb28996d8dc06589236c72abf9732b35 [file] [log] [blame]
// 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)
}