-
Notifications
You must be signed in to change notification settings - Fork 5
feat: cli, change run args #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d4e5423
127238b
d77758e
2cc29f4
8d5fc12
32d8914
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| { | ||
| "$schema": "http://json-schema.org/draft-07/schema#", | ||
| "title": "Specs", | ||
| "type": "object", | ||
| "additionalProperties": false, | ||
| "properties": { | ||
| "$schema": { "type": "string" }, | ||
| "balances": { | ||
| "$ref": "#/definitions/Balances" | ||
| }, | ||
| "variables": { | ||
| "$ref": "#/definitions/VariablesMap" | ||
| }, | ||
| "metadata": { | ||
| "$ref": "#/definitions/AccountsMetadata" | ||
| }, | ||
| "featureFlags": { | ||
| "type": "array", | ||
| "items": { "type": "string" } | ||
| } | ||
| }, | ||
| "definitions": { | ||
| "Balances": { | ||
| "type": "object", | ||
| "description": "Map of account names to asset balances", | ||
| "additionalProperties": false, | ||
| "patternProperties": { | ||
| "^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)*)$": { | ||
| "type": "object", | ||
| "additionalProperties": false, | ||
| "patternProperties": { | ||
| "^([A-Z]+(/[0-9]+)?)$": { | ||
| "type": "number" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }, | ||
|
|
||
| "VariablesMap": { | ||
| "type": "object", | ||
| "description": "Map of variable name to variable stringified value", | ||
| "additionalProperties": false, | ||
| "patternProperties": { | ||
| "^[a-z_]+$": { "type": "string" } | ||
| } | ||
| }, | ||
|
|
||
| "AccountsMetadata": { | ||
| "type": "object", | ||
| "description": "Map of an account metadata to the account's metadata", | ||
| "additionalProperties": false, | ||
| "patternProperties": { | ||
| "^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)*)$": { | ||
| "type": "object", | ||
| "additionalProperties": { "type": "string" } | ||
| } | ||
| } | ||
| }, | ||
|
|
||
| "TxMetadata": { | ||
| "type": "object", | ||
| "description": "Map from a metadata's key to the transaction's metadata stringied value", | ||
| "additionalProperties": { "type": "string" } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,11 +4,8 @@ import ( | |
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "strings" | ||
|
|
||
| "github.com/formancehq/numscript/internal/flags" | ||
| "github.com/formancehq/numscript/internal/interpreter" | ||
| "github.com/formancehq/numscript/internal/parser" | ||
|
|
||
|
|
@@ -20,132 +17,62 @@ const ( | |
| OutputFormatJson = "json" | ||
| ) | ||
|
|
||
| type runArgs struct { | ||
| VariablesOpt string | ||
| BalancesOpt string | ||
| MetaOpt string | ||
| RawOpt string | ||
| StdinFlag bool | ||
| OutFormatOpt string | ||
| Flags []string | ||
| type InputsFile struct { | ||
| FeatureFlags []string `json:"featureFlags"` | ||
| Variables map[string]string `json:"variables"` | ||
| Meta interpreter.AccountsMetadata `json:"metadata"` | ||
| Balances interpreter.Balances `json:"balances"` | ||
ascandone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| type inputOpts struct { | ||
| Script string `json:"script"` | ||
| Variables map[string]string `json:"variables"` | ||
| Meta interpreter.AccountsMetadata `json:"metadata"` | ||
| Balances interpreter.Balances `json:"balances"` | ||
| type RunArgs struct { | ||
| InputsPath string | ||
| OutFormatOpt string | ||
| } | ||
|
|
||
| func (o *inputOpts) fromRaw(opts runArgs) error { | ||
| if opts.RawOpt == "" { | ||
| return nil | ||
| } | ||
|
|
||
| err := json.Unmarshal([]byte(opts.RawOpt), o) | ||
| func run(scriptPath string, opts RunArgs) error { | ||
| numscriptContent, err := os.ReadFile(scriptPath) | ||
| if err != nil { | ||
| return fmt.Errorf("invalid raw input JSON: %w", err) | ||
| return err | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (o *inputOpts) fromStdin(opts runArgs) error { | ||
| if !opts.StdinFlag { | ||
| return nil | ||
| parseResult := parser.Parse(string(numscriptContent)) | ||
| if len(parseResult.Errors) != 0 { | ||
| fmt.Fprint(os.Stderr, parser.ParseErrorsToString(parseResult.Errors, string(numscriptContent))) | ||
| return fmt.Errorf("parsing failed") | ||
| } | ||
|
|
||
| bytes, err := io.ReadAll(os.Stdin) | ||
| if err != nil { | ||
| return fmt.Errorf("error reading from stdin: %w", err) | ||
| inputsPath := opts.InputsPath | ||
| if inputsPath == "" { | ||
| inputsPath = scriptPath + ".inputs.json" | ||
| } | ||
|
|
||
| err = json.Unmarshal(bytes, o) | ||
| inputsContent, err := os.ReadFile(inputsPath) | ||
| if err != nil { | ||
| return fmt.Errorf("invalid stdin JSON: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (o *inputOpts) fromOptions(path string, opts runArgs) error { | ||
| if path != "" { | ||
| numscriptContent, err := os.ReadFile(path) | ||
| if err != nil { | ||
| return fmt.Errorf("error reading script file: %w", err) | ||
| } | ||
| o.Script = string(numscriptContent) | ||
| } | ||
|
|
||
| if opts.BalancesOpt != "" { | ||
| content, err := os.ReadFile(opts.BalancesOpt) | ||
| if err != nil { | ||
| return fmt.Errorf("error reading balances file: %w", err) | ||
| } | ||
| if err := json.Unmarshal(content, &o.Balances); err != nil { | ||
| return fmt.Errorf("invalid balances JSON: %w", err) | ||
| } | ||
| } | ||
|
|
||
| if opts.MetaOpt != "" { | ||
| content, err := os.ReadFile(opts.MetaOpt) | ||
| if err != nil { | ||
| return fmt.Errorf("error reading metadata file: %w", err) | ||
| } | ||
| if err := json.Unmarshal(content, &o.Meta); err != nil { | ||
| return fmt.Errorf("invalid metadata JSON: %w", err) | ||
| } | ||
| } | ||
|
|
||
| if opts.VariablesOpt != "" { | ||
| content, err := os.ReadFile(opts.VariablesOpt) | ||
| if err != nil { | ||
| return fmt.Errorf("error reading variables file: %w", err) | ||
| } | ||
| if err := json.Unmarshal(content, &o.Variables); err != nil { | ||
| return fmt.Errorf("invalid variables JSON: %w", err) | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func run(path string, opts runArgs) error { | ||
| opt := inputOpts{ | ||
| Variables: make(map[string]string), | ||
| Meta: make(interpreter.AccountsMetadata), | ||
| Balances: make(interpreter.Balances), | ||
| } | ||
|
|
||
| if err := opt.fromRaw(opts); err != nil { | ||
| return err | ||
| } | ||
| if err := opt.fromOptions(path, opts); err != nil { | ||
| return err | ||
| } | ||
| if err := opt.fromStdin(opts); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| parseResult := parser.Parse(opt.Script) | ||
| if len(parseResult.Errors) != 0 { | ||
| fmt.Fprint(os.Stderr, parser.ParseErrorsToString(parseResult.Errors, opt.Script)) | ||
| return fmt.Errorf("parsing failed") | ||
| var inputs InputsFile | ||
| err = json.Unmarshal(inputsContent, &inputs) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to parse inputs file '%s' as JSON: %w", inputsPath, err) | ||
| } | ||
|
|
||
| featureFlags := map[string]struct{}{} | ||
| for _, flag := range opts.Flags { | ||
| for _, flag := range inputs.FeatureFlags { | ||
| featureFlags[flag] = struct{}{} | ||
| } | ||
|
|
||
| result, err := interpreter.RunProgram(context.Background(), parseResult.Value, opt.Variables, interpreter.StaticStore{ | ||
| Balances: opt.Balances, | ||
| Meta: opt.Meta, | ||
| result, iErr := interpreter.RunProgram(context.Background(), parseResult.Value, inputs.Variables, interpreter.StaticStore{ | ||
| Balances: inputs.Balances, | ||
| Meta: inputs.Meta, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: JSON Parsing Initializes Maps IncorrectlyThe Locations (1) |
||
| }, featureFlags) | ||
|
|
||
| if err != nil { | ||
| rng := err.GetRange() | ||
| fmt.Fprint(os.Stderr, err.Error()) | ||
| if iErr != nil { | ||
| rng := iErr.GetRange() | ||
| fmt.Fprint(os.Stderr, iErr.Error()) | ||
| if rng.Start != rng.End { | ||
| fmt.Fprint(os.Stderr, "\n") | ||
| fmt.Fprint(os.Stderr, err.GetRange().ShowOnSource(parseResult.Source)) | ||
| fmt.Fprint(os.Stderr, iErr.GetRange().ShowOnSource(parseResult.Source)) | ||
| } | ||
| return fmt.Errorf("execution failed") | ||
| } | ||
|
|
@@ -183,35 +110,36 @@ func showPretty(result *interpreter.ExecutionResult) error { | |
| } | ||
|
|
||
| func getRunCmd() *cobra.Command { | ||
| opts := runArgs{} | ||
| opts := RunArgs{} | ||
|
|
||
| cmd := cobra.Command{ | ||
| Use: "run", | ||
| Short: "Evaluate a numscript file", | ||
| Long: "Evaluate a numscript file, using the balances, the current metadata and the variables values as input.", | ||
| Long: `Evaluate a numscript file, taking as inputs a json file containing balances, variables and metadata. | ||
|
|
||
| The inputs file has to have the same name as the numscript file plus a ".inputs.json" suffix, for example: | ||
| run folder/my-script.num | ||
| will expect a 'folder/my-script.num.inputs.json' file where to read inputs from. | ||
|
|
||
| You can use explicitly specify where the inputs file should be using the optional --inputs argument. | ||
| `, | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| var path string | ||
| if len(args) > 0 { | ||
| path = args[0] | ||
| path := args[0] | ||
|
|
||
| err := run(path, opts) | ||
| if err != nil { | ||
| cmd.SilenceErrors = true | ||
| cmd.SilenceUsage = true | ||
| return err | ||
| } | ||
| return run(path, opts) | ||
|
|
||
| return nil | ||
| }, | ||
| } | ||
|
|
||
| // Input args | ||
| cmd.Flags().StringVarP(&opts.VariablesOpt, "variables", "v", "", "Path of a json file containing the variables") | ||
| cmd.Flags().StringVarP(&opts.BalancesOpt, "balances", "b", "", "Path of a json file containing the balances") | ||
| cmd.Flags().StringVarP(&opts.MetaOpt, "meta", "m", "", "Path of a json file containing the accounts metadata") | ||
| cmd.Flags().StringVarP(&opts.RawOpt, "raw", "r", "", "Raw json input containing script, variables, balances, metadata") | ||
| cmd.Flags().BoolVar(&opts.StdinFlag, "stdin", false, "Take input from stdin (same format as the --raw option)") | ||
|
|
||
| // Feature flag | ||
| cmd.Flags().StringSliceVar(&opts.Flags, "flags", nil, fmt.Sprintf("the feature flags to pass to the interpreter. Currently available flags: %s", | ||
| strings.Join(flags.AllFlags, ", "), | ||
| )) | ||
|
|
||
| // Output options | ||
| cmd.Flags().StringVar(&opts.OutFormatOpt, "output-format", OutputFormatPretty, "Set the output format. Available options: pretty, json.") | ||
| cmd.Flags().StringVar(&opts.InputsPath, "inputs", "", "Path of a json file containing the inputs") | ||
| cmd.Flags().StringVarP(&opts.OutFormatOpt, "output-format", "o", OutputFormatPretty, "Set the output format. Available options: pretty, json.") | ||
|
|
||
| return &cmd | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussion on the merit of Run vs RunE and Cobra error handling: https://numary.slack.com/archives/C096FH98AKC/p1753358540338769