Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/documentation/schema-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ registered member.

| Position | Admitted members | Shape |
| :-- | :-- | :-- |
| Object Constraint | `required`, `properties` | `required` contains unique field names; `properties` maps field names to Object or Value Constraints; an empty object is a no-op. |
| Object Constraint | `required`, `properties`, `anyOf` | `required` contains unique field names; `properties` maps field names to Object or Value Constraints; `anyOf` is a non-empty array of non-empty Object Constraints; an empty object is a no-op except as an `anyOf` branch. |
| Value Constraint | `enum`, `const` | `enum` is non-empty and unique; at least one member is present; both apply when present together. |

The listed members define the grammar. A `request_constraints` value may
Expand Down
79 changes: 73 additions & 6 deletions docs/specification/overview/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,16 +221,29 @@ optional `path`; nested constraint objects may not. The grammar does not admit
`ucp`. Keys in `properties` name fields on selected request objects.

The constraint begins at an Object Constraint. Object Constraints may nest
through `properties`; Value Constraints occur only as values in an Object
Constraint's `properties` map.
through `properties` and `anyOf`; Value Constraints occur only as values in an
Object Constraint's `properties` map.

| Position | Admitted members | Shape and behavior |
| :-- | :-- | :-- |
| Object Constraint | `required`, `properties` | `required` is an array of unique field names. `properties` maps field names to Object or Value Constraints. An empty Object Constraint is a valid no-op at any Object Constraint position. |
| Object Constraint | `required`, `properties`, `anyOf` | `required` is an array of unique field names. `properties` maps field names to Object or Value Constraints. `anyOf` is a non-empty array of non-empty Object Constraints, at least one of which the object must satisfy. An empty Object Constraint is a valid no-op at every Object Constraint position except an `anyOf` branch. |
| Value Constraint | `enum`, `const` | `enum` is a non-empty array of unique JSON values. `const` is any JSON value. At least one member is present; when both are present, both apply. |

No other member is admitted at either grammar position.

Members present at the same Object Constraint all apply. `anyOf` does not
narrow, override, or replace its siblings; the object must satisfy every
sibling member and at least one branch. Each branch is an ordinary Object
Constraint and cannot carry `path`, so every branch is evaluated against the
same selected object.

Branches are alternatives, not a partition: an object satisfying more than one
branch is valid. Within a branch, `properties` constrains a member only when
that member is present, so a branch pinning a discriminator through
`properties` alone is also satisfied by an object that omits it; naming the
discriminator in the branch's `required` makes the branch match only the shape
it describes.

### Path

Every `request_constraints` value has exactly one effective path. When `path`
Expand Down Expand Up @@ -534,6 +547,58 @@ The payment-handler or instrument contract defines any stronger association.
This example does not define card brands, credentials, support, availability, or
payment policy.

#### Alternative verification requirements on a submitted credential

A Business accepts more than one credential shape and requires different
verification data for each. In this example, a raw PAN must carry a `cvc`, and a
network token must carry a `cryptogram` with its `eci_value`:

<!-- ucp:example schema=shopping/types/available_payment_instrument op=read direction=response -->
```json
{
"type": "card",
"ucp": {
"request_constraints": {
"path": "$['payment']['instruments'][?@['handler_id'] == 'processor_1' && @['type'] == 'card']",
"required": ["credential"],
"properties": {
"credential": {
"anyOf": [
{
"properties": {"card_number_type": {"const": "fpan"}},
"required": ["card_number_type", "cvc"]
},
{
"properties": {"card_number_type": {"const": "network_token"}},
"required": ["card_number_type", "cryptogram", "eci_value"]
}
]
}
}
}
}
}
```

One path selects the submitted instrument, and one Object Constraint describes
it. The sibling `required` applies to every matching instrument; the `anyOf`
branches then apply to the nested `credential` object, which must satisfy at
least one. Each branch pins `card_number_type` and also names it in `required`,
so a branch matches only the credential shape it describes. A raw PAN without a
`cvc` fails, as does a network token missing its `eci_value`.

Because every branch pins the discriminator, the branch set also closes the
accepted values. A `dpan` credential is valid under
[`card_credential.json`](site:schemas/shopping/types/card_credential.json) but
satisfies neither branch, so this Business does not accept it at this path. A
Business that later accepts a new variant adds a branch for it.

Two separately targeted constraints cannot express this rule. Request
Constraints conjoin, so one value requiring `cvc` and another requiring
`cryptogram` would require both. Discriminating through the path filter instead
— selecting `fpan` credentials in one value and `network_token` credentials in
another — moves conditional logic into the selector, which paths do not carry.

## Actions

An Action is an outstanding unit of extension-defined work for a Platform to
Expand Down Expand Up @@ -2674,9 +2739,10 @@ POST /checkout-sessions/{id}/complete
"payment": {
"instruments": [
{
"id": "pi_tok_visa",
"handler_id": "merchant_tokenizer",
// ... more instrument required field
"credential": { "token": "tok_visa_123" }
"type": "card",
"credential": { "type": "card", "token": "tok_visa_123" }
}
]
},
Expand Down Expand Up @@ -2753,8 +2819,9 @@ POST /checkout-sessions/{id}/complete
"payment": {
"instruments": [
{
"id": "pi_ap2_card",
"handler_id": "ap2_234352",
// other required instruments fields
"type": "card",
"credential": {
"type": "card",
"token": "eyJhbGciOiJ..." // Token would contain payment_mandate, the signed proof of funds auth
Expand Down
27 changes: 22 additions & 5 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,19 @@ def _render_table_from_schema(
if not schema_data:
return "_No content fields defined._"

# A bare "#" is a self-root reference naming the schema being rendered.
# It carries no filename, so create_link derives an empty anchor and emits
# a broken "<page>/##" link. Recursive refs are irreducible: ucp-schema
# preserves them even under --bundle, so the docs layer is the only place
# that can name them. Resolve against this schema's own $id.
self_id = schema_data.get("$id")
self_ref_name = self_id.rsplit("/", 1)[-1] if self_id else None

def _deref_self(ref_value):
if ref_value == "#" and self_ref_name:
return self_ref_name
return ref_value

# If schema is ONLY a oneOf, render as prose instead of table
if (
"oneOf" in schema_data
Expand All @@ -722,7 +735,9 @@ def _render_table_from_schema(
links = []
for item in schema_data["oneOf"]:
if "$ref" in item:
links.append(create_link(item["$ref"], spec_file_name, context))
links.append(
create_link(_deref_self(item["$ref"]), spec_file_name, context)
)
elif item.get("type"):
links.append(f"`{item.get('type')}`")
if links:
Expand Down Expand Up @@ -811,7 +826,7 @@ def _render_table_from_schema(
)

f_type = details.get("type", "any")
ref = details.get("$ref")
ref = _deref_self(details.get("$ref"))

# Resolve UCP $defs references inline so properties render as
# expanded tables (with anchors) instead of opaque links.
Expand All @@ -837,7 +852,7 @@ def _render_table_from_schema(

# Check for Array specific logic
items = details.get("items", {})
items_ref = items.get("$ref")
items_ref = _deref_self(items.get("$ref"))

# Special handling for UCP version
version_data = None
Expand All @@ -859,7 +874,9 @@ def _render_table_from_schema(
for one_of_type in details.get("oneOf", []):
if "$ref" in one_of_type:
parts.append(
create_link(one_of_type["$ref"], spec_file_name, context)
create_link(
_deref_self(one_of_type["$ref"]), spec_file_name, context
)
)
elif one_of_type.get("type"):
parts.append(f"`{one_of_type['type']}`")
Expand Down Expand Up @@ -892,7 +909,7 @@ def _render_table_from_schema(
continue
if branch.get("$ref"):
inner_type = create_link(
branch["$ref"], spec_file_name, context
_deref_self(branch["$ref"]), spec_file_name, context
)
break
if branch.get("title"):
Expand Down
12 changes: 10 additions & 2 deletions source/schemas/common/types/constraint_expression.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,29 @@
"properties": {
"required": {
"type": "array",
"description": "Property names required by the constrained object.",
"description": "Property names required by the constrained object. Must be non-empty: an empty array applies no constraint.",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "string"
}
},
"properties": {
"type": "object",
"description": "Constraints keyed by property name.",
"description": "Constraints keyed by property name. Must be non-empty: an empty object applies no constraint.",
"minProperties": 1,
"additionalProperties": {
"oneOf": [
{"$ref": "#"},
{"$ref": "#/$defs/value_constraint"}
]
}
},
"anyOf": {
"type": "array",
"description": "Alternative Object Constraints. The constrained object must satisfy at least one. A branch must be non-empty: an empty branch is satisfied by every object and neutralizes the alternation.",
"minItems": 1,
"items": {"$ref": "#", "minProperties": 1}
Comment thread
raginpirate marked this conversation as resolved.
}
},
"additionalProperties": false,
Expand Down
12 changes: 10 additions & 2 deletions source/schemas/common/types/request_constraints.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,29 @@
},
"required": {
"type": "array",
"description": "Property names required by the constrained object.",
"description": "Property names required by the constrained object. Must be non-empty: an empty array applies no constraint.",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "string"
}
},
"properties": {
"type": "object",
"description": "Constraints keyed by property name.",
"description": "Constraints keyed by property name. Must be non-empty: an empty object applies no constraint.",
"minProperties": 1,
"additionalProperties": {
"oneOf": [
{"$ref": "constraint_expression.json"},
{"$ref": "constraint_expression.json#/$defs/value_constraint"}
]
}
},
"anyOf": {
"type": "array",
"description": "Alternative Object Constraints. The constrained object must satisfy at least one. A branch must be non-empty: an empty branch is satisfied by every object and neutralizes the alternation.",
"minItems": 1,
"items": {"$ref": "constraint_expression.json", "minProperties": 1}
}
},
"additionalProperties": false
Expand Down
Loading