diff --git a/.github/workflows/build-embeded.yml b/.github/workflows/build-embeded.yml new file mode 100644 index 00000000..c8928aef --- /dev/null +++ b/.github/workflows/build-embeded.yml @@ -0,0 +1,22 @@ +name: Embedded +on: + push: + branches: + - main + - 'release/**' + pull_request: + branches: + - main + - 'release/**' +jobs: + embedded-build: + name: Build + runs-on: ubuntu-24.04 + steps: + - name: Checkout source + uses: actions/checkout@v4 + - uses: SwiftyLab/setup-swift@latest + with: + swift-version: "6.2.3" + - name: Embedded + run: swift build -c release --target TOMLDecoder --disable-default-traits -Xswiftc -target -Xswiftc x86_64-unknown-linux-gnu -Xswiftc -enable-experimental-feature -Xswiftc Embedded -Xswiftc -wmo diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml index 7ca8580f..b0d3d74d 100644 --- a/.github/workflows/build-wasm.yml +++ b/.github/workflows/build-wasm.yml @@ -20,5 +20,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Swift for WASM run: swift sdk install https://download.swift.org/swift-6.2.3-release/wasm-sdk/swift-6.2.3-RELEASE/swift-6.2.3-RELEASE_wasm.artifactbundle.tar.gz --checksum 394040ecd5260e68bb02f6c20aeede733b9b90702c2204e178f3e42413edad2a - - name: Run tests + - name: Build run: swift build --swift-sdk swift-6.2.3-RELEASE_wasm + - name: Build without Codable + run: swift build --swift-sdk swift-6.2.3-RELEASE_wasm --disable-default-traits diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index a9ba3fec..5187cb85 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -32,6 +32,9 @@ jobs: uses: actions/checkout@v4 - name: Run tests run: swift test + - name: Run tests without Codable + if: matrix.swift_version != '6.0' + run: swift test --disable-default-traits swift-test-macos: name: ${{ matrix.os_name }} (Swift ${{ matrix.swift_version }}) @@ -50,6 +53,9 @@ jobs: uses: actions/checkout@v4 - name: Run tests run: swift test + - name: Run tests without Codable + if: matrix.swift_version != '6.0' + run: swift test --disable-default-traits swift-test-simulator: name: ${{ matrix.platform }} ${{ matrix.os_version }} (Swift 6.2) diff --git a/Makefile b/Makefile index 151a0b51..8fd5f82a 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,22 @@ SHELL = /bin/bash export LANG = en_US.UTF-8 export LC_CTYPE = en_US.UTF-8 +EMBED_SWIFT ?= $(shell \ + if command -v swiftly >/dev/null 2>&1; then \ + toolchain="$$(swiftly use --print-location 2>/dev/null | head -n1)"; \ + if [ -x "$$toolchain/usr/bin/swift" ]; then \ + echo "$$toolchain/usr/bin/swift"; \ + else \ + command -v swift; \ + fi; \ + else \ + command -v swift; \ + fi \ +) .DEFAULT_GOAL := format -.PHONY: build test generate-code generate-tests benchmark format docs +.PHONY: build test embedded generate-code generate-tests benchmark format docs docs: @Scripts/generate-docs.sh / @@ -18,6 +30,9 @@ generate-tests: build: generate-code @swift build -c release -Xswiftc -warnings-as-errors > /dev/null +embedded: generate-code + @$(EMBED_SWIFT) build -c release --target TOMLDecoder --disable-default-traits -Xswiftc -target -Xswiftc arm64-apple-macos14.0 -Xswiftc -enable-experimental-feature -Xswiftc Embedded -Xswiftc -wmo + test: generate-tests @swift test -Xswiftc -warnings-as-errors diff --git a/Package.swift b/Package.swift index 25bd292d..422da71a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 6.1 import Foundation import PackageDescription @@ -89,6 +89,10 @@ let package = Package( .executable(name: "compliance", targets: ["compliance"]), .library(name: "TOMLDecoder", targets: ["TOMLDecoder"]), ], + traits: [ + "CodableSupport", + .default(enabledTraits: ["CodableSupport"]), + ], dependencies: benchmarksDeps + docsDeps + formattingDeps, targets: targets + testTargets + benchmarkTargets, cxxLanguageStandard: .cxx20 diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 00000000..dbc6e06b --- /dev/null +++ b/Package@swift-6.0.swift @@ -0,0 +1,52 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let codableSupportEnabled: [SwiftSetting] = [.define("CodableSupport")] + +let package = Package( + name: "TOMLDecoder", + platforms: [.iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macOS(.v10_15), .visionOS(.v1)], + products: [ + .executable(name: "compliance", targets: ["compliance"]), + .library(name: "TOMLDecoder", targets: ["TOMLDecoder"]), + ], + targets: [ + .executableTarget( + name: "compliance", + dependencies: ["TOMLDecoder"] + ), + .target( + name: "TOMLDecoder", + exclude: ["gyb"], + swiftSettings: codableSupportEnabled + [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), + ] + ), + .target( + name: "Resources", + exclude: ["fixtures"] + ), + .target( + name: "ProlepticGregorianTestHelpers", + publicHeadersPath: "include" + ), + .testTarget( + name: "TOMLDecoderTests", + dependencies: [ + "ProlepticGregorianTestHelpers", + "Resources", + "TOMLDecoder", + ], + exclude: [ + "gyb", + "invalid_fixtures", + "valid_fixtures", + ], + swiftSettings: codableSupportEnabled + ), + ], + cxxLanguageStandard: .cxx20 +) diff --git a/Scripts/generate-tests.py b/Scripts/generate-tests.py index 6f526917..c7a933ee 100755 --- a/Scripts/generate-tests.py +++ b/Scripts/generate-tests.py @@ -212,6 +212,7 @@ def _generate_valid_test_file(fixtures: Iterable[str], commit: str, spec_version """// Generated by Scripts/generate-tests.py // Source: toml-test commit __COMMIT__ (spec __SPEC_VERSION__) +#if CodableSupport import Foundation import Testing import TOMLDecoder @@ -233,6 +234,7 @@ def _generate_valid_test_file(fixtures: Iterable[str], commit: str, spec_version __TESTS__ } +#endif """ ) @@ -271,6 +273,7 @@ def _generate_invalid_test_file(fixtures: Iterable[str], commit: str, spec_versi """// Generated by Scripts/generate-tests.py // Source: toml-test commit __COMMIT__ (spec __SPEC_VERSION__) +#if CodableSupport import Foundation import Testing import TOMLDecoder @@ -291,6 +294,7 @@ def _generate_invalid_test_file(fixtures: Iterable[str], commit: str, spec_versi __TESTS__ } +#endif """ ) diff --git a/Sources/TOMLDecoder/DateTime.swift b/Sources/TOMLDecoder/DateTime.swift index e38b5daa..6fab7643 100644 --- a/Sources/TOMLDecoder/DateTime.swift +++ b/Sources/TOMLDecoder/DateTime.swift @@ -19,7 +19,7 @@ /// > This means for ancient dates, /// > ``OffsetDateTime`` may disagree with `Foundation.Date` on how much time has passed since a reference date. /// > For modern dates, there's no difference between the two. -public struct OffsetDateTime: Equatable, Hashable, Sendable, Codable, CustomStringConvertible { +public struct OffsetDateTime: Equatable, Hashable, Sendable, CustomStringConvertible { /// The date component of the offset date-time. public var date: LocalDate /// The time component of the offset date-time. @@ -48,13 +48,20 @@ public struct OffsetDateTime: Equatable, Hashable, Sendable, Codable, CustomStri self.features = features } + #if CodableSupport public init(from decoder: any Decoder) throws { if let decoder = decoder as? _TOMLDecoder { self = try decoder.decode(OffsetDateTime.self) - } else { - try self.init(from: decoder) + return } + + let container = try decoder.container(keyedBy: CodingKeys.self) + date = try container.decode(LocalDate.self, forKey: .date) + time = try container.decode(LocalTime.self, forKey: .time) + offset = try container.decode(Int16.self, forKey: .offset) + features = try container.decode(Features.self, forKey: .features) } + #endif /// Create a new offset date-time from it's members. /// @@ -183,7 +190,7 @@ public struct OffsetDateTime: Equatable, Hashable, Sendable, Codable, CustomStri /// /// A parser can preserve features of a offset date-time string with this type. /// If neither lowercase nor uppercase 'T' is present, the date-time seprator is a space, which is allowed by TOML. - public struct Features: OptionSet, Hashable, Sendable, Codable { + public struct Features: OptionSet, Hashable, Sendable { /// The raw value of the features. public let rawValue: UInt8 @@ -213,7 +220,7 @@ public struct OffsetDateTime: Equatable, Hashable, Sendable, Codable, CustomStri /// A local date-time as defined by TOML. /// /// ``LocalDateTime`` stores fractional seconds to the nanosecond precision. -public struct LocalDateTime: Equatable, Hashable, Sendable, Codable, CustomStringConvertible { +public struct LocalDateTime: Equatable, Hashable, Sendable, CustomStringConvertible { /// The date component of the local date-time. public var date: LocalDate /// The time component of the local date-time. @@ -231,13 +238,18 @@ public struct LocalDateTime: Equatable, Hashable, Sendable, Codable, CustomStrin self.time = time } + #if CodableSupport public init(from decoder: any Decoder) throws { if let decoder = decoder as? _TOMLDecoder { self = try decoder.decode(LocalDateTime.self) - } else { - try self.init(from: decoder) + return } + + let container = try decoder.container(keyedBy: CodingKeys.self) + date = try container.decode(LocalDate.self, forKey: .date) + time = try container.decode(LocalTime.self, forKey: .time) } + #endif /// Create a new local date-time from it's members. /// @@ -271,7 +283,7 @@ public struct LocalDateTime: Equatable, Hashable, Sendable, Codable, CustomStrin /// A local time as defined by TOML. /// /// ``LocalTime`` stores fractional seconds to the nanosecond precision. -public struct LocalTime: Equatable, Hashable, Sendable, Codable, CustomStringConvertible { +public struct LocalTime: Equatable, Hashable, Sendable, CustomStringConvertible { /// The hour component of the local time. public var hour: UInt8 /// The minute component of the local time. @@ -340,7 +352,7 @@ public struct LocalTime: Equatable, Hashable, Sendable, Codable, CustomStringCon /// A local date as defined by TOML. /// /// ``LocalDate`` stores the year, month, and day components of a date. -public struct LocalDate: Equatable, Hashable, Sendable, Codable, CustomStringConvertible { +public struct LocalDate: Equatable, Hashable, Sendable, CustomStringConvertible { /// The year component of the local date. /// Valid range is [1, 9999]. public var year: UInt16 @@ -431,7 +443,15 @@ extension String { } } -#if canImport(Foundation) +#if CodableSupport +extension OffsetDateTime: Codable {} +extension OffsetDateTime.Features: Codable {} +extension LocalDateTime: Codable {} +extension LocalTime: Codable {} +extension LocalDate: Codable {} +#endif + +#if CodableSupport public import Foundation extension DateComponents { diff --git a/Sources/TOMLDecoder/Parsing/Parser.swift b/Sources/TOMLDecoder/Parsing/Parser.swift index dab1648d..df508855 100644 --- a/Sources/TOMLDecoder/Parsing/Parser.swift +++ b/Sources/TOMLDecoder/Parsing/Parser.swift @@ -1,3 +1,11 @@ +#if !CodableSupport +@_silgen_name("strtod") +private func cStrtod( + _ nptr: UnsafePointer?, + _ endptr: UnsafeMutablePointer?>? +) -> Double +#endif + struct Parser: ~Copyable { var token = Token.empty var cursor = 0 @@ -1158,6 +1166,34 @@ extension Token { } func unpackFloat(bytes: UnsafeBufferPointer, context: TOMLKey) throws(TOMLError) -> Double { + #if !CodableSupport + @inline(__always) + func parseNormalizedFloat(_ codeUnits: inout [UTF8.CodeUnit]) -> Double? { + guard !codeUnits.isEmpty else { + return nil + } + codeUnits.append(0) + + return codeUnits.withUnsafeMutableBufferPointer { buffer -> Double? in + guard let base = buffer.baseAddress else { + return nil + } + return base.withMemoryRebound(to: CChar.self, capacity: buffer.count) { cString in + var end: UnsafeMutablePointer? + let value = cStrtod(cString, &end) + guard let end else { + return nil + } + let expectedEnd = cString.advanced(by: buffer.count - 1) + guard end == expectedEnd else { + return nil + } + return value + } + } + } + #endif + var resultCodeUnits: [UTF8.CodeUnit] = [] var index = text.lowerBound if bytes[index] == CodeUnits.plus || bytes[index] == CodeUnits.minus { @@ -1234,9 +1270,15 @@ extension Token { } } + #if CodableSupport guard let double = Double(String(decoding: resultCodeUnits, as: UTF8.self)) else { throw TOMLError(.invalidFloat(context: context, lineNumber: lineNumber, reason: "not a float")) } + #else + guard let double = parseNormalizedFloat(&resultCodeUnits) else { + throw TOMLError(.invalidFloat(context: context, lineNumber: lineNumber, reason: "not a float")) + } + #endif return double } @@ -1494,32 +1536,19 @@ extension Token { } } - do { - let (offsetHour, offsetMinute, consumedLength) = try parseTimezoneOffset(bytes: bytes, range: index ..< endIndex, lineNumber: lineNumber) - - // Validate timezone offset ranges - if offsetHour > 24 { - throw TOMLError(.invalidDateTime3(context: context, lineNumber: lineNumber, reason: "timezone offset hour must be between 00 and 24")) - } - if offsetMinute > 59 { - throw TOMLError(.invalidDateTime3(context: context, lineNumber: lineNumber, reason: "timezone offset minute must be between 00 and 59")) - } + let (offsetHour, offsetMinute, consumedLength) = try parseTimezoneOffset(bytes: bytes, range: index ..< endIndex, lineNumber: lineNumber) - let offsetInMinutes = offsetHour * 60 + offsetMinute - timeOffset = Int16(offsetIsNegative ? -offsetInMinutes : offsetInMinutes) - index += consumedLength - } catch let parseError { - if let tomlError = parseError as? TOMLError { - switch tomlError.reason { - case let .invalidDateTime(_, reason): - throw TOMLError(.invalidDateTime3(context: context, lineNumber: lineNumber, reason: reason)) - default: - throw tomlError - } - } else { - throw TOMLError(.invalidDateTime3(context: context, lineNumber: lineNumber, reason: "timezone parsing error")) - } + // Validate timezone offset ranges + if offsetHour > 24 { + throw TOMLError(.invalidDateTime3(context: context, lineNumber: lineNumber, reason: "timezone offset hour must be between 00 and 24")) } + if offsetMinute > 59 { + throw TOMLError(.invalidDateTime3(context: context, lineNumber: lineNumber, reason: "timezone offset minute must be between 00 and 59")) + } + + let offsetInMinutes = offsetHour * 60 + offsetMinute + timeOffset = Int16(offsetIsNegative ? -offsetInMinutes : offsetInMinutes) + index += consumedLength } } @@ -1535,6 +1564,7 @@ extension Token { ) } + #if CodableSupport func unpackAnyValue(bytes: UnsafeBufferPointer, context: TOMLKey) throws(TOMLError) -> Any { let firstChar = text.count > 0 ? bytes[text.lowerBound] : nil if firstChar == CodeUnits.singleQuote || firstChar == CodeUnits.doubleQuote { @@ -1571,6 +1601,7 @@ extension Token { throw TOMLError(.invalidValueInTable(context: context, lineNumber: lineNumber)) } } + #endif } func parseTimezoneOffset(bytes: UnsafeBufferPointer, range: Range, lineNumber: Int) throws(TOMLError) -> (hour: Int, minute: Int, consumedLength: Int) { diff --git a/Sources/TOMLDecoder/Parsing/TOMLDocument.swift b/Sources/TOMLDecoder/Parsing/TOMLDocument.swift index 189b94c4..f7db1e85 100644 --- a/Sources/TOMLDecoder/Parsing/TOMLDocument.swift +++ b/Sources/TOMLDecoder/Parsing/TOMLDocument.swift @@ -1,5 +1,3 @@ -import Foundation - struct TOMLDocument: Equatable, @unchecked Sendable { let tables: [InternalTOMLTable] let arrays: [InternalTOMLArray] @@ -26,27 +24,15 @@ struct TOMLDocument: Equatable, @unchecked Sendable { } init(source: String, keyTransform: (@Sendable (String) -> String)?) throws(TOMLError) { - var hasContinousStorage = false var parser = Parser(keyTransform: keyTransform) - - do { - try source.utf8.withContiguousStorageIfAvailable { - hasContinousStorage = true - try parser.parse(bytes: $0) - } - } catch { - throw error as! TOMLError + let bytes = Array(source.utf8) + let storage = UnsafeMutableBufferPointer.allocate(capacity: bytes.count) + _ = storage.initialize(from: bytes) + defer { + storage.deinitialize() + storage.deallocate() } - - if !hasContinousStorage { - var source = source - do { - try source.withUTF8 { try parser.parse(bytes: $0) } - } catch { - throw error as! TOMLError - } - } - + try parser.parse(bytes: UnsafeBufferPointer(storage)) self = parser.finish(source: source) } } @@ -231,6 +217,7 @@ struct DateTimeComponents: Equatable { } } +#if CodableSupport extension InternalTOMLTable { func dictionary(source: TOMLDocument) throws(TOMLError) -> [String: Any] { var result = [String: Any]() @@ -271,6 +258,7 @@ extension InternalTOMLArray { return result } } +#endif extension TOMLDocument { @inline(__always) diff --git a/Sources/TOMLDecoder/Parsing/Token.swift b/Sources/TOMLDecoder/Parsing/Token.swift index c2857648..e97af190 100644 --- a/Sources/TOMLDecoder/Parsing/Token.swift +++ b/Sources/TOMLDecoder/Parsing/Token.swift @@ -22,52 +22,118 @@ struct Token: Equatable { extension Token { func unpackBool(source: String, context: TOMLKey) throws(TOMLError) -> Bool { + #if !CodableSupport + let bytes = Array(source.utf8) + let storage = UnsafeMutableBufferPointer.allocate(capacity: bytes.count) + _ = storage.initialize(from: bytes) + defer { + storage.deinitialize() + storage.deallocate() + } + return try unpackBool(bytes: UnsafeBufferPointer(storage), context: context) + #else do { return try (source.utf8.withContiguousStorageIfAvailable { try unpackBool(bytes: $0, context: context) })! } catch { throw error as! TOMLError } + #endif } func unpackFloat(source: String, context: TOMLKey) throws(TOMLError) -> Double { + #if !CodableSupport + let bytes = Array(source.utf8) + let storage = UnsafeMutableBufferPointer.allocate(capacity: bytes.count) + _ = storage.initialize(from: bytes) + defer { + storage.deinitialize() + storage.deallocate() + } + return try unpackFloat(bytes: UnsafeBufferPointer(storage), context: context) + #else do { return try (source.utf8.withContiguousStorageIfAvailable { try unpackFloat(bytes: $0, context: context) })! } catch { throw error as! TOMLError } + #endif } func unpackString(source: String, context: TOMLKey) throws(TOMLError) -> String { + #if !CodableSupport + let bytes = Array(source.utf8) + let storage = UnsafeMutableBufferPointer.allocate(capacity: bytes.count) + _ = storage.initialize(from: bytes) + defer { + storage.deinitialize() + storage.deallocate() + } + return try unpackString(bytes: UnsafeBufferPointer(storage), context: context) + #else do { return try (source.utf8.withContiguousStorageIfAvailable { try unpackString(bytes: $0, context: context) })! } catch { throw error as! TOMLError } + #endif } func unpackInteger(source: String, context: TOMLKey) throws(TOMLError) -> Int64 { + #if !CodableSupport + let bytes = Array(source.utf8) + let storage = UnsafeMutableBufferPointer.allocate(capacity: bytes.count) + _ = storage.initialize(from: bytes) + defer { + storage.deinitialize() + storage.deallocate() + } + return try unpackInteger(bytes: UnsafeBufferPointer(storage), context: context) + #else do { return try (source.utf8.withContiguousStorageIfAvailable { try unpackInteger(bytes: $0, context: context) })! } catch { throw error as! TOMLError } + #endif } func unpackDateTime(source: String, context: TOMLKey) throws(TOMLError) -> DateTimeComponents { + #if !CodableSupport + let bytes = Array(source.utf8) + let storage = UnsafeMutableBufferPointer.allocate(capacity: bytes.count) + _ = storage.initialize(from: bytes) + defer { + storage.deinitialize() + storage.deallocate() + } + return try unpackDateTime(bytes: UnsafeBufferPointer(storage), context: context) + #else do { return try (source.utf8.withContiguousStorageIfAvailable { try unpackDateTime(bytes: $0, context: context) })! } catch { throw error as! TOMLError } + #endif } func unpackOffsetDateTime(source: String, context: TOMLKey) throws(TOMLError) -> OffsetDateTime { let datetime: DateTimeComponents + #if !CodableSupport + let bytes = Array(source.utf8) + let storage = UnsafeMutableBufferPointer.allocate(capacity: bytes.count) + _ = storage.initialize(from: bytes) + defer { + storage.deinitialize() + storage.deallocate() + } + datetime = try unpackDateTime(bytes: UnsafeBufferPointer(storage), context: context) + #else do { datetime = try (source.utf8.withContiguousStorageIfAvailable { try unpackDateTime(bytes: $0, context: context) })! } catch { throw error as! TOMLError } + #endif switch (datetime.date, datetime.time, datetime.offset) { case let (.some(date), .some(time), .some(offset)): return OffsetDateTime(date: date, time: time, offset: offset, features: datetime.features) @@ -100,6 +166,7 @@ extension Token { return localTime } + #if CodableSupport func unpackAnyValue(source: String, context: TOMLKey) throws(TOMLError) -> Any { do { return try (source.utf8.withContiguousStorageIfAvailable { try unpackAnyValue(bytes: $0, context: context) })! @@ -107,4 +174,5 @@ extension Token { throw error as! TOMLError } } + #endif } diff --git a/Sources/TOMLDecoder/TOMLArray.swift b/Sources/TOMLDecoder/TOMLArray.swift index 7e0bc0cc..b1453ca9 100644 --- a/Sources/TOMLDecoder/TOMLArray.swift +++ b/Sources/TOMLDecoder/TOMLArray.swift @@ -286,6 +286,7 @@ public struct TOMLArray: Equatable, Sendable { return localTime } + #if CodableSupport func array() throws(TOMLError) -> [Any] { if isKeyed { let count = source.keyArrays.count @@ -301,6 +302,7 @@ public struct TOMLArray: Equatable, Sendable { return try source.arrays[index].array(source: source) } } + #endif @inline(__always) func resolvedArray() -> InternalTOMLArray { @@ -308,6 +310,7 @@ public struct TOMLArray: Equatable, Sendable { } } +#if CodableSupport extension TOMLArray: Codable { /// Makes ``TOMLArray`` eligible for `Codable`. /// @@ -339,7 +342,9 @@ extension TOMLArray: Codable { throw TOMLError(.notReallyCodable) } } +#endif +#if CodableSupport extension [Any] { /// Create a `[Any]` from a `TOMLArray`. /// Validating all fields recursively. @@ -356,3 +361,4 @@ extension [Any] { self = try tomlArray.array() } } +#endif diff --git a/Sources/TOMLDecoder/TOMLDecoder.docc/DevelopingTOMLDecoder.md b/Sources/TOMLDecoder/TOMLDecoder.docc/DevelopingTOMLDecoder.md index e8473937..75780b28 100644 --- a/Sources/TOMLDecoder/TOMLDecoder.docc/DevelopingTOMLDecoder.md +++ b/Sources/TOMLDecoder/TOMLDecoder.docc/DevelopingTOMLDecoder.md @@ -37,9 +37,17 @@ Our unit tests include the [official suite](https://github.com/toml-lang/toml-te from the TOML GitHub organization, systematically translated into Swift tests. -Run tests with `swift test`, as well as `bazel test //...`. +For any changes to land, `swift test` must pass on ... -Tests must pass on macOS, Ubuntu, and Windows for any changes to land. +* latest Swift toolchain on macOS, Ubuntu, and Windows +* same as above but with `--disable-default-traits` +* simulators on supported Apple flatforms +* Oldest supported Swift toolchain one of the oldest supported Apple OS. + +... in addition: + +* `bazel test //...` must pass +* the library must _build_ for a embedded Swift ## Generating Code and Tests @@ -96,6 +104,13 @@ to compare performance between two commits. ## Architecture Overview TOMLDecoder is a parser with a `Swift.Decoder` implementation sitting on top. +The latter is gated in the `CodableSupport` SwiftPM trait, +which is enabled by default. +When the trait is excluded, +the library still functions without the `Codable` APIs. +This layer does not depend on Foundation, +and it works in WASM and embedded Swift environments. + TOML has a spec, and a large number of parser implementations in many languages. diff --git a/Sources/TOMLDecoder/TOMLDecoder.docc/TOMLDecoder.md b/Sources/TOMLDecoder/TOMLDecoder.docc/TOMLDecoder.md index e9568026..452a3f94 100644 --- a/Sources/TOMLDecoder/TOMLDecoder.docc/TOMLDecoder.md +++ b/Sources/TOMLDecoder/TOMLDecoder.docc/TOMLDecoder.md @@ -15,6 +15,7 @@ This library can do 2 things to TOML: * **Deserialize**: convert TOML strings or byte sequences into strongly-typed, structured data, and provide access to parts of it. + This part works in the embedded Swift environment. * **Decode**: further convert the structured data into your `Codable` types according to your preferences. @@ -37,6 +38,8 @@ and TOMLDecoder can attempt to create instances of your type from TOML data. You can configure the decoding strategies to customize the decoder's behaviors. +The `CodableSupport` trait is included by default, +it enables this functionality. - - ``TOMLDecoder`` @@ -52,6 +55,8 @@ Parse, un-marshal, deserialize. When TOMLDecoder does this to TOML strings or bytes, each data type as defined by the TOML specification is strictly mapped to a Swift type. +This part works with embedded Swift, +when `CodableSupport` is excluded. - - ``TOMLTable`` diff --git a/Sources/TOMLDecoder/TOMLDecoder.swift b/Sources/TOMLDecoder/TOMLDecoder.swift index 68d570c9..eb3d1f83 100644 --- a/Sources/TOMLDecoder/TOMLDecoder.swift +++ b/Sources/TOMLDecoder/TOMLDecoder.swift @@ -1,3 +1,4 @@ +#if CodableSupport public import Foundation /// Convert data for a TOML document into `Codable` types. @@ -310,3 +311,4 @@ func snakeCasify(_ stringKey: String) -> String { joinedString + String(stringKey[trailingUnderscoreRange]) } } +#endif diff --git a/Sources/TOMLDecoder/TOMLKey.swift b/Sources/TOMLDecoder/TOMLKey.swift index 1c25d8fc..9186bc07 100644 --- a/Sources/TOMLDecoder/TOMLKey.swift +++ b/Sources/TOMLDecoder/TOMLKey.swift @@ -1,4 +1,4 @@ -enum TOMLKey: CodingKey { +enum TOMLKey { case string(String) case int(Int) case `super` @@ -31,3 +31,7 @@ enum TOMLKey: CodingKey { } } } + +#if CodableSupport +extension TOMLKey: CodingKey {} +#endif diff --git a/Sources/TOMLDecoder/TOMLKeyedDecodingContainer.swift b/Sources/TOMLDecoder/TOMLKeyedDecodingContainer.swift index d6d5e54b..0d08e433 100644 --- a/Sources/TOMLDecoder/TOMLKeyedDecodingContainer.swift +++ b/Sources/TOMLDecoder/TOMLKeyedDecodingContainer.swift @@ -1,6 +1,6 @@ -#if canImport(Foundation) +#if CodableSupport import Foundation -#endif + struct TOMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { private let decoder: _TOMLDecoder private let table: TOMLTable @@ -74,14 +74,12 @@ struct TOMLKeyedDecodingContainer: KeyedDecodingContainerProtoco decoder.codingPath.removeLast() } decoder.token = token - #if canImport(Foundation) // have to intercept these here otherwise Foundation will try to decode it as a float if type == Date.self { return try decoder.decode(Date.self) as! T } else if type == DateComponents.self { return try decoder.decode(DateComponents.self) as! T } - #endif if type == LocalDate.self { return try decoder.decode(LocalDate.self) as! T } else if type == LocalTime.self { @@ -134,3 +132,4 @@ struct TOMLKeyedDecodingContainer: KeyedDecodingContainerProtoco _superDecoder(forKey: key) } } +#endif diff --git a/Sources/TOMLDecoder/TOMLSingleValueDecodingContainer.Generated.swift b/Sources/TOMLDecoder/TOMLSingleValueDecodingContainer.Generated.swift index c4edc9da..f1562e3a 100644 --- a/Sources/TOMLDecoder/TOMLSingleValueDecodingContainer.Generated.swift +++ b/Sources/TOMLDecoder/TOMLSingleValueDecodingContainer.Generated.swift @@ -1,3 +1,4 @@ +#if CodableSupport // WARNING: This file is generated from TOMLSingleValueDecodingContainer.swift.gyb // Do not edit TOMLSingleValueDecodingContainer.swift directly. @@ -374,3 +375,4 @@ extension _TOMLDecoder { return components } } +#endif diff --git a/Sources/TOMLDecoder/TOMLTable.swift b/Sources/TOMLDecoder/TOMLTable.swift index 1f62f892..8941310a 100644 --- a/Sources/TOMLDecoder/TOMLTable.swift +++ b/Sources/TOMLDecoder/TOMLTable.swift @@ -29,26 +29,25 @@ extension TOMLTable { // String.init(validating:as:) does not exist in our supported OSes. extension String { fileprivate init(validatingUTF8 source: some Collection) throws(TOMLError) { + #if CodableSupport do { - if let result = try source.withContiguousStorageIfAvailable( - { buffer -> String in - try validateAndCreateString(from: buffer) - } - ) { + if let result = try source.withContiguousStorageIfAvailable({ try validateAndCreateString(from: $0) }) { self = result return } - - // Slow path: copy to contiguous buffer first - let array = Array(source) - self = try array.withUnsafeBufferPointer { buffer -> String in - try validateAndCreateString(from: buffer) - } - } catch let error as TOMLError { - throw error } catch { - fatalError("Unexpected error type") + throw error as! TOMLError + } + #endif + + let array = Array(source) + let storage = UnsafeMutableBufferPointer.allocate(capacity: array.count) + _ = storage.initialize(from: array) + defer { + storage.deinitialize() + storage.deallocate() } + self = try validateAndCreateString(from: UnsafeBufferPointer(storage)) } } @@ -382,11 +381,14 @@ public struct TOMLTable: Sendable, Equatable { return localTime } + #if CodableSupport func dictionary() throws(TOMLError) -> [String: Any] { try source.table(at: index, keyed: isKeyed).dictionary(source: source) } + #endif } +#if CodableSupport extension TOMLTable: Codable { /// Makes ``TOMLTable`` eligible for `Codable`. /// @@ -424,7 +426,9 @@ extension TOMLTable: Codable { throw TOMLError(.notReallyCodable) } } +#endif +#if CodableSupport extension [String: Any] { /// Create a `[String: Any]` from a `TOMLTable`. /// Validating all fields along the way. @@ -443,3 +447,4 @@ extension [String: Any] { self = try tomlTable.dictionary() } } +#endif diff --git a/Sources/TOMLDecoder/TOMLUnkeyedDecodingContainer.swift b/Sources/TOMLDecoder/TOMLUnkeyedDecodingContainer.swift index f71c9ac3..f1a90423 100644 --- a/Sources/TOMLDecoder/TOMLUnkeyedDecodingContainer.swift +++ b/Sources/TOMLDecoder/TOMLUnkeyedDecodingContainer.swift @@ -1,6 +1,5 @@ -#if canImport(Foundation) +#if CodableSupport import Foundation -#endif struct TOMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { private let decoder: _TOMLDecoder @@ -89,13 +88,11 @@ struct TOMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { decoder.token = token // have to intercept these here otherwise Foundation will try to decode it as a float - #if canImport(Foundation) if type == Date.self { return try decoder.decode(Date.self) as! T } else if type == DateComponents.self { return try decoder.decode(DateComponents.self) as! T } - #endif if type == LocalDate.self { return try decoder.decode(LocalDate.self) as! T } else if type == LocalTime.self { @@ -172,3 +169,4 @@ struct TOMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { _TOMLDecoder(referencing: .unkeyed(array), at: codingPath + [TOMLKey.super], strategy: decoder.strategy, isLenient: decoder.isLenient) } } +#endif diff --git a/Sources/TOMLDecoder/_TOMLDecoder.swift b/Sources/TOMLDecoder/_TOMLDecoder.swift index d46600d8..22b4a464 100644 --- a/Sources/TOMLDecoder/_TOMLDecoder.swift +++ b/Sources/TOMLDecoder/_TOMLDecoder.swift @@ -1,3 +1,4 @@ +#if CodableSupport final class _TOMLDecoder: Decoder { enum Container { case keyed(TOMLTable) @@ -72,3 +73,4 @@ extension Double { } } } +#endif diff --git a/Sources/TOMLDecoder/gyb/TOMLSingleValueDecodingContainer.swift.gyb b/Sources/TOMLDecoder/gyb/TOMLSingleValueDecodingContainer.swift.gyb index 605a38c7..48b70cf1 100644 --- a/Sources/TOMLDecoder/gyb/TOMLSingleValueDecodingContainer.swift.gyb +++ b/Sources/TOMLDecoder/gyb/TOMLSingleValueDecodingContainer.swift.gyb @@ -1,3 +1,4 @@ +#if CodableSupport %{ # gyb variables available: `__file__` gives the template’s path. from pathlib import Path @@ -196,3 +197,4 @@ extension _TOMLDecoder { return components } } +#endif diff --git a/Sources/compliance/main.swift b/Sources/compliance/main.swift index 6ed62119..78c0b8e3 100644 --- a/Sources/compliance/main.swift +++ b/Sources/compliance/main.swift @@ -1,8 +1,11 @@ // This is CLI app that provides decoder interface for the test suite at // https://github.com/BurntSushi/toml-test +#if CodableSupport import Foundation +#endif import TOMLDecoder +#if CodableSupport /// iOS 13+ compatible date formatter functions private func createISO8601FullFormatter() -> ISO8601DateFormatter { let formatter = ISO8601DateFormatter() @@ -88,3 +91,6 @@ func translate(value: Any) -> Any { let json = try JSONSerialization.data(withJSONObject: translate(value: table)) print(String(data: json, encoding: .utf8)!) +#else +fatalError("compliance executable requires CodableSupport trait.") +#endif diff --git a/Tests/TOMLDecoderTests/DateStrategyTests.swift b/Tests/TOMLDecoderTests/DateStrategyTests.swift index 4eb475ce..ee805896 100644 --- a/Tests/TOMLDecoderTests/DateStrategyTests.swift +++ b/Tests/TOMLDecoderTests/DateStrategyTests.swift @@ -1,3 +1,4 @@ +#if CodableSupport import Foundation import Testing @testable import TOMLDecoder @@ -242,3 +243,4 @@ struct DateStrategyTests { } } } +#endif diff --git a/Tests/TOMLDecoderTests/InvalidationTests.Generated.swift b/Tests/TOMLDecoderTests/InvalidationTests.Generated.swift index 2705b370..4a693904 100644 --- a/Tests/TOMLDecoderTests/InvalidationTests.Generated.swift +++ b/Tests/TOMLDecoderTests/InvalidationTests.Generated.swift @@ -1,6 +1,7 @@ // Generated by Scripts/generate-tests.py // Source: toml-test commit 0ee318ae97ae5dec5f74aeccafbdc75f435580e2 (spec 1.1.0) +#if CodableSupport import Foundation import Testing import TOMLDecoder @@ -2349,3 +2350,4 @@ struct TOMLInvalidationTests { try invalidate(pathComponents: ["table", "with-pound"]) } } +#endif diff --git a/Tests/TOMLDecoderTests/LeafValueDecodingTests.Generated.swift b/Tests/TOMLDecoderTests/LeafValueDecodingTests.Generated.swift index f8a96e45..e23ef2e8 100644 --- a/Tests/TOMLDecoderTests/LeafValueDecodingTests.Generated.swift +++ b/Tests/TOMLDecoderTests/LeafValueDecodingTests.Generated.swift @@ -1,3 +1,4 @@ +#if CodableSupport // WARNING: This file is generated from LeafValueDecodingTests.swift.gyb // Do not edit LeafValueDecodingTests.swift directly. @@ -811,3 +812,4 @@ private enum AString: String, Decodable, Equatable { case foo case bar } +#endif diff --git a/Tests/TOMLDecoderTests/Support/TOMLComplianceSupport.swift b/Tests/TOMLDecoderTests/Support/TOMLComplianceSupport.swift index 8a93c6de..f859aa8e 100644 --- a/Tests/TOMLDecoderTests/Support/TOMLComplianceSupport.swift +++ b/Tests/TOMLDecoderTests/Support/TOMLComplianceSupport.swift @@ -1,3 +1,4 @@ +#if CodableSupport import Foundation import ProlepticGregorianTestHelpers import Testing @@ -388,3 +389,4 @@ enum TOMLComplianceSupport { return description } } +#endif diff --git a/Tests/TOMLDecoderTests/TOMLDecoderTests.swift b/Tests/TOMLDecoderTests/TOMLDecoderTests.swift index 55663ce4..90b07c02 100644 --- a/Tests/TOMLDecoderTests/TOMLDecoderTests.swift +++ b/Tests/TOMLDecoderTests/TOMLDecoderTests.swift @@ -1,3 +1,4 @@ +#if CodableSupport import Foundation import Resources import Testing @@ -499,3 +500,4 @@ struct TOMLDecoderTests { _ = try TOMLDecoder().decode(CanadaFeatureCollection.self, from: Resources.canadaTOMLString) } } +#endif diff --git a/Tests/TOMLDecoderTests/ValidationTests.Generated.swift b/Tests/TOMLDecoderTests/ValidationTests.Generated.swift index 6f516b08..5fc516b6 100644 --- a/Tests/TOMLDecoderTests/ValidationTests.Generated.swift +++ b/Tests/TOMLDecoderTests/ValidationTests.Generated.swift @@ -1,6 +1,7 @@ // Generated by Scripts/generate-tests.py // Source: toml-test commit 0ee318ae97ae5dec5f74aeccafbdc75f435580e2 (spec 1.1.0) +#if CodableSupport import Foundation import Testing import TOMLDecoder @@ -1090,3 +1091,4 @@ struct TOMLValidationTests { try verifyByFixture(pathComponents: ["table", "without-super-with-values"]) } } +#endif diff --git a/Tests/TOMLDecoderTests/gyb/LeafValueDecodingTests.swift.gyb b/Tests/TOMLDecoderTests/gyb/LeafValueDecodingTests.swift.gyb index d7473395..dc6d32b7 100644 --- a/Tests/TOMLDecoderTests/gyb/LeafValueDecodingTests.swift.gyb +++ b/Tests/TOMLDecoderTests/gyb/LeafValueDecodingTests.swift.gyb @@ -1,3 +1,4 @@ +#if CodableSupport %{ # gyb variables available: `__file__` gives the template’s path. from pathlib import Path @@ -478,3 +479,4 @@ private enum AString: String, Decodable, Equatable { case foo case bar } +#endif