github.com/arr-ai/arrai@v0.319.0/pkg/arrai/out.go (about)

     1  package arrai
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path"
     9  	"strings"
    10  
    11  	"github.com/go-errors/errors"
    12  	"github.com/spf13/afero"
    13  
    14  	"github.com/arr-ai/arrai/pkg/ctxfs"
    15  	"github.com/arr-ai/arrai/pkg/fu"
    16  	"github.com/arr-ai/arrai/rel"
    17  )
    18  
    19  const (
    20  	dirField        = "dir"
    21  	fileField       = "file"
    22  	ifExistsConfig  = "ifExists"
    23  	ifExistsRemove  = "remove"
    24  	ifExistsReplace = "replace"
    25  	ifExistsMerge   = "merge"
    26  	ifExistsIgnore  = "ignore"
    27  	ifExistsFail    = "fail"
    28  )
    29  
    30  var errFileAndDirMustNotExist = errors.Errorf("%s and %s must not exist", dirField, fileField)
    31  var errFileOrDirMustExist = errors.Errorf("exactly one of %s or %s must exist", dirField, fileField)
    32  
    33  // OutputValue handles output writing for evaluated values.
    34  func OutputValue(ctx context.Context, value rel.Value, w io.Writer, out string) error {
    35  	if out != "" {
    36  		return outputValue(ctx, value, out)
    37  	}
    38  
    39  	var s string
    40  	switch v := value.(type) {
    41  	case rel.String:
    42  		s = v.String()
    43  	case rel.Bytes:
    44  		s = v.String()
    45  	case rel.Set:
    46  		if !v.IsTrue() {
    47  			s = ""
    48  		} else {
    49  			s = fu.Repr(v)
    50  		}
    51  	default:
    52  		s = fu.Repr(v)
    53  	}
    54  	fmt.Fprintf(w, "%s", s)
    55  	if s != "" && !strings.HasSuffix(s, "\n") {
    56  		if _, err := w.Write([]byte{'\n'}); err != nil {
    57  			return err
    58  		}
    59  	}
    60  
    61  	return nil
    62  }
    63  
    64  func outputValue(ctx context.Context, value rel.Value, out string) error {
    65  	parts := strings.SplitN(out, ":", 2)
    66  	if len(parts) == 1 {
    67  		parts = []string{"", parts[0]}
    68  	}
    69  	mode := parts[0]
    70  	arg := parts[1]
    71  
    72  	fs := ctxfs.RuntimeFsFrom(ctx)
    73  	switch mode {
    74  	case "file", "f", "":
    75  		return outputFile(value, arg, fs, false)
    76  	case "dir", "d":
    77  		if t, is := rel.AsDict(value); is {
    78  			if err := outputTupleDir(t, arg, fs, true); err != nil {
    79  				return err
    80  			}
    81  			return outputTupleDir(t, arg, fs, false)
    82  		}
    83  		return fmt.Errorf("result not a dict: %v", value)
    84  	}
    85  	return fmt.Errorf("invalid --out flag: %s", out)
    86  }
    87  
    88  func outputTupleDir(v rel.Value, dir string, fs afero.Fs, dryRun bool) error {
    89  	t, err := getDirField(v)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	if _, err := fs.Stat(dir); os.IsNotExist(err) {
    94  		if err := fs.Mkdir(dir, 0755); err != nil {
    95  			return err
    96  		}
    97  	}
    98  
    99  	// this is to allow empty directory
   100  	if !t.IsTrue() {
   101  		return nil
   102  	}
   103  
   104  	for e := t.(rel.Dict).DictEnumerator(); e.MoveNext(); {
   105  		k, v := e.Current()
   106  		name, is := k.(rel.String)
   107  		if !is {
   108  			return fmt.Errorf("dir output dict key must be a non-empty string")
   109  		}
   110  		subpath := path.Join(dir, name.String())
   111  		switch content := v.(type) {
   112  		case rel.Tuple:
   113  			if err := configureOutput(content, subpath, fs, dryRun); err != nil {
   114  				return err
   115  			}
   116  		case rel.Dict:
   117  			if err := outputTupleDir(content, subpath, fs, dryRun); err != nil {
   118  				return err
   119  			}
   120  		case rel.Bytes, rel.String:
   121  			if err := outputFile(content, subpath, fs, dryRun); err != nil {
   122  				return err
   123  			}
   124  		case rel.Set:
   125  			if content.IsTrue() {
   126  				return fmt.Errorf("dir output entry must be dict, string or byte array")
   127  			}
   128  			if err := outputFile(content, subpath, fs, dryRun); err != nil {
   129  				return err
   130  			}
   131  		}
   132  	}
   133  	return nil
   134  }
   135  
   136  func outputFile(content rel.Value, path string, fs afero.Fs, dryRun bool) error {
   137  	var bytes []byte
   138  	switch content := content.(type) {
   139  	case rel.Bytes:
   140  		bytes = content.Bytes()
   141  	case rel.String:
   142  		bytes = []byte(content.String())
   143  	default:
   144  		if _, is := content.(rel.Set); !(is && !content.IsTrue()) {
   145  			return fmt.Errorf("file output not string or byte array: %v", content)
   146  		}
   147  		bytes = []byte{}
   148  	}
   149  
   150  	if dryRun {
   151  		return nil
   152  	}
   153  
   154  	f, err := fs.Create(path)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	defer f.Close()
   159  
   160  	if _, err = f.Write(bytes); err != nil {
   161  		return err
   162  	}
   163  	return f.Sync()
   164  }
   165  
   166  func configureOutput(t rel.Tuple, dir string, fs afero.Fs, dryRun bool) error {
   167  	configNames := []string{ifExistsConfig}
   168  
   169  	for _, c := range configNames {
   170  		if t.HasName(c) {
   171  			for _, applier := range getConfigurators() {
   172  				if err := applier(t, dir, fs, dryRun); err != nil {
   173  					return err
   174  				}
   175  			}
   176  			return nil
   177  		}
   178  	}
   179  	return applyFilesFields(t, dir, fs, dryRun)
   180  }
   181  
   182  func getConfigurators() []func(rel.Tuple, string, afero.Fs, bool) error {
   183  	// mind the order when adding new configurations
   184  	return []func(rel.Tuple, string, afero.Fs, bool) error{
   185  		applyIfExistsConfig,
   186  	}
   187  }
   188  
   189  func applyIfExistsConfig(t rel.Tuple, dir string, fs afero.Fs, dryRun bool) (err error) {
   190  	conf, has := t.Get(ifExistsConfig)
   191  	if !has {
   192  		return nil
   193  	}
   194  	errInvalidConfig := errors.Errorf(
   195  		"%s: value '%s' is not valid value. It has to be one of %s",
   196  		ifExistsConfig, conf,
   197  		strings.Join([]string{ifExistsMerge, ifExistsRemove, ifExistsReplace, ifExistsIgnore, ifExistsFail}, ", "),
   198  	)
   199  
   200  	if _, isString := conf.(rel.String); !isString {
   201  		return errInvalidConfig
   202  	}
   203  	switch conf.String() {
   204  	case ifExistsIgnore, ifExistsRemove, ifExistsReplace, ifExistsFail:
   205  	case ifExistsMerge:
   206  		if t.HasName(fileField) {
   207  			return errors.Errorf("%s: '%s' config must not have '%s' field", ifExistsConfig, fileField, ifExistsMerge)
   208  		}
   209  	default:
   210  		return errInvalidConfig
   211  	}
   212  
   213  	if _, err := fs.Stat(dir); os.IsNotExist(err) {
   214  		if conf.String() != ifExistsRemove {
   215  			return applyFilesFields(t, dir, fs, dryRun)
   216  		}
   217  	} else if err != nil {
   218  		return err
   219  	}
   220  
   221  	switch conf.String() {
   222  	case ifExistsRemove:
   223  		if err := checkNotDirAndNotFileField(t); err != nil {
   224  			return err
   225  		}
   226  		if dryRun {
   227  			return nil
   228  		}
   229  		return fs.RemoveAll(dir)
   230  	case ifExistsReplace:
   231  		if err := checkDirXorFileField(t); err != nil {
   232  			return err
   233  		}
   234  		if dryRun {
   235  			return nil
   236  		}
   237  		if err := fs.RemoveAll(dir); err != nil {
   238  			return err
   239  		}
   240  		return applyFilesFields(t, dir, fs, dryRun)
   241  	case ifExistsMerge:
   242  		if v, has := t.Get(dirField); has {
   243  			d, err := getDirField(v)
   244  			if err != nil {
   245  				return err
   246  			}
   247  			return outputTupleDir(d, dir, fs, dryRun)
   248  		}
   249  		return errors.Errorf("%s: '%s' field must exist", ifExistsConfig, dirField)
   250  	case ifExistsIgnore:
   251  		return nil
   252  	case ifExistsFail:
   253  		return errors.Errorf("%s: '%s' exists", ifExistsConfig, dir)
   254  	}
   255  	panic("impossible")
   256  }
   257  
   258  func checkNotDirAndNotFileField(t rel.Tuple) error {
   259  	_, hasDirs := t.Get(dirField)
   260  	_, hasFiles := t.Get(fileField)
   261  	if hasDirs || hasFiles {
   262  		return errFileAndDirMustNotExist
   263  	}
   264  	return nil
   265  }
   266  
   267  func checkDirXorFileField(t rel.Tuple) error {
   268  	_, hasDirs := t.Get(dirField)
   269  	_, hasFiles := t.Get(fileField)
   270  	if hasDirs == hasFiles {
   271  		return errFileOrDirMustExist
   272  	}
   273  	return nil
   274  }
   275  
   276  func applyFilesFields(t rel.Tuple, path string, fs afero.Fs, dryRun bool) error {
   277  	if dir, has := t.Get(dirField); has {
   278  		d, err := getDirField(dir)
   279  		if err != nil {
   280  			return err
   281  		}
   282  		return outputTupleDir(d, path, fs, dryRun)
   283  	}
   284  	if file, has := t.Get(fileField); has {
   285  		return outputFile(file, path, fs, dryRun)
   286  	}
   287  	return errFileOrDirMustExist
   288  }
   289  
   290  func getDirField(v rel.Value) (rel.Set, error) {
   291  	switch k := v.(type) {
   292  	case rel.Dict, rel.EmptySet:
   293  		return k.(rel.Set), nil
   294  	}
   295  	return nil, errors.Errorf("%s must be of type Dictionary, not %T", dirField, v)
   296  }