# Parsing Rules When parsing any strings, data files (CSV, XML, JSON, Excel), or structured text, follow the **OpenAPIKit idiom** by [Matt Polzin](https://github.com/mattpolzin/OpenAPIKit). ## Reference Repository **https://github.com/mattpolzin/OpenAPIKit** Key files to study: - `Document.swift` - Root document structure, Codable implementation - `JSONSchema.swift` - Enum-based type representation - `JSONSchemaContext.swift` - Protocol-driven context pattern ## Core Principles ### 1. Protocol Before Shared Implementations When two formats/providers share behavior, or when the parser contract crosses a target boundary, define the shared contract before its concrete implementations. Do not introduce a protocol around one same-target parser with no boundary or second implementation; that is unearned abstraction. ```swift /// All parsed items share these properties protocol ParsedItemContext { var id: String { get } var rawValue: String { get } } /// Type-specific contexts extend the base protocol ProductContext: ParsedItemContext { var name: String { get } var price: Decimal { get } } ``` ### 2. Codable-Based Parsing Use Swift's built-in `Codable` with custom `init(from decoder:)`: ```swift struct Product: Decodable { let name: String let price: Decimal enum CodingKeys: String, CodingKey { case name = "NAZIV PROIZVODA" case price = "MALOPRODAJNA CIJENA" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) // Custom parsing for complex types let priceString = try container.decode(String.self, forKey: .price) price = try Self.parsePrice(priceString) } } ``` ### 3. Enum with Associated Values Represent variants using enums with associated values: ```swift enum ParsedDocument: Sendable { case csv(CSVDocument) case xml(XMLDocument) case json(JSONDocument) case excel(ExcelDocument) var unified: UnifiedDocument { switch self { case .csv(let doc): return doc.toUnified() case .xml(let doc): return doc.toUnified() case .json(let doc): return doc.toUnified() case .excel(let doc): return doc.toUnified() } } } ``` ### 4. Context Objects with CoreContext Pattern Separate core context from type-specific extensions: ```swift /// Core context all items have struct CoreContext: ParsedItemContext { let id: String let rawValue: String let format: Format let warnings: [ParseWarning] } /// Type-specific context struct PriceContext { let value: Decimal let currency: Currency let isSpecialOffer: Bool } ``` ### 5. Module Isolation per Format/Provider Separate modules for different data sources: ``` MyParser/ ├── Sources/ │ ├── ParserCore/ # Unified types, protocols │ ├── ParserCSV/ # CSV-specific parsing │ ├── ParserXML/ # XML-specific parsing │ ├── ParserJSON/ # JSON-specific parsing │ └── ParserCompat/ # Convert between formats ``` ### 6. Validation Separate from Parsing Parse first, validate second: ```swift struct ParsedItem { // ... properties func validate() throws -> [ValidationWarning] { var warnings: [ValidationWarning] = [] if id.isEmpty { warnings.append(.missingId) } if !isValidFormat { warnings.append(.invalidFormat(raw: rawValue)) } return warnings } } ``` The snippet above is the minimal shape. The **full, MANDATORY validation idiom** (composable `Validation` values, the combinator algebra, errors with a coding-path context, the default-plus-blank `Validator`, and the test style) lives in its own rule: see `universal/validation-rules.md`. A monolithic `func validate()` that appends to a `var warnings` through a tree of `if` statements is the anti-pattern that rule replaces; use it only as throwaway pseudocode, never as the shipped validator. ### 7. Builder Pattern for Transformations Immutable transformations with fluent API: ```swift extension CoreContext { func with(id newId: String) -> Self { var copy = self copy.id = newId return copy } func addingWarning(_ warning: ParseWarning) -> Self { var copy = self copy.warnings.append(warning) return copy } } ``` ### 8. Type-Safe References Use typed references for lookups: ```swift struct ItemReference: Hashable, Codable { let source: DataSource let id: String } extension Document { subscript(item ref: ItemReference) -> ParsedItem? { items.first { $0.source == ref.source && $0.id == ref.id } } } ``` ## Price/Number Parsing Helper Common price parsing that handles European formats: ```swift extension Decimal { /// Parse price string: "7,99", "7.99", "7,99€", "7.99 EUR" static func parsePrice(_ string: String) throws -> Decimal { var cleaned = string .replacingOccurrences(of: "€", with: "") .replacingOccurrences(of: "EUR", with: "") .trimmingCharacters(in: .whitespaces) // Handle European format (comma as decimal) if cleaned.contains(",") && cleaned.contains(".") { // "1.234,56" -> "1234.56" if cleaned.firstIndex(of: ",")! > cleaned.firstIndex(of: ".")! { cleaned = cleaned.replacingOccurrences(of: ".", with: "") } else { cleaned = cleaned.replacingOccurrences(of: ",", with: "") } } cleaned = cleaned.replacingOccurrences(of: ",", with: ".") guard let value = Decimal(string: cleaned) else { throw ParseError.invalidPrice(string) } return value } } ``` ## Text Encoding Handling Always handle multiple encodings: ```swift func decodeText(from data: Data) -> String? { let encodings: [String.Encoding] = [ .utf8, .windowsCP1250, // Croatian/Central European .isoLatin2, // ISO-8859-2 .isoLatin1 // Fallback ] for encoding in encodings { if let text = String(data: data, encoding: encoding) { return text } } return nil } ``` ## DO - Define protocols for shared parsing behavior - Use Codable with custom `init(from decoder:)` - Separate parsing from validation - Use enums with associated values for variants - Create type-safe references - Handle multiple text encodings - Use builder pattern for transformations ## DO NOT - Parse and validate in the same step - Use raw string manipulation without protocols - Ignore text encoding issues - Create tightly-coupled parsers - Skip the validation layer - Use force unwrapping in parsers ## Acceptance check A parser conforms when parsing and validation are separate entry points; malformed, empty, boundary, alternate-encoding, and unknown-field fixtures exercise both success and failure paths; no production parser uses force unwraps or silently drops input; and the parser test suite passes with its case count cited. For a bidirectional format, also run the corpus and awkward-value round trips required by `core/round-trip-transformation.md`.