diff --git a/preprocess_schemas.py b/preprocess_schemas.py index 9294143..6346ccf 100644 --- a/preprocess_schemas.py +++ b/preprocess_schemas.py @@ -532,6 +532,25 @@ def generate_variants(path, schema, ops, global_variant_requirements): # --- Global Normalization --- +def metadata_union_members(ucp_schema): + """Return the ``$defs`` names that form the UcpMetadata root union. + + The union spans the discovery profiles (platform/business) and every + response schema declared in ``ucp.json``. Deriving the list from + ``$defs`` keeps the generated ``UcpMetadata`` complete as the protocol + adds response types — a previous hardcoded list silently omitted + ``response_catalog_schema``, dropping catalog responses from every + model's ``ucp`` field. + """ + defs = ucp_schema.get("$defs", {}) + return [ + name + for name in defs + if name in ("platform_schema", "business_schema") + or (name.startswith("response_") and name.endswith("_schema")) + ] + + def normalize_metadata_schemas(schemas, target_dir): """ Ensures ucp.json has a root union and other files point to it generically. @@ -542,14 +561,7 @@ def normalize_metadata_schemas(schemas, target_dir): if ucp_path in schemas: ucp = schemas[ucp_path] ucp["oneOf"] = [ - {"$ref": f"#/$defs/{d}"} - for d in [ - "platform_schema", - "business_schema", - "response_checkout_schema", - "response_order_schema", - "response_cart_schema", - ] + {"$ref": f"#/$defs/{name}"} for name in metadata_union_members(ucp) ] for p_abs, s in schemas.items(): diff --git a/src/ucp_sdk/models/schemas/ucp.py b/src/ucp_sdk/models/schemas/ucp.py index 4e8b977..373f7a9 100644 --- a/src/ucp_sdk/models/schemas/ucp.py +++ b/src/ucp_sdk/models/schemas/ucp.py @@ -338,7 +338,8 @@ class ResponseCatalogSchema(Base): | BusinessSchema | ResponseCheckoutSchema | ResponseOrderSchema - | ResponseCartSchema, + | ResponseCartSchema + | ResponseCatalogSchema, Field(..., title="UCP Metadata"), ], ) diff --git a/src/ucp_sdk/models/schemas/ucp_create_request.py b/src/ucp_sdk/models/schemas/ucp_create_request.py index 5f68cf8..830d878 100644 --- a/src/ucp_sdk/models/schemas/ucp_create_request.py +++ b/src/ucp_sdk/models/schemas/ucp_create_request.py @@ -338,7 +338,8 @@ class ResponseCatalogSchema(Base): | BusinessSchema | ResponseCheckoutSchema | ResponseOrderSchema - | ResponseCartSchema, + | ResponseCartSchema + | ResponseCatalogSchema, Field(..., title="UCP Metadata Create Request"), ], ) diff --git a/src/ucp_sdk/models/schemas/ucp_update_request.py b/src/ucp_sdk/models/schemas/ucp_update_request.py index 57cf54c..423f664 100644 --- a/src/ucp_sdk/models/schemas/ucp_update_request.py +++ b/src/ucp_sdk/models/schemas/ucp_update_request.py @@ -338,7 +338,8 @@ class ResponseCatalogSchema(Base): | BusinessSchema | ResponseCheckoutSchema | ResponseOrderSchema - | ResponseCartSchema, + | ResponseCartSchema + | ResponseCatalogSchema, Field(..., title="UCP Metadata Update Request"), ], ) diff --git a/tests/test_codegen_pipeline.py b/tests/test_codegen_pipeline.py index 4890720..31f44cb 100644 --- a/tests/test_codegen_pipeline.py +++ b/tests/test_codegen_pipeline.py @@ -401,7 +401,18 @@ def test_normalize_metadata_schemas_sets_root_union_and_ucp_refs( (target_dir / "checkout_create_request.json").resolve() ) schemas = { - ucp_path: {"$defs": {}}, + ucp_path: { + "$defs": { + "version": {"type": "string"}, + "entity": {"type": "object"}, + "platform_schema": {"type": "object"}, + "business_schema": {"type": "object"}, + "response_checkout_schema": {"type": "object"}, + "response_order_schema": {"type": "object"}, + "response_cart_schema": {"type": "object"}, + "response_catalog_schema": {"type": "object"}, + } + }, checkout_path: { "properties": { "ucp": {"$ref": "ucp.json#/$defs/response_schema"} @@ -424,6 +435,7 @@ def test_normalize_metadata_schemas_sets_root_union_and_ucp_refs( {"$ref": "#/$defs/response_checkout_schema"}, {"$ref": "#/$defs/response_order_schema"}, {"$ref": "#/$defs/response_cart_schema"}, + {"$ref": "#/$defs/response_catalog_schema"}, ], ) self.assertEqual( @@ -546,6 +558,70 @@ def test_main_preprocesses_schema_tree_end_to_end(self) -> None: self.assertEqual(child_variant["required"], ["value"]) +class MetadataUnionTest(unittest.TestCase): + """The UcpMetadata root union is derived from ucp.json $defs.""" + + def test_includes_profiles_and_all_response_schemas(self) -> None: + """Profiles and every response_*_schema belong to the union.""" + ucp = { + "$defs": { + "version": {"type": "string"}, + "version_constraint": {"type": "object"}, + "requires": {"type": "object"}, + "entity": {"type": "object"}, + "base": {"type": "object"}, + "success": {"type": "object"}, + "error": {"type": "object"}, + "platform_schema": {"type": "object"}, + "business_schema": {"type": "object"}, + "response_checkout_schema": {"type": "object"}, + "response_order_schema": {"type": "object"}, + "response_cart_schema": {"type": "object"}, + "response_catalog_schema": {"type": "object"}, + } + } + self.assertEqual( + preprocess_schemas.metadata_union_members(ucp), + [ + "platform_schema", + "business_schema", + "response_checkout_schema", + "response_order_schema", + "response_cart_schema", + "response_catalog_schema", + ], + ) + + def test_picks_up_new_response_types_automatically(self) -> None: + """A response schema added upstream is included without code changes.""" + ucp = { + "$defs": { + "platform_schema": {"type": "object"}, + "business_schema": {"type": "object"}, + "response_invoice_schema": {"type": "object"}, + } + } + self.assertEqual( + preprocess_schemas.metadata_union_members(ucp), + ["platform_schema", "business_schema", "response_invoice_schema"], + ) + + def test_excludes_non_schema_defs(self) -> None: + """Helper and shared defs never leak into the metadata union.""" + ucp = { + "$defs": { + "entity": {"type": "object"}, + "request_schema": {"type": "object"}, + "base": {"type": "object"}, + } + } + self.assertEqual(preprocess_schemas.metadata_union_members(ucp), []) + + def test_empty_defs_yields_empty_union(self) -> None: + """No $defs means no union members.""" + self.assertEqual(preprocess_schemas.metadata_union_members({}), []) + + @unittest.skipUnless( HAVE_SDK, "requires the installed package (pip install -e .)" )