Skip to content

feat(website/matchers): discriminated Map | Location union - #85

Closed
guitavano wants to merge 1 commit into
mainfrom
guitavano/location-matcher-discriminated-union
Closed

feat(website/matchers): discriminated Map | Location union#85
guitavano wants to merge 1 commit into
mainfrom
guitavano/location-matcher-discriminated-union

Conversation

@guitavano

@guitavano guitavano commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Problem

Editing a Location matcher rule in the Studio CMS shows all fields at once — City, Region Code, Country and Area selection (Map) merged into one form — with no way to choose between a Location rule and a Map rule. The @format widgets also never attach.

Root cause is in the generated JSON schema (confirmed against a live /live/_meta): includeLocations/excludeLocations were typed (Location | Map)[], but both branches are open, all-optional object interfaces with no discriminator. The schema generator merges them into a single flat object:

"items": {
  "type": "object",
  "properties": { "city": , "regionCode": , "country": , "coordinates":  }
}

No anyOf, no oneOf, and the @format location / @format map annotations are dropped. So the CMS has nothing to render as a choice, and the location/map field widgets have nothing to attach to.

Fix

Add a required type literal discriminator ("location" | "map") to each branch so the generator emits a real discriminated anyOf. The Studio already renders a type-discriminated union as a branch selector (resolve-schemablock-ref with discriminatorKey: "type"AnyOfField), which restores the Map vs Location choice and lets the @format widgets bind to the correct branch.

Also expanded the field descriptions to document the cf-* header semantics.

Behavior

  • Runtime unchanged: matchLocation keys off whichever fields are present and ignores the extra type field. Existing saved rules keep matching.
  • Public interface names Location / Map are preserved; each just gains type.

Testing

  • bun run typecheck
  • bun run test ✅ 60 passing (matcher tests updated to include the type discriminator)
  • biome check on changed files ✅ (only a pre-existing any warning in an unrelated test)

Follow-up / needs verification

I couldn't confirm from the current live meta that the generator emits object-level @format (all formats there sit on string leaves). After this app is rebuilt, we should check /live/_meta to confirm format: "location" lands on the Location branch — if it doesn't, the Studio widget trigger will be adjusted to key off the discriminator instead. The Map/Location choice works regardless via the type discriminator.

🤖 Generated with Claude Code


Summary by cubic

Adds a type discriminator to the location matcher, turning (Location | Map)[] into a true discriminated union so Studio shows a Map vs Location selector and the correct @format widgets attach. Updates tests; no runtime behavior change.

  • Bug Fixes
    • Add type: "location" | "map" to Location and Map; require Map.coordinates.
    • Expand field descriptions with cf-ipcountry, cf-region-code, cf-ipcity semantics.
    • Update matcher tests to include the discriminator.

Written for commit 17a4a18. Summary will update on new commits.

Review in cubic

…ocation union

The `(Location | Map)[]` union of two open, all-optional object interfaces
gets structurally merged by the schema generator into a single flat object
({city, regionCode, country, coordinates}) with no way to tell the branches
apart. In the CMS this renders every field at once instead of letting the
editor pick "Location" (country/region/city) vs "Map" (area radius), and the
@Format annotations are lost.

Add a required `type` literal discriminator ("location" | "map") to each
branch so the generated schema is a real discriminated anyOf. The Studio
renders a type-discriminated union as a branch selector, restoring the
Map-vs-Location choice and letting the @Format widgets attach to the right
branch. Also flesh out the field descriptions (cf-* header semantics).

Runtime matching is unchanged — matchLocation keys off the fields present and
ignores the extra `type` discriminator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@guitavano
guitavano requested a review from a team July 1, 2026 15:47

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="website/matchers/location.ts">

<violation number="1" location="website/matchers/location.ts:47">
P3: The new city field description claims matching is case-insensitive, but the matcher uses strict case-sensitive equality. Either document exact casing or normalize both sides before comparing.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

/**
* @title City
* @example São Paulo
* @description Exact city name (case-insensitive) as returned by Cloudflare's cf-ipcity header.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new city field description claims matching is case-insensitive, but the matcher uses strict case-sensitive equality. Either document exact casing or normalize both sides before comparing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At website/matchers/location.ts, line 47:

<comment>The new city field description claims matching is case-insensitive, but the matcher uses strict case-sensitive equality. Either document exact casing or normalize both sides before comparing.</comment>

<file context>
@@ -7,32 +7,56 @@ export interface Coordinate {
 	/**
 	 * @title City
 	 * @example São Paulo
+	 * @description Exact city name (case-insensitive) as returned by Cloudflare's cf-ipcity header.
 	 */
 	city?: string;
</file context>
Suggested change
* @description Exact city name (case-insensitive) as returned by Cloudflare's cf-ipcity header.
* @description Exact city name as returned by Cloudflare's cf-ipcity header.

@guitavano

Copy link
Copy Markdown
Contributor Author

Closing — the type discriminator isn't needed. Verified with deco's own schema generator (@decocms/start/scripts/generate-schema.ts) that the plain (Location | Map)[] already emits a proper anyOf of two titled object branches (Location / Map), with format: map preserved on the Map branch. deco does NOT merge the union. The reason the CMS shows the fields merged is a rendering gap in the Studio (it merges non-discriminated inlined-object unions instead of offering a branch choice) — fixing that on the Studio side, no app schema change required.

@guitavano guitavano closed this Jul 1, 2026
@guitavano
guitavano deleted the guitavano/location-matcher-discriminated-union branch July 1, 2026 15:56
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