Skip to content

Commit 260474d

Browse files
committed
docs(openspec): fix Penpot API assumptions and translate to English
Correct factual errors found during API documentation review: - Fix JSON key casing: responses use camelCase (not kebab-case) with Accept: application/json — remove unnecessary CodingKeys (D3) - Update RPC URL to /api/main/methods/ (legacy /api/rpc/command/ noted) - Mark bug #7540 as resolved (January 2026), keep defensive catch - Add dual String/Number decoding for typography numeric fields - Translate all proposal artifacts from Russian to English
1 parent 98f6b61 commit 260474d

5 files changed

Lines changed: 104 additions & 92 deletions

File tree

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
## Context
22

3-
ExFig v2.8.0 уже имеет абстракцию DesignSource (`ColorsSource`, `ComponentsSource`, `TypographySource` протоколы) с `DesignSourceKind.penpot` объявленным, но выбрасывающим `unsupportedSourceKind`. Существует паттерн `swift-figma-api` standalone Swift package с protocol-based client, endpoint structs, и response models.
3+
ExFig v2.8.0 already has a DesignSource abstraction (`ColorsSource`, `ComponentsSource`, `TypographySource` protocols) with `DesignSourceKind.penpot` declared but throwing `unsupportedSourceKind`. The `swift-figma-api` pattern exists — a standalone Swift package with protocol-based client, endpoint structs, and response models.
44

5-
Penpot использует **RPC API** (не REST): все вызовы — `POST /api/rpc/command/<name>` с JSON body. Ключи в JSON — kebab-case. Числа в типографии — строки. SVG/PNG export endpoint **отсутствует**только thumbnails через `get-file-object-thumbnails`.
5+
Penpot uses an **RPC API** (not REST): all calls are `POST /api/main/methods/<name>` with JSON body (the legacy path `/api/rpc/command/<name>` is preserved for backward compatibility). With `Accept: application/json`, responses arrive in **camelCase** (middleware `json/write-camel-key`); kebab-case is preserved only in transit+json. Typography numeric fields are strings in the Clojure schema but may serialize as JSON numbers. There is **no public SVG/PNG export endpoint**the Penpot exporter uses headless Chromium (internal service); only thumbnails are available via API.
66

77
## Goals / Non-Goals
88

99
**Goals:**
1010

11-
- PenpotAPI модуль внутри ExFig (позже извлечь в `swift-penpot-api`)
12-
- Базовая поддержка: solid colors, icons (thumbnails), illustrations (thumbnails), typography
13-
- E2E тесты против реального Penpot instance
14-
- Бесшовная интеграция через существующую DesignSource абстракцию
11+
- PenpotAPI module inside ExFig (extract to `swift-penpot-api` later)
12+
- Basic support: solid colors, icons (thumbnails), illustrations (thumbnails), typography
13+
- E2E tests against a real Penpot instance
14+
- Seamless integration via the existing DesignSource abstraction
1515

1616
**Non-Goals:**
1717

18-
- SVG reconstruction из shape tree (future phase)
19-
- Gradient/image fill colors (v1 — только solid)
20-
- Dark mode для Penpot (Penpot не имеет mode-based переменных как Figma Variables)
18+
- SVG reconstruction from shape tree (future phase)
19+
- Gradient/image fill colors (v1 — solid only)
20+
- Dark mode for Penpot (Penpot has no mode-based variables like Figma Variables)
2121
- Penpot webhooks / watch mode
22-
- Извлечение в отдельный репозиторий (делаем после e2e)
22+
- Extraction into a separate repository (after e2e validation)
2323

2424
## Decisions
2525

26-
### D1: RPC endpoint protocol вместо REST
26+
### D1: RPC endpoint protocol instead of REST
2727

28-
**Решение:** Собственный `PenpotEndpoint` protocol, не наследующий от FigmaAPI.
28+
**Decision:** Custom `PenpotEndpoint` protocol, not inheriting from FigmaAPI. URL: `/api/main/methods/<commandName>` (new recommended path).
2929

3030
```swift
3131
protocol PenpotEndpoint: Sendable {
@@ -36,64 +36,70 @@ protocol PenpotEndpoint: Sendable {
3636
}
3737
```
3838

39-
**Почему:** Penpot RPC (POST + body) фундаментально отличается от Figma REST (GET + path params). Общий endpoint protocol создал бы leaky abstraction. При извлечении в `swift-penpot-api` модуль поедет as-is.
39+
**Rationale:** Penpot RPC (POST + body) is fundamentally different from Figma REST (GET + path params). A shared endpoint protocol would create a leaky abstraction. When extracted to `swift-penpot-api`, the module ships as-is.
4040

