Skip to content

fix: add the missing maxProperties constraint family - #91

Open
vishkaty wants to merge 4 commits into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/location-serves-max-properties
Open

fix: add the missing maxProperties constraint family#91
vishkaty wants to merge 4 commits into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/location-serves-max-properties

Conversation

@vishkaty

Copy link
Copy Markdown
Contributor

Fixes #90

What this changes

Adds find_root_max_properties, inject_max_properties, and
_patch_max_properties to postprocess_models.py, mirroring their
minProperties counterparts one for one (same marker guarded
idempotency, same model_fields_set union model_extra key counting
semantics, same free form object exclusion for a bound without declared
properties, already handled natively by the generator through
Field(max_length=...)). Wired into the script main() as an independent
patch pass so
both bounds can be injected into the same class without either
clobbering the other, which location_serves.json needs since it
declares minProperties 1 and maxProperties 1 together.

One deliberate difference from the function it mirrors:
find_root_min_properties treats a falsy minProperties (0) as absent,
using not minimum, which is harmless since minProperties 0 permits
everything minProperties absent already does. maxProperties 0 is a real
and different constraint, no properties allowed at all, so
find_root_max_properties checks isinstance(maximum, int) instead of
truthiness. This is new code, not a change to the existing minProperties
function, which is out of scope here.

Test plan

  • Added MaxPropertiesInjectorTest, mirroring the existing InjectorTest
    for minProperties, plus LocationServesMaxPropertiesSemanticTest
    against the real committed model, including a negative control
    proving the existing minProperties check is untouched by this change.
  • Full suite: 100 tests, 0 failures, 4 documented skips.
  • Regenerated against release/2026-08-25 as a separate commit from the
    generator fix. Three files change: LocationServes,
    LocationServesCreateRequest, LocationServesUpdateRequest.
  • Regenerated a second time and diffed the two outputs, excluding
    pycache. No difference.
  • Kill test: reverted postprocess_models.py to its pre fix state,
    regenerated, reinstalled. The same failures reappeared exactly.
    Restored the fix and regenerated again to confirm green.
  • pre-commit run on the changed files: clean.

Not included

README.md, which ruff format also reformats when generate_models.sh
runs, unrelated to this fix and present before this PR. Left untouched
to keep the diff scoped.

…uite

The HAVE_SDK import gate at the top of test_codegen_pipeline.py still
imported Description/Totals from ucp_sdk.models.schemas.shopping.types,
paths that moved to ucp_sdk.models.schemas.common.types when Universal-Commerce-Protocol#87
(the 2026-08-25 UCP release regen) restructured the schema tree. The
stale paths raise ModuleNotFoundError, which the surrounding
try/except catches and sets HAVE_SDK = False, so every test gated on
HAVE_SDK skips instead of running (unittest reports a skip as OK, so
the suite reads green while ~37% of it never executes).

Fix every stale shopping.types.* reference in the file: description,
totals (+ its request variants), signals (+ its request variants), and
error_response moved to common.types under the same class names, so
those tests now run and pass unmodified. Two targets did not survive
the schema restructuring at all -- card_payment_instrument.Constraints
(a uniqueItems brands field) and merchant_fulfillment_config's nested
additionalProperties:false object (now business_fulfillment_config,
reshaped) -- so their four tests become documented unittest.skip with
the reason recorded in the schema, not silently deleted.

Before: 89 tests, 33 skipped (26 on the HAVE_SDK gate, 7 on
'executing the module needs pydantic', which also reads HAVE_SDK).
After: 89 tests, 4 skipped, all four with a stated schema-shape
reason.

