You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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.
4
4
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.
6
6
7
7
## Goals / Non-Goals
8
8
9
9
**Goals:**
10
10
11
-
- PenpotAPI модуль внутри ExFig (позже извлечь в `swift-penpot-api`)
**Почему:** 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.
40
40
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.
42
42
43
-
### D2: application/json вместо transit+json
43
+
**Alternative:** Generic HTTP endpoint protocol over both APIs — rejected: adds complexity to both clients with no benefit.
44
44
45
-
**Решение:**`Accept: application/json` header во всех запросах.
45
+
### D2: application/json instead of transit+json
46
46
47
-
**Почему:**Transit — Clojure-specific формат без Swift библиотеки. JSON работает для всех endpoint'ов. Известный баг #7540 (JSON decode fails для файлов с Design Tokens) — edge case, обрабатываем понятной ошибкой.
47
+
**Decision:**`Accept: application/json` header on all requests.
48
48
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`.
50
50
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.
52
52
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.
54
54
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
56
56
57
-
**Альтернатива:**`JSONDecoder.keyDecodingStrategy = .custom` → отклонено: несовместимо с JSONCodec/YYJSON.
57
+
**Decision:**Models use standard Swift `Codable` synthesis without explicit `CodingKeys`.
58
58
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.
60
60
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).
62
62
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.
64
64
65
-
**Альтернатива:** Добавить `penpotClient` в SourceFactory → отклонено: ломает сигнатуру, требует создание клиента даже когда sourceKind=figma.
65
+
### D4: Client created inside Source, not passed through SourceFactory
66
66
67
-
### D5: Thumbnails для icons/images (v1)
67
+
**Decision:**`PenpotColorsSource` / `PenpotComponentsSource` create `BasePenpotClient` themselves from env var `PENPOT_ACCESS_TOKEN` and `baseURL` from config.
68
68
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.
70
70
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.
72
72
73
-
**Ограничение:** Иконки будут растровые. Warn пользователя при `format: svg` + `sourceKind: penpot`.
73
+
### D5: Thumbnails for icons/images (v1)
74
74
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.
**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.
78
78
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`.
80
80
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.
**Решение:**Простой retry (3 попытки, exponential backoff) внутри `BasePenpotClient`. Без `SharedRateLimiter`.
85
+
**Decision:**For v1 — `figmaFileId` → Penpot file UUID, `frameName` → component path filter. No refactoring to `sourceConfig` pattern.
86
86
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).
**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.
0 commit comments