Skip to content

Commit 43a0e01

Browse files
committed
fix(api-catalog): escape literal underscores when joining property-path key segments
Joining #/properties/ path segments with a bare "_" let a literally underscored property name collide with a differently-nested property path resolving to the same suffix. Double literal underscores in each segment before joining so the encoding is unambiguous.
1 parent c6036b4 commit 43a0e01

2 files changed

Lines changed: 81 additions & 68 deletions

File tree

rust/schemas/api/manifest.json

Lines changed: 66 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,46 @@
11
{
2-
"generated": "2026-07-30T22:27:36.653326688+00:00",
2+
"generated": "2026-07-30T22:49:51.251324536+00:00",
33
"domains": {
4-
"hosting-nodejs": {
5-
"file": "hosting-nodejs.json",
6-
"title": "Node.js Hosting Public API",
7-
"endpointCount": 20
8-
},
94
"catalog-products": {
105
"file": "catalog-products.json",
116
"title": "Catalog GraphQL API",
127
"endpointCount": 1
138
},
14-
"orders": {
15-
"file": "orders.json",
16-
"title": "Order Service",
17-
"endpointCount": 15
18-
},
19-
"channels": {
20-
"file": "channels.json",
21-
"title": "Channels API",
22-
"endpointCount": 8
23-
},
24-
"subscriptions": {
25-
"file": "subscriptions.json",
26-
"title": "Commerce Subscription API",
27-
"endpointCount": 8
28-
},
29-
"payment-requests": {
30-
"file": "payment-requests.json",
31-
"title": "Payment Requests API",
9+
"onboarding": {
10+
"file": "onboarding.json",
11+
"title": "Commerce Onboarding API",
3212
"endpointCount": 5
3313
},
14+
"price-adjustments": {
15+
"file": "price-adjustments.json",
16+
"title": "GoDaddy Price Adjustment API",
17+
"endpointCount": 10
18+
},
3419
"metafields": {
3520
"file": "metafields.json",
3621
"title": "Metafields API",
3722
"endpointCount": 12
3823
},
39-
"fulfillments": {
40-
"file": "fulfillments.json",
41-
"title": "Fulfillment Service",
42-
"endpointCount": 4
24+
"shipping": {
25+
"file": "shipping.json",
26+
"title": "Shipping Service",
27+
"endpointCount": 7
4328
},
44-
"bulk-operations": {
45-
"file": "bulk-operations.json",
46-
"title": "Bulk Operations API",
47-
"endpointCount": 27
29+
"orders": {
30+
"file": "orders.json",
31+
"title": "Order Service",
32+
"endpointCount": 15
4833
},
49-
"chargebacks": {
50-
"file": "chargebacks.json",
51-
"title": "Chargeback Management",
52-
"endpointCount": 10
34+
"taxes": {
35+
"file": "taxes.json",
36+
"title": "Tax GraphQL API",
37+
"endpointCount": 1
5338
},
5439
"customer-profiles": {
5540
"file": "customer-profiles.json",
5641
"title": "Customers API",
5742
"endpointCount": 9
5843
},
59-
"price-adjustments": {
60-
"file": "price-adjustments.json",
61-
"title": "GoDaddy Price Adjustment API",
62-
"endpointCount": 10
63-
},
6444
"recommendations": {
6545
"file": "recommendations.json",
6646
"title": "GoDaddy Recommendation API",
@@ -71,45 +51,65 @@
7151
"title": "Commerce Store API",
7252
"endpointCount": 8
7353
},
74-
"taxes": {
75-
"file": "taxes.json",
76-
"title": "Tax GraphQL API",
77-
"endpointCount": 1
54+
"channels": {
55+
"file": "channels.json",
56+
"title": "Channels API",
57+
"endpointCount": 8
7858
},
79-
"transactions": {
80-
"file": "transactions.json",
81-
"title": "Transactions API",
82-
"endpointCount": 17
59+
"hosting-nodejs": {
60+
"file": "hosting-nodejs.json",
61+
"title": "Node.js Hosting Public API",
62+
"endpointCount": 20
8363
},
84-
"onboarding": {
85-
"file": "onboarding.json",
86-
"title": "Commerce Onboarding API",
87-
"endpointCount": 5
64+
"payments": {
65+
"file": "payments.json",
66+
"title": "Payments API",
67+
"endpointCount": 43
8868
},
89-
"shipping": {
90-
"file": "shipping.json",
91-
"title": "Shipping Service",
92-
"endpointCount": 7
69+
"fulfillments": {
70+
"file": "fulfillments.json",
71+
"title": "Fulfillment Service",
72+
"endpointCount": 4
9373
},
9474
"domains": {
9575
"file": "domains.json",
9676
"title": "Domain Lifecycle Management API",
9777
"endpointCount": 14
9878
},
99-
"location-addresses": {
100-
"file": "location-addresses.json",
101-
"title": "Addresses API",
102-
"endpointCount": 2
103-
},
104-
"payments": {
105-
"file": "payments.json",
106-
"title": "Payments API",
107-
"endpointCount": 43
79+
"bulk-operations": {
80+
"file": "bulk-operations.json",
81+
"title": "Bulk Operations API",
82+
"endpointCount": 27
10883
},
10984
"businesses": {
11085
"file": "businesses.json",
11186
"title": "Commerce Business API",
11287
"endpointCount": 8
88+
},
89+
"transactions": {
90+
"file": "transactions.json",
91+
"title": "Transactions API",
92+
"endpointCount": 17
93+
},
94+
"chargebacks": {
95+
"file": "chargebacks.json",
96+
"title": "Chargeback Management",
97+
"endpointCount": 10
98+
},
99+
"subscriptions": {
100+
"file": "subscriptions.json",
101+
"title": "Commerce Subscription API",
102+
"endpointCount": 8
103+
},
104+
"location-addresses": {
105+
"file": "location-addresses.json",
106+
"title": "Addresses API",
107+
"endpointCount": 2
108+
},
109+
"payment-requests": {
110+
"file": "payment-requests.json",
111+
"title": "Payment Requests API",
112+
"endpointCount": 5
113113
}
114114
}
115115
}

rust/tools/generate-api-catalog/src/main.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -718,10 +718,16 @@ fn derive_defs_key_for_path(ref_str: &str) -> String {
718718
// A `#/properties/<name>` fragment (optionally nested, e.g. `#/properties/a/properties/b`)
719719
// selects one property's schema out of the file, not the file's root schema — fold the
720720
// property path into the key so two refs into the same file for different properties
721-
// don't collide under one bare file-stem key.
721+
// don't collide under one bare file-stem key. Each segment has its literal `_` doubled
722+
// before joining on a single `_`, so a property literally named `a_b` can't collide with
723+
// nested segments `a` + `b` (both would otherwise sanitize to the same `a_b` suffix).
722724
match frag.and_then(|f| f.strip_prefix("#/properties/")) {
723725
Some(rest) => {
724-
let suffix = rest.split("/properties/").collect::<Vec<_>>().join("_");
726+
let suffix = rest
727+
.split("/properties/")
728+
.map(|seg| seg.replace('_', "__"))
729+
.collect::<Vec<_>>()
730+
.join("_");
725731
sanitize_defs_key(&format!("{base_key}_{suffix}"))
726732
}
727733
None => base_key,
@@ -2282,6 +2288,13 @@ components:
22822288
derive_defs_key_for_path("./models/Foo.yaml#/properties/a/properties/b"),
22832289
"Foo_a_b"
22842290
);
2291+
// A literal property named `a_b` must not collide with nested segments `a`
2292+
// then `b` — both would sanitize to the same "a_b" suffix without escaping
2293+
// the literal underscore first.
2294+
assert_ne!(
2295+
derive_defs_key_for_path("./models/Foo.yaml#/properties/a_b"),
2296+
derive_defs_key_for_path("./models/Foo.yaml#/properties/a/properties/b")
2297+
);
22852298
}
22862299

22872300
#[test]

0 commit comments

Comments
 (0)