From a32cfe6a213f6e2e963f2abc1486312e57a30053 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 22 Apr 2020 17:24:01 -0700 Subject: [PATCH] support whole number floats decoded as integers with decoders like yams that don't like such things. --- .../Schema Object/SchemaObjectContext.swift | 31 ++++++++-- .../PetStoreAPITests.swift | 2 +- .../TomTomAPITests.swift | 2 +- .../Schema Object/SchemaObjectTests.swift | 22 +++++--- .../Schema Object/SchemaObjectYamsTests.swift | 56 +++++++++++++++++++ 5 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 Tests/OpenAPIKitTests/Schema Object/SchemaObjectYamsTests.swift diff --git a/Sources/OpenAPIKit/Schema Object/SchemaObjectContext.swift b/Sources/OpenAPIKit/Schema Object/SchemaObjectContext.swift index 9b90f2f94..ee018d8e3 100644 --- a/Sources/OpenAPIKit/Schema Object/SchemaObjectContext.swift +++ b/Sources/OpenAPIKit/Schema Object/SchemaObjectContext.swift @@ -504,10 +504,33 @@ extension JSONSchema.IntegerContext: Decodable { let exclusiveMaximum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMaximum) ?? false let exclusiveMinimum = try container.decodeIfPresent(Bool.self, forKey: .exclusiveMinimum) ?? false - maximum = (try container.decodeIfPresent(Int.self, forKey: .maximum)) - .map { Bound(value: $0, exclusive: exclusiveMaximum) } - minimum = (try container.decodeIfPresent(Int.self, forKey: .minimum)) - .map { Bound(value: $0, exclusive: exclusiveMinimum) } + // the following acrobatics thanks to some libraries (namely Yams) not + // being willing to decode floating point representations of whole numbers + // as integer values. + let maximumAttempt = try container.decodeIfPresent(Double.self, forKey: .maximum) + let minimumAttempt = try container.decodeIfPresent(Double.self, forKey: .minimum) + + maximum = try maximumAttempt.map { floatMax in + guard let integer = Int(exactly: floatMax) else { + throw InconsistencyError( + subjectName: "maximum", + details: "Expected an Integer literal but found a floating point value", + codingPath: decoder.codingPath + ) + } + return integer + }.map { Bound(value: $0, exclusive: exclusiveMaximum) } + + minimum = try minimumAttempt.map { floatMin in + guard let integer = Int(exactly: floatMin) else { + throw InconsistencyError( + subjectName: "minimum", + details: "Expected an Integer literal but found a floating point value", + codingPath: decoder.codingPath + ) + } + return integer + }.map { Bound(value: $0, exclusive: exclusiveMinimum) } } } diff --git a/Tests/OpenAPIKitCompatibilitySuite/PetStoreAPITests.swift b/Tests/OpenAPIKitCompatibilitySuite/PetStoreAPITests.swift index 2452c4cb8..3a02564d6 100644 --- a/Tests/OpenAPIKitCompatibilitySuite/PetStoreAPITests.swift +++ b/Tests/OpenAPIKitCompatibilitySuite/PetStoreAPITests.swift @@ -34,7 +34,7 @@ final class PetStoreAPICampatibilityTests: XCTestCase { func test_successfullyParsedDocument() { switch petStoreAPI { case nil: - XCTFail("Did not attempt to pull Google Books API documentation like expected.") + XCTFail("Did not attempt to pull Pet Store API documentation like expected.") case .failure(let error): let prettyError = OpenAPI.Error(from: error) XCTFail(prettyError.localizedDescription + "\n coding path: " + prettyError.codingPathString) diff --git a/Tests/OpenAPIKitCompatibilitySuite/TomTomAPITests.swift b/Tests/OpenAPIKitCompatibilitySuite/TomTomAPITests.swift index 8d09e93c0..79740baed 100644 --- a/Tests/OpenAPIKitCompatibilitySuite/TomTomAPITests.swift +++ b/Tests/OpenAPIKitCompatibilitySuite/TomTomAPITests.swift @@ -34,7 +34,7 @@ final class TomTomAPICampatibilityTests: XCTestCase { func test_successfullyParsedDocument() { switch tomtomAPI { case nil: - XCTFail("Did not attempt to pull Google Books API documentation like expected.") + XCTFail("Did not attempt to pull TomTom API documentation like expected.") case .failure(let error): let prettyError = OpenAPI.Error(from: error) XCTFail(prettyError.localizedDescription + "\n coding path: " + prettyError.codingPathString) diff --git a/Tests/OpenAPIKitTests/Schema Object/SchemaObjectTests.swift b/Tests/OpenAPIKitTests/Schema Object/SchemaObjectTests.swift index f80139e24..961c28190 100644 --- a/Tests/OpenAPIKitTests/Schema Object/SchemaObjectTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/SchemaObjectTests.swift @@ -3001,18 +3001,21 @@ extension SchemaObjectTests { ]) } - func test_decodeIntegerWithMaximum() { + func test_decodeIntegerWithMaximum() throws { let integerData = #"{"type": "integer", "maximum": 1}"#.data(using: .utf8)! let nullableIntegerData = #"{"type": "integer", "maximum": 1, "nullable": true}"#.data(using: .utf8)! let allowedValueIntegerData = #"{"type": "integer", "maximum": 2, "enum": [1, 2]}"#.data(using: .utf8)! + let integerWithWholeNumberFloatData = #"{"type": "integer", "maximum": 1.0}"#.data(using: .utf8)! - let integer = try! testDecoder.decode(JSONSchema.self, from: integerData) - let nullableInteger = try! testDecoder.decode(JSONSchema.self, from: nullableIntegerData) - let allowedValueInteger = try! testDecoder.decode(JSONSchema.self, from: allowedValueIntegerData) + let integer = try testDecoder.decode(JSONSchema.self, from: integerData) + let nullableInteger = try testDecoder.decode(JSONSchema.self, from: nullableIntegerData) + let allowedValueInteger = try testDecoder.decode(JSONSchema.self, from: allowedValueIntegerData) + let integerWithWholeNumberFloat = try testDecoder.decode(JSONSchema.self, from: integerWithWholeNumberFloatData) XCTAssertEqual(integer, JSONSchema.integer(.init(format: .generic, required: false), .init(maximum: (1, exclusive:false)))) XCTAssertEqual(nullableInteger, JSONSchema.integer(.init(format: .generic, required: false, nullable: true), .init(maximum: (1, exclusive:false)))) XCTAssertEqual(allowedValueInteger, JSONSchema.integer(.init(format: .generic, required: false, allowedValues: [1, 2]), .init(maximum: (2, exclusive:false)))) + XCTAssertEqual(integerWithWholeNumberFloat, JSONSchema.integer(required: false, maximum: (1, exclusive: false))) } func test_encodeIntegerWithExclusiveMaximum() { @@ -3105,18 +3108,21 @@ extension SchemaObjectTests { ]) } - func test_decodeIntegerWithMinimum() { + func test_decodeIntegerWithMinimum() throws { let integerData = #"{"type": "integer", "minimum": 1}"#.data(using: .utf8)! let nullableIntegerData = #"{"type": "integer", "minimum": 1, "nullable": true}"#.data(using: .utf8)! let allowedValueIntegerData = #"{"type": "integer", "minimum": 1, "enum": [1, 2]}"#.data(using: .utf8)! + let integerWithWholeNumberFloatData = #"{"type": "integer", "minimum": 1.0}"#.data(using: .utf8)! - let integer = try! testDecoder.decode(JSONSchema.self, from: integerData) - let nullableInteger = try! testDecoder.decode(JSONSchema.self, from: nullableIntegerData) - let allowedValueInteger = try! testDecoder.decode(JSONSchema.self, from: allowedValueIntegerData) + let integer = try testDecoder.decode(JSONSchema.self, from: integerData) + let nullableInteger = try testDecoder.decode(JSONSchema.self, from: nullableIntegerData) + let allowedValueInteger = try testDecoder.decode(JSONSchema.self, from: allowedValueIntegerData) + let integerWithWholeNumberFloat = try testDecoder.decode(JSONSchema.self, from: integerWithWholeNumberFloatData) XCTAssertEqual(integer, JSONSchema.integer(.init(format: .generic, required: false), .init(minimum: (1, exclusive:false)))) XCTAssertEqual(nullableInteger, JSONSchema.integer(.init(format: .generic, required: false, nullable: true), .init(minimum: (1, exclusive:false)))) XCTAssertEqual(allowedValueInteger, JSONSchema.integer(.init(format: .generic, required: false, allowedValues: [1, 2]), .init(minimum: (1, exclusive:false)))) + XCTAssertEqual(integerWithWholeNumberFloat, JSONSchema.integer(required: false, minimum: (1, exclusive: false))) } func test_encodeIntegerWithExclusiveMinimum() { diff --git a/Tests/OpenAPIKitTests/Schema Object/SchemaObjectYamsTests.swift b/Tests/OpenAPIKitTests/Schema Object/SchemaObjectYamsTests.swift new file mode 100644 index 000000000..d01aa6f54 --- /dev/null +++ b/Tests/OpenAPIKitTests/Schema Object/SchemaObjectYamsTests.swift @@ -0,0 +1,56 @@ +// +// SchemaObjectYamsTests.swift +// +// +// Created by Mathew Polzin on 4/22/20. +// + +/// +/// This file exists to facilitate regression tests for Yams-specific problems encountered +/// and fixed. +/// + +import Foundation +import XCTest +import OpenAPIKit +import Yams + +final class SchemaObjectYamsTests: XCTestCase { + func test_floatingPointWholeNumberIntegerDecode() throws { + let integerString = +""" +type: integer +minimum: 1.0 +maximum: 10.0 +""" + + let integer = try YAMLDecoder().decode(JSONSchema.self, from: integerString) + + XCTAssertEqual( + integer, + JSONSchema.integer(required: false, maximum: (10, exclusive: false), minimum: (1, exclusive: false)) + ) + } + + func test_floatingPointIntegerDecodeFails() { + let integerString = +""" +type: integer +maximum: 10.2 +""" + + XCTAssertThrowsError(try YAMLDecoder().decode(JSONSchema.self, from: integerString)) { error in + XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Inconsistency encountered when parsing `maximum`: Expected an Integer literal but found a floating point value.") + } + + let integerString2 = +""" +type: integer +minimum: 1.1 +""" + + XCTAssertThrowsError(try YAMLDecoder().decode(JSONSchema.self, from: integerString2)) { error in + XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Inconsistency encountered when parsing `minimum`: Expected an Integer literal but found a floating point value.") + } + } +}