41-
**Альтернатива:** Generic HTTP endpoint protocol поверх обоих API → отклонено: усложняет оба клиента без выгоды.
41+
**URL migration:** Penpot migrated from `/api/rpc/command/` to `/api/main/methods/`. The old path is preserved for backward compatibility, but the new one is "strongly recommended". We use the new path by default.
4242

43-
### D2: application/json вместо transit+json
43+
**Alternative:** Generic HTTP endpoint protocol over both APIs — rejected: adds complexity to both clients with no benefit.
4444

45-
**Решение:** `Accept: application/json` header во всех запросах.
45+
### D2: application/json instead of transit+json
4646

47-
**Почему:** Transit — Clojure-specific формат без Swift библиотеки. JSON работает для всех endpoint'ов. Известный баг #7540 (JSON decode fails для файлов с Design Tokens) — edge case, обрабатываем понятной ошибкой.
47+
**Decision:** `Accept: application/json` header on all requests.
4848

49-
**Альтернатива:** Написать transit parser → отклонено: непропорциональные затраты для edge case.
49+
**Rationale:** Transit is a Clojure-specific format with no Swift library. JSON works for all endpoints. The Penpot backend automatically converts keys to camelCase via `json/write-camel-key` middleware when responding with `Accept: application/json`, making responses natively compatible with Swift `Codable`.
5050

51-
### D3: Явные CodingKeys вместо key strategy
51+
**Bug #7540:** Previously, JSON decode failed for files with Design Tokens (missing write handler for `TokensLib` in `clojure.data.json`). The bug was **fixed** (January 2026). We keep a defensive catch for self-hosted instances running older Penpot versions.
5252

53-
**Решение:** Каждая модель имеет `enum CodingKeys: String, CodingKey` с маппингом kebab→camelCase.
53+
**Alternative:** Write a transit parser — rejected: disproportionate effort, and transit preserves kebab-case keys requiring additional mapping.
5454

55-
**Почему:** YYJSON (swift-yyjson) — primary JSON decoder в проекте. `JSONCodec.decode()` может не поддерживать custom key strategies. Explicit CodingKeys — паттерн из FigmaAPI для snake_case. Моделей ~6 — overhead минимальный.
55+
### D3: Standard Codable without CodingKeys
5656

57-
**Альтернатива:** `JSONDecoder.keyDecodingStrategy = .custom` → отклонено: несовместимо с JSONCodec/YYJSON.
57+
**Decision:** Models use standard Swift `Codable` synthesis without explicit `CodingKeys`.
5858

59-
### D4: Клиент создаётся внутри Source, не передаётся через SourceFactory
59+
**Rationale:** The Penpot backend automatically converts kebab-case keys to camelCase via `json/write-camel-key` middleware when responding with `Accept: application/json`. JSON responses arrive with keys like `fontFamily`, `mainInstanceId`, `fontSize`, etc. — matching standard Swift naming with no mapping required. Confirmed by the official `penpot-export` tool, which works with camelCase without transformation.
6060

61-
**Решение:** `PenpotColorsSource` / `PenpotComponentsSource` сами создают `BasePenpotClient` из env var `PENPOT_ACCESS_TOKEN` и `baseURL` из config.
61+
**Note:** Kebab-case (`font-family`, `main-instance-id`) is preserved only in `application/transit+json` format, which we do not use (D2).
6262

63-
**Почему:** Аналог `TokensFileColorsSource` (не получает FigmaAPI Client). Не требует изменения сигнатуры `SourceFactory`. Penpot client лёгкий — 1-3 API вызова на экспорт, нет смысла в shared rate limiter.
63+
**Alternative:** Explicit CodingKeys with kebab→camelCase mapping — rejected: unnecessary boilerplate since JSON already arrives in camelCase.
6464

65-
**Альтернатива:** Добавить `penpotClient` в SourceFactory → отклонено: ломает сигнатуру, требует создание клиента даже когда sourceKind=figma.
65+
### D4: Client created inside Source, not passed through SourceFactory
6666

67-
### D5: Thumbnails для icons/images (v1)
67+
**Decision:** `PenpotColorsSource` / `PenpotComponentsSource` create `BasePenpotClient` themselves from env var `PENPOT_ACCESS_TOKEN` and `baseURL` from config.
6868

69-
**Решение:** Использовать `get-file-object-thumbnails``GET /assets/by-file-media-id/<id>` для растровых thumbnails.
69+
**Rationale:** Analogous to `TokensFileColorsSource` (does not receive a FigmaAPI Client). Does not require changing the `SourceFactory` signature. The Penpot client is lightweight — 1-3 API calls per export, no benefit from a shared rate limiter.
7070

71-
**Почему:** Penpot API не имеет SVG/PNG render endpoint. Thumbnails — единственный способ получить визуальное представление компонента через API. Для иконок это субоптимально (растр вместо вектора), но работает для иллюстраций.
71+
**Alternative:** Add `penpotClient` to SourceFactory — rejected: breaks the signature, requires client creation even when sourceKind=figma.
7272

