Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What This Project Does

SwaggerSwift is a Swift code generator that converts Swagger (OpenAPI 2.0) specifications into a fully-typed, async/await-ready Swift networking layer. It downloads specs from GitHub and generates Swift packages with API clients, data models, and shared utilities.

## Commands

```bash
# Build
swift build -c release
# Binary: .build/release/swaggerswift

# Run tests
swift test --parallel

# Run a single test
swift test --filter SwaggerSwiftMLTests.ParserTests/testSomething

# Run the generator locally
export GITHUB_TOKEN="ghp_…"
.build/release/swaggerswift --swagger-file-path ./SwaggerFile.yml --verbose
```

Code style is enforced by `swift-format` (`.swift-format` config): 120-char line limit, 4-space indentation.

## Architecture

Three targets in `Package.swift`:

**`SwaggerSwiftML`** — Parsing layer. Defines all Swagger data model types (`Operation`, `Parameter`, `Schema`, `Response`, etc.) and parses JSON/YAML specs via Yams. Produces an in-memory Swagger AST.

**`SwaggerSwiftCore`** — Code generation engine. Takes the parsed AST and produces Swift source files via Stencil templates. Key components:
- `Generator` — Main orchestrator; reads `SwaggerFile.yml`, downloads specs from GitHub, and drives the pipeline
- `ModelTypeResolver` — Maps Swagger types (primitives, enums, arrays, objects, `$ref`) to Swift types
- `APIRequestFactory` / `APIResponseTypeFactory` — Build async API client methods from `Operation` nodes
- `TemplateRenderer` — Renders Stencil templates from `Sources/SwaggerSwiftCore/Templates/`

**`SwaggerSwift`** (executable) — Thin CLI wrapper using swift-argument-parser. Entry point: `MyMain.swift`.

### Data Flow

```
SwaggerFile.yml → Generator → GitHub API (downloads specs)
→ SwaggerParser (SwaggerSwiftML)
→ ModelTypeResolver → ObjectModelFactory
→ APIRequestFactory → TemplateRenderer (Stencil)
→ FileWriter → Generated Swift Package
```

### Generated Output Structure

```
Generated/
└── MyAPI/
├── Package.swift
└── Sources/
├── MyAPIShared/ # Shared networking utilities (templates)
└── UserService/ # One target per service
├── UserServiceClient.swift
└── Models/
```

### Template System

Stencil templates live in `Sources/SwaggerSwiftCore/Templates/`. Each template maps to a generated file type (API client, model struct, enum, typealias, shared library files). Templates are loaded from the bundle at runtime — the `Dockerfile` copies them explicitly.

### Testing

- `SwaggerSwiftMLTests` — Unit tests for parsing; uses YAML/JSON fixtures in `Tests/SwaggerSwiftMLTests/TestResources/`
- `SwaggerSwiftCoreTests` — Unit tests for code generation; includes golden file (snapshot) tests

