Skip to content

Commit

Permalink
Merge pull request #8 from mattpolzin/feature/dictionary_ordering
Browse files Browse the repository at this point in the history
introduce dictionary ordering.
  • Loading branch information
mattpolzin authored Jan 23, 2020
2 parents 70ff24b + a2793d1 commit 4a42df0
Show file tree
Hide file tree
Showing 19 changed files with 1,361 additions and 116 deletions.
27 changes: 27 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
"version": "0.2.3"
}
},
{
"package": "FineJSON",
"repositoryURL": "https://github.com/omochi/FineJSON.git",
"state": {
"branch": null,
"revision": "05101709243cb66d80c92e645210a3b80cf4e17f",
"version": "1.14.0"
}
},
{
"package": "Poly",
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
Expand All @@ -19,6 +28,15 @@
"version": "2.3.1"
}
},
{
"package": "RichJSONParser",
"repositoryURL": "https://github.com/omochi/RichJSONParser.git",
"state": {
"branch": null,
"revision": "263e2ecfe88d0500fa99e4cbc8c948529d335534",
"version": "3.0.0"
}
},
{
"package": "Sampleable",
"repositoryURL": "https://github.com/mattpolzin/Sampleable.git",
Expand All @@ -27,6 +45,15 @@
"revision": "df44bf1a860481109dcf455e3c6daf0a0f1bc259",
"version": "2.1.0"
}
},
{
"package": "Yams",
"repositoryURL": "https://github.com/jpsim/Yams.git",
"state": {
"branch": null,
"revision": "c947a306d2e80ecb2c0859047b35c73b8e1ca27f",
"version": "2.0.0"
}
}
]
},
Expand Down
6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")),
.package(url: "https://github.com/mattpolzin/Sampleable.git", .upToNextMajor(from: "2.0.0")),
.package(url: "https://github.com/Flight-School/AnyCodable.git", .upToNextMinor(from: "0.2.2"))
.package(url: "https://github.com/Flight-School/AnyCodable.git", .upToNextMinor(from: "0.2.2")),
.package(url: "https://github.com/jpsim/Yams.git", from: "2.0.0"), // just for tests
.package(url: "https://github.com/omochi/FineJSON.git", from: "1.14.0") // just for tests
],
targets: [
.target(
name: "OpenAPIKit",
dependencies: ["Poly", "Sampleable", "AnyCodable"]),
.testTarget(
name: "OpenAPIKitTests",
dependencies: ["OpenAPIKit"]),
dependencies: ["OpenAPIKit", "Yams", "FineJSON"]),
],
swiftLanguageVersions: [ .v5 ]
)
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ A library containing Swift types that encode to- and decode from [OpenAPI](https
- [Usage](#usage)
- [Decoding OpenAPI Documents](#decoding-openapi-documents)
- [Encoding OpenAPI Documents](#encoding-openapi-documents)
- [A note on dictionary ordering](#a-note-on-dictionary-ordering)
- [Generating OpenAPI Documents](#generating-openapi-documents)
- [OpenAPI Document structure](#openapi-document-structure)
- [Notes](#notes)
Expand Down Expand Up @@ -67,6 +68,11 @@ let encoder = ... // JSONEncoder() or YAMLEncoder()
let encodedOpenAPIDoc = try encoder.encode(openAPIDoc)
```

### A note on dictionary ordering
The **Foundation** library's `JSONEncoder` and `JSONDecoder` do not make any guarantees about the ordering of keyed containers. This means decoding a JSON OpenAPI Document and then encoding again might result in the document's various hashed structures being in a totally different order.

If retaining order is important for your use-case, I recommend the [**Yams**](https://github.com/jpsim/Yams) and [**FineJSON**](https://github.com/omochi/FineJSON) libraries for YAML and JSON respectively.

### Generating OpenAPI Documents

See [**VaporOpenAPI**](https://github.com/mattpolzin/VaporOpenAPI) / [VaporOpenAPIExample](https://github.com/mattpolzin/VaporOpenAPIExample) for an example of generating OpenAPI from a Vapor application's routes.
Expand Down Expand Up @@ -197,6 +203,8 @@ This library *is* opinionated about a few defaults when you use the Swift types,
* ex. `JSONSchema.string` is a required "string" type.
* ex. `JSONSchema.string(required: false)` is an optional "string" type.

See [**A note on dictionary ordering**](#a-note-on-dictionary-ordering) before deciding on an encoder/decoder to use with this library.

## Project Status

### OpenAPI Object (`OpenAPI.Document`)
Expand Down
18 changes: 9 additions & 9 deletions Sources/OpenAPIKit/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ extension OpenAPI {
// public let links:
// public let callbacks:

public init(schemas: [String: SchemasDict.Value] = [:],
responses: [String: ResponsesDict.Value] = [:],
parameters: [String: ParametersDict.Value] = [:],
examples: [String: ExamplesDict.Value] = [:],
requestBodies: [String: RequestBodiesDict.Value] = [:],
headers: [String: HeadersDict.Value] = [:],
securitySchemes: [String: SecuritySchemesDict.Value] = [:]) {
public init(schemas: OrderedDictionary<String, SchemasDict.Value> = [:],
responses: OrderedDictionary<String, ResponsesDict.Value> = [:],
parameters: OrderedDictionary<String, ParametersDict.Value> = [:],
examples: OrderedDictionary<String, ExamplesDict.Value> = [:],
requestBodies: OrderedDictionary<String, RequestBodiesDict.Value> = [:],
headers: OrderedDictionary<String, HeadersDict.Value> = [:],
securitySchemes: OrderedDictionary<String, SecuritySchemesDict.Value> = [:]) {
self.schemas = SchemasDict(schemas)
self.responses = ResponsesDict(responses)
self.parameters = ParametersDict(parameters)
Expand Down Expand Up @@ -64,7 +64,7 @@ extension OpenAPI {
public static var refName: String { return "responses" }
}

public typealias ResponsesDict = RefDict<Components, ResponsesName, JSONSchema>
public typealias ResponsesDict = RefDict<Components, ResponsesName, OpenAPI.Response>

public enum ParametersName: RefName {
public static var refName: String { return "parameters" }
Expand All @@ -82,7 +82,7 @@ extension OpenAPI {
public static var refName: String { return "requestBodies" }
}

public typealias RequestBodiesDict = RefDict<Components, RequestBodiesName, OpenAPI.Example>
public typealias RequestBodiesDict = RefDict<Components, RequestBodiesName, OpenAPI.Request>

public enum HeadersName: RefName {
public static var refName: String { return "headers" }
Expand Down
20 changes: 9 additions & 11 deletions Sources/OpenAPIKit/Content.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension OpenAPI {
public let schema: Either<JSONReference<Components, JSONSchema>, JSONSchema>
public let example: AnyCodable?
public let examples: Example.Map?
public let encoding: [String: Encoding]?
public let encoding: OrderedDictionary<String, Encoding>?

/// Dictionary of vendor extensions.
///
Expand All @@ -25,7 +25,7 @@ extension OpenAPI {

public init(schema: Either<JSONReference<Components, JSONSchema>, JSONSchema>,
example: AnyCodable? = nil,
encoding: [String: Encoding]? = nil,
encoding: OrderedDictionary<String, Encoding>? = nil,
vendorExtensions: [String: AnyCodable] = [:]) {
self.schema = schema
self.example = example
Expand All @@ -36,7 +36,7 @@ extension OpenAPI {

public init(schemaReference: JSONReference<Components, JSONSchema>,
example: AnyCodable? = nil,
encoding: [String: Encoding]? = nil,
encoding: OrderedDictionary<String, Encoding>? = nil,
vendorExtensions: [String: AnyCodable] = [:]) {
self.schema = .init(schemaReference)
self.example = example
Expand All @@ -47,7 +47,7 @@ extension OpenAPI {

public init(schema: JSONSchema,
example: AnyCodable? = nil,
encoding: [String: Encoding]? = nil,
encoding: OrderedDictionary<String, Encoding>? = nil,
vendorExtensions: [String: AnyCodable] = [:]) {
self.schema = .init(schema)
self.example = example
Expand All @@ -58,7 +58,7 @@ extension OpenAPI {

public init(schema: Either<JSONReference<Components, JSONSchema>, JSONSchema>,
examples: Example.Map?,
encoding: [String: Encoding]? = nil,
encoding: OrderedDictionary<String, Encoding>? = nil,
vendorExtensions: [String: AnyCodable] = [:]) {
self.schema = schema
self.examples = examples
Expand All @@ -69,7 +69,7 @@ extension OpenAPI {

public init(schemaReference: JSONReference<Components, JSONSchema>,
examples: Example.Map?,
encoding: [String: Encoding]? = nil,
encoding: OrderedDictionary<String, Encoding>? = nil,
vendorExtensions: [String: AnyCodable] = [:]) {
self.schema = .init(schemaReference)
self.examples = examples
Expand All @@ -80,7 +80,7 @@ extension OpenAPI {

public init(schema: JSONSchema,
examples: Example.Map?,
encoding: [String: Encoding]? = nil,
encoding: OrderedDictionary<String, Encoding>? = nil,
vendorExtensions: [String: AnyCodable] = [:]) {
self.schema = .init(schema)
self.examples = examples
Expand All @@ -92,11 +92,9 @@ extension OpenAPI {
}

extension OpenAPI.Content {
public typealias Map = [OpenAPI.ContentType: OpenAPI.Content]
public typealias Map = OrderedDictionary<OpenAPI.ContentType, OpenAPI.Content>
}



extension OpenAPI.Content {
internal static func firstExample(from exampleDict: OpenAPI.Example.Map) -> AnyCodable? {
return exampleDict
Expand Down Expand Up @@ -137,7 +135,7 @@ extension OpenAPI.Content: Decodable {
}

schema = try container.decode(Either<JSONReference<OpenAPI.Components, JSONSchema>, JSONSchema>.self, forKey: .schema)
encoding = try container.decodeIfPresent([String: Encoding].self, forKey: .encoding)
encoding = try container.decodeIfPresent(OrderedDictionary<String, Encoding>.self, forKey: .encoding)

if container.contains(.example) {
example = try container.decode(AnyCodable.self, forKey: .example)
Expand Down
17 changes: 2 additions & 15 deletions Sources/OpenAPIKit/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,7 @@ extension OpenAPI.Document: Encodable {
try container.encode(servers, forKey: .servers)
}

// Hack to work around Dictionary encoding
// itself as an array in this case:
let pathsStringKeyedDict = Dictionary(
paths.map { ($0.key.rawValue, $0.value) },
uniquingKeysWith: { $1 }
)
try container.encode(pathsStringKeyedDict, forKey: .paths)
try container.encode(paths, forKey: .paths)

if !components.isEmpty {
try container.encode(components, forKey: .components)
Expand Down Expand Up @@ -115,14 +109,7 @@ extension OpenAPI.Document: Decodable {

servers = try container.decodeIfPresent([OpenAPI.Server].self, forKey: .servers) ?? []

// hacky workaround for Dictionary bug
let pathsDict = try container.decode([String: Either<JSONReference<OpenAPI.Components, OpenAPI.PathItem>, OpenAPI.PathItem>].self, forKey: .paths)
paths = Dictionary(pathsDict.map { args in
let (pathString, pathItem) = args

return (OpenAPI.PathComponents(rawValue: pathString), pathItem)
},
uniquingKeysWith: { $1 })
paths = try container.decode(OpenAPI.PathItem.Map.self, forKey: .paths)

let components = try container.decodeIfPresent(OpenAPI.Components.self, forKey: .components) ?? .noComponents
self.components = components
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenAPIKit/Example.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ extension OpenAPI {
}

extension OpenAPI.Example {
public typealias Map = [String: Either<JSONReference<OpenAPI.Components, OpenAPI.Example>, OpenAPI.Example>]
public typealias Map = OrderedDictionary<String, Either<JSONReference<OpenAPI.Components, OpenAPI.Example>, OpenAPI.Example>>
}

// MARK: - Codable
Expand Down
18 changes: 3 additions & 15 deletions Sources/OpenAPIKit/Header.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension OpenAPI {
public let deprecated: Bool // default is false
public let schemaOrContent: Either<SchemaProperty, OpenAPI.Content.Map>

public typealias Map = [String: Either<JSONReference<OpenAPI.Components, Header>, Header>]
public typealias Map = OrderedDictionary<String, Either<JSONReference<OpenAPI.Components, Header>, Header>>

public typealias SchemaProperty = Either<JSONReference<OpenAPI.Components, JSONSchema>, JSONSchema>

Expand Down Expand Up @@ -87,13 +87,7 @@ extension OpenAPI.Header: Encodable {
case .a(let schema):
try container.encode(schema, forKey: .schema)
case .b(let contentMap):
// Hack to work around Dictionary encoding
// itself as an array in this case:
let stringKeyedDict = Dictionary(
contentMap.map { ($0.key.rawValue, $0.value) },
uniquingKeysWith: { $1 }
)
try container.encode(stringKeyedDict, forKey: .content)
try container.encode(contentMap, forKey: .content)
}

try description.encodeIfNotNil(to: &container, forKey: .description)
Expand All @@ -110,13 +104,7 @@ extension OpenAPI.Header: Decodable {

required = try container.decodeIfPresent(Bool.self, forKey: .required) ?? false

// hacky workaround for Dictionary decoding bug
let maybeContentDict = try container.decodeIfPresent([String: OpenAPI.Content].self, forKey: .content)
let maybeContent = maybeContentDict.map { contentDict in
Dictionary(contentDict.compactMap { contentTypeString, content in
OpenAPI.ContentType(rawValue: contentTypeString).map { ($0, content) } },
uniquingKeysWith: { $1 })
}
let maybeContent = try container.decodeIfPresent(OpenAPI.Content.Map.self, forKey: .content)

let maybeSchema = try container.decodeIfPresent(SchemaProperty.self, forKey: .schema)

Expand Down
14 changes: 11 additions & 3 deletions Sources/OpenAPIKit/JSON Utility/JSONReference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ public struct RefDict<Root: ReferenceRoot, Name: RefName, RefType: Equatable & C
public typealias Value = RefType
public typealias Key = String

let dict: [String: RefType]
let dict: OrderedDictionary<String, RefType>

public init(_ dict: [String: RefType]) {
public init(_ dict: OrderedDictionary<String, RefType>) {
self.dict = dict
}

Expand Down Expand Up @@ -155,6 +155,14 @@ extension JSONReference: Decodable {

let referenceString = try container.decode(String.self, forKey: .ref)

guard referenceString.count > 0 else {
throw DecodingError.dataCorruptedError(forKey: .ref, in: container, debugDescription: "Expected a reference string, but found an empty string instead.")
}

guard referenceString.contains("#") else {
throw DecodingError.dataCorruptedError(forKey: .ref, in: container, debugDescription: "Expected a reference string to contain '#', but found \"\(referenceString)\" instead.")
}

if referenceString.first == "#" {
// TODO: try to parse ref to components
self = .internal(.unsafe(referenceString))
Expand All @@ -176,6 +184,6 @@ extension RefDict: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

dict = try container.decode([String : RefType].self)
dict = try container.decode(OrderedDictionary<String, RefType>.self)
}
}
16 changes: 3 additions & 13 deletions Sources/OpenAPIKit/Path Item/Operation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ extension OpenAPI.PathItem {
description: String? = nil,
externalDocs: OpenAPI.ExternalDoc? = nil,
operationId: String? = nil,
parameters: Parameter.Array,
parameters: Parameter.Array = [],
requestBody: OpenAPI.Request? = nil,
responses: OpenAPI.Response.Map,
deprecated: Bool = false,
Expand Down Expand Up @@ -115,13 +115,7 @@ extension OpenAPI.PathItem.Operation: Encodable {

try requestBody.encodeIfNotNil(to: &container, forKey: .requestBody)

// Hack to work around Dictionary encoding
// itself as an array in this case:
let stringKeyedDict = Dictionary(
responses.map { ($0.key.rawValue, $0.value) },
uniquingKeysWith: { $1 }
)
try container.encode(stringKeyedDict, forKey: .responses)
try container.encode(responses, forKey: .responses)

if deprecated {
try container.encode(deprecated, forKey: .deprecated)
Expand Down Expand Up @@ -153,11 +147,7 @@ extension OpenAPI.PathItem.Operation: Decodable {

requestBody = try container.decodeIfPresent(OpenAPI.Request.self, forKey: .requestBody)

// hack to workaround Dictionary bug
let responsesDict = try container.decode([String: Either<JSONReference<OpenAPI.Components, OpenAPI.Response>, OpenAPI.Response>].self, forKey: .responses)
responses = Dictionary(responsesDict.compactMap { statusCodeString, response in
OpenAPI.Response.StatusCode(rawValue: statusCodeString).map { ($0, response) } },
uniquingKeysWith: { $1 })
responses = try container.decode(OpenAPI.Response.Map.self, forKey: .responses)

deprecated = try container.decodeIfPresent(Bool.self, forKey: .deprecated) ?? false

Expand Down
Loading

0 comments on commit 4a42df0

Please sign in to comment.