Giter Site home page Giter Site logo

pointfreeco / vapor-routing Goto Github PK

View Code? Open in Web Editor NEW
84.0 9.0 13.0 18.16 MB

A bidirectional Vapor router with more type safety and less fuss.

Home Page: http://pointfreeco.github.io/vapor-routing

License: MIT License

Swift 93.49% Makefile 6.51%
router routing server-side-swift vapor bidirectional invertible reversible

vapor-routing's Introduction

vapor-routing

A routing library for Vapor with a focus on type safety, composition, and URL generation.


Learn More

This library was discussed in an episode of Point-Free, a video series exploring functional programming and the Swift programming and the Swift language, hosted by Brandon Williams and Stephen Celis.

video poster image

Motivation

Routing in Vapor has a simple API that is similar to popular web frameworks in other languages, such as Ruby's Sinatra or Node's Express. It works well for simple routes, but complexity grows over time due to lack of type safety and the inability to generate correct URLs to pages on your site.

To see this, consider an endpoint to fetch a book that is associated with a particular user:

// GET /users/:userId/books/:bookId
app.get("users", ":userId", "books", ":bookId") { req -> BooksResponse in
  guard
    let userId = req.parameters.get("userId", Int.self),
    let bookId = req.parameters.get("bookId", Int.self)
  else {
    struct BadRequest: Error {}
    throw BadRequest()
  }

  // Logic for fetching user and book and constructing response...
  async let user = database.fetchUser(user.id)
  async let book = database.fetchBook(book.id)
  return BookResponse(...)
}

When a URL request is made to the server whose method and path matches the above pattern, the closure will be executed for handling that endpoint's logic.

Notice that we must sprinkle in validation code and error handling into the endpoint's logic in order to coerce the stringy parameter types into first class data types. This obscures the real logic of the endpoint, and any changes to the route's pattern must be kept in sync with the validation logic, such as if we rename the :userId or :bookId parameters.

In addition to these drawbacks, we often need to be able to generate valid URLs to various server endpoints. For example, suppose we wanted to generate an HTML page with a list of all the books for a user, including a link to each book. We have no choice but to manually interpolate a string to form the URL, or build our own ad hoc library of helper functions that do this string interpolation under the hood:

Node.ul(
  user.books.map { book in
    .li(
      .a(.href("/users/\(user.id)/book/\(book.id)"), book.title)
    )
  }
)
<ul>
  <li><a href="/users/42/book/321">Blob autobiography</a></li>
  <li><a href="/users/42/book/123">Life of Blob</a></li>
  <li><a href="/users/42/book/456">Blobbed around the world</a></li>
</ul>

It is our responsibility to make sure that this interpolated string matches exactly what was specified in the Vapor route. This can be tedious and error prone.

In fact, there is a typo in the above code. The URL constructed goes to "/book/:bookId", but really it should be "/books/:bookId":

- .a(.href("/users/\(user.id)/book/\(book.id)"), book.title)
+ .a(.href("/users/\(user.id)/books/\(book.id)"), book.title)

This library aims to solve these problems, and more, when dealing with routing in a Vapor application, by providing Vapor bindings to the URL Routing package.

Getting started

To use this library, one starts by constructing an enum that describes all the routes your website supports. For example, the book endpoint described above can be represented as a particular case:

enum SiteRoute {
  case userBook(userId: Int, bookId: Int)
  // more cases for each route
}

Then you construct a router, which is an object that is capable of parsing URL requests into SiteRoute values and printing SiteRoute values back into URL requests. Such routers can be built from various types the library vends, such as Path to match particular path components, Query to match particular query items, Body to decode request body data, and more:

import VaporRouting

let siteRouter = OneOf {
  // Maps the URL "/users/:userId/books/:bookId" to the
  // SiteRouter.userBook enum case.
  Route(.case(SiteRoute.userBook)) {
    Path { "users"; Digits(); "books"; Digits() }
  }

  // More uses of Route for each case in SiteRoute
}

