diff --git a/JSONAPI.playground/contents.xcplayground b/JSONAPI.playground/contents.xcplayground index a1ec109..e240eff 100644 --- a/JSONAPI.playground/contents.xcplayground +++ b/JSONAPI.playground/contents.xcplayground @@ -5,6 +5,5 @@ - \ No newline at end of file diff --git a/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift b/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift index 6afd065..35fd347 100644 --- a/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift +++ b/Sources/JSONAPIOpenAPI/JSONAPITypes+OpenAPI.swift @@ -118,6 +118,7 @@ extension ToManyRelationship: OpenAPINodeType { extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Description.Relationships: Sampleable { public static func openAPINode() throws -> JSONNode { + // TODO: const for json `type` // TODO: metadata, links let idNode = JSONNode.string(.init(format: .generic, @@ -147,7 +148,40 @@ extension Entity: OpenAPINodeType where Description.Attributes: Sampleable, Desc typeProperty, attributesProperty, relationshipsProperty - ].compactMap { $0 }) { _, value in value } + ].compactMap { $0 }) { _, value in value } + + return .object(.init(format: .generic, + required: true), + .init(properties: propertiesDict)) + } +} + +extension SingleResourceBody: OpenAPINodeType where Entity: OpenAPINodeType { + public static func openAPINode() throws -> JSONNode { + return try Entity.openAPINode() + } +} + +extension ManyResourceBody: OpenAPINodeType where Entity: OpenAPINodeType { + public static func openAPINode() throws -> JSONNode { + return .array(.init(format: .generic, + required: true), + .init(items: try Entity.openAPINode())) + } +} + +extension Document: OpenAPINodeType where PrimaryResourceBody: OpenAPINodeType { + public static func openAPINode() throws -> JSONNode { + // TODO: metadata, links, api description, includes, errors + // TODO: represent data and errors as the two distinct possible outcomes + + let primaryDataNode: JSONNode? = try PrimaryResourceBody.openAPINode() + + let primaryDataProperty = primaryDataNode.map { ("data", $0) } + + let propertiesDict = Dictionary([ + primaryDataProperty + ].compactMap { $0 }) { _, value in value } return .object(.init(format: .generic, required: true), diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift index 6d0aced..9d4d2cc 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes+Codable.swift @@ -12,6 +12,7 @@ extension JSONNode.Context: Encodable { case format case allowedValues = "enum" case nullable + case example } public func encode(to encoder: Encoder) throws { @@ -28,6 +29,10 @@ extension JSONNode.Context: Encodable { } try container.encode(nullable, forKey: .nullable) + + if example != nil { + try container.encode(example, forKey: .example) + } } } diff --git a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift index 033a3bd..65ec92b 100644 --- a/Sources/JSONAPIOpenAPI/OpenAPITypes.swift +++ b/Sources/JSONAPIOpenAPI/OpenAPITypes.swift @@ -16,6 +16,12 @@ public protocol OpenAPINodeType { static func openAPINode() throws -> JSONNode } +extension OpenAPINodeType where Self: Sampleable, Self: Encodable { + public static func openAPINodeWithExample() throws -> JSONNode { + return try openAPINode().with(example: Self.sample) + } +} + /// Anything conforming to `RawOpenAPINodeType` can provide an /// OpenAPI schema representing itself. This second protocol is /// necessary so that one type can conditionally provide a @@ -261,14 +267,18 @@ public enum JSONNode: Equatable { /// into an allowed value. public let allowedValues: [AnyCodable]? + public let example: AnyCodable? + public init(format: Format, required: Bool, nullable: Bool = false, - allowedValues: [AnyCodable]? = nil) { + allowedValues: [AnyCodable]? = nil, + example: AnyCodable? = nil) { self.format = format self.required = required self.nullable = nullable self.allowedValues = allowedValues + self.example = example } /// Return the optional version of this Context @@ -296,12 +306,21 @@ public enum JSONNode: Equatable { } /// Return this context with the given list of possible values - public func with(allowedValues: [AnyCodable]?) -> Context { + public func with(allowedValues: [AnyCodable]) -> Context { return .init(format: format, required: required, nullable: nullable, allowedValues: allowedValues) } + + /// Return this context with the given example + public func with(example: AnyCodable) -> Context { + return .init(format: format, + required: required, + nullable: nullable, + allowedValues: allowedValues, + example: example) + } } public struct NumericContext: Equatable { @@ -513,8 +532,35 @@ public enum JSONNode: Equatable { return self } } + + public func with(example codableExample: T) throws -> JSONNode { + let example: AnyCodable + if let goodToGo = codableExample as? AnyCodable { + example = goodToGo + } else { + example = AnyCodable(try JSONSerialization.jsonObject(with: JSONEncoder().encode(codableExample), options: [])) + } + + switch self { + case .boolean(let context): + return .boolean(context.with(example: example)) + case .object(let contextA, let contextB): + return .object(contextA.with(example: example), contextB) + case .array(let contextA, let contextB): + return .array(contextA.with(example: example), contextB) + case .number(let context, let contextB): + return .number(context.with(example: example), contextB) + case .integer(let context, let contextB): + return .integer(context.with(example: example), contextB) + case .string(let context, let contextB): + return .string(context.with(example: example), contextB) + case .allOf, .oneOf, .anyOf, .not: + return self + } + } } public enum OpenAPICodableError: Swift.Error { case allCasesArrayNotCodable + case exampleNotCodable } diff --git a/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift b/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift new file mode 100644 index 0000000..f709f17 --- /dev/null +++ b/Tests/JSONAPIOpenAPITests/JSONAPIDocumentOpenAPITests.swift @@ -0,0 +1,42 @@ +// +// JSONAPIDocumentOpenAPITests.swift +// JSONAPIOpenAPITests +// +// Created by Mathew Polzin on 1/21/19. +// + +import XCTest +import JSONAPI +import JSONAPIOpenAPI + +class JSONAPIDocumentOpenAPITests: XCTestCase { + func test_SingleResourceDocument() { + let node = try! SingleEntityDocument.openAPINode() + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + print(String(data: try! encoder.encode(node), encoding: .utf8)!) + } +} + +// MARK: - Test Types +extension JSONAPIDocumentOpenAPITests { + enum TestEntityDescription: EntityDescription { + static var jsonType: String { return "test" } + + struct Attributes: JSONAPI.Attributes, Sampleable { + let name: Attribute + + static var sample: Attributes { + return .init(name: "hello world") + } + } + + typealias Relationships = NoRelationships + } + + typealias TestEntity = BasicEntity + + typealias SingleEntityDocument = Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> +}