├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .travis.yml ├── Demo.playground ├── Contents.swift ├── contents.xcplayground └── playground.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── Package.swift ├── README.md ├── RouterX.podspec ├── RouterX.xcodeproj ├── RouterXTests_Info.plist ├── RouterX_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── RouterX-Package.xcscheme ├── RouterX.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Sources └── RouterX │ ├── MatchResult.swift │ ├── PatternScanError.swift │ ├── Router.swift │ ├── RouterXCore.swift │ ├── RoutingGraph.swift │ ├── RoutingPatternParser.swift │ ├── RoutingPatternScanner.swift │ ├── RoutingPatternToken.swift │ ├── URLPathScanner.swift │ └── URLPathToken.swift └── Tests ├── LinuxMain.swift └── RouterXTests ├── Extensions └── RoutingGraph.swift ├── RouterTests.swift ├── RouterXCoreTests.swift ├── RoutingPatternParserTests.swift ├── RoutingPatternScannerTests.swift └── URLPathScannerTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | # Xcode 4 | build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata 14 | *.xccheckout 15 | *.moved-aside 16 | DerivedData 17 | *.xcuserstate 18 | *.hmap 19 | *.ipa 20 | 21 | timeline.xctimeline 22 | 23 | # AppCode 24 | .idea 25 | 26 | # CocoaPods 27 | # 28 | # We recommend against adding the Pods directory to your .gitignore. However 29 | # you should judge for yourself, the pros and cons are mentioned at: 30 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 31 | # 32 | # Pods/ 33 | 34 | # Carthage 35 | # 36 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 37 | # Carthage/Checkouts 38 | 39 | Carthage/Build 40 | 41 | # OSX 42 | .DS_Store 43 | .AppleDouble 44 | .LSOverride 45 | 46 | # Icon must end with two \r 47 | Icon 48 | 49 | # Thumbnails 50 | ._* 51 | 52 | # Files that might appear in the root of a volume 53 | .DocumentRevisions-V100 54 | .fseventsd 55 | .Spotlight-V100 56 | .TemporaryItems 57 | .Trashes 58 | .VolumeIcon.icns 59 | 60 | # Directories potentially created on remote AFP share 61 | .AppleDB 62 | .AppleDesktop 63 | Network Trash Folder 64 | Temporary Items 65 | .apdisk 66 | images/logo.sketch 67 | 68 | # Fastlane specific 69 | fastlane/report.xml 70 | 71 | # Deliver temporary files 72 | fastlane/Preview.html 73 | 74 | # Snapshot generated screenshots 75 | fastlane/screenshots/**/*.png 76 | fastlane/screenshots/screenshots.html 77 | 78 | # Temporary files 79 | fastlane/test_output 80 | test_output 81 | fastlane/.env 82 | pre-change.yml 83 | .build 84 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - line_length 3 | - file_length 4 | - function_body_length 5 | - type_body_length 6 | excluded: 7 | - Carthage 8 | - Pods 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | osx_image: xcode10.2 3 | language: swift 4 | 5 | env: 6 | global: 7 | - LC_CTYPE=en_US.UTF-8 8 | - LANG=en_US.UTF-8 9 | 10 | cache: 11 | - bundler 12 | 13 | before_install: 14 | - bundle install 15 | 16 | script: 17 | - xcodebuild test -project ./RouterX.xcodeproj -scheme RouterX-Package | bundle exec xcpretty -f `xcpretty-travis-formatter` 18 | 19 | after_success: 20 | - bash <(curl -s https://codecov.io/bash) 21 | -------------------------------------------------------------------------------- /Demo.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import Foundation 4 | import RouterX 5 | //: Here I define some pattern 6 | 7 | let pattern1 = "/articles(/page/:page(/per_page/:per_page))(/sort/:sort)(.:format)" 8 | let pattern2 = "/articles/new" 9 | let pattern3 = "/articles/:id" 10 | let pattern4 = "/:article_id" 11 | 12 | //: Initialize the router, I can give a default closure to handle while given a URI path but match no one. 13 | 14 | // This is the handler that would be performed after no pattern match 15 | let defaultUnmatchHandler: Router.UnmatchHandler = { url, context in 16 | // Do something here, e.g: give some tips or show a default UI 17 | print("Default unmatch handler") 18 | print("\(url) is unmatched") 19 | 20 | // context can be provided on matching patterns 21 | if let context = context as? String { 22 | print("Context is \"\(context)\"") 23 | } 24 | print("\n") 25 | } 26 | 27 | // Initialize a router instance, consider it's global and singleton 28 | // Router.T is used for specifying context type 29 | let router = Router(defaultUnmatchHandler: defaultUnmatchHandler) 30 | 31 | //: Register patterns, the closure is the handle when matched the pattern. 32 | 33 | // Set a route pattern, the closure is a handler that would be performed after match the pattern 34 | do { 35 | try router.register(pattern: pattern1) { result in 36 | // Now, registered pattern has been matched 37 | // Do anything you want, e.g: show a UI 38 | print(result) 39 | print("\n") 40 | } 41 | } catch let error { 42 | print("register failed, reason:\n\(error.localizedDescription)\n") 43 | } 44 | 45 | do { 46 | try router.register(pattern: pattern2) { _ in 47 | // Now, registered pattern has been matched 48 | // Do anything you want, e.g: show a UI 49 | print("call new article") 50 | } 51 | } catch let error { 52 | print("register failed, reason:\n\(error.localizedDescription)\n") 53 | } 54 | //: Let match some URI Path. 55 | 56 | // A case that should be matched 57 | let path1 = "/articles/page/2/sort/recent.json?foo=bar&baz" 58 | 59 | // It's will be matched, and perform the handler that we have set up. 60 | router.match(path1) 61 | // It can pass the context for handler 62 | router.match(path1, context: "fooo") 63 | 64 | // A case that shouldn't be matched 65 | let path2 = "/articles/2/edit" 66 | 67 | let customUnmatchHandler: Router.UnmatchHandler = { (url, context) in 68 | print("This is custom unmatch handler") 69 | var string = "\(url) is no match..." 70 | // context can be provided on matching patterns 71 | if let context = context as? String { 72 | string += "\nContext is \"\(context)\"" 73 | } 74 | 75 | print(string) 76 | } 77 | // It's will not be matched, and perform the default unmatch handler that we have set up 78 | router.match(path2) 79 | 80 | // It can provide a custome unmatch handler to override the default, also can pass the context 81 | router.match(path2, context: "bar", unmatchHandler: customUnmatchHandler) 82 | -------------------------------------------------------------------------------- /Demo.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Demo.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo.playground/playground.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "xcpretty" 4 | gem "xcpretty-travis-formatter" 5 | gem "cocoapods", "~> 1.6" 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.0) 5 | activesupport (4.2.11.1) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | atomos (0.1.3) 11 | claide (1.0.2) 12 | cocoapods (1.6.1) 13 | activesupport (>= 4.0.2, < 5) 14 | claide (>= 1.0.2, < 2.0) 15 | cocoapods-core (= 1.6.1) 16 | cocoapods-deintegrate (>= 1.0.2, < 2.0) 17 | cocoapods-downloader (>= 1.2.2, < 2.0) 18 | cocoapods-plugins (>= 1.0.0, < 2.0) 19 | cocoapods-search (>= 1.0.0, < 2.0) 20 | cocoapods-stats (>= 1.0.0, < 2.0) 21 | cocoapods-trunk (>= 1.3.1, < 2.0) 22 | cocoapods-try (>= 1.1.0, < 2.0) 23 | colored2 (~> 3.1) 24 | escape (~> 0.0.4) 25 | fourflusher (>= 2.2.0, < 3.0) 26 | gh_inspector (~> 1.0) 27 | molinillo (~> 0.6.6) 28 | nap (~> 1.0) 29 | ruby-macho (~> 1.4) 30 | xcodeproj (>= 1.8.1, < 2.0) 31 | cocoapods-core (1.6.1) 32 | activesupport (>= 4.0.2, < 6) 33 | fuzzy_match (~> 2.0.4) 34 | nap (~> 1.0) 35 | cocoapods-deintegrate (1.0.4) 36 | cocoapods-downloader (1.2.2) 37 | cocoapods-plugins (1.0.0) 38 | nap 39 | cocoapods-search (1.0.0) 40 | cocoapods-stats (1.1.0) 41 | cocoapods-trunk (1.3.1) 42 | nap (>= 0.8, < 2.0) 43 | netrc (~> 0.11) 44 | cocoapods-try (1.1.0) 45 | colored2 (3.1.2) 46 | concurrent-ruby (1.1.5) 47 | escape (0.0.4) 48 | fourflusher (2.2.0) 49 | fuzzy_match (2.0.4) 50 | gh_inspector (1.1.3) 51 | i18n (0.9.5) 52 | concurrent-ruby (~> 1.0) 53 | minitest (5.11.3) 54 | molinillo (0.6.6) 55 | nanaimo (0.2.6) 56 | nap (1.1.0) 57 | netrc (0.11.0) 58 | rouge (2.0.7) 59 | ruby-macho (1.4.0) 60 | thread_safe (0.3.6) 61 | tzinfo (1.2.5) 62 | thread_safe (~> 0.1) 63 | xcodeproj (1.9.0) 64 | CFPropertyList (>= 2.3.3, < 4.0) 65 | atomos (~> 0.1.3) 66 | claide (>= 1.0.2, < 2.0) 67 | colored2 (~> 3.1) 68 | nanaimo (~> 0.2.6) 69 | xcpretty (0.3.0) 70 | rouge (~> 2.0.7) 71 | xcpretty-travis-formatter (1.0.0) 72 | xcpretty (~> 0.2, >= 0.0.7) 73 | 74 | PLATFORMS 75 | ruby 76 | 77 | DEPENDENCIES 78 | cocoapods (~> 1.6) 79 | xcpretty 80 | xcpretty-travis-formatter 81 | 82 | BUNDLED WITH 83 | 1.17.2 84 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Jun Jiang 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 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: "RouterX", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "RouterX", 12 | targets: ["RouterX"]) 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: "RouterX", 23 | dependencies: []), 24 | .testTarget( 25 | name: "RouterXTests", 26 | dependencies: ["RouterX"]) 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RouterX 2 | ==== 3 | 4 | [![Build Status](https://travis-ci.org/jasl/RouterX.svg)](https://travis-ci.org/jasl/RouterX) 5 | [![codecov.io](https://codecov.io/github/jasl/RouterX/coverage.svg?branch=master)](https://codecov.io/github/jasl/RouterX?branch=master) 6 | 7 | A Ruby on Rails flavored URI routing library in Swift. 8 | 9 | ## Usages 10 | 11 | See [Demo.playground](Demo.playground/Contents.swift) for now. 12 | 13 | Routing rules follows Rails flavor, see [Rails routing guide](http://guides.rubyonrails.org/routing.html#non-resourceful-routes) for now. 14 | 15 | ## Features 16 | - [ ] globbing support 17 | - [ ] format validation 18 | 19 | ## Requirements 20 | 21 | - iOS 9.0+ / Mac OS X 10.10+ / tvOS 9.0+ 22 | - Xcode 10.0+ 23 | 24 | ## Contributing 25 | 26 | Bug report or pull request are welcome. 27 | 28 | ### Make a pull request 29 | 30 | 1. Fork it 31 | 2. Create your feature branch (`git checkout -b my-new-feature`) 32 | 3. Commit your changes (`git commit -am 'Add some feature'`) 33 | 4. Push to the branch (`git push origin my-new-feature`) 34 | 5. Create new Pull Request 35 | 36 | Please write unit test with your code if necessary. 37 | 38 | ## License 39 | 40 | The project is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). -------------------------------------------------------------------------------- /RouterX.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'RouterX' 3 | s.version = '0.1.0' 4 | s.license = {:type => 'MIT', :file => 'MIT-LICENSE'} 5 | s.summary = 'A Ruby on Rails flavored URI routing library.' 6 | s.homepage = 'https://github.com/jasl/RouterX' 7 | s.authors = {'jasl' => 'jasl9187@hotmail.com'} 8 | s.source = {:git => 'https://github.com/jasl/RouterX.git', :tag => s.version} 9 | s.swift_version = '5.0' 10 | 11 | s.ios.deployment_target = '9.0' 12 | s.osx.deployment_target = '10.10' 13 | s.tvos.deployment_target = '9.0' 14 | 15 | s.source_files = %w(Sources/RouterX/*.swift) 16 | end 17 | -------------------------------------------------------------------------------- /RouterX.xcodeproj/RouterXTests_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 | -------------------------------------------------------------------------------- /RouterX.xcodeproj/RouterX_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 | -------------------------------------------------------------------------------- /RouterX.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | "RouterX::RouterXPackageTests::ProductTarget" /* RouterXPackageTests */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = OBJ_51 /* Build configuration list for PBXAggregateTarget "RouterXPackageTests" */; 13 | buildPhases = ( 14 | ); 15 | dependencies = ( 16 | OBJ_54 /* PBXTargetDependency */, 17 | ); 18 | name = RouterXPackageTests; 19 | productName = RouterXPackageTests; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXBuildFile section */ 24 | 48065D3321AAB88400F33408 /* PatternScanError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48065D3221AAB88400F33408 /* PatternScanError.swift */; }; 25 | 48E62CA721A695000005838B /* MatchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48E62CA621A695000005838B /* MatchResult.swift */; }; 26 | OBJ_35 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Router.swift */; }; 27 | OBJ_36 /* RouterXCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* RouterXCore.swift */; }; 28 | OBJ_37 /* RoutingGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* RoutingGraph.swift */; }; 29 | OBJ_38 /* RoutingPatternParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* RoutingPatternParser.swift */; }; 30 | OBJ_39 /* RoutingPatternScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* RoutingPatternScanner.swift */; }; 31 | OBJ_40 /* RoutingPatternToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* RoutingPatternToken.swift */; }; 32 | OBJ_41 /* URLPathScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* URLPathScanner.swift */; }; 33 | OBJ_42 /* URLPathToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* URLPathToken.swift */; }; 34 | OBJ_49 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; 35 | OBJ_60 /* RoutingGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* RoutingGraph.swift */; }; 36 | OBJ_61 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* RouterTests.swift */; }; 37 | OBJ_62 /* RouterXCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_22 /* RouterXCoreTests.swift */; }; 38 | OBJ_63 /* RoutingPatternParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_23 /* RoutingPatternParserTests.swift */; }; 39 | OBJ_64 /* RoutingPatternScannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_24 /* RoutingPatternScannerTests.swift */; }; 40 | OBJ_65 /* URLPathScannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_25 /* URLPathScannerTests.swift */; }; 41 | OBJ_68 /* RouterX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "RouterX::RouterX::Product" /* RouterX.framework */; }; 42 | /* End PBXBuildFile section */ 43 | 44 | /* Begin PBXContainerItemProxy section */ 45 | C474930B21A479D900821FA4 /* PBXContainerItemProxy */ = { 46 | isa = PBXContainerItemProxy; 47 | containerPortal = OBJ_1 /* Project object */; 48 | proxyType = 1; 49 | remoteGlobalIDString = "RouterX::RouterX"; 50 | remoteInfo = RouterX; 51 | }; 52 | C474930C21A479D900821FA4 /* PBXContainerItemProxy */ = { 53 | isa = PBXContainerItemProxy; 54 | containerPortal = OBJ_1 /* Project object */; 55 | proxyType = 1; 56 | remoteGlobalIDString = "RouterX::RouterXTests"; 57 | remoteInfo = RouterXTests; 58 | }; 59 | /* End PBXContainerItemProxy section */ 60 | 61 | /* Begin PBXFileReference section */ 62 | 48065D3221AAB88400F33408 /* PatternScanError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatternScanError.swift; sourceTree = ""; }; 63 | 48E62CA621A695000005838B /* MatchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchResult.swift; sourceTree = ""; }; 64 | OBJ_10 /* RouterXCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterXCore.swift; sourceTree = ""; }; 65 | OBJ_11 /* RoutingGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingGraph.swift; sourceTree = ""; }; 66 | OBJ_12 /* RoutingPatternParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingPatternParser.swift; sourceTree = ""; }; 67 | OBJ_13 /* RoutingPatternScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingPatternScanner.swift; sourceTree = ""; }; 68 | OBJ_14 /* RoutingPatternToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingPatternToken.swift; sourceTree = ""; }; 69 | OBJ_15 /* URLPathScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLPathScanner.swift; sourceTree = ""; }; 70 | OBJ_16 /* URLPathToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLPathToken.swift; sourceTree = ""; }; 71 | OBJ_20 /* RoutingGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingGraph.swift; sourceTree = ""; }; 72 | OBJ_21 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = ""; }; 73 | OBJ_22 /* RouterXCoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterXCoreTests.swift; sourceTree = ""; }; 74 | OBJ_23 /* RoutingPatternParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingPatternParserTests.swift; sourceTree = ""; }; 75 | OBJ_24 /* RoutingPatternScannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingPatternScannerTests.swift; sourceTree = ""; }; 76 | OBJ_25 /* URLPathScannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLPathScannerTests.swift; sourceTree = ""; }; 77 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 78 | OBJ_9 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; 79 | "RouterX::RouterX::Product" /* RouterX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RouterX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 80 | "RouterX::RouterXTests::Product" /* RouterXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = RouterXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 81 | /* End PBXFileReference section */ 82 | 83 | /* Begin PBXFrameworksBuildPhase section */ 84 | OBJ_43 /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 0; 87 | files = ( 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | OBJ_67 /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 0; 94 | files = ( 95 | OBJ_68 /* RouterX.framework in Frameworks */, 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | /* End PBXFrameworksBuildPhase section */ 100 | 101 | /* Begin PBXGroup section */ 102 | OBJ_17 /* Tests */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | OBJ_18 /* RouterXTests */, 106 | ); 107 | name = Tests; 108 | sourceTree = SOURCE_ROOT; 109 | }; 110 | OBJ_18 /* RouterXTests */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | OBJ_19 /* Extensions */, 114 | OBJ_21 /* RouterTests.swift */, 115 | OBJ_22 /* RouterXCoreTests.swift */, 116 | OBJ_23 /* RoutingPatternParserTests.swift */, 117 | OBJ_24 /* RoutingPatternScannerTests.swift */, 118 | OBJ_25 /* URLPathScannerTests.swift */, 119 | ); 120 | name = RouterXTests; 121 | path = Tests/RouterXTests; 122 | sourceTree = SOURCE_ROOT; 123 | }; 124 | OBJ_19 /* Extensions */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | OBJ_20 /* RoutingGraph.swift */, 128 | ); 129 | path = Extensions; 130 | sourceTree = ""; 131 | }; 132 | OBJ_27 /* Products */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | "RouterX::RouterXTests::Product" /* RouterXTests.xctest */, 136 | "RouterX::RouterX::Product" /* RouterX.framework */, 137 | ); 138 | name = Products; 139 | sourceTree = BUILT_PRODUCTS_DIR; 140 | }; 141 | OBJ_5 = { 142 | isa = PBXGroup; 143 | children = ( 144 | OBJ_6 /* Package.swift */, 145 | OBJ_7 /* Sources */, 146 | OBJ_17 /* Tests */, 147 | OBJ_27 /* Products */, 148 | ); 149 | sourceTree = ""; 150 | }; 151 | OBJ_7 /* Sources */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | OBJ_8 /* RouterX */, 155 | ); 156 | name = Sources; 157 | sourceTree = SOURCE_ROOT; 158 | }; 159 | OBJ_8 /* RouterX */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | OBJ_9 /* Router.swift */, 163 | 48E62CA621A695000005838B /* MatchResult.swift */, 164 | OBJ_10 /* RouterXCore.swift */, 165 | OBJ_11 /* RoutingGraph.swift */, 166 | OBJ_12 /* RoutingPatternParser.swift */, 167 | OBJ_13 /* RoutingPatternScanner.swift */, 168 | 48065D3221AAB88400F33408 /* PatternScanError.swift */, 169 | OBJ_14 /* RoutingPatternToken.swift */, 170 | OBJ_15 /* URLPathScanner.swift */, 171 | OBJ_16 /* URLPathToken.swift */, 172 | ); 173 | name = RouterX; 174 | path = Sources/RouterX; 175 | sourceTree = SOURCE_ROOT; 176 | }; 177 | /* End PBXGroup section */ 178 | 179 | /* Begin PBXNativeTarget section */ 180 | "RouterX::RouterX" /* RouterX */ = { 181 | isa = PBXNativeTarget; 182 | buildConfigurationList = OBJ_31 /* Build configuration list for PBXNativeTarget "RouterX" */; 183 | buildPhases = ( 184 | OBJ_34 /* Sources */, 185 | OBJ_43 /* Frameworks */, 186 | ); 187 | buildRules = ( 188 | ); 189 | dependencies = ( 190 | ); 191 | name = RouterX; 192 | productName = RouterX; 193 | productReference = "RouterX::RouterX::Product" /* RouterX.framework */; 194 | productType = "com.apple.product-type.framework"; 195 | }; 196 | "RouterX::RouterXTests" /* RouterXTests */ = { 197 | isa = PBXNativeTarget; 198 | buildConfigurationList = OBJ_56 /* Build configuration list for PBXNativeTarget "RouterXTests" */; 199 | buildPhases = ( 200 | OBJ_59 /* Sources */, 201 | OBJ_67 /* Frameworks */, 202 | ); 203 | buildRules = ( 204 | ); 205 | dependencies = ( 206 | OBJ_69 /* PBXTargetDependency */, 207 | ); 208 | name = RouterXTests; 209 | productName = RouterXTests; 210 | productReference = "RouterX::RouterXTests::Product" /* RouterXTests.xctest */; 211 | productType = "com.apple.product-type.bundle.unit-test"; 212 | }; 213 | "RouterX::SwiftPMPackageDescription" /* RouterXPackageDescription */ = { 214 | isa = PBXNativeTarget; 215 | buildConfigurationList = OBJ_45 /* Build configuration list for PBXNativeTarget "RouterXPackageDescription" */; 216 | buildPhases = ( 217 | OBJ_48 /* Sources */, 218 | ); 219 | buildRules = ( 220 | ); 221 | dependencies = ( 222 | ); 223 | name = RouterXPackageDescription; 224 | productName = RouterXPackageDescription; 225 | productType = "com.apple.product-type.framework"; 226 | }; 227 | /* End PBXNativeTarget section */ 228 | 229 | /* Begin PBXProject section */ 230 | OBJ_1 /* Project object */ = { 231 | isa = PBXProject; 232 | attributes = { 233 | LastUpgradeCheck = 9999; 234 | TargetAttributes = { 235 | "RouterX::RouterX" = { 236 | LastSwiftMigration = 1020; 237 | }; 238 | "RouterX::RouterXTests" = { 239 | LastSwiftMigration = 1020; 240 | }; 241 | }; 242 | }; 243 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "RouterX" */; 244 | compatibilityVersion = "Xcode 10.0"; 245 | developmentRegion = English; 246 | hasScannedForEncodings = 0; 247 | knownRegions = ( 248 | English, 249 | en, 250 | ); 251 | mainGroup = OBJ_5; 252 | productRefGroup = OBJ_27 /* Products */; 253 | projectDirPath = ""; 254 | projectRoot = ""; 255 | targets = ( 256 | "RouterX::RouterX" /* RouterX */, 257 | "RouterX::SwiftPMPackageDescription" /* RouterXPackageDescription */, 258 | "RouterX::RouterXPackageTests::ProductTarget" /* RouterXPackageTests */, 259 | "RouterX::RouterXTests" /* RouterXTests */, 260 | ); 261 | }; 262 | /* End PBXProject section */ 263 | 264 | /* Begin PBXSourcesBuildPhase section */ 265 | OBJ_34 /* Sources */ = { 266 | isa = PBXSourcesBuildPhase; 267 | buildActionMask = 0; 268 | files = ( 269 | OBJ_35 /* Router.swift in Sources */, 270 | 48E62CA721A695000005838B /* MatchResult.swift in Sources */, 271 | OBJ_36 /* RouterXCore.swift in Sources */, 272 | OBJ_37 /* RoutingGraph.swift in Sources */, 273 | OBJ_38 /* RoutingPatternParser.swift in Sources */, 274 | OBJ_39 /* RoutingPatternScanner.swift in Sources */, 275 | 48065D3321AAB88400F33408 /* PatternScanError.swift in Sources */, 276 | OBJ_40 /* RoutingPatternToken.swift in Sources */, 277 | OBJ_41 /* URLPathScanner.swift in Sources */, 278 | OBJ_42 /* URLPathToken.swift in Sources */, 279 | ); 280 | runOnlyForDeploymentPostprocessing = 0; 281 | }; 282 | OBJ_48 /* Sources */ = { 283 | isa = PBXSourcesBuildPhase; 284 | buildActionMask = 0; 285 | files = ( 286 | OBJ_49 /* Package.swift in Sources */, 287 | ); 288 | runOnlyForDeploymentPostprocessing = 0; 289 | }; 290 | OBJ_59 /* Sources */ = { 291 | isa = PBXSourcesBuildPhase; 292 | buildActionMask = 0; 293 | files = ( 294 | OBJ_60 /* RoutingGraph.swift in Sources */, 295 | OBJ_61 /* RouterTests.swift in Sources */, 296 | OBJ_62 /* RouterXCoreTests.swift in Sources */, 297 | OBJ_63 /* RoutingPatternParserTests.swift in Sources */, 298 | OBJ_64 /* RoutingPatternScannerTests.swift in Sources */, 299 | OBJ_65 /* URLPathScannerTests.swift in Sources */, 300 | ); 301 | runOnlyForDeploymentPostprocessing = 0; 302 | }; 303 | /* End PBXSourcesBuildPhase section */ 304 | 305 | /* Begin PBXTargetDependency section */ 306 | OBJ_54 /* PBXTargetDependency */ = { 307 | isa = PBXTargetDependency; 308 | target = "RouterX::RouterXTests" /* RouterXTests */; 309 | targetProxy = C474930C21A479D900821FA4 /* PBXContainerItemProxy */; 310 | }; 311 | OBJ_69 /* PBXTargetDependency */ = { 312 | isa = PBXTargetDependency; 313 | target = "RouterX::RouterX" /* RouterX */; 314 | targetProxy = C474930B21A479D900821FA4 /* PBXContainerItemProxy */; 315 | }; 316 | /* End PBXTargetDependency section */ 317 | 318 | /* Begin XCBuildConfiguration section */ 319 | OBJ_3 /* Debug */ = { 320 | isa = XCBuildConfiguration; 321 | buildSettings = { 322 | CLANG_ENABLE_OBJC_ARC = YES; 323 | COMBINE_HIDPI_IMAGES = YES; 324 | COPY_PHASE_STRIP = NO; 325 | DEBUG_INFORMATION_FORMAT = dwarf; 326 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 327 | ENABLE_NS_ASSERTIONS = YES; 328 | GCC_OPTIMIZATION_LEVEL = 0; 329 | GCC_PREPROCESSOR_DEFINITIONS = ( 330 | "DEBUG=1", 331 | "$(inherited)", 332 | ); 333 | MACOSX_DEPLOYMENT_TARGET = 10.10; 334 | ONLY_ACTIVE_ARCH = YES; 335 | OTHER_SWIFT_FLAGS = "-DXcode"; 336 | PRODUCT_NAME = "$(TARGET_NAME)"; 337 | SDKROOT = macosx; 338 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 339 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "SWIFT_PACKAGE DEBUG"; 340 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 341 | USE_HEADERMAP = NO; 342 | }; 343 | name = Debug; 344 | }; 345 | OBJ_32 /* Debug */ = { 346 | isa = XCBuildConfiguration; 347 | buildSettings = { 348 | ENABLE_TESTABILITY = YES; 349 | FRAMEWORK_SEARCH_PATHS = ( 350 | "$(inherited)", 351 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 352 | ); 353 | HEADER_SEARCH_PATHS = "$(inherited)"; 354 | INFOPLIST_FILE = RouterX.xcodeproj/RouterX_Info.plist; 355 | LD_RUNPATH_SEARCH_PATHS = ( 356 | "$(inherited)", 357 | "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", 358 | ); 359 | OTHER_CFLAGS = "$(inherited)"; 360 | OTHER_LDFLAGS = "$(inherited)"; 361 | OTHER_SWIFT_FLAGS = "$(inherited)"; 362 | PRODUCT_BUNDLE_IDENTIFIER = RouterX; 363 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 364 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 365 | SKIP_INSTALL = YES; 366 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 367 | SWIFT_VERSION = 5.0; 368 | TARGET_NAME = RouterX; 369 | }; 370 | name = Debug; 371 | }; 372 | OBJ_33 /* Release */ = { 373 | isa = XCBuildConfiguration; 374 | buildSettings = { 375 | ENABLE_TESTABILITY = YES; 376 | FRAMEWORK_SEARCH_PATHS = ( 377 | "$(inherited)", 378 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 379 | ); 380 | HEADER_SEARCH_PATHS = "$(inherited)"; 381 | INFOPLIST_FILE = RouterX.xcodeproj/RouterX_Info.plist; 382 | LD_RUNPATH_SEARCH_PATHS = ( 383 | "$(inherited)", 384 | "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", 385 | ); 386 | OTHER_CFLAGS = "$(inherited)"; 387 | OTHER_LDFLAGS = "$(inherited)"; 388 | OTHER_SWIFT_FLAGS = "$(inherited)"; 389 | PRODUCT_BUNDLE_IDENTIFIER = RouterX; 390 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 391 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 392 | SKIP_INSTALL = YES; 393 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 394 | SWIFT_VERSION = 5.0; 395 | TARGET_NAME = RouterX; 396 | }; 397 | name = Release; 398 | }; 399 | OBJ_4 /* Release */ = { 400 | isa = XCBuildConfiguration; 401 | buildSettings = { 402 | CLANG_ENABLE_OBJC_ARC = YES; 403 | COMBINE_HIDPI_IMAGES = YES; 404 | COPY_PHASE_STRIP = YES; 405 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 406 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 407 | GCC_OPTIMIZATION_LEVEL = s; 408 | MACOSX_DEPLOYMENT_TARGET = 10.10; 409 | OTHER_SWIFT_FLAGS = "-DXcode"; 410 | PRODUCT_NAME = "$(TARGET_NAME)"; 411 | SDKROOT = macosx; 412 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 413 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 414 | SWIFT_COMPILATION_MODE = wholemodule; 415 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 416 | USE_HEADERMAP = NO; 417 | }; 418 | name = Release; 419 | }; 420 | OBJ_46 /* Debug */ = { 421 | isa = XCBuildConfiguration; 422 | buildSettings = { 423 | LD = /usr/bin/true; 424 | OTHER_SWIFT_FLAGS = "-swift-version 4.2 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk"; 425 | SWIFT_VERSION = 4.2; 426 | }; 427 | name = Debug; 428 | }; 429 | OBJ_47 /* Release */ = { 430 | isa = XCBuildConfiguration; 431 | buildSettings = { 432 | LD = /usr/bin/true; 433 | OTHER_SWIFT_FLAGS = "-swift-version 4.2 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk"; 434 | SWIFT_VERSION = 4.2; 435 | }; 436 | name = Release; 437 | }; 438 | OBJ_52 /* Debug */ = { 439 | isa = XCBuildConfiguration; 440 | buildSettings = { 441 | }; 442 | name = Debug; 443 | }; 444 | OBJ_53 /* Release */ = { 445 | isa = XCBuildConfiguration; 446 | buildSettings = { 447 | }; 448 | name = Release; 449 | }; 450 | OBJ_57 /* Debug */ = { 451 | isa = XCBuildConfiguration; 452 | buildSettings = { 453 | CLANG_ENABLE_MODULES = YES; 454 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 455 | FRAMEWORK_SEARCH_PATHS = ( 456 | "$(inherited)", 457 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 458 | ); 459 | HEADER_SEARCH_PATHS = "$(inherited)"; 460 | INFOPLIST_FILE = RouterX.xcodeproj/RouterXTests_Info.plist; 461 | LD_RUNPATH_SEARCH_PATHS = ( 462 | "$(inherited)", 463 | "@loader_path/../Frameworks", 464 | "@loader_path/Frameworks", 465 | ); 466 | OTHER_CFLAGS = "$(inherited)"; 467 | OTHER_LDFLAGS = "$(inherited)"; 468 | OTHER_SWIFT_FLAGS = "$(inherited)"; 469 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 470 | SWIFT_VERSION = 5.0; 471 | TARGET_NAME = RouterXTests; 472 | }; 473 | name = Debug; 474 | }; 475 | OBJ_58 /* Release */ = { 476 | isa = XCBuildConfiguration; 477 | buildSettings = { 478 | CLANG_ENABLE_MODULES = YES; 479 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 480 | FRAMEWORK_SEARCH_PATHS = ( 481 | "$(inherited)", 482 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 483 | ); 484 | HEADER_SEARCH_PATHS = "$(inherited)"; 485 | INFOPLIST_FILE = RouterX.xcodeproj/RouterXTests_Info.plist; 486 | LD_RUNPATH_SEARCH_PATHS = ( 487 | "$(inherited)", 488 | "@loader_path/../Frameworks", 489 | "@loader_path/Frameworks", 490 | ); 491 | OTHER_CFLAGS = "$(inherited)"; 492 | OTHER_LDFLAGS = "$(inherited)"; 493 | OTHER_SWIFT_FLAGS = "$(inherited)"; 494 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 495 | SWIFT_VERSION = 5.0; 496 | TARGET_NAME = RouterXTests; 497 | }; 498 | name = Release; 499 | }; 500 | /* End XCBuildConfiguration section */ 501 | 502 | /* Begin XCConfigurationList section */ 503 | OBJ_2 /* Build configuration list for PBXProject "RouterX" */ = { 504 | isa = XCConfigurationList; 505 | buildConfigurations = ( 506 | OBJ_3 /* Debug */, 507 | OBJ_4 /* Release */, 508 | ); 509 | defaultConfigurationIsVisible = 0; 510 | defaultConfigurationName = Release; 511 | }; 512 | OBJ_31 /* Build configuration list for PBXNativeTarget "RouterX" */ = { 513 | isa = XCConfigurationList; 514 | buildConfigurations = ( 515 | OBJ_32 /* Debug */, 516 | OBJ_33 /* Release */, 517 | ); 518 | defaultConfigurationIsVisible = 0; 519 | defaultConfigurationName = Release; 520 | }; 521 | OBJ_45 /* Build configuration list for PBXNativeTarget "RouterXPackageDescription" */ = { 522 | isa = XCConfigurationList; 523 | buildConfigurations = ( 524 | OBJ_46 /* Debug */, 525 | OBJ_47 /* Release */, 526 | ); 527 | defaultConfigurationIsVisible = 0; 528 | defaultConfigurationName = Release; 529 | }; 530 | OBJ_51 /* Build configuration list for PBXAggregateTarget "RouterXPackageTests" */ = { 531 | isa = XCConfigurationList; 532 | buildConfigurations = ( 533 | OBJ_52 /* Debug */, 534 | OBJ_53 /* Release */, 535 | ); 536 | defaultConfigurationIsVisible = 0; 537 | defaultConfigurationName = Release; 538 | }; 539 | OBJ_56 /* Build configuration list for PBXNativeTarget "RouterXTests" */ = { 540 | isa = XCConfigurationList; 541 | buildConfigurations = ( 542 | OBJ_57 /* Debug */, 543 | OBJ_58 /* Release */, 544 | ); 545 | defaultConfigurationIsVisible = 0; 546 | defaultConfigurationName = Release; 547 | }; 548 | /* End XCConfigurationList section */ 549 | }; 550 | rootObject = OBJ_1 /* Project object */; 551 | } 552 | -------------------------------------------------------------------------------- /RouterX.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /RouterX.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RouterX.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /RouterX.xcodeproj/xcshareddata/xcschemes/RouterX-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /RouterX.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /RouterX.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/RouterX/MatchResult.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct MatchResult: CustomDebugStringConvertible, CustomStringConvertible { 4 | public let url: URL 5 | public let parameters: [String: String] 6 | public let context: Context? 7 | 8 | public var description: String { 9 | return """ 10 | MatchResult<\(Context.self)> { 11 | url: \(url) 12 | parameters: \(parameters) 13 | context: \(String(describing: context)) 14 | } 15 | """ 16 | } 17 | 18 | public var debugDescription: String { 19 | return description 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/RouterX/PatternScanError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PatternRegisterError: LocalizedError { 4 | case empty 5 | case missingPrefixSlash 6 | case invalidGlobbing(globbing: String, after: String) 7 | case invalidSymbol(symbol: String, after: String) 8 | case unbalanceParenthesis 9 | case unexpectToken(after: String) 10 | } 11 | -------------------------------------------------------------------------------- /Sources/RouterX/Router.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class Router { 4 | public typealias MatchedHandler = (MatchResult) -> Void 5 | public typealias UnmatchHandler = ((URL, _ context: Context?) -> Void) 6 | 7 | private let core: RouterXCore = RouterXCore() 8 | private let defaultUnmatchHandler: UnmatchHandler? 9 | 10 | private var handlerMappings: [PatternIdentifier: MatchedHandler] = [:] 11 | 12 | public init(defaultUnmatchHandler: UnmatchHandler? = nil) { 13 | self.defaultUnmatchHandler = defaultUnmatchHandler 14 | } 15 | 16 | open func register(pattern: String, handler: @escaping MatchedHandler) throws { 17 | try core.register(pattern: pattern) 18 | handlerMappings[pattern] = handler 19 | } 20 | 21 | @discardableResult 22 | open func match(_ url: URL, context: Context? = nil, unmatchHandler: UnmatchHandler? = nil) -> Bool { 23 | guard let matchedRoute = core.match(url), 24 | let matchHandler = handlerMappings[matchedRoute.patternIdentifier] else { 25 | let expectUnmatchHandler = unmatchHandler ?? defaultUnmatchHandler 26 | expectUnmatchHandler?(url, context) 27 | return false 28 | } 29 | 30 | let result = MatchResult(url: url, parameters: matchedRoute.parametars, context: context) 31 | matchHandler(result) 32 | return true 33 | } 34 | 35 | @discardableResult 36 | open func match(_ path: String, context: Context? = nil, unmatchHandler: UnmatchHandler? = nil) -> Bool { 37 | guard let url = URL(string: path) else { return false } 38 | 39 | return match(url, context: context, unmatchHandler: unmatchHandler) 40 | } 41 | } 42 | 43 | extension Router: CustomDebugStringConvertible, CustomStringConvertible { 44 | open var description: String { 45 | return self.core.description 46 | } 47 | 48 | open var debugDescription: String { 49 | return self.description 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/RouterX/RouterXCore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct MatchedRoute { 4 | public let url: URL 5 | public let parametars: [String: String] 6 | public let patternIdentifier: PatternIdentifier 7 | 8 | public init(url: URL, parameters: [String: String], patternIdentifier: PatternIdentifier) { 9 | self.url = url 10 | self.parametars = parameters 11 | self.patternIdentifier = patternIdentifier 12 | } 13 | } 14 | 15 | public class RouterXCore { 16 | private let rootRoute: RouteVertex 17 | 18 | public init() { 19 | self.rootRoute = RouteVertex() 20 | } 21 | 22 | public func register(pattern: String) throws { 23 | let tokens = RoutingPatternScanner.tokenize(pattern) 24 | 25 | guard let prefixToken = tokens.first else { throw PatternRegisterError.empty } 26 | guard prefixToken == .slash else { throw PatternRegisterError.missingPrefixSlash } 27 | 28 | var previousToken: RoutingPatternToken? 29 | var stackTokensDescription = "" 30 | var parenthesisOffset = 0 31 | for token in tokens { 32 | switch token { 33 | case .star(let globbing): 34 | if previousToken != .slash { throw PatternRegisterError.invalidGlobbing(globbing: globbing, after: stackTokensDescription) } 35 | case .symbol(let symbol): 36 | if previousToken != .slash && previousToken != .dot { throw PatternRegisterError.invalidSymbol(symbol: symbol, after: stackTokensDescription) } 37 | case .lParen: 38 | parenthesisOffset += 1 39 | case .rParen: 40 | if parenthesisOffset <= 0 { 41 | throw PatternRegisterError.unexpectToken(after: stackTokensDescription) 42 | } 43 | parenthesisOffset -= 1 44 | default: break 45 | } 46 | stackTokensDescription.append(token.description) 47 | previousToken = token 48 | } 49 | 50 | guard parenthesisOffset == 0 else { 51 | throw PatternRegisterError.unbalanceParenthesis 52 | } 53 | try RoutingPatternParser.parseAndAppendTo(self.rootRoute, routingPatternTokens: tokens, patternIdentifier: pattern) 54 | } 55 | 56 | public func match(_ url: URL) -> MatchedRoute? { 57 | let path = url.path 58 | 59 | let tokens = URLPathScanner.tokenize(path) 60 | if tokens.isEmpty { 61 | return nil 62 | } 63 | 64 | var parameters: [String: String] = [:] 65 | 66 | var tokensGenerator = tokens.makeIterator() 67 | var targetRoute: RouteVertex = rootRoute 68 | while let token = tokensGenerator.next() { 69 | if let determinativeRoute = targetRoute.namedRoutes[token.routeEdge] { 70 | targetRoute = determinativeRoute 71 | } else if let epsilonRoute = targetRoute.parameterRoute { 72 | targetRoute = epsilonRoute.1 73 | parameters[epsilonRoute.0] = String(describing: token).removingPercentEncoding ?? "" 74 | } else { 75 | return nil 76 | } 77 | } 78 | 79 | guard let pathPatternIdentifier = targetRoute.patternIdentifier else { return nil } 80 | 81 | return MatchedRoute(url: url, parameters: parameters, patternIdentifier: pathPatternIdentifier) 82 | } 83 | 84 | public func match(_ path: String) -> MatchedRoute? { 85 | guard let url = URL(string: path) else { return nil } 86 | return match(url) 87 | } 88 | } 89 | 90 | extension RouterXCore: CustomStringConvertible, CustomDebugStringConvertible { 91 | public var description: String { 92 | return self.rootRoute.description 93 | } 94 | 95 | public var debugDescription: String { 96 | return self.description 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/RouterX/RoutingGraph.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias PatternIdentifier = String 4 | 5 | internal enum RouteEdge { 6 | case dot 7 | case slash 8 | case literal(String) 9 | } 10 | 11 | extension RouteEdge: Hashable, CustomDebugStringConvertible, CustomStringConvertible { 12 | var description: String { 13 | switch self { 14 | case .literal(let value): 15 | return value 16 | case .dot: 17 | return "." 18 | case .slash: 19 | return "/" 20 | } 21 | } 22 | 23 | var debugDescription: String { 24 | return self.description 25 | } 26 | 27 | func hash(into hasher: inout Hasher) { 28 | hasher.combine(description) 29 | } 30 | } 31 | 32 | internal class RouteVertex { 33 | var namedRoutes: [RouteEdge: RouteVertex] = [:] 34 | var parameterRoute: (String, RouteVertex)? 35 | var patternIdentifier: PatternIdentifier? 36 | 37 | init(patternIdentifier: PatternIdentifier? = nil) { 38 | self.patternIdentifier = patternIdentifier 39 | } 40 | 41 | var isTerminal: Bool { 42 | return self.patternIdentifier != nil 43 | } 44 | 45 | var isFinale: Bool { 46 | return self.namedRoutes.isEmpty && self.parameterRoute == nil 47 | } 48 | } 49 | 50 | extension RouteVertex: CustomStringConvertible, CustomDebugStringConvertible { 51 | var description: String { 52 | var str = "RouteVertex {\n" 53 | 54 | str += " patternIdentifier: \(String(describing: patternIdentifier))\n" 55 | 56 | if namedRoutes.count > 0 { 57 | str += " nextRoutes: [\n" 58 | for (edge, subVertex) in self.namedRoutes { 59 | str += " \"\(edge.description)\": {\n" 60 | str += " \(subVertex.description.replacingOccurrences(of: "\n", with: "\n "))\n" 61 | str += " },\n" 62 | } 63 | str += " ]\n" 64 | } else { 65 | str += " nextRoutes: []\n" 66 | } 67 | 68 | if let epsilonRoute = self.parameterRoute { 69 | str += " episilonRoute: {\n" 70 | str += " \"\(epsilonRoute.0)\": {\n" 71 | str += " \(epsilonRoute.1.description.replacingOccurrences(of: "\n", with: "\n "))\n" 72 | str += " }\n" 73 | str += " }\n" 74 | } else { 75 | str += " episilonRoute: nil\n" 76 | } 77 | 78 | str += "}" 79 | 80 | return str 81 | } 82 | 83 | var debugDescription: String { 84 | return self.description 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/RouterX/RoutingPatternParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal enum RoutingPatternParserError: Error { 4 | case unexpectToken(got: RoutingPatternToken?, message: String) 5 | case ambiguousOptionalPattern 6 | } 7 | 8 | internal class RoutingPatternParser { 9 | typealias RoutingPatternTokenGenerator = IndexingIterator> 10 | 11 | private let routingPatternTokens: [RoutingPatternToken] 12 | private let patternIdentifier: PatternIdentifier 13 | 14 | init(routingPatternTokens: [RoutingPatternToken], patternIdentifier: PatternIdentifier) { 15 | self.routingPatternTokens = routingPatternTokens 16 | self.patternIdentifier = patternIdentifier 17 | } 18 | 19 | class func parseAndAppendTo(_ rootRoute: RouteVertex, routingPatternTokens: [RoutingPatternToken], patternIdentifier: PatternIdentifier) throws { 20 | let parser = RoutingPatternParser(routingPatternTokens: routingPatternTokens, patternIdentifier: patternIdentifier) 21 | try parser.parseAndAppendTo(rootRoute) 22 | } 23 | 24 | func parseAndAppendTo(_ rootRoute: RouteVertex) throws { 25 | var tokenGenerator = self.routingPatternTokens.makeIterator() 26 | if let token = tokenGenerator.next() { 27 | switch token { 28 | case .slash: 29 | try parseSlash(rootRoute, generator: tokenGenerator) 30 | default: 31 | throw RoutingPatternParserError.unexpectToken(got: token, message: "Pattern must start with slash.") 32 | } 33 | } else { 34 | rootRoute.patternIdentifier = self.patternIdentifier 35 | } 36 | } 37 | 38 | func parseSlash(_ context: RouteVertex, generator: RoutingPatternTokenGenerator) throws { 39 | var generator = generator 40 | 41 | guard let nextToken = generator.next() else { 42 | if let terminalRoute = context.namedRoutes[.slash] { 43 | assignPatternIdentifierIfNil(terminalRoute) 44 | } else { 45 | context.namedRoutes[.slash] = RouteVertex(patternIdentifier: self.patternIdentifier) 46 | } 47 | return 48 | } 49 | 50 | var nextRoute: RouteVertex! 51 | if let route = context.namedRoutes[.slash] { 52 | nextRoute = route 53 | } else { 54 | nextRoute = RouteVertex() 55 | context.namedRoutes[.slash] = nextRoute 56 | } 57 | 58 | switch nextToken { 59 | case let .literal(value): 60 | try parseLiteral(nextRoute, value: value, generator: generator) 61 | case let .symbol(value): 62 | try parseSymbol(nextRoute, value: value, generator: generator) 63 | case let .star(value): 64 | try parseStar(nextRoute, value: value, generator: generator) 65 | case .lParen: 66 | try parseLParen(nextRoute, generator: generator) 67 | default: 68 | throw RoutingPatternParserError.unexpectToken(got: nextToken, message: "Unexpect \(nextToken)") 69 | } 70 | } 71 | 72 | func parseLParen(_ context: RouteVertex, isFirstEnter: Bool = true, generator: RoutingPatternTokenGenerator) throws { 73 | var generator = generator 74 | 75 | if isFirstEnter && !context.isFinale { 76 | throw RoutingPatternParserError.ambiguousOptionalPattern 77 | } 78 | 79 | assignPatternIdentifierIfNil(context) 80 | 81 | var subTokens: [RoutingPatternToken] = [] 82 | var parenPairingCount = 0 83 | while let token = generator.next() { 84 | if token == .lParen { 85 | parenPairingCount += 1 86 | } else if token == .rParen { 87 | if parenPairingCount == 0 { 88 | break 89 | } else if parenPairingCount > 0 { 90 | parenPairingCount -= 1 91 | } else { 92 | throw RoutingPatternParserError.unexpectToken(got: .rParen, message: "Unexpect \(token)") 93 | } 94 | } 95 | 96 | subTokens.append(token) 97 | } 98 | 99 | var subGenerator = subTokens.makeIterator() 100 | if let token = subGenerator.next() { 101 | for ctx in contextTerminals(context) { 102 | switch token { 103 | case .slash: 104 | try parseSlash(ctx, generator: subGenerator) 105 | case .dot: 106 | try parseDot(ctx, generator: subGenerator) 107 | default: 108 | throw RoutingPatternParserError.unexpectToken(got: token, message: "Unexpect \(token)") 109 | } 110 | } 111 | } 112 | 113 | if let nextToken = generator.next() { 114 | if nextToken == .lParen { 115 | try parseLParen(context, isFirstEnter: false, generator: generator) 116 | } else { 117 | throw RoutingPatternParserError.unexpectToken(got: nextToken, message: "Unexpect \(nextToken)") 118 | } 119 | } 120 | } 121 | 122 | private func parseDot(_ context: RouteVertex, generator: RoutingPatternTokenGenerator) throws { 123 | var generator = generator 124 | 125 | guard let nextToken = generator.next() else { 126 | throw RoutingPatternParserError.unexpectToken(got: nil, message: "Expect a token after \".\"") 127 | } 128 | 129 | var nextRoute: RouteVertex! 130 | if let route = context.namedRoutes[.dot] { 131 | nextRoute = route 132 | } else { 133 | nextRoute = RouteVertex() 134 | context.namedRoutes[.dot] = nextRoute 135 | } 136 | 137 | switch nextToken { 138 | case let .literal(value): 139 | try parseLiteral(nextRoute, value: value, generator: generator) 140 | case let .symbol(value): 141 | try parseSymbol(nextRoute, value: value, generator: generator) 142 | default: 143 | throw RoutingPatternParserError.unexpectToken(got: nextToken, message: "Unexpect \(nextToken)") 144 | } 145 | } 146 | 147 | private func parseLiteral(_ context: RouteVertex, value: String, generator: RoutingPatternTokenGenerator) throws { 148 | var generator = generator 149 | 150 | guard let nextToken = generator.next() else { 151 | if let terminalRoute = context.namedRoutes[.literal(value)] { 152 | assignPatternIdentifierIfNil(terminalRoute) 153 | } else { 154 | context.namedRoutes[.literal(value)] = RouteVertex(patternIdentifier: self.patternIdentifier) 155 | } 156 | 157 | return 158 | } 159 | 160 | var nextRoute: RouteVertex! 161 | if let route = context.namedRoutes[.literal(value)] { 162 | nextRoute = route 163 | } else { 164 | nextRoute = RouteVertex() 165 | context.namedRoutes[.literal(value)] = nextRoute 166 | } 167 | 168 | switch nextToken { 169 | case .slash: 170 | try parseSlash(nextRoute, generator: generator) 171 | case .dot: 172 | try parseDot(nextRoute, generator: generator) 173 | case .lParen: 174 | try parseLParen(nextRoute, generator: generator) 175 | default: 176 | throw RoutingPatternParserError.unexpectToken(got: nextToken, message: "Unexpect \(nextToken)") 177 | } 178 | } 179 | 180 | private func parseSymbol(_ context: RouteVertex, value: String, generator: RoutingPatternTokenGenerator) throws { 181 | var generator = generator 182 | 183 | guard let nextToken = generator.next() else { 184 | if let terminalRoute = context.parameterRoute?.1 { 185 | assignPatternIdentifierIfNil(terminalRoute) 186 | } else { 187 | context.parameterRoute = (value, RouteVertex(patternIdentifier: self.patternIdentifier)) 188 | } 189 | 190 | return 191 | } 192 | 193 | var nextRoute: RouteVertex! 194 | if let route = context.parameterRoute?.1 { 195 | nextRoute = route 196 | } else { 197 | nextRoute = RouteVertex() 198 | context.parameterRoute = (value, nextRoute) 199 | } 200 | 201 | switch nextToken { 202 | case .slash: 203 | try parseSlash(nextRoute, generator: generator) 204 | case .dot: 205 | try parseDot(nextRoute, generator: generator) 206 | case .lParen: 207 | try parseLParen(nextRoute, generator: generator) 208 | default: 209 | throw RoutingPatternParserError.unexpectToken(got: nextToken, message: "Unexpect \(nextToken)") 210 | } 211 | } 212 | 213 | private func parseStar(_ context: RouteVertex, value: String, generator: RoutingPatternTokenGenerator) throws { 214 | var generator = generator 215 | 216 | if let nextToken = generator.next() { 217 | throw RoutingPatternParserError.unexpectToken(got: nextToken, message: "Unexpect \(nextToken)") 218 | } 219 | 220 | if let terminalRoute = context.parameterRoute?.1 { 221 | assignPatternIdentifierIfNil(terminalRoute) 222 | } else { 223 | context.parameterRoute = (value, RouteVertex(patternIdentifier: self.patternIdentifier)) 224 | } 225 | } 226 | 227 | private func contextTerminals(_ context: RouteVertex) -> [RouteVertex] { 228 | var contexts: [RouteVertex] = [] 229 | 230 | if context.isTerminal { 231 | contexts.append(context) 232 | } 233 | 234 | for ctx in context.namedRoutes.values { 235 | contexts.append(contentsOf: contextTerminals(ctx)) 236 | } 237 | 238 | if let ctx = context.parameterRoute?.1 { 239 | contexts.append(contentsOf: contextTerminals(ctx)) 240 | } 241 | 242 | return contexts 243 | } 244 | 245 | private func assignPatternIdentifierIfNil(_ context: RouteVertex) { 246 | if context.patternIdentifier == nil { 247 | context.patternIdentifier = self.patternIdentifier 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /Sources/RouterX/RoutingPatternScanner.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private enum _PatternScanTerminator: Character { 4 | case lParen = "(" 5 | case rParen = ")" 6 | case slash = "/" 7 | case dot = "." 8 | case star = "*" 9 | 10 | var jointFragment: (token: RoutingPatternToken?, fragment: String) { 11 | switch self { 12 | case .lParen: 13 | return (token: .lParen, fragment: "") 14 | case .rParen: 15 | return (token: .rParen, fragment: "") 16 | case .slash: 17 | return (token: .slash, fragment: "") 18 | case .dot: 19 | return (token: .dot, fragment: "") 20 | case .star: 21 | return (token: nil, fragment: "*") 22 | } 23 | } 24 | } 25 | 26 | internal struct RoutingPatternScanner { 27 | 28 | static func tokenize(_ pattern: PatternIdentifier) -> [RoutingPatternToken] { 29 | guard !pattern.isEmpty else { return [] } 30 | 31 | var appending = "" 32 | var result: [RoutingPatternToken] = pattern.reduce(into: []) { box, char in 33 | guard let terminator = _PatternScanTerminator(rawValue: char) else { 34 | appending.append(char) 35 | return 36 | } 37 | 38 | let jointFragment = terminator.jointFragment 39 | defer { 40 | if let token = jointFragment.token { 41 | box.append(token) 42 | } 43 | appending = jointFragment.fragment 44 | } 45 | 46 | guard let jointToken = _generateToken(expression: appending) else { return } 47 | box.append(jointToken) 48 | } 49 | 50 | if let tailToken = _generateToken(expression: appending) { 51 | result.append(tailToken) 52 | } 53 | return result 54 | } 55 | 56 | static private func _generateToken(expression: String) -> RoutingPatternToken? { 57 | guard let firstChar = expression.first else { return nil } 58 | let fragments = String(expression.dropFirst()) 59 | switch firstChar { 60 | case ":": 61 | return .symbol(fragments) 62 | case "*": 63 | return .star(fragments) 64 | default: 65 | return .literal(expression) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/RouterX/RoutingPatternToken.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal enum RoutingPatternToken { 4 | case slash 5 | case dot 6 | 7 | case literal(String) 8 | case symbol(String) 9 | case star(String) 10 | 11 | case lParen 12 | case rParen 13 | } 14 | 15 | extension RoutingPatternToken: CustomStringConvertible, CustomDebugStringConvertible { 16 | var description: String { 17 | switch self { 18 | case .slash: 19 | return "/" 20 | case .dot: 21 | return "." 22 | case .lParen: 23 | return "(" 24 | case .rParen: 25 | return ")" 26 | case .literal(let value): 27 | return value 28 | case .symbol(let value): 29 | return ":\(value)" 30 | case .star(let value): 31 | return "*\(value)" 32 | } 33 | } 34 | 35 | var debugDescription: String { 36 | switch self { 37 | case .slash: 38 | return "[Slash]" 39 | case .dot: 40 | return "[Dot]" 41 | case .lParen: 42 | return "[LParen]" 43 | case .rParen: 44 | return "[RParen]" 45 | case .literal(let value): 46 | return "[Literal \"\(value)\"]" 47 | case .symbol(let value): 48 | return "[Symbol \"\(value)\"]" 49 | case .star(let value): 50 | return "[Star \"\(value)\"]" 51 | } 52 | } 53 | } 54 | 55 | extension RoutingPatternToken: Hashable { 56 | 57 | func hash(into hasher: inout Hasher) { 58 | hasher.combine(description) 59 | } 60 | } 61 | 62 | func == (lhs: RoutingPatternToken, rhs: RoutingPatternToken) -> Bool { 63 | switch (lhs, rhs) { 64 | case (.slash, .slash): 65 | return true 66 | case (.dot, .dot): 67 | return true 68 | case (let .literal(lval), let .literal(rval)), 69 | (let .symbol(lval), let .symbol(rval)), 70 | (let .star(lval), let .star(rval)): 71 | return lval == rval 72 | case (.lParen, .lParen): 73 | return true 74 | case (.rParen, .rParen): 75 | return true 76 | default: 77 | return false 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/RouterX/URLPathScanner.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal struct URLPathScanner { 4 | private static let stopWordsSet: Set = [".", "/"] 5 | 6 | let path: String 7 | private(set) var startIndex: String.Index 8 | 9 | init(path: String) { 10 | self.path = path 11 | self.startIndex = self.path.startIndex 12 | } 13 | 14 | var isEOF: Bool { 15 | return self.startIndex == self.path.endIndex 16 | } 17 | 18 | private var unScannedFragment: String { 19 | return String(path[startIndex ..< path.endIndex]) 20 | } 21 | 22 | mutating func nextToken() -> URLPathToken? { 23 | let unScanned = unScannedFragment 24 | guard let firstChar = unScanned.first else { 25 | // Is end of file 26 | return nil 27 | } 28 | 29 | let offset: Int 30 | 31 | defer { 32 | startIndex = path.index(startIndex, offsetBy: offset) 33 | } 34 | 35 | switch firstChar { 36 | case "/": 37 | offset = 1 38 | return .slash 39 | case ".": 40 | offset = 1 41 | return .dot 42 | default: 43 | break 44 | } 45 | 46 | let clipStep = unScanned.firstIndex(where: { URLPathScanner.stopWordsSet.contains($0) }) ?? unScanned.endIndex 47 | let literal = unScanned[unScanned.startIndex.. [URLPathToken] { 54 | var scanner = self.init(path: path) 55 | 56 | var tokens: [URLPathToken] = [] 57 | while let token = scanner.nextToken() { 58 | tokens.append(token) 59 | } 60 | 61 | return tokens 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/RouterX/URLPathToken.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal enum URLPathToken { 4 | case slash 5 | case dot 6 | case literal(String) 7 | 8 | var routeEdge: RouteEdge { 9 | switch self { 10 | case .slash: 11 | return .slash 12 | case .dot: 13 | return .dot 14 | case let .literal(value): 15 | return .literal(value) 16 | } 17 | } 18 | } 19 | 20 | extension URLPathToken: CustomStringConvertible, CustomDebugStringConvertible { 21 | var description: String { 22 | switch self { 23 | case .slash: 24 | return "/" 25 | case .dot: 26 | return "." 27 | case .literal(let value): 28 | return value 29 | } 30 | } 31 | 32 | var debugDescription: String { 33 | switch self { 34 | case .slash: 35 | return "[Slash]" 36 | case .dot: 37 | return "[Dot]" 38 | case .literal(let value): 39 | return "[Literal \"\(value)\"]" 40 | } 41 | } 42 | } 43 | 44 | extension URLPathToken: Equatable { } 45 | 46 | func == (lhs: URLPathToken, rhs: URLPathToken) -> Bool { 47 | switch (lhs, rhs) { 48 | case (.slash, .slash): 49 | return true 50 | case (.dot, .dot): 51 | return true 52 | case (let .literal(lval), let .literal(rval)): 53 | return lval == rval 54 | default: 55 | return false 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import RouterXTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += RouterXTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/RouterXTests/Extensions/RoutingGraph.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import RouterX 3 | 4 | extension RouteVertex { 5 | func toNextVertex(_ token: RouteEdge) -> RouteVertex? { 6 | switch token { 7 | case .slash: 8 | return self.namedRoutes[.slash] 9 | case .dot: 10 | return self.namedRoutes[.dot] 11 | default: 12 | return self.namedRoutes[token] ?? self.parameterRoute?.1 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/RouterXTests/RouterTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import RouterX 4 | 5 | final class RouterTests: XCTestCase { 6 | func testIntegration() { 7 | let router = Router() 8 | 9 | let pattern1 = "/articles(/page/:page(/per_page/:per_page))(/sort/:sort)(.:format)" 10 | let pattern1Case = "/articles/page/2/sort/recent.json" 11 | var isPattern1HandlerPerformed = false 12 | let pattern1Handler: Router.MatchedHandler = { result in 13 | isPattern1HandlerPerformed = true 14 | 15 | XCTAssertEqual(result.parameters["page"], "2") 16 | XCTAssertEqual(result.parameters["format"], "json") 17 | XCTAssertEqual(result.parameters["sort"], "recent") 18 | XCTAssertTrue(result.context == "foo", "context must be foo") 19 | } 20 | 21 | XCTAssertNoThrow(try router.register(pattern: pattern1, handler: pattern1Handler)) 22 | 23 | XCTAssertTrue(router.match(pattern1Case, context: "foo")) 24 | XCTAssertTrue(isPattern1HandlerPerformed) 25 | 26 | let unmatchedCase = "/articles/2/edit" 27 | var isUnmatchHandlerPerformed = false 28 | 29 | XCTAssertFalse(router.match(unmatchedCase, unmatchHandler: { (_, _) in 30 | isUnmatchHandlerPerformed = true 31 | })) 32 | XCTAssertTrue(isUnmatchHandlerPerformed) 33 | 34 | let pattern2 = "/band/:band_id/product" 35 | let pattern2Case1 = "/band/20/product" 36 | let pattern2Case2 = "/band/21" 37 | let pattern2Case3 = "/band" 38 | 39 | XCTAssertNoThrow(try router.register(pattern: pattern2, handler: { result in 40 | XCTAssertEqual(result.parameters["band_id"], "20") 41 | XCTAssertEqual(result.parameters.count, 1) 42 | })) 43 | 44 | XCTAssertTrue(router.match(pattern2Case1)) 45 | XCTAssertFalse(router.match(pattern2Case2)) 46 | XCTAssertFalse(router.match(pattern2Case3)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/RouterXTests/RouterXCoreTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import RouterX 4 | 5 | final class RouterXCoreTests: XCTestCase { 6 | 7 | func testIntegration() { 8 | let core = RouterXCore() 9 | let pattern1 = "/articles(/page/:page(/per_page/:per_page))(/sort/:sort)(.:format)" 10 | let pattern1Case = URL(string: "/articles/page/2/sort/recent.json")! 11 | 12 | XCTAssertNoThrow(try core.register(pattern: pattern1)) 13 | 14 | guard let pattern1Matched = core.match(pattern1Case) else { 15 | XCTFail("\(pattern1Case) should be matched") 16 | return 17 | } 18 | 19 | XCTAssertEqual(pattern1Matched.patternIdentifier, pattern1) 20 | XCTAssertEqual(pattern1Matched.parametars["page"], "2") 21 | XCTAssertEqual(pattern1Matched.parametars["sort"], "recent") 22 | XCTAssertEqual(pattern1Matched.parametars["format"], "json") 23 | 24 | let unmatchedCase = URL(string: "/articles/2/edit")! 25 | 26 | XCTAssertNil(core.match(unmatchedCase)) 27 | } 28 | 29 | func testPatternMustStartWithSlash() { 30 | let core = RouterXCore() 31 | let invalidPattern = "invalid/:id" 32 | let validPattern = "/valid/:id" 33 | 34 | XCTAssertThrowsError(try core.register(pattern: invalidPattern), "Must be start with slash") { error in 35 | var succeed = false 36 | if let expectError = error as? PatternRegisterError, 37 | case .missingPrefixSlash = expectError { 38 | succeed = true 39 | } 40 | XCTAssertTrue(succeed) 41 | } 42 | XCTAssertNoThrow(try core.register(pattern: validPattern)) 43 | } 44 | 45 | func testPatternCanNotRegisterEmpty() { 46 | let core = RouterXCore() 47 | let invalidPattern = "" 48 | let validPattern = "/valid/:id" 49 | 50 | XCTAssertThrowsError(try core.register(pattern: invalidPattern), "Can not register an empty pattern") { error in 51 | var succeed = false 52 | if let expectError = error as? PatternRegisterError, 53 | case .empty = expectError { 54 | succeed = true 55 | } 56 | XCTAssertTrue(succeed) 57 | } 58 | XCTAssertNoThrow(try core.register(pattern: validPattern)) 59 | } 60 | 61 | func testPatternGlobbingMustFollowSlash() { 62 | let core = RouterXCore() 63 | let invalidPattern1 = "/slash/body*" 64 | let invalidPattern2 = "/slash/:id*name" 65 | let validPattern = "/valid/:id" 66 | 67 | XCTAssertThrowsError(try core.register(pattern: invalidPattern1), "globbing must follow slash") { error in 68 | var succeed = false 69 | if let expectError = error as? PatternRegisterError, 70 | case .invalidGlobbing(let globbing, let previous) = expectError { 71 | if globbing == "" && previous == "/slash/body" { 72 | succeed = true 73 | } 74 | XCTAssert(succeed, "invalid scanned result") 75 | } 76 | XCTAssertTrue(succeed) 77 | } 78 | 79 | XCTAssertThrowsError(try core.register(pattern: invalidPattern2), "globbing must follow slash") { error in 80 | var succeed = false 81 | if let expectError = error as? PatternRegisterError, 82 | case .invalidGlobbing(let globbing, let previous) = expectError { 83 | if globbing == "name" && previous == "/slash/:id" { 84 | succeed = true 85 | } 86 | XCTAssert(succeed, "invalid scanned result") 87 | } 88 | XCTAssertTrue(succeed) 89 | } 90 | 91 | XCTAssertNoThrow(try core.register(pattern: validPattern)) 92 | } 93 | 94 | func testPatternParenthesisMustComeInPairsAndBalance() { 95 | var core = RouterXCore() 96 | let invalidSingleParenthesisPattern = "/invalid(/foo/:foo" 97 | let validSingleeParenthesisPattern = "/valid/(/foo/:foo)" 98 | 99 | XCTAssertThrowsError(try core.register(pattern: invalidSingleParenthesisPattern), "Parenthesis in pattern must come in pairs") { error in 100 | var succeed = false 101 | if let expectError = error as? PatternRegisterError, 102 | case .unbalanceParenthesis = expectError { 103 | succeed = true 104 | } 105 | XCTAssertTrue(succeed) 106 | } 107 | XCTAssertNoThrow(try core.register(pattern: validSingleeParenthesisPattern)) 108 | 109 | core = RouterXCore() 110 | let invalidMultipleParenthesisPattern1 = "/invalid(/foo/:foo(/bar/:bar)" 111 | let validMultipleParenthesisPattern1 = "/invalid(/foo/:foo(/bar/:bar))" 112 | 113 | XCTAssertThrowsError(try core.register(pattern: invalidMultipleParenthesisPattern1), "Parenthesis in pattern must come in pairs") { error in 114 | var succeed = false 115 | if let expectError = error as? PatternRegisterError, 116 | case .unbalanceParenthesis = expectError { 117 | succeed = true 118 | } 119 | XCTAssertTrue(succeed) 120 | } 121 | XCTAssertNoThrow(try core.register(pattern: validMultipleParenthesisPattern1)) 122 | 123 | core = RouterXCore() 124 | let invalidMultipleParenthesisPattern2 = "/invalid(/foo/:foo(/bar/:bar(/zoo/:zoo))" 125 | let validMultipleParenthesisPattern2 = "/invalid(/foo/:foo(/bar/:bar(/zoo/:zoo)))" 126 | 127 | XCTAssertThrowsError(try core.register(pattern: invalidMultipleParenthesisPattern2), "Parenthesis in pattern must come in pairs") { error in 128 | var succeed = false 129 | if let expectError = error as? PatternRegisterError, 130 | case .unbalanceParenthesis = expectError { 131 | succeed = true 132 | } 133 | XCTAssertTrue(succeed) 134 | } 135 | 136 | XCTAssertNoThrow(try core.register(pattern: validMultipleParenthesisPattern2)) 137 | 138 | core = RouterXCore() 139 | let invalidMultipleParenthesisPattern3 = "/invalid(/foo/:foo(/bar/:bar))(/zoo/:zoo" 140 | let validMultipleParenthesisPattern3 = "/invalid(/foo/:foo(/bar/:bar))(/zoo/:zoo)" 141 | 142 | XCTAssertThrowsError(try core.register(pattern: invalidMultipleParenthesisPattern3), "Parenthesis in pattern must come in pairs") { error in 143 | var succeed = false 144 | if let expectError = error as? PatternRegisterError, 145 | case .unbalanceParenthesis = expectError { 146 | succeed = true 147 | } 148 | XCTAssertTrue(succeed) 149 | } 150 | XCTAssertNoThrow(try core.register(pattern: validMultipleParenthesisPattern3)) 151 | 152 | core = RouterXCore() 153 | let invalidMultipleParenthesisPattern4 = "/invalid)/foo/:foo(" 154 | let validMultipleParenthesisPattern4 = "/invalid(/foo/:foo(/bar/:bar))(/zoo/:zoo)" 155 | 156 | XCTAssertThrowsError(try core.register(pattern: invalidMultipleParenthesisPattern4), "Parenthesis in pattern must come in pairs, and balance") { error in 157 | var succeed = false 158 | if let expectError = error as? PatternRegisterError, 159 | case PatternRegisterError.unexpectToken(after: let previous) = expectError, 160 | previous == "/invalid" { 161 | succeed = true 162 | } 163 | XCTAssertTrue(succeed) 164 | } 165 | XCTAssertNoThrow(try core.register(pattern: validMultipleParenthesisPattern4)) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Tests/RouterXTests/RoutingPatternParserTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import RouterX 4 | 5 | final class RoutingPatternParserTests: XCTestCase { 6 | let patternIdentifier: RouterX.PatternIdentifier = "/test/path" 7 | 8 | func testParsingFailureShouldThrowError() { 9 | let badTokens: [RoutingPatternToken] = [.rParen, .literal("bad")] 10 | let route = RouteVertex() 11 | let parser = RoutingPatternParser(routingPatternTokens: badTokens, patternIdentifier: self.patternIdentifier) 12 | 13 | do { 14 | try parser.parseAndAppendTo(route) 15 | XCTFail("Parse bad pattern should throw error.") 16 | } catch { } 17 | } 18 | 19 | func testParseSlash() { 20 | var parser: RoutingPatternParser! 21 | var route: RouteVertex! 22 | var tokens: [RoutingPatternToken]! 23 | 24 | do { 25 | tokens = [.slash] 26 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 27 | route = RouteVertex() 28 | 29 | try parser.parseAndAppendTo(route) 30 | 31 | XCTAssertNotNil(route.toNextVertex(.slash)) 32 | 33 | let cases: [[RoutingPatternToken]] = [ 34 | [.slash, .literal("me")], 35 | [.slash, .symbol("foo")], 36 | [.slash, .star("foo")], 37 | [.slash, .lParen, .rParen] 38 | ] 39 | 40 | for tokens in cases { 41 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 42 | route = RouteVertex() 43 | 44 | try parser.parseAndAppendTo(route) 45 | } 46 | } catch { 47 | XCTFail("Should not throw errors") 48 | } 49 | 50 | do { 51 | tokens = [.slash, .rParen] 52 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 53 | route = RouteVertex() 54 | try parser.parseAndAppendTo(route) 55 | 56 | XCTFail("Should throw errors") 57 | } catch { } 58 | } 59 | 60 | func testParseDot() { 61 | var parser: RoutingPatternParser! 62 | var route: RouteVertex! 63 | var tokens: [RoutingPatternToken]! 64 | 65 | do { 66 | tokens = [.slash, .literal("foo"), .dot] 67 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 68 | route = RouteVertex() 69 | try parser.parseAndAppendTo(route) 70 | 71 | XCTFail("Should throw errors") 72 | } catch { } 73 | 74 | do { 75 | let cases: [[RoutingPatternToken]] = [ 76 | [.slash, .literal("foo"), .dot, .literal("me")], 77 | [.slash, .literal("foo"), .dot, .symbol("foo")] 78 | ] 79 | 80 | for tokens in cases { 81 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 82 | route = RouteVertex() 83 | 84 | try parser.parseAndAppendTo(route) 85 | 86 | XCTAssertNotNil(route.toNextVertex(.slash)?.toNextVertex(.literal("foo"))?.toNextVertex(.dot)) 87 | } 88 | } catch { 89 | XCTFail("Should not throw errors") 90 | } 91 | 92 | do { 93 | tokens = [.slash, .literal("foo"), .dot, .dot] 94 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 95 | route = RouteVertex() 96 | try parser.parseAndAppendTo(route) 97 | 98 | XCTFail("Should throw errors") 99 | } catch { } 100 | } 101 | 102 | func testParseLiteral() { 103 | var parser: RoutingPatternParser! 104 | var route: RouteVertex! 105 | var tokens: [RoutingPatternToken]! 106 | 107 | do { 108 | tokens = [.slash, .literal("articles")] 109 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 110 | route = RouteVertex() 111 | 112 | try parser.parseAndAppendTo(route) 113 | 114 | XCTAssertNotNil(route.toNextVertex(.slash)?.toNextVertex(.literal("articles"))) 115 | 116 | let cases: [[RoutingPatternToken]] = [ 117 | [.slash, .literal("me"), .slash], 118 | [.slash, .literal("me"), .dot, .literal("bar")], 119 | [.slash, .literal("me"), .lParen, .rParen] 120 | ] 121 | 122 | for tokens in cases { 123 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 124 | route = RouteVertex() 125 | 126 | try parser.parseAndAppendTo(route) 127 | } 128 | } catch { 129 | XCTFail("Should not throw errors") 130 | } 131 | 132 | do { 133 | tokens = [.slash, .literal("foo"), .literal("bar")] 134 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 135 | route = RouteVertex() 136 | try parser.parseAndAppendTo(route) 137 | 138 | XCTFail("Should throw errors") 139 | } catch { } 140 | } 141 | 142 | func testParseSymbol() { 143 | var parser: RoutingPatternParser! 144 | var route: RouteVertex! 145 | var tokens: [RoutingPatternToken]! 146 | 147 | do { 148 | tokens = [.slash, .symbol("id")] 149 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 150 | route = RouteVertex() 151 | 152 | try parser.parseAndAppendTo(route) 153 | 154 | XCTAssertNotNil(route.toNextVertex(.slash)?.toNextVertex(.literal("123"))) 155 | 156 | let cases: [[RoutingPatternToken]] = [ 157 | [.slash, .symbol("id"), .slash], 158 | [.slash, .symbol("id"), .dot, .literal("js")], 159 | [.slash, .symbol("id"), .lParen, .rParen] 160 | ] 161 | 162 | for tokens in cases { 163 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 164 | route = RouteVertex() 165 | 166 | try parser.parseAndAppendTo(route) 167 | } 168 | } catch { 169 | XCTFail("Should not throw errors") 170 | } 171 | 172 | do { 173 | tokens = [.slash, .symbol("foo"), .symbol("bar")] 174 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 175 | route = RouteVertex() 176 | try parser.parseAndAppendTo(route) 177 | 178 | XCTFail("Should throw errors") 179 | } catch { } 180 | } 181 | 182 | func testStar() { 183 | var parser: RoutingPatternParser! 184 | var route: RouteVertex! 185 | var tokens: [RoutingPatternToken]! 186 | 187 | do { 188 | tokens = [.slash, .star("info"), .slash] 189 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 190 | route = RouteVertex() 191 | try parser.parseAndAppendTo(route) 192 | 193 | XCTFail("Should throw errors") 194 | } catch { } 195 | 196 | do { 197 | tokens = [.slash, .star("info")] 198 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 199 | route = RouteVertex() 200 | 201 | try parser.parseAndAppendTo(route) 202 | 203 | XCTAssertNotNil(route.toNextVertex(.slash)?.toNextVertex(.literal("123"))) 204 | } catch { 205 | XCTFail("Should not throw errors") 206 | } 207 | } 208 | 209 | func testParseLParen() { 210 | var parser: RoutingPatternParser! 211 | var route: RouteVertex! 212 | var tokens: [RoutingPatternToken]! 213 | 214 | do { 215 | tokens = [.slash, .lParen, .lParen, .rParen, .rParen, .rParen] 216 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 217 | route = RouteVertex() 218 | try parser.parseAndAppendTo(route) 219 | 220 | XCTFail("Should throw errors") 221 | } catch { } 222 | 223 | do { 224 | tokens = [.slash, .lParen, .slash, .literal("test"), .rParen] 225 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 226 | route = RouteVertex() 227 | 228 | try parser.parseAndAppendTo(route) 229 | 230 | tokens = [.slash, .lParen, .slash, .literal("test"), .rParen, .lParen, .slash, .symbol("foo"), .rParen] 231 | parser = RoutingPatternParser(routingPatternTokens: tokens, patternIdentifier: self.patternIdentifier) 232 | route = RouteVertex() 233 | 234 | try parser.parseAndAppendTo(route) 235 | } catch { 236 | XCTFail("Should not throw errors") 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /Tests/RouterXTests/RoutingPatternScannerTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import RouterX 4 | 5 | final class RoutingPatternScannerTests: XCTestCase { 6 | 7 | func testScanner() { 8 | let validCases: [String: Array] = [ 9 | "/": [.slash], 10 | "*omg": [.star("omg")], 11 | "/page": [.slash, .literal("page")], 12 | "/page/": [.slash, .literal("page"), .slash], 13 | "/page!": [.slash, .literal("page!")], 14 | "/page$": [.slash, .literal("page$")], 15 | "/page&": [.slash, .literal("page&")], 16 | "/page'": [.slash, .literal("page'")], 17 | "/page+": [.slash, .literal("page+")], 18 | "/page,": [.slash, .literal("page,")], 19 | "/page=": [.slash, .literal("page=")], 20 | "/page@": [.slash, .literal("page@")], 21 | "/~page": [.slash, .literal("~page")], 22 | "/pa-ge": [.slash, .literal("pa-ge")], 23 | "/:page": [.slash, .symbol("page")], 24 | "/(:page)": [.slash, .lParen, .symbol("page"), .rParen], 25 | "(/:action)": [.lParen, .slash, .symbol("action"), .rParen], 26 | "(())": [.lParen, .lParen, .rParen, .rParen], 27 | "(.:format)": [.lParen, .dot, .symbol("format"), .rParen] 28 | ] 29 | 30 | for (pattern, expect) in validCases { 31 | let tokens = RoutingPatternScanner.tokenize(pattern) 32 | 33 | XCTAssertEqual(tokens, expect) 34 | } 35 | 36 | let invalidCases: [String: [RoutingPatternToken]] = [ 37 | "/page*": [.slash, .literal("page*")] 38 | ] 39 | 40 | for (pattern, expect) in invalidCases { 41 | let tokens = RoutingPatternScanner.tokenize(pattern) 42 | 43 | XCTAssertNotEqual(tokens, expect) 44 | } 45 | } 46 | 47 | func testRoundTrip() { 48 | let cases = [ 49 | "/", 50 | "/foo", 51 | "/foo/bar", 52 | "/foo/:id", 53 | "/:foo", 54 | "(/:foo)", 55 | "(/:foo)(/:bar)", 56 | "(/:foo(/:bar))", 57 | ".:format", 58 | ".xml", 59 | "/foo.:bar", 60 | "/foo(/:action)", 61 | "/foo(/:action)(/:bar)", 62 | "/foo(/:action(/:bar))", 63 | "*foo", 64 | "/*foo", 65 | "/bar/*foo", 66 | "/bar/(*foo)", 67 | "/sprockets.js(.:format)", 68 | "/(:locale)(.:format)" 69 | ] 70 | 71 | for pattern in cases { 72 | let tokens = RoutingPatternScanner.tokenize(pattern) 73 | let roundTripPattern = tokens.reduce("") { ($0 as String) + String(describing: $1) } 74 | 75 | XCTAssertEqual(roundTripPattern, pattern) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/RouterXTests/URLPathScannerTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import RouterX 4 | 5 | class URLPathScannerTests: XCTestCase { 6 | 7 | func testScanner() { 8 | let cases: [String: Array] = [ 9 | "/": [.slash], 10 | "//": [.slash, .slash], 11 | "/page": [.slash, .literal("page")], 12 | "/page/": [.slash, .literal("page"), .slash], 13 | "/page!": [.slash, .literal("page!")], 14 | "/page$": [.slash, .literal("page$")], 15 | "/page&": [.slash, .literal("page&")], 16 | "/page'": [.slash, .literal("page'")], 17 | "/page*": [.slash, .literal("page*")], 18 | "/page+": [.slash, .literal("page+")], 19 | "/page,": [.slash, .literal("page,")], 20 | "/page=": [.slash, .literal("page=")], 21 | "/page@": [.slash, .literal("page@")], 22 | "/~page": [.slash, .literal("~page")], 23 | "/pa-ge": [.slash, .literal("pa-ge")], 24 | "/pa ge": [.slash, .literal("pa ge")], 25 | "/page.json": [.slash, .literal("page"), .dot, .literal("json")] 26 | ] 27 | 28 | for (pattern, expect) in cases { 29 | let tokens = URLPathScanner.tokenize(pattern) 30 | 31 | XCTAssertEqual(tokens, expect) 32 | } 33 | } 34 | 35 | } 36 | --------------------------------------------------------------------------------