Skip to content

feat(api): add read-only festival catalogue API - #433

Merged
richardthe3rd merged 10 commits into
mainfrom
claude/festival-api-drinks-d1au01
Jun 15, 2026
Merged

feat(api): add read-only festival catalogue API#433
richardthe3rd merged 10 commits into
mainfrom
claude/festival-api-drinks-d1au01

Conversation

@richardthe3rd

@richardthe3rd richardthe3rd commented Jun 14, 2026

Copy link
Copy Markdown
Owner

What

Adds a proto-first, read-only catalogue API (CatalogService) that partners the existing personal MyFestivalService, both under one namespace, cambeerfestival.festival.v1alpha.

Service Resources Methods
CatalogService ("The Festival") Festival, Producer, Drink ListFestivals/GetFestival, ListProducers/GetProducer, ListDrinks/GetDrink (all reads, paginated)
MyFestivalService ("My Festival") DrinkEntry, DrinkSummary unchanged

📖 Rendered reference: the ReDoc site is generated from these protos by .github/workflows/api-docs.yml.

Why

The personal API referenced Festival (festivals/{f}) and Drink (festivals/{f}/drinks/{d}) parent resources that nothing defined or served — they existed only as resource_definition stubs. The catalogue itself was served as ad-hoc static JSON feeds, joined to the structured API only by ID convention.

CatalogService now defines Festival, Producer, and Drink canonically, so the personal/aggregate resource names resolve to a real Get/List contract. The catalogue is a reshape of the same static feeds (Festival.data_base_uri still points at them) into one versioned, documented, partner-consumable resource model — without changing the feed format.

This is a contract, not a server: every catalogue field is OUTPUT_ONLY, there are no create/update/delete RPCs, and the implementation is deliberately deferred (the static feeds remain the right tool until we need cross-festival search or a denormalised single-call grid). See #432 for that open decision.

Design notes

  • One namespace, independent maturity. Both services live in cambeerfestival.festival.v1alpha. Keeping them together does not couple stability — the version segment is the unit of promotion (granular down to the individual RPC), so the catalogue can graduate to cambeerfestival.festival.v1 first while v1alpha keeps a copy of the stable catalogue plus the still-alpha MyFestivalService. Step-by-step path in proto/buf.yaml; rationale in proto/README.md.
  • Producer is a first-class resource. The data feed is producer-keyed, so producer metadata stays normalised on Producer (festivals/{f}/producers/{p}); each Drink carries a lightweight ProducerReference (producer resource name + display_name denormalised only for cheap card/search rendering) instead of embedding the full record. This avoids duplicating brewery notes across every drink and enables brewery-directory browsing via ListProducers.
  • No server-side query. The app fetches the full (small, bounded) festival drink list and filters/sorts in memory, so ListDrinks has no filter/order_by — just pagination. The app's attributes (category, style, dispense, abv, allergens, vegan, availability/status, bar, producer) are all present on Drink for client-side use. Easy to add filtering back under v1alpha if a real need appears.
  • No stutter. Service is CatalogService (not FestivalService), so it doesn't collide with the package name.
  • No resource_definition stubs. Since everything shares one package, DrinkEntry's parent references resolve directly (duplicate definitions would fail core::0123::duplicate-resource).

Verification

  • buf lint, AIP api-linter, and OpenAPI generation all pass.
  • Proto tasks remain dev-only (mise.dev.toml), not in CI — consistent with the existing my-festival setup.
  • Generated OpenAPI (docs/code/api/openapi/) is gitignored and not committed.

Note: this restructures the previously-established cambeerfestival.myfestival.v1alpha package, so buf breaking against main would flag the move — expected for a v1alpha restructure with no stable clients, and proto tasks aren't gated in CI.

Refs #432

https://claude.ai/code/session_01FZcoF9nboAmPBr81WLtVeW


Generated by Claude Code

claude added 6 commits June 14, 2026 19:40
Define a proto-first "festival" catalogue contract (CatalogService) as the
shared, read-only counterpart to MyFestivalService:

- Festival resource (festivals/{f}) — Get, List
- Drink resource (festivals/{f}/drinks/{d}) — Get, List (filter, order_by,
  paginated), with embedded Producer

This makes Festival and Drink canonical resources. The my-festival protos
previously carried resource_definition stubs for these parent types so their
DrinkEntry/DrinkSummary names had something to reference; those stubs are now
removed since duplicate definitions would fail core::0123 and the canonical
ones resolve within the same buf module.

The catalogue reshapes the existing static JSON data feeds
(Festival.data_base_uri) into one versioned, documented surface with
server-side query, without changing the feed format.