Note: Routers are built on top of the Parsing library, which provides a general solution for parsing more nebulous data into first-class data types, like URL requests into your app's routes.

Once this little bit of upfront work is done, using the router doesn't look too dissimilar from using Vapor's native routing tools. First you mount the router to the application to take care of all routing responsibilities, and you do so by providing a closure that transforms SiteRoute to a response:

// configure.swift
public func configure(_ app: Application) throws {
  ...

  app.mount(siteRouter, use: siteHandler)
}

func siteHandler(
  request: Request,
  route: SiteRoute
) async throws -> any AsyncResponseEncodable {
  switch route {
  case let .userBook(userId: userId, bookId: bookId):
    async let user = database.fetchUser(user.id)
    async let book = database.fetchBook(book.id)
    return BookResponse(...)

  // more cases...
  }
}

Notice that handling the .userBook case is entirely focused on just the logic for the endpoint, not parsing and validating the parameters in the URL.

With that done you can now easily generate URLs to any part of your website using a type safe, concise API. For example, generating the list of book links now looks like this:

Node.ul(
  user.books.map { book in
    .li(
      .a(
        .href(siteRouter.path(for: .userBook(userId: user.id, bookId: book.id)),
        book.title
      )
    )
  }
)

Note there is no string interpolation or guessing what shape the path should be in. All of that is handled by the router. We only have to provide the data for the user and book ids, and the router takes care of the rest. If we make a change to the siteRouter, such as recognizing the singular form "/user/:userId/book/:bookId", then all paths will automatically be updated. We will not need to search the code base to replace "users" with "user" and "books" with "book".

Documentation

The documentation for releases and main are available here:

License

This library is released under the MIT license. See LICENSE for details.

vapor-routing's People

Contributors

finestructure avatar gohanlon avatar mbrandonw avatar stephencelis avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

vapor-routing's Issues

ObjectId.parser() instand -> UUID.parser()

ObjectId is from mongo db

public let wordsRouter = OneOf {
    Route(.case(WordsRoute.words)) {
        Path { "words2" }
    }
    
    Route(.case(WordsRoute.word)) {
        Path { "words2" }
        Path { Parse(.string) } // this how do it parse this like UUID.parser() *
        wordRouter
    }
}

func wordHandher(
    request: Request,
    wordID: String,
    route: WordRoute
) async throws -> AsyncResponseEncodable {
    switch route {
    case .fetch:
        guard let id = ObjectId.init(wordID) // * so I can remove this line 
        else {
            struct BadObjectId: Error {}
            throw BadObjectId()
        }
        
        return WordResponse(_id: id, englishWord: "english Word", englishDefinition: "English Definition", isReadFromNotification: false, isReadFromView: false, level: .beginner, url: router.url(for: .words(.word(id.hexString, .fetch))))
}

Vapor Abort doesn't work as expected.

I've replaced a part of the routes in an existing project using VaporRouting. I Replaced a single Vapour Controller by removing it from the registration in routes() and mounted a siteHandler and swift-parsing router that only covers the routes from that controller.
Now within a site handler when throwing for example a .badRequest error for example by using:
throw Abort(.badRequest, reason: "...")
A response is returned with status .notFound, I'm expecting this to be the route not being found. However my router did reach the end it's just the siteHandler that failed somewhere down the chain. I would expect my response to return this .badRequest error

Documentation links aren't working

Just wanted to point out that the documentation urls point to a blank page. the one for the swift-url-routing do seem to work for me!

How can I specify HTTP Method in my routes

Hi y'all!

I saw the fantastic video on the PointFree website demonstrating the ability of this library and I wanted to play around with it.
I've got myself a simple little Vapor app with a House entity and Room entity in order to have a one-to-many relation.

I'm setting up my router like this:

func setupRouter(app: Application) throws {
    let houseRouter = OneOf {
        Route(.case(HouseRoute.house)) {
            Path { "houses"; UUID.parser() }
        }
        Route(.case(HouseRoute.create)) {
            Path { "houses"; }
        }
    }
    app.mount(houseRouter, use: houseHandler)
}

And then I have my handler function looks like this:

func houseHandler(request: Request,
                  route: HouseRoute) async throws -> any AsyncResponseEncodable {
    switch route {
    case let .house(houseId: houseId):
        guard let house: House = try await House.query(on: request.db)
            .filter(\House.$id == houseId)
            .first() else {
            throw Abort(.notFound)
        }
        return HouseResponse(house: house)
    case .create:
        let house: House = try request.content.decode(House.self)
        try await house.create(on: request.db)
        return HouseResponse(house: house)
    }
}

When running the request with a POST HTTP method it fails with the following error:
image

However if I run the request with a GET HTTP method it succeeds even though a GET request shouldn't have a HTTP body according to the spec:
image

Now my question is how do I specify in the setupRouter method that the HouseRoute.create enum case maps to a POST HTTP method?

Longer POST/PUT body fails inconsistently (tests attached)

I've created a test package with nearly the same SiteRoute used in vapor-routing's tests.

I demonstrate several things in these tests that I've observed in real-world use.

  1. Posting a short comment like the one in your test works fine every time
  2. Posting a medium-length comment usually works, but it will fail a few times out of 100 repetitions
  3. Posting a long comment almost always fails in the first twenty or so repetitions, and then it will succeed.
  4. The issue can be demonstrated on a route that parses the Comment, but also on a route that just accepts raw data. So the issue isn't related to JSONDecoder.
  5. Vapor's built-in router doesn't have this issue
  6. XCTVapor can't be used to demonstrate this issue, so I test by running my example server and making URLRequests against the running example server.

✅ vapor-routing-test.zip

I've tried many things, but I've had no luck fixing this issue myself. I thought it might have been related to this open issue: vapor/vapor#2682 but that issue appears to be caused by the use of data.getString(at: 0, length: data.capacity) in the example project attached to that issue.

There are a few important things you should know about running these tests

  • You need to run the server in Xcode before you run the tests
  • When you run the tests while the server is running, it will ask you if you want to replace the server. Click "Add" instead of replacing it.
  • The issue can be demonstrated just by running the tests once, but it is even more interesting when you run the tests repeatedly. If you haven't done this before, see this screenshot for instructions:

Screenshot 2023-02-03 at 5 43 26 PM

I like to pick Maximum Repetitions: 100, but 1,000 or even 10,000 will finish in a reasonable timeframe too.

More detailed JSON Decoding error message

The feedback for corrupt or unexpected data is lacking.

struct EditPoll: Codable, Equatable {
    let name: String
    let description: String
    let duration: Int
    let roomId: UUID
}

Route(.case(PollsRoute.create)) {
    Method.post
    Body(.json(EditPoll.self))
}

error: The data couldn’t be read because it isn’t in the correct format.

I think the following would print more specific information.

print(String(describing: jsonDecodingError))
``

How do I run on IOS for SiteRouter -> this target supports 9.0

I have create new SPM for my models so I can add server + iOS now when I add it iOS project I am getting this issue

// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "WordNotificationSiteRouter",
    platforms: [.macOS(.v12)], -> I also try add `.iOS(.v14)`
    products: [
        // Products define the executables and libraries a package produces,
        // and make them visible to other packages.
        .library(name: "WordNotificationSiteRouter", targets: ["WordNotificationSiteRouter"]),
    ],
    dependencies: [
        .package(url: "https://github.com/pointfreeco/vapor-routing", from: "0.1.1"),
        .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.9.2"),
        .package(url: "https://github.com/vapor/fluent-mongo-driver.git", from: "1.1.2"),
    ],
    targets: [
        .target(
            name: "WordNotificationSiteRouter",
            dependencies: [
                .product(name: "VaporRouting", package: "vapor-routing"),
                .product(name: "_URLRouting", package: "swift-parsing"),
                .product(name: "FluentMongoDriver", package: "fluent-mongo-driver"),
            ]),
        .testTarget(
            name: "WordNotificationSiteRouterTests",
            dependencies: ["WordNotificationSiteRouter"]),
    ]
)

Showing Recent Messages
The package product 'FluentKit' requires minimum platform version 13.0 for the iOS platform, but this target supports 9.0
The package product 'MongoKitten' requires minimum platform version 12.0 for the iOS platform, but this target supports 9.0
The package product 'Vapor' requires minimum platform version 13.0 for the iOS platform, but this target supports 9.0

Breaking change in release 0.1.3

Screenshot 2023-04-16 at 22 20 31

dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
        .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
        .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
        .package(url: "https://github.com/vapor/leaf.git", from: "4.0.0"),
        .package(url: "https://github.com/pointfreeco/vapor-routing", from: "0.1.0")
    ],

