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" } + } + } +} diff --git a/internal/cmd/lsp.go b/internal/cmd/lsp.go index 6c5c3389..26c02a8e 100644 --- a/internal/cmd/lsp.go +++ b/internal/cmd/lsp.go @@ -12,6 +12,13 @@ var lspCmd = &cobra.Command{ 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() + err := lsp.RunServer() + if err != nil { + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return err + } + + return nil }, } diff --git a/internal/cmd/run.go b/internal/cmd/run.go index db8e5c47..2abb5b25 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 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, }, 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 }