feat(adcp)!: type media buy status filter unions#179
Merged
Conversation
There was a problem hiding this comment.
LGTM. Follow-ups noted below. Right shape — narrow scalar-or-array helper that fails closed on schema drift and on the wire, with the breaking marker (feat!:) carrying the type change correctly.
Things I checked
- Conventional-commit story —
feat(adcp)!: type media buy status filter unionscarries the!, matchesany → *MediaBuyStatusFilterfield-type change documented inMIGRATING.md:162-163. - Wire shape —
MarshalJSONemits bare scalar atlen==1, array otherwise;UnmarshalJSONtries scalar first then array, rejectsnullandlen==0. Faithful to bothmedia-buy/get-media-buys-request.json#/properties/status_filterandmedia-buy/get-media-buy-delivery-request.json#/properties/status_filterperad-tech-protocol-expert. - Empty/null rejection is faithful —
schema_accepts_empty_arrayonly emits reject-empty when the schema's array branch hasminItems >= 1. The generatedtypes_gen.go:1741-1772contains those reject blocks, so this isn't over-reach. - Fail-closed posture on the generator side —
supported_union_schemasre-raises bad pointers asValueError(generate.py:1351), divergent element types at line 1354, divergentminItemsat 1356. Lintervalidate_union_schema_specs(lint.py:312-355) mirrors all three and folds intohas_problemsatlint.py:542. - Tests cover scalar round-trip, array round-trip, empty-marshal rejection, null-unmarshal rejection, and empty-array-unmarshal rejection (
generated_core_refs_test.go:458-490). - Coverage budget drop 47 → 45 in
ci.yml:48matches two newly-typed fields and PR-body validation that--coverage-max-unreviewed-any 44fails as expected. types_gen.gois regenerated, not hand-edited; the new helper is emitted under_will_generate_setviasupported_union_schemas(generate.py:1144).
Follow-ups (non-blocking — file as issues)
generate.py:1144—_will_generate_set()callssupported_union_schemas()withoutskip_names=KNOWN_TYPES, whilegenerate()(line 1604) andmain()(line 1780) pass it. If a union name ever lands inKNOWN_TYPES,_will_generate_setwould still mark it auto-generated. Mirror theINLINE_SCHEMA_TYPESpattern at line 1136 and passskip_names=KNOWN_TYPEShere too. Cheap.generate.py:1000-1006—schema_accepts_empty_arrayhas noisinstance(schema, dict)guard the wayscalar_union_go_typedoes. A future non-dictitems(tuple-form schemas) would raiseAttributeError, which is not inSCHEMA_RESOLUTION_ERRORSand would crash both generator and linter outside the fail-closed path. Add the guard.generate.py:1356-1361— the "primary_schema" selection picks the shortest description (len(description) < len(primary_schema.get('description', ''))). Either the variable name is misleading or the comparator is inverted. Comment intent or pick deterministically by spec order.- No automated test exercising the generator's fail-closed path — broken
UNION_SCHEMA_TYPESpointer and non-equivalent schemas are only validated by hand in the PR body. A small unit test would lock in the contract. - No unmarshal test for garbage scalar shapes (number, object, bool). The current implementation returns the array decoder's opaque error; a test would freeze the error-surface contract for future refactors.
generated_core_refs_test.go:458-490. supported_union_schemas(skip_names=KNOWN_TYPES)is invoked three times permain()run (via_will_generate_set, thengenerate(), thenmain()). Cheap today at one helper; memoize once a few more land.
Minor nits (non-blocking)
- Pointer-to-slice for
StatusFilter.*MediaBuyStatusFilteris a pointer to a slice. With empty rejected at marshal, a bareMediaBuyStatusFilterwithomitemptywould behave identically and avoid the double indirection. ThePtr(...)convention inMIGRATING.md:225-233is consistent across the codebase, so leave it — worth a passing thought next time a union helper lands. - Scalar
Unmarshallaxness.MediaBuyStatusis a string alias, sojson.Unmarshal(data, &single)accepts any JSON string including""and unknown values. Consistent with the rest of the generated enums, but the helper inherits that laxness — note in a follow-up if enum-value validation ever becomes a project goal.
Safe to merge.
86456a5 to
a93a149
Compare
This was referenced May 26, 2026
There was a problem hiding this comment.
Approving. Right shape: narrow scalar-or-array helper, shared UNION_SCHEMA_TYPES config between generator and lint, breaking marker on the commit, regenerated adcp/types_gen.go matches the diff.
Things I checked
feat(adcp)!:marker on the single commit. The on-the-wire shape (scalar-or-array) is unchanged; the break is Go-side (StatusFilter any→*MediaBuyStatusFilter). Marker correctly applied per the release-please contract.adcp/types_gen.go:1740-1772was regenerated in this PR; no hand-edits. The helper is generator-owned, soKNOWN_TYPES/EXEMPTsemantics still hold.- Generator/lint share
UNION_SCHEMA_TYPESinadcp/schemas/generate.py:619-627.validate_union_schema_specs(adcp/schemas/lint.py:312-355) cross-checks that both pointers resolve to equivalent scalar-or-array unions with matchingminItems; CI fails closed on drift between the two surfaces. INLINE_TYPE_HINTS(adcp/schemas/generate.py:780-781) wires both fields to*MediaBuyStatusFilter; struct declarations intypes_gen.go:3610and:3633match.- Coverage budget 47→45 (
.github/workflows/ci.yml:48) matches the two newly-typed fields. The--coverage-max-unreviewed-any 44negative case in the PR validation list confirms it's tight. - Empty/null marshal+unmarshal tests in
adcp/generated_core_refs_test.go:485-489cover the value-type path. Scalar-vs-array round-trip at:478-481confirms the wire shape. - TMP signing, router, identity-agent,
skills/adcp-*paths untouched.
Follow-ups (non-blocking — file as issues)
- Field-level
nulldoesn't trigger the customUnmarshalJSON. WithStatusFilter *MediaBuyStatusFilter, Go'sencoding/jsonzeros the pointer on JSONnullwithout calling the method. The "reject null" wording inMIGRATING.mdand the PR body is accurate only for the value-typed path the test exercises — through the parent struct,{\"status_filter\": null}quietly becomes a nil pointer. Either document the field-level behavior or drop the null-rejection claim from the migration note. Interesting choice keeping the guard in the generated code for the explicit value-typed path; harmless there but not load-bearing via the parent struct. - "Shortest description wins" heuristic in
supported_union_schemas(adcp/schemas/generate.py:1359-1364) picks the right doc here by accident — both upstream descriptions begin "Filter by status. Can be a single status or array of statuses." and only the request variant trails with "Defaults to ...". A future helper whose generic description is the longer one would get the wrong doc. Swap to an explicitprimary_pointerselector inUNION_SCHEMA_TYPES, or at least comment that tie-breaking is tuple order. - Empty-rejection is a silent trip-wire on upstream
minItems.schema_accepts_empty_array(adcp/schemas/generate.py:1000-1008) returnsTruewhenminItemsis absent or 0, in which case no rejection is emitted. The generatedtypes_gen.godoes emit rejection, which proves both upstream schemas declareminItems: 1— but if upstream relaxes that constraint in a future bundle, empty filters start round-tripping without a lint error. A one-line comment onschema_accepts_empty_arraydocumenting this dependency would be load-bearing. - Pointer-to-slice ergonomics.
&MediaBuyStatusFilter{}(non-nil pointer to empty slice) hits theMarshalerror path rather than being elided. A bare slice withomitemptywould have made "no filter" cheap and impossible to misencode, but the pointer pattern is consistent with the rest ofadcp/types_gen.go. Flag for the next union helper authors; not worth re-litigating here.
Minor nits (non-blocking)
- Blank-tabbed line in generated code when empty-reject branch is omitted.
adcp/schemas/generate.py:1334emits\\t{reject_empty_unmarshal.strip()}\\n, which produces a\\t\\nline when the branch is empty. Inert today since the only configured union takes the populated branch, butgofumptwill flag it once it does fire. Gate the whole\\t...line rather than relying on.strip(). - Redundant guard in lint.
adcp/schemas/lint.py:338checkslen(reports) != error_count or len(loaded) != len(schema_specs)— the second clause is implied by the first given the error paths. Trim.
Safe to merge.
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
Validation
Expert review