From 94762d3bc3f01959b766ada00c0a67d449a49895 Mon Sep 17 00:00:00 2001 From: Tanner Oakes Date: Wed, 6 May 2026 13:51:46 -0600 Subject: [PATCH 1/4] readd variations summary --- README.md | 1 - entity/entity.go | 9 +- entity/entity_test.go | 216 ++++++++++++++++++++++++++++++++++++ query/getvariations_test.go | 2 +- query/query.go | 10 +- query/query_test.go | 3 +- query/resources.go | 8 ++ 7 files changed, 238 insertions(+), 11 deletions(-) create mode 100644 entity/entity_test.go diff --git a/README.md b/README.md index 13e4ea8..36f1ce8 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ The library keeps several legacy knobs for source compatibility, but the Creator | `PartnerType` (`Associates`) | Explicit body parameter | Ignored. Partner type is implicit | | `Merchant` | Offer filtering selector | Ignored | | `OfferCount` | Offer summary limiter | Ignored | -| `EnableVariationSummary()` | Requested variation summary resource | No-op (resource is not exposed) | ### Marketplace routing precedence diff --git a/entity/entity.go b/entity/entity.go index 49d5887..909463e 100644 --- a/entity/entity.go +++ b/entity/entity.go @@ -109,6 +109,7 @@ type Item struct { SalesRank *int `json:",omitempty"` Ancestor *Ancestor `json:",omitempty"` WebsiteSalesRank *struct { + Id string `json:"id,omitempty"` DisplayName string ContextFreeName string SalesRank int @@ -120,11 +121,13 @@ type Item struct { Large *Image `json:",omitempty"` Medium *Image `json:",omitempty"` Small *Image `json:",omitempty"` + HiRes *Image `json:"hiRes,omitempty"` } `json:",omitempty"` Variants []*struct { Large *Image `json:",omitempty"` Medium *Image `json:",omitempty"` Small *Image `json:",omitempty"` + HiRes *Image `json:"hiRes,omitempty"` } `json:",omitempty"` } `json:",omitempty"` ItemInfo *struct { @@ -295,8 +298,8 @@ type Item struct { Percentage int } `json:",omitempty"` } `json:",omitempty"` - Type string `json:",omitempty"` - ViolateMAP bool + Type string `json:",omitempty"` + ViolatesMAP bool `json:"violatesMAP,omitempty"` } `json:",omitempty"` } `json:",omitempty"` } @@ -318,6 +321,7 @@ type Price struct { type VariationDimension struct { DisplayName string + Locale string `json:",omitempty"` Name string Values []string } @@ -364,6 +368,7 @@ type Response struct { DisplayName string ContextFreeName string IsRoot bool + SalesRank *int `json:",omitempty"` } `json:",omitempty"` } `json:",omitempty"` } diff --git a/entity/entity_test.go b/entity/entity_test.go new file mode 100644 index 0000000..49b0e22 --- /dev/null +++ b/entity/entity_test.go @@ -0,0 +1,216 @@ +package entity + +import ( + "testing" +) + +// TestDecodeGetVariationsResponse exercises the lowerCamelCase Creators API +// response shape for GetVariations, including the variationSummary block +// (pageCount, variationCount, price, variationDimensions) and Image.HiRes. +func TestDecodeGetVariationsResponse(t *testing.T) { + body := []byte(`{ + "variationsResult": { + "items": [ + { + "asin": "B07YCM5K55", + "parentASIN": "B07YCM5JXX", + "detailPageURL": "https://www.amazon.com/dp/B07YCM5K55", + "score": 0.42, + "images": { + "primary": { + "small": {"url": "https://example.test/s.jpg", "height": 75, "width": 75}, + "medium": {"url": "https://example.test/m.jpg", "height": 160, "width": 160}, + "large": {"url": "https://example.test/l.jpg", "height": 500, "width": 500}, + "hiRes": {"url": "https://example.test/h.jpg", "height": 2000,"width": 2000} + } + }, + "offersV2": { + "listings": [ + { + "isBuyBoxWinner": true, + "violatesMAP": true, + "type": "New" + } + ] + } + } + ], + "variationSummary": { + "pageCount": 3, + "variationCount": 27, + "price": { + "highestPrice": {"amount": 49.99, "currency": "USD", "displayAmount": "$49.99"}, + "lowestPrice": {"amount": 19.99, "currency": "USD", "displayAmount": "$19.99"} + }, + "variationDimensions": [ + {"displayName": "Size", "locale": "en_US", "name": "size_name", "values": ["S", "M", "L"]}, + {"displayName": "Color", "locale": "en_US", "name": "color_name", "values": ["Red", "Blue"]} + ] + } + } +}`) + resp, err := DecodeResponse(body) + if err != nil { + t.Fatalf("DecodeResponse: %+v", err) + } + if resp.VariationsResult == nil { + t.Fatal("VariationsResult is nil") + } + vs := resp.VariationsResult.VariationSummary + if vs == nil { + t.Fatal("VariationSummary is nil") + } + if vs.PageCount != 3 { + t.Errorf("PageCount = %d, want 3", vs.PageCount) + } + if vs.VariationCount != 27 { + t.Errorf("VariationCount = %d, want 27", vs.VariationCount) + } + if vs.Price == nil || vs.Price.HighestPrice == nil || vs.Price.LowestPrice == nil { + t.Fatal("price/highestPrice/lowestPrice is nil") + } + if got, want := vs.Price.HighestPrice.Amount, 49.99; got != want { + t.Errorf("HighestPrice.Amount = %v, want %v", got, want) + } + if got, want := vs.Price.LowestPrice.DisplayAmount, "$19.99"; got != want { + t.Errorf("LowestPrice.DisplayAmount = %q, want %q", got, want) + } + if got, want := len(vs.VariationDimensions), 2; got != want { + t.Fatalf("len(VariationDimensions) = %d, want %d", got, want) + } + if got, want := vs.VariationDimensions[0].Locale, "en_US"; got != want { + t.Errorf("VariationDimensions[0].Locale = %q, want %q", got, want) + } + if got, want := vs.VariationDimensions[0].Name, "size_name"; got != want { + t.Errorf("VariationDimensions[0].Name = %q, want %q", got, want) + } + // Items: hiRes image and isBuyBoxWinner / violatesMAP on V2 listing. + if got, want := len(resp.VariationsResult.Items), 1; got != want { + t.Fatalf("len(Items) = %d, want %d", got, want) + } + item := resp.VariationsResult.Items[0] + if item.Images == nil || item.Images.Primary == nil || item.Images.Primary.HiRes == nil { + t.Fatal("Images.Primary.HiRes is nil") + } + if got, want := item.Images.Primary.HiRes.URL, "https://example.test/h.jpg"; got != want { + t.Errorf("HiRes.URL = %q, want %q", got, want) + } + if item.OffersV2 == nil || item.OffersV2.Listings == nil || len(*item.OffersV2.Listings) != 1 { + t.Fatal("OffersV2.Listings missing/empty") + } + listing := (*item.OffersV2.Listings)[0] + if !listing.IsBuyboxWinner { + t.Errorf("isBuyBoxWinner did not decode into IsBuyboxWinner") + } + if !listing.ViolatesMAP { + t.Errorf("violatesMAP did not decode into ViolatesMAP") + } +} + +// TestDecodeGetBrowseNodesResponseWithSalesRank verifies the new SalesRank +// field on the top-level BrowseNodesResult.BrowseNodes. +func TestDecodeGetBrowseNodesResponseWithSalesRank(t *testing.T) { + body := []byte(`{ + "browseNodesResult": { + "browseNodes": [ + { + "id": "3040", + "displayName": "Books", + "contextFreeName": "Books", + "isRoot": true, + "salesRank": 12345 + } + ] + } +}`) + resp, err := DecodeResponse(body) + if err != nil { + t.Fatalf("DecodeResponse: %+v", err) + } + if resp.BrowseNodesResult == nil || len(resp.BrowseNodesResult.BrowseNodes) != 1 { + t.Fatal("BrowseNodesResult.BrowseNodes empty") + } + bn := resp.BrowseNodesResult.BrowseNodes[0] + if bn.SalesRank == nil { + t.Fatal("BrowseNode.SalesRank is nil") + } + if got, want := *bn.SalesRank, 12345; got != want { + t.Errorf("BrowseNode.SalesRank = %d, want %d", got, want) + } +} + +// TestDecodeBrowseNodeInfoWebsiteSalesRankID exercises the websiteSalesRank.id +// field exposed by the Creators API. +func TestDecodeBrowseNodeInfoWebsiteSalesRankID(t *testing.T) { + body := []byte(`{ + "itemsResult": { + "items": [ + { + "asin": "B0", + "browseNodeInfo": { + "browseNodes": [ + { + "id": "1", + "displayName": "Books", + "contextFreeName": "Books", + "websiteSalesRank": { + "id": "1000", + "displayName": "Books", + "contextFreeName": "Books", + "salesRank": 7 + } + } + ] + } + } + ] + } +}`) + resp, err := DecodeResponse(body) + if err != nil { + t.Fatalf("DecodeResponse: %+v", err) + } + if resp.ItemsResult == nil || len(resp.ItemsResult.Items) == 0 { + t.Fatal("no items decoded") + } + bni := resp.ItemsResult.Items[0].BrowseNodeInfo + if bni == nil || len(bni.BrowseNodes) == 0 { + t.Fatal("BrowseNodeInfo.BrowseNodes empty") + } + wsr := bni.BrowseNodes[0].WebsiteSalesRank + if wsr == nil { + t.Fatal("WebsiteSalesRank is nil") + } + if got, want := wsr.Id, "1000"; got != want { + t.Errorf("WebsiteSalesRank.Id = %q, want %q", got, want) + } +} + +// TestDecodeIgnoresUnknownFields confirms that unknown JSON keys do not +// cause DecodeResponse to error (relevant if the Creators API adds new +// fields between SDK releases). +func TestDecodeIgnoresUnknownFields(t *testing.T) { + body := []byte(`{"itemsResult":{"items":[{"asin":"A","unknownField":42}]},"newTopLevel":"ok"}`) + if _, err := DecodeResponse(body); err != nil { + t.Fatalf("DecodeResponse rejected unknown field: %+v", err) + } + // Sanity: malformed JSON still errors. + if _, err := DecodeResponse([]byte("{not json")); err == nil { + t.Error("expected error for malformed JSON, got nil") + } +} + +/* Copyright 2026 goark contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/query/getvariations_test.go b/query/getvariations_test.go index 2aee61b..2a5b910 100644 --- a/query/getvariations_test.go +++ b/query/getvariations_test.go @@ -92,7 +92,7 @@ func TestResourcesInGetVariations(t *testing.T) { {q: NewGetVariations("", "", "").EnableItemInfo(), str: `{"resources":["itemInfo.byLineInfo","itemInfo.contentInfo","itemInfo.contentRating","itemInfo.classifications","itemInfo.externalIds","itemInfo.features","itemInfo.manufactureInfo","itemInfo.productInfo","itemInfo.technicalInfo","itemInfo.title","itemInfo.tradeInInfo"]}`}, {q: NewGetVariations("", "", "").EnableOffers(), str: `{"resources":["offersV2.listings.availability","offersV2.listings.condition","offersV2.listings.dealDetails","offersV2.listings.isBuyBoxWinner","offersV2.listings.loyaltyPoints","offersV2.listings.merchantInfo","offersV2.listings.price","offersV2.listings.type"]}`}, {q: NewGetVariations("", "", "").EnableOffersV2(), str: `{"resources":["offersV2.listings.availability","offersV2.listings.condition","offersV2.listings.dealDetails","offersV2.listings.isBuyBoxWinner","offersV2.listings.loyaltyPoints","offersV2.listings.merchantInfo","offersV2.listings.price","offersV2.listings.type"]}`}, - {q: NewGetVariations("", "", "").EnableVariationSummary(), str: `{}`}, + {q: NewGetVariations("", "", "").EnableVariationSummary(), str: `{"resources":["variationSummary.price.highestPrice","variationSummary.price.lowestPrice","variationSummary.variationDimension"]}`}, } for _, tc := range testCases { diff --git a/query/query.go b/query/query.go index 52c6c06..ab547c4 100644 --- a/query/query.go +++ b/query/query.go @@ -145,12 +145,12 @@ func (q *Query) BrowseNodes() *Query { return q } -// VariationSummary selects the VariationSummary resource. -// -// Deprecated: the Creators API does not expose a VariationSummary resource. -// Calls to this method are now no-ops and the response will not contain a -// VariationSummary block. Retained so existing call sites compile. +// VariationSummary sets the resource of VariationSummary. The Creators API +// returns the variation summary (page count, total variation count, price +// range and variation dimensions) under the VariationsResult.VariationSummary +// container of the response. func (q *Query) VariationSummary() *Query { + q.enableResources[resourceVariationSummary] = true return q } diff --git a/query/query_test.go b/query/query_test.go index bf32533..d0556de 100644 --- a/query/query_test.go +++ b/query/query_test.go @@ -140,8 +140,7 @@ func TestResources(t *testing.T) { {q: empty.With().ParentASIN(), str: `{"resources":["parentASIN"]}`}, {q: empty.With().CustomerReviews(), str: `{"resources":["customerReviews.count","customerReviews.starRating"]}`}, {q: empty.With().BrowseNodes(), str: `{"resources":["browseNodes.ancestor","browseNodes.children"]}`}, - // VariationSummary is no longer exposed by the Creators API. - {q: empty.With().VariationSummary(), str: `{}`}, + {q: empty.With().VariationSummary(), str: `{"resources":["variationSummary.price.highestPrice","variationSummary.price.lowestPrice","variationSummary.variationDimension"]}`}, } for _, tc := range testCases { diff --git a/query/resources.go b/query/resources.go index 14d6400..dc206cc 100644 --- a/query/resources.go +++ b/query/resources.go @@ -11,6 +11,7 @@ const ( resourceParentASIN //ParentASIN resource resourceCustomerReviews //CustomerReviews resource resourceBrowseNodes //BrowseNodes resource + resourceVariationSummary //VariationSummary resource ) // Resource string values match the Amazon Creators API enum values @@ -77,6 +78,12 @@ var ( "browseNodes.ancestor", "browseNodes.children", } + //VariationSummary resource + resourcesVariationSummary = []string{ + "variationSummary.price.highestPrice", + "variationSummary.price.lowestPrice", + "variationSummary.variationDimension", + } resourcesMap = map[resource][]string{ resourceBrowseNodeInfo: resourcesBrowseNodeInfo, @@ -87,6 +94,7 @@ var ( resourceParentASIN: resourcesParentASIN, resourceCustomerReviews: resourcesCustomerReviews, resourceBrowseNodes: resourcesBrowseNodes, + resourceVariationSummary: resourcesVariationSummary, } ) From 8de49903c9b645d7ea7b19f7c0226d5b20fa4b2f Mon Sep 17 00:00:00 2001 From: Tanner Oakes Date: Wed, 6 May 2026 14:11:05 -0600 Subject: [PATCH 2/4] remove hires --- entity/entity.go | 2 -- entity/entity_test.go | 15 +++++++-------- query/getitems_test.go | 2 +- query/getvariations_test.go | 2 +- query/query_test.go | 2 +- query/resources.go | 2 -- query/searchitems_test.go | 2 +- 7 files changed, 11 insertions(+), 16 deletions(-) diff --git a/entity/entity.go b/entity/entity.go index 909463e..dc96728 100644 --- a/entity/entity.go +++ b/entity/entity.go @@ -121,13 +121,11 @@ type Item struct { Large *Image `json:",omitempty"` Medium *Image `json:",omitempty"` Small *Image `json:",omitempty"` - HiRes *Image `json:"hiRes,omitempty"` } `json:",omitempty"` Variants []*struct { Large *Image `json:",omitempty"` Medium *Image `json:",omitempty"` Small *Image `json:",omitempty"` - HiRes *Image `json:"hiRes,omitempty"` } `json:",omitempty"` } `json:",omitempty"` ItemInfo *struct { diff --git a/entity/entity_test.go b/entity/entity_test.go index 49b0e22..22dc351 100644 --- a/entity/entity_test.go +++ b/entity/entity_test.go @@ -6,7 +6,7 @@ import ( // TestDecodeGetVariationsResponse exercises the lowerCamelCase Creators API // response shape for GetVariations, including the variationSummary block -// (pageCount, variationCount, price, variationDimensions) and Image.HiRes. +// (pageCount, variationCount, price, variationDimensions). func TestDecodeGetVariationsResponse(t *testing.T) { body := []byte(`{ "variationsResult": { @@ -20,8 +20,7 @@ func TestDecodeGetVariationsResponse(t *testing.T) { "primary": { "small": {"url": "https://example.test/s.jpg", "height": 75, "width": 75}, "medium": {"url": "https://example.test/m.jpg", "height": 160, "width": 160}, - "large": {"url": "https://example.test/l.jpg", "height": 500, "width": 500}, - "hiRes": {"url": "https://example.test/h.jpg", "height": 2000,"width": 2000} + "large": {"url": "https://example.test/l.jpg", "height": 500, "width": 500} } }, "offersV2": { @@ -84,16 +83,16 @@ func TestDecodeGetVariationsResponse(t *testing.T) { if got, want := vs.VariationDimensions[0].Name, "size_name"; got != want { t.Errorf("VariationDimensions[0].Name = %q, want %q", got, want) } - // Items: hiRes image and isBuyBoxWinner / violatesMAP on V2 listing. + // Items: primary image and isBuyBoxWinner / violatesMAP on V2 listing. if got, want := len(resp.VariationsResult.Items), 1; got != want { t.Fatalf("len(Items) = %d, want %d", got, want) } item := resp.VariationsResult.Items[0] - if item.Images == nil || item.Images.Primary == nil || item.Images.Primary.HiRes == nil { - t.Fatal("Images.Primary.HiRes is nil") + if item.Images == nil || item.Images.Primary == nil || item.Images.Primary.Large == nil { + t.Fatal("Images.Primary.Large is nil") } - if got, want := item.Images.Primary.HiRes.URL, "https://example.test/h.jpg"; got != want { - t.Errorf("HiRes.URL = %q, want %q", got, want) + if got, want := item.Images.Primary.Large.URL, "https://example.test/l.jpg"; got != want { + t.Errorf("Images.Primary.Large.URL = %q, want %q", got, want) } if item.OffersV2 == nil || item.OffersV2.Listings == nil || len(*item.OffersV2.Listings) != 1 { t.Fatal("OffersV2.Listings missing/empty") diff --git a/query/getitems_test.go b/query/getitems_test.go index d9bf0a0..c57a437 100644 --- a/query/getitems_test.go +++ b/query/getitems_test.go @@ -86,7 +86,7 @@ func TestResourcesInGetItems(t *testing.T) { str string }{ {q: NewGetItems("", "", "").EnableBrowseNodeInfo(), str: `{"resources":["browseNodeInfo.browseNodes","browseNodeInfo.browseNodes.ancestor","browseNodeInfo.browseNodes.salesRank","browseNodeInfo.websiteSalesRank"]}`}, - {q: NewGetItems("", "", "").EnableImages(), str: `{"resources":["images.primary.small","images.primary.medium","images.primary.large","images.primary.highRes","images.variants.small","images.variants.medium","images.variants.large","images.variants.highRes"]}`}, + {q: NewGetItems("", "", "").EnableImages(), str: `{"resources":["images.primary.small","images.primary.medium","images.primary.large","images.variants.small","images.variants.medium","images.variants.large"]}`}, {q: NewGetItems("", "", "").EnableItemInfo(), str: `{"resources":["itemInfo.byLineInfo","itemInfo.contentInfo","itemInfo.contentRating","itemInfo.classifications","itemInfo.externalIds","itemInfo.features","itemInfo.manufactureInfo","itemInfo.productInfo","itemInfo.technicalInfo","itemInfo.title","itemInfo.tradeInInfo"]}`}, // EnableOffers (V1) is now an alias for EnableOffersV2. {q: NewGetItems("", "", "").EnableOffers(), str: `{"resources":["offersV2.listings.availability","offersV2.listings.condition","offersV2.listings.dealDetails","offersV2.listings.isBuyBoxWinner","offersV2.listings.loyaltyPoints","offersV2.listings.merchantInfo","offersV2.listings.price","offersV2.listings.type"]}`}, diff --git a/query/getvariations_test.go b/query/getvariations_test.go index 2a5b910..23183ac 100644 --- a/query/getvariations_test.go +++ b/query/getvariations_test.go @@ -88,7 +88,7 @@ func TestResourcesInGetVariations(t *testing.T) { str string }{ {q: NewGetVariations("", "", "").EnableBrowseNodeInfo(), str: `{"resources":["browseNodeInfo.browseNodes","browseNodeInfo.browseNodes.ancestor","browseNodeInfo.browseNodes.salesRank","browseNodeInfo.websiteSalesRank"]}`}, - {q: NewGetVariations("", "", "").EnableImages(), str: `{"resources":["images.primary.small","images.primary.medium","images.primary.large","images.primary.highRes","images.variants.small","images.variants.medium","images.variants.large","images.variants.highRes"]}`}, + {q: NewGetVariations("", "", "").EnableImages(), str: `{"resources":["images.primary.small","images.primary.medium","images.primary.large","images.variants.small","images.variants.medium","images.variants.large"]}`}, {q: NewGetVariations("", "", "").EnableItemInfo(), str: `{"resources":["itemInfo.byLineInfo","itemInfo.contentInfo","itemInfo.contentRating","itemInfo.classifications","itemInfo.externalIds","itemInfo.features","itemInfo.manufactureInfo","itemInfo.productInfo","itemInfo.technicalInfo","itemInfo.title","itemInfo.tradeInInfo"]}`}, {q: NewGetVariations("", "", "").EnableOffers(), str: `{"resources":["offersV2.listings.availability","offersV2.listings.condition","offersV2.listings.dealDetails","offersV2.listings.isBuyBoxWinner","offersV2.listings.loyaltyPoints","offersV2.listings.merchantInfo","offersV2.listings.price","offersV2.listings.type"]}`}, {q: NewGetVariations("", "", "").EnableOffersV2(), str: `{"resources":["offersV2.listings.availability","offersV2.listings.condition","offersV2.listings.dealDetails","offersV2.listings.isBuyBoxWinner","offersV2.listings.loyaltyPoints","offersV2.listings.merchantInfo","offersV2.listings.price","offersV2.listings.type"]}`}, diff --git a/query/query_test.go b/query/query_test.go index d0556de..35557b3 100644 --- a/query/query_test.go +++ b/query/query_test.go @@ -131,7 +131,7 @@ func TestResources(t *testing.T) { str string }{ {q: empty.With().BrowseNodeInfo(), str: `{"resources":["browseNodeInfo.browseNodes","browseNodeInfo.browseNodes.ancestor","browseNodeInfo.browseNodes.salesRank","browseNodeInfo.websiteSalesRank"]}`}, - {q: empty.With().Images(), str: `{"resources":["images.primary.small","images.primary.medium","images.primary.large","images.primary.highRes","images.variants.small","images.variants.medium","images.variants.large","images.variants.highRes"]}`}, + {q: empty.With().Images(), str: `{"resources":["images.primary.small","images.primary.medium","images.primary.large","images.variants.small","images.variants.medium","images.variants.large"]}`}, {q: empty.With().ItemInfo(), str: `{"resources":["itemInfo.byLineInfo","itemInfo.contentInfo","itemInfo.contentRating","itemInfo.classifications","itemInfo.externalIds","itemInfo.features","itemInfo.manufactureInfo","itemInfo.productInfo","itemInfo.technicalInfo","itemInfo.title","itemInfo.tradeInInfo"]}`}, // Offers (V1) is now an alias for OffersV2 in the Creators API. {q: empty.With().Offers(), str: `{"resources":["offersV2.listings.availability","offersV2.listings.condition","offersV2.listings.dealDetails","offersV2.listings.isBuyBoxWinner","offersV2.listings.loyaltyPoints","offersV2.listings.merchantInfo","offersV2.listings.price","offersV2.listings.type"]}`}, diff --git a/query/resources.go b/query/resources.go index dc206cc..1af3817 100644 --- a/query/resources.go +++ b/query/resources.go @@ -29,11 +29,9 @@ var ( "images.primary.small", "images.primary.medium", "images.primary.large", - "images.primary.highRes", "images.variants.small", "images.variants.medium", "images.variants.large", - "images.variants.highRes", } //ItemInfo resource resourcesItemInfo = []string{ diff --git a/query/searchitems_test.go b/query/searchitems_test.go index 1e29149..3ef75f4 100644 --- a/query/searchitems_test.go +++ b/query/searchitems_test.go @@ -124,7 +124,7 @@ func TestResourcesInSearchItems(t *testing.T) { str string }{ {q: NewSearchItems("", "", "").EnableBrowseNodeInfo(), str: `{"resources":["browseNodeInfo.browseNodes","browseNodeInfo.browseNodes.ancestor","browseNodeInfo.browseNodes.salesRank","browseNodeInfo.websiteSalesRank"]}`}, - {q: NewSearchItems("", "", "").EnableImages(), str: `{"resources":["images.primary.small","images.primary.medium","images.primary.large","images.primary.highRes","images.variants.small","images.variants.medium","images.variants.large","images.variants.highRes"]}`}, + {q: NewSearchItems("", "", "").EnableImages(), str: `{"resources":["images.primary.small","images.primary.medium","images.primary.large","images.variants.small","images.variants.medium","images.variants.large"]}`}, {q: NewSearchItems("", "", "").EnableItemInfo(), str: `{"resources":["itemInfo.byLineInfo","itemInfo.contentInfo","itemInfo.contentRating","itemInfo.classifications","itemInfo.externalIds","itemInfo.features","itemInfo.manufactureInfo","itemInfo.productInfo","itemInfo.technicalInfo","itemInfo.title","itemInfo.tradeInInfo"]}`}, {q: NewSearchItems("", "", "").EnableOffers(), str: `{"resources":["offersV2.listings.availability","offersV2.listings.condition","offersV2.listings.dealDetails","offersV2.listings.isBuyBoxWinner","offersV2.listings.loyaltyPoints","offersV2.listings.merchantInfo","offersV2.listings.price","offersV2.listings.type"]}`}, {q: NewSearchItems("", "", "").EnableOffersV2(), str: `{"resources":["offersV2.listings.availability","offersV2.listings.condition","offersV2.listings.dealDetails","offersV2.listings.isBuyBoxWinner","offersV2.listings.loyaltyPoints","offersV2.listings.merchantInfo","offersV2.listings.price","offersV2.listings.type"]}`}, From 4c64254fdf1c911ac52104c3a8ab9302c66544de Mon Sep 17 00:00:00 2001 From: Tanner Oakes Date: Wed, 6 May 2026 14:39:23 -0600 Subject: [PATCH 3/4] finalize entity shape --- client_test.go | 4 +- entity/entity.go | 15 +- entity/entity_test.go | 387 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 365 insertions(+), 41 deletions(-) diff --git a/client_test.go b/client_test.go index 2ee5cd6..68552e0 100644 --- a/client_test.go +++ b/client_test.go @@ -117,7 +117,7 @@ func TestClientRequestSendsExpectedHeadersAndBody(t *testing.T) { if got, want := string(body), `{"hello":"world"}`; got != want { t.Errorf("api body = %q, want %q", got, want) } - _, _ = w.Write([]byte(`{"itemsResult":{}}`)) + _, _ = w.Write([]byte(`{"itemResults":{}}`)) } _, _, sv := newServers(t, tokenHandler, apiHandler) @@ -128,7 +128,7 @@ func TestClientRequestSendsExpectedHeadersAndBody(t *testing.T) { if err != nil { t.Fatalf("RequestContext: %v", err) } - if got, want := string(body), `{"itemsResult":{}}`; got != want { + if got, want := string(body), `{"itemResults":{}}`; got != want { t.Errorf("response body = %q, want %q", got, want) } diff --git a/entity/entity.go b/entity/entity.go index dc96728..798bbc3 100644 --- a/entity/entity.go +++ b/entity/entity.go @@ -93,7 +93,6 @@ type Item struct { ASIN string ParentASIN string DetailPageURL string - Score *float64 `json:",omitempty"` CustomerReviews *struct { Count *int `json:",omitempty"` StarRating *struct { @@ -109,7 +108,6 @@ type Item struct { SalesRank *int `json:",omitempty"` Ancestor *Ancestor `json:",omitempty"` WebsiteSalesRank *struct { - Id string `json:"id,omitempty"` DisplayName string ContextFreeName string SalesRank int @@ -296,8 +294,8 @@ type Item struct { Percentage int } `json:",omitempty"` } `json:",omitempty"` - Type string `json:",omitempty"` - ViolatesMAP bool `json:"violatesMAP,omitempty"` + Type string `json:",omitempty"` + ViolateMAP bool } `json:",omitempty"` } `json:",omitempty"` } @@ -319,7 +317,6 @@ type Price struct { type VariationDimension struct { DisplayName string - Locale string `json:",omitempty"` Name string Values []string } @@ -329,9 +326,14 @@ type Response struct { Code string Message string } `json:",omitempty"` + // The Creators API serialises this container as `itemResults` + // (item singular, results plural), not `itemsResult`. The Go field + // keeps the historical PA-API v5 name for source compatibility, but + // the explicit JSON tag is required: Go's case-insensitive json + // matching does not bridge the singular/plural difference. ItemsResult *struct { Items []Item `json:",omitempty"` - } `json:",omitempty"` + } `json:"itemResults,omitempty"` SearchResult *struct { Items []Item `json:",omitempty"` SearchRefinements *struct { @@ -366,7 +368,6 @@ type Response struct { DisplayName string ContextFreeName string IsRoot bool - SalesRank *int `json:",omitempty"` } `json:",omitempty"` } `json:",omitempty"` } diff --git a/entity/entity_test.go b/entity/entity_test.go index 22dc351..81a7064 100644 --- a/entity/entity_test.go +++ b/entity/entity_test.go @@ -15,7 +15,6 @@ func TestDecodeGetVariationsResponse(t *testing.T) { "asin": "B07YCM5K55", "parentASIN": "B07YCM5JXX", "detailPageURL": "https://www.amazon.com/dp/B07YCM5K55", - "score": 0.42, "images": { "primary": { "small": {"url": "https://example.test/s.jpg", "height": 75, "width": 75}, @@ -27,7 +26,6 @@ func TestDecodeGetVariationsResponse(t *testing.T) { "listings": [ { "isBuyBoxWinner": true, - "violatesMAP": true, "type": "New" } ] @@ -42,8 +40,8 @@ func TestDecodeGetVariationsResponse(t *testing.T) { "lowestPrice": {"amount": 19.99, "currency": "USD", "displayAmount": "$19.99"} }, "variationDimensions": [ - {"displayName": "Size", "locale": "en_US", "name": "size_name", "values": ["S", "M", "L"]}, - {"displayName": "Color", "locale": "en_US", "name": "color_name", "values": ["Red", "Blue"]} + {"displayName": "Size", "name": "size_name", "values": ["S", "M", "L"]}, + {"displayName": "Color", "name": "color_name", "values": ["Red", "Blue"]} ] } } @@ -77,13 +75,10 @@ func TestDecodeGetVariationsResponse(t *testing.T) { if got, want := len(vs.VariationDimensions), 2; got != want { t.Fatalf("len(VariationDimensions) = %d, want %d", got, want) } - if got, want := vs.VariationDimensions[0].Locale, "en_US"; got != want { - t.Errorf("VariationDimensions[0].Locale = %q, want %q", got, want) - } if got, want := vs.VariationDimensions[0].Name, "size_name"; got != want { t.Errorf("VariationDimensions[0].Name = %q, want %q", got, want) } - // Items: primary image and isBuyBoxWinner / violatesMAP on V2 listing. + // Items: primary image and isBuyBoxWinner on V2 listing. if got, want := len(resp.VariationsResult.Items), 1; got != want { t.Fatalf("len(Items) = %d, want %d", got, want) } @@ -101,23 +96,61 @@ func TestDecodeGetVariationsResponse(t *testing.T) { if !listing.IsBuyboxWinner { t.Errorf("isBuyBoxWinner did not decode into IsBuyboxWinner") } - if !listing.ViolatesMAP { - t.Errorf("violatesMAP did not decode into ViolatesMAP") - } } -// TestDecodeGetBrowseNodesResponseWithSalesRank verifies the new SalesRank -// field on the top-level BrowseNodesResult.BrowseNodes. -func TestDecodeGetBrowseNodesResponseWithSalesRank(t *testing.T) { +// TestDecodeRealGetBrowseNodesSampleResponse pins the decode contract for +// GetBrowseNodes against a sample captured from the live Creators API. +// Exercises both the recursive `ancestor` chain (Mexico → Explore the World +// → Geography & Cultures → Children's Books → Subjects → Books) and the +// `children[]` list returned for root nodes. The top-level container is +// `browseNodesResult` (browseNodes plural, result singular) — matches the +// shape used by GetVariations rather than the unique `itemResults` plural +// used only by GetItems. +func TestDecodeRealGetBrowseNodesSampleResponse(t *testing.T) { body := []byte(`{ "browseNodesResult": { "browseNodes": [ { + "ancestor": { + "ancestor": { + "ancestor": { + "ancestor": { + "ancestor": { + "contextFreeName": "Books", + "displayName": "Books", + "id": "283155" + }, + "contextFreeName": "Subjects", + "displayName": "Subjects", + "id": "1000" + }, + "contextFreeName": "Children's Books", + "displayName": "Children's Books", + "id": "4" + }, + "contextFreeName": "Children's Geography & Cultures Books", + "displayName": "Geography & Cultures", + "id": "3344091011" + }, + "contextFreeName": "Children's Explore the World Books", + "displayName": "Explore the World", + "id": "3023" + }, + "contextFreeName": "Children's Mexico Books", + "displayName": "Mexico", "id": "3040", - "displayName": "Books", + "isRoot": false + }, + { + "children": [ + {"contextFreeName": "Subjects", "displayName": "Subjects", "id": "1000"}, + {"contextFreeName": "Books Featured Categories", "displayName": "Books Featured Categories", "id": "51546011"}, + {"contextFreeName": "Specialty Boutique", "displayName": "Specialty Boutique", "id": "2349030011"} + ], "contextFreeName": "Books", - "isRoot": true, - "salesRank": 12345 + "displayName": "Books", + "id": "283155", + "isRoot": true } ] } @@ -126,23 +159,71 @@ func TestDecodeGetBrowseNodesResponseWithSalesRank(t *testing.T) { if err != nil { t.Fatalf("DecodeResponse: %+v", err) } - if resp.BrowseNodesResult == nil || len(resp.BrowseNodesResult.BrowseNodes) != 1 { - t.Fatal("BrowseNodesResult.BrowseNodes empty") + if resp.BrowseNodesResult == nil { + t.Fatal("BrowseNodesResult is nil") } - bn := resp.BrowseNodesResult.BrowseNodes[0] - if bn.SalesRank == nil { - t.Fatal("BrowseNode.SalesRank is nil") + if got, want := len(resp.BrowseNodesResult.BrowseNodes), 2; got != want { + t.Fatalf("len(BrowseNodes) = %d, want %d", got, want) } - if got, want := *bn.SalesRank, 12345; got != want { - t.Errorf("BrowseNode.SalesRank = %d, want %d", got, want) + + // First node: Mexico, with a 5-level ancestor chain ending at the + // Books root. + mexico := resp.BrowseNodesResult.BrowseNodes[0] + if got, want := mexico.Id, "3040"; got != want { + t.Errorf("Mexico.Id = %q, want %q", got, want) + } + if got, want := mexico.DisplayName, "Mexico"; got != want { + t.Errorf("Mexico.DisplayName = %q, want %q", got, want) + } + if mexico.IsRoot { + t.Errorf("Mexico.IsRoot = true, want false") + } + // Walk the ancestor chain. + expected := []struct{ id, name string }{ + {"3023", "Explore the World"}, + {"3344091011", "Geography & Cultures"}, + {"4", "Children's Books"}, + {"1000", "Subjects"}, + {"283155", "Books"}, + } + cur := mexico.Ancestor + for i, want := range expected { + if cur == nil { + t.Fatalf("ancestor chain truncated at depth %d (expected %+v)", i, want) + } + if cur.Id != want.id || cur.DisplayName != want.name { + t.Errorf("ancestor[%d] = (%q, %q), want (%q, %q)", i, cur.Id, cur.DisplayName, want.id, want.name) + } + cur = cur.Ancestor + } + if cur != nil { + t.Errorf("ancestor chain has extra node beyond root: %+v", cur) + } + + // Second node: Books root, with three children. + books := resp.BrowseNodesResult.BrowseNodes[1] + if got, want := books.Id, "283155"; got != want { + t.Errorf("Books.Id = %q, want %q", got, want) + } + if !books.IsRoot { + t.Errorf("Books.IsRoot = false, want true") + } + if got, want := len(books.Children), 3; got != want { + t.Fatalf("len(Books.Children) = %d, want %d", got, want) + } + if got, want := books.Children[2].DisplayName, "Specialty Boutique"; got != want { + t.Errorf("Books.Children[2].DisplayName = %q, want %q", got, want) + } + if got, want := books.Children[1].Id, "51546011"; got != want { + t.Errorf("Books.Children[1].Id = %q, want %q", got, want) } } -// TestDecodeBrowseNodeInfoWebsiteSalesRankID exercises the websiteSalesRank.id -// field exposed by the Creators API. -func TestDecodeBrowseNodeInfoWebsiteSalesRankID(t *testing.T) { +// TestDecodeBrowseNodeInfoWebsiteSalesRank exercises the websiteSalesRank +// nested block carried inside an item's browseNodeInfo. +func TestDecodeBrowseNodeInfoWebsiteSalesRank(t *testing.T) { body := []byte(`{ - "itemsResult": { + "itemResults": { "items": [ { "asin": "B0", @@ -153,7 +234,6 @@ func TestDecodeBrowseNodeInfoWebsiteSalesRankID(t *testing.T) { "displayName": "Books", "contextFreeName": "Books", "websiteSalesRank": { - "id": "1000", "displayName": "Books", "contextFreeName": "Books", "salesRank": 7 @@ -180,8 +260,8 @@ func TestDecodeBrowseNodeInfoWebsiteSalesRankID(t *testing.T) { if wsr == nil { t.Fatal("WebsiteSalesRank is nil") } - if got, want := wsr.Id, "1000"; got != want { - t.Errorf("WebsiteSalesRank.Id = %q, want %q", got, want) + if got, want := wsr.SalesRank, 7; got != want { + t.Errorf("WebsiteSalesRank.SalesRank = %d, want %d", got, want) } } @@ -189,7 +269,7 @@ func TestDecodeBrowseNodeInfoWebsiteSalesRankID(t *testing.T) { // cause DecodeResponse to error (relevant if the Creators API adds new // fields between SDK releases). func TestDecodeIgnoresUnknownFields(t *testing.T) { - body := []byte(`{"itemsResult":{"items":[{"asin":"A","unknownField":42}]},"newTopLevel":"ok"}`) + body := []byte(`{"itemResults":{"items":[{"asin":"A","unknownField":42}]},"newTopLevel":"ok"}`) if _, err := DecodeResponse(body); err != nil { t.Fatalf("DecodeResponse rejected unknown field: %+v", err) } @@ -199,6 +279,249 @@ func TestDecodeIgnoresUnknownFields(t *testing.T) { } } +// TestDecodeRealGetVariationsSampleResponse pins the decode contract for +// GetVariations against a sample captured from the live Creators API. +// Notably: +// - the top-level container is `variationsResult` (variations plural, +// result singular) — this differs from GetItems' `itemResults` +// (item singular, results plural). +// - the `Price` block in `variationSummary` is sometimes returned with a +// capitalised key; case-insensitive decode covers it. +// - `variationDimensions[]` does NOT include a `locale` field; ensures +// the entity stays minimal. +func TestDecodeRealGetVariationsSampleResponse(t *testing.T) { + body := []byte(`{ + "variationsResult": { + "items": [ + { + "asin": "B019MNBMS4", + "detailPageURL": "https://www.amazon.co.uk/dp/B019MNBMS4?tag=xyz-20&linkCode=ogv&th=1&psc=1", + "itemInfo": { + "title": { + "displayValue": "Tommy Hilfiger Men's Ranger Leather Passcase Wallet", + "label": "title", + "locale": "en_GB" + } + }, + "variationAttributes": [ + {"name": "size_name", "value": "One Size"}, + {"name": "color_name", "value": "Navy"} + ] + }, + { + "asin": "B073211XCB", + "detailPageURL": "https://www.amazon.co.uk/dp/B073211XCB", + "itemInfo": { + "title": { + "displayValue": "Tommy Hilfiger mens RFID Blocking Wallet", + "label": "title", + "locale": "en_GB" + } + }, + "variationAttributes": [ + {"name": "size_name", "value": "One Size"}, + {"name": "color_name", "value": "Logan - Tan"} + ] + } + ], + "variationSummary": { + "pageCount": 2, + "Price": { + "highestPrice": {"amount": 30.87, "currency": "GBP", "displayAmount": "£30.87"}, + "lowestPrice": {"amount": 17.03, "currency": "GBP", "displayAmount": "£17.03"} + }, + "variationCount": 13, + "variationDimensions": [ + {"displayName": "Size", "name": "size_name", "values": ["One Size"]}, + {"displayName": "Colour", "name": "color_name", "values": ["Brown", "Navy", "Black", "Burgundy", "Cognac", "Gray", "Green", "Logan - Tan", "Navy/Black", "Red", "Rfid-black", "Rfid-navy", "Tan"]} + ] + } + } +}`) + resp, err := DecodeResponse(body) + if err != nil { + t.Fatalf("DecodeResponse: %+v", err) + } + if resp.VariationsResult == nil { + t.Fatal("VariationsResult is nil") + } + // Items decode end-to-end with title + variationAttributes. + if got, want := len(resp.VariationsResult.Items), 2; got != want { + t.Fatalf("len(Items) = %d, want %d", got, want) + } + first := resp.VariationsResult.Items[0] + if got, want := first.ASIN, "B019MNBMS4"; got != want { + t.Errorf("Items[0].ASIN = %q, want %q", got, want) + } + if first.ItemInfo == nil || first.ItemInfo.Title == nil { + t.Fatal("Items[0].ItemInfo.Title is nil") + } + if got, want := first.ItemInfo.Title.Locale, "en_GB"; got != want { + t.Errorf("Items[0].ItemInfo.Title.Locale = %q, want %q", got, want) + } + if got, want := len(first.VariationAttributes), 2; got != want { + t.Fatalf("Items[0].VariationAttributes len = %d, want %d", got, want) + } + if got, want := first.VariationAttributes[1].Name, "color_name"; got != want { + t.Errorf("Items[0].VariationAttributes[1].Name = %q, want %q", got, want) + } + if got, want := first.VariationAttributes[1].Value, "Navy"; got != want { + t.Errorf("Items[0].VariationAttributes[1].Value = %q, want %q", got, want) + } + // VariationSummary: pageCount/variationCount/Price/variationDimensions. + vs := resp.VariationsResult.VariationSummary + if vs == nil { + t.Fatal("VariationSummary is nil") + } + if vs.PageCount != 2 { + t.Errorf("VariationSummary.PageCount = %d, want 2", vs.PageCount) + } + if vs.VariationCount != 13 { + t.Errorf("VariationSummary.VariationCount = %d, want 13", vs.VariationCount) + } + if vs.Price == nil || vs.Price.HighestPrice == nil || vs.Price.LowestPrice == nil { + t.Fatal("VariationSummary.Price.{HighestPrice,LowestPrice} missing (capitalized 'Price' key did not decode)") + } + if got, want := vs.Price.HighestPrice.Currency, "GBP"; got != want { + t.Errorf("Highest currency = %q, want %q", got, want) + } + if got, want := vs.Price.LowestPrice.DisplayAmount, "£17.03"; got != want { + t.Errorf("Lowest displayAmount = %q, want %q", got, want) + } + if got, want := len(vs.VariationDimensions), 2; got != want { + t.Fatalf("len(VariationDimensions) = %d, want %d", got, want) + } + if got, want := vs.VariationDimensions[1].DisplayName, "Colour"; got != want { + t.Errorf("VariationDimensions[1].DisplayName = %q, want %q", got, want) + } + if got, want := len(vs.VariationDimensions[1].Values), 13; got != want { + t.Errorf("VariationDimensions[1].Values count = %d, want %d", got, want) + } +} + +// TestDecodeRealGetItemsSampleResponse pins the decode contract against an +// actual GetItems response captured from the Creators API. Notably the +// top-level container is `itemResults` (item singular, results plural) — not +// the `itemsResult` shape used by PA-API v5 / the third-party Python SDK. +func TestDecodeRealGetItemsSampleResponse(t *testing.T) { + body := []byte(`{ + "errors": [ + { + "code": "ItemNotAccessible", + "message": "The ItemId B01180YUXS is not accessible through the Creators API." + } + ], + "itemResults": { + "items": [ + { + "asin": "B0199980K4", + "detailPageURL": "https://www.amazon.com/dp/B0199980K4?tag=xyz-20&linkCode=ogi&language=en_US&th=1&psc=1", + "images": { + "primary": { + "small": { + "height": 75, + "url": "https://m.media-amazon.com/images/I/61s4tTAizUL._SL75_.jpg", + "width": 56 + } + } + }, + "itemInfo": { + "title": { + "displayValue": "Genghis: The Legend of the Ten", + "label": "Title", + "locale": "en_US" + } + }, + "parentASIN": "B07QGKM68X" + }, + { + "asin": "B00BKQTA4A", + "detailPageURL": "https://www.amazon.com/dp/B00BKQTA4A?tag=xyz-20&linkCode=ogi&language=en_US&th=1&psc=1", + "images": { + "primary": { + "small": { + "height": 75, + "url": "https://m.media-amazon.com/images/I/41OiLOcQVJL._SL75_.jpg", + "width": 46 + } + } + }, + "itemInfo": { + "features": { + "displayValues": [ + "Round watch featuring logoed white dial with stick indices", + "36 mm stainless steel case with mineral dial window", + "Quartz movement with analog display", + "Leather calfskin band with buckle closure", + "Water resistant to 30 m (99 ft): In general, withstands splashes or brief immersion in water, but not suitable for swimming" + ], + "label": "Features", + "locale": "en_US" + }, + "title": { + "displayValue": "Daniel Wellington Women's 0608DW Sheffield Stainless Steel Watch", + "label": "Title", + "locale": "en_US" + } + }, + "parentASIN": "B07L5N7P32" + } + ] + } +}`) + resp, err := DecodeResponse(body) + if err != nil { + t.Fatalf("DecodeResponse: %+v", err) + } + // Errors block populates and preserves order/content. + if got, want := len(resp.Errors), 1; got != want { + t.Fatalf("len(Errors) = %d, want %d", got, want) + } + if got, want := resp.Errors[0].Code, "ItemNotAccessible"; got != want { + t.Errorf("Errors[0].Code = %q, want %q", got, want) + } + // itemResults must decode (regression: previously absent). + if resp.ItemsResult == nil { + t.Fatal("ItemsResult is nil; itemResults JSON tag failed to decode") + } + if got, want := len(resp.ItemsResult.Items), 2; got != want { + t.Fatalf("len(Items) = %d, want %d", got, want) + } + first := resp.ItemsResult.Items[0] + if got, want := first.ASIN, "B0199980K4"; got != want { + t.Errorf("Items[0].ASIN = %q, want %q", got, want) + } + if got, want := first.ParentASIN, "B07QGKM68X"; got != want { + t.Errorf("Items[0].ParentASIN = %q, want %q", got, want) + } + if got, want := first.DetailPageURL, "https://www.amazon.com/dp/B0199980K4?tag=xyz-20&linkCode=ogi&language=en_US&th=1&psc=1"; got != want { + t.Errorf("Items[0].DetailPageURL = %q, want %q", got, want) + } + if first.Images == nil || first.Images.Primary == nil || first.Images.Primary.Small == nil { + t.Fatal("Items[0].Images.Primary.Small is nil") + } + if got, want := first.Images.Primary.Small.URL, "https://m.media-amazon.com/images/I/61s4tTAizUL._SL75_.jpg"; got != want { + t.Errorf("Items[0] small image URL = %q, want %q", got, want) + } + if first.ItemInfo == nil || first.ItemInfo.Title == nil { + t.Fatal("Items[0].ItemInfo.Title is nil") + } + if got, want := first.ItemInfo.Title.DisplayValue, "Genghis: The Legend of the Ten"; got != want { + t.Errorf("Items[0].ItemInfo.Title.DisplayValue = %q, want %q", got, want) + } + // Second item exercises the features block (IdInfo). + second := resp.ItemsResult.Items[1] + if second.ItemInfo == nil || second.ItemInfo.Features == nil { + t.Fatal("Items[1].ItemInfo.Features is nil") + } + if got, want := len(second.ItemInfo.Features.DisplayValues), 5; got != want { + t.Errorf("Items[1].ItemInfo.Features.DisplayValues count = %d, want %d", got, want) + } + if got, want := second.ItemInfo.Features.Label, "Features"; got != want { + t.Errorf("Items[1].ItemInfo.Features.Label = %q, want %q", got, want) + } +} + /* Copyright 2026 goark contributors * * Licensed under the Apache License, Version 2.0 (the "License"); From ffb6ffdae67f15338e62cdff513d782f4564b2bb Mon Sep 17 00:00:00 2001 From: Tanner Oakes Date: Wed, 6 May 2026 14:40:57 -0600 Subject: [PATCH 4/4] remove comment --- entity/entity.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/entity/entity.go b/entity/entity.go index 798bbc3..4e8d7bc 100644 --- a/entity/entity.go +++ b/entity/entity.go @@ -326,11 +326,6 @@ type Response struct { Code string Message string } `json:",omitempty"` - // The Creators API serialises this container as `itemResults` - // (item singular, results plural), not `itemsResult`. The Go field - // keeps the historical PA-API v5 name for source compatibility, but - // the explicit JSON tag is required: Go's case-insensitive json - // matching does not bridge the singular/plural difference. ItemsResult *struct { Items []Item `json:",omitempty"` } `json:"itemResults,omitempty"`