Skip to content

Commit

Permalink
add patter rules
Browse files Browse the repository at this point in the history
  • Loading branch information
wsof401 committed Nov 26, 2018
1 parent 18af2dc commit 6fff25d
Show file tree
Hide file tree
Showing 12 changed files with 359 additions and 170 deletions.
21 changes: 14 additions & 7 deletions Demo.playground/Contents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,26 @@ let router = Router<Any>(defaultUnmatchHandler: defaultUnmatchHandler)
//: Register patterns, the closure is the handle when matched the pattern.

// Set a route pattern, the closure is a handler that would be performed after match the pattern
router.register(pattern: pattern1) { result in
// Now, registered pattern has been matched
// Do anything you want, e.g: show a UI
print(result)
print("\n")
do {
try router.register(pattern: pattern1) { result in
// Now, registered pattern has been matched
// Do anything you want, e.g: show a UI
print(result)
print("\n")
}
} catch let error {
print("register failed, reason:\n\(error.localizedDescription)\n")
}

router.register(pattern: pattern2) { _ in
do {
try router.register(pattern: pattern2) { _ in
// Now, registered pattern has been matched
// Do anything you want, e.g: show a UI
print("call new article")
}

} catch let error {
print("register failed, reason:\n\(error.localizedDescription)\n")
}
//: Let match some URI Path.

// A case that should be matched
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ See [Demo.playground](Demo.playground/Contents.swift) for now.

Routing rules follows Rails flavor, see [Rails routing guide](http://guides.rubyonrails.org/routing.html#non-resourceful-routes) for now.

## Features
- [ ] globbing support
- [ ] format validation

## Requirements

- iOS 9.0+ / Mac OS X 10.10+ / tvOS 9.0+
Expand Down
4 changes: 4 additions & 0 deletions RouterX.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
/* End PBXAggregateTarget section */

/* Begin PBXBuildFile section */
48065D3321AAB88400F33408 /* PatternScanError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48065D3221AAB88400F33408 /* PatternScanError.swift */; };
48E62CA721A695000005838B /* MatchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48E62CA621A695000005838B /* MatchResult.swift */; };
OBJ_35 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Router.swift */; };
OBJ_36 /* RouterXCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* RouterXCore.swift */; };
Expand Down Expand Up @@ -58,6 +59,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
48065D3221AAB88400F33408 /* PatternScanError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatternScanError.swift; sourceTree = "<group>"; };
48E62CA621A695000005838B /* MatchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchResult.swift; sourceTree = "<group>"; };
OBJ_10 /* RouterXCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterXCore.swift; sourceTree = "<group>"; };
OBJ_11 /* RoutingGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingGraph.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -163,6 +165,7 @@
OBJ_11 /* RoutingGraph.swift */,
OBJ_12 /* RoutingPatternParser.swift */,
OBJ_13 /* RoutingPatternScanner.swift */,
48065D3221AAB88400F33408 /* PatternScanError.swift */,
OBJ_14 /* RoutingPatternToken.swift */,
OBJ_15 /* URLPathScanner.swift */,
OBJ_16 /* URLPathToken.swift */,
Expand Down Expand Up @@ -260,6 +263,7 @@
OBJ_37 /* RoutingGraph.swift in Sources */,
OBJ_38 /* RoutingPatternParser.swift in Sources */,
OBJ_39 /* RoutingPatternScanner.swift in Sources */,
48065D3321AAB88400F33408 /* PatternScanError.swift in Sources */,
OBJ_40 /* RoutingPatternToken.swift in Sources */,
OBJ_41 /* URLPathScanner.swift in Sources */,
OBJ_42 /* URLPathToken.swift in Sources */,
Expand Down
10 changes: 10 additions & 0 deletions Sources/RouterX/PatternScanError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

public enum PatternRegisterError: LocalizedError {
case empty
case missingPrefixSlash
case invalidGlobbing(globbing: String, after: String)
case invalidSymbol(symbol: String, after: String)
case unbalanceParenthesis
case unexpectToken(after: String)
}
12 changes: 4 additions & 8 deletions Sources/RouterX/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,14 @@ open class Router<Context> {
self.defaultUnmatchHandler = defaultUnmatchHandler
}

open func register(pattern: String, handler: @escaping MatchedHandler) -> Bool {
if self.core.registerRoutingPattern(pattern) {
self.handlerMappings[pattern] = handler
return true
} else {
return false
}
open func register(pattern: String, handler: @escaping MatchedHandler) throws {
try core.register(pattern: pattern)
handlerMappings[pattern] = handler
}

@discardableResult
open func match(_ url: URL, context: Context? = nil, unmatchHandler: UnmatchHandler? = nil) -> Bool {
guard let matchedRoute = core.matchURL(url),
guard let matchedRoute = core.match(url),
let matchHandler = handlerMappings[matchedRoute.patternIdentifier] else {
let expectUnmatchHandler = unmatchHandler ?? defaultUnmatchHandler
expectUnmatchHandler?(url, context)
Expand Down
43 changes: 33 additions & 10 deletions Sources/RouterX/RouterXCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,41 @@ public class RouterXCore {
self.rootRoute = RouteVertex()
}

public func registerRoutingPattern(_ pattern: String) -> Bool {
public func register(pattern: String) throws {
let tokens = RoutingPatternScanner.tokenize(pattern)

do {
try RoutingPatternParser.parseAndAppendTo(self.rootRoute, routingPatternTokens: tokens, patternIdentifier: pattern)
return true
} catch {
return false
guard let prefixToken = tokens.first else { throw PatternRegisterError.empty }
guard prefixToken == .slash else { throw PatternRegisterError.missingPrefixSlash }

var previousToken: RoutingPatternToken?
var stackTokensDescription = ""
var parenthesisOffset = 0
for token in tokens {
switch token {
case .star(let globbing):
if previousToken != .slash { throw PatternRegisterError.invalidGlobbing(globbing: globbing, after: stackTokensDescription) }
case .symbol(let symbol):
if previousToken != .slash && previousToken != .dot { throw PatternRegisterError.invalidSymbol(symbol: symbol, after: stackTokensDescription) }
case .lParen:
parenthesisOffset += 1
case .rParen:
if parenthesisOffset <= 0 {
throw PatternRegisterError.unexpectToken(after: stackTokensDescription)
}
parenthesisOffset -= 1
default: break
}
stackTokensDescription.append(token.description)
previousToken = token
}

guard parenthesisOffset == 0 else {
throw PatternRegisterError.unbalanceParenthesis
}
try RoutingPatternParser.parseAndAppendTo(self.rootRoute, routingPatternTokens: tokens, patternIdentifier: pattern)
}

public func matchURL(_ url: URL) -> MatchedRoute? {
public func match(_ url: URL) -> MatchedRoute? {
let path = url.path

let tokens = URLPathScanner.tokenize(path)
Expand Down Expand Up @@ -58,9 +81,9 @@ public class RouterXCore {
return MatchedRoute(url: url, parameters: parameters, patternIdentifier: pathPatternIdentifier)
}

public func matchURLPath(_ urlPath: String) -> MatchedRoute? {
guard let url = URL(string: urlPath) else { return nil }
return matchURL(url)
public func match(_ path: String) -> MatchedRoute? {
guard let url = URL(string: path) else { return nil }
return match(url)
}
}

Expand Down
75 changes: 37 additions & 38 deletions Sources/RouterX/RoutingPatternParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ internal class RoutingPatternParser {
self.patternIdentifier = patternIdentifier
}

class func parseAndAppendTo(_ rootRoute: RouteVertex, routingPatternTokens: [RoutingPatternToken], patternIdentifier: PatternIdentifier) throws {
let parser = RoutingPatternParser(routingPatternTokens: routingPatternTokens, patternIdentifier: patternIdentifier)
try parser.parseAndAppendTo(rootRoute)
}

func parseAndAppendTo(_ rootRoute: RouteVertex) throws {
var tokenGenerator = self.routingPatternTokens.makeIterator()
if let token = tokenGenerator.next() {
Expand All @@ -30,9 +35,38 @@ internal class RoutingPatternParser {
}
}

class func parseAndAppendTo(_ rootRoute: RouteVertex, routingPatternTokens: [RoutingPatternToken], patternIdentifier: PatternIdentifier) throws {
let parser = RoutingPatternParser(routingPatternTokens: routingPatternTokens, patternIdentifier: patternIdentifier)
try parser.parseAndAppendTo(rootRoute)
func parseSlash(_ context: RouteVertex, generator: RoutingPatternTokenGenerator) throws {
var generator = generator

guard let nextToken = generator.next() else {
if let terminalRoute = context.namedRoutes[.slash] {
assignPatternIdentifierIfNil(terminalRoute)
} else {
context.namedRoutes[.slash] = RouteVertex(patternIdentifier: self.patternIdentifier)
}
return
}

var nextRoute: RouteVertex!
if let route = context.namedRoutes[.slash] {
nextRoute = route
} else {
nextRoute = RouteVertex()
context.namedRoutes[.slash] = nextRoute
}

switch nextToken {
case let .literal(value):
try parseLiteral(nextRoute, value: value, generator: generator)
case let .symbol(value):
try parseSymbol(nextRoute, value: value, generator: generator)
case let .star(value):
try parseStar(nextRoute, value: value, generator: generator)
case .lParen:
try parseLParen(nextRoute, generator: generator)
default:
throw RoutingPatternParserError.unexpectToken(got: nextToken, message: "Unexpect \(nextToken)")
}
}

func parseLParen(_ context: RouteVertex, isFirstEnter: Bool = true, generator: RoutingPatternTokenGenerator) throws {
Expand Down Expand Up @@ -85,41 +119,6 @@ internal class RoutingPatternParser {
}
}

func parseSlash(_ context: RouteVertex, generator: RoutingPatternTokenGenerator) throws {
var generator = generator

guard let nextToken = generator.next() else {
if let terminalRoute = context.namedRoutes[.slash] {
assignPatternIdentifierIfNil(terminalRoute)
} else {
context.namedRoutes[.slash] = RouteVertex(patternIdentifier: self.patternIdentifier)
}

return
}

var nextRoute: RouteVertex!
if let route = context.namedRoutes[.slash] {
nextRoute = route
} else {
nextRoute = RouteVertex()
context.namedRoutes[.slash] = nextRoute
}

switch nextToken {
case let .literal(value):
try parseLiteral(nextRoute, value: value, generator: generator)
case let .symbol(value):
try parseSymbol(nextRoute, value: value, generator: generator)
case let .star(value):
try parseStar(nextRoute, value: value, generator: generator)
case .lParen:
try parseLParen(nextRoute, generator: generator)
default:
throw RoutingPatternParserError.unexpectToken(got: nextToken, message: "Unexpect \(nextToken)")
}
}

private func parseDot(_ context: RouteVertex, generator: RoutingPatternTokenGenerator) throws {
var generator = generator

Expand Down
106 changes: 49 additions & 57 deletions Sources/RouterX/RoutingPatternScanner.swift
Original file line number Diff line number Diff line change
@@ -1,76 +1,68 @@
import Foundation

internal struct RoutingPatternScanner {
private static let stopWordsSet: Set<Character> = ["(", ")", "/"]

let expression: String

private(set) var position: String.Index

private var unScannedFragment: String {
return String(expression[position..<expression.endIndex])
}

var isEOF: Bool {
return self.position == self.expression.endIndex
}

init(expression: String) {
self.expression = expression
self.position = self.expression.startIndex
private enum _PatternScanTerminator: Character {
case lParen = "("
case rParen = ")"
case slash = "/"
case dot = "."
case star = "*"

var jointFragment: (token: RoutingPatternToken?, fragment: String) {
switch self {
case .lParen:
return (token: .lParen, fragment: "")
case .rParen:
return (token: .rParen, fragment: "")
case .slash:
return (token: .slash, fragment: "")
case .dot:
return (token: .dot, fragment: "")
case .star:
return (token: nil, fragment: "*")
}
}
}

mutating func nextToken() -> RoutingPatternToken? {
guard !isEOF else { return nil }

guard let firstChar = unScannedFragment.first else { return nil }
internal struct RoutingPatternScanner {

self.position = expression.index(position, offsetBy: 1)
static func tokenize(_ pattern: PatternIdentifier) -> [RoutingPatternToken] {
guard !pattern.isEmpty else { return [] }

switch firstChar {
case "/":
return .slash
case ".":
return .dot
case "(":
return .lParen
case ")":
return .rParen
default:
break
}
var appending = ""
var result: [RoutingPatternToken] = pattern.reduce(into: []) { box, char in
guard let terminator = _PatternScanTerminator(rawValue: char) else {
appending.append(char)
return
}

var fragment = ""
var stepPosition = 0
for char in self.unScannedFragment {
if RoutingPatternScanner.stopWordsSet.contains(char) {
break
let jointFragment = terminator.jointFragment
defer {
if let token = jointFragment.token {
box.append(token)
}
appending = jointFragment.fragment
}

fragment.append(char)
stepPosition += 1
guard let jointToken = _generateToken(expression: appending) else { return }
box.append(jointToken)
}

self.position = expression.index(self.position, offsetBy: stepPosition)
if let tailToken = _generateToken(expression: appending) {
result.append(tailToken)
}
return result
}

static private func _generateToken(expression: String) -> RoutingPatternToken? {
guard let firstChar = expression.first else { return nil }
let fragments = String(expression.dropFirst())
switch firstChar {
case ":":
return .symbol(fragment)
return .symbol(fragments)
case "*":
return .star(fragment)
return .star(fragments)
default:
return .literal("\(firstChar)\(fragment)")
return .literal(expression)
}
}

static func tokenize(_ expression: String) -> [RoutingPatternToken] {
var scanner = RoutingPatternScanner(expression: expression)

var tokens: [RoutingPatternToken] = []
while let token = scanner.nextToken() {
tokens.append(token)
}

return tokens
}
}
Loading

0 comments on commit 6fff25d

Please sign in to comment.