Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/swiftlint_analyze.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ jobs:
run: xcodebuild -scheme Yams -project Yams.xcodeproj clean build-for-testing > xcodebuild.log
shell: bash
- name: Install SwiftLint
run: HOMEBREW_NO_AUTO_UPDATE=1 brew install https://raw.github.com/Homebrew/homebrew-core/master/Formula/swiftlint.rb
run: brew install swiftlint
- name: Run SwiftLint Analyze
run: swiftlint analyze --strict --compiler-log-path xcodebuild.log --reporter github-actions-logging
4 changes: 2 additions & 2 deletions .github/workflows/xcodebuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ jobs:
- version: '11.1'
flags_for_test: -parallel-testing-enabled NO
- version: '11.2'
flags_for_test: -parallel-testing-enabled NO -enableCodeCoverage YES
flags_for_test: -parallel-testing-enabled NO
- version: '11.3'
flags_for_test: -parallel-testing-enabled NO -enableCodeCoverage YES
flags_for_test: -parallel-testing-enabled NO
- version: '11.4'
flags_for_test: -parallel-testing-enabled NO -enableCodeCoverage YES
xcode_flags: ['-scheme Yams -project Yams.xcodeproj']
Expand Down
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@

##### Enhancements

* None.
* `YAMLDecoder` now conforms to the `TopLevelDecoder` protocol when
Apple's Combine framework is available.
[JP Simard](https://github.com/jpsim)
[#261](https://github.com/jpsim/Yams/issues/261)

* Add `YAMLDecoder.decode(...)` overload tha takes a YAML string encoded
as `Data` using UTF8 or UTF16.
[JP Simard](https://github.com/jpsim)

##### Bug Fixes

Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ and a third one for a [Yams-native](#yamsnode) representation.
- **Encoding: `YAMLEncoder.encode(_:)`**
Produces a YAML `String` from an instance of type conforming to `Encodable`.
- **Decoding: `YAMLDecoder.decode(_:from:)`**
Decodes an instance of type conforming to `Decodable` from YAML `String`.
Decodes an instance of type conforming to `Decodable` from YAML `String` or
`Data`.

```swift
import Foundation
Expand Down Expand Up @@ -160,6 +161,25 @@ let node = try Yams.compose(yaml: yaml)
map == node
```

#### Integrating with [Combine](https://developer.apple.com/documentation/combine)

When Apple's Combine framework is available, `YAMLDecoder` conforms to the
`TopLevelDecoder` protocol, which allows it to be used with the
`decode(type:decoder:)` operator:

```swift
import Combine
import Foundation
import Yams

func fetchBook(from url: URL) -> AnyPublisher<Book, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Book.self, decoder: YAMLDecoder())
.eraseToAnyPublisher()
}
```

## License

Both Yams and libYAML are MIT licensed.
Expand Down
35 changes: 34 additions & 1 deletion Sources/Yams/Decoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class YAMLDecoder {
///
/// - returns: Returns the decoded type `T`.
///
/// - throws: `DecodingError` if something went wrong while decoding.
/// - throws: `DecodingError` or `YamlError` if something went wrong while decoding.
public func decode<T>(_ type: T.Type = T.self,
from yaml: String,
userInfo: [CodingUserInfoKey: Any] = [:]) throws -> T where T: Swift.Decodable {
Expand All @@ -44,6 +44,25 @@ public class YAMLDecoder {
}
}

/// Decode a `Decodable` type from a given `Data` and optional user info mapping.
///
/// - parameter type: `Decodable` type to decode.
/// - parameter yaml: YAML data to decode.
/// - parameter userInfo: Additional key/values which can be used when looking up keys to decode.
///
/// - returns: Returns the decoded type `T`.
///
/// - throws: `DecodingError` or `YamlError` if something went wrong while decoding.
public func decode<T>(_ type: T.Type = T.self,
from yamlData: Data,
userInfo: [CodingUserInfoKey: Any] = [:]) throws -> T where T: Swift.Decodable {
guard let yamlString = String(data: yamlData, encoding: encoding.swiftStringEncoding) else {
throw YamlError.dataCouldNotBeDecoded(encoding: encoding.swiftStringEncoding)
}

return try decode(type, from: yamlString, userInfo: userInfo)
}

/// Encoding
public var encoding: Parser.Encoding
}
Expand Down Expand Up @@ -324,3 +343,17 @@ extension URL: ScalarConstructible {
return URL(string: scalar.string)
}
}

// MARK: TopLevelDecoder

#if canImport(Combine)
import protocol Combine.TopLevelDecoder

extension YAMLDecoder: TopLevelDecoder {
public typealias Input = Data

public func decode<T>(_ type: T.Type, from: Data) throws -> T where T: Decodable {
try decode(type, from: from, userInfo: [:])
}
}
#endif
36 changes: 35 additions & 1 deletion Sources/Yams/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,21 @@ public final class Parser {
}
return key.utf8.withContiguousStorageIfAvailable({ _ in true }) != nil ? .utf8 : .utf16
}()

/// The equivalent `Swift.Encoding` value for `self`.
internal var swiftStringEncoding: String.Encoding {
switch self {
case .utf8:
return .utf8
case .utf16:
return .utf16
}
}
}
/// Encoding
public let encoding: Encoding

/// Set up Parser.
/// Set up a `Parser` with a `String` value as input.
///
/// - parameter string: YAML string.
/// - parameter resolver: Resolver, `.default` if omitted.
Expand Down Expand Up @@ -182,6 +192,30 @@ public final class Parser {
}
}

/// Set up a `Parser` with a `Data` value as input.
///
/// - parameter string: YAML Data encoded using the `encoding` encoding.
/// - parameter resolver: Resolver, `.default` if omitted.
/// - parameter constructor: Constructor, `.default` if omitted.
/// - parameter encoding: Encoding, `.default` if omitted.
///
/// - throws: `YamlError`.
public convenience init(yaml data: Data,
resolver: Resolver = .default,
constructor: Constructor = .default,
encoding: Encoding = .default) throws {
guard let yamlString = String(data: data, encoding: encoding.swiftStringEncoding) else {
throw YamlError.dataCouldNotBeDecoded(encoding: encoding.swiftStringEncoding)
}

try self.init(
yaml: yamlString,
resolver: resolver,
constructor: constructor,
encoding: encoding
)
}

deinit {
yaml_parser_delete(&parser)
}
Expand Down
7 changes: 7 additions & 0 deletions Sources/Yams/YamlError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public enum YamlError: Error {
/// - parameter problem: Error description.
case representer(problem: String)

/// String data could not be decoded with the specified encoding.
///
/// - parameter encoding: The string encoding used to decode the string data.
case dataCouldNotBeDecoded(encoding: String.Encoding)

/// The error context.
public struct Context: CustomStringConvertible {
/// Context text.
Expand Down Expand Up @@ -167,6 +172,8 @@ extension YamlError: CustomStringConvertible {
return "\(mark): error: composer: \(context?.description ?? "")\(problem):\n" + mark.snippet(from: yaml)
case let .writer(problem), let .emitter(problem), let .representer(problem):
return problem
case .dataCouldNotBeDecoded(encoding: let encoding):
return "String could not be decoded from data using '\(encoding)' encoding"
}
}
}
1 change: 1 addition & 0 deletions Tests/YamsTests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ add_library(YamsTests
SpecTests.swift
StringTests.swift
TestHelper.swift
TopLevelDecoderTests.swift
YamlErrorTests.swift)
set_target_properties(YamsTests PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
Expand Down
26 changes: 13 additions & 13 deletions Tests/YamsTests/EncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,26 +284,26 @@ class EncoderTests: XCTestCase { // swiftlint:disable:this type_body_length

expectEqual(type(of: decoded), Employee.self, "Expected decoded value to be of type Employee; got \(type(of: decoded)) instead.")
}

func testDecodingAnchors() throws {
struct AnchorSample: Decodable {
struct Host: Decodable {
let here: Bool
}

let host: Host
}

let yaml = """
z: &anchor
here: true
host:
<<: *anchor
"""

let decoder = YAMLDecoder()
let decoded = try decoder.decode(AnchorSample.self, from: yaml)

XCTAssertTrue(decoded.host.here)
}

Expand Down Expand Up @@ -351,20 +351,20 @@ class EncoderTests: XCTestCase { // swiftlint:disable:this type_body_length

if let expectedYAML = yamlString {
XCTAssertEqual(producedYAML, expectedYAML, "Produced YAML not identical to expected YAML.",
file: file, line: line)
file: (file), line: line)
}

let decoder = YAMLDecoder()
let decoded = try decoder.decode(T.self, from: producedYAML)
XCTAssertEqual(decoded, value, "\(T.self) did not round-trip to an equal value.",
file: file, line: line)
file: (file), line: line)

} catch let error as EncodingError {
XCTFail("Failed to encode \(T.self) from YAML by error: \(error)", file: file, line: line)
XCTFail("Failed to encode \(T.self) from YAML by error: \(error)", file: (file), line: line)
} catch let error as DecodingError {
XCTFail("Failed to decode \(T.self) from YAML by error: \(error)", file: file, line: line)
XCTFail("Failed to decode \(T.self) from YAML by error: \(error)", file: (file), line: line)
} catch {
XCTFail("Rout trip test of \(T.self) failed with error: \(error)", file: file, line: line)
XCTFail("Rout trip test of \(T.self) failed with error: \(error)", file: (file), line: line)
}
}
}
Expand All @@ -375,21 +375,21 @@ public func expectEqual<T: Equatable>(
_ message: @autoclosure () -> String = "",
file: StaticString = #file, line: UInt = #line
) {
XCTAssertEqual(expected, actual, message(), file: file, line: line)
XCTAssertEqual(expected, actual, message(), file: (file), line: line)
}

public func expectEqual(
_ expected: Any.Type, _ actual: Any.Type,
_ message: @autoclosure () -> String = "",
file: StaticString = #file, line: UInt = #line
) {
XCTAssertTrue(expected == actual, message(), file: file, line: line)
XCTAssertTrue(expected == actual, message(), file: (file), line: line)
}

public func expectUnreachable(
_ message: @autoclosure () -> String = "",
file: StaticString = #file, line: UInt = #line) {
XCTFail("this code should not be executed: \(message())", file: file, line: line)
XCTFail("this code should not be executed: \(message())", file: (file), line: line)
}

private func expectEqualPaths(
Expand Down
10 changes: 5 additions & 5 deletions Tests/YamsTests/TestHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,26 +68,26 @@ func YamsAssertEqual(_ lhs: Any?, _ rhs: Any?,
return true
case let (lhs?, nil):
let message = { "(\"\(type(of: lhs))(\(dumped(lhs)))\") is not equal to (\"nil\")" }
XCTFail(joined(message(), context()), file: file, line: line)
XCTFail(joined(message(), context()), file: (file), line: line)
return false
case let (nil, rhs?):
let message = { "(\"nil\") is not equal to (\"\(type(of: rhs))(\(dumped(rhs)))\")" }
XCTFail(joined(message(), context()), file: file, line: line)
XCTFail(joined(message(), context()), file: (file), line: line)
return false
case let (lhs as Double, rhs as Double):
if lhs.isNaN && rhs.isNaN { return true } // NaN is not equal to any value, including NaN
XCTAssertEqual(lhs, rhs, context(), file: file, line: line)
XCTAssertEqual(lhs, rhs, context(), file: (file), line: line)
return lhs == rhs
case let (lhs as AnyHashable, rhs as AnyHashable):
XCTAssertEqual(lhs, rhs, context(), file: file, line: line)
XCTAssertEqual(lhs, rhs, context(), file: (file), line: line)
return lhs == rhs
case let (lhs as (Any, Any), rhs as (Any, Any)):
return equal(lhs.0, rhs.0) && equal(lhs.1, rhs.1)
case let (lhs as Set<AnyHashable>, rhs as Set<AnyHashable>):
return lhs == rhs
default:
let message = { "Can't compare \(type(of: lhs))(\(dumped(lhs))) to \(type(of: rhs))(\(dumped(rhs)))" }
XCTFail(joined(message(), context()), file: file, line: line)
XCTFail(joined(message(), context()), file: (file), line: line)
return false
}
}
Expand Down
36 changes: 36 additions & 0 deletions Tests/YamsTests/TopLevelDecoderTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// TopLevelDecoderTests.swift
// Yams
//
// Created by JP Simard on 2020-07-05.
// Copyright (c) 2020 Yams. All rights reserved.
//

#if canImport(Combine) && compiler(>=5.2)
import Combine
import XCTest
@testable import Yams

@available(iOS 13.0, macOS 10.15.0, tvOS 13.0, watchOS 6.0, *)
class TopLevelDecoderTests: XCTestCase {
func testDecodeFromYAMLDecoder() throws {
let yaml = """
name: Bird
"""
let data = try XCTUnwrap(yaml.data(using: Parser.Encoding.default.swiftStringEncoding))

struct Foo: Decodable {
var name: String
}

var foo: Foo?
_ = Just(data)
.decode(type: Foo.self, decoder: YAMLDecoder())
.sink(
receiveCompletion: { _ in },
receiveValue: { foo = $0 }
)
XCTAssertEqual(foo?.name, "Bird")
}
}
#endif
17 changes: 16 additions & 1 deletion Tests/YamsTests/YamlErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,20 @@ class YamlErrorTests: XCTestCase {
)
}
}

func testYamlErrorDataCouldNotBeDecoded() {
let yamlString = """
emoji: 🙃
"""
let utf16Data = yamlString.data(using: .utf16)!
XCTAssertThrowsError(try Parser(yaml: utf16Data, encoding: .utf8)) { error in
XCTAssertTrue(error is YamlError)
XCTAssertEqual("\(error)", """
String could not be decoded from data using 'Unicode (UTF-8)' encoding
"""
)
}
}
}

extension YamlErrorTests {
Expand All @@ -147,7 +161,8 @@ extension YamlErrorTests {
("testSingleRootThrowsOnInvalidYaml", testSingleRootThrowsOnInvalidYaml),
("testSingleRootThrowsOnMultipleDocuments", testSingleRootThrowsOnMultipleDocuments),
("testUndefinedAliasCausesError", testUndefinedAliasCausesError),
("testScannerErrorMayHaveNullContext", testScannerErrorMayHaveNullContext)
("testScannerErrorMayHaveNullContext", testScannerErrorMayHaveNullContext),
("testYamlErrorDataCouldNotBeDecoded", testYamlErrorDataCouldNotBeDecoded)
]
}
}
Loading