├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── documentation.yml ├── RegularExpressionDecoder.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── RegularExpressionDecoder_Info.plist ├── RegularExpressionDecoderTests_Info.plist ├── xcshareddata │ └── xcschemes │ │ └── RegularExpressionDecoder.xcscheme └── project.pbxproj ├── RegularExpressionDecoder.playground ├── contents.xcplayground ├── Contents.swift └── Sources │ └── Stock.swift ├── RegularExpressionDecoder.xcworkspace ├── xcshareddata │ └── IDEWorkspaceChecks.plist └── contents.xcworkspacedata ├── LICENSE.md ├── Package.swift ├── Sources └── RegularExpressionDecoder │ ├── RegularExpressionPattern.swift │ ├── SingleValueDecodingContainer.swift │ ├── UnkeyedDecodingContainer.swift │ ├── KeyedDecodingContainer.swift │ └── RegularExpressionDecoder.swift ├── Tests └── RegularExpressionDecoderTests │ └── RegularExpressionDecodingTests.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [mattt] 2 | custom: https://flight.school/books/strings 3 | -------------------------------------------------------------------------------- /RegularExpressionDecoder.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /RegularExpressionDecoder.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /RegularExpressionDecoder.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RegularExpressionDecoder.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RegularExpressionDecoder.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /RegularExpressionDecoder.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /RegularExpressionDecoder.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import RegularExpressionDecoder 2 | 3 | let ticker = """ 4 | AAPL 170.69▲0.51 5 | GOOG 1122.57▲2.41 6 | AMZN 1621.48▼18.52 7 | MSFT 106.57=0.00 8 | SWIFT 5.0.0▲1.0.0 9 | """ 10 | 11 | let pattern: RegularExpressionPattern = #""" 12 | \b 13 | (?<\#(.symbol)>[A-Z]{1,4}) \s+ 14 | (?<\#(.price)>\d{1,}\.\d{2}) \s* 15 | (?<\#(.sign)>([▲▼](?!0\.00))|(=(?=0\.00))) 16 | (?<\#(.change)>\d{1,}\.\d{2}) 17 | \b 18 | """# 19 | 20 | let decoder = try RegularExpressionDecoder(pattern: pattern, options: .allowCommentsAndWhitespace) 21 | 22 | try decoder.decode([Stock].self, from: ticker) 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | macos: 11 | runs-on: macos-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v1 16 | - name: Build and Test 17 | run: swift test 18 | 19 | linux: 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | matrix: 24 | swift: ["5.1", "5.2", "latest"] 25 | 26 | container: 27 | image: swift:${{ matrix.swift }} 28 | 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v1 32 | - name: Build and Test 33 | run: swift test --enable-test-discovery 34 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - .github/workflows/documentation.yml 9 | - Sources/**.swift 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v1 18 | - name: Generate Documentation 19 | uses: SwiftDocOrg/swift-doc@master 20 | with: 21 | inputs: "Sources/SwiftDoc" 22 | output: "Documentation" 23 | - name: Upload Documentation to Wiki 24 | uses: SwiftDocOrg/github-wiki-publish-action@v1 25 | with: 26 | path: "Documentation" 27 | env: 28 | GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 29 | -------------------------------------------------------------------------------- /RegularExpressionDecoder.xcodeproj/RegularExpressionDecoder_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /RegularExpressionDecoder.xcodeproj/RegularExpressionDecoderTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /RegularExpressionDecoder.playground/Sources/Stock.swift: -------------------------------------------------------------------------------- 1 | public struct Stock: Decodable { 2 | public let symbol: String 3 | public var price: Double 4 | 5 | enum Sign: String, Decodable { 6 | case gain = "▲" 7 | case unchanged = "=" 8 | case loss = "▼" 9 | } 10 | 11 | private var sign: Sign 12 | private var change: Double = 0.0 13 | public var movement: Double { 14 | switch sign { 15 | case .gain: return +change 16 | case .unchanged: return 0.0 17 | case .loss: return -change 18 | } 19 | } 20 | 21 | public enum CodingKeys: String, CodingKey { 22 | case symbol 23 | case price 24 | case sign 25 | case change 26 | } 27 | } 28 | 29 | extension Stock.Sign: CustomStringConvertible { 30 | var description: String { 31 | return self.rawValue 32 | } 33 | } 34 | 35 | extension Stock: CustomStringConvertible { 36 | public var description: String { 37 | return "\(self.symbol) \(self.price)\(self.sign)\(self.change)" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Read Evaluate Press, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "RegularExpressionDecoder", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "RegularExpressionDecoder", 12 | targets: ["RegularExpressionDecoder"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "RegularExpressionDecoder", 23 | dependencies: [], 24 | path: "Sources" 25 | ), 26 | .testTarget( 27 | name: "RegularExpressionDecoderTests", 28 | dependencies: ["RegularExpressionDecoder"], 29 | path: "Tests" 30 | ), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /Sources/RegularExpressionDecoder/RegularExpressionPattern.swift: -------------------------------------------------------------------------------- 1 | public struct RegularExpressionPattern: LosslessStringConvertible, ExpressibleByStringInterpolation where T: Decodable, CodingKeys: CodingKey & Hashable { 2 | public var description: String 3 | public var captures: Set? 4 | 5 | public init?(_ description: String) { 6 | self.description = description 7 | } 8 | 9 | public init(stringLiteral value: String) { 10 | self.init(value)! 11 | } 12 | 13 | public init(stringInterpolation: StringInterpolation) { 14 | self.init(stringInterpolation.string)! 15 | self.captures = stringInterpolation.captures 16 | } 17 | 18 | public struct StringInterpolation: StringInterpolationProtocol { 19 | var string: String = "" 20 | var captures: Set = [] 21 | 22 | public init(literalCapacity: Int, interpolationCount: Int) { 23 | self.string.reserveCapacity(literalCapacity) 24 | } 25 | 26 | public mutating func appendLiteral(_ literal: String) { 27 | self.string.append(literal) 28 | } 29 | 30 | public mutating func appendInterpolation(_ key: CodingKeys) { 31 | precondition(!self.captures.contains(key), "\(key) already captured") 32 | precondition(!key.stringValue.contains { !$0.isLetter }, "invalid capture name \(key.stringValue)") 33 | self.string.append(key.stringValue) 34 | self.captures.insert(key) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/RegularExpressionDecoder/SingleValueDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 4 | extension _RegularExpressionDecoder { 5 | final class SingleValueContainer { 6 | let string: String 7 | let match: NSTextCheckingResult? 8 | var codingPath: [CodingKey] 9 | var userInfo: [CodingUserInfoKey: Any] 10 | 11 | init(string: String, match: NSTextCheckingResult?, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { 12 | self.string = string 13 | self.match = match 14 | self.codingPath = codingPath 15 | self.userInfo = userInfo 16 | } 17 | } 18 | } 19 | 20 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 21 | extension _RegularExpressionDecoder.SingleValueContainer: SingleValueDecodingContainer { 22 | func decodeNil() -> Bool { 23 | return self.match == nil 24 | } 25 | 26 | func decode(_ type: T.Type) throws -> T where T : Decodable { 27 | let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot decode type \(type)") 28 | throw DecodingError.typeMismatch(type, context) 29 | } 30 | 31 | func decode(_ type: T.Type) throws -> T where T: Decodable, T: LosslessStringConvertible { 32 | guard let value = T(self.string) else { 33 | let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot decode type \(type)") 34 | throw DecodingError.typeMismatch(type, context) 35 | } 36 | return value 37 | } 38 | } 39 | 40 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 41 | extension _RegularExpressionDecoder.SingleValueContainer: RegularExpressionDecodingContainer {} 42 | -------------------------------------------------------------------------------- /Sources/RegularExpressionDecoder/UnkeyedDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 4 | extension _RegularExpressionDecoder { 5 | final class UnkeyedContainer { 6 | let string: String 7 | let matches: [NSTextCheckingResult] 8 | var codingPath: [CodingKey] 9 | var userInfo: [CodingUserInfoKey: Any] 10 | 11 | var currentIndex: Int = 0 12 | 13 | init(string: String, matches: [NSTextCheckingResult], codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { 14 | self.string = string 15 | self.matches = matches 16 | self.codingPath = codingPath 17 | self.userInfo = userInfo 18 | } 19 | } 20 | } 21 | 22 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 23 | extension _RegularExpressionDecoder.UnkeyedContainer: UnkeyedDecodingContainer { 24 | var count: Int? { 25 | return self.matches.count 26 | } 27 | 28 | var isAtEnd: Bool { 29 | return self.currentIndex >= self.count ?? 0 30 | } 31 | 32 | func decodeNil() throws -> Bool { 33 | return !isAtEnd 34 | } 35 | 36 | func decode(_ type: T.Type) throws -> T where T : Decodable { 37 | guard !isAtEnd else { 38 | throw DecodingError.dataCorruptedError(in: self, debugDescription: "no more matches") 39 | } 40 | 41 | defer { self.currentIndex += 1 } 42 | 43 | let decoder = _RegularExpressionDecoder(string: self.string, matches: [self.matches[self.currentIndex]]) 44 | 45 | return try T(from: decoder) 46 | } 47 | 48 | func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { 49 | defer { self.currentIndex += 1 } 50 | 51 | return _RegularExpressionDecoder.UnkeyedContainer(string: self.string, matches: [self.matches[self.currentIndex]], codingPath: self.codingPath, userInfo: self.userInfo) 52 | } 53 | 54 | func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey { 55 | defer { self.currentIndex += 1 } 56 | 57 | let container = _RegularExpressionDecoder.KeyedContainer(string: self.string, match: self.matches[self.currentIndex], codingPath: self.codingPath, userInfo: self.userInfo) 58 | return KeyedDecodingContainer(container) 59 | } 60 | 61 | func superDecoder() throws -> Decoder { 62 | fatalError("Unimplemented") 63 | } 64 | } 65 | 66 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 67 | extension _RegularExpressionDecoder.UnkeyedContainer: RegularExpressionDecodingContainer {} 68 | -------------------------------------------------------------------------------- /Tests/RegularExpressionDecoderTests/RegularExpressionDecodingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | @testable import RegularExpressionDecoder 4 | 5 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 6 | struct Stock: Decodable { 7 | let symbol: String 8 | var price: Double 9 | 10 | enum Sign: String, Decodable { 11 | case gain = "▲" 12 | case unchanged = "=" 13 | case loss = "▼" 14 | } 15 | 16 | private var sign: Sign 17 | private var change: Double = 0.0 18 | var movement: Double { 19 | switch sign { 20 | case .gain: return +change 21 | case .unchanged: return 0.0 22 | case .loss: return -change 23 | } 24 | } 25 | 26 | enum CodingKeys: String, CodingKey { 27 | case symbol 28 | case price 29 | case sign 30 | case change 31 | } 32 | } 33 | 34 | // swiftlint:disable force_try 35 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 36 | class RegularExpressionDecodingTests: XCTestCase { 37 | var decoder: RegularExpressionDecoder! 38 | 39 | override func setUp() { 40 | let pattern: RegularExpressionPattern = #""" 41 | \b 42 | (?<\#(.symbol)>[A-Z]{1,4}) \s+ 43 | (?<\#(.price)>\d{1,}\.\d{2}) \s* 44 | (?<\#(.sign)>([▲▼](?!0\.00))|(=(?=0\.00))) 45 | (?<\#(.change)>\d{1,}\.\d{2}) 46 | \b 47 | """# 48 | 49 | self.decoder = try! RegularExpressionDecoder(pattern: pattern, options: .allowCommentsAndWhitespace) 50 | } 51 | 52 | func testDecodeSingle() { 53 | let string = "AAPL 170.69▲0.51" 54 | let stock = try! self.decoder.decode(Stock.self, from: string) 55 | 56 | XCTAssertEqual(stock.symbol, "AAPL") 57 | XCTAssertEqual(stock.price, 170.69, accuracy: 0.01) 58 | XCTAssertEqual(stock.movement, 0.51, accuracy: 0.01) 59 | } 60 | 61 | func testDecodeMultiple() { 62 | let string = """ 63 | AAPL 170.69▲0.51 64 | GOOG 1122.57▲2.41 65 | AMZN 1621.48▼18.52 66 | MSFT 106.57=0.00 67 | SWIFT 5.0▲1.0.0 68 | """ 69 | 70 | let stocks = try! self.decoder.decode([Stock].self, from: string) 71 | 72 | guard stocks.count == 4 else { 73 | XCTFail("decoded \(stocks.count) of 4 valid stocks") 74 | return 75 | } 76 | 77 | let AAPL = stocks[0] 78 | XCTAssertEqual(AAPL.symbol, "AAPL") 79 | XCTAssertEqual(AAPL.price, 170.69, accuracy: 0.01) 80 | XCTAssertEqual(AAPL.movement, 0.51, accuracy: 0.01) 81 | 82 | let GOOG = stocks[1] 83 | XCTAssertEqual(GOOG.symbol, "GOOG") 84 | XCTAssertEqual(GOOG.price, 1122.57, accuracy: 0.01) 85 | XCTAssertEqual(GOOG.movement, 2.41, accuracy: 0.01) 86 | 87 | let AMZN = stocks[2] 88 | XCTAssertEqual(AMZN.symbol, "AMZN") 89 | XCTAssertEqual(AMZN.price, 1621.48, accuracy: 0.01) 90 | XCTAssertEqual(AMZN.movement, -18.52, accuracy: 0.01) 91 | 92 | let MSFT = stocks[3] 93 | XCTAssertEqual(MSFT.symbol, "MSFT") 94 | XCTAssertEqual(MSFT.price, 106.57, accuracy: 0.01) 95 | XCTAssertEqual(MSFT.movement, 0.0, accuracy: 0.01) 96 | } 97 | 98 | func testDecodeInvalid() { 99 | let string = "AAPL 170.69" // missing sign and change 100 | 101 | XCTAssertThrowsError(try self.decoder.decode(Stock.self, from: string)) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/RegularExpressionDecoder/KeyedDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 4 | extension NSTextCheckingResult { 5 | func range(for key: Key) -> NSRange? where Key: CodingKey { 6 | if let position = key.intValue { 7 | return self.range(at: position) 8 | } else { 9 | return self.range(withName: key.stringValue) 10 | } 11 | } 12 | } 13 | 14 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 15 | extension _RegularExpressionDecoder { 16 | final class KeyedContainer where Key: CodingKey { 17 | let string: String 18 | let match: NSTextCheckingResult? 19 | var codingPath: [CodingKey] 20 | var userInfo: [CodingUserInfoKey: Any] 21 | 22 | init(string: String, match: NSTextCheckingResult?, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { 23 | self.string = string 24 | self.match = match 25 | self.codingPath = codingPath 26 | self.userInfo = userInfo 27 | } 28 | 29 | func range(for key: Key) -> Range? { 30 | guard let nsrange = self.match?.range(for: key), 31 | nsrange.location != NSNotFound 32 | else { 33 | return nil 34 | } 35 | 36 | return Range(nsrange, in: self.string) 37 | } 38 | 39 | func string(for key: Key) -> String? { 40 | guard let range = self.range(for: key) else { 41 | return nil 42 | } 43 | 44 | return String(self.string[range]) 45 | } 46 | } 47 | } 48 | 49 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 50 | extension _RegularExpressionDecoder.KeyedContainer: KeyedDecodingContainerProtocol { 51 | var allKeys: [Key] { 52 | guard let captureGroupNames = self.userInfo[._captureGroupNames] as? [String] else { 53 | return [] 54 | } 55 | 56 | return captureGroupNames.compactMap { Key(stringValue: $0) } 57 | } 58 | 59 | func contains(_ key: Key) -> Bool { 60 | return self.range(for: key) != nil 61 | } 62 | 63 | func decodeNil(forKey key: Key) throws -> Bool { 64 | return self.match == nil || !contains(key) 65 | } 66 | 67 | func decode(_ type: String.Type, forKey key: Key) throws -> String { 68 | guard let string = self.string(for: key) else { 69 | let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "String: \(self.string)") 70 | throw DecodingError.keyNotFound(key, context) 71 | } 72 | 73 | return string 74 | } 75 | 76 | func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable, T: LosslessStringConvertible { 77 | let string = try self.decode(String.self, forKey: key) 78 | 79 | guard let value = T(string) else { 80 | let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "String: \(self.string)") 81 | throw DecodingError.typeMismatch(type, context) 82 | } 83 | 84 | return value 85 | } 86 | 87 | func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { 88 | let string = try self.decode(String.self, forKey: key) 89 | 90 | let decoder = _RegularExpressionDecoder(string: string, matches: [self.match].compactMap {$0}) 91 | let value = try T(from: decoder) 92 | 93 | return value 94 | } 95 | 96 | func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { 97 | fatalError("Unimplemented") 98 | } 99 | 100 | func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { 101 | fatalError("Unimplemented") 102 | } 103 | 104 | func superDecoder() throws -> Decoder { 105 | fatalError("Unimplemented") 106 | } 107 | 108 | func superDecoder(forKey key: Key) throws -> Decoder { 109 | fatalError("Unimplemented") 110 | } 111 | } 112 | 113 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 114 | extension _RegularExpressionDecoder.KeyedContainer: RegularExpressionDecodingContainer {} 115 | -------------------------------------------------------------------------------- /Sources/RegularExpressionDecoder/RegularExpressionDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | 5 | */ 6 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 7 | final public class RegularExpressionDecoder { 8 | private(set) var regularExpression: NSRegularExpression 9 | var captureGroupNames: [String]? 10 | 11 | public enum Error: Swift.Error { 12 | case noMatches 13 | case tooManyMatches 14 | } 15 | 16 | public init(pattern: RegularExpressionPattern, options: NSRegularExpression.Options = []) throws { 17 | self.regularExpression = try NSRegularExpression(pattern: pattern.description, options: options) 18 | self.captureGroupNames = pattern.captures?.map { $0.stringValue } 19 | } 20 | 21 | public func decode(_ type: T.Type, from string: String, options: NSRegularExpression.MatchingOptions = []) throws -> T { 22 | let range = NSRange(string.startIndex.. [T] { 39 | let range = NSRange(string.startIndex.. SingleValueDecodingContainer { 83 | assertCanCreateContainer() 84 | 85 | let container = SingleValueContainer(string: self.string, match: self.matches.first, codingPath: self.codingPath, userInfo: self.userInfo) 86 | self.container = container 87 | 88 | return container 89 | } 90 | 91 | func container(keyedBy type: Key.Type) -> KeyedDecodingContainer where Key : CodingKey { 92 | assertCanCreateContainer() 93 | 94 | let container = KeyedContainer(string: self.string, match: self.matches.first, codingPath: self.codingPath, userInfo: self.userInfo) 95 | self.container = container 96 | 97 | return KeyedDecodingContainer(container) 98 | } 99 | 100 | func unkeyedContainer() -> UnkeyedDecodingContainer { 101 | assertCanCreateContainer() 102 | 103 | let container = UnkeyedContainer(string: self.string, matches: self.matches, codingPath: self.codingPath, userInfo: self.userInfo) 104 | self.container = container 105 | 106 | return container 107 | } 108 | } 109 | 110 | @available(OSX 10.13, iOS 11, tvOS 11, watchOS 4, *) 111 | protocol RegularExpressionDecodingContainer: class {} 112 | -------------------------------------------------------------------------------- /RegularExpressionDecoder.xcodeproj/xcshareddata/xcschemes/RegularExpressionDecoder.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Regular Expression Decoder 2 | 3 | [![Build Status][build status badge]][build status] 4 | [![License][license badge]][license] 5 | [![Swift Version][swift version badge]][swift version] 6 | 7 | A decoder that constructs objects from regular expression matches. 8 | 9 | --- 10 | 11 | For more information about creating your own custom decoders, 12 | consult Chapter 7 of the 13 | [Flight School Guide to Swift Codable](https://flight.school/books/codable). 14 | For more information about using regular expressions in Swift, 15 | check out Chapter 6 of the 16 | [Flight School Guide to Swift Strings](https://flight.school/books/strings). 17 | 18 | ## Requirements 19 | 20 | - Swift 5+ 21 | - iOS 11+ or macOS 10.13+ 22 | 23 | ## Usage 24 | 25 | ```swift 26 | import RegularExpressionDecoder 27 | 28 | let ticker = """ 29 | AAPL 170.69▲0.51 30 | GOOG 1122.57▲2.41 31 | AMZN 1621.48▼18.52 32 | MSFT 106.57=0.00 33 | SWIFT 5.0.0▲1.0.0 34 | """ 35 | 36 | let pattern: RegularExpressionPattern = #""" 37 | \b 38 | (?<\#(.symbol)>[A-Z]{1,4}) \s+ 39 | (?<\#(.price)>\d{1,}\.\d{2}) \s* 40 | (?<\#(.sign)>([▲▼=]) 41 | (?<\#(.change)>\d{1,}\.\d{2}) 42 | \b 43 | """# 44 | 45 | let decoder = try RegularExpressionDecoder( 46 | pattern: pattern, 47 | options: .allowCommentsAndWhitespace 48 | ) 49 | 50 | try decoder.decode([Stock].self, from: ticker) 51 | // Decodes [AAPL, GOOG, AMZN, MSFT] (but not SWIFT, which is invalid) 52 | ``` 53 | 54 | ## Explanation 55 | 56 | Let's say that you're building an app that parses stock quotes 57 | from a text-based stream of price changes. 58 | 59 | ```swift 60 | let ticker = """ 61 | AAPL 170.69▲0.51 62 | GOOG 1122.57▲2.41 63 | AMZN 1621.48▼18.52 64 | MSFT 106.57=0.00 65 | """ 66 | ``` 67 | 68 | Each stock is represented by the following structure: 69 | 70 | - The **symbol**, consisting of 1 to 4 uppercase letters, followed by a space 71 | - The **price**, formatted as a number with 2 decimal places 72 | - A **sign**, indicating a price gain (`▲`), loss (`▼`), or no change (`=`) 73 | - The **magnitude** of the gain or loss, formatted the same as the price 74 | 75 | These format constraints lend themselves naturally 76 | to representation by a regular expression, 77 | such as: 78 | 79 | ```perl 80 | /\b[A-Z]{1,4} \d{1,}\.\d{2}[▲▼=]\d{1,}\.\d{2}\b/ 81 | ``` 82 | 83 | > Note: 84 | > The `\b` metacharacter anchors matches to word boundaries. 85 | 86 | This regular expression can distinguish between 87 | valid and invalid stock quotes. 88 | 89 | ```swift 90 | "AAPL 170.69▲0.51" // valid 91 | "SWIFT 5.0.0▲1.0.0" // invalid 92 | ``` 93 | 94 | However, to extract individual components from a quote 95 | (symbol, price, etc.) 96 | the regular expression must contain capture groups, 97 | of which there are two varieties: 98 | positional capture groups and 99 | named capture groups. 100 | 101 | Positional capture groups are demarcated in the pattern 102 | by enclosing parentheses (`(___)`). 103 | With some slight modifications, 104 | we can make original regular expression capture each part of the stock quote: 105 | 106 | ```perl 107 | /\b([A-Z]{1,4}) (\d{1,}\.\d{2})([▲▼=])(\d{1,}\.\d{2})\b/ 108 | ``` 109 | 110 | When matched, 111 | the symbol can be accessed by the first capture group, 112 | the price by the second, 113 | and so on. 114 | 115 | For large numbers of capture groups --- 116 | especially in patterns with nested groups --- 117 | one can easily lose track of which parts correspond to which positions. 118 | So another approach is to assign names to capture groups, 119 | which are denoted by the syntax `(?___)`. 120 | 121 | ```perl 122 | /\b 123 | (?[A-Z]{1,4}) \s+ 124 | (?\d{1,}\.\d{2}) \s* 125 | (?([▲▼=]) 126 | (?\d{1,}\.\d{2}) 127 | \b/ 128 | ``` 129 | 130 | > Note: 131 | > Most regular expression engines --- 132 | > including the one used by `NSRegularExpression` --- 133 | > provide a mode to ignore whitespace; 134 | > this lets you segment long patterns over multiple lines, 135 | > making them easier to read and understand. 136 | 137 | Theoretically, this approach allows you to access each group by name 138 | for each match of the regular expression. 139 | In practice, doing this in Swift can be inconvenient, 140 | as it requires you to interact with cumbersome `NSRegularExpression` APIs 141 | and somehow incorporate it into your model layer. 142 | 143 | `RegularExpressionDecoder` provides a convenient solution 144 | to constructing `Decodable` objects from regular expression matches 145 | by automatically matching coding keys to capture group names. 146 | And it can do so safely, 147 | thanks to the new `ExpressibleByStringInterpolation` protocol in Swift 5. 148 | 149 | To understand how, 150 | let's start by considering the following `Stock` model, 151 | which adopts the `Decodable` protocol: 152 | 153 | ```swift 154 | struct Stock: Decodable { 155 | let symbol: String 156 | var price: Double 157 | 158 | enum Sign: String, Decodable { 159 | case gain = "▲" 160 | case unchanged = "=" 161 | case loss = "▼" 162 | } 163 | 164 | private var sign: Sign 165 | private var change: Double = 0.0 166 | var movement: Double { 167 | switch sign { 168 | case .gain: return +change 169 | case .unchanged: return 0.0 170 | case .loss: return -change 171 | } 172 | } 173 | } 174 | ``` 175 | 176 | So far, so good. 177 | 178 | Now, normally, the Swift compiler 179 | automatically synthesizes conformance to `Decodable`, 180 | including a nested `CodingKeys` type. 181 | But in order to make this next part work correctly, 182 | we'll have to do this ourselves: 183 | 184 | ```swift 185 | extension Stock { 186 | enum CodingKeys: String, CodingKey { 187 | case symbol 188 | case price 189 | case sign 190 | case change 191 | } 192 | } 193 | ``` 194 | 195 | Here's where things get really interesting: 196 | remember our regular expression with named capture patterns from before? 197 | _We can replace the hard-coded names 198 | with interpolations of the `Stock` type's coding keys._ 199 | 200 | ```swift 201 | import RegularExpressionDecoder 202 | 203 | let pattern: RegularExpressionPattern = #""" 204 | \b 205 | (?<\#(.symbol)>[A-Z]{1,4}) \s+ 206 | (?<\#(.price)>\d{1,}\.\d{2}) \s* 207 | (?<\#(.sign)>[▲▼=]) 208 | (?<\#(.change)>\d{1,}\.\d{2}) 209 | \b 210 | """# 211 | ``` 212 | 213 | > Note: 214 | > This example benefits greatly from another new feature in Swift 5: 215 | > raw string literals. 216 | > Those octothorps (`#`) at the start and end 217 | > tell the compiler to ignore escape characters (`\`) 218 | > unless they also include an octothorp (`\#( )`). 219 | > Using raw string literals, 220 | > we can write regular expression metacharacters like `\b`, `\d`, and `\s` 221 | > without double escaping them (i.e. `\\b`). 222 | 223 | Thanks to `ExpressibleByStringInterpolation`, 224 | we can restrict interpolation segments to only accept those coding keys, 225 | thereby ensuring a direct 1:1 match between capture groups 226 | and their decoded properties. 227 | And not only that --- 228 | this approach lets us to verify that keys have valid regex-friendly names 229 | and aren't captured more than once. 230 | It's enormously powerful, 231 | allowing code to be incredibly expressive 232 | without compromising safety or performance. 233 | 234 | When all is said and done, 235 | `RegularExpressionDecoder` lets you decode types 236 | from a string according to a regular expression pattern 237 | much the same as you might from JSON or a property list 238 | using a decoder: 239 | 240 | ```swift 241 | let decoder = try RegularExpressionDecoder( 242 | pattern: pattern, 243 | options: .allowCommentsAndWhitespace 244 | ) 245 | 246 | try decoder.decode([Stock].self, from: ticker) 247 | // Decodes [AAPL, GOOG, AMZN, MSFT] 248 | ``` 249 | 250 | ## License 251 | 252 | MIT 253 | 254 | ## Contact 255 | 256 | Mattt ([@mattt](https://twitter.com/mattt)) 257 | 258 | [build status]: https://github.com/Flight-School/RegularExpressionDecoder/actions?query=workflow%3ACI 259 | [build status badge]: https://github.com/Flight-School/RegularExpressionDecoder/workflows/CI/badge.svg 260 | [license]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat 261 | [license badge]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat 262 | [swift version]: https://swift.org/download/ 263 | [swift version badge]: http://img.shields.io/badge/swift%20version-5.0-orange.svg?style=flat 264 | -------------------------------------------------------------------------------- /RegularExpressionDecoder.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | "RegularExpressionDecoder::RegularExpressionDecoderPackageTests::ProductTarget" /* RegularExpressionDecoderPackageTests */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = OBJ_38 /* Build configuration list for PBXAggregateTarget "RegularExpressionDecoderPackageTests" */; 13 | buildPhases = ( 14 | ); 15 | dependencies = ( 16 | OBJ_41 /* PBXTargetDependency */, 17 | ); 18 | name = RegularExpressionDecoderPackageTests; 19 | productName = RegularExpressionDecoderPackageTests; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXBuildFile section */ 24 | F866325422184A0E00637DE5 /* RegularExpressionPattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866325322184A0E00637DE5 /* RegularExpressionPattern.swift */; }; 25 | OBJ_26 /* KeyedDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* KeyedDecodingContainer.swift */; }; 26 | OBJ_27 /* RegularExpressionDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* RegularExpressionDecoder.swift */; }; 27 | OBJ_28 /* SingleValueDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* SingleValueDecodingContainer.swift */; }; 28 | OBJ_29 /* UnkeyedDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* UnkeyedDecodingContainer.swift */; }; 29 | OBJ_36 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; 30 | OBJ_47 /* RegularExpressionDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* RegularExpressionDecodingTests.swift */; }; 31 | OBJ_49 /* RegularExpressionDecoder.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "RegularExpressionDecoder::RegularExpressionDecoder::Product" /* RegularExpressionDecoder.framework */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXContainerItemProxy section */ 35 | F89F2BD52216E93E0063EB2D /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = OBJ_1 /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = "RegularExpressionDecoder::RegularExpressionDecoder"; 40 | remoteInfo = RegularExpressionDecoder; 41 | }; 42 | F89F2BD62216E9400063EB2D /* PBXContainerItemProxy */ = { 43 | isa = PBXContainerItemProxy; 44 | containerPortal = OBJ_1 /* Project object */; 45 | proxyType = 1; 46 | remoteGlobalIDString = "RegularExpressionDecoder::RegularExpressionDecoderTests"; 47 | remoteInfo = RegularExpressionDecoderTests; 48 | }; 49 | /* End PBXContainerItemProxy section */ 50 | 51 | /* Begin PBXFileReference section */ 52 | F866325322184A0E00637DE5 /* RegularExpressionPattern.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularExpressionPattern.swift; sourceTree = ""; }; 53 | OBJ_10 /* RegularExpressionDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularExpressionDecoder.swift; sourceTree = ""; }; 54 | OBJ_11 /* SingleValueDecodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleValueDecodingContainer.swift; sourceTree = ""; }; 55 | OBJ_12 /* UnkeyedDecodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnkeyedDecodingContainer.swift; sourceTree = ""; }; 56 | OBJ_15 /* RegularExpressionDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularExpressionDecodingTests.swift; sourceTree = ""; }; 57 | OBJ_19 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 58 | OBJ_20 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 59 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 60 | OBJ_9 /* KeyedDecodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedDecodingContainer.swift; sourceTree = ""; }; 61 | "RegularExpressionDecoder::RegularExpressionDecoder::Product" /* RegularExpressionDecoder.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = RegularExpressionDecoder.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | "RegularExpressionDecoder::RegularExpressionDecoderTests::Product" /* RegularExpressionDecoderTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = RegularExpressionDecoderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 63 | /* End PBXFileReference section */ 64 | 65 | /* Begin PBXFrameworksBuildPhase section */ 66 | OBJ_30 /* Frameworks */ = { 67 | isa = PBXFrameworksBuildPhase; 68 | buildActionMask = 0; 69 | files = ( 70 | ); 71 | runOnlyForDeploymentPostprocessing = 0; 72 | }; 73 | OBJ_48 /* Frameworks */ = { 74 | isa = PBXFrameworksBuildPhase; 75 | buildActionMask = 0; 76 | files = ( 77 | OBJ_49 /* RegularExpressionDecoder.framework in Frameworks */, 78 | ); 79 | runOnlyForDeploymentPostprocessing = 0; 80 | }; 81 | /* End PBXFrameworksBuildPhase section */ 82 | 83 | /* Begin PBXGroup section */ 84 | OBJ_13 /* Tests */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | OBJ_14 /* RegularExpressionDecoderTests */, 88 | ); 89 | path = Tests; 90 | sourceTree = SOURCE_ROOT; 91 | }; 92 | OBJ_14 /* RegularExpressionDecoderTests */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | OBJ_15 /* RegularExpressionDecodingTests.swift */, 96 | ); 97 | path = RegularExpressionDecoderTests; 98 | sourceTree = ""; 99 | }; 100 | OBJ_16 /* Products */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | "RegularExpressionDecoder::RegularExpressionDecoderTests::Product" /* RegularExpressionDecoderTests.xctest */, 104 | "RegularExpressionDecoder::RegularExpressionDecoder::Product" /* RegularExpressionDecoder.framework */, 105 | ); 106 | name = Products; 107 | sourceTree = BUILT_PRODUCTS_DIR; 108 | }; 109 | OBJ_5 = { 110 | isa = PBXGroup; 111 | children = ( 112 | OBJ_6 /* Package.swift */, 113 | OBJ_7 /* Sources */, 114 | OBJ_13 /* Tests */, 115 | OBJ_16 /* Products */, 116 | OBJ_19 /* LICENSE.md */, 117 | OBJ_20 /* README.md */, 118 | ); 119 | sourceTree = ""; 120 | }; 121 | OBJ_7 /* Sources */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | OBJ_8 /* RegularExpressionDecoder */, 125 | ); 126 | path = Sources; 127 | sourceTree = SOURCE_ROOT; 128 | }; 129 | OBJ_8 /* RegularExpressionDecoder */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | F866325322184A0E00637DE5 /* RegularExpressionPattern.swift */, 133 | OBJ_10 /* RegularExpressionDecoder.swift */, 134 | OBJ_11 /* SingleValueDecodingContainer.swift */, 135 | OBJ_9 /* KeyedDecodingContainer.swift */, 136 | OBJ_12 /* UnkeyedDecodingContainer.swift */, 137 | ); 138 | path = RegularExpressionDecoder; 139 | sourceTree = ""; 140 | }; 141 | /* End PBXGroup section */ 142 | 143 | /* Begin PBXNativeTarget section */ 144 | "RegularExpressionDecoder::RegularExpressionDecoder" /* RegularExpressionDecoder */ = { 145 | isa = PBXNativeTarget; 146 | buildConfigurationList = OBJ_22 /* Build configuration list for PBXNativeTarget "RegularExpressionDecoder" */; 147 | buildPhases = ( 148 | OBJ_25 /* Sources */, 149 | OBJ_30 /* Frameworks */, 150 | ); 151 | buildRules = ( 152 | ); 153 | dependencies = ( 154 | ); 155 | name = RegularExpressionDecoder; 156 | productName = RegularExpressionDecoder; 157 | productReference = "RegularExpressionDecoder::RegularExpressionDecoder::Product" /* RegularExpressionDecoder.framework */; 158 | productType = "com.apple.product-type.framework"; 159 | }; 160 | "RegularExpressionDecoder::RegularExpressionDecoderTests" /* RegularExpressionDecoderTests */ = { 161 | isa = PBXNativeTarget; 162 | buildConfigurationList = OBJ_43 /* Build configuration list for PBXNativeTarget "RegularExpressionDecoderTests" */; 163 | buildPhases = ( 164 | OBJ_46 /* Sources */, 165 | OBJ_48 /* Frameworks */, 166 | ); 167 | buildRules = ( 168 | ); 169 | dependencies = ( 170 | OBJ_50 /* PBXTargetDependency */, 171 | ); 172 | name = RegularExpressionDecoderTests; 173 | productName = RegularExpressionDecoderTests; 174 | productReference = "RegularExpressionDecoder::RegularExpressionDecoderTests::Product" /* RegularExpressionDecoderTests.xctest */; 175 | productType = "com.apple.product-type.bundle.unit-test"; 176 | }; 177 | "RegularExpressionDecoder::SwiftPMPackageDescription" /* RegularExpressionDecoderPackageDescription */ = { 178 | isa = PBXNativeTarget; 179 | buildConfigurationList = OBJ_32 /* Build configuration list for PBXNativeTarget "RegularExpressionDecoderPackageDescription" */; 180 | buildPhases = ( 181 | OBJ_35 /* Sources */, 182 | ); 183 | buildRules = ( 184 | ); 185 | dependencies = ( 186 | ); 187 | name = RegularExpressionDecoderPackageDescription; 188 | productName = RegularExpressionDecoderPackageDescription; 189 | productType = "com.apple.product-type.framework"; 190 | }; 191 | /* End PBXNativeTarget section */ 192 | 193 | /* Begin PBXProject section */ 194 | OBJ_1 /* Project object */ = { 195 | isa = PBXProject; 196 | attributes = { 197 | LastSwiftMigration = 9999; 198 | LastUpgradeCheck = 9999; 199 | }; 200 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "RegularExpressionDecoder" */; 201 | compatibilityVersion = "Xcode 3.2"; 202 | developmentRegion = English; 203 | hasScannedForEncodings = 0; 204 | knownRegions = ( 205 | en, 206 | ); 207 | mainGroup = OBJ_5; 208 | productRefGroup = OBJ_16 /* Products */; 209 | projectDirPath = ""; 210 | projectRoot = ""; 211 | targets = ( 212 | "RegularExpressionDecoder::RegularExpressionDecoder" /* RegularExpressionDecoder */, 213 | "RegularExpressionDecoder::SwiftPMPackageDescription" /* RegularExpressionDecoderPackageDescription */, 214 | "RegularExpressionDecoder::RegularExpressionDecoderPackageTests::ProductTarget" /* RegularExpressionDecoderPackageTests */, 215 | "RegularExpressionDecoder::RegularExpressionDecoderTests" /* RegularExpressionDecoderTests */, 216 | ); 217 | }; 218 | /* End PBXProject section */ 219 | 220 | /* Begin PBXSourcesBuildPhase section */ 221 | OBJ_25 /* Sources */ = { 222 | isa = PBXSourcesBuildPhase; 223 | buildActionMask = 0; 224 | files = ( 225 | OBJ_26 /* KeyedDecodingContainer.swift in Sources */, 226 | F866325422184A0E00637DE5 /* RegularExpressionPattern.swift in Sources */, 227 | OBJ_27 /* RegularExpressionDecoder.swift in Sources */, 228 | OBJ_28 /* SingleValueDecodingContainer.swift in Sources */, 229 | OBJ_29 /* UnkeyedDecodingContainer.swift in Sources */, 230 | ); 231 | runOnlyForDeploymentPostprocessing = 0; 232 | }; 233 | OBJ_35 /* Sources */ = { 234 | isa = PBXSourcesBuildPhase; 235 | buildActionMask = 0; 236 | files = ( 237 | OBJ_36 /* Package.swift in Sources */, 238 | ); 239 | runOnlyForDeploymentPostprocessing = 0; 240 | }; 241 | OBJ_46 /* Sources */ = { 242 | isa = PBXSourcesBuildPhase; 243 | buildActionMask = 0; 244 | files = ( 245 | OBJ_47 /* RegularExpressionDecodingTests.swift in Sources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | /* End PBXSourcesBuildPhase section */ 250 | 251 | /* Begin PBXTargetDependency section */ 252 | OBJ_41 /* PBXTargetDependency */ = { 253 | isa = PBXTargetDependency; 254 | target = "RegularExpressionDecoder::RegularExpressionDecoderTests" /* RegularExpressionDecoderTests */; 255 | targetProxy = F89F2BD62216E9400063EB2D /* PBXContainerItemProxy */; 256 | }; 257 | OBJ_50 /* PBXTargetDependency */ = { 258 | isa = PBXTargetDependency; 259 | target = "RegularExpressionDecoder::RegularExpressionDecoder" /* RegularExpressionDecoder */; 260 | targetProxy = F89F2BD52216E93E0063EB2D /* PBXContainerItemProxy */; 261 | }; 262 | /* End PBXTargetDependency section */ 263 | 264 | /* Begin XCBuildConfiguration section */ 265 | OBJ_23 /* Debug */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | ENABLE_TESTABILITY = YES; 269 | FRAMEWORK_SEARCH_PATHS = ( 270 | "$(inherited)", 271 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 272 | ); 273 | HEADER_SEARCH_PATHS = "$(inherited)"; 274 | INFOPLIST_FILE = RegularExpressionDecoder.xcodeproj/RegularExpressionDecoder_Info.plist; 275 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 276 | OTHER_CFLAGS = "$(inherited)"; 277 | OTHER_LDFLAGS = "$(inherited)"; 278 | OTHER_SWIFT_FLAGS = "$(inherited)"; 279 | PRODUCT_BUNDLE_IDENTIFIER = RegularExpressionDecoder; 280 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 281 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 282 | SKIP_INSTALL = YES; 283 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 284 | SWIFT_VERSION = 4.0; 285 | TARGET_NAME = RegularExpressionDecoder; 286 | }; 287 | name = Debug; 288 | }; 289 | OBJ_24 /* Release */ = { 290 | isa = XCBuildConfiguration; 291 | buildSettings = { 292 | ENABLE_TESTABILITY = YES; 293 | FRAMEWORK_SEARCH_PATHS = ( 294 | "$(inherited)", 295 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 296 | ); 297 | HEADER_SEARCH_PATHS = "$(inherited)"; 298 | INFOPLIST_FILE = RegularExpressionDecoder.xcodeproj/RegularExpressionDecoder_Info.plist; 299 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 300 | OTHER_CFLAGS = "$(inherited)"; 301 | OTHER_LDFLAGS = "$(inherited)"; 302 | OTHER_SWIFT_FLAGS = "$(inherited)"; 303 | PRODUCT_BUNDLE_IDENTIFIER = RegularExpressionDecoder; 304 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 305 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 306 | SKIP_INSTALL = YES; 307 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 308 | SWIFT_VERSION = 4.0; 309 | TARGET_NAME = RegularExpressionDecoder; 310 | }; 311 | name = Release; 312 | }; 313 | OBJ_3 /* Debug */ = { 314 | isa = XCBuildConfiguration; 315 | buildSettings = { 316 | CLANG_ENABLE_OBJC_ARC = YES; 317 | COMBINE_HIDPI_IMAGES = YES; 318 | COPY_PHASE_STRIP = NO; 319 | DEBUG_INFORMATION_FORMAT = dwarf; 320 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 321 | ENABLE_NS_ASSERTIONS = YES; 322 | GCC_OPTIMIZATION_LEVEL = 0; 323 | GCC_PREPROCESSOR_DEFINITIONS = ( 324 | "DEBUG=1", 325 | "$(inherited)", 326 | ); 327 | MACOSX_DEPLOYMENT_TARGET = 10.10; 328 | ONLY_ACTIVE_ARCH = YES; 329 | OTHER_SWIFT_FLAGS = "-DXcode"; 330 | PRODUCT_NAME = "$(TARGET_NAME)"; 331 | SDKROOT = macosx; 332 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 333 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "SWIFT_PACKAGE DEBUG"; 334 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 335 | USE_HEADERMAP = NO; 336 | }; 337 | name = Debug; 338 | }; 339 | OBJ_33 /* Debug */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | LD = /usr/bin/true; 343 | OTHER_SWIFT_FLAGS = "-swift-version 4 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk"; 344 | SWIFT_VERSION = 4.0; 345 | }; 346 | name = Debug; 347 | }; 348 | OBJ_34 /* Release */ = { 349 | isa = XCBuildConfiguration; 350 | buildSettings = { 351 | LD = /usr/bin/true; 352 | OTHER_SWIFT_FLAGS = "-swift-version 4 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk"; 353 | SWIFT_VERSION = 4.0; 354 | }; 355 | name = Release; 356 | }; 357 | OBJ_39 /* Debug */ = { 358 | isa = XCBuildConfiguration; 359 | buildSettings = { 360 | }; 361 | name = Debug; 362 | }; 363 | OBJ_4 /* Release */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | CLANG_ENABLE_OBJC_ARC = YES; 367 | COMBINE_HIDPI_IMAGES = YES; 368 | COPY_PHASE_STRIP = YES; 369 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 370 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 371 | GCC_OPTIMIZATION_LEVEL = s; 372 | MACOSX_DEPLOYMENT_TARGET = 10.10; 373 | OTHER_SWIFT_FLAGS = "-DXcode"; 374 | PRODUCT_NAME = "$(TARGET_NAME)"; 375 | SDKROOT = macosx; 376 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 377 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 378 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 379 | USE_HEADERMAP = NO; 380 | }; 381 | name = Release; 382 | }; 383 | OBJ_40 /* Release */ = { 384 | isa = XCBuildConfiguration; 385 | buildSettings = { 386 | }; 387 | name = Release; 388 | }; 389 | OBJ_44 /* Debug */ = { 390 | isa = XCBuildConfiguration; 391 | buildSettings = { 392 | CLANG_ENABLE_MODULES = YES; 393 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 394 | FRAMEWORK_SEARCH_PATHS = ( 395 | "$(inherited)", 396 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 397 | ); 398 | HEADER_SEARCH_PATHS = "$(inherited)"; 399 | INFOPLIST_FILE = RegularExpressionDecoder.xcodeproj/RegularExpressionDecoderTests_Info.plist; 400 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; 401 | OTHER_CFLAGS = "$(inherited)"; 402 | OTHER_LDFLAGS = "$(inherited)"; 403 | OTHER_SWIFT_FLAGS = "$(inherited)"; 404 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 405 | SWIFT_VERSION = 4.0; 406 | TARGET_NAME = RegularExpressionDecoderTests; 407 | }; 408 | name = Debug; 409 | }; 410 | OBJ_45 /* Release */ = { 411 | isa = XCBuildConfiguration; 412 | buildSettings = { 413 | CLANG_ENABLE_MODULES = YES; 414 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 415 | FRAMEWORK_SEARCH_PATHS = ( 416 | "$(inherited)", 417 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 418 | ); 419 | HEADER_SEARCH_PATHS = "$(inherited)"; 420 | INFOPLIST_FILE = RegularExpressionDecoder.xcodeproj/RegularExpressionDecoderTests_Info.plist; 421 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; 422 | OTHER_CFLAGS = "$(inherited)"; 423 | OTHER_LDFLAGS = "$(inherited)"; 424 | OTHER_SWIFT_FLAGS = "$(inherited)"; 425 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 426 | SWIFT_VERSION = 4.0; 427 | TARGET_NAME = RegularExpressionDecoderTests; 428 | }; 429 | name = Release; 430 | }; 431 | /* End XCBuildConfiguration section */ 432 | 433 | /* Begin XCConfigurationList section */ 434 | OBJ_2 /* Build configuration list for PBXProject "RegularExpressionDecoder" */ = { 435 | isa = XCConfigurationList; 436 | buildConfigurations = ( 437 | OBJ_3 /* Debug */, 438 | OBJ_4 /* Release */, 439 | ); 440 | defaultConfigurationIsVisible = 0; 441 | defaultConfigurationName = Release; 442 | }; 443 | OBJ_22 /* Build configuration list for PBXNativeTarget "RegularExpressionDecoder" */ = { 444 | isa = XCConfigurationList; 445 | buildConfigurations = ( 446 | OBJ_23 /* Debug */, 447 | OBJ_24 /* Release */, 448 | ); 449 | defaultConfigurationIsVisible = 0; 450 | defaultConfigurationName = Release; 451 | }; 452 | OBJ_32 /* Build configuration list for PBXNativeTarget "RegularExpressionDecoderPackageDescription" */ = { 453 | isa = XCConfigurationList; 454 | buildConfigurations = ( 455 | OBJ_33 /* Debug */, 456 | OBJ_34 /* Release */, 457 | ); 458 | defaultConfigurationIsVisible = 0; 459 | defaultConfigurationName = Release; 460 | }; 461 | OBJ_38 /* Build configuration list for PBXAggregateTarget "RegularExpressionDecoderPackageTests" */ = { 462 | isa = XCConfigurationList; 463 | buildConfigurations = ( 464 | OBJ_39 /* Debug */, 465 | OBJ_40 /* Release */, 466 | ); 467 | defaultConfigurationIsVisible = 0; 468 | defaultConfigurationName = Release; 469 | }; 470 | OBJ_43 /* Build configuration list for PBXNativeTarget "RegularExpressionDecoderTests" */ = { 471 | isa = XCConfigurationList; 472 | buildConfigurations = ( 473 | OBJ_44 /* Debug */, 474 | OBJ_45 /* Release */, 475 | ); 476 | defaultConfigurationIsVisible = 0; 477 | defaultConfigurationName = Release; 478 | }; 479 | /* End XCConfigurationList section */ 480 | }; 481 | rootObject = OBJ_1 /* Project object */; 482 | } 483 | --------------------------------------------------------------------------------