Skip to content

Commit f93e9bd

Browse files
authored
Merge pull request #342 from mattpolzin/improve-inconsistency-errors
Improve integer max/min parsing and inconsistency errors
2 parents f7f08a9 + 7d10cf8 commit f93e9bd

File tree

11 files changed

+189
-44
lines changed

11 files changed

+189
-44
lines changed

Sources/OpenAPIKit/Encoding and Decoding Errors/DocumentDecodingError.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ extension OpenAPI.Error.Decoding.Document {
6767
case .other(let decodingError):
6868
return decodingError.relativeCodingPathString
6969
case .inconsistency(let error):
70-
return error.codingPath.isEmpty
71-
? ""
72-
: error.codingPath.dropLast().stringValue
70+
return error.codingPath.isEmpty ? ""
71+
: error.pathIncludesSubject ? error.codingPath.dropLast().stringValue
72+
: error.codingPath.stringValue
7373
case .path(let pathError):
7474
return pathError.relativeCodingPathString
7575
case .neither(let eitherError):

Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift

+22-15
Original file line numberDiff line numberDiff line change
@@ -977,30 +977,37 @@ extension JSONSchema.IntegerContext: Decodable {
977977
// the following acrobatics thanks to some libraries (namely Yams) not
978978
// being willing to decode floating point representations of whole numbers
979979
// as integer values.
980-
let exclusiveMaximumAttempt = try container.decodeIfPresent(Double.self, forKey: .exclusiveMaximum)
981-
let exclusiveMinimumAttempt = try container.decodeIfPresent(Double.self, forKey: .exclusiveMinimum)
982-
983-
let maximumAttempt = try container.decodeIfPresent(Double.self, forKey: .maximum)
984-
let minimumAttempt = try container.decodeIfPresent(Double.self, forKey: .minimum)
985-
986-
func boundFromAttempt(_ attempt: Double?, max: Bool, exclusive: Bool) throws -> Bound? {
987-
return try attempt.map { floatVal in
980+
let exclusiveIntegerMaximumAttempt = try? container.decodeIfPresent(Int.self, forKey: .exclusiveMaximum)
981+
let exclusiveIntegerMinimumAttempt = try? container.decodeIfPresent(Int.self, forKey: .exclusiveMinimum)
982+
let exclusiveDoubleMaximumAttempt = try container.decodeIfPresent(Double.self, forKey: .exclusiveMaximum)
983+
let exclusiveDoubleMinimumAttempt = try container.decodeIfPresent(Double.self, forKey: .exclusiveMinimum)
984+
985+
let maximumIntegerAttempt = try? container.decodeIfPresent(Int.self, forKey: .maximum)
986+
let minimumIntegerAttempt = try? container.decodeIfPresent(Int.self, forKey: .minimum)
987+
let maximumDoubleAttempt = try container.decodeIfPresent(Double.self, forKey: .maximum)
988+
let minimumDoubleAttempt = try container.decodeIfPresent(Double.self, forKey: .minimum)
989+
990+
func boundFrom(integer intAttempt: Int?, double doubleAttempt: Double?, max: Bool, exclusive: Bool) throws -> Bound? {
991+
let value = try intAttempt
992+
?? doubleAttempt.map { floatVal in
988993
guard let integer = Int(exactly: floatVal) else {
989994
throw InconsistencyError(
990995
subjectName: max ? "maximum" : "minimum",
991-
details: "Expected an Integer literal but found a floating point value",
992-
codingPath: decoder.codingPath
996+
details: "Expected an Integer literal but found a floating point value (\(String(describing: floatVal)))",
997+
codingPath: decoder.codingPath,
998+
pathIncludesSubject: false
993999
)
9941000
}
995-
return Bound(value: integer, exclusive: exclusive)
1001+
return integer
9961002
}
1003+
return value.map { Bound(value: $0, exclusive: exclusive) }
9971004
}
9981005

999-
maximum = try boundFromAttempt(exclusiveMaximumAttempt, max: true, exclusive: true)
1000-
?? boundFromAttempt(maximumAttempt, max: true, exclusive: false)
1006+
maximum = try boundFrom(integer: exclusiveIntegerMaximumAttempt, double: exclusiveDoubleMaximumAttempt, max: true, exclusive: true)
1007+
?? boundFrom(integer: maximumIntegerAttempt, double: maximumDoubleAttempt, max: true, exclusive: false)
10011008

1002-
minimum = try boundFromAttempt(exclusiveMinimumAttempt, max: false, exclusive: true)
1003-
?? boundFromAttempt(minimumAttempt, max: false, exclusive: false)
1009+
minimum = try boundFrom(integer: exclusiveIntegerMinimumAttempt, double: exclusiveDoubleMinimumAttempt, max: false, exclusive: true)
1010+
?? boundFrom(integer: minimumIntegerAttempt, double: minimumDoubleAttempt, max: false, exclusive: false)
10041011
}
10051012
}
10061013

Sources/OpenAPIKit30/Encoding and Decoding Errors/DocumentDecodingError.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ extension OpenAPI.Error.Decoding.Document {
6767
case .other(let decodingError):
6868
return decodingError.relativeCodingPathString
6969
case .inconsistency(let error):
70-
return error.codingPath.isEmpty
71-
? ""
72-
: error.codingPath.dropLast().stringValue
70+
return error.codingPath.isEmpty ? ""
71+
: error.pathIncludesSubject ? error.codingPath.dropLast().stringValue
72+
: error.codingPath.stringValue
7373
case .path(let pathError):
7474
return pathError.relativeCodingPathString
7575
case .neither(let eitherError):

Sources/OpenAPIKit30/Schema Object/JSONSchemaContext.swift

+20-10
Original file line numberDiff line numberDiff line change
@@ -836,30 +836,40 @@ extension JSONSchema.IntegerContext: Decodable {
836836
// the following acrobatics thanks to some libraries (namely Yams) not
837837
// being willing to decode floating point representations of whole numbers
838838
// as integer values.
839-
let maximumAttempt = try container.decodeIfPresent(Double.self, forKey: .maximum)
840-
let minimumAttempt = try container.decodeIfPresent(Double.self, forKey: .minimum)
839+
let maximumIntegerAttempt = try? container.decodeIfPresent(Int.self, forKey: .maximum)
840+
let minimumIntegerAttempt = try? container.decodeIfPresent(Int.self, forKey: .minimum)
841+
let maximumDoubleAttempt = try container.decodeIfPresent(Double.self, forKey: .maximum)
842+
let minimumDoubleAttempt = try container.decodeIfPresent(Double.self, forKey: .minimum)
841843

842-
maximum = try maximumAttempt.map { floatMax in
844+
let maximumAttempt = try maximumIntegerAttempt
845+
?? maximumDoubleAttempt.map { floatMax in
843846
guard let integer = Int(exactly: floatMax) else {
844847
throw InconsistencyError(
845848
subjectName: "maximum",
846-
details: "Expected an Integer literal but found a floating point value",
847-
codingPath: decoder.codingPath
849+
details: "Expected an Integer literal but found a floating point value (\(String(describing: floatMax)))",
850+
codingPath: decoder.codingPath,
851+
pathIncludesSubject: false
848852
)
849853
}
850854
return integer
851-
}.map { Bound(value: $0, exclusive: exclusiveMaximum) }
855+
}
856+
857+
maximum = maximumAttempt.map { Bound(value: $0, exclusive: exclusiveMaximum) }
852858

853-
minimum = try minimumAttempt.map { floatMin in
859+
let minimumAttempt = try minimumIntegerAttempt
860+
?? minimumDoubleAttempt.map { floatMin in
854861
guard let integer = Int(exactly: floatMin) else {
855862
throw InconsistencyError(
856863
subjectName: "minimum",
857-
details: "Expected an Integer literal but found a floating point value",
858-
codingPath: decoder.codingPath
864+
details: "Expected an Integer literal but found a floating point value (\(String(describing: floatMin)))",
865+
codingPath: decoder.codingPath,
866+
pathIncludesSubject: false
859867
)
860868
}
861869
return integer
862-
}.map { Bound(value: $0, exclusive: exclusiveMinimum) }
870+
}
871+
872+
minimum = minimumAttempt.map { Bound(value: $0, exclusive: exclusiveMinimum) }
863873
}
864874
}
865875

Sources/OpenAPIKitCore/Encoding and Decoding Errors And Warnings/InconsistencyError.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public struct InconsistencyError: Swift.Error, CustomStringConvertible, OpenAPIE
1212
public let subjectName: String
1313
public let details: String
1414
public let codingPath: [CodingKey]
15+
public let pathIncludesSubject: Bool
1516

1617
public var contextString: String { "" }
1718
public var errorCategory: ErrorCategory { .inconsistency(details: details) }
@@ -20,9 +21,10 @@ public struct InconsistencyError: Swift.Error, CustomStringConvertible, OpenAPIE
2021

2122
public var description: String { localizedDescription }
2223

23-
public init(subjectName: String, details: String, codingPath: [CodingKey]) {
24+
public init(subjectName: String, details: String, codingPath: [CodingKey], pathIncludesSubject: Bool = true) {
2425
self.subjectName = subjectName
2526
self.details = details
2627
self.codingPath = codingPath
28+
self.pathIncludesSubject = pathIncludesSubject
2729
}
2830
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// SchemaErrorTests.swift
3+
//
4+
//
5+
// Created by Mathew Polzin.
6+
//
7+
8+
import Foundation
9+
import XCTest
10+
import OpenAPIKit30
11+
import Yams
12+
13+
final class SchemaErrorTests: XCTestCase {
14+
func test_nonIntegerMaximumForIntegerSchema() {
15+
let documentYML =
16+
"""
17+
openapi: "3.0.0"
18+
info:
19+
title: test
20+
version: 1.0
21+
paths:
22+
/hello/world:
23+
get:
24+
responses:
25+
'200':
26+
description: hello
27+
content:
28+
'application/json':
29+
schema:
30+
type: integer
31+
maximum: 1.234
32+
"""
33+
34+
XCTAssertThrowsError(try testDecoder.decode(OpenAPI.Document.self, from: documentYML)) { error in
35+
36+
let openAPIError = OpenAPI.Error(from: error)
37+
38+
XCTAssertEqual(openAPIError.localizedDescription, "Found neither a $ref nor a JSONSchema in .content['application/json'].schema for the status code '200' response of the **GET** endpoint under `/hello/world`. \n\nJSONSchema could not be decoded because:\nInconsistency encountered when parsing `maximum`: Expected an Integer literal but found a floating point value (1.234)..")
39+
XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [
40+
"paths",
41+
"/hello/world",
42+
"get",
43+
"responses",
44+
"200",
45+
"content",
46+
"application/json",
47+
"schema"
48+
])
49+
}
50+
}
51+
}

Tests/OpenAPIKit30Tests/Schema Object/JSONSchemaTests.swift

+20-8
Original file line numberDiff line numberDiff line change
@@ -4099,16 +4099,19 @@ extension SchemaObjectTests {
40994099
let nullableIntegerData = #"{"type": "integer", "maximum": 1, "nullable": true}"#.data(using: .utf8)!
41004100
let allowedValueIntegerData = #"{"type": "integer", "maximum": 2, "enum": [1, 2]}"#.data(using: .utf8)!
41014101
let integerWithWholeNumberFloatData = #"{"type": "integer", "maximum": 1.0}"#.data(using: .utf8)!
4102+
let integerWithLargestPossibleMaxData = #"{"type": "integer", "maximum": 9223372036854775807}"#.data(using: .utf8)!
41024103

41034104
let integer = try orderUnstableDecode(JSONSchema.self, from: integerData)
41044105
let nullableInteger = try orderUnstableDecode(JSONSchema.self, from: nullableIntegerData)
41054106
let allowedValueInteger = try orderUnstableDecode(JSONSchema.self, from: allowedValueIntegerData)
41064107
let integerWithWholeNumberFloat = try orderUnstableDecode(JSONSchema.self, from: integerWithWholeNumberFloatData)
4108+
let integerWithLargestPossibleMax = try orderUnstableDecode(JSONSchema.self, from: integerWithLargestPossibleMaxData)
41074109

41084110
XCTAssertEqual(integer, JSONSchema.integer(.init(format: .generic), .init(maximum: (1, exclusive:false))))
41094111
XCTAssertEqual(nullableInteger, JSONSchema.integer(.init(format: .generic, nullable: true), .init(maximum: (1, exclusive:false))))
41104112
XCTAssertEqual(allowedValueInteger, JSONSchema.integer(.init(format: .generic, allowedValues: [1, 2]), .init(maximum: (2, exclusive:false))))
41114113
XCTAssertEqual(integerWithWholeNumberFloat, JSONSchema.integer(maximum: (1, exclusive: false)))
4114+
XCTAssertEqual(integerWithLargestPossibleMax, JSONSchema.integer(maximum: (9223372036854775807, exclusive: false)))
41124115
}
41134116

41144117
func test_encodeIntegerWithExclusiveMaximum() {
@@ -4151,18 +4154,21 @@ extension SchemaObjectTests {
41514154
])
41524155
}
41534156

4154-
func test_decodeIntegerWithExclusiveMaximum() {
4157+
func test_decodeIntegerWithExclusiveMaximum() throws {
41554158
let integerData = #"{"type": "integer", "maximum": 1, "exclusiveMaximum": true}"#.data(using: .utf8)!
41564159
let nullableIntegerData = #"{"type": "integer", "maximum": 1, "exclusiveMaximum": true, "nullable": true}"#.data(using: .utf8)!
41574160
let allowedValueIntegerData = #"{"type": "integer", "maximum": 5, "exclusiveMaximum": true, "enum": [2, 3]}"#.data(using: .utf8)!
4161+
let integerWithLargestPossibleMaxData = #"{"type": "integer", "exclusiveMaximum": true, "maximum": 9223372036854775807}"#.data(using: .utf8)!
41584162

4159-
let integer = try! orderUnstableDecode(JSONSchema.self, from: integerData)
4160-
let nullableInteger = try! orderUnstableDecode(JSONSchema.self, from: nullableIntegerData)
4161-
let allowedValueInteger = try! orderUnstableDecode(JSONSchema.self, from: allowedValueIntegerData)
4163+
let integer = try orderUnstableDecode(JSONSchema.self, from: integerData)
4164+
let nullableInteger = try orderUnstableDecode(JSONSchema.self, from: nullableIntegerData)
4165+
let allowedValueInteger = try orderUnstableDecode(JSONSchema.self, from: allowedValueIntegerData)
4166+
let integerWithLargestPossibleMax = try orderUnstableDecode(JSONSchema.self, from: integerWithLargestPossibleMaxData)
41624167

41634168
XCTAssertEqual(integer, JSONSchema.integer(.init(format: .generic), .init(maximum: (1, exclusive:true))))
41644169
XCTAssertEqual(nullableInteger, JSONSchema.integer(.init(format: .generic, nullable: true), .init(maximum: (1, exclusive:true))))
41654170
XCTAssertEqual(allowedValueInteger, JSONSchema.integer(.init(format: .generic, allowedValues: [2, 3]), .init(maximum: (5, exclusive:true))))
4171+
XCTAssertEqual(integerWithLargestPossibleMax, JSONSchema.integer(maximum: (9223372036854775807, exclusive: true)))
41664172
}
41674173

41684174
func test_encodeIntegerWithMinimum() {
@@ -4206,16 +4212,19 @@ extension SchemaObjectTests {
42064212
let nullableIntegerData = #"{"type": "integer", "minimum": 1, "nullable": true}"#.data(using: .utf8)!
42074213
let allowedValueIntegerData = #"{"type": "integer", "minimum": 1, "enum": [1, 2]}"#.data(using: .utf8)!
42084214
let integerWithWholeNumberFloatData = #"{"type": "integer", "minimum": 1.0}"#.data(using: .utf8)!
4215+
let integerWithSmallestPossibleMinData = #"{"type": "integer", "minimum": -9223372036854775808}"#.data(using: .utf8)!
42094216

42104217
let integer = try orderUnstableDecode(JSONSchema.self, from: integerData)
42114218
let nullableInteger = try orderUnstableDecode(JSONSchema.self, from: nullableIntegerData)
42124219
let allowedValueInteger = try orderUnstableDecode(JSONSchema.self, from: allowedValueIntegerData)
42134220
let integerWithWholeNumberFloat = try orderUnstableDecode(JSONSchema.self, from: integerWithWholeNumberFloatData)
4221+
let integerWithSmallestPossibleMin = try orderUnstableDecode(JSONSchema.self, from: integerWithSmallestPossibleMinData)
42144222

42154223
XCTAssertEqual(integer, JSONSchema.integer(.init(format: .generic), .init(minimum: (1, exclusive:false))))
42164224
XCTAssertEqual(nullableInteger, JSONSchema.integer(.init(format: .generic, nullable: true), .init(minimum: (1, exclusive:false))))
42174225
XCTAssertEqual(allowedValueInteger, JSONSchema.integer(.init(format: .generic, allowedValues: [1, 2]), .init(minimum: (1, exclusive:false))))
42184226
XCTAssertEqual(integerWithWholeNumberFloat, JSONSchema.integer(minimum: (1, exclusive: false)))
4227+
XCTAssertEqual(integerWithSmallestPossibleMin, JSONSchema.integer(minimum: (-9223372036854775808, exclusive: false)))
42194228
}
42204229

42214230
func test_encodeIntegerWithExclusiveMinimum() {
@@ -4258,18 +4267,21 @@ extension SchemaObjectTests {
42584267
])
42594268
}
42604269

4261-
func test_decodeIntegerWithExclusiveMinimum() {
4270+
func test_decodeIntegerWithExclusiveMinimum() throws {
42624271
let integerData = #"{"type": "integer", "minimum": 1, "exclusiveMinimum": true}"#.data(using: .utf8)!
42634272
let nullableIntegerData = #"{"type": "integer", "minimum": 1, "exclusiveMinimum": true, "nullable": true}"#.data(using: .utf8)!
42644273
let allowedValueIntegerData = #"{"type": "integer", "minimum": 1, "exclusiveMinimum": true, "enum": [2, 3]}"#.data(using: .utf8)!
4274+
let integerWithSmallestPossibleMinData = #"{"type": "integer", "exclusiveMinimum": true, "minimum": -9223372036854775808}"#.data(using: .utf8)!
42654275

4266-
let integer = try! orderUnstableDecode(JSONSchema.self, from: integerData)
4267-
let nullableInteger = try! orderUnstableDecode(JSONSchema.self, from: nullableIntegerData)
4268-
let allowedValueInteger = try! orderUnstableDecode(JSONSchema.self, from: allowedValueIntegerData)
4276+
let integer = try orderUnstableDecode(JSONSchema.self, from: integerData)
4277+
let nullableInteger = try orderUnstableDecode(JSONSchema.self, from: nullableIntegerData)
4278+
let allowedValueInteger = try orderUnstableDecode(JSONSchema.self, from: allowedValueIntegerData)
4279+
let integerWithSmallestPossibleMin = try orderUnstableDecode(JSONSchema.self, from: integerWithSmallestPossibleMinData)
42694280

42704281
XCTAssertEqual(integer, JSONSchema.integer(.init(format: .generic), .init(minimum: (1, exclusive:true))))
42714282
XCTAssertEqual(nullableInteger, JSONSchema.integer(.init(format: .generic, nullable: true), .init(minimum: (1, exclusive:true))))
42724283
XCTAssertEqual(allowedValueInteger, JSONSchema.integer(.init(format: .generic, allowedValues: [2, 3]), .init(minimum: (1, exclusive:true))))
4284+
XCTAssertEqual(integerWithSmallestPossibleMin, JSONSchema.integer(minimum: (-9223372036854775808, exclusive: true)))
42734285
}
42744286

42754287
func test_encodeString() {

Tests/OpenAPIKit30Tests/Schema Object/SchemaObjectYamsTests.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ final class SchemaObjectYamsTests: XCTestCase {
4040
"""
4141

4242
XCTAssertThrowsError(try YAMLDecoder().decode(JSONSchema.self, from: integerString)) { error in
43-
XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Inconsistency encountered when parsing `maximum`: Expected an Integer literal but found a floating point value.")
43+
XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Inconsistency encountered when parsing `maximum`: Expected an Integer literal but found a floating point value (10.2).")
4444
}
4545

4646
let integerString2 =
@@ -50,7 +50,7 @@ final class SchemaObjectYamsTests: XCTestCase {
5050
"""
5151

5252
XCTAssertThrowsError(try YAMLDecoder().decode(JSONSchema.self, from: integerString2)) { error in
53-
XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Inconsistency encountered when parsing `minimum`: Expected an Integer literal but found a floating point value.")
53+
XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Inconsistency encountered when parsing `minimum`: Expected an Integer literal but found a floating point value (1.1).")
5454
}
5555
}
5656
}

0 commit comments

Comments
 (0)