diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..46a090a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,10 @@ +name: Publish to pub.dev +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+*" +jobs: + publish: + permissions: + id-token: write + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 diff --git a/.gitignore b/.gitignore index c0120f0..670203d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,5 @@ -# https://dart.dev/guides/libraries/private-files -# Created by `dart pub` .dart_tool/ - -# Avoid committing pubspec.lock for library packages; see -# https://dart.dev/guides/libraries/private-files#pubspeclock. -pubspec.lock +node_modules/ .DS_Store +pubspec.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d8b8a5..21446f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,16 @@ -# Spry v5.0.0-beta.1 +# Spry v5.0.0 -To install Spry v5.0.0-beta.1 run this command: +To install Spry v5.0.0 run this command: ```bash -dart pub add spry:^5.0.0-beta +dart pub add spry ``` Or update your `pubspec.yaml` file: ```yaml dependencies: - spry: ^5.0.0-beta + spry: ^5.0.0 ``` ## What's Changed diff --git a/bun.lockb b/bun.lockb index 9558b1b..39ad538 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index d72be1f..4531e68 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,5 +1,10 @@ import { defineConfig } from "vitepress"; +const guide = { + text: "Guide", + items: [{ text: "App Instance", link: "/guide/app" }], +}; + export default defineConfig({ title: "Spry", titleTemplate: "Spry: :title", @@ -19,27 +24,10 @@ export default defineConfig({ pattern: "https://github.com/medz/spry/edit/main/docs/:path", }, nav: [ - { - text: "Guide", - items: [ - { text: "App", link: "/guide/app" }, - { text: "Routing", link: "/guide/routing" }, - { text: "Handler", link: "/guide/handler" }, - { text: "Event", link: "/guide/event" }, - { text: "WebSocket", link: "/guide/websocket/introduction" }, - ], - }, - { - text: "Platforms", - items: [ - { text: "Plain", link: "/platforms/plain" }, - { text: "IO (dart:io)", link: "/platforms/io" }, - { text: "Web", link: "/platforms/web" }, - ], - }, + guide, { text: "Examples", - link: "https://github.com/medz/spry/tree/main/examples", + link: "https://github.com/medz/spry/tree/main/example", }, ], sidebar: [ @@ -48,40 +36,7 @@ export default defineConfig({ text: "Getting Started", link: "/getting-started", }, - { - text: "Basics", - items: [ - { text: "App", link: "/guide/app" }, - { text: "Routing", link: "/guide/routing" }, - { text: "Handler", link: "/guide/handler" }, - { text: "Event", link: "/guide/event" }, - ], - }, - { - text: "WebSocket", - items: [ - { text: "Introduction", link: "/guide/websocket/introduction" }, - { text: "Hooks", link: "/guide/websocket/hooks" }, - { text: "Peer", link: "/guide/websocket/peer" }, - { text: "Message", link: "/guide/websocket/message" }, - ], - }, - { - text: "Advanced", - items: [{ text: "Cookies", link: "/advanced/cookies" }], - }, - { - text: "Platforms", - items: [ - { - text: "Create a new platform", - link: "/platforms/create", - }, - { text: "Plain", link: "/platforms/plain" }, - { text: "IO (dart:io)", link: "/platforms/io" }, - { text: "Web", link: "/platforms/web" }, - ], - }, + guide, ], socialLinks: [ { icon: "github", link: "https://github.com/medz/spry" }, diff --git a/docs/advanced/cookies.md b/docs/advanced/cookies.md deleted file mode 100644 index 3a71493..0000000 --- a/docs/advanced/cookies.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -title: Advanced → Cookies ---- - -# Cookies - -[![Pub Version](https://img.shields.io/pub/v/spry_cookie.svg)](https://pub.dev/packages/spry_cookie) - -An HTTP cookie is a small piece of data stored by the user's browser. Cookies were designed to be a reliable mechanism for websites to remember stateful information. When the user visits the website again, the cookie is automatically sent with the request. - -## Integration - -Spry is more focused on routing and APIs servers and does not have built-in support for Cookies. - -Install Spry cookies support package (`spry_cookie`): - -```bash -dart pub add spry_cookie -``` - -Or, update your `pubspec.yaml` file: - -```dart -dependencies: - spry_cookie: -``` - -## Usege - -Spry Cookies supports global and single handler mode. - -### Global Support Cookies - -```dart -import 'package:spry_cookie/spry_cookie.dart'; - -app.use(cookie()); -``` - -### Only single handler - -Wrap a closure handler with `cookieWith`: - -```dart -import 'package:spry_cookie/spry_cookie.dart'; - -app.get('/user', cookieWith((event) { - // ... -})); -``` - -## Sign/Unsign cookies - -Spray Cookie supports signed and unsigned cookies. You only need to configure the security key and hash algorithm: - -```dart -app.use(cookie( - secret: "Your cookie sign secret", -)); -``` - -By default, the `SHA-256` hash algorithm is used. If you want to customize it, please set `algorithm`: - -```dart -import 'package:crypto/crypto.dart'; - -app.use(cookie( - secret: "Your cookie sign secret", - algorithm: md5, // Set algorithm to MD5 -)); -``` - -## Automatic `secure` settings - -使用 `autoSecureSet` 选项,当 Handler 中未设置 `secure` 时候,会判断当前请求是否是 `https` 自动设置: - -```dart -app.use(cookie( - autoSecureSet: true -)); -``` - -## Cookie options - -### `domain` - -Specifies the value for the [Domain Set-Cookie attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3). By default, no domain is set, and most clients will consider the cookie to apply to only the current domain. - -### `expires` - -Specifies the `DateTime` to be the value for the [Expires Set-Cookie attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1). By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and will delete it on a condition like exiting a web browser application. - -::: tip - -the [cookie storage model specification](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3) states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this, so if both are set, they should point to the same date and time. - -::: - -### `httpOnly` - -Specifies the boolean value for the [HttpOnly Set-Cookie attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.6). When truthy, the `HttpOnly` attribute is set, otherwise it is not. By default, the `HttpOnly` attribute is not set. - -### `maxAge` - -Specifies the `int` (in seconds) to be the value for the [Max-Age Set-Cookie attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2). The given number will be converted to an integer by rounding down. By default, no maximum age is set. - -### `partitioned` - -Specifies the `bool?` value for the [Partitioned Set-Cookie attribute](https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1). When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the `Partitioned` attribute is not set. - -### `path` - -Specifies the value for the [Path Set-Cookie attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.4). By default, the path is considered the ["default path"](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4). - -### `secure` - -Specifies the boolean value for the [Secure Set-Cookie attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.5). When truthy, the `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set. - -### `sameSite` - -Specifies the `SameSite` enum to be the value for the [SameSite Set-Cookie attribute](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7). - -* `SameSite.lax`: will set the `SameSite` attribute to `Lax` for lax same site enforcement. -* `SameSite.none`: will set the `SameSite` attribute to `None` for an explicit cross-site cookie. -* `SameSite.strict`: will set the `SameSite` attribute to `Strict` for strict same site enforcement. - -## Managing cookies - -We extend the `Event` object to add a `cookies` object to manage Cookies: - -```dart -app.use((event) { - print(event.cookie.get("user_id")); -}); -``` - -### `event.cookies.get` - -Gets a Request/Response cookie value. - -### `event.cookies.getAll` - -Gets all Request/Response cookies list. - -### `event.cookies.set` - -Sets a new cookie. - -### `event.cookies.delete` - -Deletes a cookie. - -### `event.cookies.serialize` - -Serialize a cookie. diff --git a/docs/getting-started.md b/docs/getting-started.md index 6a2dd40..3ab303d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -25,7 +25,7 @@ Creates a new file `app.dart`(or `main.dart` | `server.dart`): ::: code-group -<<< ../examples/simple-io/app.dart +<<< ../example/io.dart ::: @@ -63,7 +63,7 @@ return '⚡️ Tadaa!'; We then use Spry’s built-in `dart:io` platform support to wrap the app instance into a handler that `HttpServer` can use: ```dart -final handler = const IOPlatform().createHandler(app); +final handler = toIOHandler(app); ``` Finally, we create an HTTP server from `dart:io` and listen for requests to pass to the Spry app: diff --git a/docs/guide/app.md b/docs/guide/app.md index 7cf8be0..d038bec 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -1,118 +1,46 @@ --- -title: Basics → App (Spry) +title: Guide → App Instance --- -# App (Spry) +# App Instance -The heart of Spry is the `app` instance. It is the core handler for incoming requests. You can use the application instance to register event handlers. +App instance is the core of a Spry. -## Initialize the application - -You can use the default factory of `Spry` to create a new Spry application instance: - -```dart -final app = Spry(); -``` - -There are some additional options supported when initializing the application: - -### `locals` - -`locals` is an initialization data with a `Map` type, which defaults to `null`. It exists to hold the instance of the entire App extension function. When your application has written extension functions locally and needs to be initialized for global sharing, setting it is very useful. - -When the initialization of App is completed, it will be converted to a `Locals` object. Here is a simple demonstration of globally shared data: - -```dart -final app = Spry(locals: { - 'name': 'Seven', -}); - -final app.use((event) { - print(event.locals.get('nane')); // Seven +--- - return next(); -}); -``` +The core of a Spry is an `app` instance. It is the core of the server that handles incoming requests. You can use app instance to register event handlers. -### `router` +## Initializing an app -Spry uses the Radix-Tree router implemented in [RoutingKit](https://pub.dev/packages/routingkit) by default. You can also implement your own Router for Spry to use. Just make it satisfy the `Router` signature: +You can create a new Spry app instance using `createSpry` utility: ```dart -import 'package:routingkit/routingkit.dart'; - -class MyRouter implements Router { - ... -} - -final app = Spry(router: MyRouter()); +final app = createSpry(); ``` -### `routerDriver` +## Registering event handlers -This is a custom Router implementation provided by [RoutingKit](https://pub.dev/packages/routingkit). When you pass it to Spry when using other RoutingKit directories, Spry will use this driver to create Router instances. - -### `caseSensitive` - -This option tells Router whether to distinguish between upper and lower case paths. The default is `false` (ignore case). If you use Spry for other scenarios, you may need it. +You can register [event handlers](/guide/event-handler) to app instance stack use `app.use`: ```dart -final app = Spry(caseSensitive: false); +app.use((event) => 'Hello Spry!'); ``` -## Adding Routes +## Routing -`addRoute` is the basic method for the entire Core to implement and add routing handlers: +To learn about routing, see the [Routing guide](/guide/routing). -```dart -app.addRoute(, , ); -``` +## Internals ::: tip -For more information, please see [Basics → Routing](/guide/routing) +This details are mainly informational. never directly use internals for production applications! ::: -## Adding Handlers - -Spry is capable of stack (Onion, growing from the inside out) processing. Each time `addHandler` is used, it will be added to the innermost layer of the nested layer. When calling, use them one by one in the order of addition. - -```dart -app.addHandler(ClosureHandler((event) => ...)); -``` - -Each added layer has the ability to return `Response` independently. This will result in that if a layer directly returns a `Response` object, the later added layer will not be called. -Of course, this is intentional, just like peeling an onion, we only need to peel specific layers, and there is no need to peel the onion layer by layer. - -If a layer wants to call the following layer, it needs to use `next` to implement it: - -```dart -MyHandler implements Handler { - Future handle(Event event) async { - print('Before'); - final res = await next(event); - print('After'); - - return res; - } -} -``` - -## `.use(...)` - -`addHandler` is a low API, it is not easy to use. So you can use `use` in Spry to quickly add a Handler without implementing the `Handler` interface: - -```dart -app.use((Event event) { - print('Hi'); - - return next(event); -}); -``` - -## Fallback +::: warning +As it is an internal exposed detail, it may change at any time. +::: -After we run the Spry server, there is always a possibility of encountering a route that is not registered. Spry uses a `404` response by default. If you want to customize the implementation of a Handler that cannot find the path, please use `fallback`: +Spry app instance has some additional properties. However it is usually not recommended to directly access them unless you know what are you doing! -```dart -app.fallback((event) => 404); -``` +* `app.stack`: App stack handlers. +* `app.router`: App router instance. diff --git a/docs/guide/event.md b/docs/guide/event.md deleted file mode 100644 index 28edf0e..0000000 --- a/docs/guide/event.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Basics → Event ---- - -# Event - -Every time a new HTTP request comes, Spry internally creates an Event object and passes it though event handlers until sending the response. - -An event is passed through all the lifecycle hooks and composable utils to use it as context. - -Example: - -```dart -app.use((event) { - console.log('Request: ${event.method} ${event.uri.toString()}'); - - return next(event); // Call next handler. -}); -``` - -## `event.app` - -Returns the Spry instance for the request event. - -## `event.locals` - -A container for passing values ​​in the Handlers stack. - -## `event.request` - -Spry abstract Request request object. - -## `event.uri` - -The requested URI for the request event. - -If the request URI is absolute (e.g. 'https://www.example.com/foo') then -it is returned as-is. Otherwise, the returned URI is reconstructed by -using the request URI path (e.g. '/foo') and HTTP header fields. - -To reconstruct the scheme, the 'X-Forwarded-Proto' header is used. - -To reconstruct the host, the 'X-Forwarded-Host' header is used. If it is -not present then the 'Host' header is used. If neither is present then -the host name of the server is used. - -## `event.getClientAddress()` - -Returns client address, value formated of `:port`. - -The returned value comes from the Platform implementation. -if the platform does not support it, an empty string will be returned. - -## `event.handlers` - -Access to the normalized request handlers. - -## `event.method` - -Access to the normalized (uppercase) request method. - -## `event.params` - -Returns the [Params](/guide/routing#params) of dynamic routing. - -## `event.route?` - -Return [Route], when the route has not yet started matching or has not been matched to return null, usually this situation is when the route is registered and has entered the fallback processor. diff --git a/docs/guide/handler.md b/docs/guide/handler.md deleted file mode 100644 index 26f7d7e..0000000 --- a/docs/guide/handler.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: Basics → Handler ---- - -# Handler - -After creating an app instance, you can start defining your application logic using event handlers. -An event handler is a function that receive an `Event` instance and returns a response. You can compare it to controllers in other frameworks. - -## Defining event handlers - -You can define event handlers using `Handler` interface. - -```dart -class MyHandler implements Handler { - Future handle(Event event) async { - return Response.text('Result string'); - } -} -``` - -But usually you don't need to do this. Because Spry's logic registration API provides a simpler closure type method: - -```dart -app.use((event) { - return 'Result string'; -}); -``` - -## Responsible - -Values returned from event handlers are automatically converted to responses. It can be: - -* JSON serializable value. If returning a JSON object or serializable value, it will be stringified and sent with default `application/json` content-type. -* `String`/`num`/`bool`: Sent as-is using default `text/plain` content-type. -* `Map`/`List`: Sent as-is using default `application/json` content-type. -* `null`/`void`: Spry with end response with `204 - No Content` status code. -* Any `Object` include `toJson()` method: Sent as-is using default `application/json` content-type. -* `Stream>` -* `Responsible` - -Any of above values could also be wrapped in a `Future`. This means that you can return a `Future` from your event handler and Spry will wait for it to resolve before sending the response. - -**Example**: Send text response: - -```dart -app.use((event) => 'Hello, Spry!'); -app.use((event) => 1); -app.use((event) => 2.1); -app.use((event) => true); -``` - -**Example**: Send JSON response: - -```dart -app.use((event) => {"url": event.uri.toString()}); -app.use((event) => [1, 2, 3]); - -class User { - late String name; - - toJson() => {'name': name}; -} -app.use((event) => User()..name = 'Bob'); -``` - -**Example**: Send a Future value: - -```dart -app.use((event) { - final completer = Completer(); - Timer(Duration(seconds: 1), () { - completer.complete('One second later'); - }); - - return completer.future; -}); -``` - -**Example**: Send a Stream: - -```dart -app.use((event) { - return File("foo.txt").openRead(); -}); -``` - -## Handlers stack - -In Spry, we abandoned the so-called middleware design and designed the concept of Handlers stack, where each layer interrupts the call by default. - -```dart -app.use((event) => 'value'); -app.use((event) => 'value 2'); // Will never be executed! -``` - -The app expects that each Handler call will return a value, so the Handler needs to actively tell the app to call the next Handler (using the `next` function): - -```dart -app.use((event) { - print(1); - - return next(event); -}); -app.use((event) { - print(2); -}); - -// console: 1, 2 -``` - -It can also perform functions similar to After-middleware: - -```dart -app.use((event) async { - final res = next(event); - print(1); - - return res; -}); -app.use((event) { - print(2); -}); - -// console: 2, 1 -``` diff --git a/docs/guide/routing.md b/docs/guide/routing.md deleted file mode 100644 index 7a1be65..0000000 --- a/docs/guide/routing.md +++ /dev/null @@ -1,337 +0,0 @@ ---- -title: Basics → Routing ---- - -# Routing - -Routing is the process of finding an appropriate request handler for an incoming request. The routing core of Spry is a high-performance Radix-Tree router based on [RoutingKit](https://pub.dev/packages/routingkit). - -[[toc]] - -## Overview - -To understand how routing works in Spry, first you should understand the basics about HTTP requests. Take a look at the sample request below: - -```http -GET /hello/spry HTTP/1.1 -host: spry.fun -content-length: 0 -``` - -This is a simple `GET` HTTP request to `/hello/spry`. If you enter the URL below into your browser's address bar, your browser will send this request. - -```http -http://spry.fun/hello/spry -``` - -### HTTP Method - -The first part of the request is the HTTP method. `GET` is the most common HTTP method, here are some other common HTTP methods: - -- `GET`: Get a resource -- `POST`: Create a resource -- `PUT`: Update a resource -- `DELETE`: Delete a resource -- `PATCH`: Update some properties of a resource - -### Request Path - -After the HTTP method is the request's URI. It consists of a path starting with `/` and an optional query string after `?`. The HTTP method and path are what Spry uses to route requests. - -### Router Methods - -Let's see how this request is handled in Spry: - -```dart -app.get('/hello/spry', (event) { - return 'Hello, Spry!'; -}); -``` - -All common HTTP methods can be used as methods of `Spry`. They accept a string representing the path, separated by `/`. -Note that `Spry` and `RoutesBuilder` do not build in all HTTP methods, you can use `on` to write manually: - -```dart -app.on("get", /hello/spry, (event) => "Hello, Spry!"); -``` - -After registering this route, the sample HTTP request above will get the following HTTP response: - -```http -HTTP/1.1 200 OK -content-length: 12 -content-type: text/plain; charset=utf-8 - -Hello, Spry! -``` - -### Route Parameters - -Now that we've successfully routed requests based on HTTP method and path, let me try to make the path dynamic. - -::: warning - -The `spry` name is hardcoded in both the path and the response. Let's make it dynamic so that you can access `/hello/` and get a response. - -::: - -```dart -app.get('/hello/:name', (event) { - final name = request.params.get("name"); - return 'Hello, $name!'; -}); -``` - -By using a path segment prefixed with `:` we indicate to the route that this is a dynamic path parameter. Now, any string provided here will match this route. We can then access the value of the string using `event.params`. - -If we run the sample request again, you will still get a response greeting `spry`. But now you can add any name after `/hello/` and see it in the response. Let's try `/hello/dart`. - -::: code-group - -```http [request] -GET /hello/dart HTTP/1.1 -content-length: 0 -``` - -```http [response] -HTTP/1.1 200 OK -content-length: 12 - -Hello, dart! -``` - -::: - -Now that you know the basics, check out the other sections to learn more. - -## Routes - -### Methods - -You can use a variety of HTTP method helpers to register routes directly to your Spry `Spry`: - -```dart -app.get("/foo/bar", (event) { - // ... -}); -``` - -Route handlers support you to return any `Responsible` content, including `String`, `Map`, `List`, `File`, `Stream`, etc. - -You can also specify the type of the return value of the route handler through the `T` type parameter: - -```dart -app.get("/foo", (event) { - return "bar"; -}); -``` - -This is a list of built-in HTTP methods: - -- `get` -- `post` -- `put` -- `patch` -- `delete` -- `head` - -Procesing HTTP method helpers, there is also an `on` function that accepts the HTTP method as an input parameter: - -```dart -app.on( - method: "get", - path: "/foo/bar", - (event) => { ... }, -); -``` - -### Path Segments - -Each route registration method accepts a string representation of `Segment`, and has the following four situations: - -- Constant (`foo`) -- Parameter (`:foo`) -- Anything (`*`) -- Catchall (`**`) - -#### Constant Segment - -This is a static Segment, only requests with an exact match string at this location are allowed. - -```dart -app.get("/foo/bar", (event) { - // ... -}); -``` - -#### Parameter Segment - -This is a parameter Segment, any string at this location will be allowed. Parameter Segment is specified with a `:` prefix, the string after `:` will be used as the parameter name. - -```dart -app.get("/foo/:bar", (event) { - // ... -}); -``` - -#### Anything Segment - -This is the same as the Parameter Segment, except that the parameter value is discarded. It is specified with a `*` prefix. - -```dart -app.get("/foo/*/baz", (event) { - // ... -}); -``` - -#### Catchall Segement - -This is a dynamic route component that matches one or more Segments, specified with `**`. Any string in the request will be allowed to match this location or after this location. - -```dart -// GET /foo/bar -// GET /foo/bar/baz -// ... -app.get("/foo/**", (event) { - // ... -}); -``` - -### Params - -When using parameter Segment (prefixed with `:`), the URI value for that location will be stored in `event.params`. You can access the value using the name in Path Sgements: - -```dart -app.get("/foo/:bar", (event) { - final bar = event.params("bar"); - // ... -}); -``` - -:: tip - -We can be sure that `event.params(...)` will never return `null` here because our path contains `:bar`. But if the parameter is processed in advance by middleware or other programs, we need to consider the `null` situation. - -::: - -Values matched via Catchall (`**`) or Anything (`*`) Segment will be stored in `request.params` as `Iterable`. You can access them using `request.params.catchall`: - -```dart -app.get("/foo/**", (event) { - final catchall = request.params.catchall; - // ... -}); -``` - -::: tip - -If your path contains multiple Parameter Segments, such as `/foo/:bar/:bar`, you use `event.params('bar')` only returns the first value, you can use `event.params.valuesOf('bar')` to get all values. - -::: - -### Body - -You can read the stream data of the request directly. - -```dart -app.post("/foo", (event) { - return event.request.body -}); -``` - -You can also send a `Stream` as the body of the response when the Handler returns `Stream`: - -```dart -app.post("/foo", (event) { - return File("foo.txt").openRead(); -}); -``` - -Of course, you can return any data without calling `write` or other methods of `request.response`. It supports `String`, `Map`, `List`, `File`, `Stream>`, etc. Of course, you can return an instance that implements `Responsible`. - -## Route Groups - -Route grouping allows you to create a group of routes with specific route prefixes or specific handlers. The grouping function supports both builder and closure syntax. - -All grouping methods return a `RoutesBuilder` instance, which means you can infinitely mix, match, and nest groups with other route building methods. - -::: tip - -Route groups can help you better organize your routes, but they are not required. - -::: - -### Path Prefix - -Path prefix routing groups allow you to add a prefix path before a routing group. - -```dart -final users = app.groupd(route: "/users"); - -// GET /users -users.get("/", (event) => ...); - -// POST /users -users.post("/", (event) => ...); - -// GET /users/:id -users.get("/:id", (event) => ...); -``` - -Any path component you can pass to helper methods such as `get`, `post` can be passed to `groupd`. -There is another syntax based on closures: - -```dart -app.group(route: "/users", (routes) { - // GET /users - routes.get("/", (event) => ...); - - // POST /users - routes.post("/", (event) => ...); - - // GET /users/:id - routes.get(":id", (event) => ...); -}); -``` - -Nested path prefixes allow you to define your CRUD API more concisely: - -```dart -app.group(route: "/users", (users) { - // GET /users - users.get('/', (event) => ...); - // POST /users - users.post('/', (event) => ...); - - users.group(path: ":id", (user) { - // GET /users/:id - user.get('/', (event) => ...); - // PUT /users/:id - user.put('/', (event) => ...); - // DELETE /users/:id - user.delete('/', (event) => ...); - }); -}); -``` - -### Handlers stack - -In addition to path prefixes, routing groups also allow you to add handlers to routing groups. - -```dart -app.get('fast-thing', (event) => ...); -app.group(uses: [SlowHandler()], (routes) { - routes.get('slow-thing', (event) => ...); -}); -``` - -This is particularly useful for protecting a subset of routes with different authentication handler. - -```dart -app.post('/login', (event) => ...); - -final auth = app.groupd(uses: [AuthHandler()]); - -auth.get('/profile', (event) => ...); -auth.get('/logout', (event) => ...); -``` diff --git a/docs/guide/websocket/hooks.md b/docs/guide/websocket/hooks.md deleted file mode 100644 index 95010e4..0000000 --- a/docs/guide/websocket/hooks.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: WebSocket → Hooks -description: Using WebSocket hooks API, you can define a WebSocket server that works across runtimes with same synax. ---- - -# Hooks - -Using WebSocket hooks API, you can define a WebSocket server that works across runtimes with same synax. - ---- - -Spry WebSocket provides a cross-platform API to define WebSocket servers. An implementation with these hooks works across runtimes without needing you to go into details of any of them (while you always have the power to control low-level hooks). You can only define the life-cycle hooks that you only need and only those will be called on runtime. - -::: warning -Spry WebSocket API is still under development and can change. -::: - -```dart -import 'package:spry/websocket.dart'; - -class MyHooks implements Hooks { - @override - FutureOr fallback(Event event) { - // If the platform does not support WebSocket or the upgrade fails, - // it will be called. - print('[ws] Not support.'); - return const Response(null, status: 426); - } - - @override - FutureOr onClose(Peer peer, {int? code, String? reason}) { - // Received a hook from a connected client or actively closed the websocket - // call on the server side. - print('[ws] close'); - } - - @override - FutureOr onError(Peer peer, error) { - // Hook for errors from the server side. - print('[ws] error: ${Error.safeToString(error)}'); - } - - @override - FutureOr onMessage(Peer peer, Message message) { - // Hook when receiving messages from connected clients. - final text = message.text(); - print('[ws] message: $text'); - - if (text.contains('ping')) { - peer.sendText('pong'); - } - } - - @override - FutureOr onUpgrade(Event event) { - // Called when upgrading request to WebSocket - return CreatePeerOptions(...); - } -} -``` diff --git a/docs/guide/websocket/introduction.md b/docs/guide/websocket/introduction.md deleted file mode 100644 index dce5e81..0000000 --- a/docs/guide/websocket/introduction.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: WebSocket → Introduction ---- - -# Introduction - -Writing a real-time WebSocket server that works across different WebSocket runtimes is challenging because there is no single standard for WebSocket servers. You usually need to learn a lot of details about different API implementations, which also makes switching from one runtime to another expensive. Spry WebSocket is the solution to this problem! - -## Basic using... - -```dart -import 'package:spry/websocket.dart'; - -app.ws('/chat/rooms/:id', chatRoomHooks); -``` - -## Platform runtime - -`package:spry/websocket.dart` exports a mixin for `WebSocketPlatform` that only works on `Platform`. - -If the platform implementation you are using does not implement 'WebSocketPlatform', then your app will not support WebSockets. - -## `app.ws` - -This is a method for extending the Spry app instance to register WebSocket [Hooks](/guide/websocket/hooks): - -```dart -class ChatHooks implements Hooks { - ... -} - -app.ws('chat', ChatHooks()); -``` diff --git a/docs/guide/websocket/message.md b/docs/guide/websocket/message.md deleted file mode 100644 index 76645e4..0000000 --- a/docs/guide/websocket/message.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: WebSocket → Peer ---- - -# Message - -On message [hook](/guide/websocket/hooks), you receive a message object containing an incoming message from the client. - -## `message.text()` - -Get stringified `String` version of the message. - -## `message.bytes()` - -Get stringified `Uint8List` version of the message. - -## `message.raw` - -Message raw data, Types: `Uint8List` or `String`. diff --git a/docs/guide/websocket/peer.md b/docs/guide/websocket/peer.md deleted file mode 100644 index f37d5d3..0000000 --- a/docs/guide/websocket/peer.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: WebSocket → Peer ---- - -# Peer - -Peer object allows easily interacting with connected clients. - ---- - -Websocket hooks accept a peer instance as their first argument. You can use peer object to get information about each connected client or send a message to them. - -## `peer.readyState` - -Client connection status (might be `-1`) - -::: tip -Read more is [readyState in MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) -::: - -## `peer.protocol` - -Returns the websocket selected protocol. - -## `peer.extensions` - -Returns the websocket cliend-side request extensions. - -## `peer.send` - -Send a bytes message to the connected client. - -## `peer.sendText` - -Send a `String` message to the connected client. - -## `peer.close` - -Close websocket connect. diff --git a/docs/platforms/create.md b/docs/platforms/create.md deleted file mode 100644 index 50976fa..0000000 --- a/docs/platforms/create.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Platforms → Create a new platform ---- - -# Platform - -Run Spry everywhere using platforms. - ---- - -The app instance of Spry is lightweight without any logic about runtime it is going to run. Using Spry `Platform`, we can easily integrate server with each runtime. - -There are 3 base platforms: - -* [**Plain**](/platforms/plain) -* [**IO(`dart:io`)**](/platforms/io) -* [**Web**](/platforms/web) - -## Create a new platform - -To create a new platform support, we only need to implement the `Platform` interface: - -```dart -typedef Input = ; -typedef Output = ; - -class MyPlatform extends Platform { - ... -} -``` - -## WebSocket support - -In general, WebSocket support is optional. If your platform supports WebSocket, you only need to `with WebSocketPlatform` on your platform implementation: - -```dart -class MyPlatform - extends Platform - with WebSocketPlatform -{ - FutureOr websocket(Event event, Input request, Hooks hooks) { - // Your platform upgrading websocket logic. - } -} -``` diff --git a/docs/platforms/io.md b/docs/platforms/io.md deleted file mode 100644 index 95efb38..0000000 --- a/docs/platforms/io.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Platforms → IO (dart:io) ---- - -# IO - -Natively run Spry app with `dart:io` HTTP server. - ---- - -To listen to `HttpServer` and enable Spry app, convert Spry app to `dart:io` HTTP server listener using `IOPlatform` platform. - -## Usage - -First, create an Spry app: - -::: code-group -```dart [app.dart] -import 'package:spry/spry.dart'; - -final Spry app = () { - final app = Spry(); - app.use((event) => 'hello world!'); - - return app; -}(); -``` -::: - -Create HTTP server entry: - -::: code-group -```dart [server.dart] -import 'package:spry/spry.dart'; - -void main() async { - final server = await HttpServer.bind('127.0.0.1', 3000); - final handler = const IOPlatform().createHandler(app); - - server.listen(handler); - - print('🚀 HTTP server listen on http://127.0.0.1:3000'); -} -``` -::: - -Now, you can run you Spry app natively with `dart:io`: - -```bash -dart run server.dart -``` - -## Compile to executable program - -The IO platform allows you to compile to binary executable programs using the `dart compile exe` command: - -```bash -dart compile exe server.dart -o server -``` - -Start the server: - -```bash -./server -# console: 🚀 HTTP server listen on http://127.0.0.1:3000 -``` diff --git a/docs/platforms/plain.md b/docs/platforms/plain.md deleted file mode 100644 index ef4db51..0000000 --- a/docs/platforms/plain.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Platforms → Plain ---- - -# Plain - -Run Spry app into any unknown runtime! - ---- - -Using plain adapter you can have an object input/output interface. - -::: tip -This can be also be particularly useful for testing your app or running inside lambda-like environments. -::: - -::: warning -Plain platform not support websocket. -::: - -## Usage - -First, create Spry app entry: - -::: code-group -```dart [app.dart] -import 'package:spry/spry.dart'; - -final Spry app = () { - final app = Spry(); - app.use((event) => 'hello world!'); - - return app; -}(); -``` -::: - -Create plain entry: - -::: code-group -```dart [plain.dart] -import 'package:spry/plain.dart'; -import 'app.dart'; - -final handler = const PlainPlatform().createHandler(app); -``` -::: - -## Local testing - -You can test platform using any runtime: - -::: code-group -```dart [plain_test.dart] -import 'package:spry/plain.dart'; -import 'package:test/test.dart'; -import 'plain.dart'; - -void main() { - test('Basic request', () async { - final request = PlainRequest(method: 'get', uri: Uri(path: '/')); - final response = await handler(request); - - expect(response.status, 200); - expect(response.headers.get('content-type'), contains('text/plain')); - expect(await response.text(), contains('hello world')); - }); -} -``` -::: - -The response example JSON (**This is not a real return, but just a visual demonstration of data**): - -```json -{ - status: 200, - statusText: "OK", - headers: { - "content-type": "text/plain; charset=utf8" - }, - body: "hello world!" -} -``` diff --git a/docs/platforms/web.md b/docs/platforms/web.md deleted file mode 100644 index b36d8eb..0000000 --- a/docs/platforms/web.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: Platforms → Web ---- - -# Web - -Run your Spry app in edge runtimes with Web API compatibility. - ---- - -In order to run Spry app in web compatible edge runtimes supporting [`fetch` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) with [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response), use `WebPlatform` platform to convert Spry app into a fetch-like function. - -## Pre dependencies - -* [`web`](https://pub.dev/packages/web): Run with `dart pub add web` install the [`web`](https://pub.dev/packages/web) package. - -## Usage - -First, create app entry: - -::: code-group -```dart [app.dart] -import 'package:spry/spry.dart'; - -final app = () { - final app = Spry(); - - app.use((event) => Response.text('Hello world!')); - - return app -}(); -``` -::: - -Create web entry: - -::: code-group -```dart [web.dart] -import 'package:spry/web.dart'; -import 'app.dart'; - -final handler = const WebPlatform().createHandler(app); -``` -::: - -## Local testing - -You can test using any compatible JavaScript runtime by passing a Request object. - -::: code-group -```dart [web_test.dart] -import 'package:web/web.dart'; -import 'web.dart'; - -void main() async { - final request = Request('http://localhost/'.toJS); - final response = await handler(request); - - print(await response.text().toDart); // Hello world! -} -``` -::: - -Compile to JavaScript file: - -```bash -dart compile js web_test.dart -o web_test.js -``` - -Run the `web_test.js`: - -::: code-group -```bash [Bun] -bun run web_test.js -``` - -```bash [Node.js] -node ./web_test.js -``` - -```html [Browser] - -``` -::: diff --git a/example/bun.dart b/example/bun.dart index 2258c82..c10d6cd 100644 --- a/example/bun.dart +++ b/example/bun.dart @@ -5,7 +5,7 @@ import 'package:spry/ws.dart'; void main() async { final app = createSpry(); - app.all('/**', (event) => Response.json({"a": 1})); + app.all('/**', (event) => 'Hello Spry!'); app.ws('/ws', defineHooks(message: (peer, message) { peer.send(message); })); diff --git a/example/io.dart b/example/io.dart index d5a912f..ca3f5f7 100644 --- a/example/io.dart +++ b/example/io.dart @@ -7,7 +7,7 @@ import 'package:spry/ws.dart'; void main() async { final app = createSpry(); - app.all('/**', (event) => getClientAddress(event)); + app.all('/**', (event) => 'Hello Spry!'); app.ws('/ws', defineHooks(message: (peer, message) { peer.send(message); })); diff --git a/lib/src/_constants.dart b/lib/src/_constants.dart index cd2f0d8..a0f8415 100644 --- a/lib/src/_constants.dart +++ b/lib/src/_constants.dart @@ -2,5 +2,4 @@ const kApp = #spry.app; const kRequest = #spry.event.request; const kNext = #spry.event.next; const kParams = #spry.event.params; -const kAllMethod = '#SPRY/__ALL__'; const kClientAddress = #spry.event.client_address; diff --git a/lib/src/create_spry.dart b/lib/src/create_spry.dart index 28b8756..d20c370 100644 --- a/lib/src/create_spry.dart +++ b/lib/src/create_spry.dart @@ -3,10 +3,10 @@ import 'package:routingkit/routingkit.dart'; import 'types.dart'; /// Creates a new [Spry] application. -Spry createSpry({RouterContext? router, Iterable? stack}) { +Spry createSpry({Router? router, Iterable? stack}) { return _SpryImpl( router: switch (router) { - RouterContext router => router, + Router router => router, _ => createRouter(), }, stack: [...?stack], @@ -17,14 +17,14 @@ class _SpryImpl implements Spry { const _SpryImpl({required this.router, required this.stack}); @override - final RouterContext router; + final Router router; @override final List stack; @override void on(String method, String path, Handler handler) { - addRoute(router, method, path, handler); + router.add(method, path, handler); } @override diff --git a/lib/src/routes+all.dart b/lib/src/routes+all.dart index 0596c62..c77beeb 100644 --- a/lib/src/routes+all.dart +++ b/lib/src/routes+all.dart @@ -1,10 +1,10 @@ // ignore_for_file: file_names -import '_constants.dart'; import 'types.dart'; /// The [all] method extension. extension RoutesAll on Routes { /// Adds a all request method route. - void all(String path, Handler handler) => on(kAllMethod, path, handler); + void all(String path, Handler handler) => + router.add(null, path, handler); } diff --git a/lib/src/to_handler.dart b/lib/src/to_handler.dart index 0e6ae04..9ed6039 100644 --- a/lib/src/to_handler.dart +++ b/lib/src/to_handler.dart @@ -21,10 +21,10 @@ Handler toHandler(Spry app) { ); } -Handler _createRouterHandler(RouterContext context) { +Handler _createRouterHandler(Router router) { return (event) { final request = useRequest(event); - final route = _lookup(context, request.method, request.uri.path); + final route = _lookup(router, request.method, request.uri.path); if (route == null) { return Response(null, status: 404); @@ -38,19 +38,19 @@ Handler _createRouterHandler(RouterContext context) { } MatchedRoute? _lookup( - RouterContext context, String method, String path) { - MatchedRoute? findLastRoute(String method) { - return findRoute(context, method, path)?.lastOrNull; + Router router, String method, String path) { + MatchedRoute? findRoute(String? method) { + return router.find(method, path); } return switch (method) { - 'HEAD' => switch (findLastRoute('HEAD')) { + 'HEAD' => switch (findRoute('HEAD')) { MatchedRoute route => route, - _ => _lookup(context, 'GET', path), + _ => _lookup(router, 'GET', path), }, - String method => switch (findLastRoute(method)) { + String method => switch (findRoute(method)) { MatchedRoute route => route, - _ => findLastRoute(kAllMethod), + _ => findRoute(null), }, }; } diff --git a/lib/src/types.dart b/lib/src/types.dart index db1e1fa..20c1eaa 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:meta/meta.dart'; import 'package:routingkit/routingkit.dart'; /// Request event interface. @@ -19,17 +20,20 @@ abstract interface class Event { /// Spry handler. typedef Handler = FutureOr Function(Event event); +/// Spry Routes abstract interface class Routes { + /// The [RouterContext] bound to the current [Spry] application + @internal + Router get router; + /// Adds a handler on match [method] and [path]. void on(String method, String path, Handler handler); } /// Spry application. abstract interface class Spry implements Routes { - /// The [RouterContext] bound to the current [Spry] application - RouterContext get router; - /// Stack handler in Spry application. + @internal List get stack; /// Adds a [Handler] into [stack]. diff --git a/lib/src/use_params.dart b/lib/src/use_params.dart index e975860..e8963c9 100644 --- a/lib/src/use_params.dart +++ b/lib/src/use_params.dart @@ -7,19 +7,6 @@ import 'types.dart'; Params useParams(Event event) { return switch (event.get(kParams)) { Params params => params, - _ => const _EmptyParams(), + _ => const {} as Params, }; } - -class _EmptyParams implements Params { - const _EmptyParams(); - - @override - String? get catchall => null; - - @override - String? get(String name) => null; - - @override - Iterable get unnamed => const []; -} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8aa4417 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "private": true, + "scripts": { + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs" + }, + "dependencies": { + "vitepress": "^1.3.1" + } +} diff --git a/pubspec.yaml b/pubspec.yaml index ec35518..1e88f1e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: spry description: Spry is a lightweight, composable Dart web framework designed to work collaboratively with various runtime platforms. -version: 5.0.0-bata.1 +version: 5.0.0 homepage: https://spry.fun repository: https://github.com/medz/spry @@ -10,7 +10,8 @@ environment: dependencies: crypto: ^3.0.3 http_parser: ^4.1.0 - routingkit: ^2.1.0 + meta: ^1.15.0 + routingkit: ^3.0.3 web: ^1.0.0 dev_dependencies: