feat(api): add read-only festival catalogue API - #433
Merged
Conversation
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.
3 tasks
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.
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.
Merged
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a proto-first, read-only catalogue API (
CatalogService) that partners the existing personalMyFestivalService, both under one namespace,cambeerfestival.festival.v1alpha.CatalogService("The Festival")Festival,Producer,DrinkListFestivals/GetFestival,ListProducers/GetProducer,ListDrinks/GetDrink(all reads, paginated)MyFestivalService("My Festival")DrinkEntry,DrinkSummary📖 Rendered reference: the ReDoc site is generated from these protos by
.github/workflows/api-docs.yml.Why
The personal API referenced
Festival(festivals/{f}) andDrink(festivals/{f}/drinks/{d}) parent resources that nothing defined or served — they existed only asresource_definitionstubs. The catalogue itself was served as ad-hoc static JSON feeds, joined to the structured API only by ID convention.CatalogServicenow definesFestival,Producer, andDrinkcanonically, 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_uristill 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
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 tocambeerfestival.festival.v1first whilev1alphakeeps a copy of the stable catalogue plus the still-alphaMyFestivalService. Step-by-step path inproto/buf.yaml; rationale inproto/README.md.Produceris a first-class resource. The data feed is producer-keyed, so producer metadata stays normalised onProducer(festivals/{f}/producers/{p}); eachDrinkcarries a lightweightProducerReference(producer resource name +display_namedenormalised 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 viaListProducers.ListDrinkshas nofilter/order_by— just pagination. The app's attributes (category, style, dispense, abv, allergens, vegan, availability/status, bar, producer) are all present onDrinkfor client-side use. Easy to add filtering back underv1alphaif a real need appears.CatalogService(notFestivalService), so it doesn't collide with the package name.resource_definitionstubs. Since everything shares one package,DrinkEntry's parent references resolve directly (duplicate definitions would failcore::0123::duplicate-resource).Verification
buf lint, AIPapi-linter, and OpenAPI generation all pass.mise.dev.toml), not in CI — consistent with the existing my-festival setup.docs/code/api/openapi/) is gitignored and not committed.Refs #432
https://claude.ai/code/session_01FZcoF9nboAmPBr81WLtVeW
Generated by Claude Code