Skip to content

Commit 9ba536a

Browse files
committed
Add dynamic beverage types discovery endpoint to Cloudflare Worker
Added a new API endpoint that dynamically discovers available beverage types for a given festival by parsing the upstream directory listing. New endpoint: - GET /{festivalId}/available_beverage_types.json Features: - Fetches directory listing from data.cambridgebeerfestival.com - Parses HTML to extract all .json files - Returns sorted array of beverage type names - Includes CORS headers for web app access - Caches responses for 1 hour - Returns 404 for non-existent festivals This endpoint eliminates the need to manually maintain beverage type lists in festivals.json. The app can now query the API to discover what's actually available for each festival. Example response: { "festival_id": "cbf2025", "available_beverage_types": [ "apple-juice", "beer", "cider", "international-beer", "low-no", "mead", "perry", "wine" ], "timestamp": "2025-12-02T10:30:00.000Z" } Updated README.md with documentation for the new endpoint. Related to #78
1 parent 98ba639 commit 9ba536a

2 files changed

Lines changed: 156 additions & 0 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,

0 commit comments

Comments
 (0)