Forcing previous release fixes my issue

 .package(url: "https://github.com/pointfreeco/vapor-routing", esact: "0.1.2")

Unable to run with Docker

Hi! I am getting an error after adding this library to generated by vapor new project with fluent+postgre.
This error does not appears with docker compose build but, after running docker compose up I got this:

$ docker compose up   
[+] Running 2/2
 ⠿ Container testpointfree-db-1   Created                                                                                                         0.0s
 ⠿ Container testpointfree-app-1  Recreated                                                                                                       0.1s
Attaching to testpointfree-app-1, testpointfree-db-1
testpointfree-db-1   | 
testpointfree-db-1   | PostgreSQL Database directory appears to contain a database; Skipping initialization
testpointfree-db-1   | 
testpointfree-db-1   | 2022-05-24 13:57:41.910 UTC [1] LOG:  starting PostgreSQL 14.3 on x86_64-pc-linux-musl, compiled by gcc (Alpine 10.3.1_git20211027) 10.3.1 20211027, 64-bit
testpointfree-db-1   | 2022-05-24 13:57:41.911 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
testpointfree-db-1   | 2022-05-24 13:57:41.911 UTC [1] LOG:  listening on IPv6 address "::", port 5432
testpointfree-db-1   | 2022-05-24 13:57:41.914 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
testpointfree-db-1   | 2022-05-24 13:57:41.920 UTC [22] LOG:  database system was shut down at 2022-05-24 13:51:55 UTC
testpointfree-db-1   | 2022-05-24 13:57:41.933 UTC [1] LOG:  database system is ready to accept connections
testpointfree-app-1  | ./Run: error while loading shared libraries: libcurl.so.4: cannot open shared object file: No such file or directory
testpointfree-app-1 exited with code 127

My Package.swift:

// swift-tools-version:5.6
import PackageDescription

let package = Package(
    name: "TestPointfree",
    platforms: [
       .macOS(.v12)
    ],
    dependencies: [
        // 💧 A server-side Swift web framework.
        .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
        .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
        .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),
        .package(url: "https://github.com/pointfreeco/vapor-routing", from: "0.1.0"),
    ],
    targets: [
        .target(
            name: "App",
            dependencies: [
                .product(name: "Fluent", package: "fluent"),
                .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
                .product(name: "Vapor", package: "vapor"),
                .product(name: "VaporRouting", package: "vapor-routing"),
            ],
            swiftSettings: [
                // Enable better optimizations when building in Release configuration. Despite the use of
                // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
                // builds. See <https://github.com/swift-server/guides/blob/main/docs/building.md#building-for-production> for details.
                .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
            ]
        ),
        .executableTarget(name: "Run", dependencies: [.target(name: "App")]),
        .testTarget(name: "AppTests", dependencies: [
            .target(name: "App"),
            .product(name: "XCTVapor", package: "vapor"),
        ])
    ]
)

Project without this library runs as expected.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.