buf lint, AIP api-linter, and OpenAPI generation all pass.
Avoid the cambeerfestival.catalog.v1alpha.CatalogService stutter (and match
the original "festival API" framing): the service is now FestivalService and
its file is festival_service.proto, mirroring my_festival_service.proto. The
package stays catalog; resources are unchanged.

buf lint, AIP api-linter, and OpenAPI generation all pass.
Collapse the catalog and myfestival packages into one consistent namespace,
cambeerfestival.festival.v1alpha, exposing two services over the same resource
hierarchy (mirroring the app's two destinations):

- CatalogService    -> Festival, Drink (shared, read-only catalogue)
- MyFestivalService -> DrinkEntry, DrinkSummary (personal + aggregates)

This removes the catalog/myfestival sibling-naming inconsistency and the
catalog.CatalogService / festival.FestivalService stutter. Service is named
CatalogService (not FestivalService) so it doesn't stutter against the package.
Because every type now shares one package, DrinkEntry's parent references to
Festival/Drink resolve directly with no resource_definition stubs.

No wire/field changes — package move only. buf lint, AIP api-linter, and
OpenAPI generation all pass.
Clarify that keeping CatalogService and MyFestivalService in one package does
not couple their stability: the version segment is the unit of promotion, so
the catalogue can graduate to cambeerfestival.festival.v1 first while
cambeerfestival.festival.v1alpha keeps a copy of the stable catalogue plus the
still-alpha MyFestivalService. Document the step-by-step path in buf.yaml and
the rationale in the proto README.
Promotion is granular down to the individual method: a stable version package
can carry only the RPCs that are ready (e.g. hold back the ListDrinks filter
grammar or BatchUpdateDrinkEntries) while the rest stay in alpha.
The data feed is producer-keyed, so producer metadata is naturally normalised
(once per producer). The previous Drink.producer embed re-duplicated a brewery's
location/founding-year/notes across every one of its drinks on the wire.

Model Producer as a first-class resource (festivals/{f}/producers/{p}) with
Get/List, and replace Drink's full embed with a ProducerReference (producer
resource name + display_name denormalised only for cheap card/search rendering).
This keeps brewery detail normalised, matches the feed's shape, and enables
brewery-directory browsing.

buf lint, AIP api-linter, and OpenAPI generation all pass.
claude added 2 commits June 14, 2026 20:32
Add a prominent link to the GitHub Pages ReDoc reference
(richardthe3rd.github.io/cambridge-beer-festival-app) generated from the protos
by the api-docs workflow, and note the per-PR openapi.yaml artifact. Update the
ReDoc page title from "MyFestival API" to "Festival API" since the spec now
covers the catalogue (CatalogService) as well.
The app fetches the full festival drink list and filters/sorts in memory (the
set is small and fully cached), so server-side query was unused speculative
surface. Remove ListDrinksRequest.filter and order_by and update the rationale:
the catalogue API's value is one coherent, documented, partner-consumable
resource model, not server-side query. Pagination (page_size/page_token) stays.

Easy to reintroduce under v1alpha if a real need appears.

buf lint, AIP api-linter, and OpenAPI generation all pass.
@richardthe3rd richardthe3rd changed the title feat(api): add read-only catalogue API alongside my-festival feat(api): add read-only festival catalogue API Jun 14, 2026
claude added 2 commits June 14, 2026 20:51
The proto CI job (bufbuild/buf-action) runs `buf breaking` against main, which
flagged the deletion of the old myfestival/*.proto files when they moved into
the unified cambeerfestival.festival.v1alpha namespace.

Set breaking.ignore_unstable_packages: true so alpha/beta packages are free to
churn (rename, move, restructure) while the API is still being shaped; only
stable (v1+) packages are guarded. Verified `buf breaking` against origin/main
passes locally.
…urces

The upstream feeds carry a last-updated timestamp the contract was discarding:
beverage files have a per-category top-level `timestamp`, and the registry has a
top-level `last_updated`. Expose them as OUTPUT_ONLY update_time:

- Drink.update_time / Producer.update_time  <- beverage file timestamp (per
  festival category; the file is the unit of change, so items in a category
  share the value)
- Festival.update_time                      <- registry last_updated (registry-wide)

Document the freshness/polling model in the proto README: conditional re-fetch
(If-None-Match/304) keyed off update_time, not item-level delta (the feeds are
whole-file snapshots, so per-drink change attribution isn't available).

buf lint, AIP api-linter, OpenAPI generation, and buf breaking all pass.
@richardthe3rd
richardthe3rd merged commit ba45f1a into main Jun 15, 2026
16 checks passed
@richardthe3rd
richardthe3rd deleted the claude/festival-api-drinks-d1au01 branch June 15, 2026 08:03
@github-actions github-actions Bot mentioned this pull request Jun 15, 2026
@github-actions github-actions Bot mentioned this pull request Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants