Skip to content

Commit

Permalink
Add specialized decoding error type for request bodies. tweak the way…
Browse files Browse the repository at this point in the history
… context string is used.
  • Loading branch information
mattpolzin committed Feb 29, 2020
1 parent 126a793 commit 769a8d6
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 16 deletions.
22 changes: 20 additions & 2 deletions Sources/OpenAPIKit/Decoding Errors/OperationDecodingError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ extension OpenAPI.Error.Decoding {
internal let relativeCodingPath: [CodingKey]

public enum Context {
case request(Request)
case response(Response)
case inconsistency(InconsistencyError)
case generic(Swift.DecodingError)
Expand All @@ -26,6 +27,8 @@ extension OpenAPI.Error.Decoding {
extension OpenAPI.Error.Decoding.Operation {
public var subjectName: String {
switch context {
case .request(let error):
return error.subjectName
case .response(let error):
return error.subjectName
case .inconsistency(let error):
Expand All @@ -37,10 +40,12 @@ extension OpenAPI.Error.Decoding.Operation {
}
}

public var contextString: String { endpoint.rawValue }
public var contextString: String { "" }

public var errorCategory: ErrorCategory {
switch context {
case .request(let error):
return error.errorCategory
case .response(let error):
return error.errorCategory
case .inconsistency(let error):
Expand All @@ -54,6 +59,8 @@ extension OpenAPI.Error.Decoding.Operation {

public var codingPath: [CodingKey] {
switch context {
case .request(let error):
return error.codingPath
case .response(let error):
return error.codingPath
case .inconsistency(let error):
Expand All @@ -69,6 +76,15 @@ extension OpenAPI.Error.Decoding.Operation {
relativeCodingPath.stringValue
}

internal init(_ error: OpenAPI.Error.Decoding.Request) {
var codingPath = error.codingPath.dropFirst(2)
let verb = OpenAPI.HttpVerb(rawValue: codingPath.removeFirst().stringValue.uppercased())!

endpoint = verb
context = .request(error)
relativeCodingPath = Array(codingPath)
}

internal init(_ error: OpenAPI.Error.Decoding.Response) {
var codingPath = error.codingPath.dropFirst(2)
let verb = OpenAPI.HttpVerb(rawValue: codingPath.removeFirst().stringValue.uppercased())!
Expand Down Expand Up @@ -99,6 +115,8 @@ extension OpenAPI.Error.Decoding.Operation {
internal init(unwrapping error: Swift.DecodingError) {
if let decodingError = error.underlyingError as? Swift.DecodingError {
self = Self(unwrapping: decodingError)
} else if let responseError = error.underlyingError as? OpenAPI.Error.Decoding.Request {
self = Self(responseError)
} else if let responseError = error.underlyingError as? OpenAPI.Error.Decoding.Response {
self = Self(responseError)
} else if let inconsistencyError = error.underlyingError as? InconsistencyError {
Expand All @@ -115,7 +133,7 @@ extension OpenAPI.Error.Decoding.Operation {
if polyError.individualTypeFailures[0].typeString == "$ref" && polyError.individualTypeFailures[1].codingPath(relativeTo: polyError.codingPath).count > 1 {
self = Self(unwrapping: polyError.individualTypeFailures[1].error)
return
} else if polyError.individualTypeFailures[1].typeString == "$ref" && polyError.individualTypeFailures[0].codingPath(relativeTo: polyError.codingPath).count > 1 {
} else if polyError.individualTypeFailures[1].typeString == "$ref" && polyError.individualTypeFailures[0].codingPath(relativeTo: polyError.codingPath).count > 0 {
self = Self(unwrapping: polyError.individualTypeFailures[0].error)
return
}
Expand Down
12 changes: 8 additions & 4 deletions Sources/OpenAPIKit/Decoding Errors/PathDecodingError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ extension OpenAPI.Error.Decoding.Path {
case .endpoint(let endpointError):
switch endpointError.context {
case .response(let responseError):
let responseContext = responseError.contextString == "default"
let responseContext = responseError.statusCode.rawValue == "default"
? "default"
: "status code '\(responseError.contextString)'"
return "\(relativeCodingPath)for the \(responseContext) response of the **\(endpointError.contextString)** endpoint under `\(path.rawValue)`"
: "status code '\(responseError.statusCode.rawValue)'"
return "\(relativeCodingPath)for the \(responseContext) response of the **\(endpointError.endpoint.rawValue)** endpoint under `\(path.rawValue)`"
case .request:
return "\(relativeCodingPath)for the request body of the **\(endpointError.endpoint.rawValue)** endpoint under `\(path.rawValue)`"
case .generic, .inconsistency, .neither:
return "\(relativeCodingPath)for the **\(endpointError.contextString)** endpoint under `\(path.rawValue)`"
return "\(relativeCodingPath)for the **\(endpointError.endpoint.rawValue)** endpoint under `\(path.rawValue)`"
}
case .generic, .neither:
return "\(relativeCodingPath)under the `\(path.rawValue)` path"
Expand Down Expand Up @@ -82,6 +84,8 @@ extension OpenAPI.Error.Decoding.Path {
switch endpointError.context {
case .response(let responseError):
return responseError.relativeCodingPathString
case .request(let requestError):
return requestError.relativeCodingPathString
case .generic, .inconsistency, .neither:
return endpointError.relativeCodingPathString
}
Expand Down
107 changes: 107 additions & 0 deletions Sources/OpenAPIKit/Decoding Errors/RequestDecodingError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//
// RequestDecodingError.swift
//
//
// Created by Mathew Polzin on 2/28/20.
//

import Foundation
import Poly

extension OpenAPI.Error.Decoding {
public struct Request: OpenAPIError {
public let context: Context
internal let relativeCodingPath: [CodingKey]

public enum Context {
case inconsistency(InconsistencyError)
case generic(Swift.DecodingError)
case neither(PolyDecodeNoTypesMatchedError)
}
}
}

extension OpenAPI.Error.Decoding.Request {
public var subjectName: String {
switch context {
case .inconsistency(let error):
return error.subjectName
case .generic(let decodingError):
return decodingError.subjectName
case .neither(let polyError):
return polyError.subjectName
}
}

public var contextString: String { "" }

public var errorCategory: ErrorCategory {
switch context {
case .inconsistency(let error):
return .inconsistency(details: error.details)
case .generic(let decodingError):
return decodingError.errorCategory
case .neither(let polyError):
return polyError.errorCategory
}
}

public var codingPath: [CodingKey] {
switch context {
case .inconsistency(let error):
return error.codingPath
case .generic(let decodingError):
return decodingError.codingPath
case .neither(let polyError):
return polyError.codingPath
}
}

internal var relativeCodingPathString: String {
relativeCodingPath.stringValue
}

internal static func relativePath(from path: [CodingKey]) -> [CodingKey] {
guard let responsesIdx = path.firstIndex(where: { $0.stringValue == "requestBody" }) else {
return path
}
return Array(path.dropFirst(responsesIdx.advanced(by: 1)))
}

internal init(_ error: InconsistencyError) {
context = .inconsistency(error)
relativeCodingPath = Self.relativePath(from: error.codingPath)
}

internal init(_ error: Swift.DecodingError) {
context = .generic(error)
relativeCodingPath = Self.relativePath(from: error.codingPathWithoutSubject)
}

internal init(unwrapping error: Swift.DecodingError) {
if let decodingError = error.underlyingError as? Swift.DecodingError {
self = Self(unwrapping: decodingError)
} else if let inconsistencyError = error.underlyingError as? InconsistencyError {
self = Self(inconsistencyError)
} else if let polyError = error.underlyingError as? PolyDecodeNoTypesMatchedError {
self = Self(polyError)
} else {
self = Self(error)
}
}

internal init(_ polyError: PolyDecodeNoTypesMatchedError) {
if polyError.individualTypeFailures.count == 2 {
if polyError.individualTypeFailures[0].typeString == "$ref" && polyError.individualTypeFailures[1].codingPath(relativeTo: polyError.codingPath).count > 1 {
self = Self(unwrapping: polyError.individualTypeFailures[1].error)
return
} else if polyError.individualTypeFailures[1].typeString == "$ref" && polyError.individualTypeFailures[0].codingPath(relativeTo: polyError.codingPath).count > 1 {
self = Self(unwrapping: polyError.individualTypeFailures[0].error)
return
}
}
context = .neither(polyError)
relativeCodingPath = Self.relativePath(from: polyError.codingPath)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ extension OpenAPI.Error.Decoding.Response {
}

internal init(_ error: Swift.DecodingError) {
var codingPath = Self.relativePath(from: error.codingPath)
var codingPath = Self.relativePath(from: error.codingPathWithoutSubject)
let code = codingPath.removeFirst().stringValue.lowercased()

statusCode = OpenAPI.Response.StatusCode(rawValue: code)!
Expand Down
2 changes: 2 additions & 0 deletions Sources/OpenAPIKit/OpenAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ extension OpenAPI {
public let codingPath: [CodingKey]
public let underlyingError: Swift.Error

public var codingPathString: String { codingPath.stringValue }

public init(from underlyingError: Swift.Error) {
self.underlyingError = underlyingError
if let openAPIError = underlyingError as? OpenAPIError {
Expand Down
3 changes: 3 additions & 0 deletions Sources/OpenAPIKit/Path Item/Operation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ extension OpenAPI.PathItem.Operation: Decodable {
security = try decodeSecurityRequirements(from: container, forKey: .security, given: nil)

servers = try container.decodeIfPresent([OpenAPI.Server].self, forKey: .servers)
} catch let error as OpenAPI.Error.Decoding.Request {

throw OpenAPI.Error.Decoding.Operation(error)
} catch let error as OpenAPI.Error.Decoding.Response {

throw OpenAPI.Error.Decoding.Operation(error)
Expand Down
18 changes: 15 additions & 3 deletions Sources/OpenAPIKit/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import Poly

extension OpenAPI {
public struct Request: Equatable {
Expand Down Expand Up @@ -49,10 +50,21 @@ extension OpenAPI.Request: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

description = try container.decodeIfPresent(String.self, forKey: .description)
do {
description = try container.decodeIfPresent(String.self, forKey: .description)

content = try container.decode(OpenAPI.Content.Map.self, forKey: .content)
content = try container.decode(OpenAPI.Content.Map.self, forKey: .content)

required = try container.decodeIfPresent(Bool.self, forKey: .required) ?? false
required = try container.decodeIfPresent(Bool.self, forKey: .required) ?? false
} catch let error as InconsistencyError {

throw OpenAPI.Error.Decoding.Request(error)
} catch let error as Swift.DecodingError {

throw OpenAPI.Error.Decoding.Request(error)
} catch let error as PolyDecodeNoTypesMatchedError {

throw OpenAPI.Error.Decoding.Request(error)
}
}
}
3 changes: 3 additions & 0 deletions Sources/OpenAPIKit/Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,13 @@ extension OpenAPI.Response: Decodable {
content = try container.decodeIfPresent(OpenAPI.Content.Map.self, forKey: .content) ?? [:]

} catch let error as InconsistencyError {

throw OpenAPI.Error.Decoding.Response(error)
} catch let error as PolyDecodeNoTypesMatchedError {

throw OpenAPI.Error.Decoding.Response(error)
} catch let error as DecodingError {

throw OpenAPI.Error.Decoding.Response(error)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ paths:

let openAPIError = OpenAPI.Error(from: error)

XCTAssertEqual(openAPIError.localizedDescription, "Expected to find `schema` key in .requestBody.content['application/json'] for the **GET** endpoint under `/hello/world` but it is missing.")
XCTAssertEqual(openAPIError.localizedDescription, "Expected to find `schema` key in .content['application/json'] for the request body of the **GET** endpoint under `/hello/world` but it is missing.")
XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [
"paths",
"/hello/world",
Expand All @@ -80,6 +80,7 @@ paths:
"content",
"application/json"
])
XCTAssertEqual(openAPIError.codingPathString, ".paths['/hello/world'].get.requestBody.content['application/json']")
}
}

Expand All @@ -103,7 +104,7 @@ paths:

let openAPIError = OpenAPI.Error(from: error)

XCTAssertEqual(openAPIError.localizedDescription, "Expected `application/json` value in .requestBody.content for the **GET** endpoint under `/hello/world` to be parsable as Mapping but it was not.")
XCTAssertEqual(openAPIError.localizedDescription, "Expected `application/json` value in .content for the request body of the **GET** endpoint under `/hello/world` to be parsable as Mapping but it was not.")
XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [
"paths",
"/hello/world",
Expand Down Expand Up @@ -139,7 +140,7 @@ paths:

let openAPIError = OpenAPI.Error(from: error)

XCTAssertEqual(openAPIError.localizedDescription, "Inconsistency encountered when parsing `Vendor Extension` in .requestBody.content['application/json'] for the **GET** endpoint under `/hello/world`: Found a vendor extension property that does not begin with the required 'x-' prefix.")
XCTAssertEqual(openAPIError.localizedDescription, "Inconsistency encountered when parsing `Vendor Extension` in .content['application/json'] for the request body of the **GET** endpoint under `/hello/world`: Found a vendor extension property that does not begin with the required 'x-' prefix.")
XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [
"paths",
"/hello/world",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ paths:

let openAPIError = OpenAPI.Error(from: error)

XCTAssertEqual(openAPIError.localizedDescription, "Found neither a $ref nor a JSONSchema in .requestBody.content['application/json'].schema for the **GET** endpoint under `/hello/world`. \n\nJSONSchema could not be decoded because:\nExpected `type` value to be parsable as Scalar but it was not..")
XCTAssertEqual(openAPIError.localizedDescription, "Found neither a $ref nor a JSONSchema in .content['application/json'].schema for the request body of the **GET** endpoint under `/hello/world`. \n\nJSONSchema could not be decoded because:\nExpected `type` value to be parsable as Scalar but it was not..")
XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [
"paths",
"/hello/world",
Expand Down
2 changes: 0 additions & 2 deletions Tests/OpenAPIKitErrorReportingTests/ResponseErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ paths:
type: string
"""



XCTAssertThrowsError(try testDecoder.decode(OpenAPI.Document.self, from: documentYML)) { error in

let openAPIError = OpenAPI.Error(from: error)
Expand Down

0 comments on commit 769a8d6

Please sign in to comment.