Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Type-safe URL Routing in Swift (Japanese)

Type-safe URL Routing in Swift (Japanese)

Eac0bf787b5279aca5e699ece096956e?s=128

Yasuhiro Inami

November 20, 2016
Tweet

Transcript

  1. Type-safe URL Routing in Swift 2016/11/20 iOS AllStars 2 #eventdots

    Yasuhiro Inami / @inamiy
  2. https://eventdots.jp/ iOS/AllStars/2

  3. /iOS/AllStars/2

  4. /iOS/{title}/{version}

  5. URL routings everywhere! • ΫϥΠΞϯταΠυ • Universal Links (>= iOS

    9) • Custom URL Schemes (<= iOS 8) • αʔόʔαΠυ • Perfect, Kitura, Zewo, Vapor, etc • Node.js Express෩
  6. ΫϥΠΞϯταΠυ(ྫ) enum Sitemap { case event(title: String, version: Int) //

    "/iOS/{title}/{version}" case notFound } struct Router { func match(path: String) -> Sitemap { if let captures: [String] = myRegex("/iOS/(.+)/(\\d+)", path), captures.count == 2, let version = Int(captures[1]) { return .event(captures[0], version) } return .notFound } }
  7. func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping

    ([Any]?) -> () ) -> Bool { // iOS Universal Links guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return false } let sitemap = self.router.match(url.path) switch sitemap { case let .event(title, version): self.presentAwesomeOpeningMovie(title, version) return true case .notFound: print("Invalid URL.") return false } }
  8. αʔόʔαΠυ(ྫ) // Perfect routes.add(method: .get, uri: "/iOS/{title}/{version}", handler: requestHandler) //

    Zewo route.get("/iOS/:title/:version") { request in guard let title: String = request.pathParameters["title"], let version: String = request.pathParameters["version"] else { ... } /* Warning: All pathComponents are captured in String! */ ... } // Vapor drop.get("iOS", String.self, Int.self) { (request, title: String, version: Int) in ... // ← ܕʹ஫໨ }
  9. None
  10. // Vapor routing APIs // ⚠ AUTOMATICALLY GENERATED CODE ///

    GET /<path>/ public func get(_ p0: String = "", handler: (Request) throws -> ResponseRepresentable) /// GET /{wildcard}/ public func get<W0: StringInitializable>(_ w0: W0.Type, handler: (Request, W0) throws -> ResponseRepresentable) /// GET /<path>/{wildcard}/ public func get<W0: StringInitializable>(_ p0: String, _ w0: W0.Type, handler: (Request, W0) throws -> ResponseRepresentable) /// GET /<path>/<path>/ public func get(_ p0: String, _ p1: String, handler: (Request) throws -> ResponseRepresentable) /// GET /{wildcard}/<path>/{wildcard}/ public func get<W0: StringInitializable, W1: StringInitializable>(_ w0: W0.Type, _ p0: String, _ w1: W1.Type, handler: (Request, W0, W1) throws -> ResponseRepresentable)
  11. ϝλϓϩάϥϛϯά !

  12. GET /{wildcard}/<path>/ {wildcard}/<path>/{wildcard}/...

  13. !

  14. Functional Programming

  15. https://realm.io/news/ tryswift-yasuhiro-inami-parser-combinator

  16. Parser Combinator 1จࣈ୯ҐͰύʔε

  17. None
  18. 1จࣈ୯ҐͰύʔε จࣈྻ୯ҐͰύʔε (จࣈྻ = pathComponent)

  19. Route ܕ /// ঢ়ଶมԽؔ਺ͷίϯςφ public struct Route<Value> { let parse:

    ([String]) -> ([String], Value)? } • Ҿ਺: pathComponents as [String] • ฦΓ஋: "݁Ռ" ͱ "࢒Γͷೖྗ" ͷ Optional tuple
  20. // MARK: Functor public func <^> <Value1, Value2>( f: @escaping

    (Value1) -> Value2, route: Route<Value1> ) -> Route<Value2> { return Route { pathComponents in let reply = route.parse(pathComponents) switch reply { case let .some(pathComponents2, value): return (pathComponents2, f(value)) case .none: return nil } } }
  21. // MARK: Applicative public func pure<Value>(_ value: Value) -> Route<Value>

    { return Route { ($0, value) } } public func <*> <Value1, Value2>( route1: Route<(Value1) -> Value2>, route2: @autoclosure @escaping () -> Route<Value1> ) -> Route<Value2> { return Route { pathComponents in let reply = route1.parse(pathComponents) switch reply { case let .some(pathComponents2, f): return (f <^> route2()).parse(pathComponents2) case .none: return nil } } }
  22. // MARK: Applicative public func <**> <Value1, Value2>( route1: Route<Value1>,

    route2: @autoclosure @escaping () -> Route<(Value1) -> Value2> ) -> Route<Value2> { return { a in { $0(a) } } <^> route1 <*> route2 } public func <* <Value1, Value2>( route1: Route<Value1>, route2: @autoclosure @escaping () -> Route<Value2> ) -> Route<Value1> { return const <^> route1 <*> route2 } public func *> <Value1, Value2>( route1: Route<Value1>, route2: @autoclosure @escaping () -> Route<Value2> ) -> Route<Value2> { return const(id) <^> route1 <*> route2 }
  23. // MARK: Alternative public func empty<Value>() -> Route<Value> { return

    Route { _ in nil } } public func <|> <Value>( route1: Route<Value>, route2: @autoclosure @escaping () -> Route<Value> ) -> Route<Value> { return Route { pathComponents in let reply = route1.parse(pathComponents) switch reply { case .some: return reply case .none: return route2().parse(pathComponents) } } }
  24. // MARK: Monad // [ίϝϯτΞ΢τ] // Ϟφυ͸ΦʔόʔεϖοΫͳͷͰࠓճඞཁ͋Γ·ͤΜɻ // ͳͥͳΒɺURLϧʔςΟϯάͰpathComponentΛ1ͭͣͭύʔε͢Δࡍʹɺ //

    ݸʑͷॲཧ͸લޙؔ܎ʹґଘ͠ͳ͍͔ΒͰ͢ʢจ຺ࣗ༝จ๏ʣ // // ݴ͍׵͑Δͱɺ৽͍͠ Route ΛϥϯλΠϜதʹੜ੒͢Δඞཁ͕ͳ͘ɺ // ίϯύΠϧ࣌ʹఆٛͨ͠ Route ʹ͍ͭͯ // ΞϓϦΧςΟϒελΠϧͰஞ࣍ద༻͢Ε͹े෼͔ͭߴ଎Ͱ͢ɻ
  25. Monadic Applicative URL Parser

  26. public func capture<Value>(_ parse: @escaping (String) -> Value?) -> Route<Value>

    { return Route { pathComponents in if let (first, rest) = uncons(pathComponents), let parsed = parse(first) { return (Array(rest), parsed) } return nil } } public func int() -> Route<Int> { return capture { Int($0) } } public func match(_ string: String) -> Route<()> { return capture { $0 == string ? () : nil } } public func choice<Value>(_ routes: [Route<Value>]) -> Route<Value> { return routes.reduce(empty()) { $0 <|> $1 } }
  27. Ϩοπߏจղੳʂ

  28. /// "/iOS/{title}/{version}" Λύʔε let route: Route<(String, Int)> = curry {

    ($1, $2) } <^> match("iOS") <*> string() <*> int() // ΑΓΦγϟϨͳه๏: // let route = // /"iOS" </> string() </> int() <&!> flipCurry { ($1, $2) } let value1 = route.run(["iOS", "AllStars", "2"]) expect(value1?.0) == "AllStars" expect(value1?.1) == 2 let value2 = route.run(["P", "P", "A", "P"]) expect(value2).to(beNil())
  29. /// - "/R/foo/{int}" ==> `Sitemap.foo(int)` /// - "/R/bar/{double}" ==> `Sitemap.bar(double)`

    /// - "/R/baz/{string}" ==> `Sitemap.baz(string)` /// - "/R/foo/bar" ==> `Sitemap.fooBar` /// - otherwise ==> `Sitemap.notFound` let route: Route<Sitemap> = match("R") *> choice([ match("foo") *> int() <&> Sitemap.foo, match("bar") *> double() <&> Sitemap.bar, match("baz") *> string() <&> Sitemap.baz, match("foo") *> match("bar") *> pure(Sitemap.fooBar), ]) <|> pure(Sitemap.notFound) // `<&>` = flipped map = `flip(<^>)`
  30. // (ଓ͖) let route: Route<Sitemap> = match("R") *> choice([ match("foo")

    *> int() <&> Sitemap.foo, match("bar") *> double() <&> Sitemap.bar, match("baz") *> string() <&> Sitemap.baz, match("foo") *> match("bar") *> pure(Sitemap.fooBar), ]) <|> pure(Sitemap.notFound) expect(route.run(["R", "foo", "123"])) == .foo(123) expect(route.run(["R", "bar", "4.5"])) == .bar(4.5) expect(route.run(["R", "baz", "xyz"])) == .baz("xyz") expect(route.run(["R", "foo", "bar"])) == .fooBar expect(route.run(["R", "foo", "xxx"])) == .notFound
  31. None
  32. ຊ౰ʹʁ

  33. // ໰୊ͷ͋Δ choice ͷॻ͖ํ choice([ match("foo") *> int() <&> Sitemap.foo,

    match("bar") *> double() <&> Sitemap.bar, match("baz") *> string() <&> Sitemap.baz, match("foo") *> match("bar") *> pure(Sitemap.fooBar), ]) 1. "foo" + int ΛνΣοΫ 2. "bar" + double ΛνΣοΫ 3. "baz" + string ΛνΣοΫ 4. "foo" + "bar" ΛνΣοΫ ... "foo" Λ2ճνΣοΫ!? !
  34. ސ٬͕ຊ౰ʹඞཁͩͬͨ΋ͷ // ࠷దԽ͞Εͨ choice choice([ match("foo") *> choice([ int() <&>

    Sitemap.foo, match("bar") *> pure(Sitemap.fooBar) ]), match("bar") *> double() <&> Sitemap.bar, match("baz") *> string() <&> Sitemap.baz ])
  35. enumΛ࢖ͬͯ ߏ଄Λղऍ͢Δ

  36. ৽͍͠ Route ܕ (Enumϕʔε) /// Enumϕʔεͷ URL route. public indirect

    enum Route<Value> { case match(String, Route<Value>) case capture((String) -> Any?, (Any) -> Route<Value>) case choice([Route<Value>]) case term(Value) case zero } • .match ͸ "Ϛονจࣈྻ" ͱ "ޙଓͷ Route" ΛؚΉ • .capture ͸ "ύʔεॲཧ" ͱ "ޙଓ Route ม׵" ͷ2ͭͷؔ਺
  37. extension Route { // ৽͘͠ run Λ࣮૷ʢؔ਺ϕʔεͰ͸ͳ͘ͳͬͨͨΊʣ public func run(_

    pathComponents: [String]) -> Value? { switch self { case let .match(string, route): ... case let .capture(parse, convert): ... case let .choice(routes): ... case let .term(value): return value case .zero: return nil } } } // ͞Βʹɺલճͱಉ༷ɺFunctor, Applicative, Alternative ΋࣮૷͢Δ...
  38. ࠷దԽॲཧ

  39. public func optimize<Value>(_ route: Route<Value>) -> Route<Value> { switch route

    { case let .match(string, route): return .match(string, optimize(route)) case let .capture(parse, convert): return .capture(parse, { optimize(convert($0)) }) case let .choice(routes): return _optimizeChoice(routes) case .term, .zero: return route } }
  40. private func _optimizeChoice<Value>( _ routes: [Route<Value>] ) -> Route<Value> {

    if routes.isEmpty { return .zero } // ιʔτ ! ಉ͡ઌ಄ͷ Route ͰάϧʔϓԽ ! ϑϥοτԽ let sortedRoutes = routes.sorted(by: isAscending) let arrangedRoutes = groupBy(isSameRouteHead)(ArraySlice(sortedRoutes)) .map(flatten) return .choice(arrangedRoutes) }
  41. Ϩοπ࠷దԽʂ

  42. // ࠷దԽલ (`match("foo")`͕ॏෳ) let naiveRoute: Route<Sitemap> = match("R") *> choice([

    match("foo") *> int() <&> Sitemap.foo, match("bar") *> double() <&> Sitemap.bar, match("baz") *> string() <&> Sitemap.baz, match("foo") *> match("bar") *> pure(Sitemap.fooBar) ]) // ࠷దԽޙ let optimizedRoute = optimize(naiveRoute)
  43. ݁Ռ: naiveRoute = .match("R", .choice([ .match("foo", .capture(int)), .match("bar", .capture(double)), .match("baz",

    .capture(string)), .match("foo", .match("bar", .term(fooBar))) ])) optimizedRoute = .match("R", .choice([ .match("bar", .capture(double)), .match("baz", .capture(string)), .match("foo", .choice([ .capture(int), .match("bar", .term(fooBar)) ])) ]))
  44. FunRouter https://github.com/inamiy/FunRouter

  45. ·ͱΊ • จࣈྻϕʔεͷURLॲཧ͸؆୯͚ͩͲɺܕ҆શͰ͸ͳ͍ • Monadic Applicative Parser Λ࢝ΊΑ͏ • EnumΛ࢖͏ͱ࠷దԽ΋Ͱ͖ΔΑ

    • ⾠஫ҙ "Expression was too complex to be solved in reasonable time." Τϥʔ͕ग़ͯ΋ٽ͔ͳ͍ • Swift 4 Ͱվળ͞ΕΔ͜ͱΛظ଴͠·͠ΐ͏ !
  46. Thanks!