Skip to content

feat(inspect): evaluate -f/--format as a Go-template across all inspect commands#42

Merged
us merged 2 commits into
us:mainfrom
alex-mextner:feat/inspect-format-eval
Jun 27, 2026
Merged

feat(inspect): evaluate -f/--format as a Go-template across all inspect commands#42
us merged 2 commits into
us:mainfrom
alex-mextner:feat/inspect-format-eval

Conversation

@alex-mextner

Copy link
Copy Markdown
Contributor

What

mocker inspect -- and the dedicated mocker container inspect /
mocker image inspect subcommands -- declare -f/--format (mirroring
docker inspect) but parse then ignore it, always printing the full JSON array.
This adds Go text/template evaluation for the {{ .Dotted.Path }} field-access
subset and routes every inspect entry point through one shared formatter.

Why

Docker-shaped callers drive inspect with a template and expect a bare
scalar
, e.g. dev stands and CI health gates:

docker inspect -f '{{.State.Running}}' <container> | grep -q true

With the flag unevaluated, that grep is a coin-flip on the substring true
appearing somewhere in the JSON dump. Every templated inspect caller is broken
until the template is actually evaluated. (ContainerInspect.swift already
carried a // ... will be wired up in the follow-up (PR 2) note for exactly
this -- this is that wiring.)

How

  • GoTemplate (Formatters/GoTemplate.swift): evaluates the
    {{ .Dotted.Path }} subset only. Substitution is a single left-to-right pass
    by match range
    , so a resolved value that itself contains {{...}} is emitted
    verbatim and can never re-trigger substitution (repeated tokens can't
    contaminate each other either).
  • Fails loud, never silent-wrong: any {{...}} block that is not a supported
    field access ({{if}}, {{range}}, {{json .X}}, {{index …}}, a bare
    {{.}}, …) throws GoTemplateError rather than passing through as misleading
    literal text. --format json is handled as Docker's documented special value
    (emit the JSON), not as a template.
  • InspectFormat (Formatters/InspectFormat.swift): one shared emitOne /
    emitArray path used by inspect, container inspect, and image inspect --
    renders --format when set (one line per record), otherwise prints the JSON
    exactly as before. No duplicated formatting logic.
  • Paths resolve against the Docker-shaped inspect JSON from feat(inspect): Docker-compatible container inspect output (closes #36) #40
    (.State.Running, .State.Status, .Id, .Name, .Config.Image,
    .NetworkSettings.IPAddress, …). Scalars render the Go way: bare
    true/false, integers without a decimal point, strings verbatim; an
    unknown/null path and array/object leaves render empty (documented scope limit).

Tests

Tests/MockerTests/GoTemplateTests.swift covers the dev-stand health-gate paths,
integer-vs-bool scalars (including Pid 0 on a stopped container, the case the
CFBoolean type-id check guards), whitespace, multi-token / repeated tokens,
no cross-token / re-entrant substitution, array-leaf-empty (pinned), the
JSONSerialization bool->NSNumber bridging path, and fail-loud rejection of
if / range / json / index / bare {{.}}.

Verification

GoTemplate is pure Foundation; it was compiled and run standalone under Swift
6.3.2 with the full test matrix above -- all pass, including
{{.State.Running}} -> true/false, {{.State.Pid}} -> 1234/0 (integer,
not true/false), the previously-buggy cross-token case {{.A}}-{{.B}} with
A == "{{.B}}" -> {{.B}}-x, and the unsupported-action throws. InspectFormat
typechecks against the real TableFormatter + GoTemplate. End to end, a
docker inspect -f '{{.State.Running}}' <container> | grep -q true health gate
against a real Apple-container-backed Postgres/Redis now passes.

Note: the maintainer's CI is the build authority for the full graph -- this was
developed on a host without the matching macOS SDK to link
apple/containerization, so only the standalone-run + targeted typecheck
verification above was possible locally. A CLI-level integration test for
InspectFormat.emit* (using the repo's captureStdout helper) is a sensible
follow-up once it can be exercised against the engine.

alex-mextner and others added 2 commits June 26, 2026 22:50
…ct commands

`inspect` -- and the dedicated `container inspect` / `image inspect` subcommands
-- declared `-f/--format` mirroring `docker inspect`, but parsed and then ignored
it: the full JSON array was always printed. Docker callers drive inspect with a
template and expect a bare scalar, e.g. dev stands and CI health gates:

    docker inspect -f '{{.State.Running}}' <container> | grep -q true

Against the unevaluated flag that grep is a coin-flip on the substring "true"
appearing somewhere in the JSON dump.

Add a small Go text/template evaluator (`GoTemplate`) for the `{{ .Dotted.Path }}`
field-access subset real inspect callers use, and route every inspect entry point
through a shared `InspectFormat` helper that renders `--format` (one line per
record) and otherwise prints the JSON exactly as before. This also wires
`--format` into `container inspect` and `image inspect`, which previously accepted
the flag for Docker surface parity but never applied it.

Behavior:
- Paths resolve against the Docker-shaped inspect JSON from us#40 (`.State.Running`,
  `.State.Status`, `.Id`, `.Name`, `.Config.Image`, `.NetworkSettings.IPAddress`,
  ...). Scalars render the Go way: bare `true`/`false`, integers without a decimal
  point, strings verbatim; an unknown/null path and array/object leaves render empty.
- Substitution is a single left-to-right pass by match range, so a resolved value
  that itself contains `{{...}}` is emitted verbatim and never re-triggers
  substitution.
- `--format json` is treated as Docker's documented special value (emit the JSON),
  not as a template.
- Any `{{...}}` block outside the supported field-access subset (`if`, `range`,
  `json`, `index`, a bare `{{.}}`, ...) throws rather than passing through as
  misleading literal text -- a silently-unevaluated template would feed false data
  to the grep/jq pipelines these outputs drive.

Add GoTemplateTests covering the dev-stand health-gate paths, integer-vs-bool
scalars (incl. Pid 0 on a stopped container), whitespace, multi-token / repeated
tokens, no cross-token / re-entrant substitution, array-leaf-empty, the
JSONSerialization bool->NSNumber bridging path, and fail-loud rejection of
unsupported actions.
GoTemplateError only conformed to CustomStringConvertible, but ArgumentParser
prints thrown errors through localizedDescription, which honors LocalizedError
only. Without this the helpful "unsupported template action" message was
replaced by a generic "The operation couldn't be completed" string. Conform to
LocalizedError (matching MockerError) so the message surfaces. Fail-loud
behaviour (nonzero exit) was already correct.
@us

us commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Thanks @alex-mextner — excellent work. 🙌 The scope call is spot on: a minimal {{.Dotted.Path}} subset that fails loud on if/range/json/index/bare {{.}} rather than silently feeding wrong data to a grep/jq pipeline is exactly right for the health-gate use case. The CFBoolean check before the integer branch (so {{.State.Pid}} 0 isn't rendered as false) and the range-splice that avoids re-entrant substitution are both nicely handled and well tested.

Verified locally (the full graph builds against apple/containerization here): swift build clean, 221/221 tests pass including your 19 GoTemplate cases.

I pushed one small follow-up commit: GoTemplateError now conforms to LocalizedError (matching MockerError). ArgumentParser prints thrown errors via localizedDescription, which only honors LocalizedError — without it your helpful "unsupported template action…" message was being replaced by a generic "The operation couldn't be completed" string. The fail-loud nonzero exit was already correct; this just makes the message surface.

The documented scope limits (unknown path → empty, array/object leaf → empty) are reasonable for a first cut. Merging now — thanks for the great contribution!

@us us merged commit 804b370 into us:main Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants