├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── Cargo.toml ├── README.md ├── examples ├── .gitignore ├── Cargo.toml ├── README.md └── rocket-svelte │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── public │ └── .gitkeep │ ├── src │ └── main.rs │ ├── svelte-app │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── Pages │ │ │ └── Hello.svelte │ │ ├── global.css │ │ └── main.js │ └── webpack.config.js │ └── templates │ └── app.html.hbs └── src ├── lib.rs └── rocket.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | 34 | fmt: 35 | name: Rustfmt 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | profile: minimal 42 | toolchain: stable 43 | override: true 44 | - run: rustup component add rustfmt 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: fmt 48 | args: --all -- --check 49 | 50 | clippy: 51 | name: Clippy 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | profile: minimal 58 | toolchain: stable 59 | override: true 60 | - run: rustup component add clippy 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: clippy 64 | args: -- -D warnings 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "inertia_rs" 3 | version = "0.2.0" 4 | edition = "2021" 5 | license = "MIT" 6 | homepage = "https://github.com/stuarth/inertia-rs" 7 | repository = "https://github.com/stuarth/inertia-rs" 8 | description = "Inertia.js for Rust" 9 | keywords = ["inertia", "web", "framework", "rocket"] 10 | categories = ["web-programming"] 11 | readme = "README.md" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | rocket = { version = "0.5", features = ["json"], optional = true } 17 | serde = { version = "1", features = ["derive"] } 18 | serde_json = "1.0" 19 | tracing = "0.1" 20 | 21 | [features] 22 | default = ["rocket"] 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inertia.rs 2 | 3 | [![Current Crates.io Version](https://img.shields.io/crates/v/inertia-rs)](https://crates.io/crates/inertia_rs) 4 | [![Build Status](https://github.com/stuarth/inertia-rs/workflows/CI/badge.svg)](https://github.com/stuarth/inertia-rs/actions) 5 | [![docs.rs](https://img.shields.io/badge/docs-latest-blue.svg?style=flat)](https://docs.rs/inertia_rs/) 6 | 7 | [Inertia.js](https://inertiajs.com/) implementations for Rust. Currently supports [Rocket](https://rocket.rs/). 8 | 9 | ## Why Inertia? 10 | 11 | From [inertiajs.com](https://inertiajs.com/) 12 | 13 | > Inertia is a new approach to building classic server-driven web apps. We call it the modern monolith. 14 | > 15 | > Inertia allows you to create fully client-side rendered, single-page apps, without much of the complexity that comes with modern SPAs. It does this by leveraging existing server-side frameworks. 16 | > 17 | > Inertia has no client-side routing, nor does it require an API. Simply build controllers and page views like you've always done! 18 | 19 | Inertia.rs brings a straightforward integration to Rust. 20 | 21 | ## Installation 22 | 23 | Add the following line to your `Cargo.toml` 24 | ```toml 25 | inertia_rs = { version = "0.2.0", features = ["rocket"] } 26 | ``` 27 | 28 | ## Usage 29 | 30 | `inertia_rs` defines a succinct interface for creating Inertia.js apps in Rocket. It's comprised of two elements, 31 | 32 | * **`Inertia`** 33 | 34 | a [Responder](https://api.rocket.rs/v0.5-rc/rocket/response/trait.Responder.html) that's generic over `T`, the Inertia component's properties 35 | 36 | * **`VersionFairing`** 37 | 38 | Responsible for asset version checks. Constructed via `VersionFairing::new`, which is given the asset version and a closure responsible for generating the Inertia's HTML template. 39 | 40 | ### Sample Rocket Server 41 | 42 | ```rust 43 | #[macro_use] 44 | extern crate rocket; 45 | 46 | use inertia_rs::rocket::{Inertia, VersionFairing}; 47 | use rocket::response::Responder; 48 | use rocket_dyn_templates::Template; 49 | 50 | #[derive(serde::Serialize)] 51 | struct Hello { 52 | some_property: String, 53 | } 54 | 55 | #[get("/hello")] 56 | fn hello() -> Inertia { 57 | Inertia::response( 58 | // the component to render 59 | "hello", 60 | // the props to pass our component 61 | Hello { some_property: "hello world!".into() }, 62 | ) 63 | } 64 | 65 | #[launch] 66 | fn rocket() -> _ { 67 | rocket::build() 68 | .mount("/", routes![hello]) 69 | .attach(Template::fairing()) 70 | // Version fairing is configured with current asset version, and a 71 | // closure to generate the html template response 72 | // `ctx` contains `data_page`, a json-serialized string of 73 | // the inertia props 74 | .attach(VersionFairing::new("1", |request, ctx| { 75 | Template::render("app", ctx).respond_to(request) 76 | })) 77 | } 78 | 79 | ``` -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "rocket-svelte" 4 | ] -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | * **[`rocket-svelte`](./rocket-svelte)** 4 | 5 | Demonstration of Rocket + Svelte. See the project's README for details -------------------------------------------------------------------------------- /examples/rocket-svelte/.gitignore: -------------------------------------------------------------------------------- 1 | public/build/bundle.* -------------------------------------------------------------------------------- /examples/rocket-svelte/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rocket-svelte" 3 | version = "0.1.0" 4 | edition = "2018" 5 | workspace = "../" 6 | publish = false 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | inertia_rs = { path = "../..", features = ["rocket"] } 12 | rocket = { version = "0.5.0-rc.1", features = ["json"] } 13 | serde = { version = "1", features = ["derive"] } 14 | 15 | [dependencies.rocket_dyn_templates] 16 | version = "0.1.0-rc.1" 17 | features = ["handlebars"] -------------------------------------------------------------------------------- /examples/rocket-svelte/README.md: -------------------------------------------------------------------------------- 1 | # Rocket Svelte 2 | 3 | ## Build JS Assets 4 | 5 | `npm run build` 6 | 7 | ## Start the Server 8 | 9 | `cargo run` -------------------------------------------------------------------------------- /examples/rocket-svelte/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stuarth/inertia-rs/2f8f38cc795c06ee11b93297ac4f8e4e43482569/examples/rocket-svelte/public/.gitkeep -------------------------------------------------------------------------------- /examples/rocket-svelte/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rocket; 3 | 4 | use inertia_rs::{rocket::VersionFairing, Inertia}; 5 | use rocket::fs::FileServer; 6 | use rocket::response::Responder; 7 | use rocket_dyn_templates::Template; 8 | 9 | #[derive(serde::Serialize)] 10 | struct Hello { 11 | name: String, 12 | } 13 | 14 | #[get("/hello")] 15 | fn hello() -> Inertia { 16 | Inertia::response( 17 | // the component to render 18 | "Hello", 19 | // the props to pass our component 20 | Hello { 21 | name: "world".into(), 22 | }, 23 | ) 24 | } 25 | 26 | #[launch] 27 | fn rocket() -> _ { 28 | rocket::build() 29 | .mount("/", routes![hello]) 30 | .attach(Template::fairing()) 31 | .mount("/public", FileServer::from(rocket::fs::relative!("public"))) 32 | // Version fairing is configured with current asset version, and a 33 | // closure to generate the html template response 34 | .attach(VersionFairing::new("1", |request, ctx| { 35 | Template::render("app", ctx).respond_to(request) 36 | })) 37 | } 38 | -------------------------------------------------------------------------------- /examples/rocket-svelte/svelte-app/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /examples/rocket-svelte/svelte-app/README.md: -------------------------------------------------------------------------------- 1 | # svelte app 2 | 3 | This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template-webpack. 4 | 5 | To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit): 6 | 7 | ```bash 8 | npx degit sveltejs/template-webpack svelte-app 9 | cd svelte-app 10 | ``` 11 | 12 | *Note that you will need to have [Node.js](https://nodejs.org) installed.* 13 | 14 | 15 | ## Get started 16 | 17 | Install the dependencies... 18 | 19 | ```bash 20 | cd svelte-app 21 | npm install 22 | ``` 23 | 24 | ...then start webpack: 25 | 26 | ```bash 27 | npm run dev 28 | ``` 29 | 30 | Navigate to [localhost:8080](http://localhost:8080). You should see your app running. Edit a component file in `src`, save it, and the page should reload with your changes. 31 | 32 | 33 | ## Deploying to the web 34 | 35 | ### With [now](https://zeit.co/now) 36 | 37 | Install `now` if you haven't already: 38 | 39 | ```bash 40 | npm install -g now 41 | ``` 42 | 43 | Then, from within your project folder: 44 | 45 | ```bash 46 | now 47 | ``` 48 | 49 | As an alternative, use the [Now desktop client](https://zeit.co/download) and simply drag the unzipped project folder to the taskbar icon. 50 | 51 | ### With [surge](https://surge.sh/) 52 | 53 | Install `surge` if you haven't already: 54 | 55 | ```bash 56 | npm install -g surge 57 | ``` 58 | 59 | Then, from within your project folder: 60 | 61 | ```bash 62 | npm run build 63 | surge public 64 | ``` 65 | -------------------------------------------------------------------------------- /examples/rocket-svelte/svelte-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "cross-env": "^7.0.3", 6 | "css-loader": "^5.0.1", 7 | "mini-css-extract-plugin": "^1.3.4", 8 | "svelte": "^3.31.2", 9 | "svelte-loader": "^3.0.0", 10 | "webpack": "^5.16.0", 11 | "webpack-cli": "^4.7.2", 12 | "webpack-dev-server": "^3.11.2" 13 | }, 14 | "scripts": { 15 | "build": "cross-env NODE_ENV=production webpack", 16 | "dev": "webpack serve --content-base public" 17 | }, 18 | "dependencies": { 19 | "@inertiajs/inertia": "^0.9.4", 20 | "@inertiajs/inertia-svelte": "^0.7.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/rocket-svelte/svelte-app/src/Pages/Hello.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

Hello {name}!!

7 |
-------------------------------------------------------------------------------- /examples/rocket-svelte/svelte-app/src/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | body { 8 | color: #333; 9 | margin: 0; 10 | padding: 8px; 11 | box-sizing: border-box; 12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 13 | } 14 | 15 | a { 16 | color: rgb(0,100,200); 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | a:visited { 25 | color: rgb(0,80,160); 26 | } 27 | 28 | label { 29 | display: block; 30 | } 31 | 32 | input, button, select, textarea { 33 | font-family: inherit; 34 | font-size: inherit; 35 | -webkit-padding: 0.4em 0; 36 | padding: 0.4em; 37 | margin: 0 0 0.5em 0; 38 | box-sizing: border-box; 39 | border: 1px solid #ccc; 40 | border-radius: 2px; 41 | } 42 | 43 | input:disabled { 44 | color: #ccc; 45 | } 46 | 47 | button { 48 | color: #333; 49 | background-color: #f4f4f4; 50 | outline: none; 51 | } 52 | 53 | button:disabled { 54 | color: #999; 55 | } 56 | 57 | button:not(:disabled):active { 58 | background-color: #ddd; 59 | } 60 | 61 | button:focus { 62 | border-color: #666; 63 | } 64 | -------------------------------------------------------------------------------- /examples/rocket-svelte/svelte-app/src/main.js: -------------------------------------------------------------------------------- 1 | import { createInertiaApp } from '@inertiajs/inertia-svelte' 2 | 3 | createInertiaApp({ 4 | resolve: name => require(`./Pages/${name}.svelte`), 5 | setup({ el, App, props }) { 6 | new App({ target: el, props }) 7 | }, 8 | }) -------------------------------------------------------------------------------- /examples/rocket-svelte/svelte-app/webpack.config.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 2 | const path = require('path'); 3 | 4 | const mode = process.env.NODE_ENV || 'development'; 5 | const prod = mode === 'production'; 6 | 7 | module.exports = { 8 | entry: { 9 | 'build/bundle': ['./src/main.js'] 10 | }, 11 | resolve: { 12 | alias: { 13 | svelte: path.dirname(require.resolve('svelte/package.json')) 14 | }, 15 | extensions: ['.mjs', '.js', '.svelte'], 16 | mainFields: ['svelte', 'browser', 'module', 'main'] 17 | }, 18 | output: { 19 | path: path.join(__dirname, '../public'), 20 | filename: '[name].js', 21 | chunkFilename: '[name].[id].js' 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.svelte$/, 27 | use: { 28 | loader: 'svelte-loader', 29 | options: { 30 | compilerOptions: { 31 | dev: !prod 32 | }, 33 | emitCss: prod, 34 | hotReload: !prod 35 | } 36 | } 37 | }, 38 | { 39 | test: /\.css$/, 40 | use: [ 41 | MiniCssExtractPlugin.loader, 42 | 'css-loader' 43 | ] 44 | }, 45 | { 46 | // required to prevent errors from Svelte on Webpack 5+ 47 | test: /node_modules\/svelte\/.*\.mjs$/, 48 | resolve: { 49 | fullySpecified: false 50 | } 51 | } 52 | ] 53 | }, 54 | mode, 55 | plugins: [ 56 | new MiniCssExtractPlugin({ 57 | filename: '[name].css' 58 | }) 59 | ], 60 | devtool: prod ? false : 'source-map', 61 | devServer: { 62 | hot: true 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /examples/rocket-svelte/templates/app.html.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "rocket")] 2 | pub mod rocket; 3 | 4 | pub(crate) static X_INERTIA: &str = "X-Inertia"; 5 | pub(crate) static X_INERTIA_VERSION: &str = "X-Inertia-Version"; 6 | pub(crate) static X_INERTIA_LOCATION: &str = "X-Inertia-Location"; 7 | 8 | pub struct Inertia { 9 | component: String, 10 | props: T, 11 | url: Option, 12 | } 13 | -------------------------------------------------------------------------------- /src/rocket.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::type_complexity)] 2 | 3 | use super::{Inertia, X_INERTIA, X_INERTIA_LOCATION, X_INERTIA_VERSION}; 4 | use rocket::fairing::{Fairing, Info, Kind}; 5 | use rocket::http::{self, Method}; 6 | use rocket::request::Request; 7 | use rocket::response::{self, Responder, Response}; 8 | use rocket::serde::json::Json; 9 | use rocket::Data; 10 | use rocket::{error, get, routes, uri}; 11 | use serde::Serialize; 12 | use std::sync::Arc; 13 | use tracing::trace; 14 | 15 | #[derive(Serialize)] 16 | struct InertiaResponse { 17 | component: String, 18 | props: T, 19 | url: String, 20 | version: Option, 21 | } 22 | 23 | const BASE_ROUTE: &str = "/inertia-rs"; 24 | 25 | trait InertiaRequest { 26 | fn inertia_request(&self) -> bool; 27 | 28 | fn inertia_version(&self) -> Option; 29 | } 30 | 31 | impl<'a> InertiaRequest for Request<'a> { 32 | fn inertia_request(&self) -> bool { 33 | self.headers().get_one(X_INERTIA).is_some() 34 | } 35 | 36 | fn inertia_version(&self) -> Option { 37 | self.headers().get_one(X_INERTIA_VERSION).map(|s| s.into()) 38 | } 39 | } 40 | 41 | #[derive(Serialize)] 42 | pub struct HtmlResponseContext { 43 | data_page: String, 44 | } 45 | 46 | #[derive(Serialize, Clone)] 47 | struct InertiaVersion(String); 48 | 49 | impl AsRef for InertiaVersion { 50 | fn as_ref(&self) -> &str { 51 | self.0.as_ref() 52 | } 53 | } 54 | 55 | impl<'r, 'o: 'r, R: Serialize> Responder<'r, 'o> for Inertia { 56 | #[inline(always)] 57 | fn respond_to(self, request: &'r Request<'_>) -> response::Result<'o> { 58 | // todo: not right, needs query 59 | let url = self.url.unwrap_or_else(|| request.uri().path().to_string()); 60 | let version = request.local_cache(|| None); 61 | 62 | let inertia_response = InertiaResponse { 63 | component: self.component, 64 | props: self.props, 65 | url, 66 | version: version.clone(), 67 | }; 68 | 69 | if request.inertia_request() { 70 | Response::build() 71 | .merge(Json(inertia_response).respond_to(request)?) 72 | .raw_header(X_INERTIA, "true") 73 | .ok() 74 | } else { 75 | let ctx = HtmlResponseContext { 76 | data_page: serde_json::to_string(&inertia_response) 77 | .map_err(|_e| http::Status::InternalServerError)?, 78 | }; 79 | 80 | match request.rocket().state::() { 81 | Some(f) => f.0(request, &ctx), 82 | None => { 83 | error!("Responder not found"); 84 | http::Status::InternalServerError.respond_to(request) 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | impl Inertia { 92 | /// Construct a response for the given component and props. Defaults to 93 | /// the request's url. 94 | pub fn response>(component: C, props: T) -> Self { 95 | Self { 96 | component: component.into(), 97 | props, 98 | url: None, 99 | } 100 | } 101 | 102 | /// Specify the url. Defaults to request's 103 | pub fn with_url>(mut self, url: U) -> Self { 104 | self.url = Some(url.into()); 105 | self 106 | } 107 | } 108 | 109 | pub struct VersionFairing<'resp> { 110 | version: String, 111 | html_response: 112 | Arc, &HtmlResponseContext) -> response::Result<'resp> + Send + Sync>, 113 | } 114 | 115 | impl<'resp> VersionFairing<'resp> { 116 | pub fn new<'a, 'b, F, V: Into>(version: V, html_response: F) -> Self 117 | where 118 | F: Fn(&Request<'_>, &HtmlResponseContext) -> response::Result<'resp> 119 | + Send 120 | + Sync 121 | + 'static, 122 | { 123 | Self { 124 | version: version.into(), 125 | html_response: Arc::new(html_response), 126 | } 127 | } 128 | } 129 | 130 | struct VersionConflictResponse(String); 131 | 132 | impl<'r, 'o: 'r> Responder<'r, 'o> for VersionConflictResponse { 133 | #[inline(always)] 134 | fn respond_to(self, _request: &'r Request<'_>) -> response::Result<'o> { 135 | Response::build() 136 | .status(http::Status::Conflict) 137 | .raw_header(X_INERTIA_LOCATION, self.0) 138 | .ok() 139 | } 140 | } 141 | 142 | #[get("/version-conflict?")] 143 | fn version_conflict(location: String) -> VersionConflictResponse { 144 | VersionConflictResponse(location) 145 | } 146 | 147 | struct ResponderFn<'resp>( 148 | Arc, &HtmlResponseContext) -> response::Result<'resp> + Send + Sync>, 149 | ); 150 | 151 | #[rocket::async_trait] 152 | impl Fairing for VersionFairing<'static> { 153 | fn info(&self) -> Info { 154 | Info { 155 | name: "Inertia Asset Versioning", 156 | kind: Kind::Ignite | Kind::Request, 157 | } 158 | } 159 | 160 | async fn on_ignite(&self, rocket: rocket::Rocket) -> rocket::fairing::Result { 161 | Ok(rocket 162 | .mount(BASE_ROUTE, routes![version_conflict]) 163 | .manage(ResponderFn(self.html_response.clone()))) 164 | } 165 | 166 | async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) { 167 | if request.method() == Method::Get && request.inertia_request() { 168 | let request_version = request.inertia_version(); 169 | 170 | trace!( 171 | "request version {:?} / asset version {}", 172 | &request_version, 173 | &self.version 174 | ); 175 | 176 | if request_version.as_ref() != Some(&self.version) { 177 | let uri = uri!( 178 | "/inertia-rs", 179 | version_conflict(location = request.uri().path().as_str().to_owned()) 180 | ); 181 | 182 | trace!("\tredirecting to {}", &uri.to_string()); 183 | 184 | request.set_uri(uri); 185 | } 186 | } 187 | } 188 | } 189 | 190 | #[cfg(test)] 191 | mod tests { 192 | use super::*; 193 | use rocket::{ 194 | http::{Header, Status}, 195 | local::blocking::Client, 196 | }; 197 | 198 | #[derive(Serialize)] 199 | struct Props { 200 | n: i32, 201 | } 202 | 203 | #[get("/foo")] 204 | fn foo() -> Inertia { 205 | Inertia::response("foo", Props { n: 42 }) 206 | } 207 | 208 | const CURRENT_VERSION: &str = "1"; 209 | 210 | fn rocket() -> rocket::Rocket { 211 | rocket::build() 212 | .mount("/", routes![foo]) 213 | .attach(VersionFairing::new(CURRENT_VERSION, |request, ctx| { 214 | serde_json::to_string(ctx).unwrap().respond_to(request) 215 | })) 216 | } 217 | 218 | #[test] 219 | fn html_response_sent() { 220 | let client = Client::tracked(rocket()).unwrap(); 221 | 222 | // no X-Inertia header should fall back to the response closure 223 | let req = client.get("/foo"); 224 | 225 | let resp = req.dispatch(); 226 | let headers = resp.headers(); 227 | 228 | assert_eq!(resp.status(), Status::Ok); 229 | assert_eq!( 230 | headers.get_one("Content-Type"), 231 | Some("text/plain; charset=utf-8") 232 | ); 233 | } 234 | 235 | #[test] 236 | fn json_sent_versions_eq() { 237 | let client = Client::tracked(rocket()).unwrap(); 238 | 239 | let req = client 240 | .get("/foo") 241 | .header(Header::new(X_INERTIA, "true")) 242 | .header(Header::new(X_INERTIA_VERSION, CURRENT_VERSION)); 243 | 244 | let resp = req.dispatch(); 245 | let headers = resp.headers(); 246 | 247 | assert_eq!(resp.status(), Status::Ok); 248 | assert_eq!(headers.get_one("Content-Type"), Some("application/json")); 249 | } 250 | 251 | #[test] 252 | fn json_sent_versions_different() { 253 | let client = Client::tracked(rocket()).unwrap(); 254 | 255 | let req = client 256 | .get("/foo") 257 | .header(Header::new(X_INERTIA, "true")) 258 | .header(Header::new(X_INERTIA_VERSION, "OUTDATED_VERSION")); 259 | 260 | let resp = req.dispatch(); 261 | 262 | assert_eq!(resp.status(), Status::Conflict); 263 | } 264 | 265 | #[test] 266 | fn json_sent_version_absent() { 267 | let client = Client::tracked(rocket()).unwrap(); 268 | 269 | let req = client.get("/foo").header(Header::new(X_INERTIA, "true")); 270 | 271 | let resp = req.dispatch(); 272 | 273 | assert_eq!(resp.status(), Status::Conflict); 274 | } 275 | 276 | #[test] 277 | fn not_found_response() { 278 | let client = Client::tracked(rocket()).unwrap(); 279 | 280 | let req = client 281 | .get("/not/a/real/path") 282 | .header(Header::new(X_INERTIA, "true")) 283 | .header(Header::new(X_INERTIA_VERSION, CURRENT_VERSION)); 284 | 285 | let resp = req.dispatch(); 286 | 287 | assert_eq!(resp.status(), Status::NotFound); 288 | } 289 | } 290 | --------------------------------------------------------------------------------