[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [mattt]\ncustom: https://flight.school/books/strings\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    branches: [master]\n\njobs:\n  macos:\n    runs-on: macos-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v1\n      - name: Build and Test\n        run: swift test\n\n  linux:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        swift: [\"5.1\", \"5.2\", \"latest\"]\n\n    container:\n      image: swift:${{ matrix.swift }}\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v1\n      - name: Build and Test\n        run: swift test --enable-test-discovery\n"
  },
  {
    "path": ".github/workflows/documentation.yml",
    "content": "name: Documentation\n\non:\n  push:\n    branches:\n      - master\n    paths:\n      - .github/workflows/documentation.yml\n      - Sources/**.swift\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v1\n      - name: Generate Documentation\n        uses: SwiftDocOrg/swift-doc@master\n        with:\n          inputs: \"Sources/SwiftDoc\"\n          output: \"Documentation\"\n      - name: Upload Documentation to Wiki\n        uses: SwiftDocOrg/github-wiki-publish-action@v1\n        with:\n          path: \"Documentation\"\n        env:\n          GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n/.build\n/Packages\n/*.xcodeproj\n"
  },
  {
    "path": "LICENSE.md",
    "content": "Copyright 2019 Read Evaluate Press, LLC\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the \"Software\"),\nto deal in the Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "Package.swift",
    "content": "// swift-tools-version:5.0\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"RegularExpressionDecoder\",\n    products: [\n        // Products define the executables and libraries produced by a package, and make them visible to other packages.\n        .library(\n            name: \"RegularExpressionDecoder\",\n            targets: [\"RegularExpressionDecoder\"]),\n    ],\n    dependencies: [\n        // Dependencies declare other packages that this package depends on.\n        // .package(url: /* package url */, from: \"1.0.0\"),\n    ],\n    targets: [\n        // Targets are the basic building blocks of a package. A target can define a module or a test suite.\n        // Targets can depend on other targets in this package, and on products in packages which this package depends on.\n        .target(\n            name: \"RegularExpressionDecoder\",\n            dependencies: [],\n            path: \"Sources\"\n        ),\n        .testTarget(\n            name: \"RegularExpressionDecoderTests\",\n            dependencies: [\"RegularExpressionDecoder\"],\n            path: \"Tests\"\n        ),\n    ]\n)\n"
  },
  {
    "path": "README.md",
    "content": "# Regular Expression Decoder\n\n[![Build Status][build status badge]][build status]\n[![License][license badge]][license]\n[![Swift Version][swift version badge]][swift version]\n\nA decoder that constructs objects from regular expression matches.\n\n---\n\nFor more information about creating your own custom decoders,\nconsult Chapter 7 of the\n[Flight School Guide to Swift Codable](https://flight.school/books/codable).\nFor more information about using regular expressions in Swift,\ncheck out Chapter 6 of the\n[Flight School Guide to Swift Strings](https://flight.school/books/strings).\n\n## Requirements\n\n- Swift 5+\n- iOS 11+ or macOS 10.13+\n\n## Usage\n\n```swift\nimport RegularExpressionDecoder\n\nlet ticker = \"\"\"\nAAPL 170.69▲0.51\nGOOG 1122.57▲2.41\nAMZN 1621.48▼18.52\nMSFT 106.57=0.00\nSWIFT 5.0.0▲1.0.0\n\"\"\"\n\nlet pattern: RegularExpressionPattern<Stock, Stock.CodingKeys> = #\"\"\"\n\\b\n(?<\\#(.symbol)>[A-Z]{1,4}) \\s+\n(?<\\#(.price)>\\d{1,}\\.\\d{2}) \\s*\n(?<\\#(.sign)>([▲▼=])\n(?<\\#(.change)>\\d{1,}\\.\\d{2})\n\\b\n\"\"\"#\n\nlet decoder = try RegularExpressionDecoder<Stock>(\n                    pattern: pattern,\n                    options: .allowCommentsAndWhitespace\n                  )\n\ntry decoder.decode([Stock].self, from: ticker)\n// Decodes [AAPL, GOOG, AMZN, MSFT] (but not SWIFT, which is invalid)\n```\n\n## Explanation\n\nLet's say that you're building an app that parses stock quotes\nfrom a text-based stream of price changes.\n\n```swift\nlet ticker = \"\"\"\nAAPL 170.69▲0.51\nGOOG 1122.57▲2.41\nAMZN 1621.48▼18.52\nMSFT 106.57=0.00\n\"\"\"\n```\n\nEach stock is represented by the following structure:\n\n- The **symbol**, consisting of 1 to 4 uppercase letters, followed by a space\n- The **price**, formatted as a number with 2 decimal places\n- A **sign**, indicating a price gain (`▲`), loss (`▼`), or no change (`=`)\n- The **magnitude** of the gain or loss, formatted the same as the price\n\nThese format constraints lend themselves naturally\nto representation by a <dfn>regular expression</dfn>,\nsuch as:\n\n```perl\n/\\b[A-Z]{1,4} \\d{1,}\\.\\d{2}[▲▼=]\\d{1,}\\.\\d{2}\\b/\n```\n\n> Note:\n> The `\\b` metacharacter anchors matches to word boundaries.\n\nThis regular expression can distinguish between\nvalid and invalid stock quotes.\n\n```swift\n\"AAPL 170.69▲0.51\" // valid\n\"SWIFT 5.0.0▲1.0.0\" // invalid\n```\n\nHowever, to extract individual components from a quote\n(symbol, price, etc.)\nthe regular expression must contain <dfn>capture groups</dfn>,\nof which there are two varieties:\n<dfn>positional capture groups</dfn> and\n<dfn>named capture groups</dfn>.\n\nPositional capture groups are demarcated in the pattern\nby enclosing parentheses (`(___)`).\nWith some slight modifications,\nwe can make original regular expression capture each part of the stock quote:\n\n```perl\n/\\b([A-Z]{1,4}) (\\d{1,}\\.\\d{2})([▲▼=])(\\d{1,}\\.\\d{2})\\b/\n```\n\nWhen matched,\nthe symbol can be accessed by the first capture group,\nthe price by the second,\nand so on.\n\nFor large numbers of capture groups ---\nespecially in patterns with nested groups ---\none can easily lose track of which parts correspond to which positions.\nSo another approach is to assign names to capture groups,\nwhich are denoted by the syntax `(?<NAME>___)`.\n\n```perl\n/\\b\n(?<symbol>[A-Z]{1,4}) \\s+\n(?<price>\\d{1,}\\.\\d{2}) \\s*\n(?<sign>([▲▼=])\n(?<change>\\d{1,}\\.\\d{2})\n\\b/\n```\n\n> Note:\n> Most regular expression engines ---\n> including the one used by `NSRegularExpression` ---\n> provide a mode to ignore whitespace;\n> this lets you segment long patterns over multiple lines,\n> making them easier to read and understand.\n\nTheoretically, this approach allows you to access each group by name\nfor each match of the regular expression.\nIn practice, doing this in Swift can be inconvenient,\nas it requires you to interact with cumbersome `NSRegularExpression` APIs\nand somehow incorporate it into your model layer.\n\n`RegularExpressionDecoder` provides a convenient solution\nto constructing `Decodable` objects from regular expression matches\nby automatically matching coding keys to capture group names.\nAnd it can do so safely,\nthanks to the new `ExpressibleByStringInterpolation` protocol in Swift 5.\n\nTo understand how,\nlet's start by considering the following `Stock` model,\nwhich adopts the `Decodable` protocol:\n\n```swift\nstruct Stock: Decodable {\n    let symbol: String\n    var price: Double\n\n    enum Sign: String, Decodable {\n        case gain = \"▲\"\n        case unchanged = \"=\"\n        case loss = \"▼\"\n    }\n\n    private var sign: Sign\n    private var change: Double = 0.0\n    var movement: Double {\n        switch sign {\n        case .gain: return +change\n        case .unchanged: return 0.0\n        case .loss: return -change\n        }\n    }\n}\n```\n\nSo far, so good.\n\nNow, normally, the Swift compiler\nautomatically synthesizes conformance to `Decodable`,\nincluding a nested `CodingKeys` type.\nBut in order to make this next part work correctly,\nwe'll have to do this ourselves:\n\n```swift\nextension Stock {\n    enum CodingKeys: String, CodingKey {\n        case symbol\n        case price\n        case sign\n        case change\n    }\n}\n```\n\nHere's where things get really interesting:\nremember our regular expression with named capture patterns from before?\n_We can replace the hard-coded names\nwith interpolations of the `Stock` type's coding keys._\n\n```swift\nimport RegularExpressionDecoder\n\nlet pattern: RegularExpressionPattern<Stock, Stock.CodingKeys> = #\"\"\"\n\\b\n(?<\\#(.symbol)>[A-Z]{1,4}) \\s+\n(?<\\#(.price)>\\d{1,}\\.\\d{2}) \\s*\n(?<\\#(.sign)>[▲▼=])\n(?<\\#(.change)>\\d{1,}\\.\\d{2})\n\\b\n\"\"\"#\n```\n\n> Note:\n> This example benefits greatly from another new feature in Swift 5:\n> <dfn>raw string literals</dfn>.\n> Those octothorps (`#`) at the start and end\n> tell the compiler to ignore escape characters (`\\`)\n> unless they also include an octothorp (`\\#( )`).\n> Using raw string literals,\n> we can write regular expression metacharacters like `\\b`, `\\d`, and `\\s`\n> without double escaping them (i.e. `\\\\b`).\n\nThanks to `ExpressibleByStringInterpolation`,\nwe can restrict interpolation segments to only accept those coding keys,\nthereby ensuring a direct 1:1 match between capture groups\nand their decoded properties.\nAnd not only that ---\nthis approach lets us to verify that keys have valid regex-friendly names\nand aren't captured more than once.\nIt's enormously powerful,\nallowing code to be incredibly expressive\nwithout compromising safety or performance.\n\nWhen all is said and done,\n`RegularExpressionDecoder` lets you decode types\nfrom a string according to a regular expression pattern\nmuch the same as you might from JSON or a property list\nusing a decoder:\n\n```swift\nlet decoder = try RegularExpressionDecoder<Stock>(\n                        pattern: pattern,\n                        options: .allowCommentsAndWhitespace\n                  )\n\ntry decoder.decode([Stock].self, from: ticker)\n// Decodes [AAPL, GOOG, AMZN, MSFT]\n```\n\n## License\n\nMIT\n\n## Contact\n\nMattt ([@mattt](https://twitter.com/mattt))\n\n[build status]: https://github.com/Flight-School/RegularExpressionDecoder/actions?query=workflow%3ACI\n[build status badge]: https://github.com/Flight-School/RegularExpressionDecoder/workflows/CI/badge.svg\n[license]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat\n[license badge]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat\n[swift version]: https://swift.org/download/\n[swift version badge]: http://img.shields.io/badge/swift%20version-5.0-orange.svg?style=flat\n"
  },
  {
    "path": "RegularExpressionDecoder.playground/Contents.swift",
    "content": "import RegularExpressionDecoder\n\nlet ticker = \"\"\"\nAAPL 170.69▲0.51\nGOOG 1122.57▲2.41\nAMZN 1621.48▼18.52\nMSFT 106.57=0.00\nSWIFT 5.0.0▲1.0.0\n\"\"\"\n\nlet pattern: RegularExpressionPattern<Stock, Stock.CodingKeys> = #\"\"\"\n\\b\n(?<\\#(.symbol)>[A-Z]{1,4}) \\s+\n(?<\\#(.price)>\\d{1,}\\.\\d{2}) \\s*\n(?<\\#(.sign)>([▲▼](?!0\\.00))|(=(?=0\\.00)))\n(?<\\#(.change)>\\d{1,}\\.\\d{2})\n\\b\n\"\"\"#\n\nlet decoder = try RegularExpressionDecoder<Stock>(pattern: pattern, options: .allowCommentsAndWhitespace)\n\ntry decoder.decode([Stock].self, from: ticker)\n"
  },
  {
    "path": "RegularExpressionDecoder.playground/Sources/Stock.swift",
    "content": "public struct Stock: Decodable {\n    public let symbol: String\n    public var price: Double\n    \n    enum Sign: String, Decodable {\n        case gain = \"▲\"\n        case unchanged = \"=\"\n        case loss = \"▼\"\n    }\n    \n    private var sign: Sign\n    private var change: Double = 0.0\n    public var movement: Double {\n        switch sign {\n        case .gain: return +change\n        case .unchanged: return 0.0\n        case .loss: return -change\n        }\n    }\n\n    public enum CodingKeys: String, CodingKey {\n        case symbol\n        case price\n        case sign\n        case change\n    }\n}\n\nextension Stock.Sign: CustomStringConvertible {\n    var description: String {\n        return self.rawValue\n    }\n}\n\nextension Stock: CustomStringConvertible {\n    public var description: String {\n        return \"\\(self.symbol) \\(self.price)\\(self.sign)\\(self.change)\"\n    }\n}\n"
  },
  {
    "path": "RegularExpressionDecoder.playground/contents.xcplayground",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<playground version='5.0' target-platform='macos' executeOnSourceChanges='false'>\n    <timeline fileName='timeline.xctimeline'/>\n</playground>"
  },
  {
    "path": "RegularExpressionDecoder.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:RegularExpressionDecoder.playground\">\n   </FileRef>\n   <FileRef\n      location = \"group:RegularExpressionDecoder.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "RegularExpressionDecoder.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "Sources/RegularExpressionDecoder/KeyedDecodingContainer.swift",
    "content": "import Foundation\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension NSTextCheckingResult {\n    func range<Key>(for key: Key) -> NSRange? where Key: CodingKey {\n        if let position = key.intValue {\n            return self.range(at: position)\n        } else {\n            return self.range(withName: key.stringValue)\n        }\n    }\n}\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension _RegularExpressionDecoder {\n    final class KeyedContainer<Key> where Key: CodingKey {\n        let string: String\n        let match: NSTextCheckingResult?\n        var codingPath: [CodingKey]\n        var userInfo: [CodingUserInfoKey: Any]\n\n        init(string: String, match: NSTextCheckingResult?, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {\n            self.string = string\n            self.match = match\n            self.codingPath = codingPath\n            self.userInfo = userInfo\n        }\n\n        func range(for key: Key) -> Range<String.Index>? {\n            guard let nsrange = self.match?.range(for: key),\n                nsrange.location != NSNotFound\n                else {\n                    return nil\n            }\n\n            return Range(nsrange, in: self.string)\n        }\n\n        func string(for key: Key) -> String? {\n            guard let range = self.range(for: key) else {\n                return nil\n            }\n\n            return String(self.string[range])\n        }\n    }\n}\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension _RegularExpressionDecoder.KeyedContainer: KeyedDecodingContainerProtocol {\n    var allKeys: [Key] {\n        guard let captureGroupNames = self.userInfo[._captureGroupNames] as? [String] else {\n            return []\n        }\n\n        return captureGroupNames.compactMap { Key(stringValue: $0) }\n    }\n\n    func contains(_ key: Key) -> Bool {\n        return self.range(for: key) != nil\n    }\n\n    func decodeNil(forKey key: Key) throws -> Bool {\n        return self.match == nil || !contains(key)\n    }\n\n    func decode(_ type: String.Type, forKey key: Key) throws -> String {\n        guard let string = self.string(for: key) else {\n            let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: \"String: \\(self.string)\")\n            throw DecodingError.keyNotFound(key, context)\n        }\n\n        return string\n    }\n\n    func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable, T: LosslessStringConvertible {\n        let string = try self.decode(String.self, forKey: key)\n\n        guard let value = T(string) else {\n            let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: \"String: \\(self.string)\")\n            throw DecodingError.typeMismatch(type, context)\n        }\n\n        return value\n    }\n\n    func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {\n        let string = try self.decode(String.self, forKey: key)\n\n        let decoder = _RegularExpressionDecoder(string: string, matches: [self.match].compactMap {$0})\n        let value = try T(from: decoder)\n\n        return value\n    }\n\n    func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {\n        fatalError(\"Unimplemented\")\n    }\n\n    func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {\n        fatalError(\"Unimplemented\")\n    }\n\n    func superDecoder() throws -> Decoder {\n        fatalError(\"Unimplemented\")\n    }\n\n    func superDecoder(forKey key: Key) throws -> Decoder {\n        fatalError(\"Unimplemented\")\n    }\n}\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension _RegularExpressionDecoder.KeyedContainer: RegularExpressionDecodingContainer {}\n"
  },
  {
    "path": "Sources/RegularExpressionDecoder/RegularExpressionDecoder.swift",
    "content": "import Foundation\n\n/**\n\n */\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nfinal public class RegularExpressionDecoder<T: Decodable> {\n    private(set) var regularExpression: NSRegularExpression\n    var captureGroupNames: [String]?\n\n    public enum Error: Swift.Error {\n        case noMatches\n        case tooManyMatches\n    }\n\n    public init<CodingKeys>(pattern: RegularExpressionPattern<T, CodingKeys>, options: NSRegularExpression.Options = []) throws {\n        self.regularExpression = try NSRegularExpression(pattern: pattern.description, options: options)\n        self.captureGroupNames = pattern.captures?.map { $0.stringValue }\n    }\n\n    public func decode(_ type: T.Type, from string: String, options: NSRegularExpression.MatchingOptions = []) throws -> T {\n        let range = NSRange(string.startIndex..<string.endIndex, in: string)\n        let matches = self.regularExpression.matches(in: string, options: options, range: range)\n\n        switch matches.count {\n        case 0:\n            throw Error.noMatches\n        case 1:\n            let decoder = _RegularExpressionDecoder(string: string, matches: matches)\n            decoder.userInfo[._captureGroupNames] = self.captureGroupNames\n\n            return try T(from: decoder)\n        default:\n            throw Error.tooManyMatches\n        }\n    }\n\n    public func decode(_ type: [T].Type, from string: String, options: NSRegularExpression.MatchingOptions = []) throws -> [T] {\n        let range = NSRange(string.startIndex..<string.endIndex, in: string)\n        let matches = self.regularExpression.matches(in: string, options: options, range: range)\n\n        switch matches.count {\n        case 0:\n            return []\n        default:\n            let decoder = _RegularExpressionDecoder(string: string, matches: matches)\n            decoder.userInfo[._captureGroupNames] = self.captureGroupNames\n\n            return try [T](from: decoder)\n        }\n    }\n}\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\n// swiftlint:disable:next type_name\nfinal class _RegularExpressionDecoder {\n    var codingPath: [CodingKey] = []\n\n    var userInfo: [CodingUserInfoKey: Any] = [:]\n\n    var container: RegularExpressionDecodingContainer?\n    fileprivate var string: String\n    fileprivate var matches: [NSTextCheckingResult]\n\n    init(string: String, matches: [NSTextCheckingResult]) {\n        self.string = string\n        self.matches = matches\n    }\n}\n\nextension CodingUserInfoKey {\n    // swiftlint:disable:next identifier_name\n    static let _captureGroupNames = CodingUserInfoKey(rawValue: \"captureGroupNames\")!\n}\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension _RegularExpressionDecoder: Decoder {\n    fileprivate func assertCanCreateContainer() {\n        precondition(self.container == nil)\n    }\n\n    func singleValueContainer() -> SingleValueDecodingContainer {\n        assertCanCreateContainer()\n\n        let container = SingleValueContainer(string: self.string, match: self.matches.first, codingPath: self.codingPath, userInfo: self.userInfo)\n        self.container = container\n\n        return container\n    }\n\n    func container<Key>(keyedBy type: Key.Type) -> KeyedDecodingContainer<Key> where Key : CodingKey {\n        assertCanCreateContainer()\n\n        let container = KeyedContainer<Key>(string: self.string, match: self.matches.first, codingPath: self.codingPath, userInfo: self.userInfo)\n        self.container = container\n\n        return KeyedDecodingContainer(container)\n    }\n\n    func unkeyedContainer() -> UnkeyedDecodingContainer {\n        assertCanCreateContainer()\n\n        let container = UnkeyedContainer(string: self.string, matches: self.matches, codingPath: self.codingPath, userInfo: self.userInfo)\n        self.container = container\n\n        return container\n    }\n}\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nprotocol RegularExpressionDecodingContainer: class {}\n"
  },
  {
    "path": "Sources/RegularExpressionDecoder/RegularExpressionPattern.swift",
    "content": "public struct RegularExpressionPattern<T, CodingKeys>: LosslessStringConvertible, ExpressibleByStringInterpolation where T: Decodable, CodingKeys: CodingKey & Hashable {\n    public var description: String\n    public var captures: Set<CodingKeys>?\n\n    public init?(_ description: String) {\n        self.description = description\n    }\n\n    public init(stringLiteral value: String) {\n        self.init(value)!\n    }\n\n    public init(stringInterpolation: StringInterpolation) {\n        self.init(stringInterpolation.string)!\n        self.captures = stringInterpolation.captures\n    }\n\n    public struct StringInterpolation: StringInterpolationProtocol {\n        var string: String = \"\"\n        var captures: Set<CodingKeys> = []\n\n        public init(literalCapacity: Int, interpolationCount: Int) {\n            self.string.reserveCapacity(literalCapacity)\n        }\n\n        public mutating func appendLiteral(_ literal: String) {\n            self.string.append(literal)\n        }\n\n        public mutating func appendInterpolation(_ key: CodingKeys) {\n            precondition(!self.captures.contains(key), \"\\(key) already captured\")\n            precondition(!key.stringValue.contains { !$0.isLetter }, \"invalid capture name \\(key.stringValue)\")\n            self.string.append(key.stringValue)\n            self.captures.insert(key)\n        }\n    }\n}\n"
  },
  {
    "path": "Sources/RegularExpressionDecoder/SingleValueDecodingContainer.swift",
    "content": "import Foundation\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension _RegularExpressionDecoder {\n    final class SingleValueContainer {\n        let string: String\n        let match: NSTextCheckingResult?\n        var codingPath: [CodingKey]\n        var userInfo: [CodingUserInfoKey: Any]\n\n        init(string: String, match: NSTextCheckingResult?, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {\n            self.string = string\n            self.match = match\n            self.codingPath = codingPath\n            self.userInfo = userInfo\n        }\n    }\n}\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension _RegularExpressionDecoder.SingleValueContainer: SingleValueDecodingContainer {\n    func decodeNil() -> Bool {\n        return self.match == nil\n    }\n\n    func decode<T>(_ type: T.Type) throws -> T where T : Decodable {\n        let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: \"Cannot decode type \\(type)\")\n        throw DecodingError.typeMismatch(type, context)\n    }\n\n    func decode<T>(_ type: T.Type) throws -> T where T: Decodable, T: LosslessStringConvertible {\n        guard let value = T(self.string) else {\n            let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: \"Cannot decode type \\(type)\")\n            throw DecodingError.typeMismatch(type, context)\n        }\n        return value\n    }\n}\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension _RegularExpressionDecoder.SingleValueContainer: RegularExpressionDecodingContainer {}\n"
  },
  {
    "path": "Sources/RegularExpressionDecoder/UnkeyedDecodingContainer.swift",
    "content": "import Foundation\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension _RegularExpressionDecoder {\n    final class UnkeyedContainer {\n        let string: String\n        let matches: [NSTextCheckingResult]\n        var codingPath: [CodingKey]\n        var userInfo: [CodingUserInfoKey: Any]\n\n        var currentIndex: Int = 0\n\n        init(string: String, matches: [NSTextCheckingResult], codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {\n            self.string = string\n            self.matches = matches\n            self.codingPath = codingPath\n            self.userInfo = userInfo\n        }\n    }\n}\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension _RegularExpressionDecoder.UnkeyedContainer: UnkeyedDecodingContainer {\n    var count: Int? {\n        return self.matches.count\n    }\n\n    var isAtEnd: Bool {\n        return self.currentIndex >= self.count ?? 0\n    }\n\n    func decodeNil() throws -> Bool {\n        return !isAtEnd\n    }\n\n    func decode<T>(_ type: T.Type) throws -> T where T : Decodable {\n        guard !isAtEnd else {\n            throw DecodingError.dataCorruptedError(in: self, debugDescription: \"no more matches\")\n        }\n\n        defer { self.currentIndex += 1 }\n\n        let decoder = _RegularExpressionDecoder(string: self.string, matches: [self.matches[self.currentIndex]])\n\n        return try T(from: decoder)\n    }\n\n    func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {\n        defer { self.currentIndex += 1 }\n\n        return _RegularExpressionDecoder.UnkeyedContainer(string: self.string, matches: [self.matches[self.currentIndex]], codingPath: self.codingPath, userInfo: self.userInfo)\n    }\n\n    func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {\n        defer { self.currentIndex += 1 }\n\n        let container = _RegularExpressionDecoder.KeyedContainer<NestedKey>(string: self.string, match: self.matches[self.currentIndex], codingPath: self.codingPath, userInfo: self.userInfo)\n        return KeyedDecodingContainer(container)\n    }\n\n    func superDecoder() throws -> Decoder {\n        fatalError(\"Unimplemented\")\n    }\n}\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nextension _RegularExpressionDecoder.UnkeyedContainer: RegularExpressionDecodingContainer {}\n"
  },
  {
    "path": "Tests/RegularExpressionDecoderTests/RegularExpressionDecodingTests.swift",
    "content": "import XCTest\nimport Foundation\n@testable import RegularExpressionDecoder\n\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nstruct Stock: Decodable {\n    let symbol: String\n    var price: Double\n\n    enum Sign: String, Decodable {\n        case gain = \"▲\"\n        case unchanged = \"=\"\n        case loss = \"▼\"\n    }\n\n    private var sign: Sign\n    private var change: Double = 0.0\n    var movement: Double {\n        switch sign {\n        case .gain: return +change\n        case .unchanged: return 0.0\n        case .loss: return -change\n        }\n    }\n\n    enum CodingKeys: String, CodingKey {\n        case symbol\n        case price\n        case sign\n        case change\n    }\n}\n\n// swiftlint:disable force_try\n@available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *)\nclass RegularExpressionDecodingTests: XCTestCase {\n    var decoder: RegularExpressionDecoder<Stock>!\n\n    override func setUp() {\n        let pattern: RegularExpressionPattern<Stock, Stock.CodingKeys> = #\"\"\"\n        \\b\n        (?<\\#(.symbol)>[A-Z]{1,4}) \\s+\n        (?<\\#(.price)>\\d{1,}\\.\\d{2}) \\s*\n        (?<\\#(.sign)>([▲▼](?!0\\.00))|(=(?=0\\.00)))\n        (?<\\#(.change)>\\d{1,}\\.\\d{2})\n        \\b\n        \"\"\"#\n\n        self.decoder = try! RegularExpressionDecoder<Stock>(pattern: pattern, options: .allowCommentsAndWhitespace)\n    }\n\n    func testDecodeSingle() {\n        let string = \"AAPL 170.69▲0.51\"\n        let stock = try! self.decoder.decode(Stock.self, from: string)\n\n        XCTAssertEqual(stock.symbol, \"AAPL\")\n        XCTAssertEqual(stock.price, 170.69, accuracy: 0.01)\n        XCTAssertEqual(stock.movement, 0.51, accuracy: 0.01)\n    }\n\n    func testDecodeMultiple() {\n        let string = \"\"\"\n        AAPL 170.69▲0.51\n        GOOG 1122.57▲2.41\n        AMZN 1621.48▼18.52\n        MSFT 106.57=0.00\n        SWIFT 5.0▲1.0.0\n        \"\"\"\n\n        let stocks = try! self.decoder.decode([Stock].self, from: string)\n\n        guard stocks.count == 4 else {\n            XCTFail(\"decoded \\(stocks.count) of 4 valid stocks\")\n            return\n        }\n\n        let AAPL = stocks[0]\n        XCTAssertEqual(AAPL.symbol, \"AAPL\")\n        XCTAssertEqual(AAPL.price, 170.69, accuracy: 0.01)\n        XCTAssertEqual(AAPL.movement, 0.51, accuracy: 0.01)\n\n        let GOOG = stocks[1]\n        XCTAssertEqual(GOOG.symbol, \"GOOG\")\n        XCTAssertEqual(GOOG.price, 1122.57, accuracy: 0.01)\n        XCTAssertEqual(GOOG.movement, 2.41, accuracy: 0.01)\n\n        let AMZN = stocks[2]\n        XCTAssertEqual(AMZN.symbol, \"AMZN\")\n        XCTAssertEqual(AMZN.price, 1621.48, accuracy: 0.01)\n        XCTAssertEqual(AMZN.movement, -18.52, accuracy: 0.01)\n\n        let MSFT = stocks[3]\n        XCTAssertEqual(MSFT.symbol, \"MSFT\")\n        XCTAssertEqual(MSFT.price, 106.57, accuracy: 0.01)\n        XCTAssertEqual(MSFT.movement, 0.0, accuracy: 0.01)\n    }\n\n    func testDecodeInvalid() {\n        let string = \"AAPL 170.69\" // missing sign and change\n\n        XCTAssertThrowsError(try self.decoder.decode(Stock.self, from: string))\n    }\n}\n"
  }
]