diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift index 7a8ed2ddf..f8c0d065a 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift @@ -653,6 +653,54 @@ extension JSONSchema { self._minProperties = minProperties } } + + /// The context that only applies to `.string` schemas. + public struct StringContext: Equatable { + public let maxLength: Int? + let _minLength: Int? + + public let contentMediaType: OpenAPI.ContentType? + public let contentEncoding: OpenAPI.ContentEncoding? + + public var minLength: Int { + return _minLength ?? 0 + } + + /// Regular expression + public let pattern: String? + + public init( + maxLength: Int? = nil, + minLength: Int? = nil, + pattern: String? = nil, + contentMediaType: OpenAPI.ContentType? = nil, + contentEncoding: OpenAPI.ContentEncoding? = nil + ) { + self.maxLength = maxLength + self._minLength = minLength + self.pattern = pattern + self.contentMediaType = contentMediaType + self.contentEncoding = contentEncoding + } + + // we make the following a static function so it doesn't muddy the namespace while auto-completing on a value. + public static func _minLength(_ context: StringContext) -> Int? { + return context._minLength + } + } +} + +extension OpenAPI { + /// An encoding, as specified in RFC 2054, part 6.1 and RFC 4648. + public enum ContentEncoding: String, Codable { + case _7bit = "7bit" + case _8bit = "8bit" + case binary + case quoted_printable = "quoted-printable" + case base16 + case base32 + case base64 + } } // MARK: - Codable @@ -1068,3 +1116,37 @@ extension JSONSchema.ObjectContext: Decodable { return properties } } + +extension JSONSchema.StringContext { + public enum CodingKeys: String, CodingKey { + case maxLength + case minLength + case pattern + case contentMediaType + case contentEncoding + } +} + +extension JSONSchema.StringContext: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(maxLength, forKey: .maxLength) + try container.encodeIfPresent(_minLength, forKey: .minLength) + try container.encodeIfPresent(pattern, forKey: .pattern) + try container.encodeIfPresent(contentMediaType, forKey: .contentMediaType) + try container.encodeIfPresent(contentEncoding, forKey: .contentEncoding) + } +} + +extension JSONSchema.StringContext: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + maxLength = try container.decodeIfPresent(Int.self, forKey: .maxLength) + _minLength = try container.decodeIfPresent(Int.self, forKey: .minLength) + pattern = try container.decodeIfPresent(String.self, forKey: .pattern) + contentMediaType = try container.decodeIfPresent(OpenAPI.ContentType.self, forKey: .contentMediaType) + contentEncoding = try container.decodeIfPresent(OpenAPI.ContentEncoding.self, forKey: .contentEncoding) + } +} diff --git a/Sources/OpenAPIKit/_CoreReExport.swift b/Sources/OpenAPIKit/_CoreReExport.swift index 00447c26e..a5cebfef8 100644 --- a/Sources/OpenAPIKit/_CoreReExport.swift +++ b/Sources/OpenAPIKit/_CoreReExport.swift @@ -44,7 +44,6 @@ public extension OpenAPI.Response { public extension JSONSchema { typealias Permissions = OpenAPIKitCore.Shared.JSONSchemaPermissions - typealias StringContext = OpenAPIKitCore.Shared.StringContext typealias ReferenceContext = OpenAPIKitCore.Shared.ReferenceContext } diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift index 9c7a99d0a..5a7eb190e 100644 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift @@ -573,6 +573,34 @@ extension JSONSchema { self._minProperties = minProperties } } + + /// The context that only applies to `.string` schemas. + public struct StringContext: Equatable { + public let maxLength: Int? + let _minLength: Int? + + public var minLength: Int { + return _minLength ?? 0 + } + + /// Regular expression + public let pattern: String? + + public init( + maxLength: Int? = nil, + minLength: Int? = nil, + pattern: String? = nil + ) { + self.maxLength = maxLength + self._minLength = minLength + self.pattern = pattern + } + + // we make the following a static function so it doesn't muddy the namespace while auto-completing on a value. + public static func _minLength(_ context: StringContext) -> Int? { + return context._minLength + } + } } // MARK: - Codable @@ -930,3 +958,31 @@ extension JSONSchema.ObjectContext: Decodable { return properties } } + +extension JSONSchema.StringContext { + public enum CodingKeys: String, CodingKey { + case maxLength + case minLength + case pattern + } +} + +extension JSONSchema.StringContext: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(maxLength, forKey: .maxLength) + try container.encodeIfPresent(_minLength, forKey: .minLength) + try container.encodeIfPresent(pattern, forKey: .pattern) + } +} + +extension JSONSchema.StringContext: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + maxLength = try container.decodeIfPresent(Int.self, forKey: .maxLength) + _minLength = try container.decodeIfPresent(Int.self, forKey: .minLength) + pattern = try container.decodeIfPresent(String.self, forKey: .pattern) + } +} diff --git a/Sources/OpenAPIKit30/_CoreReExport.swift b/Sources/OpenAPIKit30/_CoreReExport.swift index 00447c26e..a5cebfef8 100644 --- a/Sources/OpenAPIKit30/_CoreReExport.swift +++ b/Sources/OpenAPIKit30/_CoreReExport.swift @@ -44,7 +44,6 @@ public extension OpenAPI.Response { public extension JSONSchema { typealias Permissions = OpenAPIKitCore.Shared.JSONSchemaPermissions - typealias StringContext = OpenAPIKitCore.Shared.StringContext typealias ReferenceContext = OpenAPIKitCore.Shared.ReferenceContext } diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index f9fb9c6a2..fad60638f 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -551,6 +551,16 @@ extension OpenAPIKit30.JSONSchema.ObjectContext: To31 { } } +extension OpenAPIKit30.JSONSchema.StringContext: To31 { + fileprivate func to31() -> OpenAPIKit.JSONSchema.StringContext { + OpenAPIKit.JSONSchema.StringContext( + maxLength: maxLength, + minLength: OpenAPIKit30.JSONSchema.StringContext._minLength(self), + pattern: pattern + ) + } +} + extension OpenAPIKit30.JSONSchema: To31 { fileprivate func to31() -> OpenAPIKit.JSONSchema { let schema: OpenAPIKit.JSONSchema.Schema @@ -563,7 +573,7 @@ extension OpenAPIKit30.JSONSchema: To31 { case .integer(let core, let integral): schema = .integer(core.to31(), integral.to31()) case .string(let core, let stringy): - schema = .string(core.to31(), stringy) + schema = .string(core.to31(), stringy.to31()) case .object(let core, let objective): schema = .object(core.to31(), objective.to31()) case .array(let core, let listy): diff --git a/Sources/OpenAPIKitCore/Shared/JSONSchemaSimpleContexts.swift b/Sources/OpenAPIKitCore/Shared/JSONSchemaSimpleContexts.swift index 0e65dd39f..d5f297fa5 100644 --- a/Sources/OpenAPIKitCore/Shared/JSONSchemaSimpleContexts.swift +++ b/Sources/OpenAPIKitCore/Shared/JSONSchemaSimpleContexts.swift @@ -6,35 +6,6 @@ // extension Shared { - /// The context that only applies to `.string` schemas. - public struct StringContext: Equatable { - public let maxLength: Int? - let _minLength: Int? - - public var minLength: Int { - return _minLength ?? 0 - } - - /// Regular expression - public let pattern: String? - - public init( - maxLength: Int? = nil, - minLength: Int? = nil, - pattern: String? = nil - ) { - self.maxLength = maxLength - self._minLength = minLength - self.pattern = pattern - } - - // we make the following a static function so it doesn't muddy the namespace while auto-completing on a value. - public static func _minLength(_ context: StringContext) -> Int? { - return context._minLength - } - } - - /// The context that only applies to `.reference` schemas. public struct ReferenceContext: Equatable { public let required: Bool @@ -52,31 +23,3 @@ extension Shared { } } } - -extension Shared.StringContext { - public enum CodingKeys: String, CodingKey { - case maxLength - case minLength - case pattern - } -} - -extension Shared.StringContext: Encodable { - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encodeIfPresent(maxLength, forKey: .maxLength) - try container.encodeIfPresent(_minLength, forKey: .minLength) - try container.encodeIfPresent(pattern, forKey: .pattern) - } -} - -extension Shared.StringContext: Decodable { - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - maxLength = try container.decodeIfPresent(Int.self, forKey: .maxLength) - _minLength = try container.decodeIfPresent(Int.self, forKey: .minLength) - pattern = try container.decodeIfPresent(String.self, forKey: .pattern) - } -} diff --git a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift index dbd0c747e..ad3f677cf 100644 --- a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift +++ b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift @@ -1070,7 +1070,7 @@ fileprivate func assertEqualNewToOld(_ newSchema: OpenAPIKit.JSONSchema, _ oldSc case .string(let coreContext, let stringContext): let newStringContext = try XCTUnwrap(newSchema.stringContext) - // TODO: compare string contexts + assertEqualNewToOld(newStringContext, stringContext) try assertEqualNewToOld(newCoreContext, coreContext) case .object(let coreContext, let objectContext): @@ -1142,6 +1142,15 @@ fileprivate func assertEqualNewToOld(_ newCoreContext: OpenAPIKit.JSONSchemaCont XCTAssertEqual(newCoreContext.deprecated, oldCoreContext.deprecated) } +fileprivate func assertEqualNewToOld(_ newStringContext: OpenAPIKit.JSONSchema.StringContext, _ oldStringContext: OpenAPIKit30.JSONSchema.StringContext) { + XCTAssertEqual(newStringContext.pattern, oldStringContext.pattern) + XCTAssertEqual(newStringContext.maxLength, oldStringContext.maxLength) + XCTAssertEqual(newStringContext.minLength, oldStringContext.minLength) + XCTAssertEqual(OpenAPIKit.JSONSchema.StringContext._minLength(newStringContext), OpenAPIKit30.JSONSchema.StringContext._minLength(oldStringContext)) + XCTAssertNil(newStringContext.contentEncoding) + XCTAssertNil(newStringContext.contentMediaType) +} + fileprivate func assertEqualNewToOld(_ newExample: OpenAPIKit.OpenAPI.Example, _ oldExample: OpenAPIKit30.OpenAPI.Example) { XCTAssertEqual(newExample.summary, oldExample.summary) XCTAssertEqual(newExample.description, oldExample.description) diff --git a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift index b141776cf..084b93693 100644 --- a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift @@ -5139,6 +5139,164 @@ extension SchemaObjectTests { XCTAssertEqual(defaultValueString, JSONSchema.string(.init(format: .generic, defaultValue: "hello"), .init(pattern: ".*"))) } + func test_encodeStringWithMediaType() { + let requiredString = JSONSchema.string(.init(required: true), .init(contentMediaType: .bmp)) + let optionalString = JSONSchema.string(.init(required: false), .init(contentMediaType: .bmp)) + let nullableString = JSONSchema.string(.init(nullable: true), .init(contentMediaType: .bmp)) + let allowedValueString = JSONSchema.string(.init(), .init(contentMediaType: .bmp)) + .with(allowedValues: ["hello World", "hi"]) + let constValueString = JSONSchema.string(.init(), .init(contentMediaType: .bmp)) + .with(allowedValues: ["hello World"]) + let defaultValueString = JSONSchema.string(.init(defaultValue: "hello"), .init(contentMediaType: .bmp)) + + testEncodingPropertyLines( + entity: requiredString, + propertyLines: [ + "\"contentMediaType\" : \"image\\/bmp\",", + "\"type\" : \"string\"", + ] + ) + + testEncodingPropertyLines( + entity: optionalString, + propertyLines: [ + "\"contentMediaType\" : \"image\\/bmp\",", + "\"type\" : \"string\"" + ] + ) + + testEncodingPropertyLines( + entity: nullableString, + propertyLines: [ + "\"contentMediaType\" : \"image\\/bmp\",", + "\"type\" : [", + " \"string\",", + " \"null\"", + "]" + ] + ) + + testEncodingPropertyLines( + entity: constValueString, + propertyLines: [ + "\"const\" : \"hello World\",", + "\"contentMediaType\" : \"image\\/bmp\",", + "\"type\" : \"string\"" + ] + ) + + // can't check exact string because of order instability, but we can confirm it is encoding the + // `enum` property instead of the `const` property. + let encoded = try! orderUnstableTestStringFromEncoding(of: allowedValueString) + XCTAssert(encoded?.contains("\"enum\"") ?? false) + XCTAssert(!(encoded?.contains("\"const\"") ?? true)) + + testEncodingPropertyLines( + entity: defaultValueString, + propertyLines: [ + "\"contentMediaType\" : \"image\\/bmp\",", + "\"default\" : \"hello\",", + "\"type\" : \"string\"", + ] + ) + } + + func test_decodeStringWithMediaType() throws { + let stringData = #"{"type": "string", "contentMediaType": "image/bmp"}"#.data(using: .utf8)! + let nullableStringData = #"{"type": ["string", "null"], "contentMediaType": "image/bmp"}"#.data(using: .utf8)! + let allowedValueStringData = #"{"type": "string", "contentMediaType": "image/bmp", "enum": ["hello", "world"]}"#.data(using: .utf8)! + let defaultValueStringData = #"{"type": "string", "contentMediaType": "image/bmp", "default": "hello"}"#.data(using: .utf8)! + + let string = try orderUnstableDecode(JSONSchema.self, from: stringData) + let nullableString = try orderUnstableDecode(JSONSchema.self, from: nullableStringData) + let allowedValueString = try orderUnstableDecode(JSONSchema.self, from: allowedValueStringData) + let defaultValueString = try orderUnstableDecode(JSONSchema.self, from: defaultValueStringData) + + XCTAssertEqual(string, JSONSchema.string(.init(), .init(contentMediaType: .bmp))) + XCTAssertEqual(nullableString, JSONSchema.string(.init(nullable: true), .init(contentMediaType: .bmp))) + XCTAssertEqual(allowedValueString, JSONSchema.string(.init(allowedValues: ["hello", "world"]), .init(contentMediaType: .bmp))) + XCTAssertEqual(defaultValueString, JSONSchema.string(.init(defaultValue: "hello"), .init(contentMediaType: .bmp))) + } + + func test_encodeStringWithEncoding() { + let requiredString = JSONSchema.string(.init(required: true), .init(contentEncoding: .base64)) + let optionalString = JSONSchema.string(.init(required: false), .init(contentEncoding: .base64)) + let nullableString = JSONSchema.string(.init(nullable: true), .init(contentEncoding: .base64)) + let allowedValueString = JSONSchema.string(.init(), .init(contentEncoding: .base64)) + .with(allowedValues: ["hello World", "hi"]) + let constValueString = JSONSchema.string(.init(), .init(contentEncoding: .base64)) + .with(allowedValues: ["hello World"]) + let defaultValueString = JSONSchema.string(.init(defaultValue: "hello"), .init(contentEncoding: .base64)) + + testEncodingPropertyLines( + entity: requiredString, + propertyLines: [ + "\"contentEncoding\" : \"base64\",", + "\"type\" : \"string\"", + ] + ) + + testEncodingPropertyLines( + entity: optionalString, + propertyLines: [ + "\"contentEncoding\" : \"base64\",", + "\"type\" : \"string\"" + ] + ) + + testEncodingPropertyLines( + entity: nullableString, + propertyLines: [ + "\"contentEncoding\" : \"base64\",", + "\"type\" : [", + " \"string\",", + " \"null\"", + "]" + ] + ) + + testEncodingPropertyLines( + entity: constValueString, + propertyLines: [ + "\"const\" : \"hello World\",", + "\"contentEncoding\" : \"base64\",", + "\"type\" : \"string\"" + ] + ) + + // can't check exact string because of order instability, but we can confirm it is encoding the + // `enum` property instead of the `const` property. + let encoded = try! orderUnstableTestStringFromEncoding(of: allowedValueString) + XCTAssert(encoded?.contains("\"enum\"") ?? false) + XCTAssert(!(encoded?.contains("\"const\"") ?? true)) + + testEncodingPropertyLines( + entity: defaultValueString, + propertyLines: [ + "\"contentEncoding\" : \"base64\",", + "\"default\" : \"hello\",", + "\"type\" : \"string\"", + ] + ) + } + + func test_decodeStringWithEncoding() throws { + let stringData = #"{"type": "string", "contentEncoding": "base64"}"#.data(using: .utf8)! + let nullableStringData = #"{"type": ["string", "null"], "contentEncoding": "base64"}"#.data(using: .utf8)! + let allowedValueStringData = #"{"type": "string", "contentEncoding": "base64", "enum": ["hello", "world"]}"#.data(using: .utf8)! + let defaultValueStringData = #"{"type": "string", "contentEncoding": "base64", "default": "hello"}"#.data(using: .utf8)! + + let string = try orderUnstableDecode(JSONSchema.self, from: stringData) + let nullableString = try orderUnstableDecode(JSONSchema.self, from: nullableStringData) + let allowedValueString = try orderUnstableDecode(JSONSchema.self, from: allowedValueStringData) + let defaultValueString = try orderUnstableDecode(JSONSchema.self, from: defaultValueStringData) + + XCTAssertEqual(string, JSONSchema.string(.init(), .init(contentEncoding: .base64))) + XCTAssertEqual(nullableString, JSONSchema.string(.init(nullable: true), .init(contentEncoding: .base64))) + XCTAssertEqual(allowedValueString, JSONSchema.string(.init(allowedValues: ["hello", "world"]), .init(contentEncoding: .base64))) + XCTAssertEqual(defaultValueString, JSONSchema.string(.init(defaultValue: "hello"), .init(contentEncoding: .base64))) + } + func test_encodeAll() { let allOf = JSONSchema.all( of: [