|
| 1 | +# Fizzy SDK → Rails API Surface Communique |
| 2 | + |
| 3 | +The SDK spec has been expanded from 70 to 102 operations with full conformance coverage across Go, TypeScript, Ruby, Swift, and Kotlin. This document captures what the Rails app needs to match, verified against the Rails source. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Response shape changes already aligned with Rails |
| 8 | + |
| 9 | +These shape changes were made to match what Rails actually returns. No Rails changes needed. |
| 10 | + |
| 11 | +### UserSummary eliminated — full User everywhere |
| 12 | + |
| 13 | +The SDK now expects the full `_user.json.jbuilder` shape (`{id, name, role, active, email_address, avatar_url, created_at, url}`) in all embedded positions: `Board.creator`, `Card.creator`, `Card.assignees[]`, `Comment.creator`, `Reaction.reacter`, `Notification.creator`. Rails already renders the full partial everywhere — **no change needed**. |
| 14 | + |
| 15 | +Note: `email_address` comes from `user.identity&.email_address` — system users with no identity will have `null`. The SDK marks this field optional. |
| 16 | + |
| 17 | +### BoardSummary and ColumnSummary eliminated |
| 18 | + |
| 19 | +`Card.board` and `Card.column` now expect full Board/Column shapes. Rails card partial already renders `json.partial! "boards/board"` and `json.partial! "columns/column"` — **no change needed**. |
| 20 | + |
| 21 | +### Column.color is Object {name, value} |
| 22 | + |
| 23 | +Rails `_column.json.jbuilder` serializes `color` via `json.(column, :color)` which implicitly serializes the `Color = Struct.new(:name, :value)` as `{"name": "Blue", "value": "var(--color-card-default)"}` — **already correct**. |
| 24 | + |
| 25 | +### Tag.name → Tag.title |
| 26 | + |
| 27 | +Rails `_tag.json.jbuilder` already returns `title` (not `name`), plus `created_at` and `url`. **Already aligned**. |
| 28 | + |
| 29 | +Note: `url` is a cards index URL filtered to that tag (`cards_url(tag_ids: [tag])`), not a tag show URL. |
| 30 | + |
| 31 | +### Account: slug, created_at added |
| 32 | + |
| 33 | +Rails `_account.json.jbuilder` already returns `{id, name, slug, created_at}`. The `user` field is added at the identity level (`my/identities/show.json.jbuilder`), not in the account partial itself. **Already aligned**. |
| 34 | + |
| 35 | +### ListPins returns Card[] |
| 36 | + |
| 37 | +Rails `my/pins/index.json.jbuilder` already returns `json.array! @pins { json.partial! "cards/card", card: pin.card }` — flat card array. **Already aligned**. |
| 38 | + |
| 39 | +### All IDs are strings |
| 40 | + |
| 41 | +Rails returns string IDs via `to_param` — **no change needed**. |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## Shape discrepancies requiring Rails attention |
| 46 | + |
| 47 | +### Card.has_more_assignees, comments_url, reactions_url |
| 48 | + |
| 49 | +Rails card partial includes `has_more_assignees`, `comments_url`, and `reactions_url` fields. The SDK spec doesn't include these. Low priority but worth noting for completeness. |
| 50 | + |
| 51 | +### Card.description_html |
| 52 | + |
| 53 | +Rails card partial returns both `description` (plain text) and `description_html` (HTML). SDK spec only has `description`. Consider adding `description_html` to the spec. |
| 54 | + |
| 55 | +### Steps index: no standalone endpoint |
| 56 | + |
| 57 | +The SDK spec defines `ListSteps` as `GET /{acct}/cards/{number}/steps.json`. Rails has no `steps/index.json.jbuilder` — steps are embedded inline in `cards/show.json.jbuilder` only. Individual step CRUD does have JSON responses. |
| 58 | + |
| 59 | +**Either:** (a) Add a steps index JSON endpoint to Rails, or (b) remove `ListSteps` from the SDK spec and document that steps come from the card show response. |
| 60 | + |
| 61 | +--- |
| 62 | + |
| 63 | +## New endpoints: confirmed HTML/Turbo-only in Rails |
| 64 | + |
| 65 | +All of these routes exist in Rails but serve **only HTML or Turbo Stream responses**. Each needs a `respond_to { |format| format.json { head :ok } }` (or appropriate JSON body) added. |
| 66 | + |
| 67 | +### Void-response endpoints (just need `head :ok` / `head :no_content`) |
| 68 | + |
| 69 | +| Operation | Controller | Method | Path | |
| 70 | +|-----------|-----------|--------|------| |
| 71 | +| PublishBoard | `Boards::PublicationsController#create` | POST | `/{acct}/boards/{id}/publication.json` | |
| 72 | +| UnpublishBoard | `Boards::PublicationsController#destroy` | DELETE | `/{acct}/boards/{id}/publication.json` | |
| 73 | +| UpdateBoardInvolvement | `Boards::InvolvementsController#update` | PATCH | `/{acct}/boards/{id}/involvement.json` | |
| 74 | +| UpdateBoardEntropy | `Boards::EntropiesController#update` | PATCH | `/{acct}/boards/{id}/entropy.json` | |
| 75 | +| UpdateAccountEntropy | `Account::EntropiesController#update` | PATCH | `/{acct}/account/entropy.json` | |
| 76 | +| MoveColumnLeft | `Columns::LeftPositionsController#create` | POST | `/{acct}/columns/{id}/left_position.json` | |
| 77 | +| MoveColumnRight | `Columns::RightPositionsController#create` | POST | `/{acct}/columns/{id}/right_position.json` | |
| 78 | +| MarkCardRead | `Cards::ReadingsController#create` | POST | `/{acct}/cards/{number}/reading.json` | |
| 79 | +| MarkCardUnread | `Cards::ReadingsController#destroy` | DELETE | `/{acct}/cards/{number}/reading.json` | |
| 80 | +| PublishCard | `Cards::PublishesController#create` | POST | `/{acct}/cards/{number}/publish.json` | |
| 81 | +| UpdateUserRole | `Users::RolesController#update` | PATCH | `/{acct}/users/{id}/role.json` | |
| 82 | +| DeleteUserAvatar | `Users::AvatarsController#destroy` | DELETE | `/{acct}/users/{id}/avatar` | |
| 83 | +| UpdateAccountSettings | `Account::SettingsController#update` | PATCH | `/{acct}/account/settings.json` | |
| 84 | +| UpdateNotificationSettings | `Notifications::SettingsController#update` | PATCH | `/{acct}/notifications/settings.json` | |
| 85 | +| ResetJoinCode | `Account::JoinCodesController#destroy` | DELETE | `/{acct}/account/join_code.json` | |
| 86 | +| UpdateJoinCode | `Account::JoinCodesController#update` | PATCH | `/{acct}/account/join_code.json` | |
| 87 | +| CreatePushSubscription | `Users::PushSubscriptionsController#create` | POST | `/{acct}/users/{id}/push_subscriptions.json` | |
| 88 | +| DeletePushSubscription | `Users::PushSubscriptionsController#destroy` | DELETE | `/{acct}/users/{id}/push_subscriptions/{subId}` | |
| 89 | + |
| 90 | +### Endpoints needing JSON views (return data) |
| 91 | + |
| 92 | +| Operation | Controller | Path | Expected response | |
| 93 | +|-----------|-----------|------|-------------------| |
| 94 | +| GetAccountSettings | `Account::SettingsController#show` | GET `/{acct}/account/settings.json` | `{name}` | |
| 95 | +| GetJoinCode | `Account::JoinCodesController#show` | GET `/{acct}/account/join_code.json` | `{code, url, usage_limit?}` | |
| 96 | +| GetNotificationSettings | `Notifications::SettingsController#show` | GET `/{acct}/notifications/settings.json` | `{bundle_email_frequency}` | |
| 97 | +| ListStreamCards | `Boards::Columns::StreamsController#show` | GET `/{acct}/boards/{id}/columns/stream.json` | `Card[]` (paginated) | |
| 98 | +| ListPostponedCards | `Boards::Columns::NotNowsController#show` | GET `/{acct}/boards/{id}/columns/not_now.json` | `Card[]` (paginated) | |
| 99 | +| ListClosedCards | `Boards::Columns::ClosedsController#show` | GET `/{acct}/boards/{id}/columns/closed.json` | `Card[]` (paginated) | |
| 100 | +| SearchCards | `SearchesController#show` | GET `/{acct}/search.json?q=...` | `Card[]` (paginated) | |
| 101 | +| CreateAccountExport | `Account::ExportsController#create` | POST `/{acct}/account/exports.json` | `{id, status, created_at, download_url?}` | |
| 102 | +| GetAccountExport | `Account::ExportsController#show` | GET `/{acct}/account/exports/{id}` | `{id, status, created_at, download_url?}` | |
| 103 | + |
| 104 | +### Access tokens (partial JSON exists) |
| 105 | + |
| 106 | +| Operation | Path | Notes | |
| 107 | +|-----------|------|-------| |
| 108 | +| ListAccessTokens | GET `/my/access_tokens.json` | **Needs JSON index view** — currently HTML only | |
| 109 | +| CreateAccessToken | POST `/my/access_tokens.json` | Has inline JSON (`{token, description, permission}`) — **needs `id` and `created_at` added** | |
| 110 | +| DeleteAccessToken | DELETE `/my/access_tokens/{id}` | Needs `format.json { head :no_content }` | |
| 111 | + |
| 112 | +--- |
| 113 | + |
| 114 | +## Input field names (SDK sends these — Rails must `permit` them) |
| 115 | + |
| 116 | +| Operation | Field SDK sends | Verify `params.permit` | |
| 117 | +|-----------|----------------|----------------------| |
| 118 | +| AssignCard | `assignee_id` | Was `user_id` in some references | |
| 119 | +| TagCard | `tag_title` | Was `name` in some references | |
| 120 | +| UpdateUserRole | `role` | String: "member" or "admin" | |
| 121 | +| UpdateBoardInvolvement | `involvement` | String | |
| 122 | +| UpdateBoardEntropy | `auto_postpone_period` | Integer | |
| 123 | +| UpdateAccountEntropy | `auto_postpone_period` | Integer | |
| 124 | +| UpdateAccountSettings | `name` | String | |
| 125 | +| UpdateNotificationSettings | `bundle_email_frequency` | String | |
| 126 | +| UpdateJoinCode | `usage_limit` | Integer | |
| 127 | +| CreatePushSubscription | `endpoint`, `p256dh_key`, `auth_key` | All strings | |
| 128 | +| CreateBoard/UpdateBoard | `public_description`, `auto_postpone_period` | New optional fields | |
| 129 | +| UpdateBoard | `user_ids` | Array of strings | |
| 130 | + |
| 131 | +--- |
| 132 | + |
| 133 | +## Idempotency contract |
| 134 | + |
| 135 | +The SDK retries these operations on 503/5xx (naturally idempotent — calling twice has the same effect). Rails should ensure they're truly safe to replay: |
| 136 | + |
| 137 | +- `MarkCardRead` (POST) / `MarkCardUnread` (DELETE) — toggle read state |
| 138 | +- `MoveColumnLeft` / `MoveColumnRight` (POST) — positional, already-leftmost is a no-op |
| 139 | +- `PublishBoard` (POST) / `UnpublishBoard` (DELETE) — toggle publish state |
| 140 | +- `PublishCard` (POST) — publish draft, already-published is a no-op |
| 141 | +- `CloseCard`, `PostponeCard`, `GoldCard`, `PinCard`, `WatchCard`, `TriageCard` (POST) |
| 142 | +- `ReopenCard` (DELETE) — reopen a closed card |
| 143 | +- `ActivateWebhook`, `DeactivateWebhook` (POST) |
| 144 | +- `ReadNotification` (POST) |
| 145 | + |
| 146 | +The SDK does **not** retry: `CreateCard`, `CreateBoard`, `CreateComment`, `CreateStep`, `CreateWebhook`, `AssignCard`, `TagCard`, `CreateSession`, `CreateAccessToken`, `CreatePushSubscription`, `CreateAccountExport`. |
| 147 | + |
| 148 | +--- |
| 149 | + |
| 150 | +## Priority order |
| 151 | + |
| 152 | +1. **Shape discrepancy decisions** — Card.tags (strings vs objects), ListSteps (standalone vs embedded) |
| 153 | +2. **Void-response JSON paths** — 18 controllers need `format.json { head :ok }`, low effort |
| 154 | +3. **Data-returning JSON views** — 9 endpoints need jbuilder templates |
| 155 | +4. **Access token JSON index** — new template needed |
0 commit comments