PureXML/third_party/rules-swift/parsing-rules.md

7.2 KiB

Parsing Rules

When parsing any strings, data files (CSV, XML, JSON, Excel), or structured text, follow the OpenAPIKit idiom by Matt Polzin.

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.

/// 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:):

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:

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:

/// Core context all items have
struct CoreContext<Format: DataFormat>: 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:

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<Subject> 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:

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:

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:

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:

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.