name: letsgrow-ios-sync-openapi
description: >-
Use when syncing the iOS app's handwritten API boundary from
Modules/Sources/OpenAPI/GeneratedSources/Client.swift and Types.swift:
mirror generated client operations into Domain/APIClient, create same-named
Domain API models for consumed schemas, add mapper files in
Modules/Sources/Adapters/APIClient/Mappers, and update
Modules/Sources/Adapters/APIClient/Live plus AppMock/ApiClientMock.swift.
LetsGrow iOS Sync OpenAPI
Use this skill when the user wants to:
- sync handwritten iOS API code from
Modules/Sources/OpenAPI/GeneratedSources/Client.swift - add missing app-facing endpoints to
Modules/Sources/Domain/Services/ApiClient.swift - create or update same-named Domain API models for consumed generated schemas
- add or update mapper files in
Modules/Sources/Adapters/APIClient/Mappers/ - keep
Modules/Sources/Adapters/APIClient/Live/andAppMock/ApiClientMock.swiftaligned withAPIClient
Source Of Truth
- Treat
Modules/Sources/OpenAPI/GeneratedSources/Client.swiftas the source of truth for operation names and the top-levelComponents.Schemas.*types that cross the app boundary. - Treat
Modules/Sources/OpenAPI/GeneratedSources/Types.swiftas the source of truth for the field shape, enum cases, nested payloads, and convenience overloads of the schemas selected fromClient.swift. - Start from
Client.swiftto decide what needs syncing, then useTypes.swiftto implement the matching Domain model and mapper details. - Never hand-edit generated files under
Modules/Sources/OpenAPI/GeneratedSources/.
Boundary Rules
Modules/Sources/Domain/owns app-facing models, errors, and the handwrittenAPIClient.Modules/Sources/Adapters/APIClient/owns all translation between generated OpenAPI types and Domain types.- Generated operation names from
Client.swiftare the defaultAPIClientmethod names. If handwritten names drift, treat that as sync debt unless the user explicitly asks to preserve them. - For consumed
Components.Schemas.Xtypes, prefer a same-named handwritten Domain modelXinModules/Sources/Domain/Models/API/. - Mapper files should be keyed by the generated schema name so they stay easy to locate. Prefer
Requests/<SchemaName>+Mapping.swiftandResponses/<SchemaName>+Mapping.swift. - Do not copy generated transport wrappers like
Operations.*, status-code enums, response wrapper enums, orAPIProtocolhelper types intoDomain. - Every
APIClientchange must be reflected in bothModules/Sources/Adapters/APIClient/Live/andAppMock/ApiClientMock.swiftin the same change. - Do not introduce placeholder fallback values to satisfy schema drift (for example
title: "",agenda: niljust to keep old models, orUUID(uuidString:) ?? UUID()). Align the Domain model shape with the spec and fail fast (fatalError/force unwrap) when required contract values are invalid.
Files To Inspect First
Modules/Sources/OpenAPI/GeneratedSources/Client.swiftModules/Sources/OpenAPI/GeneratedSources/Types.swiftModules/Sources/Domain/Services/ApiClient.swiftModules/Sources/Domain/Models/API/Modules/Sources/Adapters/APIClient/Mappers/Modules/Sources/Adapters/APIClient/Live/AppMock/ApiClientMock.swiftModules/Sources/Domain/Models/Modules/Sources/Domain/Errors/
Quick Start
Run the inventory script first:
python3 .agents/skills/letsgrow-ios-sync-openapi/scripts/inventory_openapi_domain_sync.py
That script prints:
- generated endpoint names from
Client.swift - schema and payload details from
Types.swift - handwritten endpoint names from
Domain/Services/ApiClient.swift - missing or extra
APICliententries Client.swiftschema names without same-named Domain API modelsClient.swiftschema names without same-named mapper files- missing or extra initializer coverage in
LiveandAppMock
Workflow
Inventory the generated surface.
- Start with the script.
- Confirm which generated
Clientmethods are missing fromDomain.APIClient,Live, andAppMock. - Confirm which
Components.Schemas.*names referenced byClient.swiftdo not yet have same-named Domain API models or mapper files. - Open
Types.swiftfor the specific schemas you are about to sync so the Domain model and mapper match the generated field shape.
Decide the app-facing shape.
- Keep a single
APIClientdependency. - Add one handwritten closure per generated client method unless there is a deliberate, explicit reason to exclude it.
- Default to the generated operation name from
Client.swift. - Keep local-only helpers, such as cache listeners, explicit and justified rather than mixing them into the generated sync surface.
- Keep a single
Model the consumed schema types in
Domain.- Create or update handwritten
Domainmodels only for schema concepts the app uses. - Prefer
Modules/Sources/Domain/Models/API/for same-named models that mirror consumed OpenAPI-backed data. - Keep transport-only wrappers out of
Domain, but preserve schema naming for the actual payload model where it crosses the app boundary. - If the generated schema is only an adapter implementation detail and never crosses the handwritten
APIClientboundary, keep it in the adapter layer instead of adding a Domain model.
- Create or update handwritten
Update the mapper layer.
- Add request mappers from Domain inputs to
Components.Schemas.*. - Add response mappers from
Components.Schemas.*to Domain models. - Add error mappers when the endpoint exposes structured API errors.
- Keep mapper filenames keyed by generated schema names so a search for the schema lands in the correct mapper file immediately.
- Add request mappers from Domain inputs to
Update the live adapter.
- Update
Modules/Sources/Adapters/APIClient/Live/APIClient+Live.swiftplus the relevant splitAPIClient+Live*.swifthelper files. - Keep all
.ok,.internalServerError,.undocumented, and body decoding logic in the adapter layer. - Keep auth, cache, and session side effects in the live adapter, not in generated code and not in reducers.
- Update
Update the mock adapter.
- Keep
AppMock/ApiClientMock.swiftinitializer labels aligned withAPIClient. - Return deterministic placeholder Domain values using the same handwritten boundary types that the live client returns.
- Keep
Verify the boundary.
- Search for
Components.SchemasandOperations.outsideOpenAPIandAdapters/APIClient. - Feature code and
Domainshould not depend on generated transport types.
- Search for
Generation Heuristics
- Use the generated operation name as the default
APIClientmethod name. - If the endpoint returns a consumed schema already represented in
Domain, map it immediately and return the same-named handwritten Domain type. - If the endpoint returns no meaningful payload, expose
Void. - If the endpoint exposes an API-specific response wrapper whose only app value is one or two fields, unwrap it in the adapter and return the app-relevant values.
- If a generated enum casing differs from
Domain, normalize inAPIClientMappers.swift. - Prefer adding small mapper extensions over leaking generated DTOs into reducers, views, or
Domain.
When To Push Back
Push back on literal mirroring when the request implies copying generated transport types into Domain. In this repo:
Domainshould not ownOperations.*Domainshould not own generated HTTP response wrapper enumsDomainshould not return generatedComponents.Schemas.*directly fromAPIClientClient.swiftis the source of truth for what to sync first; do not start by cloning all ofTypes.swiftTypes.swiftis a schema reference, not a checklist for copying every generated type intoDomain
The right target is a handwritten Domain layer with same-named consumed payload models, while the adapter layer absorbs generated-contract churn and transport wrappers.
Validation
- Run the inventory script again after edits.
- If the inventory still shows missing
APIClient,Live, orAppMockcoverage for generated methods, the sync is incomplete. - If the inventory still shows missing same-named Domain API models or mapper files for consumed schemas, the sync is incomplete unless you have an explicit reason to keep those schemas adapter-only.
- Build with Xcode MCP if available.
- If the build is blocked by the OpenAPI generator plugin, still verify:
- no diagnostics in edited files
APIClientcontains the intended endpoint surfaceLiveandAppMockinitializer labels matchAPIClient- mapper code compiles cleanly in isolation when possible