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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# TOMLDecoder

A fast, Swift-native, minimal dependency library
that fully implements [TOML](https://toml.io/) spec 1.0.
that fully implements [TOML](https://toml.io/) spec 1.1.0.

```swift
struct Team: Codable {
Expand Down
10 changes: 5 additions & 5 deletions Scripts/generate-tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@


REPO_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_SPEC_VERSION = "1.0.0"
DEFAULT_SPEC_VERSION = "1.1.0"
TMP_ROOT = REPO_ROOT / "tmp"
TOML_TEST_REPO_URL = "git@github.com:toml-lang/toml-test.git"
TOML_TEST_COMMIT = "8be0db7469af2d97449e0a06075a1a6ecdc60231"
TOML_TEST_COMMIT = "0ee318ae97ae5dec5f74aeccafbdc75f435580e2"
TOML_TEST_DIR = TMP_ROOT / "toml-test"
VALID_FIXTURES_DIR = REPO_ROOT / "Tests" / "TOMLDecoderTests" / "valid_fixtures"
INVALID_FIXTURES_DIR = REPO_ROOT / "Tests" / "TOMLDecoderTests" / "invalid_fixtures"
Expand Down Expand Up @@ -144,7 +144,7 @@ def _generate_tags_file(tags: set[str], commit: str, spec_version: str) -> str:
tag_declarations = _generate_tag_declarations(tags)

template = textwrap.dedent(
"""// Generated by Scripts/sync_compliance_tests.py
"""// Generated by Scripts/generate-tests.py
// Source: toml-test commit __COMMIT__ (spec __SPEC_VERSION__)

import Testing
Expand Down Expand Up @@ -209,7 +209,7 @@ def _generate_valid_test_file(fixtures: Iterable[str], commit: str, spec_version
tests_block = "\n".join(tests).rstrip()

template = textwrap.dedent(
"""// Generated by Scripts/sync_compliance_tests.py
"""// Generated by Scripts/generate-tests.py
// Source: toml-test commit __COMMIT__ (spec __SPEC_VERSION__)

import Foundation
Expand Down Expand Up @@ -268,7 +268,7 @@ def _generate_invalid_test_file(fixtures: Iterable[str], commit: str, spec_versi
tests_block = "\n".join(tests).rstrip()

template = textwrap.dedent(
"""// Generated by Scripts/sync_compliance_tests.py
"""// Generated by Scripts/generate-tests.py
// Source: toml-test commit __COMMIT__ (spec __SPEC_VERSION__)

import Foundation
Expand Down
1 change: 1 addition & 0 deletions Sources/TOMLDecoder/Parsing/Constants.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
enum CodeUnits {
static let escape: UTF8.CodeUnit = 27
static let equal: UTF8.CodeUnit = 61
static let comma: UTF8.CodeUnit = 44
static let lbrace: UTF8.CodeUnit = 123
Expand Down
65 changes: 32 additions & 33 deletions Sources/TOMLDecoder/Parsing/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,16 @@ struct Parser: ~Copyable {
.syntax(
lineNumber: lineNumber,
message: "control characters are not allowed in comments"
))
)
)
}
} else {
throw TOMLError(
.syntax(
lineNumber: lineNumber,
message: "control characters are not allowed in comments"
))
)
)
}
}
position += 1
Expand Down Expand Up @@ -128,7 +130,8 @@ struct Parser: ~Copyable {
.syntax(
lineNumber: lineNumber,
message: "bare carriage return is not allowed"
))
)
)
default:
break
}
Expand Down Expand Up @@ -181,7 +184,8 @@ struct Parser: ~Copyable {

guard i < range.upperBound else {
throw TOMLError(
.syntax(lineNumber: lineNumber, message: "unterminated triple-s-quote"))
.syntax(lineNumber: lineNumber, message: "unterminated triple-s-quote")
)
}

let end = i + 3
Expand Down Expand Up @@ -221,7 +225,8 @@ struct Parser: ~Copyable {

guard i < range.upperBound else {
throw TOMLError(
.syntax(lineNumber: lineNumber, message: "unterminated triple-d-quote"))
.syntax(lineNumber: lineNumber, message: "unterminated triple-d-quote")
)
}

let end = i + 3
Expand All @@ -244,7 +249,8 @@ struct Parser: ~Copyable {

if i >= textCount || bytes[i] != CodeUnits.singleQuote {
throw TOMLError(
.syntax(lineNumber: lineNumber, message: "unterminated s-quote"))
.syntax(lineNumber: lineNumber, message: "unterminated s-quote")
)
}

emitToken(kind: .string, start: start, end: i + 1)
Expand Down Expand Up @@ -286,7 +292,8 @@ struct Parser: ~Copyable {

if i >= range.upperBound || bytes[i] != CodeUnits.doubleQuote {
throw TOMLError(
.syntax(lineNumber: lineNumber, message: "unterminated quote"))
.syntax(lineNumber: lineNumber, message: "unterminated quote")
)
}

emitToken(kind: .string, start: start, end: i + 1)
Expand Down Expand Up @@ -595,9 +602,7 @@ struct Parser: ~Copyable {
try eatToken(bytes: bytes, kind: .lbrace, isDotSpecial: true)

while true {
if token.kind == .newline {
throw TOMLError(.syntax(lineNumber: token.lineNumber, message: "newline not allowed in inline table"))
}
try skipNewlines(bytes: bytes, isDotSpecial: false)

if token.kind == .rbrace {
break
Expand All @@ -609,16 +614,10 @@ struct Parser: ~Copyable {

try parseKeyValue(bytes: bytes, tableIndex: tableIndex, isKeyed: true)

if token.kind == .newline {
throw TOMLError(.syntax(lineNumber: token.lineNumber, message: "newline not allowed in inline table"))
}
try skipNewlines(bytes: bytes, isDotSpecial: false)

if token.kind == .comma {
try eatToken(bytes: bytes, kind: .comma, isDotSpecial: true)
// Check for trailing comma - if next token is rbrace, it's a trailing comma error
if token.kind == .rbrace {
throw TOMLError(.syntax(lineNumber: token.lineNumber, message: "trailing comma not allowed in inline table"))
}
continue
}
break
Expand All @@ -633,9 +632,7 @@ struct Parser: ~Copyable {
try eatToken(bytes: bytes, kind: .lbrace, isDotSpecial: true)

while true {
if token.kind == .newline {
throw TOMLError(.syntax(lineNumber: token.lineNumber, message: "newline not allowed in inline table"))
}
try skipNewlines(bytes: bytes, isDotSpecial: false)

if token.kind == .rbrace {
break
Expand All @@ -647,16 +644,10 @@ struct Parser: ~Copyable {

try parseKeyValue(bytes: bytes, tableIndex: tableIndex, isKeyed: false)

if token.kind == .newline {
throw TOMLError(.syntax(lineNumber: token.lineNumber, message: "newline not allowed in inline table"))
}
try skipNewlines(bytes: bytes, isDotSpecial: false)

if token.kind == .comma {
try eatToken(bytes: bytes, kind: .comma, isDotSpecial: true)
// Check for trailing comma - if next token is rbrace, it's a trailing comma error
if token.kind == .rbrace {
throw TOMLError(.syntax(lineNumber: token.lineNumber, message: "trailing comma not allowed in inline table"))
}
continue
}
break
Expand Down Expand Up @@ -1251,7 +1242,7 @@ extension Token {
// For standalone time values, don't advance index
mustParseTime = true
}
if let (hour, minute, second, _) = scanTime(bytes: bytes, range: index ..< text.upperBound) {
if let (hour, minute, second, newIndex) = scanTime(bytes: bytes, range: index ..< text.upperBound) {
// Validate time components
if hour > 23 {
throw TOMLError(.invalidDateTime3(context: context, lineNumber: lineNumber, reason: "hour must be between 00 and 23"))
Expand All @@ -1265,7 +1256,7 @@ extension Token {

time = (hour, minute, second)

index += 8
index = newIndex
if index < text.upperBound, bytes[index] == CodeUnits.dot {
index += 1
let beforeNanoIndex = index
Expand Down Expand Up @@ -1512,7 +1503,8 @@ func basicString(bytes: UnsafeBufferPointer<UInt8>, range: Range<Int>, multiline
lineNumber: 0,
message:
"basic multiline strings cannot contain more than 2 consecutive double quotes"
))
)
)
}
} else {
consecutiveQuotes = 0
Expand Down Expand Up @@ -1557,8 +1549,13 @@ func basicString(bytes: UnsafeBufferPointer<UInt8>, range: Range<Int>, multiline
ch = bytes[index]
index += 1

if ch == CodeUnits.lowerU || ch == CodeUnits.upperU {
let hexCount = (ch == CodeUnits.lowerU ? 4 : 8)
if ch == CodeUnits.lowerU || ch == CodeUnits.upperU || ch == CodeUnits.lowerX {
let hexCount = switch ch {
case CodeUnits.lowerU: 4
case CodeUnits.upperU: 8
case CodeUnits.lowerX: 2
default: fatalError("Unsupported CodeUnit: \(ch)")
}
var ucs: UInt32 = 0
for _ in 0 ..< hexCount {
if index >= endIndex {
Expand Down Expand Up @@ -1594,6 +1591,8 @@ func basicString(bytes: UnsafeBufferPointer<UInt8>, range: Range<Int>, multiline
ch = CodeUnits.cr
} else if ch == CodeUnits.lowerN {
ch = CodeUnits.lf
} else if ch == CodeUnits.lowerE {
ch = CodeUnits.escape
} else if ch != CodeUnits.doubleQuote, ch != CodeUnits.backslash {
throw TOMLError(.illegalEscapeCharacter(ch))
}
Expand Down Expand Up @@ -1666,7 +1665,7 @@ func scanTime(bytes: UnsafeBufferPointer<UInt8>, range: Range<Int>) -> (Int, Int

index += 2
guard index < range.upperBound, bytes[index] == CodeUnits.colon else {
return nil
return (hour, minute, 0, index) // Seconds are optional since 1.1.0. When omitted, :00 seconds is assumed
}

index += 1
Expand Down
4 changes: 0 additions & 4 deletions Sources/TOMLDecoder/Parsing/TOMLDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,6 @@ struct KeyValuePair: Equatable {
let key: String
let keyHash: Int
var value: Token

static func == (lhs: Self, rhs: Self) -> Bool {
lhs.keyHash == rhs.keyHash && lhs.key == rhs.key && lhs.value == rhs.value
}
}

struct InternalTOMLTable: Equatable, Sendable {
Expand Down
6 changes: 0 additions & 6 deletions Sources/TOMLDecoder/Parsing/Token.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ struct Token: Equatable {
let text: Range<Int>

static let empty = Token(kind: .newline, lineNumber: 1, text: 0 ..< 0)

init(kind: Kind, lineNumber: Int, text: Range<Int>) {
self.kind = kind
self.lineNumber = lineNumber
self.text = text
}
}

extension Token {
Expand Down
2 changes: 1 addition & 1 deletion Sources/TOMLDecoder/TOMLDecoder.docc/TOMLDecoder.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ It converts TOML into your `Codable` types
It provides fast TOML deserialization
(like `JSONDeserializer` for JSON),
and type-safe access to parsed results.
`TOMLDecoder` implements the TOML 1.0 spec.
`TOMLDecoder` implements the TOML 1.1.0 spec.

This library can do 2 things to TOML:
* **Deserialize**:
Expand Down
3 changes: 1 addition & 2 deletions Sources/TOMLDecoder/TOMLDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ func snakeCasify(_ stringKey: String) -> String {
}

// Do a cheap isEmpty check before creating and appending potentially empty strings
let result: String = if leadingUnderscoreRange.isEmpty, trailingUnderscoreRange.isEmpty {
return if leadingUnderscoreRange.isEmpty, trailingUnderscoreRange.isEmpty {
joinedString
} else if !leadingUnderscoreRange.isEmpty, !trailingUnderscoreRange.isEmpty {
// Both leading and trailing underscores
Expand All @@ -309,5 +309,4 @@ func snakeCasify(_ stringKey: String) -> String {
// Just trailing
joinedString + String(stringKey[trailingUnderscoreRange])
}
return result
}
4 changes: 2 additions & 2 deletions Sources/TOMLDecoder/TOMLKeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ struct TOMLKeyedDecodingContainer<Key: CodingKey>: KeyedDecodingContainerProtoco
}
}

func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable {
func decode<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T {
if type == TOMLArray.self {
return try decode(TOMLArray.self, forKey: key) as! T
} else if type == TOMLTable.self {
Expand Down Expand Up @@ -95,7 +95,7 @@ struct TOMLKeyedDecodingContainer<Key: CodingKey>: KeyedDecodingContainerProtoco
return try T(from: decoder)
}

func nestedContainer<NestedKey>(keyedBy _: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey {
func nestedContainer<NestedKey: CodingKey>(keyedBy _: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> {
do {
let nestedTable = try table.table(forKey: key.stringValue)
var nestedCodingPath = codingPath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ extension _TOMLDecoder: SingleValueDecodingContainer {
}

@inline(__always)
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
func decode<T: Decodable>(_ type: T.Type) throws -> T {
if type == Bool.self {
return try decode(Bool.self) as! T
} else if type == String.self {
Expand Down
10 changes: 4 additions & 6 deletions Sources/TOMLDecoder/TOMLUnkeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ struct TOMLUnkeyedDecodingContainer: UnkeyedDecodingContainer {
}
}

mutating func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
mutating func decode<T: Decodable>(_ type: T.Type) throws -> T {
if type == TOMLArray.self {
return try decode(TOMLArray.self) as! T
} else if type == TOMLTable.self {
Expand All @@ -75,12 +75,10 @@ struct TOMLUnkeyedDecodingContainer: UnkeyedDecodingContainer {
// Try to get nested table or array
if let nestedTable = try? array.table(atIndex: currentIndex) {
let nestedDecoder = _TOMLDecoder(referencing: .keyed(nestedTable), at: nestedCodingPath, strategy: decoder.strategy, isLenient: decoder.isLenient)
let decoded = try T(from: nestedDecoder)
return decoded
return try T(from: nestedDecoder)
} else if let nestedArray = try? array.array(atIndex: currentIndex) {
let nestedDecoder = _TOMLDecoder(referencing: .unkeyed(nestedArray), at: nestedCodingPath, strategy: decoder.strategy, isLenient: decoder.isLenient)
let decoded = try T(from: nestedDecoder)
return decoded
return try T(from: nestedDecoder)
}

let token = try array.token(forIndex: currentIndex, type: String(describing: T.self))
Expand Down Expand Up @@ -111,7 +109,7 @@ struct TOMLUnkeyedDecodingContainer: UnkeyedDecodingContainer {
return try T(from: decoder)
}

mutating func nestedContainer<NestedKey>(keyedBy _: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey {
mutating func nestedContainer<NestedKey: CodingKey>(keyedBy _: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> {
guard !isAtEnd else {
throw DecodingError.valueNotFound(
KeyedDecodingContainer<NestedKey>.self,
Expand Down
2 changes: 1 addition & 1 deletion Sources/TOMLDecoder/_TOMLDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class _TOMLDecoder: Decoder {
}
}

func container<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
func container<Key: CodingKey>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
guard case let .keyed(table) = container else {
throw DecodingError.valueNotFound(
KeyedDecodingContainer<Key>.self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ extension _TOMLDecoder: SingleValueDecodingContainer {
}

@inline(__always)
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
func decode<T: Decodable>(_ type: T.Type) throws -> T {
% for i, type_name in enumerate([t[0] for t in native_types] + int_types + ["Double", "Float"]):
${"if" if i == 0 else "} else if"} type == ${type_name}.self {
return try decode(${type_name}.self) as! T
Expand Down
2 changes: 1 addition & 1 deletion Sources/compliance/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import Foundation
import TOMLDecoder

// iOS 13+ compatible date formatter functions
/// iOS 13+ compatible date formatter functions
private func createISO8601FullFormatter() -> ISO8601DateFormatter {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
Expand Down
Loading
Loading