feat(structerr): structured JSON errors with typed interception and semantic exit codes#65
Merged
Conversation
7 tasks
…N errors Introduce HandleError() which classifies errors from cobra and structcli, writes a JSON StructuredError to the provided writer, and returns a semantic exit code from the exitcode package. Error classification covers: - Cobra string patterns: missing required flags, invalid flag values, unknown flags, unknown commands - structcli typed errors: ValidationError, InputError - Unmarshal/decode errors from mapstructure with field-level detail - Config errors: unknown keys, parse errors, not found - Source attribution: distinguishes CLI flag vs env var vs config origin using flag annotations and os.Getenv checks ExecuteOrExit() is a convenience wrapper for the common main() pattern.
24 tests covering: nil error, missing required flags (with/without env hints, single/multiple), invalid flag values (CLI and env var source attribution), unknown flags, unknown commands (with available list), validation errors (direct and wrapped), input errors, config errors (unknown keys, not found, parse), unmarshal decode errors (env var attribution, no env, unparseable), generic fallback, JSON output validity, helper functions, and two integration tests using real structcli Define/Unmarshal.
Replace manual Execute+log.Fatalln with structcli.ExecuteOrExit for structured JSON error output. Add SilenceErrors and SilenceUsage to demonstrate clean machine-readable error handling.
…is set Cobra checks required flags before viper merges env vars. When an env var IS set but cobra still fires "required flag not set", the env var value is fine — cobra just doesn't see it. Report missing_required_flag with a hint, not env_invalid_value.
…essages - findFlagForField now matches via field path annotations, fixing cases where Go field names (LogLevel) differ from flag names (level) - classifyUnmarshalError tries multiple regex patterns to handle different mapstructure error formats (parse errors, invalid string errors, generic decode errors) - classifyInvalidArg now populates expected type from flag metadata - Env error messages rewritten to be agent-friendly: "env var MYAPP_PORT: invalid value "xyz" for flag "port" (expected int)" instead of raw Go error internals - Added flagType() helper and parseDecodeError() with three patterns
…erage - Remove reDecodeFieldGeneric which captured wrong quoted string on multi-quote errors (eg. 'Port'...'config'...'bad' matched Port/config instead of Port/bad). Replace with reDecodeFieldName that safely extracts only the field name. - Add tests for findFlagForField path annotation matching (LogLevel→level) - Add TestParseDecodeError covering all three regex patterns + no match - Add TestFindFlagForField covering direct name, path annotation, and miss - Add TestFlagType covering int, string, and nonexistent flags - Add TestExtractLongFlagName_Fallback for short-only flag specs - Add TestHandleError_InvalidFlagValueFromEnvVar_CobraPath for cobra-path env attribution with expected type enrichment Coverage: findFlagForField 57→100%, parseDecodeError 71→100%, extractLongFlagName 75→100%, classifyUnmarshalError 93→100%.
…d detect enum errors - classifyValidation now calls ve.Details() to extract structured Field, Rule, Param, and Value from each validation error instead of only the message - Resolves Go struct field names to CLI flag names via findFlagForField - Adds Param field to Violation struct for rule parameters (e.g., "18" for min=18) - Adds flagEnumValues helper to read enum annotations from flags - classifyInvalidArg and classifyUnmarshalError now check enum annotations and return exit code 15 (InvalidFlagEnum) when value is not in allowed set - classifyMissingRequired enriches hint with "required by validation" when the flag has a validate annotation containing "required" - Adds flagValidateRules and contains helpers
…ions - TestHandleError_ValidationFailed_WithDetails: real validator errors produce Violation with Field (resolved to flag name), Rule, Param, Value - TestHandleError_ValidationFailed_FieldToFlagMapping: StructField "LogLevel" maps to flag "level" via path annotation - TestHandleError_InvalidFlagEnum: flag with enum annotation, bad value not in allowed set produces exit code 15 with available array - TestHandleError_InvalidFlagEnum_ValidValue: enum takes precedence over type error - TestHandleError_InvalidFlagEnum_UnmarshalPath: enum detection via unmarshal path - TestHandleError_MissingRequiredFlagWithValidateHint: required flag with validate annotation includes "required by validation" in hint - TestFlagEnumValues, TestFlagValidateRules, TestContains: unit tests for helpers
Add FlagError type in errors/ with FlagErrorKind (InvalidValue, Unknown) and structured metadata (FlagName, Value, Cause). Add SetupFlagErrors() which hooks cobra's SetFlagErrorFunc to intercept flag parsing errors at creation time and wrap them in typed FlagError values. HandleError then uses errors.As to classify them — no regex at classification time. Regex-based classification is preserved as a fallback for CLIs that don't call SetupFlagErrors. 5 new tests: typed invalid value, typed unknown flag, short flag extraction, enum violation via typed path, regex fallback without setup.
…mand resolution Add FlagError type with structured metadata (FlagName, Value, Kind, CommandPath, ExpectedType, EnumValues, EnvVars) to errors/. Add SetupFlagErrors() which hooks cobra's SetFlagErrorFunc to intercept flag parsing errors at creation time and wrap them in typed FlagError values with pre-populated metadata from the actual command. HandleError then uses errors.As — no regex at classification time. Switch ExecuteOrExit from Execute() to ExecuteC() which returns the actual subcommand that failed, giving HandleError the correct flag set for metadata lookups (type, env vars, enum values) and the correct command path for error context.
…andling Full example CLI with srv and usr subcommands showing SetupFlagErrors, SetupJSONSchema, ExecuteOrExit, and ValidatableOptions. Comment header lists every error scenario with expected exit codes.
Add 12 new tests covering: typed FlagError invalid value, unknown flag, short flag extraction, enum violation, regex fallback without setup, env var source attribution via typed path, validation with empty Details, env var regex fallback path, unrecognized error format, CommandPath from FlagError, and valid enum value not classified as violation.
…Exit Simplify FlagError to carry only FlagName, Value, Kind, Cause — remove CommandPath, ExpectedType, EnumValues, EnvVars fields that duplicated metadata available from the command. Since ExecuteC() now gives HandleError the correct subcommand, all enrichment (type lookup, enum check, env attribution) happens once in classifyInvalidArg via the same code path for both typed and regex errors. ExecuteOrExit now automatically sets SilenceErrors and SilenceUsage on the root command, so cobra doesn't print its own error messages or usage text. Users no longer need to remember to set these manually.
HandleError requires the command where the error originated, not the root. Document why (flag metadata lookup), what goes wrong if root is passed (degraded output), and how to get it right (ExecuteOrExit or ExecuteC).
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
HandleError(cmd, err, writer)— classifies errors, writes JSON to stderr, returns semantic exit codeExecuteOrExit(cmd)— one-line convenience formain(), auto-setsSilenceErrors/SilenceUsage, usesExecuteC()for correct command resolutionSetupFlagErrors(rootCmd)— intercepts flag parsing errors viacobra.SetFlagErrorFuncproducing typedFlagErrorvalues, eliminating regex for ~90% of error classificationstructerrsubpackage followingconfig/debug/jsonschemapatternValidationError.Details()with per-field violationsEnumValuerannotations (exit code 15)Two layers of error classification
Layer 1:
SetupFlagErrors(typed, preferred)Intercepts errors at creation time via
cobra.SetFlagErrorFunc, producing typedFlagErrorvalues with structured metadata.HandleErroruseserrors.As— no regex needed.Layer 2: Regex fallback (for cobra string errors)
When
SetupFlagErrorsis not called, or for error types cobra doesn't expose typed errors for (required flags, unknown commands),HandleErrorfalls back to string pattern matching on cobra's error messages.Usage
One-line:
Manual control:
Important:
HandleErrorrequires the command where the error originated, not the root. UseExecuteC()orExecuteOrExitto get this right. See doc comment for details.Error classification
errormissing_required_flagFlagErroror string pattern +Changed()invalid_flag_valueFlagError+EnumValuerannotationinvalid_flag_enumos.Getenvenv_invalid_valueFlagErroror string patternunknown_flagunknown_commanderrors.AsonValidationError+Details()validation_failedinvalid_flag_valueorenv_invalid_valueconfig_*errorChanges
structerr/types.go(new)structerr.Optionssubpackagestructerr.go(new)SetupFlagErrors,HandleError,ExecuteOrExit,StructuredError,Violation, classification enginestructerr_test.go(new)errors/errors.goFlagErrortype with Kind/FlagName/Value/Cause fieldsexamples/simple/main.goExecuteOrExitwithSilenceErrors/SilenceUsageexamples/structerr/(new)Test coverage
classifyclassifyUnmarshalErrorparseDecodeErrorflagTypefindFlagForFieldflagEnvVarsflagEnumValuesflagValidateRulesextractLongFlagNameparseQuotedListcommandPathclassifyMissingRequiredclassifyInvalidArgclassifyUnknownCommandSetupFlagErrorsHandleErrorclassifyValidationExecuteOrExit