refactor: make command.Command valid-by-construction#21
Merged
Conversation
fe3e758 to
bcc87fd
Compare
alanshaw
approved these changes
May 21, 2026
Command was `type Command string`, so its validity invariant (leading
slash, lowercase, no trailing slash) was not enforced by the type:
arbitrary strings could be converted in, the zero value was an invalid
empty string, and the wire-decode path did an unchecked
`command.Command(sval)`.
Model Command on did.DID instead: a struct with an unexported field,
obtainable only through validating constructors. Add MarshalCBOR/
UnmarshalCBOR/DagJSON methods (cbor-gen delegates to them, like did.DID),
so decode now validates and rejects non-conforming commands. The wire
format is unchanged — a command still serializes as the same CBOR text
string (fixtures and test vectors round-trip identically).
- Command is now struct{ str string }; add MustParse, Undef, Defined.
- Regenerate invocation/delegation datamodels to delegate (struct path).
- receipt.Command: const -> var command.MustParse(...).
- Invoke/Delegate no longer re-parse the command (it is valid by
construction); a cheap Defined() guard preserves rejection of the
undefined zero value.
- Add command_test.go: parse table, MustParse, CBOR/DAG-JSON round-trips
and decode-validation guarantees.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bcc87fd to
27db4cf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
command.Commandwastype Command string, so its validity invariant — leading slash, lowercase, no trailing slash — was not enforced by the type. Three ways to hold an invalid command existed:command.Command("FOO/")is legal Go.var c command.Commandis"", whichParseitself rejects.command.Command(sval)with no check.This PR models
Commandon the existingdid.DIDprecedent: a struct with an unexported field, obtainable only through validating constructors. Invalid commands become unrepresentable.What changed
Commandis nowstruct{ str string }(wastype Command string). AddedMustParse,Undef, andDefined(), mirroringdid.DID.MarshalCBOR/UnmarshalCBOR/MarshalDagJSON/UnmarshalDagJSON/MarshalJSON/UnmarshalJSON) copied fromdid.DID. cbor-gen now delegates to them (the(struct)codegen path), and decode validates — a non-conforming command from the wire is rejected instead of silently accepted.delegation/invocation/container/receipttests stay green).receipt.Command:const→var command.MustParse(...)(can'tconsta struct).invocation.go/delegation.go:string(command)→command.String()at the re-parse boundary.MustParse. The "bad command" test now feedscommand.Undef— the only error path still reachable now that invalid commands can't be constructed.command_test.go(the package previously had no tests): parse table,MustParse, CBOR/DAG-JSON round-trips, and the decode-validation guarantees.Notes
New(segments...)still trusts its caller's segments (likecid/didbuilders) rather than validating, to avoid a breaking signature change rippling intobindcom. The headline guarantee comes from the struct itself.bindingwork; this also makes a futurebinding.Of(cmd command.Command)a safe, error-free primitive.🤖 Generated with Claude Code