73-
**Ограничение:** Иконки будут растровые. Warn пользователя при `format: svg` + `sourceKind: penpot`.
73+
### D5: Thumbnails for icons/images (v1)
7474

75-
**Future:** SVG reconstruction из shape tree (parse objects → build SVG DOM) — отдельная фаза после e2e валидации базового flow.
75+
**Decision:** Use `get-file-object-thumbnails``GET /assets/by-file-media-id/<id>` for raster thumbnails.
7676

77-
### D6: Переиспользование полей IconsSourceInput/ImagesSourceInput
77+
**Rationale:** Penpot API has no SVG/PNG render endpoint for external consumers. Thumbnails are the only way to obtain a visual representation of a component via API. Suboptimal for icons (raster instead of vector), but works for illustrations.
7878

79-
**Решение:** Для v1 — `figmaFileId` → Penpot file UUID, `frameName` → component path filter. Без рефакторинга на `sourceConfig` pattern.
79+
**Limitation:** Icons will be raster. Warn the user when `format: svg` + `sourceKind: penpot`.
8080

81-
**Почему:** Минимальные изменения в ExFigCore. Рефакторинг на `ComponentsSourceConfig` (как у Colors) — follow-up, когда появится 3-й source. Прагматичный подход: имена полей неидеальные, но типы совпадают (String).
81+
**Future:** SVG reconstruction from shape tree (parse objects → build SVG DOM) — separate phase after e2e validation of the basic flow.
8282

83-
### D7: Простой retry вместо SharedRateLimiter
83+
### D6: Reuse IconsSourceInput/ImagesSourceInput fields
8484

85-
**Решение:** Простой retry (3 попытки, exponential backoff) внутри `BasePenpotClient`. Без `SharedRateLimiter`.
85+
**Decision:** For v1 — `figmaFileId` → Penpot file UUID, `frameName` → component path filter. No refactoring to `sourceConfig` pattern.
8686

87-
**Почему:** Penpot API — 1-3 вызова на экспорт (`get-file` возвращает всё). Rate limits не задокументированы. SharedRateLimiter оправдан для Figma (десятки запросов, известные лимиты), но overhead для Penpot.
87+
**Rationale:** Minimal changes to ExFigCore. Refactoring to `ComponentsSourceConfig` (like Colors) — follow-up when a 3rd source appears. Pragmatic approach: field names are not ideal, but types match (String).
88+
89+
### D7: Simple retry instead of SharedRateLimiter
90+
91+
**Decision:** Simple retry (3 attempts, exponential backoff) inside `BasePenpotClient`. No `SharedRateLimiter`.
92+
93+
**Rationale:** Penpot API — 1-3 calls per export (`get-file` returns everything). Rate limits are undocumented. SharedRateLimiter is justified for Figma (dozens of requests, known limits), but overhead for Penpot.
8894

8995
## Risks / Trade-offs
9096

91-
| Risk | Mitigation |
92-
| ------------------------------------------- | --------------------------------------------------------------------------------- |
93-
| Нет SVG export → иконки растровые | Warn пользователя. Документировать ограничение. SVG reconstruction в future phase |
94-
| JSON bug #7540 (Design Tokens) | Catch decode error → понятное сообщение с workaround |
95-
| Penpot API нестабилен (нет версионирования) | E2E тесты как canary. Version check через `get-profile` |
96-
| Большой `get-file` response | YYJSON эффективно парсит. Декодируем только нужные секции через optional поля |
97-
| String numerics в типографии | `Double(string)` с guard + warning, never force-unwrap |
98-
| Kebab-case → CodingKeys boilerplate | ~6 моделей, терпимо. При извлечении в пакет — один раз написать |
99-
| Self-hosted Penpot разные версии | Configurable `baseURL`. E2E против cloud, manual testing для self-hosted |
97+
| Risk | Mitigation |
98+
| ------------------------------------------ | -------------------------------------------------------------------------- |
99+
| No SVG export → raster icons | Warn the user. Document the limitation. SVG reconstruction in future phase |
100+
| JSON bug #7540 (Design Tokens) | Fixed (January 2026). Defensive catch for older self-hosted versions |
101+
| Penpot API unstable (no versioning) | E2E tests as canary. Version check via `get-profile` |
102+
| Large `get-file` response | YYJSON parses efficiently. Decode only needed sections via optional fields |
103+
| String vs Number in typography | Custom `Codable` init with `decodeIfPresent` for both String and Double |
104+
| URL migration (`/api/rpc/``/api/main/`) | Use new path by default. Old path preserved for backward compatibility |
105+
| Self-hosted Penpot different versions | Configurable `baseURL`. E2E against cloud, manual testing for self-hosted |

0 commit comments

Comments
 (0)