From 3067e3bdf43eb0fddf36f741e8c4c5a150d30779 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Thu, 8 Oct 2026 16:33:59 +0100 Subject: [PATCH 1/2] feat(mapping): add evaluate command, deprecate run Adds fga mapping evaluate as the canonical name for the command previously called fga mapping run. run remains functional as an alias but prints a deprecation warning to stderr when invoked, signalling callers to migrate to evaluate. Uses cobra Aliases to share the implementation without duplication, and cmd.CalledAs() to detect the alias at runtime for the warning. Integration tests updated to use evaluate; test 025 added to verify the deprecation warning fires when using run. --- cmd/mapping/mapping.go | 2 +- cmd/mapping/run.go | 73 +++++++++++++++++++---------------- tests/mapping-test-cases.yaml | 67 +++++++++++++++++++------------- 3 files changed, 80 insertions(+), 62 deletions(-) diff --git a/cmd/mapping/mapping.go b/cmd/mapping/mapping.go index a40b5170..2637ec35 100644 --- a/cmd/mapping/mapping.go +++ b/cmd/mapping/mapping.go @@ -74,5 +74,5 @@ func init() { MappingCmd.AddCommand(validateCmd) MappingCmd.AddCommand(testCmd) MappingCmd.AddCommand(initCmd) - MappingCmd.AddCommand(runCmd) + MappingCmd.AddCommand(evaluateCmd) } diff --git a/cmd/mapping/run.go b/cmd/mapping/run.go index 6c5dd7c1..0bf8efd1 100644 --- a/cmd/mapping/run.go +++ b/cmd/mapping/run.go @@ -466,17 +466,18 @@ func emitBuffered( } var ( - runFormat string - runWritesOnly bool - runInputFile string - runAggregate bool - runContinueOnError bool - runInteractive bool + evaluateFormat string + evaluateWritesOnly bool + evaluateInputFile string + evaluateAggregate bool + evaluateContinueOnError bool + evaluateInteractive bool ) -var runCmd = &cobra.Command{ - Use: "run [mapping-file]", - Short: "Evaluate a mapping against JSON input and emit tuple operations", +var evaluateCmd = &cobra.Command{ + Use: "evaluate [mapping-file]", + Aliases: []string{"run"}, + Short: "Evaluate a mapping against JSON input and emit tuple operations", Long: `Reads JSONL from stdin (or --input) and evaluates it against the mapping file. Input is JSON Lines: one JSON object per line. Outputs tuple operations as JSONL (default) or a JSON batch (--format json). @@ -495,26 +496,30 @@ a conflict is a runtime error. Both --format json and --aggregate buffer the who before emitting; the default streaming JSONL does not. --continue-on-error skips any record that fails to parse or evaluate (warning to stderr) and continues; the command still exits non-zero if any record was skipped.`, - Example: ` echo '{"id":"anne","org":"acme"}' | fga mapping run mapping.yaml - fga mapping run mapping.yaml --input event.json --format json - fga mapping run --writes-only mapping.yaml > out.jsonl && fga tuple write --store-id $STORE_ID --file out.jsonl - fga mapping run mapping.yaml -i`, + Example: ` echo '{"id":"anne","org":"acme"}' | fga mapping evaluate mapping.yaml + fga mapping evaluate mapping.yaml --input event.json --format json + fga mapping evaluate --writes-only mapping.yaml > out.jsonl && fga tuple write --store-id $STORE_ID --file out.jsonl + fga mapping evaluate mapping.yaml -i`, Args: cobra.RangeArgs(0, 1), RunE: func(cmd *cobra.Command, args []string) error { + if cmd.CalledAs() == "run" { + fmt.Fprintln(cmd.ErrOrStderr(), `warning: "fga mapping run" is deprecated, use "fga mapping evaluate" instead`) + } + errStream := cmd.ErrOrStderr() opts := runMappingOptions{ - format: runFormat, - writesOnly: runWritesOnly, - aggregate: runAggregate, - continueOnError: runContinueOnError, + format: evaluateFormat, + writesOnly: evaluateWritesOnly, + aggregate: evaluateAggregate, + continueOnError: evaluateContinueOnError, } // Validate the interactive invocation before any prompt, so a bad flag // combination or a redirected stream never blocks on asking for a mapping // path first. - if runInteractive { - if err := checkInteractiveFlags(opts, runInputFile, cmd.Flags().Changed("format")); err != nil { + if evaluateInteractive { + if err := checkInteractiveFlags(opts, evaluateInputFile, cmd.Flags().Changed("format")); err != nil { fmt.Fprintln(errStream, "Error: "+err.Error()) os.Exit(2) } @@ -530,7 +535,7 @@ continues; the command still exits non-zero if any record was skipped.`, path := promptMappingFile(args, errStream) - if runInteractive { + if evaluateInteractive { err := runMappingInteractive(cmd.Context(), path, cmd.InOrStdin(), cmd.OutOrStdout(), errStream) if errors.Is(err, errMappingInvalid) { os.Exit(2) @@ -541,10 +546,10 @@ continues; the command still exits non-zero if any record was skipped.`, inputReader := cmd.InOrStdin() - if runInputFile != "" { - file, err := os.Open(runInputFile) + if evaluateInputFile != "" { + file, err := os.Open(evaluateInputFile) if err != nil { - return fmt.Errorf("opening input %s: %w", runInputFile, err) + return fmt.Errorf("opening input %s: %w", evaluateInputFile, err) } defer file.Close() @@ -570,23 +575,23 @@ continues; the command still exits non-zero if any record was skipped.`, } func init() { - runCmd.Flags().StringVar(&runFormat, "format", "jsonl", `Output format: "jsonl" or "json"`) - runCmd.Flags().BoolVar( - &runWritesOnly, "writes-only", false, + evaluateCmd.Flags().StringVar(&evaluateFormat, "format", "jsonl", `Output format: "jsonl" or "json"`) + evaluateCmd.Flags().BoolVar( + &evaluateWritesOnly, "writes-only", false, "Emit only write operations in ClientTupleKey format (consumable by fga tuple write)", ) - runCmd.Flags().StringVar( - &runInputFile, "input", "", "Path to JSONL input file, one JSON object per line (default: stdin)") - runCmd.Flags().BoolVar( - &runAggregate, "aggregate", false, + evaluateCmd.Flags().StringVar( + &evaluateInputFile, "input", "", "Path to JSONL input file, one JSON object per line (default: stdin)") + evaluateCmd.Flags().BoolVar( + &evaluateAggregate, "aggregate", false, "Buffer all records and collapse them (dedup tuples and filters, detect write/delete conflicts) before emitting", ) - runCmd.Flags().BoolVar( - &runContinueOnError, "continue-on-error", false, + evaluateCmd.Flags().BoolVar( + &evaluateContinueOnError, "continue-on-error", false, "Skip input records that fail to parse or evaluate (warn to stderr) and exit non-zero if any were skipped", ) - runCmd.Flags().BoolVarP( - &runInteractive, "interactive", "i", false, + evaluateCmd.Flags().BoolVarP( + &evaluateInteractive, "interactive", "i", false, "Explore the mapping in a terminal loop: paste JSON documents and see the tuples they produce (requires a TTY)", ) } diff --git a/tests/mapping-test-cases.yaml b/tests/mapping-test-cases.yaml index 4b3e61c4..c8f7cc5a 100644 --- a/tests/mapping-test-cases.yaml +++ b/tests/mapping-test-cases.yaml @@ -80,9 +80,9 @@ tests: not-contains: - "tests:" - # fga mapping run - 012 - run emits JSONL tuple from stdin: - command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping run ./tests/fixtures/basic-mapping.yaml' + # fga mapping evaluate (run is a deprecated alias) + 012 - evaluate emits JSONL tuple from stdin: + command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping evaluate ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: json: @@ -91,8 +91,8 @@ tests: relation: "member" object: "org:acme" - 013 - run --writes-only emits ClientTupleKey shape without op or action field: - command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping run --writes-only ./tests/fixtures/basic-mapping.yaml' + 013 - evaluate --writes-only emits ClientTupleKey shape without op or action field: + command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping evaluate --writes-only ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: json: @@ -103,41 +103,41 @@ tests: - '"op"' - '"action"' - 014 - run --input reads from file: - command: fga mapping run --input ./tests/fixtures/mapping-event.json ./tests/fixtures/basic-mapping.yaml + 014 - evaluate --input reads from file: + command: fga mapping evaluate --input ./tests/fixtures/mapping-event.json ./tests/fixtures/basic-mapping.yaml exit-code: 0 stdout: json: op: "write" user: "user:anne" - 015 - run --format json --writes-only returns a flat array of tuples: - command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping run --format json --writes-only ./tests/fixtures/basic-mapping.yaml' + 015 - evaluate --format json --writes-only returns a flat array of tuples: + command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping evaluate --format json --writes-only ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: json: 0.user: "user:anne" - 016 - run --format json returns a batch object with writes: - command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping run --format json ./tests/fixtures/basic-mapping.yaml' + 016 - evaluate --format json returns a batch object with writes: + command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping evaluate --format json ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: json: writes.0.user: "user:anne" - 017 - run rejects unknown format: - command: bash -c 'echo '"'"'{}'"'"' | fga mapping run --format xml ./tests/fixtures/basic-mapping.yaml' + 017 - evaluate rejects unknown format: + command: bash -c 'echo '"'"'{}'"'"' | fga mapping evaluate --format xml ./tests/fixtures/basic-mapping.yaml' exit-code: 2 - 018 - run rejects invalid mapping: - command: bash -c 'echo '"'"'{}'"'"' | fga mapping run ./tests/fixtures/invalid-mapping.yaml' + 018 - evaluate rejects invalid mapping: + command: bash -c 'echo '"'"'{}'"'"' | fga mapping evaluate ./tests/fixtures/invalid-mapping.yaml' exit-code: 2 stderr: contains: - "unclosed" - 019 - run streams one op per record for multi-record input: - command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"bob","org":"acme"}\n'"'"' | fga mapping run ./tests/fixtures/basic-mapping.yaml' + 019 - evaluate streams one op per record for multi-record input: + command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"bob","org":"acme"}\n'"'"' | fga mapping evaluate ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: line-count: 2 @@ -145,21 +145,21 @@ tests: - "user:anne" - "user:bob" - 020 - run --aggregate dedups an identical tuple across records: - command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"anne","org":"acme"}\n'"'"' | fga mapping run --aggregate ./tests/fixtures/basic-mapping.yaml' + 020 - evaluate --aggregate dedups an identical tuple across records: + command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"anne","org":"acme"}\n'"'"' | fga mapping evaluate --aggregate ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: line-count: 1 - 021 - run --aggregate reports a cross-record write/delete conflict: - command: bash -c 'printf '"'"'{"id":"anne","org":"acme","op":"add"}\n{"id":"anne","org":"acme","op":"remove"}\n'"'"' | fga mapping run --aggregate ./tests/fixtures/conflicting-mapping.yaml' + 021 - evaluate --aggregate reports a cross-record write/delete conflict: + command: bash -c 'printf '"'"'{"id":"anne","org":"acme","op":"add"}\n{"id":"anne","org":"acme","op":"remove"}\n'"'"' | fga mapping evaluate --aggregate ./tests/fixtures/conflicting-mapping.yaml' exit-code: 1 stderr: contains: - "conflict" - 022 - run --continue-on-error skips a malformed record and exits non-zero: - command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\nnot json\n{"id":"bob","org":"acme"}\n'"'"' | fga mapping run --continue-on-error ./tests/fixtures/basic-mapping.yaml' + 022 - evaluate --continue-on-error skips a malformed record and exits non-zero: + command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\nnot json\n{"id":"bob","org":"acme"}\n'"'"' | fga mapping evaluate --continue-on-error ./tests/fixtures/basic-mapping.yaml' exit-code: 1 stdout: line-count: 2 @@ -170,15 +170,28 @@ tests: contains: - "skipping record on line 2" - 023 - run --continue-on-error with all valid records exits zero: - command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"bob","org":"acme"}\n'"'"' | fga mapping run --continue-on-error ./tests/fixtures/basic-mapping.yaml' + 023 - evaluate --continue-on-error with all valid records exits zero: + command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"bob","org":"acme"}\n'"'"' | fga mapping evaluate --continue-on-error ./tests/fixtures/basic-mapping.yaml' exit-code: 0 stdout: line-count: 2 - 024 - run --aggregate dedups identical unresolved filters across records: - command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"anne","org":"acme"}\n'"'"' | fga mapping run --aggregate --format json ./tests/fixtures/filter-mapping.yaml' + 024 - evaluate --aggregate dedups identical unresolved filters across records: + command: bash -c 'printf '"'"'{"id":"anne","org":"acme"}\n{"id":"anne","org":"acme"}\n'"'"' | fga mapping evaluate --aggregate --format json ./tests/fixtures/filter-mapping.yaml' exit-code: 0 stdout: json: tuple_filter_operations.#: "1" + + 025 - run alias prints deprecation warning and still produces output: + command: bash -c 'echo '"'"'{"id":"anne","org":"acme"}'"'"' | fga mapping run ./tests/fixtures/basic-mapping.yaml' + exit-code: 0 + stdout: + json: + op: "write" + user: "user:anne" + relation: "member" + object: "org:acme" + stderr: + contains: + - "deprecated" From b6ce12222c3997081ebfaf7a8915e46b10a009ac Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Thu, 8 Oct 2026 16:38:35 +0100 Subject: [PATCH 2/2] docs: update README for fga mapping evaluate --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f2657bc0..52ca2b69 100644 --- a/README.md +++ b/README.md @@ -1309,15 +1309,17 @@ fga mapping **init** [mapping.yaml] Created mapping.yaml ``` -##### Run Mapping +##### Evaluate Mapping ###### Command -fga mapping **run** [mapping-file] +fga mapping **evaluate** [mapping-file] Reads JSONL (one JSON object per line) from stdin (or `--input`) and emits tuple operations as JSONL (default) or a JSON batch. Runs entirely offline. Rules using `tuple_filters` cannot be expanded without a store; they are reported as warnings on stderr, or under `tuple_filter_operations` with `--format json`. The mapping file is optional: omit it in an interactive terminal to choose one from a `.yaml`/`.yml` file picker. +> `fga mapping run` is a deprecated alias for `fga mapping evaluate` and will be removed in a future release. + ###### Parameters * `--input`: Path to a JSONL input file, one JSON object per line (default: stdin) * `--format`: Output format — `jsonl` (default) or `json` @@ -1327,9 +1329,9 @@ The mapping file is optional: omit it in an interactive terminal to choose one f * `--interactive` / `-i`: Explore the mapping in a terminal loop — type or paste a JSON document and see the tuple operations it produces. The document is evaluated as soon as it forms a complete JSON value, so a single-line object is evaluated on Enter and a multi-line one when its closing brace is typed. Supports line editing (arrow keys, history). Requires an interactive terminal (both stdin and stdout must be a TTY) and cannot be combined with `--input`, `--writes-only`, `--format`, `--aggregate`, or `--continue-on-error`. ###### Example -`echo '{"id":"anne","org":"acme"}' | fga mapping run mapping.yaml` +`echo '{"id":"anne","org":"acme"}' | fga mapping evaluate mapping.yaml` -`fga mapping run --writes-only mapping.yaml --input event.json > out.jsonl && fga tuple write --store-id $FGA_STORE_ID --file out.jsonl` +`fga mapping evaluate --writes-only mapping.yaml --input event.json > out.jsonl && fga tuple write --store-id $FGA_STORE_ID --file out.jsonl` ###### Response ```json @@ -1340,7 +1342,7 @@ The mapping file is optional: omit it in an interactive terminal to choose one f In a terminal, `-i` starts an explorer loop. Type or paste a JSON document and the resulting tuple operations are printed as an aligned table. The document is evaluated as soon as it parses as complete JSON — a single-line object on Enter, a multi-line one when its closing brace is typed. While more input is expected the prompt shows `...` and a one-time hint notes that the document is not yet valid JSON; pressing Enter on a blank line evaluates whatever is buffered. Invalid JSON is reported with the line, column, and a caret under the offending character. A rule with `tuple_filters` cannot be resolved offline, so its filter conditions are shown as `filter:patch` or `filter:delete` rows (the action distinguishes how the store is reconciled), with the desired-state tuples it reconciles toward shown as indented `desired` rows (they drive a read-diff-write against a store rather than being written directly). Filter fields left unset match any value and render as `*`, and a conditioned tuple shows its condition name and rendered context in brackets. Line editing (arrow keys, history) is available. Available commands: `:reload` re-reads and recompiles the mapping from disk, `:trace on|off` toggles the per-rule trace, and `:quit` exits. ``` -$ fga mapping run mapping.yaml -i +$ fga mapping evaluate mapping.yaml -i mapping loaded: 2 rules. Type or paste a JSON document; it is evaluated once complete. commands: :reload :trace on|off :quit > {"id":"anne","org":"acme"}