Skip to content
Open
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
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,52 @@ Keywords that would change a schema's *shape* rather than merely constrain
it — currently `patternProperties` — fail generation outright, because no
correct Go type can be produced for them.

### Known upstream, unfixed: request variants wrap response types

Reported upstream as [python-sdk#34](https://github.com/Universal-Commerce-Protocol/python-sdk/issues/34) in April 2026, still open. **This one affects consumers today**, so it is listed here rather than only in the history below.

Variant generation rewrites external `$ref`s only inside `properties`. A schema whose alternatives live in a top-level `oneOf`/`anyOf`/`allOf` keeps its refs pointing at the base response files, so the generated *request* variant wraps *response* types:

```go
type FulfillmentDestinationCreateRequest struct {
RetailLocation *RetailLocation `json:"-"` // want RetailLocationCreateRequest
ShippingDestination *ShippingDestination `json:"-"` // want ShippingDestinationCreateRequest
}
```

`ShippingDestination` requires `id`; its request variant does not, because a client creating a destination has no server-assigned id yet. So a spec-valid create request is rejected:

validate: id: required property is missing

The honest count is smaller than the raw one, and worth stating precisely.
Twelve refs in variant files point at a base schema. Six of those are
correct — `message_error`, `message_info` and `message_warning` have no
request variants, so pointing at the base is the only option. Of the
remaining six, four carry a behavioural consequence: `postal_address`'s
variant is identical to its base in both properties and required, so the
two `shipping_destination_*_request` → `postal_address.json` refs are wrong
without being harmful. The four that matter are the `fulfillment_destination`
create and update variants.

`ucp-go` reproduces this deliberately. Preprocessor parity is byte-for-byte,
so upstream's preprocessing defects are ours until upstream fixes them, and
diverging unilaterally would break the parity that makes the committed
goldens trustworthy — the same reasoning applied to the dangling-`$ref`
defect below before it was fixed.

**The differential harness cannot catch this class, by construction.**
`Validate` and the oracle both read the same preprocessed schema, so both
are wrong in the same way and agree. "Zero disagreements" is true here and
tells you nothing. Only a comparison against the *source* spec sees it —
which is what preprocessor parity is, and parity reports a match because the
defect is faithfully reproduced. It is the clearest example in this
repository of why agreement between two implementations is evidence about
enforcement and not about meaning.

`TestVariantUnionRefsStillPointAtBaseSchemas` pins the exact set, so
upstream's fix arrives as a build failure that says to re-pin and port,
rather than as something noticed on the next manual sweep.

### Resolved upstream: dangling entity references

Reported as [python-sdk#72](https://github.com/Universal-Commerce-Protocol/python-sdk/issues/72) and **fixed** in python-sdk `d650f0b` ([PR #79](https://github.com/Universal-Commerce-Protocol/python-sdk/pull/79)). Recorded because the mechanism generalizes.
Expand Down
126 changes: 126 additions & 0 deletions conformance/variantrefs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package conformance

import (
"encoding/json"
"os"
"path/filepath"
"sort"
"strings"
"testing"
)

// Upstream python-sdk#34: variant generation rewrites external $refs only
// inside `properties`, so a schema whose alternatives sit in a top-level
// oneOf/anyOf/allOf keeps refs pointing at the base (response) files. The
// generated request variant then wraps response types.
//
// ucp-go reproduces this exactly, and deliberately: preprocessor parity is
// byte-for-byte, so upstream's preprocessing bugs are ours until upstream
// fixes them. Diverging unilaterally would break the parity that makes the
// committed goldens trustworthy.
//
// **The differential harness cannot catch this class.** Validate and the
// oracle both read the same preprocessed schema, so both are wrong the same
// way and agree. Zero disagreements is true here and says nothing. Only a
// comparison against the SOURCE spec — which is what preprocessor parity
// is — can see it, and parity reports a match because we faithfully
// reproduce the defect.
//
// So this test is the notification mechanism. It pins the exact set, which
// makes upstream's fix arrive as a build failure telling us to port, rather
// than as something we notice on the next manual sweep.
func TestVariantUnionRefsStillPointAtBaseSchemas(t *testing.T) {
// file -> keyword -> refs, for variant schemas whose top-level union
// branches reference a base file that HAS a request variant of its own.
// A ref to a base with no variant is correct and is not listed: the six
// message_* refs resolve that way, since message_error/info/warning
// have no request variants.
want := map[string][]string{
"shopping/types/fulfillment_destination_create_request.json": {
"oneOf retail_location.json", "oneOf shipping_destination.json",
},
"shopping/types/fulfillment_destination_update_request.json": {
"oneOf retail_location.json", "oneOf shipping_destination.json",
},
"shopping/types/shipping_destination_create_request.json": {
"allOf postal_address.json",
},
"shopping/types/shipping_destination_update_request.json": {
"allOf postal_address.json",
},
}

got := map[string][]string{}
root := filepath.Join("..", "goldens", goldenVersion)
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(path, "_request.json") {
return err
}
raw, err := os.ReadFile(path)
if err != nil {
return err
}
var doc map[string]any
if err := json.Unmarshal(raw, &doc); err != nil {
return err
}
rel, err := filepath.Rel(root, path)
if err != nil {
return err
}
op := "update"
if strings.Contains(rel, "_create_request") {
op = "create"
}
for _, kw := range []string{"oneOf", "anyOf", "allOf"} {
branches, _ := doc[kw].([]any)
for _, b := range branches {
bm, ok := b.(map[string]any)
if !ok {
continue
}
ref, _ := bm["$ref"].(string)
if !strings.HasSuffix(ref, ".json") || strings.Contains(ref, "_request.json") {
continue
}
// Only a defect if the referenced base actually has the
// corresponding variant to point at.
variant := strings.TrimSuffix(ref, ".json") + "_" + op + "_request.json"
if _, err := os.Stat(filepath.Join(filepath.Dir(path), variant)); err != nil {
continue
}
got[filepath.ToSlash(rel)] = append(got[filepath.ToSlash(rel)], kw+" "+ref)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}

for k := range got {
sort.Strings(got[k])
}
for k := range want {
sort.Strings(want[k])
}

for file, refs := range want {
if _, ok := got[file]; !ok {
t.Errorf("%s no longer carries base-pointing refs.\n"+
"If python-sdk#34 has been fixed upstream, re-pin the goldens and "+
"port it: the generated request variants will start wrapping "+
"request types, which is a breaking change worth a release note.", file)
continue
}
if strings.Join(got[file], ",") != strings.Join(refs, ",") {
t.Errorf("%s: refs changed\n got %v\n want %v", file, got[file], refs)
}
delete(got, file)
}
for file, refs := range got {
t.Errorf("%s: base-pointing refs appeared in a file not previously affected: %v\n"+
"Either the corpus grew a new case of python-sdk#34, or variant "+
"generation regressed.", file, refs)
}
}
Loading