Skip to content
Merged
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
The FeatherOpenAPI library makes it easy to define OpenAPI specifications using Swift in a type-safe way.

[
![Release: 1.0.0-rc.1](https://img.shields.io/badge/Release-1%2E0%2E0--rc%2E1-F05138)
![Release: 1.0.0-rc.2](https://img.shields.io/badge/Release-1%2E0%2E0--rc%2E2-F05138)
](
https://github.com/feather-framework/feather-openapi/releases/tag/1.0.0-rc.1
https://github.com/feather-framework/feather-openapi/releases/tag/1.0.0-rc.2
)

## Features
Expand Down Expand Up @@ -35,7 +35,7 @@ The FeatherOpenAPI library makes it easy to define OpenAPI specifications using
Use Swift Package Manager; add the dependency to your `Package.swift` file:

```swift
.package(url: "https://github.com/feather-framework/feather-openapi", exact: "1.0.0-rc.1"),
.package(url: "https://github.com/feather-framework/feather-openapi", exact: "1.0.0-rc.2"),
```

Then add `FeatherOpenAPI` to your target dependencies:
Expand Down
24 changes: 21 additions & 3 deletions Sources/FeatherOpenAPI/Content/ContentMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,28 @@
// Created by Tibor Bödecs on 2026. 01. 23.
//

public import OpenAPIKit30
import OpenAPIKit30

/// Ordered map of content types to content definitions.
public typealias ContentMap = OrderedDictionary<
OpenAPI.ContentType,
public typealias ContentMap = OrderedMap<
ContentTypeKey,
any ContentRepresentable
>

extension OrderedMap
where Key == ContentTypeKey, Value == any ContentRepresentable {
var openAPIContentMap:
OrderedDictionary<
OpenAPI.ContentType,
OpenAPI.Content
>
{
var results: OrderedDictionary<OpenAPI.ContentType, OpenAPI.Content> =
[:]
for (key, content) in self {
results[OpenAPI.ContentType(rawValue: key.rawValue)!] =
content.openAPIContent()
}
return results
}
}
27 changes: 27 additions & 0 deletions Sources/FeatherOpenAPI/Content/ContentTypeKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// ContentTypeKey.swift
// feather-openapi
//

/// A media type key that does not expose OpenAPIKit types to API generators.
public struct ContentTypeKey: Hashable, Sendable, ExpressibleByStringLiteral {
public let rawValue: String

public init(_ rawValue: String) {
self.rawValue = rawValue
}

public init(stringLiteral value: String) {
self.init(value)
}

public static let json = Self("application/json")
public static let xml = Self("application/xml")
public static let form = Self("application/x-www-form-urlencoded")
public static let multipartForm = Self("multipart/form-data")
public static let octetStream = Self("application/octet-stream")

public static func other(_ rawValue: String) -> Self {
Self(rawValue)
}
}
21 changes: 20 additions & 1 deletion Sources/FeatherOpenAPI/Document/DocumentRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ extension DocumentRepresentable {
openAPIVersion: .v3_0_0,
info: info.openAPIInfo(),
servers: servers.map { $0.openAPIServer() },
paths: paths.mapValues { .init($0.openAPIPathItem()) },
paths: openAPIPaths,
components: components.openAPIComponents(),
security: referencedSecurityRequirements.map {
$0.openAPISecurityRequirement()
Expand All @@ -89,4 +89,23 @@ extension DocumentRepresentable {
vendorExtensions: vendorExtensions
)
}

private var openAPIPaths:
OrderedDictionary<
OpenAPI.Path,
Either<JSONReference<OpenAPI.PathItem>, OpenAPI.PathItem>
>
{
var results:
OrderedDictionary<
OpenAPI.Path,
Either<JSONReference<OpenAPI.PathItem>, OpenAPI.PathItem>
> = [:]
for (path, item) in paths {
results[OpenAPI.Path(rawValue: path)] = .init(
item.openAPIPathItem()
)
}
return results
}
}
4 changes: 1 addition & 3 deletions Sources/FeatherOpenAPI/Header/HeaderMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
// Created by Tibor Bödecs on 2026. 01. 23.
//

public import OpenAPIKit30

/// Ordered map of header names to header definitions.
public typealias HeaderMap = OrderedDictionary<
public typealias HeaderMap = OrderedMap<
String,
any HeaderRepresentable
>
72 changes: 72 additions & 0 deletions Sources/FeatherOpenAPI/Map/OrderedMap.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// OrderedMap.swift
// feather-openapi
//

import OpenAPIKit30

/// An insertion-ordered map used by FeatherOpenAPI's public abstractions.
public struct OrderedMap<Key: Hashable, Value>:
ExpressibleByDictionaryLiteral,
Sequence
{
private var storage: OrderedDictionary<Key, Value>

var orderedDictionary: OrderedDictionary<Key, Value> { storage }

public init(dictionaryLiteral elements: (Key, Value)...) {
self.storage = [:]
for (key, value) in elements {
self.storage[key] = value
}
}

public init() {
self.storage = [:]
}

public var keys: [Key] { Array(storage.keys) }
public var values: [Value] { Array(storage.values) }
public var isEmpty: Bool { storage.isEmpty }
public var count: Int { storage.count }

public subscript(key: Key) -> Value? {
get { storage[key] }
set { storage[key] = newValue }
}

public mutating func merge(
_ other: Self,
uniquingKeysWith combine: (Value, Value) throws -> Value
) rethrows {
try storage.merge(other.storage, uniquingKeysWith: combine)
}

public func mapValues<Transformed>(
_ transform: (Value) throws -> Transformed
) rethrows -> OrderedMap<Key, Transformed> {
var result = OrderedMap<Key, Transformed>()
for (key, value) in storage {
result[key] = try transform(value)
}
return result
}

public struct Iterator: IteratorProtocol {
private var iterator: IndexingIterator<[(Key, Value)]>

fileprivate init(_ elements: [(Key, Value)]) {
self.iterator = elements.makeIterator()
}

public mutating func next() -> (Key, Value)? {
iterator.next()
}
}

public func makeIterator() -> Iterator {
Iterator(storage.map { ($0.key, $0.value) })
}
}

extension OrderedMap: Sendable where Key: Sendable, Value: Sendable {}
29 changes: 24 additions & 5 deletions Sources/FeatherOpenAPI/Operation/OperationRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,18 @@ extension OperationRepresentable {
tags.isEmpty ? nil : tags.map { $0.name }
}

private var openAPISecurityRequirements: [OpenAPI.SecurityRequirement]? {
private func openAPIResponses(
_ responseMap: ResponseMap
) -> OrderedDictionary<
OpenAPI.Response.StatusCode,
Either<JSONReference<OpenAPI.Response>, OpenAPI.Response>
> {
responseMap.openAPIResponseMap
}

private func openAPISecurityRequirements(
_ security: [any SecurityRequirementRepresentable]?
) -> [OpenAPI.SecurityRequirement]? {
guard let security, !security.isEmpty else {
return nil
}
Expand All @@ -106,6 +117,14 @@ extension OperationRepresentable {
/// Builds an OpenAPI operation.
/// - Returns: The OpenAPI operation.
public func openAPIOperation() -> OpenAPI.Operation {
openAPIOperation(responseMap: responseMap, security: security)
}

/// Builds an OpenAPI operation using custom responses and security.
public func openAPIOperation(
responseMap: ResponseMap,
security: [any SecurityRequirementRepresentable]?
) -> OpenAPI.Operation {
if let requestBody {
return .init(
tags: openAPITags,
Expand All @@ -115,10 +134,10 @@ extension OperationRepresentable {
operationId: operationId,
parameters: parameters.map { $0.openAPIParameter() },
requestBody: requestBody.openAPIRequestBody(),
responses: responseMap.mapValues { $0.openAPIResponse() },
responses: openAPIResponses(responseMap),
callbacks: [:],
deprecated: deprecated,
security: openAPISecurityRequirements,
security: openAPISecurityRequirements(security),
servers: servers?.map { $0.openAPIServer() },
vendorExtensions: vendorExtensions
)
Expand All @@ -130,10 +149,10 @@ extension OperationRepresentable {
externalDocs: nil,
operationId: operationId,
parameters: parameters.map { $0.openAPIParameter() },
responses: responseMap.mapValues { $0.openAPIResponse() },
responses: openAPIResponses(responseMap),
callbacks: [:],
deprecated: deprecated,
security: openAPISecurityRequirements,
security: openAPISecurityRequirements(security),
servers: servers?.map { $0.openAPIServer() },
vendorExtensions: vendorExtensions
)
Expand Down
6 changes: 2 additions & 4 deletions Sources/FeatherOpenAPI/PathItem/PathMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
// Created by Tibor Bödecs on 2026. 01. 23.
//

public import OpenAPIKit30

/// Ordered map of OpenAPI paths to path items.
public typealias PathMap = OrderedDictionary<
OpenAPI.Path,
public typealias PathMap = OrderedMap<
String,
any PathItemRepresentable
>
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ extension RequestBodyRepresentable {
.init(
.init(
description: description,
content: contentMap.mapValues { $0.openAPIContent() },
content: contentMap.openAPIContentMap,
required: `required`,
vendorExtensions: vendorExtensions
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Tibor Bödecs on 2026. 01. 22.
//

public import OpenAPIKit30

/// Request body with binary content.
public protocol BinaryRequestBodyRepresentable: RequestBodyRepresentable {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Tibor Bödecs on 2026. 01. 22.
//

public import OpenAPIKit30

/// Request body with form-encoded content.
public protocol FormRequestBodyRepresentable: RequestBodyRepresentable {
/// The schema type used in the form content.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Tibor Bödecs on 2026. 01. 22.
//

public import OpenAPIKit30

/// Request body with JSON content.
public protocol JSONRequestBodyRepresentable: RequestBodyRepresentable {
/// The JSON schema type used in the request body.
Expand Down
27 changes: 24 additions & 3 deletions Sources/FeatherOpenAPI/Response/Abstraction/ResponseMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,31 @@
// Created by Tibor Bödecs on 2026. 01. 23.
//

public import OpenAPIKit30
import OpenAPIKit30

/// Ordered map of response status codes to response definitions.
public typealias ResponseMap = OrderedDictionary<
OpenAPI.Response.StatusCode,
public typealias ResponseMap = OrderedMap<
ResponseStatusKey,
any ResponseRepresentable
>

extension ResponseMap {
var openAPIResponseMap: OpenAPI.Response.Map {
var results: OpenAPI.Response.Map = [:]
for (key, response) in self {
let statusCode: OpenAPI.Response.StatusCode
switch key {
case .status(let value):
statusCode = .status(code: value)
case .default:
statusCode = .default
case .range(let range):
statusCode = .range(
OpenAPI.Response.StatusCode.Range(rawValue: range.rawValue)!
)
}
results[statusCode] = response.openAPIResponse()
}
return results
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,17 @@ extension ResponseRepresentable {
JSONReference<OpenAPI.Response>, OpenAPI.Response
> {
let responseHeaders: OpenAPI.Header.Map? =
headerMap.isEmpty ? nil : headerMap.mapValues { $0.openAPIHeader() }
headerMap.isEmpty
? nil
: headerMap.orderedDictionary.mapValues {
$0.openAPIHeader()
}

return .init(
.init(
description: description,
headers: responseHeaders,
content: contentMap.mapValues { $0.openAPIContent() },
content: contentMap.openAPIContentMap,
links: [:],
vendorExtensions: vendorExtensions
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// ResponseStatusKey.swift
// feather-openapi
//

/// A response status code or range, independent of OpenAPIKit.
public enum ResponseStatusKey: Hashable, Sendable,
ExpressibleByIntegerLiteral
{
case status(Int)
case `default`
case range(Range)

public enum Range: String, Hashable, Sendable {
case information = "1XX"
case success = "2XX"
case redirect = "3XX"
case clientError = "4XX"
case serverError = "5XX"
}

public init(integerLiteral value: Int) {
self = .status(value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
// Created by Tibor Bödecs on 2026. 01. 23.
//

public import OpenAPIKit30

/// Response with binary content.
public protocol BinaryResponseRepresentable: ResponseRepresentable {

Expand Down
Loading
Loading