Skip to content

Commit 94e5d73

Browse files
Merge pull request #84 from richardthe3rd/claude/fix-beer-categories-013WJ2MyMGWNHTZPGvxwhSHt
Nnew dynamic API endpoint to the Cloudflare Worker for discovering available beverage types for each festival
2 parents 9d30dd8 + 9ba536a commit 94e5d73

3 files changed

Lines changed: 157 additions & 3 deletions

File tree

cloudflare-worker/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,68 @@ The GitHub Actions workflow automatically deploys the worker on push to `main`.
4747
- Expired or revoked
4848
- Missing required permissions (needs "Workers Scripts: Edit" at minimum)
4949

50+
## API Endpoints
51+
52+
The worker provides several endpoints:
53+
54+
### Proxy Endpoints
55+
56+
Proxies requests to `data.cambridgebeerfestival.com` with CORS headers:
57+
58+
- `/{festivalId}/{beverageType}.json` - Get beverage data (e.g., `/cbf2025/beer.json`)
59+
60+
### Metadata Endpoints
61+
62+
Dynamic API endpoints that provide festival metadata:
63+
64+
- `/festivals.json` - Returns the festivals registry with all festival metadata
65+
- `/{festivalId}/available_beverage_types.json` - **NEW!** Dynamically discovers available beverage types for a festival
66+
67+
Example:
68+
```bash
69+
# Get available beverage types for CBF 2025
70+
curl https://cbf-data-proxy.<your-subdomain>.workers.dev/cbf2025/available_beverage_types.json
71+
72+
# Response:
73+
{
74+
"festival_id": "cbf2025",
75+
"available_beverage_types": [
76+
"apple-juice",
77+
"beer",
78+
"cider",
79+
"international-beer",
80+
"low-no",
81+
"mead",
82+
"perry",
83+
"wine"
84+
],
85+
"timestamp": "2025-12-02T10:30:00.000Z"
86+
}
87+
```
88+
89+
This endpoint:
90+
- Dynamically fetches the directory listing from the upstream API
91+
- Parses the HTML to find all `.json` files
92+
- Returns them as a sorted array
93+
- Caches the result for 1 hour
94+
95+
### Health Check
96+
97+
- `/health` - Returns `{"status": "ok"}` for monitoring
98+
5099
## Testing
51100

52101
After deployment, test the proxy:
53102

54103
```bash
104+
# Test beverage data proxy
55105
curl https://cbf-data-proxy.<your-subdomain>.workers.dev/cbf2025/beer.json
106+
107+
# Test dynamic beverage types discovery
108+
curl https://cbf-data-proxy.<your-subdomain>.workers.dev/cbf2025/available_beverage_types.json
109+
110+
# Test festivals registry
111+
curl https://cbf-data-proxy.<your-subdomain>.workers.dev/festivals.json
56112
```
57113

58114
## Updating the Flutter App

cloudflare-worker/worker.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ export default {
5858
});
5959
}
6060

61+
// Handle dynamic available_beverage_types.json endpoint
62+
// Pattern: /{festivalId}/available_beverage_types.json
63+
const availableTypesMatch = url.pathname.match(/^\/([^\/]+)\/available_beverage_types\.json$/);
64+
if (availableTypesMatch) {
65+
return handleAvailableBeverageTypes(availableTypesMatch[1], request);
66+
}
67+
6168
// Proxy the request to the upstream API
6269
const upstreamUrl = UPSTREAM_URL + url.pathname + url.search;
6370

@@ -90,6 +97,99 @@ export default {
9097
},
9198
};
9299

100+
/**
101+
* Dynamically discovers available beverage types for a festival
102+
* by fetching the directory listing from the upstream API
103+
*
104+
* @param {string} festivalId - The festival ID (e.g., 'cbf2025', 'cbfw2025')
105+
* @param {Request} request - The original request for CORS handling
106+
* @returns {Response} JSON response with available beverage types
107+
*/
108+
async function handleAvailableBeverageTypes(festivalId, request) {
109+
try {
110+
// Fetch the directory listing for this festival
111+
const upstreamUrl = `${UPSTREAM_URL}/${festivalId}/`;
112+
const response = await fetch(upstreamUrl, {
113+
headers: {
114+
'User-Agent': 'Cambridge-Beer-Festival-App-Proxy/1.0',
115+
},
116+
});
117+
118+
if (!response.ok) {
119+
return new Response(JSON.stringify({
120+
error: 'Festival not found',
121+
festival_id: festivalId,
122+
}), {
123+
status: 404,
124+
headers: {
125+
'Content-Type': 'application/json',
126+
...getCorsHeaders(request),
127+
},
128+
});
129+
}
130+
131+
// Parse the HTML directory listing to find .json files
132+
const html = await response.text();
133+
const beverageTypes = parseDirectoryListingForBeverageTypes(html);
134+
135+
// Return the list of available beverage types
136+
return new Response(JSON.stringify({
137+
festival_id: festivalId,
138+
available_beverage_types: beverageTypes,
139+
timestamp: new Date().toISOString(),
140+
}), {
141+
status: 200,
142+
headers: {
143+
'Content-Type': 'application/json',
144+
'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
145+
...getCorsHeaders(request),
146+
},
147+
});
148+
} catch (error) {
149+
return new Response(JSON.stringify({
150+
error: 'Failed to fetch beverage types',
151+
message: error.message,
152+
}), {
153+
status: 500,
154+
headers: {
155+
'Content-Type': 'application/json',
156+
...getCorsHeaders(request),
157+
},
158+
});
159+
}
160+
}
161+
162+
/**
163+
* Parses an Apache-style directory listing HTML to extract beverage type JSON files
164+
*
165+
* @param {string} html - The HTML content of the directory listing
166+
* @returns {string[]} Array of beverage type names (without .json extension)
167+
*/
168+
function parseDirectoryListingForBeverageTypes(html) {
169+
const beverageTypes = [];
170+
171+
// Match href attributes that point to .json files
172+
// Regex pattern: <a href="filename.json">
173+
const jsonFilePattern = /<a href="([^"]+\.json)"/gi;
174+
let match;
175+
176+
while ((match = jsonFilePattern.exec(html)) !== null) {
177+
const filename = match[1];
178+
179+
// Skip the available_beverage_types.json itself to avoid recursion
180+
if (filename === 'available_beverage_types.json') {
181+
continue;
182+
}
183+
184+
// Remove .json extension to get the beverage type name
185+
const beverageType = filename.replace(/\.json$/, '');
186+
beverageTypes.push(beverageType);
187+
}
188+
189+
// Sort alphabetically for consistency
190+
return beverageTypes.sort();
191+
}
192+
93193
function handleCorsPreflight(request) {
94194
return new Response(null, {
95195
status: 204,

data/festivals.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"perry",
2828
"mead",
2929
"wine",
30+
"apple-juice",
3031
"low-no"
3132
],
3233
"data_base_url": "https://cbf-data-proxy.richard-alcock.workers.dev/cbf2025",
@@ -52,9 +53,6 @@
5253
},
5354
"available_beverage_types": [
5455
"beer",
55-
"international-beer",
56-
"cider",
57-
"perry",
5856
"low-no"
5957
],
6058
"data_base_url": "https://cbf-data-proxy.richard-alcock.workers.dev/cbfw2025",

0 commit comments

Comments
 (0)