From 2cdf4a202d0388560e76c7a74bd3851237350a80 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 31 Aug 2026 09:30:41 -0500 Subject: [PATCH] remove simplification support from OpenAPIKit30 module --- .../Schema Object/JSONSchema+Combining.swift | 688 --------- .../Schema Object/SimplifiedJSONSchema.swift | 143 -- .../SwaggerDocSamplesTests.swift | 28 - .../DereferencedSchemaObjectTests.swift | 30 - .../SchemaFragmentCombiningTests.swift | 1259 ----------------- .../Schema Object/SchemaFragmentTests.swift | 10 - 6 files changed, 2158 deletions(-) delete mode 100644 Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift delete mode 100644 Sources/OpenAPIKit30/Schema Object/SimplifiedJSONSchema.swift delete mode 100644 Tests/OpenAPIKit30Tests/Schema Object/SchemaFragmentCombiningTests.swift diff --git a/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift b/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift deleted file mode 100644 index dd213437a5..0000000000 --- a/Sources/OpenAPIKit30/Schema Object/JSONSchema+Combining.swift +++ /dev/null @@ -1,688 +0,0 @@ -// -// JSONSchema+Combining.swift -// -// -// Created by Mathew Polzin on 8/1/20. -// - -import OpenAPIKitCore - -extension Array where Element == JSONSchema { - /// An array of schema fragments can be combined into a - /// single `DereferencedJSONSchema` if all references can - /// be looked up locally and none of the fragments conflict. - /// - /// Combining fragments will both remove references and attempt - /// to reject any results that would represent impossible schemas - /// -- that is, schemas that cannot be satisfied and could not ever - /// be used to validate anything (guaranteed validation failure). - public func combined(resolvingAgainst components: OpenAPI.Components) throws -> DereferencedJSONSchema { - var resolver = FragmentCombiner(components: components) - try resolver.combine(self) - return try resolver.dereferencedSchema() - } -} - -public struct JSONSchemaResolutionError: Swift.Error, CustomStringConvertible { - internal let underlyingError: _JSONSchemaResolutionError - - internal init(_ underlyingError: _JSONSchemaResolutionError) { - self.underlyingError = underlyingError - } - - public var description: String { - String(describing: underlyingError) - } - - // The following can be used for pattern matching but are not good - // errors for totally lacking any context: - public static let unsupported: JSONSchemaResolutionError = .init(.unsupported(because: "")) - public static let typeConflict: JSONSchemaResolutionError = .init(.typeConflict(original: .string, new: .string)) - public static let formatConflict: JSONSchemaResolutionError = .init(.formatConflict(original: "", new: "")) - public static let attributeConflict: JSONSchemaResolutionError = .init(.attributeConflict(jsonType: nil, name: "", original: "", new: "")) - public static let inconsistency: JSONSchemaResolutionError = .init(.inconsistency("")) -} - -public func ~=(lhs: JSONSchemaResolutionError, rhs: JSONSchemaResolutionError) -> Bool { - switch (lhs.underlyingError, rhs.underlyingError) { - case (.unsupported, .unsupported), - (.typeConflict, .typeConflict), - (.formatConflict, .formatConflict), - (.attributeConflict, .attributeConflict), - (.inconsistency, .inconsistency): - return true - default: - return false - } -} - -/// Just an internal error enum to ensure I have all errors covered but -/// also allow adding cases without being a breaking change. -/// -/// I expect this to be an area where I may want to make fixes and add -/// errors without breaknig changes, so this annoying workaround for -/// the absense of a "non-frozen" enum is a must. -internal enum _JSONSchemaResolutionError: CustomStringConvertible, Equatable, Sendable { - case unsupported(because: String) - case typeConflict(original: JSONType, new: JSONType) - case formatConflict(original: String, new: String) - case attributeConflict(jsonType: JSONType?, name: String, original: String, new: String) - - case inconsistency(String) - - var description: String { - switch self { - case .unsupported(because: let reason): - return "The given schema does not yet support combining in OpenAPIKit because \(reason)." - case .typeConflict(original: let original, new: let new): - return "Found conflicting schema types. A schema cannot be both \(original.rawValue) and \(new.rawValue)." - case .formatConflict(original: let original, new: let new): - return "Found conflicting formats. A schema cannot be both \(original) and \(new)." - case .attributeConflict(jsonType: let jsonType, name: let name, original: let original, new: let new): - let contextString = jsonType?.rawValue ?? "A schema" - return "Found conflicting properties. \(contextString) cannot have \(name) with both \(original) and \(new) values." - case .inconsistency(let description): - return "Found inconsistency: \(description)." - } - } -} - -/// The FragmentCombiner takes any number of fragments and determines if they can be -/// meaningfully combined. -/// -/// Conflicts will be determined as fragments are added and when you ask for -/// a `dereferencedSchema()` the fragment resolver will determine if it has enough information -/// to build and dereference the schema. -/// -/// Current Limitations (will throw `.unsupported` for these reasons): -/// - Does not handle inversion via `not` or combination via `any`, `one`, `all`. -internal struct FragmentCombiner { - private let components: OpenAPI.Components - private var combinedFragment: JSONSchema? - - /// Set up for constructing a schema using the given Components Object. Call `combine(_:)` - /// to start adding schema fragments to the partial schema definition. - /// - /// Once all fragments have been combined, call `dereferencedSchema` to attempt to build a `DereferencedJSONSchema`. - init(components: OpenAPI.Components) { - self.components = components - } - - /// Combine the existing partial schema with the given fragment. - /// - /// - Throws: If any fragments combined together would result in an invalid schema or - /// if there is not enough information in the fragments to build a complete schema. - mutating func combine(_ fragment: JSONSchema) throws { - - let combinedFragment: JSONSchema - if let currentFragment = self.combinedFragment { - combinedFragment = currentFragment - } else { - // combination can turn `required: false` into `required: true` - // but not the other way around. We start optional and the first - // time we combine with something required we become required. - combinedFragment = .fragment(.init(required: false)) - } - - // make sure any less specialized fragment (i.e. general) is on the left - let lessSpecializedFragment: JSONSchema - let equallyOrMoreSpecializedFragment: JSONSchema - switch (combinedFragment.value, fragment.value) { - case (.fragment, _): - lessSpecializedFragment = combinedFragment - equallyOrMoreSpecializedFragment = fragment - default: - lessSpecializedFragment = fragment - equallyOrMoreSpecializedFragment = combinedFragment - } - - switch (lessSpecializedFragment.value, equallyOrMoreSpecializedFragment.value) { - case (.all(let schemas, core: let core), let other), (let other, .all(let schemas, core: let core)): - // tease apart one allOf if there is one and continue from there. - try self.combine(schemas + [.fragment(core), JSONSchema(schema: other)]) - - case (_, .reference(let reference, let context)), (.reference(let reference, let context), _): - var component = try components.lookup(reference) - if !context.required { - component = component.optionalSchemaObject() - } - try combine(component) - - case (.fragment(let leftCoreContext), .fragment(let rightCoreContext)): - self.combinedFragment = .fragment(try leftCoreContext.combined(with: rightCoreContext)) - case (.fragment(let leftCoreContext), .boolean(let rightCoreContext)): - self.combinedFragment = .boolean(try leftCoreContext.combined(with: rightCoreContext)) - case (.fragment(let leftCoreContext), .integer(let rightCoreContext, let integerContext)): - self.combinedFragment = .integer(try leftCoreContext.combined(with: rightCoreContext), integerContext) - case (.fragment(let leftCoreContext), .number(let rightCoreContext, let numericContext)): - self.combinedFragment = .number(try leftCoreContext.combined(with: rightCoreContext), numericContext) - case (.fragment(let leftCoreContext), .string(let rightCoreContext, let stringContext)): - self.combinedFragment = .string(try leftCoreContext.combined(with: rightCoreContext), stringContext) - case (.fragment(let leftCoreContext), .array(let rightCoreContext, let arrayContext)): - self.combinedFragment = .array(try leftCoreContext.combined(with: rightCoreContext), arrayContext) - case (.fragment(let leftCoreContext), .object(let rightCoreContext, let objectContext)): - self.combinedFragment = .object(try leftCoreContext.combined(with: rightCoreContext), objectContext) - case (.boolean(let leftCoreContext), .boolean(let rightCoreContext)): - self.combinedFragment = .boolean(try leftCoreContext.combined(with: rightCoreContext)) - case (.integer(let leftCoreContext, let leftIntegerContext), .integer(let rightCoreContext, let rightIntegerContext)): - self.combinedFragment = .integer(try leftCoreContext.combined(with: rightCoreContext), try leftIntegerContext.combined(with: rightIntegerContext)) - case (.number(let leftCoreContext, let leftNumericContext), .number(let rightCoreContext, let rightNumericContext)): - self.combinedFragment = .number(try leftCoreContext.combined(with: rightCoreContext), try leftNumericContext.combined(with: rightNumericContext)) - case (.string(let leftCoreContext, let leftStringContext), .string(let rightCoreContext, let rightStringContext)): - self.combinedFragment = .string(try leftCoreContext.combined(with: rightCoreContext), try leftStringContext.combined(with: rightStringContext)) - case (.array(let leftCoreContext, let leftArrayContext), .array(let rightCoreContext, let rightArrayContext)): - self.combinedFragment = .array(try leftCoreContext.combined(with: rightCoreContext), try leftArrayContext.combined(with: rightArrayContext)) - case (.object(let leftCoreContext, let leftObjectContext), .object(let rightCoreContext, let rightObjectContext)): - self.combinedFragment = .object(try leftCoreContext.combined(with: rightCoreContext), try leftObjectContext.combined(with: rightObjectContext, resolvingIn: components)) - - case (_, .any), (.any, _), (_, .not), (.not, _), (_, .one), (.one, _): - throw JSONSchemaResolutionError(.unsupported(because: "not, any(of:), and one(of:) are not yet supported for schema resolution")) - case (.boolean, _), - (.integer, _), - (.number, _), - (.string, _), - (.array, _), - (.object, _): - throw ( - zip(combinedFragment.jsonType, fragment.jsonType).map { - JSONSchemaResolutionError(.typeConflict(original: $0, new: $1)) - } ?? JSONSchemaResolutionError( - .unsupported(because: "Encountered an unexpected problem with schema fragments of types \(String(describing: combinedFragment.jsonType)) and \(String(describing: fragment.jsonType))") - ) - ) - } - } - - /// Combine the existing partial schema with the given fragments. - /// - /// - Throws: If any fragments combined together would result in an invalid schema or - /// if there is not enough information in the fragments to build a complete schema. - mutating func combine(_ fragments: [JSONSchema]) throws { - for fragment in fragments { - try combine(fragment) - } - } - - func dereferencedSchema() throws -> DereferencedJSONSchema { - guard let combinedFragment = self.combinedFragment else { - // just give the more bare-bones schema possible if nothing - // has been combined yet. This schema is `{ }` (empty). - return .fragment(.init()) - } - - let jsonSchema: JSONSchema - switch combinedFragment.value { - case .fragment, .reference: - jsonSchema = combinedFragment - case .boolean(let coreContext): - jsonSchema = .boolean(try coreContext.validatedContext()) - case .integer(let coreContext, let integerContext): - jsonSchema = .integer(try coreContext.validatedContext(), try integerContext.validatedContext()) - case .number(let coreContext, let numericContext): - jsonSchema = .number(try coreContext.validatedContext(), try numericContext.validatedContext()) - case .string(let coreContext, let stringContext): - jsonSchema = .string(try coreContext.validatedContext(), try stringContext.validatedContext()) - case .array(let coreContext, let arrayContext): - jsonSchema = .array(try coreContext.validatedContext(), try arrayContext.validatedContext()) - case .object(let coreContext, let objectContext): - jsonSchema = .object(try coreContext.validatedContext(), try objectContext.validatedContext()) - case .all(of: let schemas, core: let coreContext): - jsonSchema = try .all(of: schemas, core: coreContext.validatedContext()) - case .any(of: let schemas, core: let coreContext): - jsonSchema = try .any(of: schemas, core: coreContext.validatedContext()) - case .one(of: let schemas, core: let coreContext): - jsonSchema = try .one(of: schemas, core: coreContext.validatedContext()) - case .not: - throw JSONSchemaResolutionError(.unsupported(because: "`.not` is not yet supported for schema simplification")) - } - return try jsonSchema.simplified(given: components) - } -} - -// MARK: - Combining Fragments - -internal func conflicting(_ left: T?, _ right: T?) -> (T, T)? where T: Equatable { - return zip(left, right).flatMap { $0 == $1 ? nil : ($0, $1) } -} - -extension JSONSchema.CoreContext where Format == JSONTypeFormat.AnyFormat { - /// Go from less specialized to more specialized while combining. - internal func combined( - with other: JSONSchema.CoreContext - ) throws -> JSONSchema.CoreContext { - guard let newFormat = OtherFormat(rawValue: format.rawValue) else { - throw JSONSchemaResolutionError(.inconsistency("A given format (\(format.rawValue) cannot be applied to the format type: \(OtherFormat.self)")) - } - - typealias OtherContext = JSONSchema.CoreContext - - let transformedContext = OtherContext( - format: newFormat, - required: required, - nullable: _nullable, - permissions: _permissions, - deprecated: _deprecated, - title: title, - description: description, - discriminator: discriminator, - externalDocs: externalDocs, - allowedValues: allowedValues, - defaultValue: defaultValue, - example: example - ) - return try transformedContext.combined(with: other) - } -} - -extension JSONSchema.CoreContext { - internal func combined(with other: Self) throws -> Self { - let newFormat = try format.combined(with: other.format) - - if let conflict = conflicting(description, other.description) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: nil, name: "description", original: conflict.0, new: conflict.1)) - } - let newDescription = description ?? other.description - - if let conflict = conflicting(_permissions, other._permissions) { - throw JSONSchemaResolutionError(.inconsistency("A schema cannot be both \(conflict.0.rawValue) and \(conflict.1.rawValue).")) - } - let newPermissions: JSONSchema.Permissions? - if _permissions == nil && other._permissions == nil { - newPermissions = nil - } else { - switch (_permissions, other._permissions) { - case (.readOnly, .writeOnly), (.writeOnly, .readOnly): - throw JSONSchemaResolutionError(.inconsistency("Schemas cannot be read-only and write-only")) - case (.readOnly, .readOnly), (nil, .readOnly), (.readOnly, nil): - newPermissions = .readOnly - case (.writeOnly, .writeOnly), (nil, .writeOnly), (.writeOnly, nil): - newPermissions = .writeOnly - default: - newPermissions = .readWrite - } - } - - if let conflict = conflicting(discriminator, other.discriminator) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: nil, name: "discriminator", original: String(describing: conflict.0), new: String(describing: conflict.1))) - } - let newDiscriminator = discriminator ?? other.discriminator - - if let conflict = conflicting(title, other.title) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: nil, name: "title", original: conflict.0, new: conflict.1)) - } - let newTitle = title ?? other.title - - if let conflict = conflicting(_nullable, other._nullable) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: nil, name: "nullable", original: String(conflict.0), new: String(conflict.1))) - } - let newNullable = _nullable ?? other._nullable - - if let conflict = conflicting(_deprecated, other._deprecated) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: nil, name: "deprecated", original: String(conflict.0), new: String(conflict.1))) - } - let newDeprecated = _deprecated ?? other._deprecated - - if let conflict = conflicting(externalDocs, other.externalDocs) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: nil, name: "externalDocs", original: String(describing: conflict.0), new: String(describing: conflict.1))) - } - let newExternalDocs = externalDocs ?? other.externalDocs - - if let conflict = conflicting(allowedValues, other.allowedValues) { - throw JSONSchemaResolutionError( - .attributeConflict( - jsonType: nil, - name: "allowedValues", - original: conflict.0.map(String.init(describing:)).joined(separator: ", "), - new: conflict.1.map(String.init(describing:)).joined(separator: ", ") - ) - ) - } - let newAllowedValues = allowedValues ?? other.allowedValues - let newDefaultValue = defaultValue ?? other.defaultValue - - if let conflict = conflicting(example, other.example) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: nil, name: "example", original: String(describing: conflict.0), new: String(describing: conflict.1))) - } - let newExample = example ?? other.example - - let newRequired = required || other.required - return .init( - format: newFormat, - required: newRequired, - nullable: newNullable, - permissions: newPermissions, - deprecated: newDeprecated, - title: newTitle, - description: newDescription, - discriminator: newDiscriminator, - externalDocs: newExternalDocs, - allowedValues: newAllowedValues, - defaultValue: newDefaultValue, - example: newExample - ) - } -} - -extension OpenAPIFormat { - internal func combined(with other: Self) throws -> Self { - switch (self, other) { - case (.unspecified, .unspecified): - return .unspecified - case (.unspecified, _): - return other - case (_, .unspecified): - return self - default: - if let conflict = conflicting(self, other) { - throw JSONSchemaResolutionError(.formatConflict(original: conflict.0.rawValue, new: conflict.1.rawValue)) - } else { - return self - } - } - } -} - -extension JSONSchema.IntegerContext { - internal func combined(with other: JSONSchema.IntegerContext) throws -> JSONSchema.IntegerContext { - if let conflict = conflicting(multipleOf, other.multipleOf) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .integer, name: "multipleOf", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(maximum?.value, other.maximum?.value) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .integer, name: "maximum", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(maximum?.exclusive, other.maximum?.exclusive) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .integer, name: "exclusiveMaximum", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(minimum?.value, other.minimum?.value) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .integer, name: "minimum", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(minimum?.exclusive, other.minimum?.exclusive) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .integer, name: "exclusiveMinimum", original: String(conflict.0), new: String(conflict.1))) - } - // explicitly declaring these constants one at a time - // helps the type checker a lot. - let newMultipleOf = multipleOf ?? other.multipleOf - let newMaximum = maximum ?? other.maximum - let newMinimum = minimum ?? other.minimum - return .init( - multipleOf: newMultipleOf, - maximum: newMaximum, - minimum: newMinimum - ) - } -} - -extension JSONSchema.NumericContext { - internal func combined(with other: JSONSchema.NumericContext) throws -> JSONSchema.NumericContext { - if let conflict = conflicting(multipleOf, other.multipleOf) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .number, name: "multipleOf", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(maximum?.value, other.maximum?.value) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .number, name: "maximum", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(maximum?.exclusive, other.maximum?.exclusive) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .number, name: "exclusiveMaximum", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(minimum?.value, other.minimum?.value) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .number, name: "minimum", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(minimum?.exclusive, other.minimum?.exclusive) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .number, name: "exclusiveMinimum", original: String(conflict.0), new: String(conflict.1))) - } - // explicitly declaring these constants one at a time - // helps the type checker a lot. - let newMultipleOf = multipleOf ?? other.multipleOf - let newMaximum = maximum ?? other.maximum - let newMinimum = minimum ?? other.minimum - return .init( - multipleOf: newMultipleOf, - maximum: newMaximum, - minimum: newMinimum - ) - } -} - -extension JSONSchema.StringContext { - internal func combined(with other: JSONSchema.StringContext) throws -> JSONSchema.StringContext { - if let conflict = conflicting(maxLength, other.maxLength) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "maxLength", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(Self._minLength(self), Self._minLength(other)) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "minLength", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(pattern, other.pattern) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .string, name: "pattern", original: conflict.0, new: conflict.1)) - } - // explicitly declaring these constants one at a time - // helps the type checker a lot. - let newMaxLength = maxLength ?? other.maxLength - let newMinLength = Self._minLength(self) ?? Self._minLength(other) - let newPattern = pattern ?? other.pattern - return .init( - maxLength: newMaxLength, - minLength: newMinLength, - pattern: newPattern - ) - } -} - -extension JSONSchema.ArrayContext { - internal func combined(with other: JSONSchema.ArrayContext) throws -> JSONSchema.ArrayContext { - if let conflict = conflicting(items, other.items) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .array, name: "items", original: String(describing: conflict.0), new: String(describing: conflict.1))) - } - if let conflict = conflicting(maxItems, other.maxItems) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .array, name: "maxItems", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(_minItems, other._minItems) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .array, name: "minItems", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(_uniqueItems, other._uniqueItems) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .array, name: "uniqueItems", original: String(conflict.0), new: String(conflict.1))) - } - // explicitly declaring these constants one at a time - // helps the type checker a lot. - let newItems = items ?? other.items - let newMaxItems = maxItems ?? other.maxItems - let newMinItesm = _minItems ?? other._minItems - let newUniqueItesm = _uniqueItems ?? other._uniqueItems - return .init( - items: newItems, - maxItems: newMaxItems, - minItems: newMinItesm, - uniqueItems: newUniqueItesm - ) - } -} - -extension JSONSchema.ObjectContext { - internal func combined(with other: JSONSchema.ObjectContext, resolvingIn components: OpenAPI.Components) throws -> JSONSchema.ObjectContext { - let combinedProperties = try combine(properties: properties, with: other.properties, resolvingIn: components) - - if let conflict = conflicting(maxProperties, other.maxProperties) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .object, name: "maxProperties", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(_minProperties, other._minProperties) { - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .object, name: "minProperties", original: String(conflict.0), new: String(conflict.1))) - } - if let conflict = conflicting(additionalProperties, other.additionalProperties) { - let originalDescription: String - switch conflict.0 { - case .a(let bool): - originalDescription = String(bool) - case .b(let schema): - originalDescription = String(describing: schema) - } - let newDescription: String - switch conflict.1 { - case .a(let bool): - newDescription = String(bool) - case .b(let schema): - newDescription = String(describing: schema) - } - throw JSONSchemaResolutionError(.attributeConflict(jsonType: .object, name: "additionalProperties", original: originalDescription, new: newDescription)) - } - // explicitly declaring these constants one at a time - // helps the type checker a lot. - let newMaxProperties = maxProperties ?? other.maxProperties - let newMinProperties = _minProperties ?? other._minProperties - let newAdditionalProperties = additionalProperties ?? other.additionalProperties - return .init( - properties: combinedProperties, - additionalProperties: newAdditionalProperties, - maxProperties: newMaxProperties, - minProperties: newMinProperties - ) - } -} - -internal func combine(properties left: OrderedDictionary, with right: OrderedDictionary, resolvingIn components: OpenAPI.Components) throws -> OrderedDictionary { - var combined = right - for (key, lhs) in left { - if let rhs = combined[key] { - var resolver = FragmentCombiner(components: components) - try resolver.combine([lhs, rhs]) - combined[key] = try resolver.dereferencedSchema().jsonSchema - } else { - combined[key] = lhs - } - } - return combined -} - -// MARK: - Fragment Context -> Full Context - -extension JSONSchema.CoreContext { - internal func validatedContext() throws -> JSONSchema.CoreContext { - guard let newFormat = NewFormat(rawValue: format.rawValue) else { - throw JSONSchemaResolutionError(.inconsistency("Tried to create a \(NewFormat.self) from the incompatible format value: \(format.rawValue)")) - } - - return .init( - format: newFormat, - required: required, - nullable: _nullable, - permissions: _permissions, - deprecated: _deprecated, - title: title, - description: description, - discriminator: discriminator, - externalDocs: externalDocs, - allowedValues: allowedValues, - defaultValue: defaultValue, - example: example - ) - } -} - -extension JSONSchema.IntegerContext { - internal func validatedContext() throws -> JSONSchema.IntegerContext { - let validatedMinimum: Bound? - if let minimum { - guard minimum.value >= 0 else { - throw JSONSchemaResolutionError(.inconsistency("Integer minimum (\(minimum.value) cannot be below 0")) - } - - validatedMinimum = minimum - } else { - validatedMinimum = nil - } - if let (min, max) = zip(validatedMinimum, maximum) { - guard min.value <= max.value else { - throw JSONSchemaResolutionError(.inconsistency("Integer minimum (\(min.value) cannot be higher than maximum (\(max.value)")) - } - } - return .init( - multipleOf: multipleOf, - maximum: maximum, - minimum: validatedMinimum - ) - } -} - -extension JSONSchema.NumericContext { - internal func validatedContext() throws -> JSONSchema.NumericContext { - let validatedMinimum: Bound? - if let minimum { - guard minimum.value >= 0 else { - throw JSONSchemaResolutionError(.inconsistency("Number minimum (\(minimum.value) cannot be below 0")) - } - - validatedMinimum = minimum - } else { - validatedMinimum = nil - } - if let (min, max) = zip(validatedMinimum, maximum) { - guard min.value <= max.value else { - throw JSONSchemaResolutionError(.inconsistency("Number minimum (\(min.value) cannot be higher than maximum (\(max.value)")) - } - } - return .init( - multipleOf: multipleOf, - maximum: maximum, - minimum: validatedMinimum - ) - } -} - -extension JSONSchema.StringContext { - internal func validatedContext() throws -> JSONSchema.StringContext { - if let minimum = Self._minLength(self) { - guard minimum >= 0 else { - throw JSONSchemaResolutionError(.inconsistency("String minimum length (\(minimum) cannot be less than 0")) - } - } - if let (min, max) = zip(minLength, maxLength) { - guard min <= max else { - throw JSONSchemaResolutionError(.inconsistency("String minimum length (\(min) cannot be higher than maximum (\(max)")) - } - } - return .init( - maxLength: maxLength, - minLength: Self._minLength(self), - pattern: pattern - ) - } -} - -extension JSONSchema.ArrayContext { - internal func validatedContext() throws -> JSONSchema.ArrayContext { - if let minimum = _minItems { - guard minimum >= 0 else { - throw JSONSchemaResolutionError(.inconsistency("Array minimum length (\(minimum) cannot be less than 0")) - } - } - if let (min, max) = zip(minItems, maxItems) { - guard min <= max else { - throw JSONSchemaResolutionError(.inconsistency("Array minimum length (\(min) cannot be higher than maximum (\(max)")) - } - } - return .init( - items: items, - maxItems: maxItems, - minItems: _minItems, - uniqueItems: _uniqueItems - ) - } -} - -extension JSONSchema.ObjectContext { - internal func validatedContext() throws -> JSONSchema.ObjectContext { - if let minimum = _minProperties { - guard minimum >= 0 else { - throw JSONSchemaResolutionError(.inconsistency("Object minimum number of properties (\(minimum) cannot be less than 0")) - } - } - if let (min, max) = zip(minProperties, maxProperties) { - guard min <= max else { - throw JSONSchemaResolutionError(.inconsistency("Object minimum number of properties (\(min) cannot be higher than maximum (\(max)")) - } - } - return .init( - properties: properties, - additionalProperties: additionalProperties, - maxProperties: maxProperties, - minProperties: _minProperties - ) - } -} diff --git a/Sources/OpenAPIKit30/Schema Object/SimplifiedJSONSchema.swift b/Sources/OpenAPIKit30/Schema Object/SimplifiedJSONSchema.swift deleted file mode 100644 index 3c06ab72f3..0000000000 --- a/Sources/OpenAPIKit30/Schema Object/SimplifiedJSONSchema.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// SimplifiedJSONSchema.swift -// - -import OpenAPIKitCore - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -extension JSONSchema { - /// Get a simplified `DereferencedJSONSchema`. - /// - /// A fully simplified JSON Schema is both dereferenced and also - /// reduced to a more normal form where possible. - /// - /// As an example, many compound schemas can be simplified. - /// - /// { - /// "allOf": [ - /// { "type": "object", "description": "Hello World" }, - /// { - /// "properties": [ - /// "name": { "type": "string" } - /// ] - /// } - /// ] - /// } - /// - /// simplifies to -> - /// - /// { - /// "type": "object", - /// "description": "Hello World", - /// "properties": [ - /// "name": { "type": "string" } - /// ] - /// } - /// - /// You can create simplified schemas from the `DereferencedJSONSchema` - /// type with the `simplified()` method or you can create simplified schemas from - /// the `JSONSchema` type with the `simplified(given:)` method (which - /// combines dereferencing and resolving by taking the `OpenAPI.Components` as - /// input). - public func simplified(given components: OpenAPI.Components) throws -> DereferencedJSONSchema { - return try self.dereferenced(in: components).simplified() - } -} - -extension DereferencedJSONSchema { - /// Get a simplified `DereferencedJSONSchema`. - /// - /// A fully simplified JSON Schema is both dereferenced and also - /// reduced to a more normal form where possible. - /// - /// As an example, many compound schemas can be simplified. - /// - /// { - /// "allOf": [ - /// { "type": "object", "description": "Hello World" }, - /// { - /// "properties": [ - /// "name": { "type": "string" } - /// ] - /// } - /// ] - /// } - /// - /// simplifies to -> - /// - /// { - /// "type": "object", - /// "description": "Hello World", - /// "properties": [ - /// "name": { "type": "string" } - /// ] - /// } - /// - /// You can create simplified schemas from the `DereferencedJSONSchema` - /// type with the `simplified()` method or you can create simplified schemas from - /// the `JSONSchema` type with the `simplified(given:)` method (which - /// combines dereferencing and resolving by taking the `OpenAPI.Components` as - /// input). - public func simplified() throws -> DereferencedJSONSchema { - let dereferencedSchema: DereferencedJSONSchema - switch self { - case .all: - var resolver = FragmentCombiner(components: .noComponents) - try resolver.combine(self.jsonSchema) - dereferencedSchema = try resolver.dereferencedSchema() - - // we don't currently have any schema resolution steps other than - // combining allOf schemas. We do need to dig into any other compound - // schemas to attempt to resolve them, though. - - case .object(let core, let object): - let additionalProperties: Either? = try object.additionalProperties.map { - switch $0 { - case .a(let bool): - return .a(bool) - case .b(let schema): - return .b(try schema.simplified()) - } - } - dereferencedSchema = .object( - core, - .init( - properties: try object.properties.mapValues { try $0.simplified() }, - additionalProperties: additionalProperties, - maxProperties: object.maxProperties, - minProperties: object._minProperties - ) - ) - - case .array(let core, let array): - dereferencedSchema = .array( - core, - .init( - items: try array.items.map { try $0.simplified() }, - maxItems: array.maxItems, - minItems: array._minItems, - uniqueItems: array._uniqueItems - ) - ) - - case .any(of: let schemas, core: let core): - dereferencedSchema = .any(of: try schemas.map { try $0.simplified() }, core: core) - - case .one(of: let schemas, core: let core): - dereferencedSchema = .one(of: try schemas.map { try $0.simplified() }, core: core) - - case .not(let schema, core: let core): - dereferencedSchema = .not(try schema.simplified(), core: core) - - default: - dereferencedSchema = self - } - - return dereferencedSchema - } -} diff --git a/Tests/OpenAPIKit30RealSpecSuite/SwaggerDocSamplesTests.swift b/Tests/OpenAPIKit30RealSpecSuite/SwaggerDocSamplesTests.swift index e5494fa00d..1e66f1f226 100644 --- a/Tests/OpenAPIKit30RealSpecSuite/SwaggerDocSamplesTests.swift +++ b/Tests/OpenAPIKit30RealSpecSuite/SwaggerDocSamplesTests.swift @@ -81,34 +81,6 @@ final class SwaggerDocSamplesTests: XCTestCase { XCTAssertEqual(resolvedDoc.routes.count, 1) XCTAssertEqual(resolvedDoc.endpoints.count, 1) - - let dogSchema = JSONSchema.object( - discriminator: .init(propertyName: "pet_type"), - properties: [ - "pet_type": .string, - "bark": .boolean(required: false), - "breed": .string(required: false, allowedValues: "Dingo", "Husky", "Retriever", "Shepherd") - ] - ) - let catSchema = JSONSchema.object( - discriminator: .init(propertyName: "pet_type"), - properties: [ - "pet_type": .string, - "hunts": .boolean(required: false), - "age": .integer(required: false) - ] - ) - - XCTAssertEqual( - try resolvedDoc.endpoints[0].requestBody?.content[.json]?.schema?.simplified().jsonSchema, - JSONSchema.one( - of: [ - catSchema, - dogSchema - ], - core: .init(discriminator: .init(propertyName: "pet_type")) - ) - ) } catch let error { let friendlyError = OpenAPI.Error(from: error) throw friendlyError diff --git a/Tests/OpenAPIKit30Tests/Schema Object/DereferencedSchemaObjectTests.swift b/Tests/OpenAPIKit30Tests/Schema Object/DereferencedSchemaObjectTests.swift index 8e69d58dd2..2847048fea 100644 --- a/Tests/OpenAPIKit30Tests/Schema Object/DereferencedSchemaObjectTests.swift +++ b/Tests/OpenAPIKit30Tests/Schema Object/DereferencedSchemaObjectTests.swift @@ -126,21 +126,6 @@ final class DereferencedSchemaObjectTests: XCTestCase { let t20 = JSONSchema.all(of: [.string(.init(), .init())], core: .init(discriminator: .init(propertyName: "test"))).dereferenced() XCTAssertEqual(t20, .all(of: [.string(.init(), .init())], core: .init(discriminator: .init(propertyName: "test")))) XCTAssertEqual(t20?.discriminator, .init(propertyName: "test")) - - // bonus tests around simplifying: - let t21 = try JSONSchema.all(of: []).dereferenced()?.simplified() - XCTAssertEqual(t21, .fragment(.init(description: nil))) - XCTAssertNil(t21?.discriminator) - XCTAssertNotNil(t21?.coreContext) - - let t22 = try JSONSchema.all(of: [.string(.init(), .init())]).dereferenced()?.simplified() - XCTAssertEqual(t22, .string(.init(), .init())) - XCTAssertNil(t22?.discriminator) - XCTAssertEqual(t22?.coreContext as? JSONSchema.CoreContext, .init()) - - let t23 = try JSONSchema.all(of: [.string(.init(), .init())], core: .init(discriminator: .init(propertyName: "test"))).dereferenced()?.simplified() - XCTAssertEqual(t23, .string(.init(discriminator: .init(propertyName: "test")), .init())) - XCTAssertEqual(t23?.discriminator, .init(propertyName: "test")) } func test_throwingBasicConstructionsFromSchemaObject() throws { @@ -242,21 +227,6 @@ final class DereferencedSchemaObjectTests: XCTestCase { let t20 = try JSONSchema.all(of: [.string(.init(), .init())], core: .init(discriminator: .init(propertyName: "test"))).dereferenced(in: components) XCTAssertEqual(t20, .all(of: [.string(.init(), .init())], core: .init(discriminator: .init(propertyName: "test")))) XCTAssertEqual(t20.discriminator, .init(propertyName: "test")) - - // bonus tests around simplifying: - let t21 = try JSONSchema.all(of: []).dereferenced(in: components).simplified() - XCTAssertEqual(t21, .fragment(.init(description: nil))) - XCTAssertNil(t21.discriminator) - XCTAssertNotNil(t21.coreContext) - - let t22 = try JSONSchema.all(of: [.string(.init(), .init())]).dereferenced(in: components).simplified() - XCTAssertEqual(t22, .string(.init(), .init())) - XCTAssertNil(t22.discriminator) - XCTAssertEqual(t22.coreContext as? JSONSchema.CoreContext, .init()) - - let t23 = try JSONSchema.all(of: [.string(.init(), .init())], core: .init(discriminator: .init(propertyName: "test"))).dereferenced(in: components).simplified() - XCTAssertEqual(t23, .string(.init(discriminator: .init(propertyName: "test")), .init())) - XCTAssertEqual(t23.discriminator, .init(propertyName: "test")) } func test_optionalReferenceMissing() { diff --git a/Tests/OpenAPIKit30Tests/Schema Object/SchemaFragmentCombiningTests.swift b/Tests/OpenAPIKit30Tests/Schema Object/SchemaFragmentCombiningTests.swift deleted file mode 100644 index 15ca7c739b..0000000000 --- a/Tests/OpenAPIKit30Tests/Schema Object/SchemaFragmentCombiningTests.swift +++ /dev/null @@ -1,1259 +0,0 @@ -// -// SchemaFragmentCombiningTests.swift -// - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -import XCTest -import OpenAPIKit30 - -final class SchemaFragmentCombiningTests: XCTestCase { - // MARK: - Empty - func test_resolveEmptyFragmentsList() throws { - let fragments: [JSONSchema] = [] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .fragment(.init()) - ) - } - - // MARK: - Single Fragment - func test_resolvingSingleDescription() { - let fragments: [JSONSchema] = [ - .fragment(.init(description: "hello world")) - ] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .fragment(.init(description: "hello world")) - ) - } - - func test_resolvingSingleBoolean() { - let fragments: [JSONSchema] = [ - .boolean(.init()) - ] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .boolean(.init()) - ) - } - - func test_resolvingSingleInteger() { - let fragments: [JSONSchema] = [ - .integer(.init(), .init()) - ] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .integer(.init(), .init()) - ) - } - - func test_resolvingSingleNumber() { - let fragments: [JSONSchema] = [ - .number(.init(), .init()) - ] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .number(.init(), .init()) - ) - } - - func test_resolveSingleString() { - let fragments: [JSONSchema] = [ - .string(.init(), .init()) - ] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .string(.init(), .init()) - ) - } - - func test_resolvingSingleArray() { - let fragments: [JSONSchema] = [ - .array(.init(), .init()) - ] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .array(.init(), DereferencedJSONSchema.ArrayContext(JSONSchema.ArrayContext())!) - ) - } - - func test_resolvingSingleObject() { - let fragments: [JSONSchema] = [ - .object(.init(), .init(properties: [:])) - ] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .object(.init(), DereferencedJSONSchema.ObjectContext(JSONSchema.ObjectContext(properties: [:]))!) - ) - } - - func test_resolvingSingleObjectReadOnly() { - let fragments: [JSONSchema] = [ - .object(.init(permissions: .readOnly), .init(properties: [:])) - ] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .object(.init(permissions: .readOnly), DereferencedJSONSchema.ObjectContext(JSONSchema.ObjectContext(properties: [:]))!) - ) - } - - func test_resolvingSingleObjectWriteOnly() { - let fragments: [JSONSchema] = [ - .object(.init(permissions: .writeOnly), .init(properties: [:])) - ] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .object(.init(permissions: .writeOnly), DereferencedJSONSchema.ObjectContext(JSONSchema.ObjectContext(properties: [:]))!) - ) - } - - func test_rootObjectRequired() throws { - try assertOrderIndependentCombinedEqual( - [ - .object(.init(), .init(properties: [:])) - ], - .object(.init(), DereferencedJSONSchema.ObjectContext(.init(properties: [:]))!) - ) - } - - func test_rootObjectPropertiesRequired() throws { - try assertOrderIndependentCombinedEqual( - [ - .object(.init(), .init(properties: ["test": .string])) - ], - .object( - .init(), - DereferencedJSONSchema.ObjectContext( - .init(properties: ["test": .string]) - )! - ) - ) - } - - func test_rootObjectPropertiesOptional() throws { - try assertOrderIndependentCombinedEqual( - [ - .object(.init(), .init(properties: ["test": .string(required: false)])) - ], - .object( - .init(), - DereferencedJSONSchema.ObjectContext( - .init(properties: ["test": .string(required: false)]) - )! - ) - ) - } - - // MARK: - Formats - func test_resolvingSingleIntegerWithFormat() { - let fragments: [JSONSchema] = [ - .integer(.init(format: .int32), .init()) - ] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .integer(.init(format: .int32), .init()) - ) - } - - func test_resolvingSingleNumberWithFormat() { - let fragments: [JSONSchema] = [ - .number(.init(format: .double), .init()) - ] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .number(.init(format: .double), .init()) - ) - } - - func test_resolveSingleStringWithFormat() { - let fragments: [JSONSchema] = [ - .string(.init(format: .binary), .init()) - ] - XCTAssertEqual( - try fragments.combined(resolvingAgainst: .noComponents), - .string(.init(format: .binary), .init()) - ) - } - - // MARK: - Fragment Combinations - func assertOrderIndependentCombinedEqual(_ fragments: [JSONSchema], _ schema: DereferencedJSONSchema, file: StaticString = #file, line: UInt = #line) throws { - try assertCombinedEqual(fragments, schema, file: file, line: line) - try assertCombinedEqual(fragments.reversed(), schema, file: file, line: line) - } - - func assertCombinedEqual(_ fragments: [JSONSchema], _ schema: DereferencedJSONSchema, file: StaticString = #file, line: UInt = #line) throws { - let resolved = try fragments.combined(resolvingAgainst: .noComponents) - let schemaString = try orderUnstableTestStringFromEncoding(of: schema.jsonSchema) - let resolvedSchemaString = try orderUnstableTestStringFromEncoding(of: resolved.jsonSchema) - XCTAssertEqual( - resolved, - schema, - "\n\n\(resolvedSchemaString ?? "nil") \n!=\n \(schemaString ?? "nil")", - file: (file), - line: line - ) - } - - func test_resolveAnyFragmentAndDisciminatorFragment() throws { - let fragmentsAndResults: [(JSONSchema, DereferencedJSONSchema)] = [ - (.boolean(.init()), .boolean(.init(discriminator: .init(propertyName: "test")))), - (.integer(.init(), .init()), .integer(.init(discriminator: .init(propertyName: "test")), .init())), - (.number(.init(), .init()), .number(.init(discriminator: .init(propertyName: "test")), .init())), - (.string(.init(), .init()), .string(.init(discriminator: .init(propertyName: "test")), .init())), - (.array(.init(), .init()), .array(.init(discriminator: .init(propertyName: "test")), DereferencedJSONSchema.ArrayContext(.init())!)), - (.object(.init(), .init(properties: [:])), .object(.init(discriminator: .init(propertyName: "test")), DereferencedJSONSchema.ObjectContext(.init(properties: [:]))!)) - ] - - for (fragment, result) in fragmentsAndResults { - try assertOrderIndependentCombinedEqual( - [ - fragment, - .fragment(.init(discriminator: .init(propertyName: "test"))) - ], - result - ) - } - } - - func test_resolveStringFragmentAndFormatFragment() throws { - try assertOrderIndependentCombinedEqual( - [ - .string(.init(), .init()), - .fragment(.init(format: .other("binary"))) - ], - .string(.init(format: .binary), .init()) - ) - } - - func test_threeStringFragments() throws { - try assertOrderIndependentCombinedEqual( - [ - .string(.init(description: "test"), .init(minLength: 2)), - .string(.init(format: .byte), .init(maxLength: 5)), - .string(.init(description: "test"), .init()) - ], - .string(.init(format: .byte, description: "test"), .init(maxLength: 5, minLength: 2)) - ) - } - - func test_optionalAndOptional() throws { - try assertOrderIndependentCombinedEqual( - [ - .string(required: false), - .string(required: false) - ], - .string(.init(required: false), .init()) - ) - } - - func test_requiredAndOptional() throws { - try assertOrderIndependentCombinedEqual( - [ - .string(required: false), - .string(required: true) - ], - .string(.init(), .init()) - ) - } - - func test_requiredAndRequired() throws { - try assertOrderIndependentCombinedEqual( - [ - .string(required: true), - .string(required: true) - ], - .string(.init(), .init()) - ) - } - - func test_deeperObjectFragments() throws { - let fragments: [JSONSchema] = [ - .object(.init(), .init(properties: [:], additionalProperties: .init(true))), - .object(.init(description: "nested"), .init(properties: [:])), - .object( - .init(), - .init( - properties: [ - "required": .string - ], - minProperties: 2 - ) - ), - .object( - .init(), - .init( - properties: [ - "optional": .boolean(required: false), - "someObject": .object(required: false), - "anything": .fragment(.init(description: nil)) - ], - minProperties: 2 - ) - ) - ] - - try assertCombinedEqual(fragments, .object( - .init(description: "nested"), - DereferencedJSONSchema.ObjectContext( - .init( - properties: [ - "required": .string, - "optional": .boolean(required: false), - "someObject": .object(required: false), - "anything": .fragment(.init(description: nil)) - ], - additionalProperties: .init(true), - minProperties: 2 - ) - )! - )) - - try assertCombinedEqual(fragments.reversed(), .object( - .init(description: "nested"), - DereferencedJSONSchema.ObjectContext( - .init( - properties: [ - "optional": .boolean(required: false), - "someObject": .object(required: false), - "anything": .fragment(.init(description: nil)), - "required": .string - ], - additionalProperties: .init(true), - minProperties: 2 - ) - )! - )) - } - - func test_evenDeeperObjectFragments() throws { - let fragments: [JSONSchema] = [ - .object( - .init(), - .init( - properties: [ - "more_object": .object(required: false, properties: ["boolean": .boolean]) - ] - ) - ), - .object( - .init(), - .init( - properties: [ - "more_fragments": .all( - of: [ - .object(.init(description: "nested"), .init(properties: ["someObject": .object])), - .object(.init(title: "nested test"), .init( - properties: [ - "boolean": .boolean(format: .other("integer"), required: false), - "string": .string(maxLength: 50), - "integer": .integer(required: false, maximum: (10, exclusive: false)), - "number": .number(required: false, maximum: (33.2, exclusive: false)), - "array": .array(required: false, maxItems: 22) - ] - )), - .object(.init(title: "nested test"), .init( - properties: [ - "boolean": .boolean(required: false, description: "boolean"), - "string": .string(description: "string"), - "integer": .integer(required: false, description: "integer"), - "number": .number(required: false, description: "number"), - "array": .array(required: true, description: "array") - ] - )) - ] - ) - ] - ) - ) - ] - - try assertCombinedEqual(fragments, DereferencedJSONSchema.object( - .init(), - DereferencedJSONSchema.ObjectContext( - .init( - properties: [ - "more_object": .object(required: false, properties: ["boolean": .boolean]), - "more_fragments": .object( - title: "nested test", - description: "nested", - properties: [ - "someObject": .object, - "boolean": .boolean(format: .other("integer"), required: false, description: "boolean"), - "string": .string(required: true, description: "string", maxLength: 50), - "integer": .integer(required: false, description: "integer", maximum: (10, exclusive: false)), - "number": .number(required: false, description: "number", maximum: (33.2, exclusive: false)), - "array": .array(required: true, description: "array", maxItems: 22) - ] - ) - ] - ) - )! - )) - - try assertCombinedEqual(fragments.reversed(), DereferencedJSONSchema.object( - .init(), - DereferencedJSONSchema.ObjectContext( - .init( - properties: [ - "more_fragments": .object( - title: "nested test", - description: "nested", - properties: [ - "someObject": .object, - "boolean": .boolean(format: .other("integer"), required: false, description: "boolean"), - "string": .string(required: true, description: "string", maxLength: 50), - "integer": .integer(required: false, description: "integer", maximum: (10, exclusive: false)), - "number": .number(required: false, description: "number", maximum: (33.2, exclusive: false)), - "array": .array(required: true, description: "array", maxItems: 22) - ] - ), - "more_object": .object(required: false, properties: ["boolean": .boolean]) - ] - ) - )! - )) - } - - func test_minLessThanMaxObject() throws { - try assertOrderIndependentCombinedEqual( - [ - .object(.init(), .init(properties: [:], minProperties: 2)), - .object(.init(), .init(properties: [:], maxProperties: 3)) - ], - .object(.init(), DereferencedJSONSchema.ObjectContext(.init(properties: [:], maxProperties: 3, minProperties: 2))!) - ) - } - - func test_minLessThanMaxArray() throws { - try assertOrderIndependentCombinedEqual( - [ - .array(.init(), .init(minItems: 2)), - .array(.init(), .init(maxItems: 3)) - ], - .array(.init(), DereferencedJSONSchema.ArrayContext(.init(maxItems: 3, minItems: 2))!) - ) - } - - func test_minLessThanMaxString() throws { - try assertOrderIndependentCombinedEqual( - [ - .string(.init(), .init(minLength: 2)), - .string(.init(), .init(maxLength: 3)) - ], - .string(.init(), .init(maxLength: 3, minLength: 2)) - ) - } - - func test_minLessThanMaxNumber() throws { - try assertOrderIndependentCombinedEqual( - [ - .number(.init(), .init(minimum: (2, exclusive: false))), - .number(.init(), .init(maximum: (3, exclusive: false))) - ], - .number(.init(), .init(maximum: (3, exclusive: false), minimum: (2, exclusive: false))) - ) - } - - func test_minLessThanMaxInteger() throws { - try assertOrderIndependentCombinedEqual( - [ - .integer(.init(), .init(minimum: (2, exclusive: false))), - .integer(.init(), .init(maximum: (3, exclusive: false))) - ], - .integer(.init(), .init(maximum: (3, exclusive: false), minimum: (2, exclusive: false))) - ) - } - - // MARK: - Dereferencing - func test_referenceNotFound() { - let t1 = [JSONSchema.reference(.component(named: "test"))] - XCTAssertThrowsError(try t1.combined(resolvingAgainst: .noComponents)) { error in - XCTAssertEqual((error as? OpenAPI.Components.ReferenceError)?.description, "Failed to look up a JSON Reference. \'test\' was not found in schemas.") - } - - let t2 = [ - JSONSchema.object(.init(description: "test"), .init(properties: [:])), - JSONSchema.object(.init(), .init(properties: [ "test": .reference(.component(named: "test"))])) - ] - XCTAssertThrowsError(try t2.combined(resolvingAgainst: .noComponents)) { error in - XCTAssertEqual((error as? OpenAPI.Components.ReferenceError)?.description, "Failed to look up a JSON Reference. \'test\' was not found in schemas.") - } - } - - func test_referenceFound() throws { - let components = OpenAPI.Components( - schemas: [ - "test": .string - ] - ) - - let t1 = [JSONSchema.reference(.component(named: "test"))] - let schema1 = try t1.combined(resolvingAgainst: components) - XCTAssertEqual( - schema1, - JSONSchema.string.dereferenced() - ) - - let t2 = [ - JSONSchema.object(.init(description: "test"), .init(properties: [:])), - JSONSchema.object(.init(), .init(properties: [ "test": .reference(.component(named: "test"))])) - ] - let schema2 = try t2.combined(resolvingAgainst: components) - XCTAssertEqual( - schema2, - JSONSchema.object(description: "test", properties: ["test": .string]).dereferenced() - ) - } - - // MARK: - Compound Nestings - func test_allOfInAllOf() throws { - let t1 = JSONSchema.all( - of: [ - .object(title: "hello world"), - .object(description: "hi"), - .all( - of: [ - .object( - properties: [ - "string": .string - ] - ), - .object(minProperties: 1) - ] - ) - ] - ) - - let expectedSimplification = JSONSchema.object( - title: "hello world", - description: "hi", - minProperties: 1, - properties: [ - "string": .string - ] - ).dereferenced() - - let schema = try t1.simplified(given: .noComponents) - - XCTAssertEqual(schema, expectedSimplification) - } - - // MARK: - Conflict Failures - func test_typeConflicts() { - let booleanFragment = JSONSchema.boolean(.init()) - let integerFragment = JSONSchema.integer(.init(), .init()) - let numberFragment = JSONSchema.number(.init(), .init()) - let stringFragment = JSONSchema.string(.init(), .init()) - let arrayFragment = JSONSchema.array(.init(), .init()) - let objectFragment = JSONSchema.object(.init(), .init(properties: [:])) - - let fragments = [ - booleanFragment, - integerFragment, - numberFragment, - stringFragment, - arrayFragment, - objectFragment - ] - - for left in fragments { - for right in fragments where right != left { - XCTAssertThrowsError(try [left, right].combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .typeConflict) - } - } - } - } - - func test_booleanFormatConflicts() { - // boolean does not have any built-in formats, but we can use two different custom formats. - let format1: JSONTypeFormat.BooleanFormat = .other("integer") - let format2: JSONTypeFormat.BooleanFormat = .other("textual") - - let formatStrings = [ - format1, - format2 - ] - - for left in formatStrings { - for right in formatStrings where left != right { - let fragments: [JSONSchema] = [ - .boolean(.init(format: left)), - .boolean(.init(format: right)) - ] - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .formatConflict) - } - } - } - } - - func test_integerFormatConflicts() { - let int32: JSONTypeFormat.IntegerFormat = .int32 - let int64: JSONTypeFormat.IntegerFormat = .int64 - let uint32: JSONTypeFormat.IntegerFormat = .extended(.uint32) - let other: JSONTypeFormat.IntegerFormat = .other("bigint") - - let formatStrings = [ - int32, - int64, - uint32, - other - ] - - for left in formatStrings { - for right in formatStrings where left != right { - let fragments: [JSONSchema] = [ - .integer(.init(format: left), .init()), - .integer(.init(format: right), .init()) - ] - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .formatConflict) - } - } - } - } - - func test_numberFormatConflicts() { - let float: JSONTypeFormat.NumberFormat = .float - let double: JSONTypeFormat.NumberFormat = .double - let other: JSONTypeFormat.NumberFormat = .other("bigint") - - let formatStrings = [ - float, - double, - other - ] - - for left in formatStrings { - for right in formatStrings where left != right { - let fragments: [JSONSchema] = [ - .number(.init(format: left), .init()), - .number(.init(format: right), .init()) - ] - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .formatConflict) - } - } - } - } - - func test_StringFormatConflicts() { - let byte: JSONTypeFormat.StringFormat = .byte - let binary: JSONTypeFormat.StringFormat = .binary - let date: JSONTypeFormat.StringFormat = .date - let dateTime: JSONTypeFormat.StringFormat = .dateTime - let password: JSONTypeFormat.StringFormat = .password - let uuid: JSONTypeFormat.StringFormat = .extended(.uuid) - let other: JSONTypeFormat.StringFormat = .other("moontalk") - - let formatStrings = [ - byte, - binary, - date, - dateTime, - password, - uuid, - other - ] - - for left in formatStrings { - for right in formatStrings where left != right { - let fragments: [JSONSchema] = [ - .string(.init(format: left), .init()), - .string(.init(format: right), .init()) - ] - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .formatConflict) - } - } - } - } - - func test_ArrayFormatConflicts() { - // array does not have any built-in formats, but we can use two different custom formats. - let format1: JSONTypeFormat.ArrayFormat = .other("numbered") - let format2: JSONTypeFormat.ArrayFormat = .other("bulleted") - - let formatStrings = [ - format1, - format2 - ] - - for left in formatStrings { - for right in formatStrings where left != right { - let fragments: [JSONSchema] = [ - .array(.init(format: left), .init()), - .array(.init(format: right), .init()) - ] - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .formatConflict) - } - } - } - } - - func test_ObjectFormatConflicts() { - // object does not have any built-in formats, but we can use two different custom formats. - let format1: JSONTypeFormat.ObjectFormat = .other("compact") - let format2: JSONTypeFormat.ObjectFormat = .other("pretty") - - let formatStrings = [ - format1, - format2 - ] - - for left in formatStrings { - for right in formatStrings where left != right { - let fragments: [JSONSchema] = [ - .object(.init(format: left), .init(properties: [:])), - .object(.init(format: right), .init(properties: [:])) - ] - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .formatConflict) - } - } - } - } - - func test_generalAttributeConflicts() { - - typealias AnyContext = JSONSchema.CoreContext - - let differentDescription = [ - AnyContext(description: "string1"), - AnyContext(description: "string2") - ] - - let differentDiscriminator = [ - AnyContext(discriminator: .init(propertyName: "string1")), - AnyContext(discriminator: .init(propertyName: "string2")) - ] - - let differentTitle = [ - AnyContext(title: "string1"), - AnyContext(title: "string2") - ] - - let differentNullable = [ - AnyContext(nullable: true), - AnyContext(nullable: false) - ] - - let differentDeprecated = [ - AnyContext(deprecated: true), - AnyContext(deprecated: false) - ] - - let differentExternalDocs = [ - AnyContext(externalDocs: .init(url: URL(string: "https://string1.com")!)), - AnyContext(externalDocs: .init(url: URL(string: "https://string2.com")!)) - ] - - let differentAllowedValues = [ - AnyContext(allowedValues: ["string1"]), - AnyContext(allowedValues: ["string2"]) - ] - - let differentExample = [ - AnyContext(example: "string1"), - AnyContext(example: "string2") - ] - - let differences = [ - differentDescription, - differentDiscriminator, - differentTitle, - differentNullable, - differentDeprecated, - differentExternalDocs, - differentAllowedValues, - differentExample - ] - - // break up for type checking - let fragmentsArray1: [[JSONSchema]] = differences.map { $0.map { .fragment($0) } } - let fragmentsArray2: [[JSONSchema]] = differences.map { $0.map { .boolean($0.transformed()) } } - let fragmentsArray3: [[JSONSchema]] = differences.map { $0.map { .integer($0.transformed(), .init()) } } - let fragmentsArray4: [[JSONSchema]] = differences.map { $0.map { .number($0.transformed(), .init()) } } - let fragmentsArray5: [[JSONSchema]] = differences.map { $0.map { .string($0.transformed(), .init()) } } - let fragmentsArray6: [[JSONSchema]] = differences.map { $0.map { .array($0.transformed(), .init()) } } - let fragmentsArray7: [[JSONSchema]] = differences.map { $0.map { .object($0.transformed(), .init(properties: [:])) } } - - let allFragmentsArrays = fragmentsArray1 - + fragmentsArray2 - + fragmentsArray3 - + fragmentsArray4 - + fragmentsArray5 - + fragmentsArray6 - + fragmentsArray7 - - for fragments in allFragmentsArrays { - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .attributeConflict, "\(error) is not ~= `.attributeConflict` -- \(fragments)") - } - } - } - - func test_integerAttributeConflicts() { - let differentMultipleOf = [ - JSONSchema.IntegerContext(multipleOf: 10), - JSONSchema.IntegerContext(multipleOf: 2) - ] - - let differentMaximum = [ - JSONSchema.IntegerContext(maximum: (10, exclusive: false)), - JSONSchema.IntegerContext(maximum: (100, exclusive: false)) - ] - - let differentExclusiveMaximum = [ - JSONSchema.IntegerContext(maximum: (10, exclusive: true)), - JSONSchema.IntegerContext(maximum: (10, exclusive: false)) - ] - - let differentMinimum = [ - JSONSchema.IntegerContext(minimum: (1, exclusive: false)), - JSONSchema.IntegerContext(minimum: (3, exclusive: false)) - ] - - let differentExclusiveMinimum = [ - JSONSchema.IntegerContext(minimum: (10, exclusive: true)), - JSONSchema.IntegerContext(minimum: (10, exclusive: false)) - ] - - let differences = [ - differentMultipleOf, - differentMaximum, - differentExclusiveMaximum, - differentMinimum, - differentExclusiveMinimum - ] - - for difference in differences { - let fragments: [JSONSchema] = difference.map { .integer(.init(), $0) } - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .attributeConflict, "\(error) is not ~= `.attributeConflict` -- \(fragments)") - } - } - } - - func test_numberAttributeConflicts() { - let differentMultipleOf = [ - JSONSchema.NumericContext(multipleOf: 10), - JSONSchema.NumericContext(multipleOf: 2) - ] - - let differentMaximum = [ - JSONSchema.NumericContext(maximum: (10, exclusive: false)), - JSONSchema.NumericContext(maximum: (100, exclusive: false)) - ] - - let differentExclusiveMaximum = [ - JSONSchema.NumericContext(maximum: (10, exclusive: true)), - JSONSchema.NumericContext(maximum: (10, exclusive: false)) - ] - - let differentMinimum = [ - JSONSchema.NumericContext(minimum: (1, exclusive: false)), - JSONSchema.NumericContext(minimum: (3, exclusive: false)) - ] - - let differentExclusiveMinimum = [ - JSONSchema.NumericContext(minimum: (10, exclusive: true)), - JSONSchema.NumericContext(minimum: (10, exclusive: false)) - ] - - let differences = [ - differentMultipleOf, - differentMaximum, - differentExclusiveMaximum, - differentMinimum, - differentExclusiveMinimum - ] - - for difference in differences { - let fragments: [JSONSchema] = difference.map { .number(.init(), $0) } - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .attributeConflict, "\(error) is not ~= `.attributeConflict` -- \(fragments)") - } - } - } - - func test_StringAttributeConflicts() { - let differentMaxLength = [ - JSONSchema.StringContext(maxLength: 10), - JSONSchema.StringContext(maxLength: 2) - ] - - let differentMinLength = [ - JSONSchema.StringContext(minLength: 10), - JSONSchema.StringContext(minLength: 100) - ] - - let differentPattern = [ - JSONSchema.StringContext(pattern: "string1"), - JSONSchema.StringContext(pattern: "string2") - ] - - let differences = [ - differentMaxLength, - differentMinLength, - differentPattern - ] - - for difference in differences { - let fragments: [JSONSchema] = difference.map { .string(.init(), $0) } - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .attributeConflict, "\(error) is not ~= `.attributeConflict` -- \(fragments)") - } - } - } - - func test_ArrayAttributeConflicts() { - let differentItems = [ - JSONSchema.ArrayContext(items: .string), - JSONSchema.ArrayContext(items: .boolean) - ] - - let differentMaxItems = [ - JSONSchema.ArrayContext(maxItems: 10), - JSONSchema.ArrayContext(maxItems: 100) - ] - - let differentMinItems = [ - JSONSchema.ArrayContext(minItems: 1), - JSONSchema.ArrayContext(minItems: 2) - ] - - let differentUniqueItems = [ - JSONSchema.ArrayContext(uniqueItems: true), - JSONSchema.ArrayContext(uniqueItems: false) - ] - - let differences = [ - differentItems, - differentMaxItems, - differentMinItems, - differentUniqueItems - ] - - for difference in differences { - let fragments: [JSONSchema] = difference.map { .array(.init(), $0) } - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .attributeConflict, "\(error) is not ~= `.attributeConflict` -- \(fragments)") - } - } - } - - func test_ObjectAttributeConflicts() { - let differentMaxProperties = [ - JSONSchema.ObjectContext(properties: [:], maxProperties: 10), - JSONSchema.ObjectContext(properties: [:], maxProperties: 2) - ] - - let differentMinProperties = [ - JSONSchema.ObjectContext(properties: [:], minProperties: 10), - JSONSchema.ObjectContext(properties: [:], minProperties: 100) - ] - - let differentProperties = [ - JSONSchema.ObjectContext(properties: ["string1": .string(description: "truth")]), - JSONSchema.ObjectContext(properties: ["string1": .string(description: "falsity")]) - ] - - let differentAdditionalProperties1 = [ - JSONSchema.ObjectContext(properties: [:], additionalProperties: .init(true)), - JSONSchema.ObjectContext(properties: [:], additionalProperties: .init(false)) - ] - - let differentAdditionalProperties2 = [ - JSONSchema.ObjectContext(properties: [:], additionalProperties: .init(true)), - JSONSchema.ObjectContext(properties: [:], additionalProperties: .init(.string)) - ] - - let differentAdditionalProperties3 = [ - JSONSchema.ObjectContext(properties: [:], additionalProperties: .init(.boolean)), - JSONSchema.ObjectContext(properties: [:], additionalProperties: .init(.string)) - ] - - let differences = [ - differentMaxProperties, - differentMinProperties, - differentProperties, - differentAdditionalProperties1, - differentAdditionalProperties2, - differentAdditionalProperties3 - ] - - for difference in differences { - let fragments: [JSONSchema] = difference.map { .object(.init(), $0) } - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents), "\(fragments)") { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .attributeConflict, "\(error) is not ~= `.attributeConflict` -- \(fragments)") - } - } - } - - // MARK: - Inconsistency Failures - func test_generalGenericErrors() { - - let fragmentsArray: [[JSONSchema]] = [ - // boolean readOnly/writeOnly, readOnly/readWrite, writeOnly/readWrite - [ - .boolean(.init(permissions: .readOnly)), - .boolean(.init(permissions: .writeOnly)) - ], - [ - .boolean(.init(permissions: .readOnly)), - .boolean(.init(permissions: .readWrite)) - ], - [ - .boolean(.init(permissions: .writeOnly)), - .boolean(.init(permissions: .readWrite)) - ], - // integer readOnly/writeOnly, readOnly/readWrite, writeOnly/readWrite - [ - .integer(.init(permissions: .readOnly), .init()), - .integer(.init(permissions: .writeOnly), .init()) - ], - [ - .integer(.init(permissions: .readOnly), .init()), - .integer(.init(permissions: .readWrite), .init()) - ], - [ - .integer(.init(permissions: .writeOnly), .init()), - .integer(.init(permissions: .readWrite), .init()) - ], - // number readOnly/writeOnly, readOnly/readWrite, writeOnly/readWrite - [ - .number(.init(permissions: .readOnly), .init()), - .number(.init(permissions: .writeOnly), .init()) - ], - [ - .number(.init(permissions: .readOnly), .init()), - .number(.init(permissions: .readWrite), .init()) - ], - [ - .number(.init(permissions: .writeOnly), .init()), - .number(.init(permissions: .readWrite), .init()) - ], - // string readOnly/writeOnly, readOnly/readWrite, writeOnly/readWrite - [ - .string(.init(permissions: .readOnly), .init()), - .string(.init(permissions: .writeOnly), .init()) - ], - [ - .string(.init(permissions: .readOnly), .init()), - .string(.init(permissions: .readWrite), .init()) - ], - [ - .string(.init(permissions: .writeOnly), .init()), - .string(.init(permissions: .readWrite), .init()) - ], - // array readOnly/writeOnly, readOnly/readWrite, writeOnly/readWrite - [ - .array(.init(permissions: .readOnly), .init()), - .array(.init(permissions: .writeOnly), .init()) - ], - [ - .array(.init(permissions: .readOnly), .init()), - .array(.init(permissions: .readWrite), .init()) - ], - [ - .array(.init(permissions: .writeOnly), .init()), - .array(.init(permissions: .readWrite), .init()) - ], - // object readOnly/writeOnly, readOnly/readWrite, writeOnly/readWrite - [ - .object(.init(permissions: .readOnly), .init(properties: [:])), - .object(.init(permissions: .writeOnly), .init(properties: [:])) - ], - [ - .object(.init(permissions: .readOnly), .init(properties: [:])), - .object(.init(permissions: .readWrite), .init(properties: [:])) - ], - [ - .object(.init(permissions: .writeOnly), .init(properties: [:])), - .object(.init(permissions: .readWrite), .init(properties: [:])) - ] - ] - - for fragments in fragmentsArray { - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .inconsistency, "\(error) is not ~= `.inconsistency` -- \(fragments)") - } - } - } - - func test_integerGenericErrors() { - - let minBelowZero = [ - JSONSchema.IntegerContext(minimum: (-1, exclusive: false)) - ] - - let minHigherThanMax = [ - JSONSchema.IntegerContext(minimum: (10, exclusive: false)), - JSONSchema.IntegerContext(maximum: (2, exclusive: false)) - ] - - let inconsistencies = [ - minBelowZero, - minHigherThanMax - ] - - // break up for type checking - let fragmentsArray: [[JSONSchema]] = inconsistencies.map { $0.map { .integer(.init(), $0) } } - - for fragments in fragmentsArray { - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .inconsistency, "\(error) is not ~= `.inconsistency` -- \(fragments)") - } - } - } - - func test_numberGenericErrors() { - - let minBelowZero = [ - JSONSchema.NumericContext(minimum: (-1, exclusive: false)) - ] - - let minHigherThanMax = [ - JSONSchema.NumericContext(minimum: (10, exclusive: false)), - JSONSchema.NumericContext(maximum: (2, exclusive: false)) - ] - - let inconsistencies = [ - minBelowZero, - minHigherThanMax - ] - - // break up for type checking - let fragmentsArray: [[JSONSchema]] = inconsistencies.map { $0.map { .number(.init(), $0) } } - - for fragments in fragmentsArray { - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .inconsistency, "\(error) is not ~= `.inconsistency` -- \(fragments)") - } - } - } - - func test_stringGenericErrors() { - - let minBelowZero = [ - JSONSchema.StringContext(minLength: -1) - ] - - let minHigherThanMax = [ - JSONSchema.StringContext(minLength: 10), - JSONSchema.StringContext(maxLength: 2) - ] - - let inconsistencies = [ - minBelowZero, - minHigherThanMax - ] - - // break up for type checking - let fragmentsArray: [[JSONSchema]] = inconsistencies.map { $0.map { .string(.init(), $0) } } - - for fragments in fragmentsArray { - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .inconsistency, "\(error) is not ~= `.inconsistency` -- \(fragments)") - } - } - } - - func test_arrayGenericErrors() { - - let minBelowZero = [ - JSONSchema.ArrayContext(minItems: -1) - ] - - let minHigherThanMax = [ - JSONSchema.ArrayContext(minItems: 10), - JSONSchema.ArrayContext(maxItems: 2) - ] - - let inconsistencies = [ - minBelowZero, - minHigherThanMax - ] - - // break up for type checking - let fragmentsArray: [[JSONSchema]] = inconsistencies.map { $0.map { .array(.init(), $0) } } - - for fragments in fragmentsArray { - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .inconsistency, "\(error) is not ~= `.inconsistency` -- \(fragments)") - } - } - } - - func test_objectGenericErrors() { - - let minBelowZero = [ - JSONSchema.ObjectContext(properties: [:], minProperties: -1) - ] - - let minHigherThanMax = [ - JSONSchema.ObjectContext(properties: [:], minProperties: 10), - JSONSchema.ObjectContext(properties: [:], maxProperties: 2) - ] - - let inconsistencies = [ - minBelowZero, - minHigherThanMax - ] - - // break up for type checking - let fragmentsArray: [[JSONSchema]] = inconsistencies.map { $0.map { .object(.init(), $0) } } - - for fragments in fragmentsArray { - XCTAssertThrowsError(try fragments.combined(resolvingAgainst: .noComponents)) { error in - guard let error = error as? JSONSchemaResolutionError else { XCTFail("Received unexpected error"); return } - XCTAssert(error ~= .inconsistency, "\(error) is not ~= `.inconsistency` -- \(fragments)") - } - } - } -} - -extension JSONSchema.CoreContext { - internal func transformed() -> JSONSchema.CoreContext { - - return .init( - format: NewFormat(rawValue: format.rawValue)!, - required: required, - nullable: nullable, - permissions: permissions, - deprecated: deprecated, - title: title, - description: description, - discriminator: discriminator, - externalDocs: externalDocs, - allowedValues: allowedValues, - example: example - ) - } -} diff --git a/Tests/OpenAPIKit30Tests/Schema Object/SchemaFragmentTests.swift b/Tests/OpenAPIKit30Tests/Schema Object/SchemaFragmentTests.swift index d1272115d8..dc06fe6323 100644 --- a/Tests/OpenAPIKit30Tests/Schema Object/SchemaFragmentTests.swift +++ b/Tests/OpenAPIKit30Tests/Schema Object/SchemaFragmentTests.swift @@ -53,16 +53,6 @@ final class SchemaFragmentTests: XCTestCase { let generalProperties = JSONSchema.CoreContext(format: .other("date"), nullable: false, permissions: .readWrite, deprecated: false, title: "Date", description: "a date", discriminator: .init(propertyName: "test"), externalDocs: .init(url: URL(string: "http://url.com")!), allowedValues: [], example: "2020-01-01") let t1 = JSONSchema.fragment(generalProperties) assertSameGeneralProperties(t1, as: generalProperties) - let t2 = JSONSchema.integer(generalProperties.transformed(), .init(multipleOf: 10, maximum: (20, exclusive: false), minimum: (0, exclusive: true))) - assertSameGeneralProperties(t2, as: generalProperties) - let t3 = JSONSchema.number(generalProperties.transformed(), .init(multipleOf: 12.5, maximum: (25, exclusive: false), minimum: (0, exclusive: false))) - assertSameGeneralProperties(t3, as: generalProperties) - let t4 = JSONSchema.string(generalProperties.transformed(), .init(maxLength: 5, minLength: 1, pattern: ".*")) - assertSameGeneralProperties(t4, as: generalProperties) - let t5 = JSONSchema.array(generalProperties.transformed(), .init(items: .string, maxItems: 7, minItems: 2, uniqueItems: true)) - assertSameGeneralProperties(t5, as: generalProperties) - let t6 = JSONSchema.object(generalProperties.transformed(), .init(properties: ["hello": .string], additionalProperties: .init(.string), maxProperties: 100, minProperties: 0)) - assertSameGeneralProperties(t6, as: generalProperties) } func test_jsonType() {