CI runs on both macOS (Xcode 26.2) and Linux (Swift 6.0) via `.github/workflows/build_and_test.yml`.
7 changes: 6 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ let package = Package(
"SwaggerSwiftCore",
]
),
.target(
name: "SwaggerSwiftIR",
dependencies: []
),
.target(
name: "SwaggerSwiftML",
dependencies: [
.product(name: "Yams", package: "Yams")
"SwaggerSwiftIR",
.product(name: "Yams", package: "Yams"),
]
),
.target(
Expand Down
34 changes: 34 additions & 0 deletions Sources/SwaggerSwiftIR/IRDocument.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/// The top-level representation of a parsed API specification.
/// Both Swagger 2.0 and OpenAPI 3.x parsers convert their formats into this type.
public struct IRDocument {
public let info: IRInfo
public let servers: [IRServer]
/// All named path items, keyed by path string (e.g. "/pets/{id}").
public let paths: [String: IRPathItem]
/// All named schemas available for `$ref` resolution, keyed by schema name.
public let schemas: [String: IRSchema]

public init(
info: IRInfo,
servers: [IRServer],
paths: [String: IRPathItem],
schemas: [String: IRSchema]
) {
self.info = info
self.servers = servers
self.paths = paths
self.schemas = schemas
}
}

public struct IRInfo {
public let title: String
public let description: String?
public let version: String

public init(title: String, description: String?, version: String) {
self.title = title
self.description = description
self.version = version
}
}
38 changes: 38 additions & 0 deletions Sources/SwaggerSwiftIR/IROperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/// A single API operation (endpoint).
public struct IROperation {
public let operationId: String?
public let summary: String?
public let description: String?
public let deprecated: Bool
public let tags: [String]
/// Path, query, header, and cookie parameters. Body/form parameters are expressed via `requestBody`.
public let parameters: [IRParameter]
/// The request body, if this operation accepts one.
public let requestBody: IRRequestBody?
/// Possible responses keyed by response code.
public let responses: [IRResponseCode: IRResponse]
/// Vendor extension fields (x-*).
public let customFields: [String: String]

public init(
operationId: String?,
summary: String?,
description: String?,
deprecated: Bool,
tags: [String],
parameters: [IRParameter],
requestBody: IRRequestBody?,
responses: [IRResponseCode: IRResponse],
customFields: [String: String]
) {
self.operationId = operationId
self.summary = summary
self.description = description
self.deprecated = deprecated
self.tags = tags
self.parameters = parameters
self.requestBody = requestBody
self.responses = responses
self.customFields = customFields
}
}
59 changes: 59 additions & 0 deletions Sources/SwaggerSwiftIR/IRParameter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/// A single parameter on an operation. Body and form data are not modelled here —
/// use `IRRequestBody` on `IROperation` instead.
public struct IRParameter {
public let name: String
public let location: IRParameterLocation
public let description: String?
public let required: Bool
public let schema: IRSchema
/// Controls serialization of array/object values (replaces Swagger 2.0 `collectionFormat`).
public let style: IRParameterStyle?
/// Whether array/object values are exploded into separate query parameters.
public let explode: Bool?
public let allowEmptyValue: Bool?

public init(
name: String,
location: IRParameterLocation,
description: String?,
required: Bool,
schema: IRSchema,
style: IRParameterStyle? = nil,
explode: Bool? = nil,
allowEmptyValue: Bool? = nil
) {
self.name = name
self.location = location
self.description = description
self.required = required
self.schema = schema
self.style = style
self.explode = explode
self.allowEmptyValue = allowEmptyValue
}
}

public enum IRParameterLocation {
case path
case query
case header
case cookie
}

/// Serialization style for parameter values. Replaces Swagger 2.0 `collectionFormat`.
public enum IRParameterStyle: String {
/// Comma-separated values. Default for path and header parameters.
case simple
/// Separate key=value pairs. Default for query and cookie parameters.
case form
/// Dot-prefixed values (path parameters only).
case label
/// Semicolon-prefixed values (path parameters only).
case matrix
/// Space-separated values (query parameters only).
case spaceDelimited
/// Pipe-separated values (query parameters only).
case pipeDelimited
/// Nested object expansion via bracket notation (query parameters only).
case deepObject
}
60 changes: 60 additions & 0 deletions Sources/SwaggerSwiftIR/IRPathItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/// A single path in the API, containing an operation per HTTP method.
public struct IRPathItem {
/// Parameters that apply to all operations on this path unless overridden at the operation level.
public let parameters: [IRParameter]
public let get: IROperation?
public let put: IROperation?
public let post: IROperation?
public let delete: IROperation?
public let options: IROperation?
public let head: IROperation?
public let patch: IROperation?
public let trace: IROperation?

public init(
parameters: [IRParameter] = [],
get: IROperation? = nil,
put: IROperation? = nil,
post: IROperation? = nil,
delete: IROperation? = nil,
options: IROperation? = nil,
head: IROperation? = nil,
patch: IROperation? = nil,
trace: IROperation? = nil
) {
self.parameters = parameters
self.get = get
self.put = put
self.post = post
self.delete = delete
self.options = options
self.head = head
self.patch = patch
self.trace = trace
}

/// Returns the operation for a given HTTP method, if defined.
public func operation(for method: HTTPMethod) -> IROperation? {
switch method {
case .get: return get
case .put: return put
case .post: return post
case .delete: return delete
case .options: return options
case .head: return head
case .patch: return patch
case .trace: return trace
}
}

/// All (method, operation) pairs defined on this path item.
public var operations: [(method: HTTPMethod, operation: IROperation)] {
HTTPMethod.allCases.compactMap { method in
operation(for: method).map { (method, $0) }
}
}
}

public enum HTTPMethod: String, CaseIterable {
case get, put, post, delete, options, head, patch, trace
}
39 changes: 39 additions & 0 deletions Sources/SwaggerSwiftIR/IRRequestBody.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/// The request body of an operation. Replaces Swagger 2.0's `in: body` and `in: formData` parameters.
public struct IRRequestBody {
public let description: String?
public let required: Bool
/// Media type objects keyed by MIME type string (e.g. `"application/json"`).
public let content: [String: IRMediaType]

public init(description: String?, required: Bool, content: [String: IRMediaType]) {
self.description = description
self.required = required
self.content = content
}
}

/// A schema and optional encoding rules for a specific media type.
public struct IRMediaType {
public let schema: IRSchema?
/// Encoding rules for individual properties, used in multipart/form-data and
/// application/x-www-form-urlencoded request bodies.
public let encoding: [String: IREncoding]

public init(schema: IRSchema?, encoding: [String: IREncoding] = [:]) {
self.schema = schema
self.encoding = encoding
}
}

/// Encoding rules for a single property in a multipart or form-urlencoded request body.
public struct IREncoding {
public let contentType: String?
public let style: IRParameterStyle?
public let explode: Bool?

public init(contentType: String?, style: IRParameterStyle?, explode: Bool?) {
self.contentType = contentType
self.style = style
self.explode = explode
}
}
24 changes: 24 additions & 0 deletions Sources/SwaggerSwiftIR/IRResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// A possible response from an operation.
public struct IRResponse {
public let description: String?
/// Response body schemas keyed by MIME type string (e.g. `"application/json"`).
public let content: [String: IRMediaType]
/// Response headers keyed by header name.
public let headers: [String: IRSchema]

public init(description: String?, content: [String: IRMediaType], headers: [String: IRSchema] = [:]) {
self.description = description
self.content = content
self.headers = headers
}
}

/// The HTTP status code for a response.
public enum IRResponseCode: Hashable {
/// A specific HTTP status code (e.g. 200, 404).
case status(Int)
/// A wildcard range (e.g. "2XX", "4XX"). Only valid in OpenAPI 3.x.
case range(String)
/// The fallback response when no specific status code matches.
case `default`
}
Loading
Loading