Skip to content

Commit ce7f3ba

Browse files
committed
Refactor TOONDecoder: auto-detect indent, automatic path expansion
- Remove configurable indent property; auto-detect from first indented line - Add .automatic path expansion mode as new default (falls back on collision) - Consolidate specVersion into top-level toonSpecVersion constant - Remove separate TOONEncoder/TOONDecoder library exports from Package.swift - Reduce default maxDepth from 128 to 32 for stack safety - Simplify README with unified quick start example
1 parent f521e12 commit ce7f3ba

7 files changed

Lines changed: 166 additions & 139 deletions

File tree

Package.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,6 @@ let package = Package(
1717
name: "ToonFormat",
1818
targets: ["ToonFormat"]
1919
),
20-
.library(
21-
name: "TOONEncoder",
22-
targets: ["TOONEncoder"]
23-
),
24-
.library(
25-
name: "TOONDecoder",
26-
targets: ["TOONDecoder"]
27-
),
2820
],
2921
targets: [
3022
.target(

README.md

Lines changed: 15 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,12 @@ dependencies: [
9090
Then add the dependency to your target:
9191

9292
```swift
93-
// For both encoder and decoder:
9493
.target(name: "YourTarget", dependencies: ["ToonFormat"])
95-
96-
// Or individually:
97-
.target(name: "YourTarget", dependencies: ["TOONEncoder"])
98-
.target(name: "YourTarget", dependencies: ["TOONDecoder"])
9994
```
10095

10196
## Usage
10297

103-
### Encoding
104-
105-
#### Quick Start
98+
### Quick Start
10699

107100
```swift
108101
import ToonFormat
@@ -114,6 +107,7 @@ struct User: Codable {
114107
let active: Bool
115108
}
116109

110+
// Encoding
117111
let user = User(
118112
id: 123,
119113
name: "Ada",
@@ -124,15 +118,15 @@ let user = User(
124118
let encoder = TOONEncoder()
125119
let data = try encoder.encode(user)
126120
print(String(data: data, encoding: .utf8)!)
127-
```
128-
129-
Output:
121+
// id: 123
122+
// name: Ada
123+
// tags[2]: reading,gaming
124+
// active: true
130125

131-
```
132-
id: 123
133-
name: Ada
134-
tags[2]: reading,gaming
135-
active: true
126+
// Decoding
127+
let decoder = TOONDecoder()
128+
let decoded = try decoder.decode(User.self, from: data)
129+
print(decoded.name) // "Ada"
136130
```
137131

138132
#### Custom Delimiters
@@ -293,84 +287,7 @@ database.connection:
293287
port: 5432
294288
```
295289

296-
### Decoding
297-
298-
#### Basic Decoding
299-
300-
```swift
301-
import TOONDecoder
302-
303-
struct User: Codable {
304-
let id: Int
305-
let name: String
306-
let tags: [String]
307-
let active: Bool
308-
}
309-
310-
let toon = """
311-
id: 123
312-
name: Ada
313-
tags[2]: reading,gaming
314-
active: true
315-
"""
316-
317-
let decoder = TOONDecoder()
318-
let user = try decoder.decode(User.self, from: Data(toon.utf8))
319-
print(user.name) // "Ada"
320-
```
321-
322-
#### Tabular Format
323-
324-
```swift
325-
struct Item: Codable {
326-
let sku: String
327-
let qty: Int
328-
let price: Double
329-
}
330-
331-
struct Order: Codable {
332-
let items: [Item]
333-
}
334-
335-
let toon = """
336-
items[2]{sku,qty,price}:
337-
A1,2,9.99
338-
B2,1,14.5
339-
"""
340-
341-
let decoder = TOONDecoder()
342-
let order = try decoder.decode(Order.self, from: Data(toon.utf8))
343-
print(order.items.count) // 2
344-
```
345-
346-
#### Path Expansion
347-
348-
Path expansion unfolds dotted keys into nested objects — the inverse of TOONEncoder's key folding:
349-
350-
```swift
351-
struct Config: Codable {
352-
struct Database: Codable {
353-
struct Connection: Codable {
354-
let host: String
355-
let port: Int
356-
}
357-
let connection: Connection
358-
}
359-
let database: Database
360-
}
361-
362-
let toon = """
363-
database.connection.host: localhost
364-
database.connection.port: 5432
365-
"""
366-
367-
let decoder = TOONDecoder()
368-
decoder.expandPaths = .safe
369-
let config = try decoder.decode(Config.self, from: Data(toon.utf8))
370-
print(config.database.connection.host) // "localhost"
371-
```
372-
373-
#### Decoding Limits
290+
### Decoding Limits
374291

375292
Protect against malicious or malformed input:
376293

@@ -389,8 +306,7 @@ decoder.limits = TOONDecoder.DecodingLimits(
389306
Check the supported TOON specification version:
390307

391308
```swift
392-
print(TOONEncoder.specVersion) // "3.0"
393-
print(TOONDecoder.specVersion) // "3.0"
309+
print(toonSpecVersion) // "3.0"
394310
```
395311

396312
## Contributing
@@ -414,9 +330,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
414330

415331
## Documentation
416332

417-
- [TOON Spec](https://github.com/toon-format/spec) - Official specification
418-
- [Issues](https://github.com/toon-format/toon-swift/issues) - Bug reports and features
419-
- [Contributing](CONTRIBUTING.md) - Contribution guidelines
333+
- [📜 TOON Spec](https://github.com/toon-format/spec) - Official specification
334+
- [🐛 Issues](https://github.com/toon-format/toon-swift/issues) - Bug reports and features
335+
- [🤝 Contributing](CONTRIBUTING.md) - Contribution guidelines
420336

421337
## License
422338

Sources/TOONDecoder/TOONDecoder.swift

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,12 @@ import Foundation
55
/// This decoder conforms to the TOON (Token-Oriented Object Notation) specification version 3.0.
66
/// For more information, see: https://github.com/toon-format/spec
77
public final class TOONDecoder {
8-
/// The TOON specification version this decoder conforms to
9-
public static let specVersion = "3.0"
10-
11-
/// Number of spaces per indentation level
12-
public var indent: Int = 2
13-
148
/// Path expansion mode for dotted keys
159
///
1610
/// When enabled, dotted keys like `a.b.c: value` are expanded to nested objects.
1711
/// This is the inverse operation of TOONEncoder's keyFolding option.
1812
///
19-
/// Example with `.safe`:
13+
/// Example with `.safe` or `.automatic`:
2014
/// ```toon
2115
/// user.profile.name: John
2216
/// ```
@@ -26,7 +20,7 @@ public final class TOONDecoder {
2620
/// profile:
2721
/// name: John
2822
/// ```
29-
public var expandPaths: PathExpansion = .disabled
23+
public var expandPaths: PathExpansion = .automatic
3024

3125
/// Limits for decoding to prevent resource exhaustion
3226
///
@@ -35,36 +29,59 @@ public final class TOONDecoder {
3529

3630
/// Path expansion mode
3731
public enum PathExpansion: Hashable, Sendable {
32+
/// Automatic path expansion - expands dotted keys when they match the target type structure
33+
/// Falls back gracefully if expansion causes conflicts
34+
case automatic
35+
3836
/// No path expansion - dotted keys decoded as literal strings
3937
case disabled
4038

4139
/// Safe path expansion: expand dotted keys to nested objects with collision detection
40+
/// Throws an error on collision
4241
case safe
4342
}
4443

4544
/// Limits for decoding to prevent resource exhaustion
4645
public struct DecodingLimits: Hashable, Sendable {
47-
/// Maximum input size in bytes (default: 10 MB)
46+
/// Maximum input size in bytes
4847
public var maxInputSize: Int
4948

50-
/// Maximum nesting depth (default: 128)
49+
/// Maximum nesting depth
5150
public var maxDepth: Int
5251

53-
/// Maximum number of keys in a single object (default: 10,000)
52+
/// Maximum number of keys in a single object
5453
public var maxObjectKeys: Int
5554

56-
/// Maximum array length (default: 100,000)
55+
/// Maximum array length
5756
public var maxArrayLength: Int
5857

5958
/// Default limits suitable for most use cases
59+
///
60+
/// - `maxInputSize`: 10 MB
61+
/// - `maxDepth`: 32 (prevents stack overflow from deep nesting)
62+
/// - `maxObjectKeys`: 10,000
63+
/// - `maxArrayLength`: 100,000
6064
public static let `default` = DecodingLimits(
6165
maxInputSize: 10 * 1024 * 1024,
62-
maxDepth: 128,
63-
maxObjectKeys: 10000,
66+
maxDepth: 32,
67+
maxObjectKeys: 10_000,
6468
maxArrayLength: 100_000
6569
)
6670

67-
/// No limits - use with caution on trusted input only
71+
/// Decoding limits that impose no restrictions.
72+
///
73+
/// - Warning: This configuration is unsafe for untrusted input and
74+
/// should only be used with data from trusted sources.
75+
/// Without limits, malicious input can cause excessive memory usage,
76+
/// stack overflow from deep nesting, or denial of service attacks.
77+
///
78+
/// Use this only when you have full control over the input data and
79+
/// need to decode arbitrarily large or complex TOON structures.
80+
///
81+
/// For production use with external input, use
82+
/// ``default`` or
83+
/// ``init(maxInputSize:maxDepth:maxObjectKeys:maxArrayLength:)``
84+
/// with appropriate limits instead.
6885
public static let unlimited = DecodingLimits(
6986
maxInputSize: .max,
7087
maxDepth: .max,
@@ -83,8 +100,8 @@ public final class TOONDecoder {
83100
/// Creates a new TOON decoder with default configuration
84101
///
85102
/// Default settings:
86-
/// - `indent`: 2 spaces
87-
/// - `expandPaths`: `.disabled`
103+
/// - `expandPaths`: `.automatic`
104+
/// - `limits`: `.default`
88105
public init() {}
89106

90107
/// Decodes TOON format data to the specified type
@@ -103,7 +120,7 @@ public final class TOONDecoder {
103120
throw TOONDecodingError.invalidFormat("Data is not valid UTF-8")
104121
}
105122

106-
let parser = Parser(text: text, indentSize: indent, expandPaths: expandPaths, limits: limits)
123+
let parser = Parser(text: text, expandPaths: expandPaths, limits: limits)
107124
let value = try parser.parse()
108125

109126
let decoder = _Decoder(value: value, codingPath: [], userInfo: [:])
@@ -227,17 +244,17 @@ private enum Value: Equatable {
227244

228245
private final class Parser {
229246
private let lines: [String]
230-
private let indentSize: Int
247+
private var indentSize: Int = 2
231248
private let expandPaths: TOONDecoder.PathExpansion
232249
private let limits: TOONDecoder.DecodingLimits
233250
private var currentLine: Int = 0
251+
private var indentDetected: Bool = false
234252

235-
init(text: String, indentSize: Int, expandPaths: TOONDecoder.PathExpansion, limits: TOONDecoder.DecodingLimits) {
253+
init(text: String, expandPaths: TOONDecoder.PathExpansion, limits: TOONDecoder.DecodingLimits) {
236254
// Split by LF, handling potential CR+LF
237255
lines = text.replacingOccurrences(of: "\r\n", with: "\n")
238256
.split(separator: "\n", omittingEmptySubsequences: false)
239257
.map(String.init)
240-
self.indentSize = indentSize
241258
self.expandPaths = expandPaths
242259
self.limits = limits
243260
}
@@ -322,8 +339,14 @@ private final class Parser {
322339
index = line.index(after: index)
323340
}
324341

325-
// Validate indentation is multiple of indent size
326-
let depth = spaces / indentSize
342+
// Auto-detect indent size from first indented line
343+
if spaces > 0 && !indentDetected {
344+
indentSize = spaces
345+
indentDetected = true
346+
}
347+
348+
// Calculate depth based on detected or default indent size
349+
let depth = indentSize > 0 ? spaces / indentSize : 0
327350
return (depth, line[index...])
328351
}
329352

@@ -384,8 +407,20 @@ private final class Parser {
384407
let (key, value) = try parseKeyValuePair(String(content), atDepth: depth)
385408

386409
// Handle path expansion if enabled
387-
if expandPaths == .safe && key.contains(".") && key.isValidDottedPath {
388-
try expandDottedKey(key, value: value, into: &values, keyOrder: &keyOrder)
410+
if (expandPaths == .safe || expandPaths == .automatic) && key.contains(".") && key.isValidDottedPath {
411+
do {
412+
try expandDottedKey(key, value: value, into: &values, keyOrder: &keyOrder)
413+
} catch {
414+
// For .automatic mode, fall back to literal key on collision
415+
if expandPaths == .automatic {
416+
if !keyOrder.contains(key) {
417+
keyOrder.append(key)
418+
}
419+
values[key] = value
420+
} else {
421+
throw error
422+
}
423+
}
389424
} else {
390425
if !keyOrder.contains(key) {
391426
keyOrder.append(key)

Sources/TOONEncoder/TOONEncoder.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ import Foundation
66
/// For more information, see: https://github.com/toon-format/spec
77
public final class TOONEncoder {
88

9-
/// The TOON specification version this encoder conforms to
10-
public static let specVersion = "3.0"
11-
129
/// Number of spaces per indentation level
1310
public var indent: Int = 2
1411

Sources/ToonFormat/ToonFormat.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@
33

44
@_exported import TOONDecoder
55
@_exported import TOONEncoder
6+
7+
/// The TOON specification version supported by this library
8+
public let toonSpecVersion = "3.0"

0 commit comments

Comments
 (0)