Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Optional Error Context #46

Merged
merged 2 commits into from
Dec 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
--indentcase false
--trimwhitespace always
--ranges nospace
--empty tuple
--operatorfunc nospace
--ifdef noindent
--stripunusedargs closure-only
58 changes: 39 additions & 19 deletions Sources/XMLCoder/Auxiliaries/XMLStackParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,58 @@ class _XMLStackParser: NSObject {
var root: _XMLElement?
private var stack: [_XMLElement] = []

static func parse(with data: Data) throws -> KeyedBox {
static func parse(with data: Data, errorContextLength length: UInt) throws -> KeyedBox {
let parser = _XMLStackParser()

guard let node = try parser.parse(with: data) else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: [],
debugDescription: "The given data could not be parsed into XML."
))
}
let node = try parser.parse(with: data, errorContextLength: length)

return node.flatten()
}

func parse(with data: Data) throws -> _XMLElement? {
func parse(with data: Data, errorContextLength: UInt) throws -> _XMLElement {
let xmlParser = XMLParser(data: data)
xmlParser.delegate = self

guard xmlParser.parse() else {
if let error = xmlParser.parserError {
throw error
}
return nil
guard !xmlParser.parse(), root == nil else {
return root!
}

guard let error = xmlParser.parserError else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: [],
debugDescription: "The given data could not be parsed into XML."
))
}

guard errorContextLength > 0 else {
throw error
}

return root
let string = String(data: data, encoding: .utf8) ?? ""
let lines = string.split(separator: "\n")
var errorPosition = 0
let offset = Int(errorContextLength / 2)
for i in 0..<xmlParser.lineNumber - 1 {
errorPosition += lines[i].count
}
errorPosition += xmlParser.columnNumber
let lowerBound = String.Index(encodedOffset: errorPosition - offset)
let upperBound = String.Index(encodedOffset: errorPosition + offset)

let context = string[lowerBound..<upperBound]

throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: [],
debugDescription: """
\(error.localizedDescription) \
at line \(xmlParser.lineNumber), column \(xmlParser.columnNumber):
`\(context)`
""",
underlyingError: error
))
}

