Skip to content

Commit 4911b7c

Browse files
committed
feat(examples): feed export
1 parent 8bf3210 commit 4911b7c

5 files changed

Lines changed: 161 additions & 7 deletions

File tree

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ This repo bootstraps Go support for the [Agentic Commerce Protocol](https://deve
1414

1515
- **Checkout API** — plug your own business logic into `NewCheckoutHandler` by implementing `CheckoutSessionService`. The handler exposes the official ACP checkout contract over `net/http`, supports optional signature verification and timestamp skew enforcement, and emits typed responses generated from the OpenAPI spec.
1616
- **Delegated Payment API** — payment service providers implement `DelegatedPaymentProvider` and wire it up via `NewDelegatedPaymentHandler` (optionally adding `DelegatedPaymentWithAuthenticator` and signature enforcement) to tokenize credentials and emit delegated vault tokens.
17+
- **Product Feed** - helper for building and exporting a [product feed](https://developers.openai.com/commerce/specs/feed).
1718

18-
## Example Servers
19+
## Examples
1920

20-
Two runnable samples live under [`examples`](examples):
21+
Three runnable samples live under [`examples`](examples):
2122

2223
- [`examples/checkout`](examples/checkout) implements `CheckoutSessionService` with an in-memory catalog and session store.
2324
- [`examples/delegated_payment`](examples/delegated_payment) implements `DelegatedPaymentProvider` with an in-memory vault token map.
25+
- [`examples/feed`](examples/feed) builds and exports a product feed in JSONL and CSV formats.
2426

25-
### Checkout sample
27+
### Checkout Sample
2628

2729
```bash
2830
go run ./examples/checkout
@@ -67,7 +69,7 @@ export ACP_WEBHOOK_SECRET="super-secret"
6769
go run ./examples/checkout
6870
```
6971

70-
### Delegated payment sample
72+
### Delegated Payment Sample
7173

7274
```bash
7375
go run ./examples/delegated_payment
@@ -104,6 +106,14 @@ curl -sS -X POST http://localhost:8080/agentic_commerce/delegate_payment \
104106
}'
105107
```
106108

109+
### Product Feed Sample
110+
111+
```bash
112+
go run ./examples/feed
113+
```
114+
115+
This writes compressed feed exports to `examples/feed/output/product_feed.jsonl.gz` and `examples/feed/output/product_feed.csv.gz`.
116+
107117
## License
108118

109119
[Apache 2.0](/LICENSE)

examples/feed/feed.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/sumup/acp/feed"
10+
)
11+
12+
func main() {
13+
products := sampleProducts()
14+
productFeed := feed.New(products)
15+
16+
outDir := filepath.Join("examples", "feed", "output")
17+
if err := os.MkdirAll(outDir, 0o755); err != nil {
18+
log.Fatalf("create output directory: %v", err)
19+
}
20+
21+
jsonlPath := filepath.Join(outDir, "product_feed.jsonl.gz")
22+
if err := writeJSONL(productFeed, jsonlPath); err != nil {
23+
log.Fatalf("write jsonl feed: %v", err)
24+
}
25+
26+
csvPath := filepath.Join(outDir, "product_feed.csv.gz")
27+
if err := writeCSV(productFeed, csvPath); err != nil {
28+
log.Fatalf("write csv feed: %v", err)
29+
}
30+
31+
log.Printf("wrote %s", jsonlPath)
32+
log.Printf("wrote %s", csvPath)
33+
}
34+
35+
func writeJSONL(productFeed feed.Feed, path string) error {
36+
file, err := os.Create(path)
37+
if err != nil {
38+
return fmt.Errorf("create file: %w", err)
39+
}
40+
defer func() {
41+
_ = file.Close()
42+
}()
43+
44+
return productFeed.WriteJSONLGz(file)
45+
}
46+
47+
func writeCSV(productFeed feed.Feed, path string) error {
48+
file, err := os.Create(path)
49+
if err != nil {
50+
return fmt.Errorf("create file: %w", err)
51+
}
52+
defer func() {
53+
_ = file.Close()
54+
}()
55+
56+
return productFeed.WriteCSVGz(file)
57+
}
58+
59+
func sampleProducts() []feed.Product {
60+
return []feed.Product{
61+
{
62+
EnableSearch: true,
63+
EnableCheckout: true,
64+
ID: "sku_latte_12oz",
65+
Title: "Oat Milk Latte (12oz)",
66+
Description: "Smooth espresso with steamed oat milk.",
67+
Link: "https://store.example.com/products/sku_latte_12oz",
68+
ProductCategory: "Food, Beverages & Tobacco > Beverages > Coffee",
69+
ImageLink: "https://store.example.com/images/sku_latte_12oz.jpg",
70+
Price: "6.50 USD",
71+
Availability: "in_stock",
72+
SellerName: "Example Roasters",
73+
SellerURL: "https://store.example.com",
74+
InventoryQuantity: intPtr(120),
75+
Shipping: []string{
76+
"US:CA:Ground:5.00 USD",
77+
"US:NY:Ground:6.00 USD",
78+
},
79+
PickupMethod: "in_store",
80+
PickupSLA: "1 day",
81+
},
82+
{
83+
EnableSearch: true,
84+
EnableCheckout: true,
85+
ID: "sku_mug_slate",
86+
Title: "Stoneware Mug (Slate)",
87+
Description: "12oz stoneware mug with matte finish.",
88+
Link: "https://store.example.com/products/sku_mug_slate",
89+
ProductCategory: "Home & Garden > Kitchen & Dining > Drinkware",
90+
ImageLink: "https://store.example.com/images/sku_mug_slate.jpg",
91+
AdditionalImageLink: []string{
92+
"https://store.example.com/images/sku_mug_slate_alt1.jpg",
93+
"https://store.example.com/images/sku_mug_slate_alt2.jpg",
94+
},
95+
Price: "15.00 USD",
96+
SalePrice: "12.00 USD",
97+
Availability: "in_stock",
98+
Condition: feed.ProductConditionNew,
99+
Brand: "Example Roasters",
100+
Material: "Stoneware",
101+
Dimensions: "4x4x4 in",
102+
Shipping: []string{
103+
"US:::7.00 USD",
104+
},
105+
SellerName: "Example Roasters",
106+
SellerURL: "https://store.example.com",
107+
},
108+
{
109+
EnableSearch: true,
110+
EnableCheckout: true,
111+
ID: "sku_shirt_black_s",
112+
Title: "Logo T-Shirt (Black, S)",
113+
Description: "Soft cotton tee with embroidered logo.",
114+
Link: "https://store.example.com/products/sku_shirt_black_s",
115+
ProductCategory: "Apparel & Accessories > Clothing",
116+
ImageLink: "https://store.example.com/images/sku_shirt_black.jpg",
117+
Price: "24.00 USD",
118+
Availability: "in_stock",
119+
ItemGroupID: "sku_shirt_black",
120+
ItemGroupTitle: "Logo T-Shirt (Black)",
121+
Color: "Black",
122+
Size: "S",
123+
SizeSystem: "US",
124+
Gender: "unisex",
125+
SellerName: "Example Roasters",
126+
SellerURL: "https://store.example.com",
127+
},
128+
}
129+
}
130+
131+
func intPtr(v int) *int {
132+
return &v
133+
}

feed/csv.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func (p Product) csvRecord() []string {
102102
p.Width,
103103
p.Height,
104104
p.Weight,
105-
p.AgeGroup,
105+
string(p.AgeGroup),
106106
p.ImageLink,
107107
joinStrings(p.AdditionalImageLink),
108108
p.VideoLink,

feed/feed.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ const (
2222
ProductConditionUsed ProductCondition = "used"
2323
)
2424

25+
// ProductAgeGroup is the target demographic of the product.
26+
type ProductAgeGroup string
27+
28+
const (
29+
ProductAgeGroupNewborn ProductAgeGroup = "newborn"
30+
ProductAgeGroupInfant ProductAgeGroup = "infant"
31+
ProductAgeGroupToddler ProductAgeGroup = "toddler"
32+
ProductAgeGroupKids ProductAgeGroup = "kids"
33+
ProductAgeGroupAdult ProductAgeGroup = "adult"
34+
)
35+
2536
// Product describes a single product entry in an ACP product feed.
2637
// Field names follow the feed specification for JSONL and CSV export.
2738
type Product struct {
@@ -86,7 +97,7 @@ type Product struct {
8697
// Weight is the product weight with a unit.
8798
Weight string `json:"weight,omitempty" csv:"weight"`
8899
// AgeGroup is the target demographic such as newborn, infant, toddler, kids, or adult.
89-
AgeGroup string `json:"age_group,omitempty" csv:"age_group"`
100+
AgeGroup ProductAgeGroup `json:"age_group,omitempty" csv:"age_group"`
90101

91102
// Media
92103
//

feed/feed_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func gunzipBytes(data []byte) ([]byte, error) {
5959
if err != nil {
6060
return nil, err
6161
}
62-
defer reader.Close()
62+
defer func() { _ = reader.Close() }()
6363

6464
return io.ReadAll(reader)
6565
}

0 commit comments

Comments
 (0)