No production code changes; test-only.
location_serves.json declares both minProperties: 1 AND maxProperties:
1 at the schema root ("The Platform MUST supply exactly one target
form"), but only minProperties was ever scanned:
find_root_min_properties reads schema.get("minProperties") and there
is no symmetric find_root_max_properties at all (maxProperties has
been unhandled since PR Universal-Commerce-Protocol#55 added the minProperties family for issue
Universal-Commerce-Protocol#49). The committed LocationServes model enforces the minimum but not
the maximum, so a map naming both point and address validates in
violation of the schema.

Adds, mirroring InjectorTest (the existing minProperties injector
test) one for one:

  - MaxPropertiesInjectorTest: injector-level unit tests against
    synthetic fixtures for find_root_max_properties (schema scan) and
    inject_max_properties (validator injection), including that both
    bounds can coexist on the same class without clobbering each
    other, and that a free-form object (no named properties, already
    handled natively via Field(max_length=...)) stays out of scope --
    mirroring the min side's existing free-form-object exclusion.

  - LocationServesMaxPropertiesSemanticTest: exercises the real
    committed LocationServes model. Includes a negative control
    (test_empty_still_rejected_by_the_existing_minimum) proving the
    pre-existing minProperties check is untouched by this change, and
    a case confirming an extension key still counts toward the total
    under extra="allow" key-counting semantics.

RED: 100 tests, 2 failures + 6 errors (find_root_max_properties and
inject_max_properties do not exist yet), 4 documented skips
(unchanged, from the root-cause-0 commit).
find_root_min_properties (added in Universal-Commerce-Protocol#55 for issue Universal-Commerce-Protocol#49) scans root-level
minProperties on object schemas with declared properties, but
maxProperties never grew a matching scanner: there is no
find_root_max_properties at all. location_serves.json declares both
minProperties: 1 and maxProperties: 1 on the same schema ("the
Platform MUST supply exactly one target form"), so the committed
LocationServes model enforces the minimum but silently accepts an
object naming both point and address, which JSON Schema rejects.

Adds find_root_max_properties, inject_max_properties, and
_patch_max_properties, mirroring their minProperties counterparts one
for one (same marker-guarded idempotency, same
model_fields_set | model_extra key-counting semantics, same free-form
object exclusion for maxProperties without declared properties,
already handled natively via Field(max_length=...)). Wired into
main() as an independent patch pass so both bounds can be injected
into the same class without either clobbering the other.

One deliberate deviation from the minProperties scanner it mirrors:
find_root_min_properties treats a falsy minProperties (0) as absent
via "not minimum", which is harmless since minProperties: 0 permits
everything minProperties: absent already does. maxProperties: 0 is a
real, different constraint (no properties allowed at all), so
find_root_max_properties checks "isinstance(maximum, int)" instead of
truthiness -- new code, not a fix to the existing (out of scope)
min-side function.

Generator-level change only (postprocess_models.py); no generated
model files touched in this commit. Regeneration follows in a
separate commit, which is what turns the two still-red semantic tests
green (the injector-level unit tests added in the prior commit --
which exercise find_root_max_properties/inject_max_properties
directly against synthetic fixtures, not the committed models --
already pass).

100 tests, 2 failures (LocationServesMaxPropertiesSemanticTest), 4
documented skips.
Regenerates via ./generate_models.sh 2026-08-25 (the same command the
model-drift CI job runs) to pick up the postprocessing fix in the
prior commit. Three files change, all in the location_serves family:
LocationServes, LocationServesCreateRequest and
LocationServesUpdateRequest each gain an _enforce_max_properties
validator alongside their existing _enforce_min_properties one.

Verified:
- Full suite: 100 tests, 0 failures, 4 documented skips (both new
  semantic tests from the RED commit now pass).
- Double-regen: ran generate_models.sh 2026-08-25 twice; diff -rq
  between both outputs (excluding __pycache__) is empty.
- Kill-test: reverted postprocess_models.py to its pre-fix state,
  regenerated, reinstalled -- the same 2 failures + 6 errors from the
  RED commit reappeared verbatim. Restored the fix and regenerated
  again to confirm the suite returns to green.
- pre-commit run on the changed files: clean.

Not committed: README.md, which ruff format also reformats as a
pre-existing docstring-code-block spacing drift in main, unrelated to
this fix (see the equivalent note on the jwk-conditional-rules branch).
@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:needs-triage Signal that the PR is ready for human triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: location_serves.json maxProperties is never enforced

3 participants