func withCurrentElement(_ body: (inout _XMLElement) throws -> Void) rethrows {
func withCurrentElement(_ body: (inout _XMLElement) throws -> ()) rethrows {
guard !stack.isEmpty else {
return
}
Expand Down Expand Up @@ -92,8 +116,4 @@ extension _XMLStackParser: XMLParserDelegate {
currentElement.append(value: string)
}
}

func parser(_: XMLParser, parseErrorOccurred parseError: Error) {
print(parseError)
}
}
2 changes: 1 addition & 1 deletion Sources/XMLCoder/Box/NullBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extension NullBox: Box {
extension NullBox: SimpleBox {}

extension NullBox: Equatable {
static func == (_: NullBox, _: NullBox) -> Bool {
static func ==(_: NullBox, _: NullBox) -> Bool {
return true
}
}
Expand Down
19 changes: 10 additions & 9 deletions Sources/XMLCoder/Decoder/XMLDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ open class XMLDecoder {
/// Contextual user-provided information for use during decoding.
open var userInfo: [CodingUserInfoKey: Any] = [:]

// The error context length
open var errorContextLength: UInt = 0

/// Options set on the top-level encoder to pass down the decoding hierarchy.
struct _Options {
let dateDecodingStrategy: DateDecodingStrategy
Expand Down Expand Up @@ -247,15 +250,10 @@ open class XMLDecoder {
/// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid XML.
/// - throws: An error if any box throws an error during decoding.
open func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
let topLevel: Box
do {
topLevel = try _XMLStackParser.parse(with: data)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: [],
debugDescription: "The given data was not valid XML.", underlyingError: error
))
}
let topLevel: Box = try _XMLStackParser.parse(
with: data,
errorContextLength: errorContextLength
)

let decoder = _XMLDecoder(referencing: topLevel, options: options)

Expand Down Expand Up @@ -289,6 +287,9 @@ class _XMLDecoder: Decoder {
return options.userInfo
}

// The error context lenght
open var errorContextLenght: UInt = 0

// MARK: - Initialization

/// Initializes `self` with the given top-level container and options.
Expand Down
4 changes: 2 additions & 2 deletions Sources/XMLCoder/Encoder/XMLEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ open class XMLEncoder {
/// Encode the `Date` as a custom value encoded by the given closure.
///
/// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
case custom((Date, Encoder) throws -> Void)
case custom((Date, Encoder) throws -> ())
}

/// The strategy to use for encoding `String` values.
Expand All @@ -86,7 +86,7 @@ open class XMLEncoder {
/// Encode the `Data` as a custom value encoded by the given closure.
///
/// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
case custom((Data, Encoder) throws -> Void)
case custom((Data, Encoder) throws -> ())
}

/// The strategy to use for non-XML-conforming floating-point values (IEEE 754 infinity and NaN).
Expand Down
6 changes: 4 additions & 2 deletions Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class XMLStackParserTests: XCTestCase {
let xmlString = "<container><value>42</value></container>"
let xmlData = xmlString.data(using: .utf8)!

let root: _XMLElement? = try parser.parse(with: xmlData)
let root: _XMLElement? = try parser.parse(with: xmlData,
errorContextLength: 0)

let expected = _XMLElement(
key: "container",
Expand All @@ -37,6 +38,7 @@ class XMLStackParserTests: XCTestCase {
let xmlString = "lorem ipsum"
let xmlData = xmlString.data(using: .utf8)!

XCTAssertThrowsError(try parser.parse(with: xmlData))
XCTAssertThrowsError(try parser.parse(with: xmlData,
errorContextLength: 0))
}
}
52 changes: 52 additions & 0 deletions Tests/XMLCoderTests/ErrorContextTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// ErrorContextTest.swift
// XMLCoder
//
// Created by Matvii Hodovaniuk on 12/27/18.
//

import Foundation
import XCTest
@testable import XMLCoder

final class ErrorContextTest: XCTestCase {
struct Container: Codable {
let value: [String: Int]
}

func testErrorContext() {
let decoder = XMLDecoder()
decoder.errorContextLength = 8

let xmlString =
"""
<container>
test1
</blah>
<container>
test2
</container>
"""
let xmlData = xmlString.data(using: .utf8)!

XCTAssertThrowsError(try decoder.decode(Container.self,
from: xmlData)) { error in
guard case let DecodingError.dataCorrupted(ctx) = error,
let underlying = ctx.underlyingError else {
XCTAssert(false, "wrong error type thrown")
return
}

XCTAssertEqual(ctx.debugDescription, """
\(underlying.localizedDescription) \
at line 3, column 8:
`blah>
<c`
""")
}
}

static var allTests = [
("testErrorContext", testErrorContext),
]
}
4 changes: 4 additions & 0 deletions XMLCoder.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
BF9457F521CBB6BC005ACFDE /* DecimalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9457EA21CBB6BC005ACFDE /* DecimalTests.swift */; };
BF9457F621CBB6BC005ACFDE /* KeyedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9457EB21CBB6BC005ACFDE /* KeyedTests.swift */; };
BF9457F721CBB6BC005ACFDE /* DataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9457EC21CBB6BC005ACFDE /* DataTests.swift */; };
D1E0C85321D8E65E0042A261 /* ErrorContextTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E0C85121D8E6540042A261 /* ErrorContextTest.swift */; };
D1FC040521C7EF8200065B43 /* RJISample.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FC040421C7EF8200065B43 /* RJISample.swift */; };
OBJ_48 /* DecodingErrorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* DecodingErrorExtension.swift */; };
OBJ_49 /* XMLDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* XMLDecoder.swift */; };
Expand Down Expand Up @@ -161,6 +162,7 @@
BF9457EA21CBB6BC005ACFDE /* DecimalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecimalTests.swift; sourceTree = "<group>"; };
BF9457EB21CBB6BC005ACFDE /* KeyedTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyedTests.swift; sourceTree = "<group>"; };
BF9457EC21CBB6BC005ACFDE /* DataTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataTests.swift; sourceTree = "<group>"; };
D1E0C85121D8E6540042A261 /* ErrorContextTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorContextTest.swift; sourceTree = "<group>"; };
D1FC040421C7EF8200065B43 /* RJISample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RJISample.swift; sourceTree = "<group>"; };
OBJ_10 /* DecodingErrorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingErrorExtension.swift; sourceTree = "<group>"; };
OBJ_11 /* XMLDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLDecoder.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -322,6 +324,7 @@
OBJ_29 /* BreakfastTest.swift */,
OBJ_30 /* CDCatalog.swift */,
OBJ_31 /* CDTest.swift */,
D1E0C85121D8E6540042A261 /* ErrorContextTest.swift */,
OBJ_33 /* NodeEncodingStrategyTests.swift */,
OBJ_34 /* NoteTest.swift */,
OBJ_35 /* PlantCatalog.swift */,
Expand Down Expand Up @@ -551,6 +554,7 @@
BF9457CD21CBB516005ACFDE /* FloatBoxTests.swift in Sources */,
BF9457F621CBB6BC005ACFDE /* KeyedTests.swift in Sources */,
BF9457C821CBB516005ACFDE /* BoolBoxTests.swift in Sources */,
D1E0C85321D8E65E0042A261 /* ErrorContextTest.swift in Sources */,
BF9457F421CBB6BC005ACFDE /* UIntTests.swift in Sources */,
OBJ_89 /* RJITest.swift in Sources */,
BF9457F121CBB6BC005ACFDE /* FloatTests.swift in Sources */,
Expand Down