├── .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 | [](https://travis-ci.org/jasl/RouterX)
5 | [](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 |
--------------------------------------------------------------------------------