├── .gitignore ├── .travis.yml ├── Info.plist ├── JLRoutes.podspec ├── JLRoutes.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── JLRoutes-iOS.xcscheme │ └── JLRoutes.xcscheme ├── JLRoutes ├── JLRParsingUtilities.h ├── JLRParsingUtilities.m ├── JLRRouteDefinition.h ├── JLRRouteDefinition.m ├── JLRRouteHandler.h ├── JLRRouteHandler.m ├── JLRRouteRequest.h ├── JLRRouteRequest.m ├── JLRRouteResponse.h ├── JLRRouteResponse.m ├── JLRoutes.h └── JLRoutes.m ├── JLRoutesTests ├── Info.plist └── JLRoutesTests.m ├── LICENSE ├── Package.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode9.2 3 | branches: 4 | only: 5 | master 6 | script: travis_retry xcodebuild test -scheme 'JLRoutes-iOS' -configuration Debug -destination "platform=iOS Simulator,name=iPhone 6,OS=8.4" -destination "platform=iOS Simulator,name=iPhone 6,OS=9.3" -destination "platform=iOS Simulator,name=iPhone 6,OS=10.3.1" -destination "platform=iOS Simulator,name=iPhone 6,OS=latest" 7 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /JLRoutes.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "JLRoutes" 3 | s.version = "2.1" 4 | s.summary = "URL routing library for iOS with a simple block-based API." 5 | s.homepage = "https://github.com/joeldev/JLRoutes" 6 | s.license = "BSD 3-Clause \"New\" License" 7 | s.author = { "Joel Levin" => "joel@joeldev.com" } 8 | s.source = { :git => "https://github.com/joeldev/JLRoutes.git", :tag => "2.1" } 9 | s.framework = 'Foundation' 10 | s.requires_arc = true 11 | 12 | s.source_files = 'JLRoutes', 'JLRoutes/*.{h,m}' 13 | 14 | s.ios.deployment_target = '8.0' 15 | s.osx.deployment_target = '10.10' 16 | s.tvos.deployment_target = '9.0' 17 | end 18 | -------------------------------------------------------------------------------- /JLRoutes.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5C5AD9B51B45C07300ED25A3 /* JLRoutes.h in Headers */ = {isa = PBXBuildFile; fileRef = 5D33681A16C6DC9300F983AA /* JLRoutes.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 5C5AD9B61B45C07800ED25A3 /* JLRoutes.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D33681C16C6DC9300F983AA /* JLRoutes.m */; }; 12 | 5D33681616C6DC9300F983AA /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D33681516C6DC9300F983AA /* Foundation.framework */; }; 13 | 5D33681D16C6DC9300F983AA /* JLRoutes.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D33681C16C6DC9300F983AA /* JLRoutes.m */; }; 14 | 5D3CB0721C73DA2700870B55 /* JLRoutesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D3CB0711C73DA2700870B55 /* JLRoutesTests.m */; }; 15 | 5D3CB0741C73DA2700870B55 /* libJLRoutes.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D33681216C6DC9300F983AA /* libJLRoutes.a */; }; 16 | 5DA69C5C1DAB4C3A007C8E9C /* JLRParsingUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DA69C541DAB4C3A007C8E9C /* JLRParsingUtilities.h */; }; 17 | 5DA69C5D1DAB4C3A007C8E9C /* JLRParsingUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DA69C541DAB4C3A007C8E9C /* JLRParsingUtilities.h */; }; 18 | 5DA69C5E1DAB4C3A007C8E9C /* JLRParsingUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA69C551DAB4C3A007C8E9C /* JLRParsingUtilities.m */; }; 19 | 5DA69C5F1DAB4C3A007C8E9C /* JLRParsingUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA69C551DAB4C3A007C8E9C /* JLRParsingUtilities.m */; }; 20 | 5DA69C601DAB4C3A007C8E9C /* JLRRouteDefinition.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DA69C561DAB4C3A007C8E9C /* JLRRouteDefinition.h */; settings = {ATTRIBUTES = (Public, ); }; }; 21 | 5DA69C611DAB4C3A007C8E9C /* JLRRouteDefinition.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DA69C561DAB4C3A007C8E9C /* JLRRouteDefinition.h */; settings = {ATTRIBUTES = (Public, ); }; }; 22 | 5DA69C621DAB4C3A007C8E9C /* JLRRouteDefinition.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA69C571DAB4C3A007C8E9C /* JLRRouteDefinition.m */; }; 23 | 5DA69C631DAB4C3A007C8E9C /* JLRRouteDefinition.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA69C571DAB4C3A007C8E9C /* JLRRouteDefinition.m */; }; 24 | 5DA69C641DAB4C3A007C8E9C /* JLRRouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DA69C581DAB4C3A007C8E9C /* JLRRouteRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25 | 5DA69C651DAB4C3A007C8E9C /* JLRRouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DA69C581DAB4C3A007C8E9C /* JLRRouteRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; 26 | 5DA69C661DAB4C3A007C8E9C /* JLRRouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA69C591DAB4C3A007C8E9C /* JLRRouteRequest.m */; }; 27 | 5DA69C671DAB4C3A007C8E9C /* JLRRouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA69C591DAB4C3A007C8E9C /* JLRRouteRequest.m */; }; 28 | 5DA69C681DAB4C3A007C8E9C /* JLRRouteResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DA69C5A1DAB4C3A007C8E9C /* JLRRouteResponse.h */; settings = {ATTRIBUTES = (Public, ); }; }; 29 | 5DA69C691DAB4C3A007C8E9C /* JLRRouteResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DA69C5A1DAB4C3A007C8E9C /* JLRRouteResponse.h */; settings = {ATTRIBUTES = (Public, ); }; }; 30 | 5DA69C6A1DAB4C3A007C8E9C /* JLRRouteResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA69C5B1DAB4C3A007C8E9C /* JLRRouteResponse.m */; }; 31 | 5DA69C6B1DAB4C3A007C8E9C /* JLRRouteResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA69C5B1DAB4C3A007C8E9C /* JLRRouteResponse.m */; }; 32 | 5DE775661EA1B15200375C1D /* JLRRouteHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DE775641EA1B15200375C1D /* JLRRouteHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 33 | 5DE775671EA1B15200375C1D /* JLRRouteHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DE775641EA1B15200375C1D /* JLRRouteHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34 | 5DE775681EA1B15200375C1D /* JLRRouteHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DE775651EA1B15200375C1D /* JLRRouteHandler.m */; }; 35 | 5DE775691EA1B15200375C1D /* JLRRouteHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DE775651EA1B15200375C1D /* JLRRouteHandler.m */; }; 36 | D0C20A3017061066007746A6 /* JLRoutes.h in Headers */ = {isa = PBXBuildFile; fileRef = 5D33681A16C6DC9300F983AA /* JLRoutes.h */; settings = {ATTRIBUTES = (Public, ); }; }; 37 | /* End PBXBuildFile section */ 38 | 39 | /* Begin PBXContainerItemProxy section */ 40 | 5D3CB0751C73DA2700870B55 /* PBXContainerItemProxy */ = { 41 | isa = PBXContainerItemProxy; 42 | containerPortal = 5D33680A16C6DC9300F983AA /* Project object */; 43 | proxyType = 1; 44 | remoteGlobalIDString = 5D33681116C6DC9300F983AA; 45 | remoteInfo = JLRoutes; 46 | }; 47 | /* End PBXContainerItemProxy section */ 48 | 49 | /* Begin PBXFileReference section */ 50 | 5CB4EB491B45BF5B0058E91A /* JLRoutes.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JLRoutes.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 5D33681216C6DC9300F983AA /* libJLRoutes.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libJLRoutes.a; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 5D33681516C6DC9300F983AA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 53 | 5D33681A16C6DC9300F983AA /* JLRoutes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JLRoutes.h; sourceTree = ""; }; 54 | 5D33681C16C6DC9300F983AA /* JLRoutes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JLRoutes.m; sourceTree = ""; }; 55 | 5D33682616C6DC9300F983AA /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; 56 | 5D33684416C6DCCC00F983AA /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 57 | 5D3CB06F1C73DA2700870B55 /* JLRoutesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JLRoutesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | 5D3CB0711C73DA2700870B55 /* JLRoutesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JLRoutesTests.m; sourceTree = ""; }; 59 | 5D3CB0731C73DA2700870B55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 60 | 5DA69C541DAB4C3A007C8E9C /* JLRParsingUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JLRParsingUtilities.h; sourceTree = ""; }; 61 | 5DA69C551DAB4C3A007C8E9C /* JLRParsingUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JLRParsingUtilities.m; sourceTree = ""; }; 62 | 5DA69C561DAB4C3A007C8E9C /* JLRRouteDefinition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JLRRouteDefinition.h; sourceTree = ""; }; 63 | 5DA69C571DAB4C3A007C8E9C /* JLRRouteDefinition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JLRRouteDefinition.m; sourceTree = ""; }; 64 | 5DA69C581DAB4C3A007C8E9C /* JLRRouteRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JLRRouteRequest.h; sourceTree = ""; }; 65 | 5DA69C591DAB4C3A007C8E9C /* JLRRouteRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JLRRouteRequest.m; sourceTree = ""; }; 66 | 5DA69C5A1DAB4C3A007C8E9C /* JLRRouteResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JLRRouteResponse.h; sourceTree = ""; }; 67 | 5DA69C5B1DAB4C3A007C8E9C /* JLRRouteResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JLRRouteResponse.m; sourceTree = ""; }; 68 | 5DE775641EA1B15200375C1D /* JLRRouteHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JLRRouteHandler.h; sourceTree = ""; }; 69 | 5DE775651EA1B15200375C1D /* JLRRouteHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JLRRouteHandler.m; sourceTree = ""; }; 70 | /* End PBXFileReference section */ 71 | 72 | /* Begin PBXFrameworksBuildPhase section */ 73 | 5CB4EB451B45BF5B0058E91A /* Frameworks */ = { 74 | isa = PBXFrameworksBuildPhase; 75 | buildActionMask = 2147483647; 76 | files = ( 77 | ); 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | 5D33680F16C6DC9300F983AA /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | 5D33681616C6DC9300F983AA /* Foundation.framework in Frameworks */, 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | 5D3CB06C1C73DA2700870B55 /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | 5D3CB0741C73DA2700870B55 /* libJLRoutes.a in Frameworks */, 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | /* End PBXFrameworksBuildPhase section */ 97 | 98 | /* Begin PBXGroup section */ 99 | 5D33680916C6DC9300F983AA = { 100 | isa = PBXGroup; 101 | children = ( 102 | 5D33681716C6DC9300F983AA /* JLRoutes */, 103 | 5D3CB0701C73DA2700870B55 /* JLRoutesTests */, 104 | 5D33681416C6DC9300F983AA /* Frameworks */, 105 | 5D33681316C6DC9300F983AA /* Products */, 106 | ); 107 | sourceTree = ""; 108 | }; 109 | 5D33681316C6DC9300F983AA /* Products */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 5D33681216C6DC9300F983AA /* libJLRoutes.a */, 113 | 5CB4EB491B45BF5B0058E91A /* JLRoutes.framework */, 114 | 5D3CB06F1C73DA2700870B55 /* JLRoutesTests.xctest */, 115 | ); 116 | name = Products; 117 | sourceTree = ""; 118 | }; 119 | 5D33681416C6DC9300F983AA /* Frameworks */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 5D33681516C6DC9300F983AA /* Foundation.framework */, 123 | 5D33682616C6DC9300F983AA /* UIKit.framework */, 124 | 5D33684416C6DCCC00F983AA /* CoreGraphics.framework */, 125 | ); 126 | name = Frameworks; 127 | sourceTree = ""; 128 | }; 129 | 5D33681716C6DC9300F983AA /* JLRoutes */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 5D33681A16C6DC9300F983AA /* JLRoutes.h */, 133 | 5D33681C16C6DC9300F983AA /* JLRoutes.m */, 134 | 5DA69C531DAB4C3A007C8E9C /* Classes */, 135 | ); 136 | path = JLRoutes; 137 | sourceTree = ""; 138 | }; 139 | 5D3CB0701C73DA2700870B55 /* JLRoutesTests */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 5D3CB0711C73DA2700870B55 /* JLRoutesTests.m */, 143 | 5D3CB0731C73DA2700870B55 /* Info.plist */, 144 | ); 145 | path = JLRoutesTests; 146 | sourceTree = ""; 147 | }; 148 | 5DA69C531DAB4C3A007C8E9C /* Classes */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 5DA69C561DAB4C3A007C8E9C /* JLRRouteDefinition.h */, 152 | 5DA69C571DAB4C3A007C8E9C /* JLRRouteDefinition.m */, 153 | 5DE775641EA1B15200375C1D /* JLRRouteHandler.h */, 154 | 5DE775651EA1B15200375C1D /* JLRRouteHandler.m */, 155 | 5DA69C581DAB4C3A007C8E9C /* JLRRouteRequest.h */, 156 | 5DA69C591DAB4C3A007C8E9C /* JLRRouteRequest.m */, 157 | 5DA69C5A1DAB4C3A007C8E9C /* JLRRouteResponse.h */, 158 | 5DA69C5B1DAB4C3A007C8E9C /* JLRRouteResponse.m */, 159 | 5DA69C541DAB4C3A007C8E9C /* JLRParsingUtilities.h */, 160 | 5DA69C551DAB4C3A007C8E9C /* JLRParsingUtilities.m */, 161 | ); 162 | name = Classes; 163 | sourceTree = ""; 164 | }; 165 | /* End PBXGroup section */ 166 | 167 | /* Begin PBXHeadersBuildPhase section */ 168 | 5CB4EB461B45BF5B0058E91A /* Headers */ = { 169 | isa = PBXHeadersBuildPhase; 170 | buildActionMask = 2147483647; 171 | files = ( 172 | 5DA69C691DAB4C3A007C8E9C /* JLRRouteResponse.h in Headers */, 173 | 5DA69C5D1DAB4C3A007C8E9C /* JLRParsingUtilities.h in Headers */, 174 | 5DE775671EA1B15200375C1D /* JLRRouteHandler.h in Headers */, 175 | 5DA69C611DAB4C3A007C8E9C /* JLRRouteDefinition.h in Headers */, 176 | 5C5AD9B51B45C07300ED25A3 /* JLRoutes.h in Headers */, 177 | 5DA69C651DAB4C3A007C8E9C /* JLRRouteRequest.h in Headers */, 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | }; 181 | D0C20A2F1706105A007746A6 /* Headers */ = { 182 | isa = PBXHeadersBuildPhase; 183 | buildActionMask = 2147483647; 184 | files = ( 185 | 5DA69C681DAB4C3A007C8E9C /* JLRRouteResponse.h in Headers */, 186 | 5DA69C5C1DAB4C3A007C8E9C /* JLRParsingUtilities.h in Headers */, 187 | 5DE775661EA1B15200375C1D /* JLRRouteHandler.h in Headers */, 188 | 5DA69C601DAB4C3A007C8E9C /* JLRRouteDefinition.h in Headers */, 189 | D0C20A3017061066007746A6 /* JLRoutes.h in Headers */, 190 | 5DA69C641DAB4C3A007C8E9C /* JLRRouteRequest.h in Headers */, 191 | ); 192 | runOnlyForDeploymentPostprocessing = 0; 193 | }; 194 | /* End PBXHeadersBuildPhase section */ 195 | 196 | /* Begin PBXNativeTarget section */ 197 | 5CB4EB481B45BF5B0058E91A /* JLRoutes-iOS */ = { 198 | isa = PBXNativeTarget; 199 | buildConfigurationList = 5CB4EB601B45BF5B0058E91A /* Build configuration list for PBXNativeTarget "JLRoutes-iOS" */; 200 | buildPhases = ( 201 | 5CB4EB441B45BF5B0058E91A /* Sources */, 202 | 5CB4EB451B45BF5B0058E91A /* Frameworks */, 203 | 5CB4EB461B45BF5B0058E91A /* Headers */, 204 | 5CB4EB471B45BF5B0058E91A /* Resources */, 205 | ); 206 | buildRules = ( 207 | ); 208 | dependencies = ( 209 | ); 210 | name = "JLRoutes-iOS"; 211 | productName = "JLRoutes-iOS"; 212 | productReference = 5CB4EB491B45BF5B0058E91A /* JLRoutes.framework */; 213 | productType = "com.apple.product-type.framework"; 214 | }; 215 | 5D33681116C6DC9300F983AA /* JLRoutes */ = { 216 | isa = PBXNativeTarget; 217 | buildConfigurationList = 5D33683716C6DC9300F983AA /* Build configuration list for PBXNativeTarget "JLRoutes" */; 218 | buildPhases = ( 219 | 5D33680E16C6DC9300F983AA /* Sources */, 220 | 5D33680F16C6DC9300F983AA /* Frameworks */, 221 | D0C20A2F1706105A007746A6 /* Headers */, 222 | ); 223 | buildRules = ( 224 | ); 225 | dependencies = ( 226 | ); 227 | name = JLRoutes; 228 | productName = JLRoutes; 229 | productReference = 5D33681216C6DC9300F983AA /* libJLRoutes.a */; 230 | productType = "com.apple.product-type.library.static"; 231 | }; 232 | 5D3CB06E1C73DA2700870B55 /* JLRoutesTests */ = { 233 | isa = PBXNativeTarget; 234 | buildConfigurationList = 5D3CB0771C73DA2700870B55 /* Build configuration list for PBXNativeTarget "JLRoutesTests" */; 235 | buildPhases = ( 236 | 5D3CB06B1C73DA2700870B55 /* Sources */, 237 | 5D3CB06C1C73DA2700870B55 /* Frameworks */, 238 | 5D3CB06D1C73DA2700870B55 /* Resources */, 239 | ); 240 | buildRules = ( 241 | ); 242 | dependencies = ( 243 | 5D3CB0761C73DA2700870B55 /* PBXTargetDependency */, 244 | ); 245 | name = JLRoutesTests; 246 | productName = JLRoutesTests; 247 | productReference = 5D3CB06F1C73DA2700870B55 /* JLRoutesTests.xctest */; 248 | productType = "com.apple.product-type.bundle.unit-test"; 249 | }; 250 | /* End PBXNativeTarget section */ 251 | 252 | /* Begin PBXProject section */ 253 | 5D33680A16C6DC9300F983AA /* Project object */ = { 254 | isa = PBXProject; 255 | attributes = { 256 | LastTestingUpgradeCheck = 0510; 257 | LastUpgradeCheck = 0900; 258 | ORGANIZATIONNAME = "Afterwork Studios"; 259 | TargetAttributes = { 260 | 5CB4EB481B45BF5B0058E91A = { 261 | CreatedOnToolsVersion = 6.4; 262 | }; 263 | 5D3CB06E1C73DA2700870B55 = { 264 | CreatedOnToolsVersion = 7.3; 265 | }; 266 | }; 267 | }; 268 | buildConfigurationList = 5D33680D16C6DC9300F983AA /* Build configuration list for PBXProject "JLRoutes" */; 269 | compatibilityVersion = "Xcode 3.2"; 270 | developmentRegion = English; 271 | hasScannedForEncodings = 0; 272 | knownRegions = ( 273 | en, 274 | ); 275 | mainGroup = 5D33680916C6DC9300F983AA; 276 | productRefGroup = 5D33681316C6DC9300F983AA /* Products */; 277 | projectDirPath = ""; 278 | projectRoot = ""; 279 | targets = ( 280 | 5D33681116C6DC9300F983AA /* JLRoutes */, 281 | 5CB4EB481B45BF5B0058E91A /* JLRoutes-iOS */, 282 | 5D3CB06E1C73DA2700870B55 /* JLRoutesTests */, 283 | ); 284 | }; 285 | /* End PBXProject section */ 286 | 287 | /* Begin PBXResourcesBuildPhase section */ 288 | 5CB4EB471B45BF5B0058E91A /* Resources */ = { 289 | isa = PBXResourcesBuildPhase; 290 | buildActionMask = 2147483647; 291 | files = ( 292 | ); 293 | runOnlyForDeploymentPostprocessing = 0; 294 | }; 295 | 5D3CB06D1C73DA2700870B55 /* Resources */ = { 296 | isa = PBXResourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | ); 300 | runOnlyForDeploymentPostprocessing = 0; 301 | }; 302 | /* End PBXResourcesBuildPhase section */ 303 | 304 | /* Begin PBXSourcesBuildPhase section */ 305 | 5CB4EB441B45BF5B0058E91A /* Sources */ = { 306 | isa = PBXSourcesBuildPhase; 307 | buildActionMask = 2147483647; 308 | files = ( 309 | 5DA69C6B1DAB4C3A007C8E9C /* JLRRouteResponse.m in Sources */, 310 | 5DA69C671DAB4C3A007C8E9C /* JLRRouteRequest.m in Sources */, 311 | 5C5AD9B61B45C07800ED25A3 /* JLRoutes.m in Sources */, 312 | 5DA69C5F1DAB4C3A007C8E9C /* JLRParsingUtilities.m in Sources */, 313 | 5DA69C631DAB4C3A007C8E9C /* JLRRouteDefinition.m in Sources */, 314 | 5DE775691EA1B15200375C1D /* JLRRouteHandler.m in Sources */, 315 | ); 316 | runOnlyForDeploymentPostprocessing = 0; 317 | }; 318 | 5D33680E16C6DC9300F983AA /* Sources */ = { 319 | isa = PBXSourcesBuildPhase; 320 | buildActionMask = 2147483647; 321 | files = ( 322 | 5DA69C6A1DAB4C3A007C8E9C /* JLRRouteResponse.m in Sources */, 323 | 5DA69C661DAB4C3A007C8E9C /* JLRRouteRequest.m in Sources */, 324 | 5D33681D16C6DC9300F983AA /* JLRoutes.m in Sources */, 325 | 5DA69C5E1DAB4C3A007C8E9C /* JLRParsingUtilities.m in Sources */, 326 | 5DA69C621DAB4C3A007C8E9C /* JLRRouteDefinition.m in Sources */, 327 | 5DE775681EA1B15200375C1D /* JLRRouteHandler.m in Sources */, 328 | ); 329 | runOnlyForDeploymentPostprocessing = 0; 330 | }; 331 | 5D3CB06B1C73DA2700870B55 /* Sources */ = { 332 | isa = PBXSourcesBuildPhase; 333 | buildActionMask = 2147483647; 334 | files = ( 335 | 5D3CB0721C73DA2700870B55 /* JLRoutesTests.m in Sources */, 336 | ); 337 | runOnlyForDeploymentPostprocessing = 0; 338 | }; 339 | /* End PBXSourcesBuildPhase section */ 340 | 341 | /* Begin PBXTargetDependency section */ 342 | 5D3CB0761C73DA2700870B55 /* PBXTargetDependency */ = { 343 | isa = PBXTargetDependency; 344 | target = 5D33681116C6DC9300F983AA /* JLRoutes */; 345 | targetProxy = 5D3CB0751C73DA2700870B55 /* PBXContainerItemProxy */; 346 | }; 347 | /* End PBXTargetDependency section */ 348 | 349 | /* Begin XCBuildConfiguration section */ 350 | 5CB4EB5C1B45BF5B0058E91A /* Debug */ = { 351 | isa = XCBuildConfiguration; 352 | buildSettings = { 353 | CLANG_ENABLE_MODULES = YES; 354 | CLANG_WARN_BOOL_CONVERSION = YES; 355 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 356 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 357 | CLANG_WARN_UNREACHABLE_CODE = YES; 358 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 359 | CURRENT_PROJECT_VERSION = 1; 360 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 361 | DEFINES_MODULE = YES; 362 | DYLIB_COMPATIBILITY_VERSION = 1; 363 | DYLIB_CURRENT_VERSION = 1; 364 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 365 | ENABLE_STRICT_OBJC_MSGSEND = YES; 366 | GCC_NO_COMMON_BLOCKS = YES; 367 | GCC_PREPROCESSOR_DEFINITIONS = ( 368 | "DEBUG=1", 369 | "$(inherited)", 370 | ); 371 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 372 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 373 | GCC_WARN_UNDECLARED_SELECTOR = YES; 374 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 375 | GCC_WARN_UNUSED_FUNCTION = YES; 376 | INFOPLIST_FILE = Info.plist; 377 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 378 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 379 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 380 | MTL_ENABLE_DEBUG_INFO = YES; 381 | PRODUCT_BUNDLE_IDENTIFIER = "com.joeldev.$(PRODUCT_NAME:rfc1034identifier)"; 382 | PRODUCT_NAME = JLRoutes; 383 | SKIP_INSTALL = YES; 384 | TARGETED_DEVICE_FAMILY = "1,2"; 385 | VERSIONING_SYSTEM = "apple-generic"; 386 | VERSION_INFO_PREFIX = ""; 387 | }; 388 | name = Debug; 389 | }; 390 | 5CB4EB5D1B45BF5B0058E91A /* Release */ = { 391 | isa = XCBuildConfiguration; 392 | buildSettings = { 393 | CLANG_ENABLE_MODULES = YES; 394 | CLANG_WARN_BOOL_CONVERSION = YES; 395 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 396 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 397 | CLANG_WARN_UNREACHABLE_CODE = YES; 398 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 399 | COPY_PHASE_STRIP = NO; 400 | CURRENT_PROJECT_VERSION = 1; 401 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 402 | DEFINES_MODULE = YES; 403 | DYLIB_COMPATIBILITY_VERSION = 1; 404 | DYLIB_CURRENT_VERSION = 1; 405 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 406 | ENABLE_NS_ASSERTIONS = YES; 407 | ENABLE_STRICT_OBJC_MSGSEND = YES; 408 | GCC_NO_COMMON_BLOCKS = YES; 409 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 410 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 411 | GCC_WARN_UNDECLARED_SELECTOR = YES; 412 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 413 | GCC_WARN_UNUSED_FUNCTION = YES; 414 | INFOPLIST_FILE = Info.plist; 415 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 416 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 417 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 418 | MTL_ENABLE_DEBUG_INFO = NO; 419 | PRODUCT_BUNDLE_IDENTIFIER = "com.joeldev.$(PRODUCT_NAME:rfc1034identifier)"; 420 | PRODUCT_NAME = JLRoutes; 421 | SKIP_INSTALL = YES; 422 | TARGETED_DEVICE_FAMILY = "1,2"; 423 | VERSIONING_SYSTEM = "apple-generic"; 424 | VERSION_INFO_PREFIX = ""; 425 | }; 426 | name = Release; 427 | }; 428 | 5D33683516C6DC9300F983AA /* Debug */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | ALWAYS_SEARCH_USER_PATHS = NO; 432 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 433 | CLANG_CXX_LIBRARY = "libc++"; 434 | CLANG_ENABLE_OBJC_ARC = YES; 435 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 436 | CLANG_WARN_BOOL_CONVERSION = YES; 437 | CLANG_WARN_COMMA = YES; 438 | CLANG_WARN_CONSTANT_CONVERSION = YES; 439 | CLANG_WARN_EMPTY_BODY = YES; 440 | CLANG_WARN_ENUM_CONVERSION = YES; 441 | CLANG_WARN_INFINITE_RECURSION = YES; 442 | CLANG_WARN_INT_CONVERSION = YES; 443 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 444 | CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES; 445 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 446 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 447 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; 448 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 449 | CLANG_WARN_STRICT_PROTOTYPES = YES; 450 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 451 | CLANG_WARN_UNREACHABLE_CODE = YES; 452 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 453 | COPY_PHASE_STRIP = NO; 454 | ENABLE_STRICT_OBJC_MSGSEND = YES; 455 | ENABLE_TESTABILITY = YES; 456 | GCC_C_LANGUAGE_STANDARD = gnu99; 457 | GCC_DYNAMIC_NO_PIC = NO; 458 | GCC_NO_COMMON_BLOCKS = YES; 459 | GCC_OPTIMIZATION_LEVEL = 0; 460 | GCC_PREPROCESSOR_DEFINITIONS = ( 461 | "DEBUG=1", 462 | "$(inherited)", 463 | ); 464 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 465 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 466 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 467 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 468 | GCC_WARN_PEDANTIC = NO; 469 | GCC_WARN_UNDECLARED_SELECTOR = YES; 470 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 471 | GCC_WARN_UNUSED_FUNCTION = YES; 472 | GCC_WARN_UNUSED_VARIABLE = YES; 473 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 474 | ONLY_ACTIVE_ARCH = YES; 475 | SDKROOT = iphoneos; 476 | }; 477 | name = Debug; 478 | }; 479 | 5D33683616C6DC9300F983AA /* Release */ = { 480 | isa = XCBuildConfiguration; 481 | buildSettings = { 482 | ALWAYS_SEARCH_USER_PATHS = NO; 483 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 484 | CLANG_CXX_LIBRARY = "libc++"; 485 | CLANG_ENABLE_OBJC_ARC = YES; 486 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 487 | CLANG_WARN_BOOL_CONVERSION = YES; 488 | CLANG_WARN_COMMA = YES; 489 | CLANG_WARN_CONSTANT_CONVERSION = YES; 490 | CLANG_WARN_EMPTY_BODY = YES; 491 | CLANG_WARN_ENUM_CONVERSION = YES; 492 | CLANG_WARN_INFINITE_RECURSION = YES; 493 | CLANG_WARN_INT_CONVERSION = YES; 494 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 495 | CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES; 496 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 497 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 498 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; 499 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 500 | CLANG_WARN_STRICT_PROTOTYPES = YES; 501 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 502 | CLANG_WARN_UNREACHABLE_CODE = YES; 503 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 504 | COPY_PHASE_STRIP = YES; 505 | ENABLE_STRICT_OBJC_MSGSEND = YES; 506 | GCC_C_LANGUAGE_STANDARD = gnu99; 507 | GCC_NO_COMMON_BLOCKS = YES; 508 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 509 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 510 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 511 | GCC_WARN_PEDANTIC = NO; 512 | GCC_WARN_UNDECLARED_SELECTOR = YES; 513 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 514 | GCC_WARN_UNUSED_FUNCTION = YES; 515 | GCC_WARN_UNUSED_VARIABLE = YES; 516 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 517 | SDKROOT = iphoneos; 518 | VALIDATE_PRODUCT = YES; 519 | }; 520 | name = Release; 521 | }; 522 | 5D33683816C6DC9300F983AA /* Debug */ = { 523 | isa = XCBuildConfiguration; 524 | buildSettings = { 525 | DSTROOT = /tmp/JLRoutes.dst; 526 | GCC_PRECOMPILE_PREFIX_HEADER = NO; 527 | GCC_PREFIX_HEADER = ""; 528 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 529 | OTHER_LDFLAGS = "-ObjC"; 530 | PRODUCT_NAME = "$(TARGET_NAME)"; 531 | PUBLIC_HEADERS_FOLDER_PATH = "include/${PRODUCT_NAME}"; 532 | SKIP_INSTALL = YES; 533 | }; 534 | name = Debug; 535 | }; 536 | 5D33683916C6DC9300F983AA /* Release */ = { 537 | isa = XCBuildConfiguration; 538 | buildSettings = { 539 | DSTROOT = /tmp/JLRoutes.dst; 540 | GCC_PRECOMPILE_PREFIX_HEADER = NO; 541 | GCC_PREFIX_HEADER = ""; 542 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 543 | OTHER_LDFLAGS = "-ObjC"; 544 | PRODUCT_NAME = "$(TARGET_NAME)"; 545 | PUBLIC_HEADERS_FOLDER_PATH = "include/${PRODUCT_NAME}"; 546 | SKIP_INSTALL = YES; 547 | }; 548 | name = Release; 549 | }; 550 | 5D3CB0781C73DA2700870B55 /* Debug */ = { 551 | isa = XCBuildConfiguration; 552 | buildSettings = { 553 | CLANG_ANALYZER_NONNULL = YES; 554 | CLANG_ENABLE_MODULES = YES; 555 | CLANG_WARN_BOOL_CONVERSION = YES; 556 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 557 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 558 | CLANG_WARN_UNREACHABLE_CODE = YES; 559 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 560 | DEBUG_INFORMATION_FORMAT = dwarf; 561 | ENABLE_STRICT_OBJC_MSGSEND = YES; 562 | GCC_NO_COMMON_BLOCKS = YES; 563 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 564 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 565 | GCC_WARN_UNDECLARED_SELECTOR = YES; 566 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 567 | GCC_WARN_UNUSED_FUNCTION = YES; 568 | INFOPLIST_FILE = JLRoutesTests/Info.plist; 569 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 570 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 571 | MTL_ENABLE_DEBUG_INFO = YES; 572 | OTHER_LDFLAGS = ( 573 | "-all_load", 574 | "-ObjC", 575 | ); 576 | PRODUCT_BUNDLE_IDENTIFIER = com.joeldev.JLRoutesTests; 577 | PRODUCT_NAME = "$(TARGET_NAME)"; 578 | }; 579 | name = Debug; 580 | }; 581 | 5D3CB0791C73DA2700870B55 /* Release */ = { 582 | isa = XCBuildConfiguration; 583 | buildSettings = { 584 | CLANG_ANALYZER_NONNULL = YES; 585 | CLANG_ENABLE_MODULES = YES; 586 | CLANG_WARN_BOOL_CONVERSION = YES; 587 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 588 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 589 | CLANG_WARN_UNREACHABLE_CODE = YES; 590 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 591 | COPY_PHASE_STRIP = NO; 592 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 593 | ENABLE_NS_ASSERTIONS = NO; 594 | ENABLE_STRICT_OBJC_MSGSEND = YES; 595 | GCC_NO_COMMON_BLOCKS = YES; 596 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 597 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 598 | GCC_WARN_UNDECLARED_SELECTOR = YES; 599 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 600 | GCC_WARN_UNUSED_FUNCTION = YES; 601 | INFOPLIST_FILE = JLRoutesTests/Info.plist; 602 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 603 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 604 | MTL_ENABLE_DEBUG_INFO = NO; 605 | OTHER_LDFLAGS = ( 606 | "-all_load", 607 | "-ObjC", 608 | ); 609 | PRODUCT_BUNDLE_IDENTIFIER = com.joeldev.JLRoutesTests; 610 | PRODUCT_NAME = "$(TARGET_NAME)"; 611 | }; 612 | name = Release; 613 | }; 614 | /* End XCBuildConfiguration section */ 615 | 616 | /* Begin XCConfigurationList section */ 617 | 5CB4EB601B45BF5B0058E91A /* Build configuration list for PBXNativeTarget "JLRoutes-iOS" */ = { 618 | isa = XCConfigurationList; 619 | buildConfigurations = ( 620 | 5CB4EB5C1B45BF5B0058E91A /* Debug */, 621 | 5CB4EB5D1B45BF5B0058E91A /* Release */, 622 | ); 623 | defaultConfigurationIsVisible = 0; 624 | defaultConfigurationName = Release; 625 | }; 626 | 5D33680D16C6DC9300F983AA /* Build configuration list for PBXProject "JLRoutes" */ = { 627 | isa = XCConfigurationList; 628 | buildConfigurations = ( 629 | 5D33683516C6DC9300F983AA /* Debug */, 630 | 5D33683616C6DC9300F983AA /* Release */, 631 | ); 632 | defaultConfigurationIsVisible = 0; 633 | defaultConfigurationName = Release; 634 | }; 635 | 5D33683716C6DC9300F983AA /* Build configuration list for PBXNativeTarget "JLRoutes" */ = { 636 | isa = XCConfigurationList; 637 | buildConfigurations = ( 638 | 5D33683816C6DC9300F983AA /* Debug */, 639 | 5D33683916C6DC9300F983AA /* Release */, 640 | ); 641 | defaultConfigurationIsVisible = 0; 642 | defaultConfigurationName = Release; 643 | }; 644 | 5D3CB0771C73DA2700870B55 /* Build configuration list for PBXNativeTarget "JLRoutesTests" */ = { 645 | isa = XCConfigurationList; 646 | buildConfigurations = ( 647 | 5D3CB0781C73DA2700870B55 /* Debug */, 648 | 5D3CB0791C73DA2700870B55 /* Release */, 649 | ); 650 | defaultConfigurationIsVisible = 0; 651 | defaultConfigurationName = Release; 652 | }; 653 | /* End XCConfigurationList section */ 654 | }; 655 | rootObject = 5D33680A16C6DC9300F983AA /* Project object */; 656 | } 657 | -------------------------------------------------------------------------------- /JLRoutes.xcodeproj/xcshareddata/xcschemes/JLRoutes-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 80 | 81 | 87 | 88 | 89 | 90 | 91 | 92 | 98 | 99 | 105 | 106 | 107 | 108 | 110 | 111 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /JLRoutes.xcodeproj/xcshareddata/xcschemes/JLRoutes.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 78 | 79 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /JLRoutes/JLRParsingUtilities.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | 18 | @interface JLRParsingUtilities : NSObject 19 | 20 | + (NSString *)variableValueFrom:(NSString *)value decodePlusSymbols:(BOOL)decodePlusSymbols; 21 | 22 | + (NSDictionary *)queryParams:(NSDictionary *)queryParams decodePlusSymbols:(BOOL)decodePlusSymbols; 23 | 24 | + (NSArray *)expandOptionalRoutePatternsForPattern:(NSString *)routePattern; 25 | 26 | @end 27 | 28 | 29 | NS_ASSUME_NONNULL_END 30 | -------------------------------------------------------------------------------- /JLRoutes/JLRParsingUtilities.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import "JLRParsingUtilities.h" 14 | 15 | 16 | @interface NSArray (JLRoutes_Utilities) 17 | 18 | - (NSArray *)JLRoutes_allOrderedCombinations; 19 | - (NSArray *)JLRoutes_filter:(BOOL (^)(id object))filterBlock; 20 | - (NSArray *)JLRoutes_map:(id (^)(id object))mapBlock; 21 | 22 | @end 23 | 24 | 25 | @interface NSString (JLRoutes_Utilities) 26 | 27 | - (NSArray *)JLRoutes_trimmedPathComponents; 28 | 29 | @end 30 | 31 | 32 | #pragma mark - Parsing Utility Methods 33 | 34 | 35 | @interface JLRParsingUtilities_RouteSubpath : NSObject 36 | 37 | @property (nonatomic, strong) NSArray *subpathComponents; 38 | @property (nonatomic, assign) BOOL isOptionalSubpath; 39 | 40 | @end 41 | 42 | 43 | @implementation JLRParsingUtilities_RouteSubpath 44 | 45 | - (NSString *)description 46 | { 47 | NSString *type = self.isOptionalSubpath ? @"OPTIONAL" : @"REQUIRED"; 48 | return [NSString stringWithFormat:@"%@ - %@: %@", [super description], type, [self.subpathComponents componentsJoinedByString:@"/"]]; 49 | } 50 | 51 | - (BOOL)isEqual:(id)object 52 | { 53 | if (![object isKindOfClass:[self class]]) { 54 | return NO; 55 | } 56 | 57 | JLRParsingUtilities_RouteSubpath *otherSubpath = (JLRParsingUtilities_RouteSubpath *)object; 58 | if (![self.subpathComponents isEqual:otherSubpath.subpathComponents]) { 59 | return NO; 60 | } 61 | 62 | if (self.isOptionalSubpath != otherSubpath.isOptionalSubpath) { 63 | return NO; 64 | } 65 | 66 | return YES; 67 | } 68 | 69 | - (NSUInteger)hash 70 | { 71 | return self.subpathComponents.hash ^ self.isOptionalSubpath; 72 | } 73 | 74 | @end 75 | 76 | 77 | @implementation JLRParsingUtilities 78 | 79 | + (NSString *)variableValueFrom:(NSString *)value decodePlusSymbols:(BOOL)decodePlusSymbols 80 | { 81 | if (!decodePlusSymbols) { 82 | return value; 83 | } 84 | return [value stringByReplacingOccurrencesOfString:@"+" withString:@" " options:NSLiteralSearch range:NSMakeRange(0, value.length)]; 85 | } 86 | 87 | + (NSDictionary *)queryParams:(NSDictionary *)queryParams decodePlusSymbols:(BOOL)decodePlusSymbols 88 | { 89 | if (!decodePlusSymbols) { 90 | return queryParams; 91 | } 92 | 93 | NSMutableDictionary *updatedQueryParams = [NSMutableDictionary dictionary]; 94 | 95 | for (NSString *name in queryParams) { 96 | id value = queryParams[name]; 97 | 98 | if ([value isKindOfClass:[NSArray class]]) { 99 | NSMutableArray *variables = [NSMutableArray array]; 100 | for (NSString *arrayValue in (NSArray *)value) { 101 | [variables addObject:[self variableValueFrom:arrayValue decodePlusSymbols:YES]]; 102 | } 103 | updatedQueryParams[name] = [variables copy]; 104 | } else if ([value isKindOfClass:[NSString class]]) { 105 | NSString *variable = [self variableValueFrom:value decodePlusSymbols:YES]; 106 | updatedQueryParams[name] = variable; 107 | } else { 108 | NSAssert(NO, @"Unexpected query parameter type: %@", NSStringFromClass([value class])); 109 | } 110 | } 111 | 112 | return [updatedQueryParams copy]; 113 | } 114 | 115 | + (NSArray *)expandOptionalRoutePatternsForPattern:(NSString *)routePattern 116 | { 117 | /* this method exists to take a route pattern that is known to contain optional params, such as: 118 | 119 | /path/:thing/(/a)(/b)(/c) 120 | 121 | and create the following paths: 122 | 123 | /path/:thing/a/b/c 124 | /path/:thing/a/b 125 | /path/:thing/a/c 126 | /path/:thing/b/c 127 | /path/:thing/a 128 | /path/:thing/b 129 | /path/:thing/c 130 | 131 | */ 132 | 133 | if ([routePattern rangeOfString:@"("].location == NSNotFound) { 134 | return @[]; 135 | } 136 | 137 | // First, parse the route pattern into subpath objects. 138 | NSArray *subpaths = [self _routeSubpathsForPattern:routePattern]; 139 | if (subpaths.count == 0) { 140 | return @[]; 141 | } 142 | 143 | // Next, etract out the required subpaths. 144 | NSSet *requiredSubpaths = [NSSet setWithArray:[subpaths JLRoutes_filter:^BOOL(JLRParsingUtilities_RouteSubpath *subpath) { 145 | return !subpath.isOptionalSubpath; 146 | }]]; 147 | 148 | // Then, expand the subpath permutations into possible route patterns. 149 | NSArray *> *allSubpathCombinations = [subpaths JLRoutes_allOrderedCombinations]; 150 | 151 | // Finally, we need to filter out any possible route patterns that don't actually satisfy the rules of the route. 152 | // What this means in practice is throwing out any that do not contain all required subpaths (since those are explicitly not optional). 153 | NSArray *> *validSubpathCombinations = [allSubpathCombinations JLRoutes_filter:^BOOL(NSArray *possibleRouteSubpaths) { 154 | return [requiredSubpaths isSubsetOfSet:[NSSet setWithArray:possibleRouteSubpaths]]; 155 | }]; 156 | 157 | // Once we have a filtered list of valid subpaths, we just need to convert them back into string routes that can we registered. 158 | NSArray *validSubpathRouteStrings = [validSubpathCombinations JLRoutes_map:^id(NSArray *subpaths) { 159 | NSString *routePattern = @"/"; 160 | for (JLRParsingUtilities_RouteSubpath *subpath in subpaths) { 161 | NSString *subpathString = [subpath.subpathComponents componentsJoinedByString:@"/"]; 162 | routePattern = [routePattern stringByAppendingPathComponent:subpathString]; 163 | } 164 | return routePattern; 165 | }]; 166 | 167 | // Before returning, sort them by length so that the longest and most specific routes are registered first before the less specific shorter ones. 168 | validSubpathRouteStrings = [validSubpathRouteStrings sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"length" ascending:NO selector:@selector(compare:)]]]; 169 | 170 | return validSubpathRouteStrings; 171 | } 172 | 173 | + (NSArray *)_routeSubpathsForPattern:(NSString *)routePattern 174 | { 175 | NSMutableArray *subpaths = [NSMutableArray array]; 176 | 177 | NSScanner *scanner = [NSScanner scannerWithString:routePattern]; 178 | while (![scanner isAtEnd]) { 179 | NSString *preOptionalSubpath = nil; 180 | BOOL didScan = [scanner scanUpToString:@"(" intoString:&preOptionalSubpath]; 181 | if (!didScan) { 182 | NSAssert([routePattern characterAtIndex:scanner.scanLocation] == '(', @"Unexpected character: %c", [routePattern characterAtIndex:scanner.scanLocation]); 183 | } 184 | 185 | if (!scanner.isAtEnd) { 186 | // otherwise, advance past the ( character 187 | scanner.scanLocation = scanner.scanLocation + 1; 188 | } 189 | 190 | if (preOptionalSubpath.length > 0 && ![preOptionalSubpath isEqualToString:@")"] && ![preOptionalSubpath isEqualToString:@"/"]) { 191 | // content before the start of an optional subpath 192 | JLRParsingUtilities_RouteSubpath *subpath = [[JLRParsingUtilities_RouteSubpath alloc] init]; 193 | subpath.subpathComponents = [preOptionalSubpath JLRoutes_trimmedPathComponents]; 194 | [subpaths addObject:subpath]; 195 | } 196 | 197 | if (scanner.isAtEnd) { 198 | break; 199 | } 200 | 201 | NSString *optionalSubpath = nil; 202 | didScan = [scanner scanUpToString:@")" intoString:&optionalSubpath]; 203 | NSAssert(didScan, @"Could not find closing parenthesis"); 204 | 205 | scanner.scanLocation = scanner.scanLocation + 1; 206 | 207 | if (optionalSubpath.length > 0) { 208 | JLRParsingUtilities_RouteSubpath *subpath = [[JLRParsingUtilities_RouteSubpath alloc] init]; 209 | subpath.isOptionalSubpath = YES; 210 | subpath.subpathComponents = [optionalSubpath JLRoutes_trimmedPathComponents]; 211 | [subpaths addObject:subpath]; 212 | } 213 | } 214 | 215 | return [subpaths copy]; 216 | } 217 | 218 | @end 219 | 220 | 221 | #pragma mark - Categories 222 | 223 | 224 | @implementation NSArray (JLRoutes_Utilities) 225 | 226 | - (NSArray *)JLRoutes_allOrderedCombinations 227 | { 228 | NSInteger length = self.count; 229 | if (length == 0) { 230 | return [NSArray arrayWithObject:[NSArray array]]; 231 | } 232 | 233 | id lastObject = [self lastObject]; 234 | NSArray *subarray = [self subarrayWithRange:NSMakeRange(0, length - 1)]; 235 | NSArray *subarrayCombinations = [subarray JLRoutes_allOrderedCombinations]; 236 | NSMutableArray *combinations = [NSMutableArray arrayWithArray:subarrayCombinations]; 237 | 238 | for (NSArray *subarrayCombos in subarrayCombinations) { 239 | [combinations addObject:[subarrayCombos arrayByAddingObject:lastObject]]; 240 | } 241 | 242 | return [NSArray arrayWithArray:combinations]; 243 | } 244 | 245 | - (NSArray *)JLRoutes_filter:(BOOL (^)(id object))filterBlock 246 | { 247 | NSParameterAssert(filterBlock != nil); 248 | NSMutableArray *filteredArray = [NSMutableArray array]; 249 | 250 | for (id object in self) { 251 | if (filterBlock(object)) { 252 | [filteredArray addObject:object]; 253 | } 254 | } 255 | 256 | return [filteredArray copy]; 257 | } 258 | 259 | - (NSArray *)JLRoutes_map:(id (^)(id object))mapBlock 260 | { 261 | NSParameterAssert(mapBlock != nil); 262 | NSMutableArray *mappedArray = [NSMutableArray array]; 263 | 264 | for (id object in self) { 265 | id mappedObject = mapBlock(object); 266 | [mappedArray addObject:mappedObject]; 267 | } 268 | 269 | return [mappedArray copy]; 270 | } 271 | 272 | @end 273 | 274 | 275 | @implementation NSString (JLRoutes_Utilities) 276 | 277 | - (NSArray *)JLRoutes_trimmedPathComponents 278 | { 279 | // Trims leading and trailing slashes and then separates by slash 280 | return [[self stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/"]] componentsSeparatedByString:@"/"]; 281 | } 282 | 283 | @end 284 | 285 | -------------------------------------------------------------------------------- /JLRoutes/JLRRouteDefinition.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import 14 | #import "JLRRouteRequest.h" 15 | #import "JLRRouteResponse.h" 16 | 17 | NS_ASSUME_NONNULL_BEGIN 18 | 19 | 20 | /** 21 | JLRRouteDefinition is a model object representing a registered route, including the URL scheme, route pattern, and priority. 22 | 23 | This class can be subclassed to customize route parsing behavior by overriding -routeResponseForRequest:. 24 | -callHandlerBlockWithParameters can also be overriden to customize the parameters passed to the handlerBlock. 25 | */ 26 | 27 | @interface JLRRouteDefinition : NSObject 28 | 29 | /// The URL scheme for which this route applies, or JLRoutesGlobalRoutesScheme if global. 30 | @property (nonatomic, copy, readonly) NSString *scheme; 31 | 32 | /// The route pattern. 33 | @property (nonatomic, copy, readonly) NSString *pattern; 34 | 35 | /// The priority of this route pattern. 36 | @property (nonatomic, assign, readonly) NSUInteger priority; 37 | 38 | /// The route pattern path components. 39 | @property (nonatomic, copy, readonly) NSArray *patternPathComponents; 40 | 41 | /// The handler block to invoke when a match is found. 42 | @property (nonatomic, copy, readonly) BOOL (^handlerBlock)(NSDictionary *parameters); 43 | 44 | /// Check for route definition equality. 45 | - (BOOL)isEqualToRouteDefinition:(JLRRouteDefinition *)routeDefinition; 46 | 47 | 48 | ///---------------------------------- 49 | /// @name Creating Route Definitions 50 | ///---------------------------------- 51 | 52 | 53 | /** 54 | Creates a new route definition. The created definition can be directly added to an instance of JLRoutes. 55 | 56 | This is the designated initializer. 57 | 58 | @param pattern The full route pattern ('/foo/:bar') 59 | @param priority The route priority, or 0 if default. 60 | @param handlerBlock The handler block to call when a successful match is found. 61 | 62 | @returns The newly initialized route definition. 63 | */ 64 | - (instancetype)initWithPattern:(NSString *)pattern priority:(NSUInteger)priority handlerBlock:(BOOL (^)(NSDictionary *parameters))handlerBlock NS_DESIGNATED_INITIALIZER; 65 | 66 | /// Unavailable, use initWithScheme:pattern:priority:handlerBlock: instead. 67 | - (instancetype)init NS_UNAVAILABLE; 68 | 69 | /// Unavailable, use initWithScheme:pattern:priority:handlerBlock: instead. 70 | + (instancetype)new NS_UNAVAILABLE; 71 | 72 | 73 | ///---------------------------------- 74 | /// @name Responding To Registration 75 | ///---------------------------------- 76 | 77 | 78 | /** 79 | Called when the route has been registered for the given scheme. 80 | 81 | @param scheme The scheme this route has become active for. 82 | */ 83 | - (void)didBecomeRegisteredForScheme:(NSString *)scheme; 84 | 85 | 86 | ///------------------------------- 87 | /// @name Matching Route Requests 88 | ///------------------------------- 89 | 90 | 91 | /** 92 | Creates and returns a JLRRouteResponse for the provided JLRRouteRequest. The response specifies if there was a match or not. 93 | 94 | @param request The JLRRouteRequest to create a response for. 95 | 96 | @returns An JLRRouteResponse instance representing the result of attempting to match request to thie route definition. 97 | */ 98 | - (JLRRouteResponse *)routeResponseForRequest:(JLRRouteRequest *)request; 99 | 100 | 101 | /** 102 | Invoke handlerBlock with the given parameters. This may be overriden by subclasses. 103 | 104 | @param parameters The parameters to pass to handlerBlock. 105 | 106 | @returns The value returned by calling handlerBlock (YES if it is considered handled and NO if not). 107 | */ 108 | - (BOOL)callHandlerBlockWithParameters:(NSDictionary *)parameters; 109 | 110 | 111 | ///--------------------------------- 112 | /// @name Creating Match Parameters 113 | ///--------------------------------- 114 | 115 | 116 | /** 117 | Creates and returns the full set of match parameters to be passed as part of a valid match. 118 | Subclasses can override this method to mutate the match parameters, or simply call it to generate the expected value. 119 | 120 | @param request The request being routed. 121 | @param routeVariables The parsed route variables (aka a route of '/route/:param' being routed with '/foo/bar' would create [ 'param' : 'bar' ]) 122 | 123 | @returns The full set of match parameters to be passed as part of a valid match. 124 | @see defaultMatchParametersForRequest: 125 | @see routeVariablesForRequest: 126 | */ 127 | - (NSDictionary *)matchParametersForRequest:(JLRRouteRequest *)request routeVariables:(NSDictionary *)routeVariables; 128 | 129 | 130 | /** 131 | Creates and returns the default base match parameters for a given request. Does not include any parsed fields. 132 | 133 | @param request The request being routed. 134 | 135 | @returns The default match parameters for a given request. Only includes key/value pairs for JLRoutePatternKey, JLRouteURLKey, and JLRouteSchemeKey. 136 | */ 137 | - (NSDictionary *)defaultMatchParametersForRequest:(JLRRouteRequest *)request; 138 | 139 | 140 | ///------------------------------- 141 | /// @name Parsing Route Variables 142 | ///------------------------------- 143 | 144 | 145 | /** 146 | Parses and returns route variables for the given request. 147 | 148 | @param request The request to parse variable values from. 149 | 150 | @returns The parsed route variables if there was a match, or nil if it was not a match. 151 | */ 152 | - (nullable NSDictionary *)routeVariablesForRequest:(JLRRouteRequest *)request; 153 | 154 | 155 | /** 156 | Parses value into a variable name, including stripping out any extra characters if needed. 157 | 158 | @param value The raw string value that should be parsed into a variable name. 159 | 160 | @returns The variable name to use as the key of a key/value pair in the parsed route variables. 161 | */ 162 | - (NSString *)routeVariableNameForValue:(NSString *)value; 163 | 164 | 165 | /** 166 | Parses value into a variable value, including stripping out any extra characters if needed. 167 | 168 | @param value The raw string value that should be parsed into a variable value. 169 | 170 | @returns The variable value to use as the value of a key/value pair in the parsed route variables. 171 | */ 172 | - (NSString *)routeVariableValueForValue:(NSString *)value; 173 | 174 | 175 | @end 176 | 177 | 178 | NS_ASSUME_NONNULL_END 179 | -------------------------------------------------------------------------------- /JLRoutes/JLRRouteDefinition.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import "JLRRouteDefinition.h" 14 | #import "JLRoutes.h" 15 | #import "JLRParsingUtilities.h" 16 | 17 | 18 | @interface JLRRouteDefinition () 19 | 20 | @property (nonatomic, copy) NSString *pattern; 21 | @property (nonatomic, copy) NSString *scheme; 22 | @property (nonatomic, assign) NSUInteger priority; 23 | @property (nonatomic, copy) NSArray *patternPathComponents; 24 | @property (nonatomic, copy) BOOL (^handlerBlock)(NSDictionary *parameters); 25 | 26 | @end 27 | 28 | 29 | @implementation JLRRouteDefinition 30 | 31 | - (instancetype)initWithPattern:(NSString *)pattern priority:(NSUInteger)priority handlerBlock:(BOOL (^)(NSDictionary *parameters))handlerBlock 32 | { 33 | NSParameterAssert(pattern != nil); 34 | 35 | if ((self = [super init])) { 36 | self.pattern = pattern; 37 | self.priority = priority; 38 | self.handlerBlock = handlerBlock; 39 | 40 | if ([pattern length] > 0 && [pattern characterAtIndex:0] == '/') { 41 | pattern = [pattern substringFromIndex:1]; 42 | } 43 | 44 | self.patternPathComponents = [pattern componentsSeparatedByString:@"/"]; 45 | } 46 | return self; 47 | } 48 | 49 | - (NSString *)description 50 | { 51 | return [NSString stringWithFormat:@"<%@ %p> - %@ (priority: %@)", NSStringFromClass([self class]), self, self.pattern, @(self.priority)]; 52 | } 53 | 54 | - (BOOL)isEqual:(id)object 55 | { 56 | if (object == self) { 57 | return YES; 58 | } 59 | 60 | if ([object isKindOfClass:[JLRRouteDefinition class]]) { 61 | return [self isEqualToRouteDefinition:(JLRRouteDefinition *)object]; 62 | } else { 63 | return [super isEqual:object]; 64 | } 65 | } 66 | 67 | - (BOOL)isEqualToRouteDefinition:(JLRRouteDefinition *)routeDefinition 68 | { 69 | if (!((self.pattern == nil && routeDefinition.pattern == nil) || [self.pattern isEqualToString:routeDefinition.pattern])) { 70 | return NO; 71 | } 72 | 73 | if (!((self.scheme == nil && routeDefinition.scheme == nil) || [self.scheme isEqualToString:routeDefinition.scheme])) { 74 | return NO; 75 | } 76 | 77 | if (!((self.patternPathComponents == nil && routeDefinition.patternPathComponents == nil) || [self.patternPathComponents isEqualToArray:routeDefinition.patternPathComponents])) { 78 | return NO; 79 | } 80 | 81 | if (self.priority != routeDefinition.priority) { 82 | return NO; 83 | } 84 | 85 | return YES; 86 | } 87 | 88 | - (NSUInteger)hash 89 | { 90 | return self.pattern.hash ^ @(self.priority).hash ^ self.scheme.hash ^ self.patternPathComponents.hash; 91 | } 92 | 93 | #pragma mark - Main API 94 | 95 | - (JLRRouteResponse *)routeResponseForRequest:(JLRRouteRequest *)request 96 | { 97 | BOOL patternContainsWildcard = [self.patternPathComponents containsObject:@"*"]; 98 | 99 | if (request.pathComponents.count != self.patternPathComponents.count && !patternContainsWildcard) { 100 | // definitely not a match, nothing left to do 101 | return [JLRRouteResponse invalidMatchResponse]; 102 | } 103 | 104 | NSDictionary *routeVariables = [self routeVariablesForRequest:request]; 105 | 106 | if (routeVariables != nil) { 107 | // It's a match, set up the param dictionary and create a valid match response 108 | NSDictionary *matchParams = [self matchParametersForRequest:request routeVariables:routeVariables]; 109 | return [JLRRouteResponse validMatchResponseWithParameters:matchParams]; 110 | } else { 111 | // nil variables indicates no match, so return an invalid match response 112 | return [JLRRouteResponse invalidMatchResponse]; 113 | } 114 | } 115 | 116 | - (BOOL)callHandlerBlockWithParameters:(NSDictionary *)parameters 117 | { 118 | if (self.handlerBlock == nil) { 119 | return YES; 120 | } 121 | 122 | return self.handlerBlock(parameters); 123 | } 124 | 125 | - (void)didBecomeRegisteredForScheme:(NSString *)scheme 126 | { 127 | NSAssert(self.scheme == nil, @"Route definitions should not be added to multiple schemes."); 128 | self.scheme = scheme; 129 | } 130 | 131 | #pragma mark - Parsing Route Variables 132 | 133 | - (NSDictionary *)routeVariablesForRequest:(JLRRouteRequest *)request 134 | { 135 | NSMutableDictionary *routeVariables = [NSMutableDictionary dictionary]; 136 | 137 | BOOL isMatch = YES; 138 | NSUInteger index = 0; 139 | 140 | for (NSString *patternComponent in self.patternPathComponents) { 141 | NSString *URLComponent = nil; 142 | BOOL isPatternComponentWildcard = [patternComponent isEqualToString:@"*"]; 143 | 144 | if (index < [request.pathComponents count]) { 145 | URLComponent = request.pathComponents[index]; 146 | } else if (!isPatternComponentWildcard) { 147 | // URLComponent is not a wildcard and index is >= request.pathComponents.count, so bail 148 | isMatch = NO; 149 | break; 150 | } 151 | 152 | if ([patternComponent hasPrefix:@":"]) { 153 | // this is a variable, set it in the params 154 | NSAssert(URLComponent != nil, @"URLComponent cannot be nil"); 155 | NSString *variableName = [self routeVariableNameForValue:patternComponent]; 156 | NSString *variableValue = [self routeVariableValueForValue:URLComponent]; 157 | 158 | // Consult the parsing utilities as well to do any other standard variable transformations 159 | BOOL decodePlusSymbols = ((request.options & JLRRouteRequestOptionDecodePlusSymbols) == JLRRouteRequestOptionDecodePlusSymbols); 160 | variableValue = [JLRParsingUtilities variableValueFrom:variableValue decodePlusSymbols:decodePlusSymbols]; 161 | 162 | routeVariables[variableName] = variableValue; 163 | } else if (isPatternComponentWildcard) { 164 | // match wildcards 165 | NSUInteger minRequiredParams = index; 166 | if (request.pathComponents.count >= minRequiredParams) { 167 | // match: /a/b/c/* has to be matched by at least /a/b/c 168 | routeVariables[JLRouteWildcardComponentsKey] = [request.pathComponents subarrayWithRange:NSMakeRange(index, request.pathComponents.count - index)]; 169 | isMatch = YES; 170 | } else { 171 | // not a match: /a/b/c/* cannot be matched by URL /a/b/ 172 | isMatch = NO; 173 | } 174 | break; 175 | } else if (![patternComponent isEqualToString:URLComponent]) { 176 | // break if this is a static component and it isn't a match 177 | isMatch = NO; 178 | break; 179 | } 180 | index++; 181 | } 182 | 183 | if (!isMatch) { 184 | // Return nil to indicate that there was not a match 185 | routeVariables = nil; 186 | } 187 | 188 | return [routeVariables copy]; 189 | } 190 | 191 | - (NSString *)routeVariableNameForValue:(NSString *)value 192 | { 193 | NSString *name = value; 194 | 195 | if (name.length > 1 && [name characterAtIndex:0] == ':') { 196 | // Strip off the ':' in front of param names 197 | name = [name substringFromIndex:1]; 198 | } 199 | 200 | if (name.length > 1 && [name characterAtIndex:name.length - 1] == '#') { 201 | // Strip of trailing fragment 202 | name = [name substringToIndex:name.length - 1]; 203 | } 204 | 205 | return name; 206 | } 207 | 208 | - (NSString *)routeVariableValueForValue:(NSString *)value 209 | { 210 | // Remove percent encoding 211 | NSString *var = [value stringByRemovingPercentEncoding]; 212 | 213 | if (var.length > 1 && [var characterAtIndex:var.length - 1] == '#') { 214 | // Strip of trailing fragment 215 | var = [var substringToIndex:var.length - 1]; 216 | } 217 | 218 | return var; 219 | } 220 | 221 | #pragma mark - Creating Match Parameters 222 | 223 | - (NSDictionary *)matchParametersForRequest:(JLRRouteRequest *)request routeVariables:(NSDictionary *)routeVariables 224 | { 225 | NSMutableDictionary *matchParams = [NSMutableDictionary dictionary]; 226 | 227 | // Add the parsed query parameters ('?a=b&c=d'). Also includes fragment. 228 | BOOL decodePlusSymbols = ((request.options & JLRRouteRequestOptionDecodePlusSymbols) == JLRRouteRequestOptionDecodePlusSymbols); 229 | [matchParams addEntriesFromDictionary:[JLRParsingUtilities queryParams:request.queryParams decodePlusSymbols:decodePlusSymbols]]; 230 | 231 | // Add the actual parsed route variables (the items in the route prefixed with ':'). 232 | [matchParams addEntriesFromDictionary:routeVariables]; 233 | 234 | // Add the additional parameters, if any were specified in the request. 235 | if (request.additionalParameters != nil) { 236 | [matchParams addEntriesFromDictionary:request.additionalParameters]; 237 | } 238 | 239 | // Finally, add the base parameters. This is done last so that these cannot be overriden by using the same key in your route or query. 240 | [matchParams addEntriesFromDictionary:[self defaultMatchParametersForRequest:request]]; 241 | 242 | return [matchParams copy]; 243 | } 244 | 245 | - (NSDictionary *)defaultMatchParametersForRequest:(JLRRouteRequest *)request 246 | { 247 | return @{JLRoutePatternKey: self.pattern ?: [NSNull null], JLRouteURLKey: request.URL ?: [NSNull null], JLRouteSchemeKey: self.scheme ?: [NSNull null]}; 248 | } 249 | 250 | #pragma mark - NSCopying 251 | 252 | - (id)copyWithZone:(NSZone *)zone 253 | { 254 | JLRRouteDefinition *copy = [[[self class] alloc] initWithPattern:self.pattern priority:self.priority handlerBlock:self.handlerBlock]; 255 | copy.scheme = self.scheme; 256 | return copy; 257 | } 258 | 259 | @end 260 | -------------------------------------------------------------------------------- /JLRoutes/JLRRouteHandler.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | 18 | @protocol JLRRouteHandlerTarget; 19 | 20 | 21 | /** 22 | JLRRouteHandler is a helper class for creating handler blocks intended to be passed to an addRoute: call. 23 | 24 | This is specifically useful for cases in which you want a separate object or class to be the handler 25 | for a deeplink route. An example might be a view controller class that you want to instantiate and present 26 | in response to a deeplink route. 27 | */ 28 | 29 | @interface JLRRouteHandler : NSObject 30 | 31 | /// Unavailable. 32 | - (instancetype)init NS_UNAVAILABLE; 33 | 34 | /// Unavailable. 35 | + (instancetype)new NS_UNAVAILABLE; 36 | 37 | 38 | /** 39 | Creates and returns a block that calls handleRouteWithParameters: on a weak target objet. 40 | 41 | The block returned from this method should be passed as the handler block of an addRoute: call. 42 | 43 | @param weakTarget The target object that should handle a matched route. 44 | 45 | @returns A new handler block for the provided weakTarget. 46 | 47 | @discussion There is no change of ownership of the target object, only a weak pointer (hence 'weakTarget') is captured in the block. 48 | If the object is deallocated, the handler will no longer be called (but the route will remain registered unless explicitly removed). 49 | */ 50 | 51 | + (BOOL (^__nonnull)(NSDictionary *parameters))handlerBlockForWeakTarget:(__weak id )weakTarget; 52 | 53 | 54 | /** 55 | Creates and returns a block that creates a new instance of targetClass (which must conform to JLRRouteHandlerTarget), and then calls 56 | handleRouteWithParameters: on it. The created object is then passed as the parameter to the completion block. 57 | 58 | The block returned from this method should be passed as the handler block of an addRoute: call. 59 | 60 | @param targetClass The target class to create for handling the route request. Must conform to JLRRouteHandlerTarget. 61 | @param completionHandler The completion block to call after creating the new targetClass instance. 62 | 63 | @returns A new handler block for creating instances of targetClass. 64 | 65 | @discussion JLRoutes does not retain or own the created object. It's expected that the created object that is passed through the completion handler 66 | will be used and owned by the calling application. 67 | */ 68 | 69 | + (BOOL (^__nonnull)(NSDictionary *parameters))handlerBlockForTargetClass:(Class)targetClass completion:(BOOL (^)(id createdObject))completionHandler; 70 | 71 | @end 72 | 73 | 74 | /** 75 | Classes conforming to the JLRRouteHandlerTarget protocol can be used as a route handler target. 76 | */ 77 | 78 | @protocol JLRRouteHandlerTarget 79 | 80 | @optional 81 | 82 | /** 83 | Initialize an instance of the conforming class by passing matched route parameters from a JLRoutes route. 84 | 85 | @param parameters The match parameters passed to use when initializing the object. These are passed from a JLRoutes handler block. 86 | 87 | @returns An initialized instance of the conforming class. 88 | */ 89 | 90 | - (instancetype)initWithRouteParameters:(NSDictionary *)parameters; 91 | 92 | 93 | /** 94 | Called for a successful route match. 95 | 96 | @param parameters The match parameters passed to the handler block. 97 | 98 | @returns YES if the route was handled, NO if matching a different route should be attempted. 99 | */ 100 | 101 | - (BOOL)handleRouteWithParameters:(NSDictionary *)parameters; 102 | 103 | @end 104 | 105 | 106 | NS_ASSUME_NONNULL_END 107 | -------------------------------------------------------------------------------- /JLRoutes/JLRRouteHandler.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import "JLRRouteHandler.h" 14 | 15 | 16 | @implementation JLRRouteHandler 17 | 18 | + (BOOL (^)(NSDictionary *parameters))handlerBlockForWeakTarget:(__weak id )weakTarget 19 | { 20 | NSParameterAssert([weakTarget respondsToSelector:@selector(handleRouteWithParameters:)]); 21 | 22 | return ^BOOL(NSDictionary *parameters) { 23 | return [weakTarget handleRouteWithParameters:parameters]; 24 | }; 25 | } 26 | 27 | + (BOOL (^)(NSDictionary *parameters))handlerBlockForTargetClass:(Class)targetClass completion:(BOOL (^)(id createdObject))completionHandler 28 | { 29 | NSParameterAssert([targetClass conformsToProtocol:@protocol(JLRRouteHandlerTarget)]); 30 | NSParameterAssert([targetClass instancesRespondToSelector:@selector(initWithRouteParameters:)]); 31 | NSParameterAssert(completionHandler != nil); // we want to force external ownership of the newly created object by handing it back. 32 | 33 | return ^BOOL(NSDictionary *parameters) { 34 | id createdObject = [[targetClass alloc] initWithRouteParameters:parameters]; 35 | return completionHandler(createdObject); 36 | }; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /JLRoutes/JLRRouteRequest.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | 18 | /// Options bitmask generated from JLRoutes global options methods. 19 | typedef NS_OPTIONS(NSUInteger, JLRRouteRequestOptions) { 20 | /// No options specified. 21 | JLRRouteRequestOptionsNone = 0, 22 | 23 | /// If present, decoding plus symbols is enabled. 24 | JLRRouteRequestOptionDecodePlusSymbols = 1 << 0, 25 | 26 | /// If present, treating URL hosts as path components is enabled. 27 | JLRRouteRequestOptionTreatHostAsPathComponent = 1 << 1 28 | }; 29 | 30 | 31 | /** 32 | JLRRouteRequest is a model representing a request to route a URL. 33 | It gets parsed into path components and query parameters, which are then used by JLRRouteDefinition to attempt a match. 34 | */ 35 | 36 | @interface JLRRouteRequest : NSObject 37 | 38 | /// The URL being routed. 39 | @property (nonatomic, copy, readonly) NSURL *URL; 40 | 41 | /// The URL's path components. 42 | @property (nonatomic, strong, readonly) NSArray *pathComponents; 43 | 44 | /// The URL's query parameters. 45 | @property (nonatomic, strong, readonly) NSDictionary *queryParams; 46 | 47 | /// Route request options, generally configured from the framework global options. 48 | @property (nonatomic, assign, readonly) JLRRouteRequestOptions options; 49 | 50 | /// Additional parameters to pass through as part of the match parameters dictionary. 51 | @property (nonatomic, copy, nullable, readonly) NSDictionary *additionalParameters; 52 | 53 | 54 | ///------------------------------- 55 | /// @name Creating Route Requests 56 | ///------------------------------- 57 | 58 | 59 | /** 60 | Creates a new route request. 61 | 62 | @param URL The URL to route. 63 | @param options Options bitmask specifying parsing behavior. 64 | @param additionalParameters Additional parameters to include in any match dictionary created against this request. 65 | 66 | @returns The newly initialized route request. 67 | */ 68 | - (instancetype)initWithURL:(NSURL *)URL options:(JLRRouteRequestOptions)options additionalParameters:(nullable NSDictionary *)additionalParameters NS_DESIGNATED_INITIALIZER; 69 | 70 | /// Unavailable, use initWithURL:options:additionalParameters: instead. 71 | - (instancetype)init NS_UNAVAILABLE; 72 | 73 | /// Unavailable, use initWithURL:options:additionalParameters: instead. 74 | + (instancetype)new NS_UNAVAILABLE; 75 | 76 | @end 77 | 78 | 79 | NS_ASSUME_NONNULL_END 80 | -------------------------------------------------------------------------------- /JLRoutes/JLRRouteRequest.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import "JLRRouteRequest.h" 14 | 15 | 16 | @interface JLRRouteRequest () 17 | 18 | @property (nonatomic, copy) NSURL *URL; 19 | @property (nonatomic, strong) NSArray *pathComponents; 20 | @property (nonatomic, strong) NSDictionary *queryParams; 21 | @property (nonatomic, assign) JLRRouteRequestOptions options; 22 | @property (nonatomic, copy) NSDictionary *additionalParameters; 23 | 24 | @end 25 | 26 | 27 | @implementation JLRRouteRequest 28 | 29 | - (instancetype)initWithURL:(NSURL *)URL options:(JLRRouteRequestOptions)options additionalParameters:(nullable NSDictionary *)additionalParameters 30 | { 31 | if ((self = [super init])) { 32 | self.URL = URL; 33 | self.options = options; 34 | self.additionalParameters = additionalParameters; 35 | 36 | BOOL treatsHostAsPathComponent = ((options & JLRRouteRequestOptionTreatHostAsPathComponent) == JLRRouteRequestOptionTreatHostAsPathComponent); 37 | 38 | NSURLComponents *components = [NSURLComponents componentsWithString:[self.URL absoluteString]]; 39 | 40 | if (components.host.length > 0 && (treatsHostAsPathComponent || (![components.host isEqualToString:@"localhost"] && [components.host rangeOfString:@"."].location == NSNotFound))) { 41 | // convert the host to "/" so that the host is considered a path component 42 | NSString *host = [components.percentEncodedHost copy]; 43 | components.host = @"/"; 44 | components.percentEncodedPath = [host stringByAppendingPathComponent:(components.percentEncodedPath ?: @"")]; 45 | } 46 | 47 | NSString *path = [components percentEncodedPath]; 48 | 49 | // handle fragment if needed 50 | if (components.fragment != nil) { 51 | BOOL fragmentContainsQueryParams = NO; 52 | NSURLComponents *fragmentComponents = [NSURLComponents componentsWithString:components.percentEncodedFragment]; 53 | 54 | if (fragmentComponents.query == nil && fragmentComponents.path != nil) { 55 | fragmentComponents.query = fragmentComponents.path; 56 | } 57 | 58 | if (fragmentComponents.queryItems.count > 0) { 59 | // determine if this fragment is only valid query params and nothing else 60 | fragmentContainsQueryParams = fragmentComponents.queryItems.firstObject.value.length > 0; 61 | } 62 | 63 | if (fragmentContainsQueryParams) { 64 | // include fragment query params in with the standard set 65 | components.queryItems = [(components.queryItems ?: @[]) arrayByAddingObjectsFromArray:fragmentComponents.queryItems]; 66 | } 67 | 68 | if (fragmentComponents.path != nil && (!fragmentContainsQueryParams || ![fragmentComponents.path isEqualToString:fragmentComponents.query])) { 69 | // handle fragment by include fragment path as part of the main path 70 | path = [path stringByAppendingString:[NSString stringWithFormat:@"#%@", fragmentComponents.percentEncodedPath]]; 71 | } 72 | } 73 | 74 | // strip off leading slash so that we don't have an empty first path component 75 | if (path.length > 0 && [path characterAtIndex:0] == '/') { 76 | path = [path substringFromIndex:1]; 77 | } 78 | 79 | // strip off trailing slash for the same reason 80 | if (path.length > 0 && [path characterAtIndex:path.length - 1] == '/') { 81 | path = [path substringToIndex:path.length - 1]; 82 | } 83 | 84 | // split apart into path components 85 | self.pathComponents = [path componentsSeparatedByString:@"/"]; 86 | 87 | // convert query items into a dictionary 88 | NSArray *queryItems = [components queryItems] ?: @[]; 89 | NSMutableDictionary *queryParams = [NSMutableDictionary dictionary]; 90 | for (NSURLQueryItem *item in queryItems) { 91 | if (item.value == nil) { 92 | continue; 93 | } 94 | 95 | if (queryParams[item.name] == nil) { 96 | // first time seeing a param with this name, set it 97 | queryParams[item.name] = item.value; 98 | } else if ([queryParams[item.name] isKindOfClass:[NSArray class]]) { 99 | // already an array of these items, append it 100 | NSArray *values = (NSArray *)(queryParams[item.name]); 101 | queryParams[item.name] = [values arrayByAddingObject:item.value]; 102 | } else { 103 | // existing non-array value for this key, create an array 104 | id existingValue = queryParams[item.name]; 105 | queryParams[item.name] = @[existingValue, item.value]; 106 | } 107 | } 108 | 109 | self.queryParams = [queryParams copy]; 110 | } 111 | return self; 112 | } 113 | 114 | - (NSString *)description 115 | { 116 | return [NSString stringWithFormat:@"<%@ %p> - URL: %@", NSStringFromClass([self class]), self, [self.URL absoluteString]]; 117 | } 118 | 119 | @end 120 | -------------------------------------------------------------------------------- /JLRoutes/JLRRouteResponse.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import 14 | 15 | NS_ASSUME_NONNULL_BEGIN 16 | 17 | 18 | /** 19 | JLRRouteResponse is the response from attempting to route a JLRRouteRequest. 20 | */ 21 | 22 | @interface JLRRouteResponse : NSObject 23 | 24 | /// Indicates if the response is a match or not. 25 | @property (nonatomic, assign, readonly, getter=isMatch) BOOL match; 26 | 27 | /// The match parameters (or nil for an invalid response). 28 | @property (nonatomic, copy, readonly, nullable) NSDictionary *parameters; 29 | 30 | /// Check for route response equality 31 | - (BOOL)isEqualToRouteResponse:(JLRRouteResponse *)response; 32 | 33 | 34 | ///------------------------------- 35 | /// @name Creating Responses 36 | ///------------------------------- 37 | 38 | 39 | /// Creates an invalid match response. 40 | + (instancetype)invalidMatchResponse; 41 | 42 | /// Creates a valid match response with the given parameters. 43 | + (instancetype)validMatchResponseWithParameters:(NSDictionary *)parameters; 44 | 45 | /// Unavailable, please use +invalidMatchResponse or +validMatchResponseWithParameters: instead. 46 | - (instancetype)init NS_UNAVAILABLE; 47 | 48 | /// Unavailable, please use +invalidMatchResponse or +validMatchResponseWithParameters: instead. 49 | + (instancetype)new NS_UNAVAILABLE; 50 | 51 | @end 52 | 53 | 54 | NS_ASSUME_NONNULL_END 55 | -------------------------------------------------------------------------------- /JLRoutes/JLRRouteResponse.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import "JLRRouteResponse.h" 14 | 15 | 16 | @interface JLRRouteResponse () 17 | 18 | @property (nonatomic, assign, getter=isMatch) BOOL match; 19 | @property (nonatomic, copy) NSDictionary *parameters; 20 | 21 | @end 22 | 23 | 24 | @implementation JLRRouteResponse 25 | 26 | + (instancetype)invalidMatchResponse 27 | { 28 | JLRRouteResponse *response = [[[self class] alloc] init]; 29 | response.match = NO; 30 | return response; 31 | } 32 | 33 | + (instancetype)validMatchResponseWithParameters:(NSDictionary *)parameters 34 | { 35 | JLRRouteResponse *response = [[[self class] alloc] init]; 36 | response.match = YES; 37 | response.parameters = parameters; 38 | return response; 39 | } 40 | 41 | - (NSString *)description 42 | { 43 | return [NSString stringWithFormat:@"<%@ %p> - match: %@, params: %@", NSStringFromClass([self class]), self, (self.match ? @"YES" : @"NO"), self.parameters]; 44 | } 45 | 46 | - (BOOL)isEqual:(id)object 47 | { 48 | if (object == self) { 49 | return YES; 50 | } 51 | 52 | if ([object isKindOfClass:[self class]]) { 53 | return [self isEqualToRouteResponse:(JLRRouteResponse *)object]; 54 | } else { 55 | return [super isEqual:object]; 56 | } 57 | } 58 | 59 | - (BOOL)isEqualToRouteResponse:(JLRRouteResponse *)response 60 | { 61 | if (self.isMatch != response.isMatch) { 62 | return NO; 63 | } 64 | 65 | if (!((self.parameters == nil && response.parameters == nil) || [self.parameters isEqualToDictionary:response.parameters])) { 66 | return NO; 67 | } 68 | 69 | return YES; 70 | } 71 | 72 | - (NSUInteger)hash 73 | { 74 | return @(self.match).hash ^ self.parameters.hash; 75 | } 76 | 77 | - (id)copyWithZone:(NSZone *)zone 78 | { 79 | JLRRouteResponse *copy = [[[self class] alloc] init]; 80 | copy.match = self.isMatch; 81 | copy.parameters = self.parameters; 82 | return copy; 83 | } 84 | 85 | @end 86 | -------------------------------------------------------------------------------- /JLRoutes/JLRoutes.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import 14 | 15 | #import "JLRRouteDefinition.h" 16 | #import "JLRRouteHandler.h" 17 | #import "JLRRouteRequest.h" 18 | #import "JLRRouteResponse.h" 19 | #import "JLRParsingUtilities.h" 20 | 21 | NS_ASSUME_NONNULL_BEGIN 22 | 23 | /// The matching route pattern, passed in the handler parameters. 24 | extern NSString *const JLRoutePatternKey; 25 | 26 | /// The original URL that was routed, passed in the handler parameters. 27 | extern NSString *const JLRouteURLKey; 28 | 29 | /// The matching route scheme, passed in the handler parameters. 30 | extern NSString *const JLRouteSchemeKey; 31 | 32 | /// The wildcard components (if present) of the matching route, passed in the handler parameters. 33 | extern NSString *const JLRouteWildcardComponentsKey; 34 | 35 | /// The global routes namespace. 36 | /// @see JLRoutes +globalRoutes 37 | extern NSString *const JLRoutesGlobalRoutesScheme; 38 | 39 | 40 | 41 | /** 42 | The JLRoutes class is the main entry-point into the JLRoutes framework. Used for accessing schemes, managing routes, and routing URLs. 43 | */ 44 | 45 | @interface JLRoutes : NSObject 46 | 47 | /// Controls whether or not this router will try to match a URL with global routes if it can't be matched in the current namespace. Default is NO. 48 | @property (nonatomic, assign) BOOL shouldFallbackToGlobalRoutes; 49 | 50 | /// Called any time routeURL returns NO. Respects shouldFallbackToGlobalRoutes. 51 | @property (nonatomic, copy, nullable) void (^unmatchedURLHandler)(JLRoutes *routes, NSURL *__nullable URL, NSDictionary *__nullable parameters); 52 | 53 | 54 | ///------------------------------- 55 | /// @name Routing Schemes 56 | ///------------------------------- 57 | 58 | 59 | /// Returns the global routing scheme 60 | + (instancetype)globalRoutes; 61 | 62 | /// Returns a routing namespace for the given scheme 63 | + (instancetype)routesForScheme:(NSString *)scheme; 64 | 65 | /// Unregister and delete an entire scheme namespace 66 | + (void)unregisterRouteScheme:(NSString *)scheme; 67 | 68 | /// Unregister all routes 69 | + (void)unregisterAllRouteSchemes; 70 | 71 | 72 | ///------------------------------- 73 | /// @name Managing Routes 74 | ///------------------------------- 75 | 76 | 77 | /// Add a route by directly inserted the route definition. This may be a subclass of JLRRouteDefinition to provide customized routing logic. 78 | - (void)addRoute:(JLRRouteDefinition *)routeDefinition; 79 | 80 | /// Registers a routePattern with default priority (0) in the receiving scheme. 81 | - (void)addRoute:(NSString *)routePattern handler:(BOOL (^__nullable)(NSDictionary *parameters))handlerBlock; 82 | 83 | /// Registers a routePattern in the global scheme namespace with a handlerBlock to call when the route pattern is matched by a URL. 84 | /// The block returns a BOOL representing if the handlerBlock actually handled the route or not. If 85 | /// a block returns NO, JLRoutes will continue trying to find a matching route. 86 | - (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^__nullable)(NSDictionary *parameters))handlerBlock; 87 | 88 | /// Registers multiple routePatterns for one handler with default priority (0) in the receiving scheme. 89 | - (void)addRoutes:(NSArray *)routePatterns handler:(BOOL (^__nullable)(NSDictionary *parameters))handlerBlock; 90 | 91 | /// Removes the route from the receiving scheme. 92 | - (void)removeRoute:(JLRRouteDefinition *)routeDefinition; 93 | 94 | /// Removes the first route matching routePattern from the receiving scheme. 95 | - (void)removeRouteWithPattern:(NSString *)routePattern; 96 | 97 | /// Removes all routes from the receiving scheme. 98 | - (void)removeAllRoutes; 99 | 100 | /// Registers a routePattern with default priority (0) using dictionary-style subscripting. 101 | - (void)setObject:(nullable id)handlerBlock forKeyedSubscript:(NSString *)routePatten; 102 | 103 | /// Return all registered routes in the receiving scheme. 104 | /// @see allRoutes 105 | - (NSArray *)routes; 106 | 107 | /// Return all registered routes across all schemes, keyed by scheme 108 | /// @see routes 109 | + (NSDictionary *> *)allRoutes; 110 | 111 | 112 | ///------------------------------- 113 | /// @name Routing URLs 114 | ///------------------------------- 115 | 116 | 117 | /// Returns YES if the provided URL will successfully match against any registered route, NO if not. 118 | + (BOOL)canRouteURL:(nullable NSURL *)URL; 119 | 120 | /// Returns YES if the provided URL will successfully match against any registered route for the current scheme, NO if not. 121 | - (BOOL)canRouteURL:(nullable NSURL *)URL; 122 | 123 | /// Routes a URL, calling handler blocks for patterns that match the URL until one returns YES. 124 | /// If no matching route is found, the unmatchedURLHandler will be called (if set). 125 | + (BOOL)routeURL:(nullable NSURL *)URL; 126 | 127 | /// Routes a URL within a particular scheme, calling handler blocks for patterns that match the URL until one returns YES. 128 | /// If no matching route is found, the unmatchedURLHandler will be called (if set). 129 | - (BOOL)routeURL:(nullable NSURL *)URL; 130 | 131 | /// Routes a URL in any routes scheme, calling handler blocks (for patterns that match URL) until one returns YES. 132 | /// Additional parameters get passed through to the matched route block. 133 | + (BOOL)routeURL:(nullable NSURL *)URL withParameters:(nullable NSDictionary *)parameters; 134 | 135 | /// Routes a URL in a specific scheme, calling handler blocks (for patterns that match URL) until one returns YES. 136 | /// Additional parameters get passed through to the matched route block. 137 | - (BOOL)routeURL:(nullable NSURL *)URL withParameters:(nullable NSDictionary *)parameters; 138 | 139 | @end 140 | 141 | 142 | // Global settings to use for parsing and routing. 143 | 144 | @interface JLRoutes (GlobalOptions) 145 | 146 | ///---------------------------------- 147 | /// @name Configuring Global Options 148 | ///---------------------------------- 149 | 150 | /// Configures verbose logging. Defaults to NO. 151 | + (void)setVerboseLoggingEnabled:(BOOL)loggingEnabled; 152 | 153 | /// Returns current verbose logging enabled state. Defaults to NO. 154 | + (BOOL)isVerboseLoggingEnabled; 155 | 156 | /// Configures if '+' should be replaced with spaces in parsed values. Defaults to YES. 157 | + (void)setShouldDecodePlusSymbols:(BOOL)shouldDecode; 158 | 159 | /// Returns if '+' should be replaced with spaces in parsed values. Defaults to YES. 160 | + (BOOL)shouldDecodePlusSymbols; 161 | 162 | /// Configures if URL host is always considered to be a path component. Defaults to NO. 163 | + (void)setAlwaysTreatsHostAsPathComponent:(BOOL)treatsHostAsPathComponent; 164 | 165 | /// Returns if URL host is always considered to be a path component. Defaults to NO. 166 | + (BOOL)alwaysTreatsHostAsPathComponent; 167 | 168 | /// Configures the default class to use when creating route definitions. Defaults to JLRRouteDefinition. 169 | + (void)setDefaultRouteDefinitionClass:(Class)routeDefinitionClass; 170 | 171 | /// Returns the default class to use when creating route definitions. Defaults to JLRRouteDefinition. 172 | + (Class)defaultRouteDefinitionClass; 173 | 174 | @end 175 | 176 | 177 | 178 | #pragma mark - Deprecated 179 | 180 | extern NSString *const kJLRoutePatternKey DEPRECATED_MSG_ATTRIBUTE("Use JLRoutePatternKey instead."); 181 | extern NSString *const kJLRouteURLKey DEPRECATED_MSG_ATTRIBUTE("Use JLRouteURLKey instead."); 182 | extern NSString *const kJLRouteSchemeKey DEPRECATED_MSG_ATTRIBUTE("Use JLRouteSchemeKey instead."); 183 | extern NSString *const kJLRouteWildcardComponentsKey DEPRECATED_MSG_ATTRIBUTE("Use JLRouteWildcardComponentsKey instead."); 184 | extern NSString *const kJLRoutesGlobalRoutesScheme DEPRECATED_MSG_ATTRIBUTE("Use JLRoutesGlobalRoutesScheme instead."); 185 | extern NSString *const kJLRouteNamespaceKey DEPRECATED_MSG_ATTRIBUTE("Use JLRouteSchemeKey instead."); 186 | extern NSString *const kJLRoutesGlobalNamespaceKey DEPRECATED_MSG_ATTRIBUTE("Use JLRoutesGlobalRoutesScheme instead."); 187 | 188 | 189 | @interface JLRoutes (Deprecated) 190 | 191 | ///---------------------------------- 192 | /// @name Deprecated Methods 193 | ///---------------------------------- 194 | 195 | /// Use the matching instance method on +globalRoutes instead. 196 | + (void)addRoute:(NSString *)routePattern handler:(BOOL (^__nullable)(NSDictionary *parameters))handlerBlock DEPRECATED_MSG_ATTRIBUTE("Use the matching instance method on +globalRoutes instead."); 197 | 198 | /// Use the matching instance method on +globalRoutes instead. 199 | + (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^__nullable)(NSDictionary *parameters))handlerBlock DEPRECATED_MSG_ATTRIBUTE("Use the matching instance method on +globalRoutes instead."); 200 | 201 | /// Use the matching instance method on +globalRoutes instead. 202 | + (void)addRoutes:(NSArray *)routePatterns handler:(BOOL (^__nullable)(NSDictionary *parameters))handlerBlock DEPRECATED_MSG_ATTRIBUTE("Use the matching instance method on +globalRoutes instead."); 203 | 204 | /// Use the matching instance method on +globalRoutes instead. 205 | + (void)removeRoute:(NSString *)routePattern DEPRECATED_MSG_ATTRIBUTE("Use the matching instance method on +globalRoutes instead."); 206 | 207 | /// Use the matching instance method on +globalRoutes instead. 208 | + (void)removeAllRoutes DEPRECATED_MSG_ATTRIBUTE("Use the matching instance method on +globalRoutes instead."); 209 | 210 | /// Use +canRouteURL: instead. 211 | + (BOOL)canRouteURL:(nullable NSURL *)URL withParameters:(nullable NSDictionary *)parameters DEPRECATED_MSG_ATTRIBUTE("Use +canRouteURL: instead."); 212 | 213 | /// Use +canRouteURL: instead. 214 | - (BOOL)canRouteURL:(nullable NSURL *)URL withParameters:(nullable NSDictionary *)parameters DEPRECATED_MSG_ATTRIBUTE("Use -canRouteURL: instead."); 215 | 216 | @end 217 | 218 | 219 | NS_ASSUME_NONNULL_END 220 | -------------------------------------------------------------------------------- /JLRoutes/JLRoutes.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import "JLRoutes.h" 14 | #import "JLRRouteDefinition.h" 15 | #import "JLRParsingUtilities.h" 16 | 17 | 18 | NSString *const JLRoutePatternKey = @"JLRoutePattern"; 19 | NSString *const JLRouteURLKey = @"JLRouteURL"; 20 | NSString *const JLRouteSchemeKey = @"JLRouteScheme"; 21 | NSString *const JLRouteWildcardComponentsKey = @"JLRouteWildcardComponents"; 22 | NSString *const JLRoutesGlobalRoutesScheme = @"JLRoutesGlobalRoutesScheme"; 23 | 24 | 25 | static NSMutableDictionary *JLRGlobal_routeControllersMap = nil; 26 | 27 | 28 | // global options (configured in +initialize) 29 | static BOOL JLRGlobal_verboseLoggingEnabled; 30 | static BOOL JLRGlobal_shouldDecodePlusSymbols; 31 | static BOOL JLRGlobal_alwaysTreatsHostAsPathComponent; 32 | static Class JLRGlobal_routeDefinitionClass; 33 | 34 | 35 | @interface JLRoutes () 36 | 37 | @property (nonatomic, strong) NSMutableArray *mutableRoutes; 38 | @property (nonatomic, strong) NSString *scheme; 39 | 40 | - (JLRRouteRequestOptions)_routeRequestOptions; 41 | 42 | @end 43 | 44 | 45 | #pragma mark - 46 | 47 | @implementation JLRoutes 48 | 49 | + (void)initialize 50 | { 51 | if (self == [JLRoutes class]) { 52 | // Set default global options 53 | JLRGlobal_verboseLoggingEnabled = NO; 54 | JLRGlobal_shouldDecodePlusSymbols = YES; 55 | JLRGlobal_alwaysTreatsHostAsPathComponent = NO; 56 | JLRGlobal_routeDefinitionClass = [JLRRouteDefinition class]; 57 | } 58 | } 59 | 60 | - (instancetype)init 61 | { 62 | if ((self = [super init])) { 63 | self.mutableRoutes = [NSMutableArray array]; 64 | } 65 | return self; 66 | } 67 | 68 | - (NSString *)description 69 | { 70 | return [self.mutableRoutes description]; 71 | } 72 | 73 | + (NSDictionary *> *)allRoutes; 74 | { 75 | NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; 76 | 77 | for (NSString *namespace in [JLRGlobal_routeControllersMap copy]) { 78 | JLRoutes *routesController = JLRGlobal_routeControllersMap[namespace]; 79 | dictionary[namespace] = [routesController.mutableRoutes copy]; 80 | } 81 | 82 | return [dictionary copy]; 83 | } 84 | 85 | 86 | #pragma mark - Routing Schemes 87 | 88 | + (instancetype)globalRoutes 89 | { 90 | return [self routesForScheme:JLRoutesGlobalRoutesScheme]; 91 | } 92 | 93 | + (instancetype)routesForScheme:(NSString *)scheme 94 | { 95 | JLRoutes *routesController = nil; 96 | 97 | static dispatch_once_t onceToken; 98 | dispatch_once(&onceToken, ^{ 99 | JLRGlobal_routeControllersMap = [[NSMutableDictionary alloc] init]; 100 | }); 101 | 102 | if (!JLRGlobal_routeControllersMap[scheme]) { 103 | routesController = [[self alloc] init]; 104 | routesController.scheme = scheme; 105 | JLRGlobal_routeControllersMap[scheme] = routesController; 106 | } 107 | 108 | routesController = JLRGlobal_routeControllersMap[scheme]; 109 | 110 | return routesController; 111 | } 112 | 113 | + (void)unregisterRouteScheme:(NSString *)scheme 114 | { 115 | [JLRGlobal_routeControllersMap removeObjectForKey:scheme]; 116 | } 117 | 118 | + (void)unregisterAllRouteSchemes 119 | { 120 | [JLRGlobal_routeControllersMap removeAllObjects]; 121 | } 122 | 123 | 124 | #pragma mark - Registering Routes 125 | 126 | - (void)addRoute:(JLRRouteDefinition *)routeDefinition 127 | { 128 | [self _registerRoute:routeDefinition]; 129 | } 130 | 131 | - (void)addRoute:(NSString *)routePattern handler:(BOOL (^)(NSDictionary *parameters))handlerBlock 132 | { 133 | [self addRoute:routePattern priority:0 handler:handlerBlock]; 134 | } 135 | 136 | - (void)addRoutes:(NSArray *)routePatterns handler:(BOOL (^)(NSDictionary *parameters))handlerBlock 137 | { 138 | for (NSString *routePattern in routePatterns) { 139 | [self addRoute:routePattern handler:handlerBlock]; 140 | } 141 | } 142 | 143 | - (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary *parameters))handlerBlock 144 | { 145 | NSArray *optionalRoutePatterns = [JLRParsingUtilities expandOptionalRoutePatternsForPattern:routePattern]; 146 | JLRRouteDefinition *route = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:routePattern priority:priority handlerBlock:handlerBlock]; 147 | 148 | if (optionalRoutePatterns.count > 0) { 149 | // there are optional params, parse and add them 150 | for (NSString *pattern in optionalRoutePatterns) { 151 | JLRRouteDefinition *optionalRoute = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:pattern priority:priority handlerBlock:handlerBlock]; 152 | [self _registerRoute:optionalRoute]; 153 | [self _verboseLog:@"Automatically created optional route: %@", optionalRoute]; 154 | } 155 | return; 156 | } 157 | 158 | [self _registerRoute:route]; 159 | } 160 | 161 | - (void)removeRoute:(JLRRouteDefinition *)routeDefinition 162 | { 163 | [self.mutableRoutes removeObject:routeDefinition]; 164 | } 165 | 166 | - (void)removeRouteWithPattern:(NSString *)routePattern 167 | { 168 | NSInteger routeIndex = NSNotFound; 169 | NSInteger index = 0; 170 | 171 | for (JLRRouteDefinition *route in [self.mutableRoutes copy]) { 172 | if ([route.pattern isEqualToString:routePattern]) { 173 | routeIndex = index; 174 | break; 175 | } 176 | index++; 177 | } 178 | 179 | if (routeIndex != NSNotFound) { 180 | [self.mutableRoutes removeObjectAtIndex:(NSUInteger)routeIndex]; 181 | } 182 | } 183 | 184 | - (void)removeAllRoutes 185 | { 186 | [self.mutableRoutes removeAllObjects]; 187 | } 188 | 189 | - (void)setObject:(id)handlerBlock forKeyedSubscript:(NSString *)routePatten 190 | { 191 | [self addRoute:routePatten handler:handlerBlock]; 192 | } 193 | 194 | - (NSArray *)routes; 195 | { 196 | return [self.mutableRoutes copy]; 197 | } 198 | 199 | #pragma mark - Routing URLs 200 | 201 | + (BOOL)canRouteURL:(NSURL *)URL 202 | { 203 | return [[self _routesControllerForURL:URL] canRouteURL:URL]; 204 | } 205 | 206 | - (BOOL)canRouteURL:(NSURL *)URL 207 | { 208 | return [self _routeURL:URL withParameters:nil executeRouteBlock:NO]; 209 | } 210 | 211 | + (BOOL)routeURL:(NSURL *)URL 212 | { 213 | return [[self _routesControllerForURL:URL] routeURL:URL]; 214 | } 215 | 216 | - (BOOL)routeURL:(NSURL *)URL 217 | { 218 | return [self _routeURL:URL withParameters:nil executeRouteBlock:YES]; 219 | } 220 | 221 | + (BOOL)routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters 222 | { 223 | return [[self _routesControllerForURL:URL] routeURL:URL withParameters:parameters]; 224 | } 225 | 226 | - (BOOL)routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters 227 | { 228 | return [self _routeURL:URL withParameters:parameters executeRouteBlock:YES]; 229 | } 230 | 231 | 232 | #pragma mark - Private 233 | 234 | + (instancetype)_routesControllerForURL:(NSURL *)URL 235 | { 236 | if (URL == nil) { 237 | return nil; 238 | } 239 | 240 | return JLRGlobal_routeControllersMap[URL.scheme] ?: [JLRoutes globalRoutes]; 241 | } 242 | 243 | - (void)_registerRoute:(JLRRouteDefinition *)route 244 | { 245 | if (route.priority == 0 || self.mutableRoutes.count == 0) { 246 | [self.mutableRoutes addObject:route]; 247 | } else { 248 | NSUInteger index = 0; 249 | BOOL addedRoute = NO; 250 | 251 | // search through existing routes looking for a lower priority route than this one 252 | for (JLRRouteDefinition *existingRoute in [self.mutableRoutes copy]) { 253 | if (existingRoute.priority < route.priority) { 254 | // if found, add the route after it 255 | [self.mutableRoutes insertObject:route atIndex:index]; 256 | addedRoute = YES; 257 | break; 258 | } 259 | index++; 260 | } 261 | 262 | // if we weren't able to find a lower priority route, this is the new lowest priority route (or same priority as self.routes.lastObject) and should just be added 263 | if (!addedRoute) { 264 | [self.mutableRoutes addObject:route]; 265 | } 266 | } 267 | 268 | [route didBecomeRegisteredForScheme:self.scheme]; 269 | } 270 | 271 | - (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters executeRouteBlock:(BOOL)executeRouteBlock 272 | { 273 | if (!URL) { 274 | return NO; 275 | } 276 | 277 | [self _verboseLog:@"Trying to route URL %@", URL]; 278 | 279 | BOOL didRoute = NO; 280 | 281 | JLRRouteRequestOptions options = [self _routeRequestOptions]; 282 | JLRRouteRequest *request = [[JLRRouteRequest alloc] initWithURL:URL options:options additionalParameters:parameters]; 283 | 284 | for (JLRRouteDefinition *route in [self.mutableRoutes copy]) { 285 | // check each route for a matching response 286 | JLRRouteResponse *response = [route routeResponseForRequest:request]; 287 | if (!response.isMatch) { 288 | continue; 289 | } 290 | 291 | [self _verboseLog:@"Successfully matched %@", route]; 292 | 293 | if (!executeRouteBlock) { 294 | // if we shouldn't execute but it was a match, we're done now 295 | return YES; 296 | } 297 | 298 | [self _verboseLog:@"Match parameters are %@", response.parameters]; 299 | 300 | // Call the handler block 301 | didRoute = [route callHandlerBlockWithParameters:response.parameters]; 302 | 303 | if (didRoute) { 304 | // if it was routed successfully, we're done - otherwise, continue trying to route 305 | break; 306 | } 307 | } 308 | 309 | if (!didRoute) { 310 | [self _verboseLog:@"Could not find a matching route"]; 311 | } 312 | 313 | // if we couldn't find a match and this routes controller specifies to fallback and its also not the global routes controller, then... 314 | if (!didRoute && self.shouldFallbackToGlobalRoutes && ![self _isGlobalRoutesController]) { 315 | [self _verboseLog:@"Falling back to global routes..."]; 316 | didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock]; 317 | } 318 | 319 | // if, after everything, we did not route anything and we have an unmatched URL handler, then call it 320 | if (!didRoute && executeRouteBlock && self.unmatchedURLHandler) { 321 | [self _verboseLog:@"Falling back to the unmatched URL handler"]; 322 | self.unmatchedURLHandler(self, URL, parameters); 323 | } 324 | 325 | return didRoute; 326 | } 327 | 328 | - (BOOL)_isGlobalRoutesController 329 | { 330 | return [self.scheme isEqualToString:JLRoutesGlobalRoutesScheme]; 331 | } 332 | 333 | - (void)_verboseLog:(NSString *)format, ... 334 | { 335 | if (!JLRGlobal_verboseLoggingEnabled || format.length == 0) { 336 | return; 337 | } 338 | 339 | va_list argsList; 340 | va_start(argsList, format); 341 | #pragma clang diagnostic push 342 | #pragma clang diagnostic ignored "-Wformat-nonliteral" 343 | NSString *formattedLogMessage = [[NSString alloc] initWithFormat:format arguments:argsList]; 344 | #pragma clang diagnostic pop 345 | 346 | va_end(argsList); 347 | NSLog(@"[JLRoutes]: %@", formattedLogMessage); 348 | } 349 | 350 | - (JLRRouteRequestOptions)_routeRequestOptions 351 | { 352 | JLRRouteRequestOptions options = JLRRouteRequestOptionsNone; 353 | 354 | if (JLRGlobal_shouldDecodePlusSymbols) { 355 | options |= JLRRouteRequestOptionDecodePlusSymbols; 356 | } 357 | if (JLRGlobal_alwaysTreatsHostAsPathComponent) { 358 | options |= JLRRouteRequestOptionTreatHostAsPathComponent; 359 | } 360 | 361 | return options; 362 | } 363 | 364 | @end 365 | 366 | 367 | #pragma mark - Global Options 368 | 369 | @implementation JLRoutes (GlobalOptions) 370 | 371 | + (void)setVerboseLoggingEnabled:(BOOL)loggingEnabled 372 | { 373 | JLRGlobal_verboseLoggingEnabled = loggingEnabled; 374 | } 375 | 376 | + (BOOL)isVerboseLoggingEnabled 377 | { 378 | return JLRGlobal_verboseLoggingEnabled; 379 | } 380 | 381 | + (void)setShouldDecodePlusSymbols:(BOOL)shouldDecode 382 | { 383 | JLRGlobal_shouldDecodePlusSymbols = shouldDecode; 384 | } 385 | 386 | + (BOOL)shouldDecodePlusSymbols 387 | { 388 | return JLRGlobal_shouldDecodePlusSymbols; 389 | } 390 | 391 | + (void)setAlwaysTreatsHostAsPathComponent:(BOOL)treatsHostAsPathComponent 392 | { 393 | JLRGlobal_alwaysTreatsHostAsPathComponent = treatsHostAsPathComponent; 394 | } 395 | 396 | + (BOOL)alwaysTreatsHostAsPathComponent 397 | { 398 | return JLRGlobal_alwaysTreatsHostAsPathComponent; 399 | } 400 | 401 | + (void)setDefaultRouteDefinitionClass:(Class)routeDefinitionClass 402 | { 403 | NSParameterAssert([routeDefinitionClass isSubclassOfClass:[JLRRouteDefinition class]]); 404 | JLRGlobal_routeDefinitionClass = routeDefinitionClass; 405 | } 406 | 407 | + (Class)defaultRouteDefinitionClass 408 | { 409 | return JLRGlobal_routeDefinitionClass; 410 | } 411 | 412 | @end 413 | 414 | 415 | #pragma mark - Deprecated 416 | 417 | NSString *const kJLRoutePatternKey = @"JLRoutePattern"; 418 | NSString *const kJLRouteURLKey = @"JLRouteURL"; 419 | NSString *const kJLRouteSchemeKey = @"JLRouteScheme"; 420 | NSString *const kJLRouteWildcardComponentsKey = @"JLRouteWildcardComponents"; 421 | NSString *const kJLRoutesGlobalRoutesScheme = @"JLRoutesGlobalRoutesScheme"; 422 | NSString *const kJLRouteNamespaceKey = @"JLRouteScheme"; 423 | NSString *const kJLRoutesGlobalNamespaceKey = @"JLRoutesGlobalRoutesScheme"; 424 | 425 | 426 | @implementation JLRoutes (Deprecated) 427 | 428 | + (void)addRoute:(NSString *)routePattern handler:(BOOL (^)(NSDictionary *parameters))handlerBlock 429 | { 430 | [[self globalRoutes] addRoute:routePattern handler:handlerBlock]; 431 | } 432 | 433 | + (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary *parameters))handlerBlock 434 | { 435 | [[self globalRoutes] addRoute:routePattern priority:priority handler:handlerBlock]; 436 | } 437 | 438 | + (void)addRoutes:(NSArray *)routePatterns handler:(BOOL (^)(NSDictionary *parameters))handlerBlock 439 | { 440 | [[self globalRoutes] addRoutes:routePatterns handler:handlerBlock]; 441 | } 442 | 443 | + (void)removeRoute:(NSString *)routePattern 444 | { 445 | [[self globalRoutes] removeRouteWithPattern:routePattern]; 446 | } 447 | 448 | + (void)removeAllRoutes 449 | { 450 | [[self globalRoutes] removeAllRoutes]; 451 | } 452 | 453 | + (BOOL)canRouteURL:(NSURL *)URL withParameters:(NSDictionary *)parameters 454 | { 455 | return [[self globalRoutes] canRouteURL:URL]; 456 | } 457 | 458 | - (BOOL)canRouteURL:(NSURL *)URL withParameters:(NSDictionary *)parameters 459 | { 460 | return [self canRouteURL:URL]; 461 | } 462 | 463 | @end 464 | -------------------------------------------------------------------------------- /JLRoutesTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /JLRoutesTests/JLRoutesTests.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Joel Levin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | */ 12 | 13 | #import 14 | #import "JLRoutes.h" 15 | #import "JLRRouteDefinition.h" 16 | #import "JLRRouteHandler.h" 17 | 18 | 19 | #define JLValidateParameterCount(expectedCount)\ 20 | XCTAssertNotNil(self.lastMatch, @"Matched something");\ 21 | XCTAssertEqual((NSInteger)[self.lastMatch count] - 3, (NSInteger)expectedCount, @"Expected parameter count") 22 | 23 | #define JLValidateParameterCountIncludingWildcard(expectedCount)\ 24 | XCTAssertNotNil(self.lastMatch, @"Matched something");\ 25 | XCTAssertEqual((NSInteger)[self.lastMatch count] - 4, (NSInteger)expectedCount, @"Expected parameter count") 26 | 27 | #define JLValidateParameter(parameter) {\ 28 | NSString *key = [[parameter allKeys] lastObject];\ 29 | NSString *value = [[parameter allValues] lastObject];\ 30 | XCTAssertEqualObjects(self.lastMatch[key], value, @"Exact parameter pair not found");} 31 | 32 | #define JLValidateAnyRouteMatched()\ 33 | XCTAssertTrue(self.didRoute, @"Expected any route to match") 34 | 35 | #define JLValidateNoLastMatch()\ 36 | XCTAssertFalse(self.didRoute, @"Expected not to route successfully") 37 | 38 | #define JLValidatePattern(pattern)\ 39 | XCTAssertEqualObjects(self.lastMatch[JLRoutePatternKey], pattern, @"Pattern did not match") 40 | 41 | #define JLValidateScheme(scheme)\ 42 | XCTAssertEqualObjects(self.lastMatch[JLRouteSchemeKey], scheme, @"Scheme did not match") 43 | 44 | 45 | #pragma mark - 46 | 47 | 48 | @interface JLRMockTargetObject : NSObject 49 | 50 | @property (nonatomic, copy) NSDictionary *routeParams; 51 | 52 | @end 53 | 54 | 55 | @interface JLRMockRouteDefinition : JLRRouteDefinition 56 | @end 57 | 58 | 59 | @interface JLRoutesTests : XCTestCase 60 | 61 | @property (assign) BOOL didRoute; 62 | @property (strong) NSDictionary *lastMatch; 63 | 64 | + (BOOL (^)(NSDictionary *))defaultRouteHandler; 65 | 66 | - (void)route:(NSString *)URLString; 67 | 68 | @end 69 | 70 | 71 | static JLRoutesTests *testsInstance = nil; 72 | 73 | 74 | @implementation JLRoutesTests 75 | 76 | + (void)setUp 77 | { 78 | [super setUp]; 79 | [JLRoutes setVerboseLoggingEnabled:YES]; 80 | } 81 | 82 | - (void)setUp 83 | { 84 | [super setUp]; 85 | 86 | testsInstance = self; 87 | 88 | // reset settings 89 | [JLRoutes setShouldDecodePlusSymbols:YES]; 90 | [JLRoutes setAlwaysTreatsHostAsPathComponent:NO]; 91 | [JLRoutes setDefaultRouteDefinitionClass:[JLRRouteDefinition class]]; 92 | } 93 | 94 | - (void)tearDown 95 | { 96 | [super tearDown]; 97 | [JLRoutes unregisterAllRouteSchemes]; 98 | } 99 | 100 | #pragma mark - Tests 101 | 102 | - (void)testRoutesArray 103 | { 104 | id defaultHandler = [[self class] defaultRouteHandler]; 105 | 106 | [[JLRoutes globalRoutes] addRoute:@"/global1" handler:defaultHandler]; 107 | [[JLRoutes globalRoutes] addRoute:@"/global2" handler:defaultHandler]; 108 | [[JLRoutes globalRoutes] addRoute:@"/global3" handler:defaultHandler]; 109 | 110 | NSArray *globalRoutes = [[JLRoutes globalRoutes] routes]; 111 | 112 | XCTAssertEqual(globalRoutes.count, 3UL); 113 | XCTAssertEqualObjects(globalRoutes[0].pattern, @"/global1"); 114 | XCTAssertEqualObjects(globalRoutes[1].pattern, @"/global2"); 115 | XCTAssertEqualObjects(globalRoutes[2].pattern, @"/global3"); 116 | 117 | [[JLRoutes routesForScheme:@"scheme"] addRoute:@"/scheme1" handler:defaultHandler]; 118 | [[JLRoutes routesForScheme:@"scheme"] addRoute:@"/scheme2" handler:defaultHandler]; 119 | 120 | NSArray *schemeRoutes = [[JLRoutes routesForScheme:@"scheme"] routes]; 121 | 122 | XCTAssertEqual(schemeRoutes.count, 2UL); 123 | XCTAssertEqualObjects(schemeRoutes[0].pattern, @"/scheme1"); 124 | XCTAssertEqualObjects(schemeRoutes[1].pattern, @"/scheme2"); 125 | 126 | NSArray *nonexistant = [[JLRoutes routesForScheme:@"foo"] routes]; 127 | XCTAssertEqual(nonexistant.count, 0UL); 128 | } 129 | 130 | - (void)testAllRoutes 131 | { 132 | id defaultHandler = [[self class] defaultRouteHandler]; 133 | 134 | [[JLRoutes globalRoutes] addRoute:@"/global1" handler:defaultHandler]; 135 | [[JLRoutes globalRoutes] addRoute:@"/global2" handler:defaultHandler]; 136 | [[JLRoutes globalRoutes] addRoute:@"/global3" handler:defaultHandler]; 137 | [[JLRoutes routesForScheme:@"scheme"] addRoute:@"/scheme1" handler:defaultHandler]; 138 | [[JLRoutes routesForScheme:@"scheme"] addRoute:@"/scheme2" handler:defaultHandler]; 139 | 140 | NSDictionary *> *allRoutes = [JLRoutes allRoutes]; 141 | 142 | NSArray *globalRoutes = allRoutes[JLRoutesGlobalRoutesScheme]; 143 | XCTAssertEqual(globalRoutes.count, 3UL); 144 | XCTAssertEqualObjects(globalRoutes[0].pattern, @"/global1"); 145 | XCTAssertEqualObjects(globalRoutes[1].pattern, @"/global2"); 146 | XCTAssertEqualObjects(globalRoutes[2].pattern, @"/global3"); 147 | 148 | NSArray *schemeRoutes = allRoutes[@"scheme"]; 149 | XCTAssertEqual(schemeRoutes.count, 2UL); 150 | XCTAssertEqualObjects(schemeRoutes[0].pattern, @"/scheme1"); 151 | XCTAssertEqualObjects(schemeRoutes[1].pattern, @"/scheme2"); 152 | } 153 | 154 | - (void)testRouting 155 | { 156 | id defaultHandler = [[self class] defaultRouteHandler]; 157 | 158 | [[JLRoutes globalRoutes] addRoute:@"/test" handler:defaultHandler]; 159 | [[JLRoutes globalRoutes] addRoute:@"/user/view/:userID" handler:defaultHandler]; 160 | [[JLRoutes globalRoutes] addRoute:@"/:object/:action/:primaryKey" handler:defaultHandler]; 161 | [[JLRoutes globalRoutes] addRoute:@"/" handler:defaultHandler]; 162 | [[JLRoutes globalRoutes] addRoute:@"/:" handler:defaultHandler]; 163 | [[JLRoutes globalRoutes] addRoute:@"/interleaving/:param1/foo/:param2" handler:defaultHandler]; 164 | [[JLRoutes globalRoutes] addRoute:@"/xyz/wildcard/*" handler:defaultHandler]; 165 | [[JLRoutes globalRoutes] addRoute:@"/route/:param/*" handler:defaultHandler]; 166 | [[JLRoutes globalRoutes] addRoute:@"/required/:requiredParam(/optional/:optionalParam)(/moreOptional/:moreOptionalParam)" handler:defaultHandler]; 167 | 168 | [self route:nil]; 169 | JLValidateNoLastMatch(); 170 | 171 | [self route:@"tests:/"]; 172 | JLValidateAnyRouteMatched(); 173 | JLValidatePattern(@"/"); 174 | JLValidateParameterCount(0); 175 | 176 | [self route:@"tests://"]; 177 | JLValidateAnyRouteMatched(); 178 | JLValidatePattern(@"/"); 179 | JLValidateParameterCount(0); 180 | 181 | [self route:@"tests://test?"]; 182 | JLValidateAnyRouteMatched(); 183 | JLValidateParameterCount(0); 184 | JLValidatePattern(@"/test"); 185 | 186 | [self route:@"tests://test/"]; 187 | JLValidateAnyRouteMatched(); 188 | JLValidateParameterCount(0); 189 | JLValidatePattern(@"/test"); 190 | 191 | [self route:@"tests://test"]; 192 | JLValidateAnyRouteMatched(); 193 | JLValidateParameterCount(0); 194 | 195 | [self route:@"tests://?key=value"]; 196 | JLValidateAnyRouteMatched(); 197 | JLValidateParameterCount(1); 198 | JLValidateParameter(@{@"key": @"value"}); 199 | 200 | [self route:@"tests://user/view/joeldev"]; 201 | JLValidateAnyRouteMatched(); 202 | JLValidateParameterCount(1); 203 | JLValidateParameter(@{@"userID": @"joeldev"}); 204 | 205 | [self route:@"tests://user/view/joeldev/"]; 206 | JLValidateAnyRouteMatched(); 207 | JLValidateParameterCount(1); 208 | JLValidateParameter(@{@"userID": @"joeldev"}); 209 | 210 | [self route:@"tests://user/view/joel%20levin"]; 211 | JLValidateAnyRouteMatched(); 212 | JLValidateParameterCount(1); 213 | JLValidateParameter(@{@"userID": @"joel levin"}); 214 | 215 | [self route:@"tests://user/view/joeldev?foo=bar&thing=stuff"]; 216 | JLValidateAnyRouteMatched(); 217 | JLValidateParameterCount(3); 218 | JLValidateParameter(@{@"userID": @"joeldev"}); 219 | JLValidateParameter(@{@"foo" : @"bar"}); 220 | JLValidateParameter(@{@"thing" : @"stuff"}); 221 | 222 | [self route:@"tests://user/view/joeldev#foo=bar&thing=stuff"]; 223 | JLValidateAnyRouteMatched(); 224 | JLValidateParameterCount(3); 225 | JLValidateParameter(@{@"userID": @"joeldev"}); 226 | JLValidateParameter(@{@"foo" : @"bar"}); 227 | JLValidateParameter(@{@"thing" : @"stuff"}); 228 | 229 | [self route:@"tests://user/view/joeldev?userID=evilPerson"]; 230 | JLValidateAnyRouteMatched(); 231 | JLValidateParameterCount(1); 232 | JLValidateParameter(@{@"userID": @"joeldev"}); 233 | 234 | [self route:@"tests://post/edit/123"]; 235 | JLValidateAnyRouteMatched(); 236 | JLValidateParameterCount(3); 237 | JLValidateParameter(@{@"object": @"post"}); 238 | JLValidateParameter(@{@"action": @"edit"}); 239 | JLValidateParameter(@{@"primaryKey": @"123"}); 240 | 241 | [self route:@"tests://interleaving/paramvalue1/foo/paramvalue2"]; 242 | JLValidateAnyRouteMatched(); 243 | JLValidateParameterCount(2); 244 | JLValidateParameter(@{@"param1": @"paramvalue1"}); 245 | JLValidateParameter(@{@"param2": @"paramvalue2"}); 246 | 247 | [self route:@"tests://xyz/wildcard"]; 248 | JLValidateAnyRouteMatched(); 249 | JLValidateParameterCountIncludingWildcard(0); 250 | 251 | [self route:@"tests://xyz/wildcard/matches/with/extra/path/components"]; 252 | JLValidateAnyRouteMatched(); 253 | JLValidateParameterCount(1); 254 | NSArray *wildcardMatches = @[@"matches", @"with", @"extra", @"path", @"components"]; 255 | JLValidateParameter(@{JLRouteWildcardComponentsKey: wildcardMatches}); 256 | 257 | [self route:@"tests://route/matches/with/wildcard"]; 258 | JLValidateAnyRouteMatched(); 259 | JLValidateParameterCount(2); 260 | JLValidateParameter(@{@"param": @"matches"}); 261 | NSArray *parameterWildcardMatches = @[@"with", @"wildcard"]; 262 | JLValidateParameter(@{JLRouteWildcardComponentsKey: parameterWildcardMatches}); 263 | 264 | [self route:@"tests://doesnt/exist/and/wont/match"]; 265 | JLValidateNoLastMatch(); 266 | 267 | [self routeURL:[NSURL URLWithString:@"/test" relativeToURL:[NSURL URLWithString:@"http://localhost"]] withParameters:nil]; 268 | JLValidateAnyRouteMatched(); 269 | JLValidatePattern(@"/test"); 270 | JLValidateParameterCount(0); 271 | 272 | [self route:@"tests://required/mustExist"]; 273 | JLValidateAnyRouteMatched(); 274 | JLValidateParameterCount(1); 275 | JLValidateParameter(@{@"requiredParam": @"mustExist"}); 276 | 277 | [self route:@"tests://required/mustExist/optional/mightExist"]; 278 | JLValidateAnyRouteMatched(); 279 | JLValidateParameterCount(2); 280 | JLValidateParameter(@{@"requiredParam": @"mustExist"}); 281 | JLValidateParameter(@{@"optionalParam": @"mightExist"}); 282 | 283 | [self route:@"tests://required/mustExist/optional/mightExist/moreOptional/mightExistToo"]; 284 | JLValidateAnyRouteMatched(); 285 | JLValidateParameterCount(3); 286 | JLValidateParameter(@{@"requiredParam": @"mustExist"}); 287 | JLValidateParameter(@{@"optionalParam": @"mightExist"}); 288 | JLValidateParameter(@{@"moreOptionalParam": @"mightExistToo"}); 289 | } 290 | 291 | - (void)testRoutingWithParameters 292 | { 293 | id defaultHandler = [[self class] defaultRouteHandler]; 294 | 295 | [[JLRoutes globalRoutes] addRoute:@"/foo/:routeParam" handler:defaultHandler]; 296 | 297 | [self route:@"/foo/bar" withParameters:@{@"stringParam": @"stringValue", @"nonStringParam": @(123)}]; 298 | JLValidateAnyRouteMatched(); 299 | JLValidateParameterCount(3); 300 | JLValidateParameter(@{@"routeParam": @"bar"}); 301 | JLValidateParameter(@{@"stringParam": @"stringValue"}); 302 | JLValidateParameter(@{@"nonStringParam": @(123)}); 303 | } 304 | 305 | - (void)testFragmentRouting 306 | { 307 | id defaultHandler = [[self class] defaultRouteHandler]; 308 | 309 | [[JLRoutes globalRoutes] addRoute:@"/user#/view/:userID" handler:defaultHandler]; 310 | [[JLRoutes globalRoutes] addRoute:@"/:object#/:action/:primaryKey" handler:defaultHandler]; 311 | [[JLRoutes globalRoutes] addRoute:@"/interleaving/:param1#/foo/:param2" handler:defaultHandler]; 312 | [[JLRoutes globalRoutes] addRoute:@"/xyz/wildcard#/*" handler:defaultHandler]; 313 | [[JLRoutes globalRoutes] addRoute:@"/route#/:param/*" handler:defaultHandler]; 314 | [[JLRoutes globalRoutes] addRoute:@"/required#/:requiredParam(/optional/:optionalParam)(/moreOptional/:moreOptionalParam)" handler:defaultHandler]; 315 | 316 | [self route:nil]; 317 | JLValidateNoLastMatch(); 318 | 319 | [self route:@"tests://user#/view/joeldev"]; 320 | JLValidateAnyRouteMatched(); 321 | JLValidateParameterCount(1); 322 | JLValidateParameter(@{@"userID": @"joeldev"}); 323 | 324 | [self route:@"tests://user#/view/joeldev/"]; 325 | JLValidateAnyRouteMatched(); 326 | JLValidateParameterCount(1); 327 | JLValidateParameter(@{@"userID": @"joeldev"}); 328 | 329 | [self route:@"tests://user#/view/joel%20levin"]; 330 | JLValidateAnyRouteMatched(); 331 | JLValidateParameterCount(1); 332 | JLValidateParameter(@{@"userID": @"joel levin"}); 333 | 334 | [self route:@"tests://user#/view/joeldev?foo=bar&thing=stuff"]; 335 | JLValidateAnyRouteMatched(); 336 | JLValidateParameterCount(3); 337 | JLValidateParameter(@{@"userID": @"joeldev"}); 338 | JLValidateParameter(@{@"foo" : @"bar"}); 339 | JLValidateParameter(@{@"thing" : @"stuff"}); 340 | 341 | [self route:@"tests://user#/view/joeldev?userID=evilPerson"]; 342 | JLValidateAnyRouteMatched(); 343 | JLValidateParameterCount(1); 344 | JLValidateParameter(@{@"userID": @"joeldev"}); 345 | 346 | [self route:@"tests://user#/view/joeldev?userID=evilPerson&search=evilSearch&evilThing=evil"]; 347 | JLValidateAnyRouteMatched(); 348 | JLValidateParameterCount(3); 349 | JLValidateParameter(@{@"userID": @"joeldev"}); 350 | JLValidateParameter(@{@"search": @"evilSearch"}); 351 | 352 | [self route:@"tests://user?search=niceSearch&go=home#/view/joeldev?userID=evilPerson&&evilThing=evil"]; 353 | JLValidateAnyRouteMatched(); 354 | JLValidateParameterCount(4); 355 | JLValidateParameter(@{@"userID": @"joeldev"}); 356 | JLValidateParameter(@{@"go": @"home"}); 357 | 358 | [self route:@"tests://post#/edit/123"]; 359 | JLValidateAnyRouteMatched(); 360 | JLValidateParameterCount(3); 361 | JLValidateParameter(@{@"object": @"post"}); 362 | JLValidateParameter(@{@"action": @"edit"}); 363 | JLValidateParameter(@{@"primaryKey": @"123"}); 364 | 365 | [self route:@"tests://interleaving/paramvalue1#/foo/paramvalue2"]; 366 | JLValidateAnyRouteMatched(); 367 | JLValidateParameterCount(2); 368 | JLValidateParameter(@{@"param1": @"paramvalue1"}); 369 | JLValidateParameter(@{@"param2": @"paramvalue2"}); 370 | 371 | [self route:@"tests://xyz/wildcard#"]; 372 | JLValidateAnyRouteMatched(); 373 | JLValidateParameterCountIncludingWildcard(0); 374 | 375 | [self route:@"tests://xyz/wildcard#/matches/with/extra/path/components"]; 376 | JLValidateAnyRouteMatched(); 377 | JLValidateParameterCount(1); 378 | NSArray *wildcardMatches = @[@"matches", @"with", @"extra", @"path", @"components"]; 379 | JLValidateParameter(@{JLRouteWildcardComponentsKey: wildcardMatches}); 380 | 381 | [self route:@"tests://route#/matches/with/wildcard"]; 382 | JLValidateAnyRouteMatched(); 383 | JLValidateParameterCount(2); 384 | JLValidateParameter(@{@"param": @"matches"}); 385 | NSArray *parameterWildcardMatches = @[@"with", @"wildcard"]; 386 | JLValidateParameter(@{JLRouteWildcardComponentsKey: parameterWildcardMatches}); 387 | 388 | [self route:@"tests://doesnt/exist#/and/wont/match"]; 389 | JLValidateNoLastMatch(); 390 | 391 | [self route:@"tests://required#/mustExist"]; 392 | JLValidateAnyRouteMatched(); 393 | JLValidateParameterCount(1); 394 | JLValidateParameter(@{@"requiredParam": @"mustExist"}); 395 | 396 | [self route:@"tests://required#/mustExist/optional/mightExist"]; 397 | JLValidateAnyRouteMatched(); 398 | JLValidateParameterCount(2); 399 | JLValidateParameter(@{@"requiredParam": @"mustExist"}); 400 | JLValidateParameter(@{@"optionalParam": @"mightExist"}); 401 | 402 | [self route:@"tests://required#/mustExist/optional/mightExist/moreOptional/mightExistToo"]; 403 | JLValidateAnyRouteMatched(); 404 | JLValidateParameterCount(3); 405 | JLValidateParameter(@{@"requiredParam": @"mustExist"}); 406 | JLValidateParameter(@{@"optionalParam": @"mightExist"}); 407 | JLValidateParameter(@{@"moreOptionalParam": @"mightExistToo"}); 408 | } 409 | 410 | - (void)testMultipleRoutePatterns 411 | { 412 | [[JLRoutes globalRoutes] addRoutes:@[@"/multiple1", @"/multiple2"] handler:[[self class] defaultRouteHandler]]; 413 | 414 | [self route:@"tests://multiple1"]; 415 | JLValidateAnyRouteMatched(); 416 | JLValidateParameterCount(0); 417 | 418 | [self route:@"tests://multiple2"]; 419 | JLValidateAnyRouteMatched(); 420 | JLValidateParameterCount(0); 421 | } 422 | 423 | - (void)testPriority 424 | { 425 | id defaultHandler = [[self class] defaultRouteHandler]; 426 | 427 | [[JLRoutes globalRoutes] addRoute:@"/test/priority/:level" handler:defaultHandler]; 428 | [[JLRoutes globalRoutes] addRoute:@"/test/priority/high" priority:20 handler:defaultHandler]; 429 | 430 | // this should match the /test/priority/high route even though there's one before it that would match if priority wasn't being set 431 | [self route:@"tests://test/priority/high"]; 432 | JLValidateAnyRouteMatched(); 433 | JLValidatePattern(@"/test/priority/high"); 434 | 435 | // test for adding only routes with non-zero priority (https://github.com/joeldev/JLRoutes/issues/46) 436 | [[JLRoutes routesForScheme:@"priorityTest"] addRoute:@"/:foo/bar/:baz" priority:20 handler:[[self class] defaultRouteHandler]]; 437 | [[JLRoutes routesForScheme:@"priorityTest"] addRoute:@"/:foo/things/:baz" priority:10 handler:[[self class] defaultRouteHandler]]; 438 | [[JLRoutes routesForScheme:@"priorityTest"] addRoute:@"/:foo/:baz" priority:1 handler:[[self class] defaultRouteHandler]]; 439 | 440 | [self route:@"priorityTest://stuff/things/foo"]; 441 | JLValidateAnyRouteMatched(); 442 | 443 | [self route:@"priorityTest://one/two"]; 444 | JLValidateAnyRouteMatched(); 445 | 446 | [self route:@"priorityTest://stuff/bar/baz"]; 447 | JLValidateAnyRouteMatched(); 448 | } 449 | 450 | - (void)testBlockReturnValue 451 | { 452 | [[JLRoutes globalRoutes] addRoute:@"/return/:value" handler:^BOOL(NSDictionary *parameters) { 453 | testsInstance.lastMatch = parameters; 454 | NSString *value = parameters[@"value"]; 455 | return [value isEqualToString:@"yes"]; 456 | }]; 457 | 458 | // even though this matches a route, the block returns NO here so there won't be a valid match 459 | [self route:@"tests://return/no"]; 460 | JLValidateNoLastMatch(); 461 | 462 | // this one is the same route but will return yes, causing it to be flagged as a match 463 | [self route:@"tests://return/yes"]; 464 | JLValidateAnyRouteMatched(); 465 | } 466 | 467 | - (void)testSchemes 468 | { 469 | id defaultHandler = [[self class] defaultRouteHandler]; 470 | 471 | [[JLRoutes globalRoutes] addRoute:@"/test" handler:defaultHandler]; 472 | [[JLRoutes routesForScheme:@"namespaceTest1"] addRoute:@"/test" handler:defaultHandler]; 473 | [[JLRoutes routesForScheme:@"namespaceTest2"] addRoute:@"/test" handler:defaultHandler]; 474 | 475 | // test that the same route can be handled differently for three different scheme namespaces 476 | [self route:@"tests://test"]; 477 | JLValidateAnyRouteMatched(); 478 | JLValidateScheme(JLRoutesGlobalRoutesScheme); 479 | 480 | [self route:@"namespaceTest1://test"]; 481 | JLValidateAnyRouteMatched(); 482 | JLValidateScheme(@"namespaceTest1"); 483 | 484 | [self route:@"namespaceTest2://test"]; 485 | JLValidateAnyRouteMatched(); 486 | JLValidateScheme(@"namespaceTest2"); 487 | } 488 | 489 | - (void)testFallbackToGlobal 490 | { 491 | id defaultHandler = [[self class] defaultRouteHandler]; 492 | 493 | [[JLRoutes globalRoutes] addRoute:@"/user/view/:userID" handler:defaultHandler]; 494 | [[JLRoutes routesForScheme:@"namespaceTest1"] addRoute:@"/test" handler:defaultHandler]; 495 | [[JLRoutes routesForScheme:@"namespaceTest2"] addRoute:@"/test" handler:defaultHandler]; 496 | [JLRoutes routesForScheme:@"namespaceTest2"].shouldFallbackToGlobalRoutes = YES; 497 | 498 | // first case, fallback is off and so this should fail because this route isnt declared as part of namespaceTest1 499 | [self route:@"namespaceTest1://user/view/joeldev"]; 500 | JLValidateNoLastMatch(); 501 | 502 | // fallback is on, so this should route 503 | [self route:@"namespaceTest2://user/view/joeldev"]; 504 | JLValidateAnyRouteMatched(); 505 | JLValidateScheme(JLRoutesGlobalRoutesScheme); 506 | JLValidateParameterCount(1); 507 | JLValidateParameter(@{@"userID" : @"joeldev"}); 508 | } 509 | 510 | - (void)testForRouteExistence 511 | { 512 | // This should return yes and no for whether we have a matching route. 513 | 514 | NSURL *shouldHaveRouteURL = [NSURL URLWithString:@"tests:/test"]; 515 | NSURL *shouldNotHaveRouteURL = [NSURL URLWithString:@"tests:/dfjkbsdkjfbskjdfb/sdasd"]; 516 | 517 | [[JLRoutes globalRoutes] addRoute:@"/test" handler:[[self class] defaultRouteHandler]]; 518 | 519 | XCTAssertTrue([[JLRoutes globalRoutes] canRouteURL:shouldHaveRouteURL], @"Should state it can route known URL"); 520 | XCTAssertFalse([[JLRoutes globalRoutes] canRouteURL:shouldNotHaveRouteURL], @"Should not state it can route unknown URL"); 521 | } 522 | 523 | - (void)testSubscripting 524 | { 525 | JLRoutes.globalRoutes[@"/subscripting"] = [[self class] defaultRouteHandler]; 526 | 527 | NSURL *shouldHaveRouteURL = [NSURL URLWithString:@"subscripting"]; 528 | 529 | XCTAssertTrue([[JLRoutes globalRoutes] canRouteURL:shouldHaveRouteURL], @"Should state it can route known URL"); 530 | } 531 | 532 | - (void)testNonSingletonUsage 533 | { 534 | JLRoutes *routes = [JLRoutes new]; 535 | NSURL *trivialURL = [NSURL URLWithString:@"/success"]; 536 | [routes addRoute:[trivialURL absoluteString] handler:nil]; 537 | XCTAssertTrue([routes routeURL:trivialURL], @"Non-singleton instance should route known URL"); 538 | } 539 | 540 | - (void)testRouteRemoval 541 | { 542 | id defaultHandler = [[self class] defaultRouteHandler]; 543 | 544 | [[JLRoutes globalRoutes] addRoute:@"/:" handler:defaultHandler]; 545 | [[JLRoutes routesForScheme:@"namespaceTest3"] addRoute:@"/test1" handler:defaultHandler]; 546 | [[JLRoutes routesForScheme:@"namespaceTest3"] addRoute:@"/test2" handler:defaultHandler]; 547 | 548 | [self route:@"namespaceTest3://test1"]; 549 | JLValidateAnyRouteMatched(); 550 | 551 | [[JLRoutes routesForScheme:@"namespaceTest3"] removeRouteWithPattern:@"/test1"]; 552 | [self route:@"namespaceTest3://test1"]; 553 | JLValidateNoLastMatch(); 554 | 555 | [self route:@"namespaceTest3://test2"]; 556 | JLValidateAnyRouteMatched(); 557 | JLValidateScheme(@"namespaceTest3"); 558 | 559 | [JLRoutes unregisterRouteScheme:@"namespaceTest3"]; 560 | 561 | // this will get matched by our "/:" route in the global namespace - we just want to make sure it doesn't get matched by namespaceTest3 562 | [self route:@"namespaceTest3://test2"]; 563 | JLValidateAnyRouteMatched(); 564 | JLValidateScheme(JLRoutesGlobalRoutesScheme); 565 | } 566 | 567 | - (void)testPercentEncoding 568 | { 569 | /* 570 | from http://en.wikipedia.org/wiki/Percent-encoding 571 | ! # $ & ' ( ) * + , / : ; = ? @ [ ] 572 | %21 %23 %24 %26 %27 %28 %29 %2A %2B %2C %2F %3A %3B %3D %3F %40 %5B %5D 573 | */ 574 | 575 | // NOTE: %2F is not supported. 576 | // [URL pathComponents] automatically expands values with %2F as if it was just a regular slash. 577 | 578 | [[JLRoutes globalRoutes] addRoute:@"/user/view/:userID" handler:[[self class] defaultRouteHandler]]; 579 | 580 | [JLRoutes setShouldDecodePlusSymbols:NO]; 581 | 582 | [self route:@"tests://user/view/joel%21levin"]; 583 | JLValidateAnyRouteMatched(); 584 | JLValidateParameterCount(1); 585 | JLValidateParameter(@{@"userID": @"joel!levin"}); 586 | 587 | [self route:@"tests://user/view/joel%23levin"]; 588 | JLValidateAnyRouteMatched(); 589 | JLValidateParameterCount(1); 590 | JLValidateParameter(@{@"userID": @"joel#levin"}); 591 | 592 | [self route:@"tests://user/view/joel%24levin"]; 593 | JLValidateAnyRouteMatched(); 594 | JLValidateParameterCount(1); 595 | JLValidateParameter(@{@"userID": @"joel$levin"}); 596 | 597 | [self route:@"tests://user/view/joel%26levin"]; 598 | JLValidateAnyRouteMatched(); 599 | JLValidateParameterCount(1); 600 | JLValidateParameter(@{@"userID": @"joel&levin"}); 601 | 602 | [self route:@"tests://user/view/joel%27levin"]; 603 | JLValidateAnyRouteMatched(); 604 | JLValidateParameterCount(1); 605 | JLValidateParameter(@{@"userID": @"joel'levin"}); 606 | 607 | [self route:@"tests://user/view/joel%28levin"]; 608 | JLValidateAnyRouteMatched(); 609 | JLValidateParameterCount(1); 610 | JLValidateParameter(@{@"userID": @"joel(levin"}); 611 | 612 | [self route:@"tests://user/view/joel%29levin"]; 613 | JLValidateAnyRouteMatched(); 614 | JLValidateParameterCount(1); 615 | JLValidateParameter(@{@"userID": @"joel)levin"}); 616 | 617 | [self route:@"tests://user/view/joel%2Alevin"]; 618 | JLValidateAnyRouteMatched(); 619 | JLValidateParameterCount(1); 620 | JLValidateParameter(@{@"userID": @"joel*levin"}); 621 | 622 | [self route:@"tests://user/view/joel%2Blevin"]; 623 | JLValidateAnyRouteMatched(); 624 | JLValidateParameterCount(1); 625 | JLValidateParameter(@{@"userID": @"joel+levin"}); 626 | 627 | [self route:@"tests://user/view/joel%2Clevin"]; 628 | JLValidateAnyRouteMatched(); 629 | JLValidateParameterCount(1); 630 | JLValidateParameter(@{@"userID": @"joel,levin"}); 631 | 632 | [self route:@"tests://user/view/joel%3Alevin"]; 633 | JLValidateAnyRouteMatched(); 634 | JLValidateParameterCount(1); 635 | JLValidateParameter(@{@"userID": @"joel:levin"}); 636 | 637 | [self route:@"tests://user/view/joel%3Blevin"]; 638 | JLValidateAnyRouteMatched(); 639 | JLValidateParameterCount(1); 640 | JLValidateParameter(@{@"userID": @"joel;levin"}); 641 | 642 | [self route:@"tests://user/view/joel%3Dlevin"]; 643 | JLValidateAnyRouteMatched(); 644 | JLValidateParameterCount(1); 645 | JLValidateParameter(@{@"userID": @"joel=levin"}); 646 | 647 | [self route:@"tests://user/view/joel%3Flevin"]; 648 | JLValidateAnyRouteMatched(); 649 | JLValidateParameterCount(1); 650 | JLValidateParameter(@{@"userID": @"joel?levin"}); 651 | 652 | [self route:@"tests://user/view/joel%40levin"]; 653 | JLValidateAnyRouteMatched(); 654 | JLValidateParameterCount(1); 655 | JLValidateParameter(@{@"userID": @"joel@levin"}); 656 | 657 | [self route:@"tests://user/view/joel%5Blevin"]; 658 | JLValidateAnyRouteMatched(); 659 | JLValidateParameterCount(1); 660 | JLValidateParameter(@{@"userID": @"joel[levin"}); 661 | 662 | [self route:@"tests://user/view/joel%5Dlevin"]; 663 | JLValidateAnyRouteMatched(); 664 | JLValidateParameterCount(1); 665 | JLValidateParameter(@{@"userID": @"joel]levin"}); 666 | } 667 | 668 | - (void)testDecodePlusSymbols 669 | { 670 | [[JLRoutes globalRoutes] addRoute:@"/user/view/:userID" handler:[[self class] defaultRouteHandler]]; 671 | 672 | [JLRoutes setShouldDecodePlusSymbols:YES]; 673 | 674 | [self route:@"tests://user/view/joel%2Blevin"]; 675 | JLValidateAnyRouteMatched(); 676 | JLValidateParameterCount(1); 677 | JLValidateParameter(@{@"userID": @"joel levin"}); 678 | 679 | [self route:@"tests://user/view/joel+levin"]; 680 | JLValidateAnyRouteMatched(); 681 | JLValidateParameterCount(1); 682 | JLValidateParameter(@{@"userID": @"joel levin"}); 683 | 684 | [self route:@"tests://user/view/test?name=joel+levin"]; 685 | JLValidateAnyRouteMatched(); 686 | JLValidateParameterCount(2); 687 | JLValidateParameter(@{@"name": @"joel levin"}); 688 | 689 | [self route:@"tests://user/view/test?people=joel+levin&people=foo+bar"]; 690 | JLValidateAnyRouteMatched(); 691 | JLValidateParameterCount(2); 692 | JLValidateParameter((@{@"people": @[@"joel levin", @"foo bar"]})); 693 | 694 | [JLRoutes setShouldDecodePlusSymbols:NO]; 695 | 696 | [self route:@"tests://user/view/joel%2Blevin"]; 697 | JLValidateAnyRouteMatched(); 698 | JLValidateParameterCount(1); 699 | JLValidateParameter(@{@"userID": @"joel+levin"}); 700 | 701 | [self route:@"tests://user/view/joel+levin"]; 702 | JLValidateAnyRouteMatched(); 703 | JLValidateParameterCount(1); 704 | JLValidateParameter(@{@"userID": @"joel+levin"}); 705 | 706 | [self route:@"tests://user/view/test?name=joel+levin"]; 707 | JLValidateAnyRouteMatched(); 708 | JLValidateParameterCount(2); 709 | JLValidateParameter(@{@"name": @"joel+levin"}); 710 | 711 | [self route:@"tests://user/view/test?people=joel+levin&people=foo+bar"]; 712 | JLValidateAnyRouteMatched(); 713 | JLValidateParameterCount(2); 714 | JLValidateParameter((@{@"people": @[@"joel+levin", @"foo+bar"]})); 715 | } 716 | 717 | - (void)testVariableEmptyFollowedByWildcard 718 | { 719 | [[JLRoutes routesForScheme:@"wildcardTests"] addRoute:@"list/:variable/detail/:variable2/*" handler:nil]; 720 | 721 | [self route:@"wildcardTests://list/variable/detail/"]; 722 | JLValidateNoLastMatch(); 723 | 724 | [self route:@"wildcardTests://list/variable/detail/variable2"]; 725 | JLValidateAnyRouteMatched(); 726 | } 727 | 728 | - (void)testOptionalRoutesAtEnd 729 | { 730 | [[JLRoutes globalRoutes] addRoute:@"/path/:thing(/new)(/anotherpath/:anotherthing)" handler:[[self class] defaultRouteHandler]]; 731 | 732 | XCTAssert([[JLRoutes globalRoutes] routes].count == 4); 733 | 734 | [self route:@"foo://path/abc/new/anotherpath/def"]; 735 | JLValidateAnyRouteMatched(); 736 | JLValidateParameter(@{@"thing": @"abc"}); 737 | JLValidateParameter(@{@"anotherthing": @"def"}); 738 | 739 | [self route:@"foo://path/foo/anotherpath/bar"]; 740 | JLValidateAnyRouteMatched(); 741 | JLValidateParameter(@{@"thing": @"foo"}); 742 | JLValidateParameter(@{@"anotherthing": @"bar"}); 743 | 744 | [self route:@"foo://path/yyy/new"]; 745 | JLValidateAnyRouteMatched(); 746 | JLValidateParameter(@{@"thing": @"yyy"}); 747 | 748 | [self route:@"foo://path/zzz"]; 749 | JLValidateAnyRouteMatched(); 750 | JLValidateParameter(@{@"thing": @"zzz"}); 751 | 752 | [self route:@"foo://path/zzz/anotherpath"]; 753 | JLValidateNoLastMatch(); 754 | } 755 | 756 | - (void)testOptionalRoutesAtStart 757 | { 758 | [[JLRoutes globalRoutes] addRoute:@"/(rest/)(app/):object/:id" handler:[[self class] defaultRouteHandler]]; 759 | 760 | XCTAssert([[JLRoutes globalRoutes] routes].count == 4); 761 | 762 | [self route:@"foo://rest/app/aaa/bbb"]; 763 | JLValidateAnyRouteMatched(); 764 | JLValidateParameter(@{@"object": @"aaa"}); 765 | JLValidateParameter(@{@"id": @"bbb"}); 766 | 767 | [self route:@"foo://app/aaa/bbb"]; 768 | JLValidateAnyRouteMatched(); 769 | JLValidateParameter(@{@"object": @"aaa"}); 770 | JLValidateParameter(@{@"id": @"bbb"}); 771 | 772 | [self route:@"foo://rest/aaa/bbb"]; 773 | JLValidateAnyRouteMatched(); 774 | JLValidateParameter(@{@"object": @"aaa"}); 775 | JLValidateParameter(@{@"id": @"bbb"}); 776 | 777 | [self route:@"foo://aaa/bbb"]; 778 | JLValidateAnyRouteMatched(); 779 | JLValidateParameter(@{@"object": @"aaa"}); 780 | JLValidateParameter(@{@"id": @"bbb"}); 781 | } 782 | 783 | - (void)testOptionalRoutesInterleaved 784 | { 785 | [[JLRoutes globalRoutes] addRoute:@"/(rest/):object/(app/):id" handler:[[self class] defaultRouteHandler]]; 786 | 787 | XCTAssert([[JLRoutes globalRoutes] routes].count == 4); 788 | 789 | [self route:@"foo://rest/aaa/app/bbb"]; 790 | JLValidateAnyRouteMatched(); 791 | JLValidateParameter(@{@"object": @"aaa"}); 792 | JLValidateParameter(@{@"id": @"bbb"}); 793 | 794 | [self route:@"foo://aaa/app/bbb"]; 795 | JLValidateAnyRouteMatched(); 796 | JLValidateParameter(@{@"object": @"aaa"}); 797 | JLValidateParameter(@{@"id": @"bbb"}); 798 | 799 | [self route:@"foo://rest/aaa/bbb"]; 800 | JLValidateAnyRouteMatched(); 801 | JLValidateParameter(@{@"object": @"aaa"}); 802 | JLValidateParameter(@{@"id": @"bbb"}); 803 | 804 | [self route:@"foo://aaa/bbb"]; 805 | JLValidateAnyRouteMatched(); 806 | JLValidateParameter(@{@"object": @"aaa"}); 807 | JLValidateParameter(@{@"id": @"bbb"}); 808 | } 809 | 810 | - (void)testPassingURLStringsAsParams 811 | { 812 | [[JLRoutes globalRoutes] addRoute:@"/web/:URLString" handler:[[self class] defaultRouteHandler]]; 813 | [[JLRoutes globalRoutes] addRoute:@"/web" handler:[[self class] defaultRouteHandler]]; 814 | 815 | [self route:@"tests://web/http%3A%2F%2Ffoobar.com%2Fbaz"]; 816 | JLValidateAnyRouteMatched(); 817 | JLValidateParameter(@{@"URLString": @"http://foobar.com/baz"}); 818 | 819 | [self route:@"tests://web?URLString=http%3A%2F%2Ffoobar.com%2Fbaz"]; 820 | JLValidateAnyRouteMatched(); 821 | JLValidateParameter(@{@"URLString": @"http://foobar.com/baz"}); 822 | } 823 | 824 | - (void)testArrayQueryParams 825 | { 826 | [[JLRoutes globalRoutes] addRoute:@"/test/foo" handler:[[self class] defaultRouteHandler]]; 827 | 828 | [self route:@"tests://test/foo?key=1&key=2&key=3&text=hi&text=there"]; 829 | JLValidateAnyRouteMatched(); 830 | JLValidateParameter((@{@"key": @[@"1", @"2", @"3"]})); 831 | JLValidateParameter((@{@"text": @[@"hi", @"there"]})); 832 | } 833 | 834 | - (void)testAddingCustomRouteDefinition 835 | { 836 | id defaultHandler = [[self class] defaultRouteHandler]; 837 | 838 | JLRMockRouteDefinition *customRoute = [[JLRMockRouteDefinition alloc] initWithPattern:@"/test" priority:0 handlerBlock:defaultHandler]; 839 | [[JLRoutes globalRoutes] addRoute:customRoute]; 840 | 841 | [self route:@"tests://test"]; 842 | 843 | JLValidateAnyRouteMatched(); 844 | JLValidateScheme(JLRoutesGlobalRoutesScheme); 845 | 846 | [[JLRoutes globalRoutes] removeRoute:customRoute]; 847 | 848 | [self route:@"tests://test"]; 849 | 850 | JLValidateNoLastMatch(); 851 | } 852 | 853 | - (void)testChangeDefaultRouteDefinitionClass 854 | { 855 | [JLRoutes setDefaultRouteDefinitionClass:[JLRMockRouteDefinition class]]; 856 | 857 | id defaultHandler = [[self class] defaultRouteHandler]; 858 | [[JLRoutes globalRoutes] addRoute:@"/foo" handler:defaultHandler]; 859 | 860 | JLRRouteDefinition *route = [JLRoutes globalRoutes].routes.firstObject; 861 | XCTAssert([route isKindOfClass:[JLRMockRouteDefinition class]]); 862 | } 863 | 864 | - (void)testTreatsHostAsPathComponent 865 | { 866 | id defaultHandler = [[self class] defaultRouteHandler]; 867 | 868 | [[JLRoutes globalRoutes] addRoute:@"/sign_in" handler:defaultHandler]; 869 | [[JLRoutes globalRoutes] addRoute:@"/path/:pathid" handler:defaultHandler]; 870 | 871 | [JLRoutes setAlwaysTreatsHostAsPathComponent:NO]; 872 | 873 | [self route:@"https://www.mydomain.com/sign_in"]; 874 | JLValidateAnyRouteMatched(); 875 | JLValidatePattern(@"/sign_in"); 876 | 877 | [self route:@"https://www.mydomain.com/path/3"]; 878 | JLValidateAnyRouteMatched(); 879 | JLValidatePattern(@"/path/:pathid"); 880 | JLValidateParameter((@{@"pathid": @"3"})); 881 | 882 | [JLRoutes setAlwaysTreatsHostAsPathComponent:YES]; 883 | 884 | [self route:@"https://www.mydomain2.com/sign_in"]; 885 | JLValidateNoLastMatch(); 886 | 887 | [self route:@"https://www.mydomain2.com/path/3"]; 888 | JLValidateNoLastMatch(); 889 | 890 | [[JLRoutes globalRoutes] addRoute:@"/www.mydomain2.com/sign_in" handler:defaultHandler]; 891 | [[JLRoutes globalRoutes] addRoute:@"/www.mydomain2.com/path/:pathid" handler:defaultHandler]; 892 | 893 | [self route:@"https://www.mydomain2.com/sign_in"]; 894 | JLValidateAnyRouteMatched(); 895 | JLValidatePattern(@"/www.mydomain2.com/sign_in"); 896 | 897 | [self route:@"https://www.mydomain2.com/path/3"]; 898 | JLValidateAnyRouteMatched(); 899 | JLValidatePattern(@"/www.mydomain2.com/path/:pathid"); 900 | JLValidateParameter((@{@"pathid": @"3"})); 901 | } 902 | 903 | - (void)testRouteDefinitionEquality 904 | { 905 | id defaultHandler = [[self class] defaultRouteHandler]; 906 | 907 | JLRRouteDefinition *routeA = [[JLRRouteDefinition alloc] initWithPattern:@"/foo" priority:0 handlerBlock:defaultHandler]; 908 | JLRRouteDefinition *routeB = [routeA copy]; 909 | 910 | XCTAssertTrue([routeA isEqual:routeB]); 911 | 912 | [routeB didBecomeRegisteredForScheme:@"scheme"]; 913 | 914 | XCTAssertFalse([routeA isEqual:routeB]); 915 | 916 | JLRRouteDefinition *routeC = [[JLRRouteDefinition alloc] initWithPattern:@"/foo/bar" priority:0 handlerBlock:defaultHandler]; 917 | 918 | XCTAssertFalse([routeA isEqual:routeC]); 919 | } 920 | 921 | - (void)testHandlerBlockForWeakTarget 922 | { 923 | JLRMockTargetObject *object = [[JLRMockTargetObject alloc] init]; 924 | id handlerBlock = [JLRRouteHandler handlerBlockForWeakTarget:object]; 925 | 926 | [[JLRoutes globalRoutes] addRoute:@"/test" handler:handlerBlock]; 927 | 928 | [self route:@"/test"]; 929 | JLValidateAnyRouteMatched(); 930 | XCTAssertEqualObjects(testsInstance.lastMatch, object.routeParams); 931 | 932 | object = nil; // ensure that the object is not retained by handler block 933 | 934 | [self route:@"/test"]; 935 | JLValidateNoLastMatch(); 936 | } 937 | 938 | - (void)testHandlerBlockForTargetClass 939 | { 940 | __block JLRMockTargetObject *createdObject = nil; 941 | id handlerBlock = [JLRRouteHandler handlerBlockForTargetClass:[JLRMockTargetObject class] completion:^BOOL (id object) { 942 | createdObject = (JLRMockTargetObject *)object; 943 | return YES; 944 | }]; 945 | 946 | [[JLRoutes globalRoutes] addRoute:@"/test" handler:handlerBlock]; 947 | 948 | [self route:@"/test"]; 949 | JLValidateAnyRouteMatched(); 950 | XCTAssertEqualObjects(testsInstance.lastMatch, createdObject.routeParams); 951 | 952 | XCTAssertNotNil(createdObject); 953 | } 954 | 955 | #pragma mark - Convenience Methods 956 | 957 | + (BOOL (^)(NSDictionary *))defaultRouteHandler 958 | { 959 | return ^BOOL (NSDictionary *params) { 960 | testsInstance.lastMatch = params; 961 | return YES; 962 | }; 963 | } 964 | 965 | - (void)route:(NSString *)URLString 966 | { 967 | [self route:URLString withParameters:nil]; 968 | } 969 | 970 | - (void)route:(NSString *)URLString withParameters:(NSDictionary *)parameters 971 | { 972 | [self routeURL:[NSURL URLWithString:URLString] withParameters:parameters]; 973 | } 974 | 975 | - (void)routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters 976 | { 977 | NSLog(@"*** Routing %@", URL); 978 | self.lastMatch = nil; 979 | self.didRoute = [JLRoutes routeURL:URL withParameters:parameters]; 980 | } 981 | 982 | @end 983 | 984 | 985 | @implementation JLRMockTargetObject 986 | 987 | - (instancetype)initWithRouteParameters:(NSDictionary *)parameters 988 | { 989 | self = [super init]; 990 | self.routeParams = parameters; 991 | testsInstance.lastMatch = parameters; 992 | return self; 993 | } 994 | 995 | - (BOOL)handleRouteWithParameters:(NSDictionary *)parameters 996 | { 997 | self.routeParams = parameters; 998 | testsInstance.lastMatch = parameters; 999 | return YES; 1000 | } 1001 | 1002 | @end 1003 | 1004 | 1005 | @implementation JLRMockRouteDefinition 1006 | 1007 | @end 1008 | 1009 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Joel Levin. All rights reserved. 2 | 3 | - Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 5 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 6 | 7 | Neither the name of JLRoutes nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.4 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "JLRoutes", 7 | platforms: [ 8 | .macOS(.v10_10), 9 | .iOS(.v9), 10 | .tvOS(.v9), 11 | ], 12 | products: [ 13 | .library( 14 | name: "JLRoutes", 15 | targets: ["JLRoutes"] 16 | ), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "JLRoutes", 21 | path: "JLRoutes", 22 | publicHeadersPath: "." 23 | ), 24 | ], 25 | swiftLanguageVersions: [.v5] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JLRoutes 2 | ======== 3 | 4 | [![Platforms](https://img.shields.io/cocoapods/p/JLRoutes.svg?style=flat)](http://cocoapods.org/pods/JLRoutes) 5 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/JLRoutes.svg)](http://cocoapods.org/pods/JLRoutes) 6 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | [![Build Status](https://travis-ci.org/joeldev/JLRoutes.svg?branch=master)](https://travis-ci.org/joeldev/JLRoutes) 8 | [![Apps](https://img.shields.io/cocoapods/at/JLRoutes.svg?maxAge=2592000)](https://cocoapods.org/pods/JLRoutes) 9 | 10 | ### What is it? ### 11 | JLRoutes is a URL routing library with a simple block-based API. It is designed to make it very easy to handle complex URL schemes in your application with minimal code. 12 | 13 | ### Installation ### 14 | JLRoutes is available for installation using [CocoaPods](https://cocoapods.org/pods/JLRoutes) or Carthage (add `github "joeldev/JLRoutes"` to your `Cartfile`). 15 | 16 | ### Requirements ### 17 | JLRoutes 2.x require iOS 8.0+ or macOS 10.10+. If you need to support iOS 7 or macOS 10.9, please use version 1.6.4 (which is the last 1.x release). 18 | 19 | ### Documentation ### 20 | Documentation is available [here](http://cocoadocs.org/docsets/JLRoutes/). 21 | 22 | ### Getting Started ### 23 | 24 | [Configure your URL schemes in Info.plist.](https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Inter-AppCommunication/Inter-AppCommunication.html#//apple_ref/doc/uid/TP40007072-CH6-SW2) 25 | 26 | ```objc 27 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 28 | { 29 | JLRoutes *routes = [JLRoutes globalRoutes]; 30 | 31 | [routes addRoute:@"/user/view/:userID" handler:^BOOL(NSDictionary *parameters) { 32 | NSString *userID = parameters[@"userID"]; // defined in the route by specifying ":userID" 33 | 34 | // present UI for viewing user with ID 'userID' 35 | 36 | return YES; // return YES to say we have handled the route 37 | }]; 38 | 39 | return YES; 40 | } 41 | 42 | - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options 43 | { 44 | return [JLRoutes routeURL:url]; 45 | } 46 | ``` 47 | 48 | Routes can also be registered with subscripting syntax: 49 | ```objc 50 | JLRoutes.globalRoutes[@"/user/view/:userID"] = ^BOOL(NSDictionary *parameters) { 51 | // ... 52 | }; 53 | ``` 54 | 55 | After adding a route for `/user/view/:userID`, the following call will cause the handler block to be called with a dictionary containing `@"userID": @"joeldev"`: 56 | ```objc 57 | NSURL *viewUserURL = [NSURL URLWithString:@"myapp://user/view/joeldev"]; 58 | [JLRoutes routeURL:viewUserURL]; 59 | ``` 60 | 61 | ### The Parameters Dictionary ### 62 | 63 | The parameters dictionary always contains at least the following three keys: 64 | ```json 65 | { 66 | "JLRouteURL": "(the NSURL that caused this block to be fired)", 67 | "JLRoutePattern": "(the actual route pattern string)", 68 | "JLRouteScheme": "(the route scheme, defaults to JLRoutesGlobalRoutesScheme)" 69 | } 70 | ``` 71 | 72 | The JLRouteScheme key refers to the scheme that the matched route lives in. [Read more about schemes.](https://github.com/joeldev/JLRoutes#scheme-namespaces) 73 | 74 | See JLRoutes.h for the list of constants. 75 | 76 | ### Handler Block Chaining ### 77 | 78 | The handler block is expected to return a boolean for if it has handled the route or not. If the block returns `NO`, JLRoutes will behave as if that route is not a match and it will continue looking for a match. A route is considered to be a match if the pattern string matches **and** the block returns `YES`. 79 | 80 | It is also important to note that if you pass nil for the handler block, an internal handler block will be created that simply returns `YES`. 81 | 82 | ### Global Configuration ### 83 | 84 | There are multiple global configuration options available to help customize JLRoutes behavior for a particular use-case. All options only take affect for the next operation. 85 | 86 | ```objc 87 | /// Configures verbose logging. Defaults to NO. 88 | + (void)setVerboseLoggingEnabled:(BOOL)loggingEnabled; 89 | 90 | /// Configures if '+' should be replaced with spaces in parsed values. Defaults to YES. 91 | + (void)setShouldDecodePlusSymbols:(BOOL)shouldDecode; 92 | 93 | /// Configures if URL host is always considered to be a path component. Defaults to NO. 94 | + (void)setAlwaysTreatsHostAsPathComponent:(BOOL)treatsHostAsPathComponent; 95 | 96 | /// Configures the default class to use when creating route definitions. Defaults to JLRRouteDefinition. 97 | + (void)setDefaultRouteDefinitionClass:(Class)routeDefinitionClass; 98 | ``` 99 | 100 | These are all configured at the `JLRoutes` class level: 101 | ```objc 102 | [JLRoutes setAlwaysTreatsHostAsPathComponent:YES]; 103 | ``` 104 | 105 | ### More Complex Example ### 106 | 107 | ```objc 108 | [[JLRoutes globalRoutes] addRoute:@"/:object/:action/:primaryKey" handler:^BOOL(NSDictionary *parameters) { 109 | NSString *object = parameters[@"object"]; 110 | NSString *action = parameters[@"action"]; 111 | NSString *primaryKey = parameters[@"primaryKey"]; 112 | // stuff 113 | return YES; 114 | }]; 115 | ``` 116 | 117 | This route would match things like `/user/view/joeldev` or `/post/edit/123`. Let's say you called `/post/edit/123` with some URL params as well: 118 | 119 | ```objc 120 | NSURL *editPost = [NSURL URLWithString:@"myapp://post/edit/123?debug=true&foo=bar"]; 121 | [JLRoutes routeURL:editPost]; 122 | ``` 123 | 124 | The parameters dictionary that the handler block receives would contain the following key/value pairs: 125 | ```json 126 | { 127 | "object": "post", 128 | "action": "edit", 129 | "primaryKey": "123", 130 | "debug": "true", 131 | "foo": "bar", 132 | "JLRouteURL": "myapp://post/edit/123?debug=true&foo=bar", 133 | "JLRoutePattern": "/:object/:action/:primaryKey", 134 | "JLRouteScheme": "JLRoutesGlobalRoutesScheme" 135 | } 136 | ``` 137 | 138 | ### Schemes ### 139 | 140 | JLRoutes supports setting up routes within a specific URL scheme. Routes that are set up within a scheme can only be matched by URLs that use a matching URL scheme. By default, all routes go into the global scheme. 141 | 142 | ```objc 143 | [[JLRoutes globalRoutes] addRoute:@"/foo" handler:^BOOL(NSDictionary *parameters) { 144 | // This block is called if the scheme is not 'thing' or 'stuff' (see below) 145 | return YES; 146 | }]; 147 | 148 | [[JLRoutes routesForScheme:@"thing"] addRoute:@"/foo" handler:^BOOL(NSDictionary *parameters) { 149 | // This block is called for thing://foo 150 | return YES; 151 | }]; 152 | 153 | [[JLRoutes routesForScheme:@"stuff"] addRoute:@"/foo" handler:^BOOL(NSDictionary *parameters) { 154 | // This block is called for stuff://foo 155 | return YES; 156 | }]; 157 | ``` 158 | 159 | This example shows that you can declare the same routes in different schemes and handle them with different callbacks on a per-scheme basis. 160 | 161 | Continuing with this example, if you were to add the following route: 162 | 163 | ```objc 164 | [[JLRoutes globalRoutes] addRoute:@"/global" handler:^BOOL(NSDictionary *parameters) { 165 | return YES; 166 | }]; 167 | ``` 168 | 169 | and then try to route the URL `thing://global`, it would not match because that route has not been declared within the `thing` scheme but has instead been declared within the global scheme (which we'll assume is how the developer wants it). However, you can easily change this behavior by setting the following property to `YES`: 170 | 171 | ```objc 172 | [JLRoutes routesForScheme:@"thing"].shouldFallbackToGlobalRoutes = YES; 173 | ``` 174 | 175 | This tells JLRoutes that if a URL cannot be routed within the `thing` scheme (aka, it starts with `thing:` but no appropriate route can be found), try to recover by looking for a matching route in the global routes scheme as well. After setting that property to `YES`, the URL `thing://global` would be routed to the `/global` handler block. 176 | 177 | 178 | ### Wildcards ### 179 | 180 | JLRoutes supports setting up routes that will match an arbitrary number of path components at the end of the routed URL. An array containing the additional path components will be added to the parameters dictionary with the key `JLRouteWildcardComponentsKey`. 181 | 182 | For example, the following route would be triggered for any URL that started with `/wildcard/`, but would be rejected by the handler if the next component wasn't `joker`. 183 | 184 | ```objc 185 | [[JLRoutes globalRoutes] addRoute:@"/wildcard/*" handler:^BOOL(NSDictionary *parameters) { 186 | NSArray *pathComponents = parameters[JLRouteWildcardComponentsKey]; 187 | if (pathComponents.count > 0 && [pathComponents[0] isEqualToString:@"joker"]) { 188 | // the route matched; do stuff 189 | return YES; 190 | } 191 | 192 | // not interested unless 'joker' is in it 193 | return NO; 194 | }]; 195 | ``` 196 | 197 | ### Optional Routes ### 198 | 199 | JLRoutes supports setting up routes with optional parameters. At the route registration moment, JLRoute will register multiple routes with all combinations of the route with the optional parameters and without the optional parameters. For example, for the route `/the(/foo/:a)(/bar/:b)`, it will register the following routes: 200 | 201 | - `/the/foo/:a/bar/:b` 202 | - `/the/foo/:a` 203 | - `/the/bar/:b` 204 | - `/the` 205 | 206 | ### Querying Routes ### 207 | 208 | There are multiple ways to query routes for programmatic uses (such as powering a debug UI). There's a method to get the full set of routes across all schemes and another to get just the specific list of routes for a given scheme. One note, you'll have to import `JLRRouteDefinition.h` as it is forward-declared. 209 | 210 | ```objc 211 | /// All registered routes, keyed by scheme 212 | + (NSDictionary *> *)allRoutes; 213 | 214 | /// Return all registered routes in the receiving scheme namespace. 215 | - (NSArray *)routes; 216 | ``` 217 | 218 | ### Handler Block Helper ### 219 | 220 | `JLRRouteHandler` is a helper class for creating handler blocks intended to be passed to an addRoute: call. 221 | 222 | This is specifically useful for cases in which you want a separate object or class to be the handler for a deeplink route. An example might be a view controller that you want to instantiate and present in response to a deeplink being opened. 223 | 224 | In order to take advantage of this helper, your target class must conform to the `JLRRouteHandlerTarget` protocol. For example: 225 | 226 | ```objc 227 | @interface MyTargetViewController : UIViewController 228 | 229 | @property (nonatomic, copy) NSDictionary *parameters; 230 | 231 | @end 232 | 233 | 234 | @implementation MyTargetViewController 235 | 236 | - (instancetype)initWithRouteParameters:(NSDictionary *)parameters 237 | { 238 | self = [super init]; 239 | 240 | _parameters = [parameters copy]; // hold on to do something with later on 241 | 242 | return self; 243 | } 244 | 245 | - (void)viewDidLoad 246 | { 247 | [super viewDidLoad]; 248 | // do something interesting with self.parameters, initialize views, etc... 249 | } 250 | 251 | @end 252 | ``` 253 | 254 | To hook this up via `JLRRouteHandler`, you could do something like this: 255 | 256 | ```objc 257 | id handlerBlock = [JLRRouteHandler handlerBlockForTargetClass:[MyTargetViewController class] completion:^BOOL (MyTargetViewController *viewController) { 258 | // Push the created view controller onto the nav controller 259 | [self.navigationController pushViewController:viewController animated:YES]; 260 | return YES; 261 | }]; 262 | 263 | [[JLRoutes globalRoutes] addRoute:@"/some/route" handler:handlerBlock]; 264 | ``` 265 | 266 | There's also a `JLRRouteHandler` convenience method for easily routing to an existing instance of an object vs creating a new instance. For example: 267 | 268 | ```objc 269 | MyTargetViewController *rootController = ...; // some object that exists and conforms to JLRRouteHandlerTarget. 270 | id handlerBlock = [JLRRouteHandler handlerBlockForWeakTarget:rootController]; 271 | 272 | [[JLRoutes globalRoutes] addRoute:@"/some/route" handler:handlerBlock]; 273 | ``` 274 | 275 | When the route is matched, it will call a method on the target object: 276 | 277 | ```objc 278 | - (BOOL)handleRouteWithParameters:(NSDictionary *)parameters; 279 | ``` 280 | 281 | These two mechanisms (weak target and class target) provide a few other ways to organize deep link handlers without writing boilerplate code for each handler or otherwise having to solve that for each app that integrates JLRoutes. 282 | 283 | ### Custom Route Parsing ### 284 | 285 | It is possible to control how routes are parsed by subclassing `JLRRouteDefinition` and using the `addRoute:` method to add instances of your custom subclass. 286 | 287 | ```objc 288 | // Custom route defintion that always matches 289 | @interface AlwaysMatchRouteDefinition : JLRRouteDefinition 290 | @end 291 | 292 | 293 | @implementation AlwaysMatchRouteDefinition 294 | 295 | - (JLRRouteResponse *)routeResponseForRequest:(JLRRouteRequest *)request 296 | { 297 | // This method is called when JLRoutes is trying to determine if we are a match for the given request object. 298 | 299 | // Create the parameters dictionary 300 | NSDictionary *variables = [self routeVariablesForRequest:request]; 301 | NSDictionary *matchParams = [self matchParametersForRequest:request routeVariables:variables]; 302 | 303 | // Return a valid match! 304 | return [JLRRouteResponse validMatchResponseWithParameters:matchParams]; 305 | } 306 | 307 | @end 308 | ``` 309 | 310 | This route can now be created an added: 311 | ```objc 312 | id handlerBlock = ... // assume exists 313 | AlwaysMatchRouteDefinition *alwaysMatch = [[AlwaysMatchRouteDefinition alloc] initWithPattern:@"/foo" priority:0 handlerBlock:handlerBlock]; 314 | [[JLRoutes globalRoutes] addRoute:alwaysMatch]; 315 | ``` 316 | 317 | Alternatively, if you've written a custom route definition and want JLRoutes to always use it when adding a route (using one of the `addRoute:` methods that takes in raw parameters), use `+setDefaultRouteDefinitionClass:` to configure it as the routing definition class: 318 | ```objc 319 | [JLRoutes setDefaultRouteDefinitionClass:[MyCustomRouteDefinition class]]; 320 | ``` 321 | 322 | ### License ### 323 | BSD 3-clause. See the [LICENSE](LICENSE) file for details. 324 | 325 | --------------------------------------------------------------------------------