From d4e5423ca13565fa1dd43fa0083413446ce46058 Mon Sep 17 00:00:00 2001 From: ascandone Date: Fri, 18 Jul 2025 14:26:47 +0200 Subject: [PATCH 1/6] feat: change cli command args --- internal/cmd/run.go | 153 ++++++++++---------------------------------- 1 file changed, 34 insertions(+), 119 deletions(-) diff --git a/internal/cmd/run.go b/internal/cmd/run.go index db8e5c47..748eb394 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -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"` } -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 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, }, 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,7 +110,7 @@ func showPretty(result *interpreter.ExecutionResult) error { } func getRunCmd() *cobra.Command { - opts := runArgs{} + opts := RunArgs{} cmd := cobra.Command{ Use: "run", @@ -198,20 +125,8 @@ func getRunCmd() *cobra.Command { }, } - // 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 } From 127238be406acd03fcf3678a6091274b835c5a1e Mon Sep 17 00:00:00 2001 From: ascandone Date: Fri, 18 Jul 2025 15:14:12 +0200 Subject: [PATCH 2/6] feat: better docs and improve err handling --- internal/cmd/run.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/internal/cmd/run.go b/internal/cmd/run.go index 748eb394..6097ef18 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -115,13 +115,23 @@ func getRunCmd() *cobra.Command { 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.", - RunE: func(cmd *cobra.Command, args []string) error { - var path string - if len(args) > 0 { - path = args[0] + 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), + Run: func(cmd *cobra.Command, args []string) { + path := args[0] + + err := run(path, opts) + if err != nil { + fmt.Fprint(os.Stderr, err) + os.Exit(1) } - return run(path, opts) }, } From d77758e1b69d09739cddbf82cc76e6652e7d9d12 Mon Sep 17 00:00:00 2001 From: ascandone Date: Fri, 18 Jul 2025 15:17:51 +0200 Subject: [PATCH 3/6] fix: fix error handling --- internal/cmd/lsp.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/cmd/lsp.go b/internal/cmd/lsp.go index 6c5c3389..7bbab49f 100644 --- a/internal/cmd/lsp.go +++ b/internal/cmd/lsp.go @@ -1,6 +1,9 @@ package cmd import ( + "fmt" + "os" + "github.com/formancehq/numscript/internal/lsp" "github.com/spf13/cobra" @@ -11,7 +14,11 @@ var lspCmd = &cobra.Command{ Short: "Run the lsp server", Long: "Run the lsp server. This command is usually meant to be used for editors integration.", Hidden: true, - RunE: func(cmd *cobra.Command, args []string) error { - return lsp.RunServer() + Run: func(cmd *cobra.Command, args []string) { + err := lsp.RunServer() + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } }, } From 2cc29f463db024a973dea7152f73227b6f919cf1 Mon Sep 17 00:00:00 2001 From: ascandone Date: Wed, 23 Jul 2025 11:45:12 +0200 Subject: [PATCH 4/6] feat: added schema for inputs file --- inputs.schema.json | 67 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 inputs.schema.json diff --git a/inputs.schema.json b/inputs.schema.json new file mode 100644 index 00000000..ee73b798 --- /dev/null +++ b/inputs.schema.json @@ -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" } + } + } +} From 8d5fc122e36b5b71aa96c25f04af1c0a9011b6cc Mon Sep 17 00:00:00 2001 From: Alessandro Scandone Date: Wed, 23 Jul 2025 11:59:10 +0200 Subject: [PATCH 5/6] feat: improve error output --- internal/cmd/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/run.go b/internal/cmd/run.go index 6097ef18..2adcfc6c 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -54,7 +54,7 @@ func run(scriptPath string, opts RunArgs) error { var inputs InputsFile err = json.Unmarshal(inputsContent, &inputs) if err != nil { - return err + return fmt.Errorf("failed to parse inputs file '%s' as JSON: %w", inputsPath, err) } featureFlags := map[string]struct{}{} From 32d891443e10477c7b3360f67ecf705854467b57 Mon Sep 17 00:00:00 2001 From: ascandone Date: Thu, 24 Jul 2025 14:40:02 +0200 Subject: [PATCH 6/6] feat: more reliable exit from cli (no explicit exit() call) --- internal/cmd/lsp.go | 12 ++++++------ internal/cmd/run.go | 9 ++++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/internal/cmd/lsp.go b/internal/cmd/lsp.go index 7bbab49f..26c02a8e 100644 --- a/internal/cmd/lsp.go +++ b/internal/cmd/lsp.go @@ -1,9 +1,6 @@ package cmd import ( - "fmt" - "os" - "github.com/formancehq/numscript/internal/lsp" "github.com/spf13/cobra" @@ -14,11 +11,14 @@ var lspCmd = &cobra.Command{ Short: "Run the lsp server", Long: "Run the lsp server. This command is usually meant to be used for editors integration.", Hidden: true, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { err := lsp.RunServer() if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(1) + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return err } + + return nil }, } diff --git a/internal/cmd/run.go b/internal/cmd/run.go index 2adcfc6c..2abb5b25 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -124,14 +124,17 @@ 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), - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { path := args[0] err := run(path, opts) if err != nil { - fmt.Fprint(os.Stderr, err) - os.Exit(1) + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return err } + + return nil }, }