Skip to content

Commit 559426b

Browse files
authored
feat: cli, change run args (#84)
* feat: change cli command args * feat: better docs and improve err handling * fix: fix error handling * feat: added schema for inputs file * feat: improve error output * feat: more reliable exit from cli (no explicit exit() call)
1 parent dab7a80 commit 559426b

File tree

3 files changed

+127
-125
lines changed

3 files changed

+127
-125
lines changed

inputs.schema.json

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Specs",
4+
"type": "object",
5+
"additionalProperties": false,
6+
"properties": {
7+
"$schema": { "type": "string" },
8+
"balances": {
9+
"$ref": "#/definitions/Balances"
10+
},
11+
"variables": {
12+
"$ref": "#/definitions/VariablesMap"
13+
},
14+
"metadata": {
15+
"$ref": "#/definitions/AccountsMetadata"
16+
},
17+
"featureFlags": {
18+
"type": "array",
19+
"items": { "type": "string" }
20+
}
21+
},
22+
"definitions": {
23+
"Balances": {
24+
"type": "object",
25+
"description": "Map of account names to asset balances",
26+
"additionalProperties": false,
27+
"patternProperties": {
28+
"^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)*)$": {
29+
"type": "object",
30+
"additionalProperties": false,
31+
"patternProperties": {
32+
"^([A-Z]+(/[0-9]+)?)$": {
33+
"type": "number"
34+
}
35+
}
36+
}
37+
}
38+
},
39+
40+
"VariablesMap": {
41+
"type": "object",
42+
"description": "Map of variable name to variable stringified value",
43+
"additionalProperties": false,
44+
"patternProperties": {
45+
"^[a-z_]+$": { "type": "string" }
46+
}
47+
},
48+
49+
"AccountsMetadata": {
50+
"type": "object",
51+
"description": "Map of an account metadata to the account's metadata",
52+
"additionalProperties": false,
53+
"patternProperties": {
54+
"^([a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)*)$": {
55+
"type": "object",
56+
"additionalProperties": { "type": "string" }
57+
}
58+
}
59+
},
60+
61+
"TxMetadata": {
62+
"type": "object",
63+
"description": "Map from a metadata's key to the transaction's metadata stringied value",
64+
"additionalProperties": { "type": "string" }
65+
}
66+
}
67+
}

internal/cmd/lsp.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ var lspCmd = &cobra.Command{
1212
Long: "Run the lsp server. This command is usually meant to be used for editors integration.",
1313
Hidden: true,
1414
RunE: func(cmd *cobra.Command, args []string) error {
15-
return lsp.RunServer()
15+
err := lsp.RunServer()
16+
if err != nil {
17+
cmd.SilenceErrors = true
18+
cmd.SilenceUsage = true
19+
return err
20+
}
21+
22+
return nil
1623
},
1724
}

internal/cmd/run.go

Lines changed: 52 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"io"
87
"os"
9-
"strings"
108

11-
"github.com/formancehq/numscript/internal/flags"
129
"github.com/formancehq/numscript/internal/interpreter"
1310
"github.com/formancehq/numscript/internal/parser"
1411

@@ -20,132 +17,62 @@ const (
2017
OutputFormatJson = "json"
2118
)
2219

23-
type runArgs struct {
24-
VariablesOpt string
25-
BalancesOpt string
26-
MetaOpt string
27-
RawOpt string
28-
StdinFlag bool
29-
OutFormatOpt string
30-
Flags []string
20+
type InputsFile struct {
21+
FeatureFlags []string `json:"featureFlags"`
22+
Variables map[string]string `json:"variables"`
23+
Meta interpreter.AccountsMetadata `json:"metadata"`
24+
Balances interpreter.Balances `json:"balances"`
3125
}
3226

33-
type inputOpts struct {
34-
Script string `json:"script"`
35-
Variables map[string]string `json:"variables"`
36-
Meta interpreter.AccountsMetadata `json:"metadata"`
37-
Balances interpreter.Balances `json:"balances"`
27+
type RunArgs struct {
28+
InputsPath string
29+
OutFormatOpt string
3830
}
3931

40-
func (o *inputOpts) fromRaw(opts runArgs) error {
41-
if opts.RawOpt == "" {
42-
return nil
43-
}
44-
45-
err := json.Unmarshal([]byte(opts.RawOpt), o)
32+
func run(scriptPath string, opts RunArgs) error {
33+
numscriptContent, err := os.ReadFile(scriptPath)
4634
if err != nil {
47-
return fmt.Errorf("invalid raw input JSON: %w", err)
35+
return err
4836
}
49-
return nil
50-
}
5137

52-
func (o *inputOpts) fromStdin(opts runArgs) error {
53-
if !opts.StdinFlag {
54-
return nil
38+
parseResult := parser.Parse(string(numscriptContent))
39+
if len(parseResult.Errors) != 0 {
40+
fmt.Fprint(os.Stderr, parser.ParseErrorsToString(parseResult.Errors, string(numscriptContent)))
41+
return fmt.Errorf("parsing failed")
5542
}
5643

57-
bytes, err := io.ReadAll(os.Stdin)
58-
if err != nil {
59-
return fmt.Errorf("error reading from stdin: %w", err)
44+
inputsPath := opts.InputsPath
45+
if inputsPath == "" {
46+
inputsPath = scriptPath + ".inputs.json"
6047
}
6148

62-
err = json.Unmarshal(bytes, o)
49+
inputsContent, err := os.ReadFile(inputsPath)
6350
if err != nil {
64-
return fmt.Errorf("invalid stdin JSON: %w", err)
65-
}
66-
return nil
67-
}
68-
69-
func (o *inputOpts) fromOptions(path string, opts runArgs) error {
70-
if path != "" {
71-
numscriptContent, err := os.ReadFile(path)
72-
if err != nil {
73-
return fmt.Errorf("error reading script file: %w", err)
74-
}
75-
o.Script = string(numscriptContent)
76-
}
77-
78-
if opts.BalancesOpt != "" {
79-
content, err := os.ReadFile(opts.BalancesOpt)
80-
if err != nil {
81-
return fmt.Errorf("error reading balances file: %w", err)
82-
}
83-
if err := json.Unmarshal(content, &o.Balances); err != nil {
84-
return fmt.Errorf("invalid balances JSON: %w", err)
85-
}
86-
}
87-
88-
if opts.MetaOpt != "" {
89-
content, err := os.ReadFile(opts.MetaOpt)
90-
if err != nil {
91-
return fmt.Errorf("error reading metadata file: %w", err)
92-
}
93-
if err := json.Unmarshal(content, &o.Meta); err != nil {
94-
return fmt.Errorf("invalid metadata JSON: %w", err)
95-
}
96-
}
97-
98-
if opts.VariablesOpt != "" {
99-
content, err := os.ReadFile(opts.VariablesOpt)
100-
if err != nil {
101-
return fmt.Errorf("error reading variables file: %w", err)
102-
}
103-
if err := json.Unmarshal(content, &o.Variables); err != nil {
104-
return fmt.Errorf("invalid variables JSON: %w", err)
105-
}
106-
}
107-
return nil
108-
}
109-
110-
func run(path string, opts runArgs) error {
111-
opt := inputOpts{
112-
Variables: make(map[string]string),
113-
Meta: make(interpreter.AccountsMetadata),
114-
Balances: make(interpreter.Balances),
115-
}
116-
117-
if err := opt.fromRaw(opts); err != nil {
118-
return err
119-
}
120-
if err := opt.fromOptions(path, opts); err != nil {
121-
return err
122-
}
123-
if err := opt.fromStdin(opts); err != nil {
12451
return err
12552
}
12653

127-
parseResult := parser.Parse(opt.Script)
128-
if len(parseResult.Errors) != 0 {
129-
fmt.Fprint(os.Stderr, parser.ParseErrorsToString(parseResult.Errors, opt.Script))
130-
return fmt.Errorf("parsing failed")
54+
var inputs InputsFile
55+
err = json.Unmarshal(inputsContent, &inputs)
56+
if err != nil {
57+
return fmt.Errorf("failed to parse inputs file '%s' as JSON: %w", inputsPath, err)
13158
}
13259

13360
featureFlags := map[string]struct{}{}
134-
for _, flag := range opts.Flags {
61+
for _, flag := range inputs.FeatureFlags {
13562
featureFlags[flag] = struct{}{}
13663
}
13764

138-
result, err := interpreter.RunProgram(context.Background(), parseResult.Value, opt.Variables, interpreter.StaticStore{
139-
Balances: opt.Balances,
140-
Meta: opt.Meta,
65+
result, iErr := interpreter.RunProgram(context.Background(), parseResult.Value, inputs.Variables, interpreter.StaticStore{
66+
Balances: inputs.Balances,
67+
Meta: inputs.Meta,
14168
}, featureFlags)
14269

143-
if err != nil {
144-
rng := err.GetRange()
145-
fmt.Fprint(os.Stderr, err.Error())
70+
if iErr != nil {
71+
rng := iErr.GetRange()
72+
fmt.Fprint(os.Stderr, iErr.Error())
14673
if rng.Start != rng.End {
14774
fmt.Fprint(os.Stderr, "\n")
148-
fmt.Fprint(os.Stderr, err.GetRange().ShowOnSource(parseResult.Source))
75+
fmt.Fprint(os.Stderr, iErr.GetRange().ShowOnSource(parseResult.Source))
14976
}
15077
return fmt.Errorf("execution failed")
15178
}
@@ -183,35 +110,36 @@ func showPretty(result *interpreter.ExecutionResult) error {
183110
}
184111

185112
func getRunCmd() *cobra.Command {
186-
opts := runArgs{}
113+
opts := RunArgs{}
187114

188115
cmd := cobra.Command{
189116
Use: "run",
190117
Short: "Evaluate a numscript file",
191-
Long: "Evaluate a numscript file, using the balances, the current metadata and the variables values as input.",
118+
Long: `Evaluate a numscript file, taking as inputs a json file containing balances, variables and metadata.
119+
120+
The inputs file has to have the same name as the numscript file plus a ".inputs.json" suffix, for example:
121+
run folder/my-script.num
122+
will expect a 'folder/my-script.num.inputs.json' file where to read inputs from.
123+
124+
You can use explicitly specify where the inputs file should be using the optional --inputs argument.
125+
`,
126+
Args: cobra.ExactArgs(1),
192127
RunE: func(cmd *cobra.Command, args []string) error {
193-
var path string
194-
if len(args) > 0 {
195-
path = args[0]
128+
path := args[0]
129+
130+
err := run(path, opts)
131+
if err != nil {
132+
cmd.SilenceErrors = true
133+
cmd.SilenceUsage = true
134+
return err
196135
}
197-
return run(path, opts)
136+
137+
return nil
198138
},
199139
}
200140

201-
// Input args
202-
cmd.Flags().StringVarP(&opts.VariablesOpt, "variables", "v", "", "Path of a json file containing the variables")
203-
cmd.Flags().StringVarP(&opts.BalancesOpt, "balances", "b", "", "Path of a json file containing the balances")
204-
cmd.Flags().StringVarP(&opts.MetaOpt, "meta", "m", "", "Path of a json file containing the accounts metadata")
205-
cmd.Flags().StringVarP(&opts.RawOpt, "raw", "r", "", "Raw json input containing script, variables, balances, metadata")
206-
cmd.Flags().BoolVar(&opts.StdinFlag, "stdin", false, "Take input from stdin (same format as the --raw option)")
207-
208-
// Feature flag
209-
cmd.Flags().StringSliceVar(&opts.Flags, "flags", nil, fmt.Sprintf("the feature flags to pass to the interpreter. Currently available flags: %s",
210-
strings.Join(flags.AllFlags, ", "),
211-
))
212-
213-
// Output options
214-
cmd.Flags().StringVar(&opts.OutFormatOpt, "output-format", OutputFormatPretty, "Set the output format. Available options: pretty, json.")
141+
cmd.Flags().StringVar(&opts.InputsPath, "inputs", "", "Path of a json file containing the inputs")
142+
cmd.Flags().StringVarP(&opts.OutFormatOpt, "output-format", "o", OutputFormatPretty, "Set the output format. Available options: pretty, json.")
215143

216144
return &cmd
217145
}

0 commit comments

Comments
 (0)