Skip to content

Commit

Permalink
Merge pull request #20 from mattpolzin/improve-parsing-errors
Browse files Browse the repository at this point in the history
Improve Parsing Errors
  • Loading branch information
mattpolzin authored Feb 29, 2020
2 parents 02bf3f4 + 769a8d6 commit 842f899
Show file tree
Hide file tree
Showing 38 changed files with 2,153 additions and 119 deletions.
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ let package = Package(
.testTarget(
name: "OpenAPIKitCompatibilitySuite",
dependencies: ["OpenAPIKit", "Yams"]),
.testTarget(
name: "OpenAPIKitErrorReportingTests",
dependencies: ["OpenAPIKit", "Yams"])
],
swiftLanguageVersions: [ .v5 ]
)
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ let decoder = ... // JSONDecoder() or YAMLDecoder()
let openAPIDoc = try decoder.decode(OpenAPI.Document, from: ...)
```

#### Decoding Errors
You can wrap any error you get back from a decoder in `OpenAPI.Error` to get a friendlier human-readable description from `localizedDescription`.

```swift
do {
try decoder.docode(OpenAPI.Document, from: ...)
} catch let error {
print(OpenAPI.Error(from: error).localizedDescription)
}
```

### Encoding OpenAPI Documents

You can encode a JSON OpenAPI document (i.e. using the `JSONEncoder` from the **Foundation** library) or a YAML OpenAPI document (i.e. using the `YAMLEncoder` from the [**Yams**](https://github.com/jpsim/Yams) library) with the following code:
Expand Down
7 changes: 5 additions & 2 deletions Sources/OpenAPIKit/CodableVendorExtendable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ protocol CodableVendorExtendable: VendorExtendable {
enum VendorExtensionDecodingError: Swift.Error {
case selfIsArrayNotDict
case foundNonStringKeys
case foundExtensionsWithoutXPrefix
}

extension CodableVendorExtendable {
Expand All @@ -71,7 +70,11 @@ extension CodableVendorExtendable {
}

guard extensions.keys.allSatisfy({ $0.lowercased().starts(with: "x-") }) else {
throw VendorExtensionDecodingError.foundExtensionsWithoutXPrefix
throw InconsistencyError(
subjectName: "Vendor Extension",
details: "Found a vendor extension property that does not begin with the required 'x-' prefix",
codingPath: decoder.codingPath
)
}

return extensions.mapValues(AnyCodable.init)
Expand Down
11 changes: 6 additions & 5 deletions Sources/OpenAPIKit/Content.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,15 @@ extension OpenAPI.Content: Decodable {
let container = try decoder.container(keyedBy: CodingKeys.self)

guard !(container.contains(.examples) && container.contains(.example)) else {
throw Error.foundBothExampleAndExamples
throw InconsistencyError(
subjectName: "Example and Examples",
details: "Only one of `example` and `examples` is allowed in the Media Type Object (`OpenAPI.Content`).",
codingPath: container.codingPath
)
}

schema = try container.decode(Either<JSONReference<OpenAPI.Components, JSONSchema>, JSONSchema>.self, forKey: .schema)

encoding = try container.decodeIfPresent(OrderedDictionary<String, Encoding>.self, forKey: .encoding)

if container.contains(.example) {
Expand All @@ -149,10 +154,6 @@ extension OpenAPI.Content: Decodable {

vendorExtensions = try Self.extensions(from: decoder)
}

public enum Error: Swift.Error {
case foundBothExampleAndExamples
}
}

extension OpenAPI.Content {
Expand Down
116 changes: 116 additions & 0 deletions Sources/OpenAPIKit/Decoding Errors/DecodingErrorExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//
// DecodingErrorExtensions.swift
//
//
// Created by Mathew Polzin on 2/26/20.
//

import Foundation

internal extension Swift.DecodingError {
var subjectName: String {
let name: String? = {
switch self {
case .keyNotFound(let key, _):
return "\(key.stringValue)"
case .typeMismatch(_, let ctx), .valueNotFound(_, let ctx), .dataCorrupted(let ctx):
return ctx.codingPath.last?.stringValue
@unknown default:
return nil
}
}()

return name ?? "[unknown object]"
}

var codingPathWithoutSubject: [CodingKey] {
switch self {
case .keyNotFound(_, let ctx):
return ctx.codingPath
case .typeMismatch(_, let ctx), .valueNotFound(_, let ctx), .dataCorrupted(let ctx):
return ctx.codingPath.count > 0 ? ctx.codingPath.dropLast() : []
@unknown default:
return []
}
}

var codingPath: [CodingKey] {
switch self {
case .keyNotFound(_, let ctx), .typeMismatch(_, let ctx), .valueNotFound(_, let ctx), .dataCorrupted(let ctx):
return ctx.codingPath
@unknown default:
return []
}
}

var relativeCodingPathString: String {
return codingPathWithoutSubject.stringValue
}

var errorCategory: ErrorCategory {
switch self {
case .typeMismatch(let type, _):
return .typeMismatch(expectedTypeName: String(describing: type))
case .valueNotFound:
return .missing(.value)
case .keyNotFound:
return .missing(.key)
case .dataCorrupted:
return .dataCorrupted(underlying: underlyingError)
@unknown default:
return .dataCorrupted(underlying: underlyingError)
}
}

var underlyingError: Swift.Error? {
switch self {
case .typeMismatch(_, let ctx), .valueNotFound(_, let ctx), .keyNotFound(_, let ctx), .dataCorrupted(let ctx):
return ctx.underlyingError
@unknown default:
return nil
}
}

func replacingPath(with codingPath: [CodingKey]) -> Self {
switch self {
case .typeMismatch(let type, let ctx):
return .typeMismatch(type, ctx.replacingPath(with: codingPath))
case .valueNotFound(let type, let ctx):
return .valueNotFound(type, ctx.replacingPath(with: codingPath))
case .keyNotFound(let key, let ctx):
return .keyNotFound(key, ctx.replacingPath(with: codingPath))
case .dataCorrupted(let ctx):
return .dataCorrupted(ctx.replacingPath(with: codingPath))
@unknown default:
return .dataCorrupted(.init(codingPath: codingPath, debugDescription: "unknown error"))
}
}
}

internal extension Swift.DecodingError.Context {
func replacingPath(with codingPath: [CodingKey]) -> Self {
return Swift.DecodingError.Context(
codingPath: codingPath,
debugDescription: debugDescription,
underlyingError: underlyingError
)
}
}

internal struct DecodingErrorWrapper: OpenAPIError {
let decodingError: Swift.DecodingError

var subjectName: String { decodingError.subjectName }

var contextString: String {
let relativeCodingPathString = decodingError.relativeCodingPathString

return relativeCodingPathString.isEmpty
? ""
: "in \(relativeCodingPathString)"
}

var errorCategory: ErrorCategory { decodingError.errorCategory }

var codingPath: [CodingKey] { decodingError.codingPath }
}
84 changes: 84 additions & 0 deletions Sources/OpenAPIKit/Decoding Errors/DocumentDecodingError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// File.swift
//
//
// Created by Mathew Polzin on 2/23/20.
//

import Foundation

extension OpenAPI.Error.Decoding {
public struct Document: OpenAPIError {
public let context: Context
public let codingPath: [CodingKey]

public enum Context {
case path(Path)
case inconsistency(InconsistencyError)
case generic(Swift.DecodingError)
}
}
}

extension OpenAPI.Error.Decoding.Document {
public var subjectName: String {
switch context {
case .path(let pathError):
return pathError.subjectName

case .generic(let decodingError):
return decodingError.subjectName

case .inconsistency(let error):
return error.subjectName
}
}

public var contextString: String {
switch context {
case .path(let pathError):
return pathError.contextString
case .generic, .inconsistency:
return relativeCodingPathString.isEmpty
? "in the root Document object"
: "in Document\(relativeCodingPathString)"
}
}

public var errorCategory: ErrorCategory {
switch context {
case .path(let pathError):
return pathError.errorCategory
case .generic(let error):
return error.errorCategory
case .inconsistency(let error):
return .inconsistency(details: error.details)
}
}

internal var relativeCodingPathString: String {
switch context {
case .generic(let decodingError):
return decodingError.relativeCodingPathString
case .inconsistency:
return ""
case .path(let pathError):
return pathError.relativeCodingPathString
}
}

internal init(_ error: DecodingError) {
context = .generic(error)
codingPath = error.codingPath
}

internal init(_ error: InconsistencyError) {
context = .inconsistency(error)
codingPath = error.codingPath
}

internal init(_ error: OpenAPI.Error.Decoding.Path) {
context = .path(error)
codingPath = error.codingPath
}
}
19 changes: 19 additions & 0 deletions Sources/OpenAPIKit/Decoding Errors/InconsistencyError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// InconsistencyError.swift
//
//
// Created by Mathew Polzin on 2/26/20.
//

import Foundation

public struct InconsistencyError: Swift.Error, OpenAPIError {
public let subjectName: String
public let details: String
public let codingPath: [CodingKey]

public var contextString: String { "" }
public var errorCategory: ErrorCategory { .inconsistency(details: details) }

public var localizedDescription: String { details }
}
Loading

0 comments on commit 842f899

Please sign in to comment.