Skip to content

Commit

Permalink
Merge pull request #3 from mattpolzin/openapikit-v2
Browse files Browse the repository at this point in the history
Openapikit v2
  • Loading branch information
mattpolzin authored Sep 28, 2020
2 parents 7386d29 + 040cef2 commit 2747214
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 66 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ jobs:
image: swift:5.2-bionic
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- run: swift test --enable-test-discovery --enable-code-coverage
- uses: mattpolzin/swift-codecov-action@0.3.0
- uses: mattpolzin/swift-codecov-action@0.4.0
with:
MINIMUM_COVERAGE: 85
12 changes: 6 additions & 6 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
},
{
"package": "OpenAPIKit",
"repositoryURL": "https://github.com/mattpolzin/OpenAPIKit",
"repositoryURL": "https://github.com/mattpolzin/OpenAPIKit.git",
"state": {
"branch": null,
"revision": "732251ae2eb1965c9323ae4f8024df908979b7cf",
"version": "1.0.1"
"revision": "dea99e8bb5d8241c5c9f815d0fb0ad487b9491b0",
"version": "2.0.0"
}
},
{
Expand All @@ -30,7 +30,7 @@
},
{
"package": "Sampleable",
"repositoryURL": "https://github.com/mattpolzin/Sampleable",
"repositoryURL": "https://github.com/mattpolzin/Sampleable.git",
"state": {
"branch": null,
"revision": "df44bf1a860481109dcf455e3c6daf0a0f1bc259",
Expand All @@ -42,8 +42,8 @@
"repositoryURL": "https://github.com/jpsim/Yams.git",
"state": {
"branch": null,
"revision": "81a65c4069c28011ee432f2858ba0de49b086677",
"version": "3.0.1"
"revision": "88caa2e6fffdbef2e91c2022d038576062042907",
"version": "4.0.0"
}
}
]
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ let package = Package(
targets: ["OpenAPIReflection"]),
],
dependencies: [
.package(url: "https://github.com/mattpolzin/OpenAPIKit.git", from: "1.0.0"),
.package(url: "https://github.com/mattpolzin/OpenAPIKit.git", from: "2.0.0"),
.package(url: "https://github.com/mattpolzin/Sampleable.git", from: "2.1.0")
],
targets: [
Expand Down
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,32 @@ See parent library at https://github.com/mattpolzin/OpenAPIKit

# OpenAPIReflection

A subset of supported Swift types require a `JSONEncoder` either to make an educated guess at the `JSONSchema` for the type or in order to turn arbitrary types into `AnyCodable` for use as schema examples or allowed values.
This library offers extended support for creating OpenAPI types from Swift types. Specifically, this library covers the subset of Swift types that require a `JSONEncoder` to either make an educated guess at the `JSONSchema` for the type or to turn arbitrary types into `AnyCodable` for use as schema examples or allowed values.

## Dates

Dates will create different OpenAPI representations depending on the encoding settings of the `JSONEncoder` passed into the schema construction method.

```swift
// encoder1 has `.iso8601` `dateEncodingStrategy`
let schema = Date().dateOpenAPISchemaGuess(using: encoder1)
// ^ equivalent to:
let sameSchema = JSONSchema.string(
format: .dateTime
)

// encoder2 has `.secondsSince1970` `dateEncodingStrategy`
let schema2 = Date().dateOpenAPISchemaGuess(using: encoder2)
// ^ equivalent to:
let sameSchema = JSONSchema.number(
format: .double
)
```

It will even try to take a guess given a custom formatter date decoding
strategy.

## Enums

Swift enums produce schemas with **allowed values** specified as long as they conform to `CaseIterable`, `Encodable`, and `AnyJSONCaseIterable` (the last of which is free given the former two).
```swift
Expand All @@ -20,6 +45,8 @@ let sameSchema = JSONSchema.string(
)
```

## Structs

Swift structs produce a best-guess schema as long as they conform to `Sampleable` and `Encodable`
```swift
struct Nested: Encodable, Sampleable {
Expand All @@ -43,3 +70,7 @@ let sameSchema = JSONSchema.object(
]
)
```

## Custom OpenAPI type representations

You can take the protocols offered by this library and OpenAPIKit and create arbitrarily complex OpenAPI types from your own Swift types. Right now, the only form of documentation on this subject is the fully realized example over in the [JSONAPI+OpenAPI](https://github.com/mattpolzin/JSONAPI-OpenAPI) library. Just look for conformances to `OpenAPISchemaType` and `OpenAPIEncodedSchemaType`.
14 changes: 4 additions & 10 deletions Sources/OpenAPIReflection/Date+OpenAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,16 @@ extension Date: DateOpenAPISchemaType {

case .secondsSince1970,
.millisecondsSince1970:
return .number(.init(format: .double,
required: true),
.init())
return .number(format: .double)

case .iso8601:
return .string(.init(format: .dateTime,
required: true),
.init())
return .string(format: .dateTime)

case .formatted(let formatter):
let hasTime = formatter.timeStyle != .none
let format: JSONTypeFormat.StringFormat = hasTime ? .dateTime : .date

return .string(.init(format: format,
required: true),
.init())
return .string(format: format)

@unknown default:
return nil
Expand All @@ -46,7 +40,7 @@ extension Date: DateOpenAPISchemaType {
extension Date: OpenAPIEncodedSchemaType {
public static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema {
guard let dateSchema: JSONSchema = try openAPISchemaGuess(for: Date(), using: encoder) else {
throw OpenAPI.TypeError.unknownNodeType(type(of: self))
throw OpenAPI.TypeError.unknownSchemaType(type(of: self))
}

return dateSchema
Expand Down
12 changes: 6 additions & 6 deletions Sources/OpenAPIReflection/OpenAPI+Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import OpenAPIKit

extension OpenAPI {
public enum TypeError: Swift.Error, CustomDebugStringConvertible {
case invalidNode
case unknownNodeType(Any.Type)
case invalidSchema
case unknownSchemaType(Any.Type)

public var debugDescription: String {
switch self {
case .invalidNode:
return "Invalid Node"
case .unknownNodeType(let type):
return "Could not determine OpenAPI node type of \(String(describing: type))"
case .invalidSchema:
return "Invalid Schema"
case .unknownSchemaType(let type):
return "Could not determine OpenAPI schema type of \(String(describing: type))"
}
}
}
Expand Down
32 changes: 15 additions & 17 deletions Sources/OpenAPIReflection/Sampleable+OpenAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ extension Sampleable where Self: Encodable {
public func genericOpenAPISchemaGuess<T>(for value: T, using encoder: JSONEncoder) throws -> JSONSchema {
// short circuit for dates
if let date = value as? Date,
let node = try type(of: date).dateOpenAPISchemaGuess(using: encoder) ?? reencodedSchemaGuess(for: date, using: encoder) {
let node = try type(of: date)
.dateOpenAPISchemaGuess(using: encoder)
?? reencodedSchemaGuess(for: date, using: encoder) {

return node
}

Expand All @@ -35,7 +38,7 @@ public func genericOpenAPISchemaGuess<T>(for value: T, using encoder: JSONEncode
}
}()

// try to snag an OpenAPI Node
// try to snag an OpenAPI Schema
let openAPINode: JSONSchema = try openAPISchemaGuess(for: child.value, using: encoder)
?? nestedGenericOpenAPISchemaGuess(for: child.value, using: encoder)

Expand All @@ -51,7 +54,7 @@ public func genericOpenAPISchemaGuess<T>(for value: T, using encoder: JSONEncode
}

if properties.count != mirror.children.count {
throw OpenAPI.TypeError.unknownNodeType(type(of: value))
throw OpenAPI.TypeError.unknownSchemaType(type(of: value))
}

// There should not be any duplication of keys since these are
Expand Down Expand Up @@ -137,28 +140,23 @@ internal func openAPISchemaGuess(for value: Any, using encoder: JSONEncoder) thr
let primitiveGuess: JSONSchema? = try {
switch value {
case is String:
return .string(.init(format: .generic,
required: true),
.init())
return .string

case is Int:
return .integer(.init(format: .generic,
required: true),
.init())
return .integer

case is Double:
return .number(.init(format: .double,
required: true),
.init())
return .number(
format: .double
)

case is Bool:
return .boolean(.init(format: .generic,
required: true))
return .boolean

case is Data:
return .string(.init(format: .binary,
required: true),
.init())
return .string(
format: .binary
)

case is DateOpenAPISchemaType:
// we don't know what Date will end up looking like without
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenAPIReflection/SchemaProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public protocol OpenAPIEncodedSchemaType {
}

extension OpenAPIEncodedSchemaType where Self: Sampleable, Self: Encodable {
public static func openAPINodeWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONSchema {
public static func openAPISchemaWithExample(using encoder: JSONEncoder = JSONEncoder()) throws -> JSONSchema {
let exampleData = try encoder.encode(Self.successSample ?? Self.sample)
let example = try JSONDecoder().decode(AnyCodable.self, from: exampleData)
return try openAPISchema(using: encoder).with(example: example)
Expand Down
40 changes: 29 additions & 11 deletions Sources/OpenAPIReflection/SwiftPrimitiveExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,44 @@ extension Optional: DateOpenAPISchemaType where Wrapped: DateOpenAPISchemaType {

extension Array: OpenAPIEncodedSchemaType where Element: OpenAPIEncodedSchemaType {
public static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema {
return .array(.init(format: .generic,
required: true),
.init(items: try Element.openAPISchema(using: encoder)))
return .array(
.init(
format: .generic,
required: true
),
.init(
items: try Element.openAPISchema(using: encoder)
)
)
}
}

extension Dictionary: RawOpenAPISchemaType where Key: RawRepresentable, Key.RawValue == String, Value: OpenAPISchemaType {
static public func rawOpenAPISchema() throws -> JSONSchema {
return .object(.init(format: .generic,
required: true),
.init(properties: [:],
additionalProperties: .init(Value.openAPISchema)))
return .object(
.init(
format: .generic,
required: true
),
.init(
properties: [:],
additionalProperties: .init(Value.openAPISchema)
)
)
}
}

extension Dictionary: OpenAPIEncodedSchemaType where Key == String, Value: OpenAPIEncodedSchemaType {
public static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema {
return .object(.init(format: .generic,
required: true),
.init(properties: [:],
additionalProperties: .init(try Value.openAPISchema(using: encoder))))
return .object(
.init(
format: .generic,
required: true
),
.init(
properties: [:],
additionalProperties: .init(try Value.openAPISchema(using: encoder))
)
)
}
}
11 changes: 1 addition & 10 deletions Tests/OpenAPIReflectionTests/GenericOpenAPISchemaTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,6 @@ import OpenAPIReflection
import Sampleable

final class GenericOpenAPISchemaTests: XCTestCase {
// func test_failsAsUnknown() {
// XCTAssertThrowsError(try FailsAsUnknown.genericOpenAPINode(using: JSONEncoder())) { error in
// guard let err = error as? OpenAPITypeError,
// case .unknownNodeType = err else {
// XCTFail("Expected unknown node type error")
// return
// }
// }
// }

func test_emptyObject() throws {
let node = try EmptyObjectType.genericOpenAPISchemaGuess(using: JSONEncoder())
Expand Down Expand Up @@ -412,7 +403,7 @@ extension GenericOpenAPISchemaTests {
self.val = val
}

init?(rawValue: Self.RawValue) {
init?(rawValue: String) {
self.val = rawValue
}
}
Expand Down
Loading

0 comments on commit 2747214

Please sign in to comment.