├── .gitignore ├── project-examples └── realworld │ ├── .gitignore │ ├── .gitattributes │ ├── diesel.toml │ ├── .env.sample │ ├── migrations │ ├── 2019-07-28-121610_create_db │ │ ├── down.sql │ │ └── up.sql │ └── 00000000000000_diesel_initial_setup │ │ ├── down.sql │ │ └── up.sql │ ├── app │ ├── tokens.rs │ ├── models.rs │ ├── routes │ │ ├── posts.rs │ │ ├── tags.rs │ │ ├── articles.rs │ │ └── users.rs │ ├── routes.rs │ ├── context.rs │ ├── schema.rs │ └── main.rs │ └── Cargo.toml ├── lib ├── nails │ ├── src │ │ ├── utils.rs │ │ ├── lib.rs │ │ ├── __rt.rs │ │ ├── utils │ │ │ ├── hyper_ext.rs │ │ │ └── tokio02_ext.rs │ │ ├── routing.rs │ │ ├── service.rs │ │ ├── request.rs │ │ └── error.rs │ └── Cargo.toml ├── nails_derive │ ├── tests │ │ ├── ui │ │ │ ├── from-request-missing-path.rs │ │ │ ├── from-request-missing-path-names.rs │ │ │ ├── from-request-enum.rs │ │ │ ├── from-request-missing-path.stderr │ │ │ ├── from-request-position-field-missing-kinds.rs │ │ │ ├── from-request-position-field-queries.rs │ │ │ ├── from-request-enum.stderr │ │ │ ├── from-request-position-field-queries.stderr │ │ │ ├── from-request-position-field-missing-kinds.stderr │ │ │ ├── from-request-missing-path-names.stderr │ │ │ ├── from-request-double-paths.rs │ │ │ ├── from-request-unknown-attr.rs │ │ │ ├── from-request-non-string-paths.rs │ │ │ ├── from-request-duplicate-path-names.stderr │ │ │ ├── from-request-non-captured-path-name.rs │ │ │ ├── from-request-unknown-attr.stderr │ │ │ ├── from-request-non-captured-path-name.stderr │ │ │ ├── from-request-duplicate-path-names.rs │ │ │ ├── from-request-double-paths.stderr │ │ │ ├── from-request-non-string-queries.rs │ │ │ ├── from-request-non-string-paths.stderr │ │ │ ├── from-request-double-queries.rs │ │ │ ├── from-request-non-string-queries.stderr │ │ │ └── from-request-double-queries.stderr │ │ └── ui.rs │ ├── Cargo.toml │ └── src │ │ ├── test_utils.rs │ │ ├── utils.rs │ │ ├── attrs.rs │ │ ├── path.rs │ │ └── lib.rs └── contextful │ ├── Cargo.toml │ └── src │ └── lib.rs └── Cargo.toml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /project-examples/realworld/.gitignore: -------------------------------------------------------------------------------- 1 | /.env 2 | -------------------------------------------------------------------------------- /lib/nails/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub mod hyper_ext; 2 | pub mod tokio02_ext; 3 | -------------------------------------------------------------------------------- /project-examples/realworld/.gitattributes: -------------------------------------------------------------------------------- 1 | /app/schema.rs linguist-generated 2 | -------------------------------------------------------------------------------- /project-examples/realworld/diesel.toml: -------------------------------------------------------------------------------- 1 | [print_schema] 2 | file = "app/schema.rs" 3 | -------------------------------------------------------------------------------- /project-examples/realworld/.env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres:///realworld 2 | SECRET_KEY=secret 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "project-examples/realworld", 4 | "lib/contextful", 5 | "lib/nails", 6 | "lib/nails_derive", 7 | ] 8 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-missing-path.rs: -------------------------------------------------------------------------------- 1 | use nails_derive::Preroute; 2 | 3 | #[derive(Preroute)] 4 | pub struct GetPostRequest {} 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /lib/contextful/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "contextful" 3 | version = "0.1.0" 4 | authors = ["Masaki Hara "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-missing-path-names.rs: -------------------------------------------------------------------------------- 1 | use nails_derive::Preroute; 2 | 3 | #[derive(Preroute)] 4 | #[nails(path = "/api/posts/{id}")] 5 | pub struct GetPostRequest; 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-enum.rs: -------------------------------------------------------------------------------- 1 | use nails_derive::Preroute; 2 | 3 | #[derive(Preroute)] 4 | #[nails(path = "/api/posts/{id}")] 5 | pub enum GetPostRequest { 6 | Foo {} 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-missing-path.stderr: -------------------------------------------------------------------------------- 1 | error: #[nails(path)] is needed 2 | --> $DIR/from-request-missing-path.rs:4:1 3 | | 4 | 4 | pub struct GetPostRequest {} 5 | | ^^^ 6 | 7 | error: could not compile `nails_derive-tests`. 8 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-position-field-missing-kinds.rs: -------------------------------------------------------------------------------- 1 | use nails_derive::Preroute; 2 | 3 | #[derive(Preroute)] 4 | #[nails(path = "/api/posts/{id}")] 5 | pub struct GetPostRequest( 6 | String, 7 | ); 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /project-examples/realworld/migrations/2019-07-28-121610_create_db/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE article_tags; 2 | DROP TABLE comments; 3 | DROP TABLE favorited_articles; 4 | DROP TABLE articles; 5 | DROP TABLE tags; 6 | DROP TABLE followings; 7 | DROP TABLE users; 8 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-position-field-queries.rs: -------------------------------------------------------------------------------- 1 | use nails_derive::Preroute; 2 | 3 | #[derive(Preroute)] 4 | #[nails(path = "/api/posts/{id}")] 5 | pub struct GetPostRequest( 6 | #[nails(query)] 7 | String, 8 | ); 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-enum.stderr: -------------------------------------------------------------------------------- 1 | error: Preroute cannot be derived for enums or unions 2 | --> $DIR/from-request-enum.rs:4:1 3 | | 4 | 4 | #[nails(path = "/api/posts/{id}")] 5 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | 7 | error: could not compile `nails_derive-tests`. 8 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-position-field-queries.stderr: -------------------------------------------------------------------------------- 1 | error: Specify name with #[nails(query = "")] 2 | --> $DIR/from-request-position-field-queries.rs:6:13 3 | | 4 | 6 | #[nails(query)] 5 | | ^^^^^ 6 | 7 | error: could not compile `nails_derive-tests`. 8 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-position-field-missing-kinds.stderr: -------------------------------------------------------------------------------- 1 | error: Specify name with #[nails(query = "")] or alike 2 | --> $DIR/from-request-position-field-missing-kinds.rs:6:5 3 | | 4 | 6 | String, 5 | | ^^^^^^ 6 | 7 | error: could not compile `nails_derive-tests`. 8 | -------------------------------------------------------------------------------- /lib/nails/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate self as nails; 2 | 3 | pub use request::Preroute; 4 | pub use routing::{Routable, Router}; 5 | pub use service::Service; 6 | 7 | #[doc(hidden)] 8 | pub mod __rt; 9 | pub mod error; 10 | pub mod request; 11 | pub mod routing; 12 | pub mod service; 13 | pub mod utils; 14 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-missing-path-names.stderr: -------------------------------------------------------------------------------- 1 | error: Missing field for binding name from {id} 2 | --> $DIR/from-request-missing-path-names.rs:4:16 3 | | 4 | 4 | #[nails(path = "/api/posts/{id}")] 5 | | ^^^^^^^^^^^^^^^^^ 6 | 7 | error: could not compile `nails_derive-tests`. 8 | -------------------------------------------------------------------------------- /lib/nails/src/__rt.rs: -------------------------------------------------------------------------------- 1 | use futures::prelude::*; 2 | 3 | pub use crate::error::NailsError; 4 | pub use crate::request::{parse_query, FromBody, FromPath, FromQuery, Preroute}; 5 | pub use futures::future::BoxFuture; 6 | pub use hyper::{Body, Method, Request}; 7 | 8 | pub fn box_future<'a, T: Future + Send + 'a>(x: T) -> BoxFuture<'a, T::Output> { 9 | x.boxed() 10 | } 11 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-double-paths.rs: -------------------------------------------------------------------------------- 1 | use nails_derive::Preroute; 2 | 3 | #[derive(Preroute)] 4 | #[nails(path = "/api/posts/{id}")] 5 | #[nails(path = "/api/posts/{idd}")] 6 | pub struct GetPostRequest {} 7 | 8 | #[derive(Preroute)] 9 | #[nails(path = "/api/posts/{id}", path = "/api/posts/{idd}")] 10 | pub struct GetPostRequest2 {} 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-unknown-attr.rs: -------------------------------------------------------------------------------- 1 | use nails_derive::Preroute; 2 | 3 | #[derive(Preroute)] 4 | #[nails(path = "/api/posts/{id}", foo)] 5 | pub struct GetPostRequest {} 6 | 7 | #[derive(Preroute)] 8 | #[nails(path = "/api/posts/{id}")] 9 | pub struct GetPostRequest2 { 10 | #[nails(query, foo)] 11 | query1: String, 12 | } 13 | 14 | fn main() {} 15 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-non-string-paths.rs: -------------------------------------------------------------------------------- 1 | use nails_derive::Preroute; 2 | 3 | #[derive(Preroute)] 4 | #[nails(path = 42)] 5 | pub struct GetPostRequest {} 6 | 7 | #[derive(Preroute)] 8 | #[nails(path = b"/api/posts/{id}")] 9 | pub struct GetPostRequest2 {} 10 | 11 | #[derive(Preroute)] 12 | #[nails(path)] 13 | pub struct GetPostRequest3 {} 14 | 15 | fn main() {} 16 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-duplicate-path-names.stderr: -------------------------------------------------------------------------------- 1 | error: Duplicate path names 2 | --> $DIR/from-request-duplicate-path-names.rs:8:13 3 | | 4 | 8 | #[nails(path = "id")] 5 | | ^^^^ 6 | 7 | error: Duplicate path names 8 | --> $DIR/from-request-duplicate-path-names.rs:17:5 9 | | 10 | 17 | id: String, 11 | | ^^ 12 | 13 | error: could not compile `nails_derive-tests`. 14 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-non-captured-path-name.rs: -------------------------------------------------------------------------------- 1 | use nails_derive::Preroute; 2 | 3 | #[derive(Preroute)] 4 | #[nails(path = "/api/posts/{id}")] 5 | struct GetPostRequest { 6 | #[nails(path = "idd")] 7 | id: String, 8 | } 9 | 10 | #[derive(Preroute)] 11 | #[nails(path = "/api/posts/{id}")] 12 | struct GetPostRequest2 { 13 | #[nails(path)] 14 | idd: String, 15 | } 16 | 17 | fn main() {} 18 | -------------------------------------------------------------------------------- /project-examples/realworld/migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /lib/nails_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nails_derive" 3 | version = "0.1.0" 4 | authors = ["Masaki Hara "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | quote = "1.0.2" 9 | syn = "1.0.4" 10 | synstructure = "0.12.1" 11 | proc-macro2 = "1.0.1" 12 | 13 | [dev-dependencies] 14 | trybuild = "1.0.17" 15 | 16 | [features] 17 | proc_macro_diagnostics = [] 18 | 19 | [lib] 20 | proc-macro = true 21 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn ui() { 3 | if option_env!("CARGO") 4 | .unwrap_or("cargo") 5 | .ends_with("cargo-tarpaulin") 6 | { 7 | eprintln!( 8 | "Skipping ui tests to avoid incompatibility between cargo-tarpaulin and trybuild" 9 | ); 10 | return; 11 | } 12 | let t = trybuild::TestCases::new(); 13 | t.compile_fail("tests/ui/*.rs"); 14 | } 15 | -------------------------------------------------------------------------------- /project-examples/realworld/app/tokens.rs: -------------------------------------------------------------------------------- 1 | use jwt::Header; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::context::AppCtx; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize)] 7 | pub struct Claims { 8 | pub sub: String, 9 | pub token: String, 10 | } 11 | 12 | pub fn encode(ctx: &AppCtx, claims: &Claims) -> String { 13 | jwt::encode(&Header::default(), claims, ctx.secret_key.as_bytes()).unwrap() 14 | } 15 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-unknown-attr.stderr: -------------------------------------------------------------------------------- 1 | error: unknown option: `foo` 2 | --> $DIR/from-request-unknown-attr.rs:4:35 3 | | 4 | 4 | #[nails(path = "/api/posts/{id}", foo)] 5 | | ^^^ 6 | 7 | error: unknown option: `foo` 8 | --> $DIR/from-request-unknown-attr.rs:10:20 9 | | 10 | 10 | #[nails(query, foo)] 11 | | ^^^ 12 | 13 | error: could not compile `nails_derive-tests`. 14 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-non-captured-path-name.stderr: -------------------------------------------------------------------------------- 1 | error: This name doesn't exist in the endpoint path 2 | --> $DIR/from-request-non-captured-path-name.rs:6:13 3 | | 4 | 6 | #[nails(path = "idd")] 5 | | ^^^^ 6 | 7 | error: This name doesn't exist in the endpoint path 8 | --> $DIR/from-request-non-captured-path-name.rs:13:13 9 | | 10 | 13 | #[nails(path)] 11 | | ^^^^ 12 | 13 | error: could not compile `nails_derive-tests`. 14 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-duplicate-path-names.rs: -------------------------------------------------------------------------------- 1 | use nails_derive::Preroute; 2 | 3 | #[derive(Preroute)] 4 | #[nails(path = "/api/posts/{id}")] 5 | pub struct GetPostRequest { 6 | #[nails(path)] 7 | id: String, 8 | #[nails(path = "id")] 9 | id2: String, 10 | } 11 | 12 | #[derive(Preroute)] 13 | #[nails(path = "/api/posts/{id}")] 14 | pub struct GetPostRequest2 { 15 | #[nails(path = "id")] 16 | id2: String, 17 | id: String, 18 | } 19 | 20 | fn main() {} 21 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-double-paths.stderr: -------------------------------------------------------------------------------- 1 | error: multiple #[nails(path)] definitions 2 | --> $DIR/from-request-double-paths.rs:5:16 3 | | 4 | 5 | #[nails(path = "/api/posts/{idd}")] 5 | | ^^^^^^^^^^^^^^^^^^ 6 | 7 | error: multiple #[nails(path)] definitions 8 | --> $DIR/from-request-double-paths.rs:9:42 9 | | 10 | 9 | #[nails(path = "/api/posts/{id}", path = "/api/posts/{idd}")] 11 | | ^^^^^^^^^^^^^^^^^^ 12 | 13 | error: could not compile `nails_derive-tests`. 14 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-non-string-queries.rs: -------------------------------------------------------------------------------- 1 | use nails_derive::Preroute; 2 | 3 | #[derive(Preroute)] 4 | #[nails(path = "/api/posts/{id}")] 5 | pub struct GetPostRequest { 6 | #[nails(query = 42)] 7 | query1: String, 8 | } 9 | 10 | #[derive(Preroute)] 11 | #[nails(path = "/api/posts/{id}")] 12 | pub struct GetPostRequest2 { 13 | #[nails(query = b"query1rename")] 14 | query1: String, 15 | } 16 | 17 | #[derive(Preroute)] 18 | #[nails(path = "/api/posts/{id}")] 19 | pub struct GetPostRequest3 { 20 | #[nails(path = 42)] 21 | id: String, 22 | } 23 | 24 | fn main() {} 25 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-non-string-paths.stderr: -------------------------------------------------------------------------------- 1 | error: string value expected in #[nails(path)] 2 | --> $DIR/from-request-non-string-paths.rs:4:16 3 | | 4 | 4 | #[nails(path = 42)] 5 | | ^^ 6 | 7 | error: string value expected in #[nails(path)] 8 | --> $DIR/from-request-non-string-paths.rs:8:16 9 | | 10 | 8 | #[nails(path = b"/api/posts/{id}")] 11 | | ^^^^^^^^^^^^^^^^^^ 12 | 13 | error: string value expected in #[nails(path)] 14 | --> $DIR/from-request-non-string-paths.rs:12:9 15 | | 16 | 12 | #[nails(path)] 17 | | ^^^^ 18 | 19 | error: could not compile `nails_derive-tests`. 20 | -------------------------------------------------------------------------------- /project-examples/realworld/app/models.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::*; 2 | 3 | #[derive(Queryable)] 4 | pub struct User { 5 | pub id: i64, 6 | pub email: String, 7 | pub token: String, 8 | pub username: String, 9 | pub bio: Option, 10 | pub image: Option, 11 | } 12 | 13 | #[derive(Insertable)] 14 | #[table_name = "users"] 15 | pub struct NewUser<'a> { 16 | pub email: &'a str, 17 | pub token: &'a str, 18 | pub username: &'a str, 19 | pub bio: Option<&'a str>, 20 | pub image: Option<&'a str>, 21 | } 22 | 23 | #[derive(Queryable)] 24 | pub struct Tag { 25 | pub id: i64, 26 | pub tag: String, 27 | } 28 | -------------------------------------------------------------------------------- /lib/nails_derive/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! assert_ts_eq { 3 | ($lhs:expr, $rhs:expr) => {{ 4 | let lhs: TokenStream = $lhs; 5 | let rhs: TokenStream = $rhs; 6 | if lhs.to_string() != rhs.to_string() { 7 | panic!( 8 | r#"assertion failed: `(left == right)` 9 | left: 10 | ``` 11 | {} 12 | ``` 13 | 14 | right: ``` 15 | {} 16 | ``` 17 | "#, 18 | synstructure::unpretty_print(&lhs), 19 | synstructure::unpretty_print(&rhs), 20 | ); 21 | } 22 | }}; 23 | ($lhs:expr, $rhs:expr,) => { 24 | assert_ts_eq!($lhs, $rhs) 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /lib/nails/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nails" 3 | version = "0.1.0" 4 | authors = ["Masaki Hara "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | failure = "0.1.6" 9 | serde = { version = "1.0.101", features = ["derive"] } 10 | serde_json = "1.0.40" 11 | futures01 = "0.1.28" 12 | futures-preview = { version = "=0.3.0-alpha.18", features = ["compat"] } 13 | tokio-io = "=0.2.0-alpha.5" 14 | tokio-executor = "=0.2.0-alpha.5" 15 | hyper = { version = "=0.13.0-alpha.2", default-features = false } 16 | runtime = "=0.3.0-alpha.7" 17 | async-trait = "0.1.13" 18 | contextful = { path = "../contextful" } 19 | nails_derive = { path = "../nails_derive" } 20 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-double-queries.rs: -------------------------------------------------------------------------------- 1 | use nails_derive::Preroute; 2 | 3 | #[derive(Preroute)] 4 | #[nails(path = "/api/posts/{id}")] 5 | pub struct GetPostRequest { 6 | #[nails(query = "query1rename")] 7 | #[nails(query = "query1renamerename")] 8 | query1: String, 9 | } 10 | 11 | #[derive(Preroute)] 12 | #[nails(path = "/api/posts/{id}")] 13 | pub struct GetPostRequest2 { 14 | #[nails(query = "query1rename", query = "query1renamerename")] 15 | query1: String, 16 | } 17 | 18 | #[derive(Preroute)] 19 | #[nails(path = "/api/posts/{id1}/{id2}")] 20 | pub struct GetPostRequest3 { 21 | #[nails(path = "id1", path = "id2")] 22 | id: String, 23 | } 24 | 25 | fn main() {} 26 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-non-string-queries.stderr: -------------------------------------------------------------------------------- 1 | error: string value or no value expected in #[nails(query)] 2 | --> $DIR/from-request-non-string-queries.rs:6:21 3 | | 4 | 6 | #[nails(query = 42)] 5 | | ^^ 6 | 7 | error: string value or no value expected in #[nails(query)] 8 | --> $DIR/from-request-non-string-queries.rs:13:21 9 | | 10 | 13 | #[nails(query = b"query1rename")] 11 | | ^^^^^^^^^^^^^^^ 12 | 13 | error: string value or no value expected in #[nails(path)] 14 | --> $DIR/from-request-non-string-queries.rs:20:20 15 | | 16 | 20 | #[nails(path = 42)] 17 | | ^^ 18 | 19 | error: could not compile `nails_derive-tests`. 20 | -------------------------------------------------------------------------------- /lib/nails_derive/tests/ui/from-request-double-queries.stderr: -------------------------------------------------------------------------------- 1 | error: multiple #[nails(query)] definitions 2 | --> $DIR/from-request-double-queries.rs:7:21 3 | | 4 | 7 | #[nails(query = "query1renamerename")] 5 | | ^^^^^^^^^^^^^^^^^^^^ 6 | 7 | error: multiple #[nails(query)] definitions 8 | --> $DIR/from-request-double-queries.rs:14:45 9 | | 10 | 14 | #[nails(query = "query1rename", query = "query1renamerename")] 11 | | ^^^^^^^^^^^^^^^^^^^^ 12 | 13 | error: multiple #[nails(path)] definitions 14 | --> $DIR/from-request-double-queries.rs:21:34 15 | | 16 | 21 | #[nails(path = "id1", path = "id2")] 17 | | ^^^^^ 18 | 19 | error: could not compile `nails_derive-tests`. 20 | -------------------------------------------------------------------------------- /project-examples/realworld/app/routes/posts.rs: -------------------------------------------------------------------------------- 1 | use hyper::{Body, Response}; 2 | use nails::error::NailsError; 3 | use nails::Preroute; 4 | use serde::Serialize; 5 | 6 | use crate::context::AppCtx; 7 | 8 | #[derive(Debug, Preroute)] 9 | #[nails(path = "/api/posts/{id}")] 10 | pub(crate) struct GetPostRequest { 11 | id: u64, 12 | } 13 | 14 | #[derive(Debug, Serialize)] 15 | pub(crate) struct GetPostBody { 16 | post: Post, 17 | } 18 | 19 | #[derive(Debug, Serialize)] 20 | pub(crate) struct Post { 21 | body: String, 22 | } 23 | 24 | pub(crate) async fn get_post( 25 | _ctx: AppCtx, 26 | _req: GetPostRequest, 27 | ) -> Result, NailsError> { 28 | let body = GetPostBody { 29 | post: Post { 30 | body: String::from("foo"), 31 | }, 32 | }; 33 | Ok(super::json_response(&body)) 34 | } 35 | -------------------------------------------------------------------------------- /project-examples/realworld/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nails-realworld" 3 | version = "0.1.0" 4 | authors = ["Masaki Hara "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | dotenv = "0.14.1" 9 | failure = "0.1.6" 10 | rand = "0.7.2" 11 | base64 = "0.10.1" 12 | serde = { version = "1.0.101", features = ["derive"] } 13 | serde_json = "1.0.40" 14 | futures-preview = { version = "=0.3.0-alpha.18", features = ["compat"] } 15 | hyper = { version = "=0.13.0-alpha.2", default-features = false } 16 | runtime = "=0.3.0-alpha.7" 17 | structopt = "0.3.2" 18 | diesel = { version = "1.4.3", features = ["postgres", "r2d2"] } 19 | jwt = { package = "jsonwebtoken", version = "6.0.1" } 20 | nails = { path = "../../lib/nails" } 21 | contextful = { path = "../../lib/contextful" } 22 | derivative = "1.0.3" 23 | 24 | [dev-dependencies] 25 | surf = "1.0.2" 26 | 27 | [[bin]] 28 | name = "realworld" 29 | path = "app/main.rs" 30 | -------------------------------------------------------------------------------- /project-examples/realworld/app/routes/tags.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | use hyper::{Body, Response}; 3 | use nails::error::NailsError; 4 | use nails::Preroute; 5 | use serde::Serialize; 6 | 7 | use crate::context::AppCtx; 8 | use crate::models::Tag; 9 | 10 | #[derive(Debug, Preroute)] 11 | #[nails(path = "/api/tags")] 12 | pub(crate) struct ListTagsRequest; 13 | 14 | #[derive(Debug, Serialize)] 15 | pub(crate) struct ListTagsResponseBody { 16 | tags: Vec, 17 | } 18 | 19 | pub(crate) async fn list_tags( 20 | ctx: AppCtx, 21 | _req: ListTagsRequest, 22 | ) -> Result, NailsError> { 23 | use crate::schema::tags::dsl::*; 24 | 25 | // TODO: async 26 | let conn = ctx.db.get().unwrap(); // TODO: handle errors 27 | let all_tags = tags.load::(&conn).unwrap(); // TODO: handle errors 28 | let body = ListTagsResponseBody { 29 | tags: all_tags.iter().map(|t| t.tag.clone()).collect(), 30 | }; 31 | Ok(super::json_response(&body)) 32 | } 33 | -------------------------------------------------------------------------------- /project-examples/realworld/app/routes.rs: -------------------------------------------------------------------------------- 1 | use hyper::{Body, Response}; 2 | use serde::Serialize; 3 | 4 | use nails::error::NailsError; 5 | use nails::{Preroute, Service}; 6 | 7 | use crate::context::AppCtx; 8 | 9 | mod articles; 10 | mod posts; 11 | mod tags; 12 | mod users; 13 | 14 | pub fn build_route(_ctx: &AppCtx) -> Service { 15 | Service::builder() 16 | .add_function_route(index) 17 | .add_function_route(users::create_user) 18 | .add_function_route(users::login) 19 | .add_function_route(posts::get_post) 20 | .add_function_route(tags::list_tags) 21 | .add_function_route(articles::list_articles) 22 | .add_function_route(articles::list_feed_articles) 23 | .finish() 24 | } 25 | 26 | #[derive(Debug, Preroute)] 27 | #[nails(path = "/")] 28 | struct IndexRequest { 29 | #[nails(query)] 30 | a: Vec, 31 | } 32 | 33 | async fn index(_ctx: AppCtx, req: IndexRequest) -> Result, NailsError> { 34 | Ok(Response::new(Body::from(format!( 35 | "Hello, world! {:?}", 36 | req.a 37 | )))) 38 | } 39 | 40 | fn json_response(body: &T) -> Response { 41 | Response::builder() 42 | .header("Content-Type", "application/json") 43 | .body(Body::from(serde_json::to_string(&body).unwrap())) 44 | .unwrap() 45 | } 46 | -------------------------------------------------------------------------------- /project-examples/realworld/migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /project-examples/realworld/app/context.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | 4 | use contextful::Context; 5 | use derivative::Derivative; 6 | use diesel::pg::PgConnection; 7 | use diesel::prelude::*; 8 | use diesel::r2d2::{ConnectionManager, Pool}; 9 | 10 | #[derive(Clone, Derivative)] 11 | #[derivative(Debug)] 12 | pub struct AppCtxInner { 13 | // TODO: async 14 | #[derivative(Debug = "ignore")] 15 | pub db: Pool>, 16 | #[derivative(Debug = "ignore")] 17 | pub secret_key: String, 18 | } 19 | 20 | #[derive(Debug, Clone)] 21 | pub struct AppCtx(pub Arc); 22 | 23 | impl std::ops::Deref for AppCtx { 24 | type Target = AppCtxInner; 25 | fn deref(&self) -> &Self::Target { 26 | &self.0 27 | } 28 | } 29 | 30 | impl Context for AppCtx {} 31 | 32 | impl AppCtx { 33 | pub fn new() -> Self { 34 | let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); 35 | // Sanity check 36 | PgConnection::establish(&database_url) 37 | .expect(&format!("Error connecting to {}", database_url)); 38 | let db = ConnectionManager::new(database_url); 39 | let db = Pool::builder().build(db).unwrap(); 40 | 41 | let secret_key = env::var("SECRET_KEY").expect("SECRET_KEY must be set"); 42 | 43 | Self(Arc::new(AppCtxInner { db, secret_key })) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /project-examples/realworld/app/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | article_tags (id) { 3 | id -> Int8, 4 | article_id -> Int8, 5 | tag_id -> Int8, 6 | } 7 | } 8 | 9 | table! { 10 | articles (id) { 11 | id -> Int8, 12 | slug -> Varchar, 13 | title -> Varchar, 14 | description -> Varchar, 15 | body -> Text, 16 | created_at -> Timestamp, 17 | updated_at -> Timestamp, 18 | author_id -> Int8, 19 | } 20 | } 21 | 22 | table! { 23 | comments (id) { 24 | id -> Int8, 25 | created_at -> Timestamp, 26 | updated_at -> Timestamp, 27 | body -> Text, 28 | article_id -> Int8, 29 | author_id -> Int8, 30 | } 31 | } 32 | 33 | table! { 34 | favorited_articles (id) { 35 | id -> Int8, 36 | article_id -> Int8, 37 | user_id -> Int8, 38 | } 39 | } 40 | 41 | table! { 42 | followings (id) { 43 | id -> Int8, 44 | following_id -> Int8, 45 | follower_id -> Int8, 46 | } 47 | } 48 | 49 | table! { 50 | tags (id) { 51 | id -> Int8, 52 | tag -> Varchar, 53 | } 54 | } 55 | 56 | table! { 57 | users (id) { 58 | id -> Int8, 59 | email -> Varchar, 60 | token -> Varchar, 61 | username -> Varchar, 62 | bio -> Nullable, 63 | image -> Nullable, 64 | } 65 | } 66 | 67 | joinable!(article_tags -> articles (article_id)); 68 | joinable!(article_tags -> tags (tag_id)); 69 | joinable!(articles -> users (author_id)); 70 | joinable!(comments -> articles (article_id)); 71 | joinable!(comments -> users (author_id)); 72 | joinable!(favorited_articles -> articles (article_id)); 73 | joinable!(favorited_articles -> users (user_id)); 74 | 75 | allow_tables_to_appear_in_same_query!( 76 | article_tags, 77 | articles, 78 | comments, 79 | favorited_articles, 80 | followings, 81 | tags, 82 | users, 83 | ); 84 | -------------------------------------------------------------------------------- /lib/nails/src/utils/hyper_ext.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for using `hyper` with `runtime`. 2 | 3 | use futures::prelude::*; 4 | 5 | use super::tokio02_ext::Compat as Tokio02Compat; 6 | use futures::task::Poll; 7 | use hyper::server::accept::Accept; 8 | use hyper::Server; 9 | use runtime::net::{TcpListener, TcpStream}; 10 | use runtime::task::Spawner; 11 | use std::net::SocketAddr; 12 | use std::pin::Pin; 13 | 14 | #[derive(Debug)] 15 | pub struct AddrIncoming { 16 | inner: TcpListener, 17 | } 18 | 19 | impl Accept for AddrIncoming { 20 | type Conn = Tokio02Compat; 21 | type Error = std::io::Error; 22 | 23 | fn poll_accept( 24 | self: Pin<&mut Self>, 25 | cx: &mut futures::task::Context<'_>, 26 | ) -> Poll>> { 27 | Pin::new(&mut self.get_mut().inner.incoming()) 28 | .poll_next(cx) 29 | .map(|x| x.map(|x| x.map(Tokio02Compat))) 30 | } 31 | } 32 | 33 | pub trait ServerBindExt { 34 | type Builder; 35 | 36 | /// Binds to the provided address, and returns a [`Builder`](Builder). 37 | /// 38 | /// # Panics 39 | /// 40 | /// This method will panic if binding to the address fails. 41 | fn bind2(addr: &SocketAddr) -> Self::Builder; 42 | 43 | fn bind2_mut(addr: &mut SocketAddr) -> Self::Builder; 44 | } 45 | 46 | impl ServerBindExt for Server { 47 | type Builder = hyper::server::Builder>; 48 | 49 | fn bind2(addr: &SocketAddr) -> Self::Builder { 50 | let incoming = TcpListener::bind(addr).unwrap_or_else(|e| { 51 | panic!("error binding to {}: {}", addr, e); 52 | }); 53 | Server::builder(AddrIncoming { inner: incoming }) 54 | .executor(Tokio02Compat::new(Spawner::new())) 55 | } 56 | 57 | fn bind2_mut(addr: &mut SocketAddr) -> Self::Builder { 58 | let incoming = TcpListener::bind(&*addr).unwrap_or_else(|e| { 59 | panic!("error binding to {}: {}", addr, e); 60 | }); 61 | if let Ok(l_addr) = incoming.local_addr() { 62 | *addr = l_addr; 63 | } 64 | Server::builder(AddrIncoming { inner: incoming }) 65 | .executor(Tokio02Compat::new(Spawner::new())) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/nails_derive/src/utils.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | use syn::{token, Field, Fields, FieldsNamed, FieldsUnnamed, Ident}; 4 | 5 | pub(crate) trait FieldsExt { 6 | fn try_construct(&self, ident: &Ident, func: F) -> Result 7 | where 8 | F: FnMut(&Field, usize) -> Result, 9 | T: ToTokens; 10 | } 11 | 12 | impl FieldsExt for Fields { 13 | fn try_construct(&self, ident: &Ident, mut func: F) -> Result 14 | where 15 | F: FnMut(&Field, usize) -> Result, 16 | T: ToTokens, 17 | { 18 | let mut t = TokenStream::new(); 19 | ident.to_tokens(&mut t); 20 | 21 | match self { 22 | Fields::Unit => (), 23 | Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => { 24 | let mut err = None; 25 | token::Paren::default().surround(&mut t, |t| { 26 | for (i, field) in unnamed.into_iter().enumerate() { 27 | match func(field, i) { 28 | Ok(x) => x, 29 | Err(e) => { 30 | err = Some(e); 31 | return; 32 | } 33 | } 34 | .to_tokens(t); 35 | quote!(,).to_tokens(t); 36 | } 37 | }); 38 | if let Some(e) = err { 39 | return Err(e); 40 | } 41 | } 42 | Fields::Named(FieldsNamed { named, .. }) => { 43 | let mut err = None; 44 | token::Brace::default().surround(&mut t, |t| { 45 | for (i, field) in named.into_iter().enumerate() { 46 | field.ident.to_tokens(t); 47 | quote!(:).to_tokens(t); 48 | match func(field, i) { 49 | Ok(x) => x, 50 | Err(e) => { 51 | err = Some(e); 52 | return; 53 | } 54 | } 55 | .to_tokens(t); 56 | quote!(,).to_tokens(t); 57 | } 58 | }); 59 | if let Some(e) = err { 60 | return Err(e); 61 | } 62 | } 63 | } 64 | Ok(t) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/nails/src/utils/tokio02_ext.rs: -------------------------------------------------------------------------------- 1 | use futures::prelude::*; 2 | 3 | use futures::task::{Context, Poll, Spawn, SpawnError, SpawnExt}; 4 | use std::io; 5 | use std::pin::Pin; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] 8 | pub struct Compat(pub T); 9 | 10 | impl Compat { 11 | pub fn new(inner: T) -> Self { 12 | Self(inner) 13 | } 14 | pub fn into_inner(self) -> T { 15 | self.0 16 | } 17 | pub fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut T> { 18 | unsafe { self.map_unchecked_mut(|this| &mut this.0) } 19 | } 20 | pub fn get_mut(&mut self) -> &mut T { 21 | &mut self.0 22 | } 23 | pub fn get_ref(&self) -> &T { 24 | &self.0 25 | } 26 | } 27 | 28 | impl tokio_io::AsyncRead for Compat 29 | where 30 | T: AsyncRead, 31 | { 32 | fn poll_read( 33 | self: Pin<&mut Self>, 34 | cx: &mut Context, 35 | buf: &mut [u8], 36 | ) -> Poll> { 37 | self.get_pin_mut().poll_read(cx, buf) 38 | } 39 | 40 | // TODO: implement other methods for faster read 41 | } 42 | 43 | impl tokio_io::AsyncBufRead for Compat 44 | where 45 | T: AsyncBufRead, 46 | { 47 | fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 48 | self.get_pin_mut().poll_fill_buf(cx) 49 | } 50 | 51 | fn consume(self: Pin<&mut Self>, amt: usize) { 52 | self.get_pin_mut().consume(amt) 53 | } 54 | } 55 | 56 | impl tokio_io::AsyncWrite for Compat 57 | where 58 | T: AsyncWrite, 59 | { 60 | fn poll_write(self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll> { 61 | self.get_pin_mut().poll_write(cx, buf) 62 | } 63 | 64 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 65 | self.get_pin_mut().poll_flush(cx) 66 | } 67 | 68 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 69 | self.get_pin_mut().poll_close(cx) 70 | } 71 | 72 | // TODO: implement other methods for faster write 73 | } 74 | 75 | impl tokio_executor::TypedExecutor for Compat 76 | where 77 | T: Spawn, 78 | U: Future + Send + 'static, 79 | { 80 | fn spawn(&mut self, future: U) -> Result<(), tokio_executor::SpawnError> { 81 | self.get_mut().spawn(future).map_err(spawn_error_compat) 82 | } 83 | 84 | fn status(&self) -> Result<(), tokio_executor::SpawnError> { 85 | self.get_ref().status().map_err(spawn_error_compat) 86 | } 87 | } 88 | 89 | fn spawn_error_compat(e: SpawnError) -> tokio_executor::SpawnError { 90 | if e.is_shutdown() { 91 | tokio_executor::SpawnError::shutdown() 92 | } else { 93 | tokio_executor::SpawnError::shutdown() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /project-examples/realworld/app/routes/articles.rs: -------------------------------------------------------------------------------- 1 | use hyper::{Body, Response}; 2 | use nails::error::NailsError; 3 | use nails::Preroute; 4 | use serde::Serialize; 5 | 6 | use crate::context::AppCtx; 7 | 8 | #[derive(Debug, Preroute)] 9 | #[nails(path = "/api/articles")] 10 | pub(crate) struct ListArticlesRequest { 11 | tag: Option, 12 | author: Option, 13 | favorited: Option, 14 | limit: Option, 15 | offset: Option, 16 | } 17 | 18 | #[derive(Debug, Serialize)] 19 | #[serde(rename_all = "camelCase")] 20 | pub(crate) struct ListArticlesResponseBody { 21 | articles: Vec
, 22 | articles_count: u64, 23 | } 24 | 25 | pub(crate) async fn list_articles( 26 | _ctx: AppCtx, 27 | _req: ListArticlesRequest, 28 | ) -> Result, NailsError> { 29 | let articles = vec![Article { 30 | slug: String::from("slug"), 31 | title: String::from("title"), 32 | description: String::from("description"), 33 | body: String::from("body"), 34 | tag_list: vec![String::from("tag2"), String::from("tag3")], 35 | created_at: String::from("2019-07-14T19:07:00+0900"), 36 | updated_at: String::from("2019-07-14T19:07:00+0900"), 37 | favorited: false, 38 | favorites_count: 0, 39 | author: Profile { 40 | username: String::from("username"), 41 | bio: String::from("bio"), 42 | image: String::from("image"), 43 | following: false, 44 | }, 45 | }]; 46 | let body = ListArticlesResponseBody { 47 | articles_count: articles.len() as u64, 48 | articles, 49 | }; 50 | Ok(super::json_response(&body)) 51 | } 52 | 53 | #[derive(Debug, Preroute)] 54 | #[nails(path = "/api/articles/feed")] 55 | pub(crate) struct ListFeedArticlesRequest { 56 | limit: Option, 57 | offset: Option, 58 | } 59 | 60 | #[derive(Debug, Serialize)] 61 | #[serde(rename_all = "camelCase")] 62 | pub(crate) struct ListFeedArticlesResponseBody { 63 | articles: Vec
, 64 | articles_count: u64, 65 | } 66 | 67 | pub(crate) async fn list_feed_articles( 68 | _ctx: AppCtx, 69 | _req: ListFeedArticlesRequest, 70 | ) -> Result, NailsError> { 71 | let articles = vec![Article { 72 | slug: String::from("slug"), 73 | title: String::from("title"), 74 | description: String::from("description"), 75 | body: String::from("body"), 76 | tag_list: vec![String::from("tag2"), String::from("tag3")], 77 | created_at: String::from("2019-07-14T19:07:00+0900"), 78 | updated_at: String::from("2019-07-14T19:07:00+0900"), 79 | favorited: false, 80 | favorites_count: 0, 81 | author: Profile { 82 | username: String::from("username"), 83 | bio: String::from("bio"), 84 | image: String::from("image"), 85 | following: false, 86 | }, 87 | }]; 88 | let body = ListFeedArticlesResponseBody { 89 | articles_count: articles.len() as u64, 90 | articles, 91 | }; 92 | Ok(super::json_response(&body)) 93 | } 94 | 95 | #[derive(Debug, Serialize)] 96 | #[serde(rename_all = "camelCase")] 97 | pub(crate) struct Article { 98 | slug: String, 99 | title: String, 100 | description: String, 101 | body: String, 102 | tag_list: Vec, 103 | created_at: String, // TODO: DateTime 104 | updated_at: String, // TODO: DateTime 105 | favorited: bool, 106 | favorites_count: u64, 107 | author: Profile, 108 | } 109 | 110 | #[derive(Debug, Serialize)] 111 | pub(crate) struct Profile { 112 | username: String, 113 | bio: String, 114 | image: String, 115 | following: bool, 116 | } 117 | -------------------------------------------------------------------------------- /project-examples/realworld/app/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel; 3 | 4 | use hyper::Server; 5 | use nails::utils::hyper_ext::ServerBindExt; 6 | use structopt::StructOpt; 7 | 8 | use crate::context::AppCtx; 9 | 10 | mod context; 11 | mod models; 12 | mod routes; 13 | mod schema; 14 | mod tokens; 15 | 16 | #[derive(Debug, Clone, StructOpt)] 17 | struct CommandOpt { 18 | #[structopt(subcommand)] 19 | subcommand: SubcommandOpt, 20 | } 21 | 22 | #[derive(Debug, Clone, StructOpt)] 23 | enum SubcommandOpt { 24 | #[structopt(name = "server")] 25 | ServerCommandOpt(ServerCommandOpt), 26 | } 27 | 28 | #[runtime::main] 29 | async fn main() -> failure::Fallible<()> { 30 | dotenv::dotenv().ok(); 31 | 32 | let opt = CommandOpt::from_args(); 33 | let ctx = AppCtx::new(); 34 | match opt.subcommand { 35 | SubcommandOpt::ServerCommandOpt(ref server_opt) => { 36 | server(&ctx, server_opt).await?; 37 | } 38 | } 39 | Ok(()) 40 | } 41 | 42 | #[derive(Debug, Clone, StructOpt)] 43 | pub(crate) struct ServerCommandOpt { 44 | #[structopt(short = "p", help = "on which port to listen")] 45 | port: Option, 46 | } 47 | 48 | pub(crate) async fn server(ctx: &AppCtx, opt: &ServerCommandOpt) -> failure::Fallible<()> { 49 | let svc = crate::routes::build_route(ctx); 50 | 51 | let host: std::net::IpAddr = "127.0.0.1".parse().unwrap(); 52 | let port = opt.port.unwrap_or(3000); 53 | let mut addr = (host, port).into(); 54 | 55 | let server = Server::bind2_mut(&mut addr).serve(svc.with_context(ctx)); 56 | println!("Listening on {}", addr); 57 | 58 | server.await?; 59 | 60 | Ok(()) 61 | } 62 | 63 | #[cfg(test)] 64 | #[derive(Debug)] 65 | pub(crate) struct TestServer { 66 | addr: std::net::SocketAddr, 67 | shutdown: Option>, 68 | } 69 | 70 | #[cfg(test)] 71 | impl TestServer { 72 | pub(crate) fn addr(&self) -> std::net::SocketAddr { 73 | self.addr 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | impl Drop for TestServer { 79 | fn drop(&mut self) { 80 | if let Some(shutdown) = self.shutdown.take() { 81 | shutdown.send(()).ok(); 82 | } 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | async fn server_for_test(ctx: &AppCtx) -> failure::Fallible { 88 | use futures::channel::oneshot; 89 | use futures::prelude::*; 90 | 91 | let svc = crate::routes::build_route(ctx); 92 | 93 | let host: std::net::IpAddr = "127.0.0.1".parse().unwrap(); 94 | let port = 0; 95 | let mut addr = (host, port).into(); 96 | 97 | let server = Server::bind2_mut(&mut addr).serve(svc.with_context(ctx)); 98 | 99 | let (tx, rx) = oneshot::channel(); 100 | 101 | runtime::spawn(async move { 102 | match server.with_graceful_shutdown(rx.map(|_| ())).await { 103 | Ok(()) => {} 104 | Err(e) => { 105 | // TODO: log it properly 106 | eprintln!("Server error: {}", e); 107 | } 108 | } 109 | }); 110 | 111 | Ok(TestServer { 112 | addr, 113 | shutdown: Some(tx), 114 | }) 115 | } 116 | 117 | #[cfg(test)] 118 | mod tests { 119 | use super::*; 120 | 121 | use surf::http::StatusCode; 122 | 123 | #[runtime::test] 124 | async fn test_server() { 125 | let server = init_test().await; 126 | 127 | let url: String = format!("http://{}/", server.addr()); 128 | let mut res = surf::get(url).await.unwrap(); 129 | assert_eq!(res.status(), StatusCode::OK); 130 | let body = res.body_string().await.unwrap(); 131 | assert_eq!(body, "Hello, world! []"); 132 | eprintln!("res = {:?}", res); 133 | } 134 | 135 | async fn init_test() -> TestServer { 136 | dotenv::dotenv().ok(); 137 | let ctx = AppCtx::new(); 138 | server_for_test(&ctx).await.unwrap() 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /project-examples/realworld/app/routes/users.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | use rand::prelude::*; 3 | 4 | use hyper::{Body, Response}; 5 | use nails::error::NailsError; 6 | use nails::request::JsonBody; 7 | use nails::Preroute; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use crate::context::AppCtx; 11 | use crate::models; 12 | use crate::tokens::{self, Claims}; 13 | 14 | #[derive(Debug, Preroute)] 15 | #[nails(path = "/api/users", method = "POST")] 16 | pub(crate) struct CreateUserRequest { 17 | #[nails(body)] 18 | body: JsonBody, 19 | } 20 | 21 | #[derive(Debug, Deserialize)] 22 | pub(crate) struct CreateUserRequestBody { 23 | user: NewUser, 24 | } 25 | 26 | #[derive(Debug, Serialize)] 27 | pub(crate) struct CreateUserResponseBody { 28 | user: User, 29 | } 30 | 31 | pub(crate) async fn create_user( 32 | ctx: AppCtx, 33 | req: CreateUserRequest, 34 | ) -> Result, NailsError> { 35 | use crate::schema::users::dsl::*; 36 | 37 | let mut rng = rand::thread_rng(); 38 | let new_token = { 39 | let mut buf = [0; 32]; 40 | rng.fill_bytes(&mut buf); 41 | base64::encode(&buf) 42 | }; 43 | let user = &req.body.0.user; 44 | 45 | // TODO: async 46 | let conn = ctx.db.get().unwrap(); // TODO: handle errors 47 | 48 | let new_user = models::NewUser { 49 | email: &user.email, 50 | token: &new_token, 51 | username: &user.username, 52 | bio: None, 53 | image: None, 54 | }; 55 | let new_user = new_user 56 | .insert_into(users) 57 | .get_result::(&conn) 58 | .unwrap(); // TODO: handle errors 59 | 60 | let body = CreateUserResponseBody { 61 | user: User::from_model(&ctx, new_user), 62 | }; 63 | Ok(super::json_response(&body)) 64 | } 65 | 66 | #[derive(Debug, Preroute)] 67 | #[nails(path = "/api/users/login", method = "POST")] 68 | pub(crate) struct LoginRequest { 69 | #[nails(body)] 70 | body: JsonBody, 71 | } 72 | 73 | #[derive(Debug, Deserialize)] 74 | pub(crate) struct LoginRequestBody { 75 | user: LoginUser, 76 | } 77 | 78 | #[derive(Debug, Serialize)] 79 | pub(crate) struct LoginResponseBody { 80 | user: User, 81 | } 82 | 83 | pub(crate) async fn login(ctx: AppCtx, req: LoginRequest) -> Result, NailsError> { 84 | use crate::schema::users::dsl::*; 85 | 86 | let login_user = &req.body.0.user; 87 | 88 | // TODO: async 89 | let conn = ctx.db.get().unwrap(); // TODO: handle errors 90 | 91 | // TODO: handle errors 92 | let found_user = users 93 | .filter(email.eq(&login_user.email)) 94 | .first::(&conn) 95 | .unwrap(); 96 | 97 | // TODO: check password 98 | 99 | let body = LoginResponseBody { 100 | user: User::from_model(&ctx, found_user), 101 | }; 102 | Ok(super::json_response(&body)) 103 | } 104 | 105 | #[derive(Debug, Clone, Deserialize)] 106 | pub(crate) struct NewUser { 107 | username: String, 108 | email: String, 109 | password: String, 110 | } 111 | 112 | #[derive(Debug, Clone, Deserialize)] 113 | pub(crate) struct LoginUser { 114 | email: String, 115 | password: String, 116 | } 117 | 118 | #[derive(Debug, Clone, Serialize)] 119 | pub(crate) struct User { 120 | email: String, 121 | token: String, 122 | username: String, 123 | bio: String, 124 | image: String, 125 | } 126 | 127 | impl User { 128 | fn from_model(ctx: &AppCtx, user: models::User) -> Self { 129 | let jwt = tokens::encode( 130 | ctx, 131 | &Claims { 132 | sub: user.id.to_string(), 133 | token: user.token, 134 | }, 135 | ); 136 | Self { 137 | email: user.email, 138 | token: jwt, 139 | username: user.username, 140 | bio: user.bio.unwrap_or_else(String::default), 141 | image: user.image.unwrap_or_else(String::default), 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /project-examples/realworld/migrations/2019-07-28-121610_create_db/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id BIGSERIAL PRIMARY KEY, 3 | email VARCHAR(50) NOT NULL, 4 | token VARCHAR(250) NOT NULL, 5 | username VARCHAR(150) NOT NULL, 6 | bio TEXT, 7 | image VARCHAR(250) 8 | ); 9 | CREATE UNIQUE INDEX index_users_on_email ON users (email); 10 | CREATE UNIQUE INDEX index_users_on_username ON users (username); 11 | 12 | CREATE TABLE followings ( 13 | id BIGSERIAL PRIMARY KEY, 14 | following_id BIGINT NOT NULL, 15 | follower_id BIGINT NOT NULL, 16 | 17 | CONSTRAINT fk_followings_following_id FOREIGN KEY (following_id) 18 | REFERENCES users (id) 19 | ON DELETE RESTRICT 20 | ON UPDATE RESTRICT 21 | NOT DEFERRABLE, 22 | CONSTRAINT fk_followings_follower_id FOREIGN KEY (follower_id) 23 | REFERENCES users (id) 24 | ON DELETE RESTRICT 25 | ON UPDATE RESTRICT 26 | NOT DEFERRABLE 27 | ); 28 | CREATE UNIQUE INDEX index_followings_on_following_id_and_follower_id ON followings ( 29 | following_id, 30 | follower_id 31 | ); 32 | CREATE INDEX index_followings_on_follower_id ON followings (follower_id); 33 | 34 | CREATE TABLE tags ( 35 | id BIGSERIAL PRIMARY KEY, 36 | tag VARCHAR(250) NOT NULL 37 | ); 38 | CREATE UNIQUE INDEX index_tags_on_tag ON tags (tag); 39 | 40 | CREATE TABLE articles ( 41 | id BIGSERIAL PRIMARY KEY, 42 | slug VARCHAR(250) NOT NULL, 43 | title VARCHAR(250) NOT NULL, 44 | description VARCHAR(250) NOT NULL, 45 | body TEXT NOT NULL, 46 | created_at TIMESTAMP NOT NULL, 47 | updated_at TIMESTAMP NOT NULL, 48 | author_id BIGINT NOT NULL, 49 | 50 | CONSTRAINT fk_articles_author_id FOREIGN KEY (author_id) 51 | REFERENCES users (id) 52 | ON DELETE RESTRICT 53 | ON UPDATE RESTRICT 54 | NOT DEFERRABLE 55 | ); 56 | CREATE UNIQUE INDEX index_articles_on_slug ON articles (slug); 57 | CREATE INDEX index_articles_on_author_id ON articles (author_id); 58 | 59 | CREATE TABLE favorited_articles ( 60 | id BIGSERIAL PRIMARY KEY, 61 | article_id BIGINT NOT NULL, 62 | user_id BIGINT NOT NULL, 63 | 64 | CONSTRAINT fk_favorited_articles_article_id FOREIGN KEY (article_id) 65 | REFERENCES articles (id) 66 | ON DELETE RESTRICT 67 | ON UPDATE RESTRICT 68 | NOT DEFERRABLE, 69 | CONSTRAINT fk_favorited_articles_user_id FOREIGN KEY (user_id) 70 | REFERENCES users (id) 71 | ON DELETE RESTRICT 72 | ON UPDATE RESTRICT 73 | NOT DEFERRABLE 74 | ); 75 | CREATE UNIQUE INDEX index_favorited_articles_on_article_id_and_user_id ON favorited_articles ( 76 | article_id, 77 | user_id 78 | ); 79 | CREATE INDEX index_favorited_articles_on_user_id ON favorited_articles (user_id); 80 | 81 | CREATE TABLE comments ( 82 | id BIGSERIAL PRIMARY KEY, 83 | created_at TIMESTAMP NOT NULL, 84 | updated_at TIMESTAMP NOT NULL, 85 | body TEXT NOT NULL, 86 | article_id BIGINT NOT NULL, 87 | author_id BIGINT NOT NULL, 88 | 89 | CONSTRAINT fk_comments_articles FOREIGN KEY (article_id) 90 | REFERENCES articles (id) 91 | ON DELETE RESTRICT 92 | ON UPDATE RESTRICT 93 | NOT DEFERRABLE, 94 | CONSTRAINT fk_comments_author_id FOREIGN KEY (author_id) 95 | REFERENCES users (id) 96 | ON DELETE RESTRICT 97 | ON UPDATE RESTRICT 98 | NOT DEFERRABLE 99 | ); 100 | CREATE INDEX index_comments_on_article_id ON comments (article_id); 101 | CREATE INDEX index_comments_on_author_id ON comments (author_id); 102 | 103 | CREATE TABLE article_tags ( 104 | id BIGSERIAL PRIMARY KEY, 105 | article_id BIGINT NOT NULL, 106 | tag_id BIGINT NOT NULL, 107 | 108 | CONSTRAINT fk_articletags_article_id FOREIGN KEY (article_id) 109 | REFERENCES articles (id) 110 | ON DELETE RESTRICT 111 | ON UPDATE RESTRICT 112 | NOT DEFERRABLE, 113 | CONSTRAINT fk_articletags_tag_id FOREIGN KEY (tag_id) 114 | REFERENCES tags (id) 115 | ON DELETE RESTRICT 116 | ON UPDATE RESTRICT 117 | NOT DEFERRABLE 118 | ); 119 | CREATE UNIQUE INDEX index_article_tags_on_article_id_and_tag_id ON article_tags ( 120 | article_id, 121 | tag_id 122 | ); 123 | CREATE UNIQUE INDEX index_article_tags_on_tag_id ON article_tags (tag_id); 124 | -------------------------------------------------------------------------------- /lib/contextful/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | pub trait Context: std::fmt::Debug + Clone {} 4 | 5 | impl Context for () {} 6 | 7 | pub trait AsContext: Context { 8 | fn as_context(&self) -> Cow<'_, U>; 9 | } 10 | 11 | impl AsContext for T { 12 | fn as_context(&self) -> Cow<'_, T> { 13 | Cow::Borrowed(self) 14 | } 15 | } 16 | 17 | #[cfg(test)] 18 | mod tests { 19 | use self::external::*; 20 | use super::*; 21 | 22 | #[cfg(feature = "todo")] 23 | mod todo { 24 | // TODO: We want to write this kind of pattern: 25 | #[derive(Debug, Clone, Context)] 26 | pub struct CommonContext { 27 | #[as_context] 28 | time: TimeMocker, 29 | #[as_context] 30 | web: WebMocker, 31 | conn: PgConnection, 32 | } 33 | 34 | impl AsContext for CommonContext { 35 | fn as_context(&self) -> Cow<'_, MiddlewareContext> { 36 | Cow::from(MiddlewareContext { 37 | time: self.time.clone(), 38 | web: self.web.clone(), 39 | }) 40 | } 41 | } 42 | 43 | #[derive(Debug, Clone, Context)] 44 | pub struct AppContext { 45 | #[as_context(TimeMocker, WebMocker)] 46 | common: CommonContext, 47 | server_config: ServerConfig, 48 | } 49 | 50 | #[derive(Debug, Clone, Context)] 51 | pub struct MiddlewareContext { 52 | #[as_context] 53 | time: TimeMocker, 54 | #[as_context] 55 | web: WebMocker, 56 | } 57 | } 58 | 59 | #[derive(Debug, Clone)] 60 | pub struct CommonContext { 61 | time: TimeMocker, 62 | web: WebMocker, 63 | conn: PgConnection, 64 | } 65 | 66 | impl Context for CommonContext {} 67 | 68 | impl AsContext for CommonContext { 69 | fn as_context(&self) -> Cow<'_, TimeMocker> { 70 | Cow::Borrowed(&self.time) 71 | } 72 | } 73 | 74 | impl AsContext for CommonContext { 75 | fn as_context(&self) -> Cow<'_, WebMocker> { 76 | Cow::Borrowed(&self.web) 77 | } 78 | } 79 | 80 | impl AsContext for CommonContext { 81 | fn as_context(&self) -> Cow<'_, MiddlewareContext> { 82 | Cow::Owned(MiddlewareContext { 83 | time: self.time.clone(), 84 | web: self.web.clone(), 85 | }) 86 | } 87 | } 88 | 89 | #[derive(Debug, Clone)] 90 | pub struct AppContext { 91 | common: CommonContext, 92 | server_config: ServerConfig, 93 | } 94 | 95 | impl Context for AppContext {} 96 | 97 | impl AsContext for AppContext { 98 | fn as_context(&self) -> Cow<'_, CommonContext> { 99 | Cow::Borrowed(&self.common) 100 | } 101 | } 102 | 103 | impl AsContext for AppContext { 104 | fn as_context(&self) -> Cow<'_, TimeMocker> { 105 | self.common.as_context() 106 | } 107 | } 108 | 109 | impl AsContext for AppContext { 110 | fn as_context(&self) -> Cow<'_, WebMocker> { 111 | self.common.as_context() 112 | } 113 | } 114 | 115 | #[derive(Debug, Clone)] 116 | pub struct MiddlewareContext { 117 | time: TimeMocker, 118 | web: WebMocker, 119 | } 120 | 121 | impl Context for MiddlewareContext {} 122 | 123 | impl AsContext for MiddlewareContext { 124 | fn as_context(&self) -> Cow<'_, TimeMocker> { 125 | Cow::Borrowed(&self.time) 126 | } 127 | } 128 | 129 | impl AsContext for MiddlewareContext { 130 | fn as_context(&self) -> Cow<'_, WebMocker> { 131 | Cow::Borrowed(&self.web) 132 | } 133 | } 134 | 135 | mod external { 136 | #[derive(Debug, Clone)] 137 | pub struct TimeMocker(()); 138 | 139 | #[derive(Debug, Clone)] 140 | pub struct WebMocker(()); 141 | 142 | #[derive(Debug, Clone)] 143 | pub struct PgConnection(()); 144 | 145 | #[derive(Debug, Clone)] 146 | pub struct ServerConfig(()); 147 | 148 | impl super::Context for TimeMocker {} 149 | impl super::Context for WebMocker {} 150 | impl super::Context for PgConnection {} 151 | impl super::Context for ServerConfig {} 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/nails/src/routing.rs: -------------------------------------------------------------------------------- 1 | use futures::prelude::*; 2 | 3 | use std::fmt; 4 | use std::marker::PhantomData; 5 | 6 | use async_trait::async_trait; 7 | use contextful::Context; 8 | use hyper::{Body, Method, Request, Response}; 9 | 10 | use crate::error::NailsError; 11 | use crate::request::Preroute; 12 | 13 | pub struct Router 14 | where 15 | Ctx: Context + Send + Sync + 'static, 16 | { 17 | routes: Vec + Send + Sync + 'static>>, 18 | _marker: PhantomData, 19 | } 20 | 21 | impl Router 22 | where 23 | Ctx: Context + Send + Sync + 'static, 24 | { 25 | pub fn new() -> Self { 26 | Self { 27 | routes: Vec::new(), 28 | _marker: PhantomData, 29 | } 30 | } 31 | 32 | pub fn add_route(&mut self, route: R) 33 | where 34 | R: Routable + Send + Sync + 'static, 35 | { 36 | self.routes.push(Box::new(route)); 37 | } 38 | 39 | pub fn add_function_route(&mut self, route: F) 40 | where 41 | F: Fn(Ctx, Req) -> Fut + Send + Sync + 'static, 42 | Fut: Future, NailsError>> + Send + 'static, 43 | Req: Preroute + Send + 'static, 44 | { 45 | self.add_route(FunctionRoute::new(route)) 46 | } 47 | } 48 | 49 | impl fmt::Debug for Router 50 | where 51 | Ctx: Context + Send + Sync + 'static, 52 | { 53 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 54 | f.debug_struct("Router").finish() 55 | } 56 | } 57 | 58 | #[async_trait] 59 | impl Routable for Router 60 | where 61 | Ctx: Context + Send + Sync + 'static, 62 | { 63 | type Ctx = Ctx; 64 | 65 | fn match_path(&self, method: &Method, path: &str) -> bool { 66 | self.routes 67 | .iter() 68 | .any(|route| route.match_path(method, path)) 69 | } 70 | async fn respond( 71 | &self, 72 | ctx: &Self::Ctx, 73 | req: Request, 74 | ) -> Result, NailsError> { 75 | let method = req.method(); 76 | let path = req.uri().path(); 77 | let mut matched_route = None; 78 | for route in &self.routes { 79 | if route.match_path(method, path) { 80 | if matched_route.is_some() { 81 | // TODO: make this Err 82 | panic!("multiple matching routes"); 83 | } 84 | matched_route = Some(route); 85 | } 86 | } 87 | matched_route 88 | .expect("no route matched") 89 | .respond(&ctx, req) 90 | .await 91 | } 92 | } 93 | 94 | #[async_trait] 95 | pub trait Routable { 96 | type Ctx: Context + Send + Sync + 'static; 97 | 98 | fn path_prefix_hint(&self) -> &str { 99 | "" 100 | } 101 | fn match_path(&self, method: &Method, path: &str) -> bool; 102 | // TODO: Result 103 | async fn respond( 104 | &self, 105 | ctx: &Self::Ctx, 106 | req: Request, 107 | ) -> Result, NailsError>; 108 | } 109 | 110 | pub struct FunctionRoute { 111 | f: F, 112 | _marker: PhantomData, 113 | } 114 | 115 | impl FunctionRoute 116 | where 117 | Ctx: Context + Send + Sync + 'static, 118 | F: Fn(Ctx, Req) -> Fut + Send + Sync, 119 | Fut: Future, NailsError>> + Send + 'static, 120 | Req: Preroute + Send, 121 | { 122 | pub fn new(f: F) -> Self { 123 | Self { 124 | f, 125 | _marker: PhantomData, 126 | } 127 | } 128 | } 129 | 130 | #[async_trait] 131 | impl Routable for FunctionRoute 132 | where 133 | Ctx: Context + Send + Sync + 'static, 134 | F: Fn(Ctx, Req) -> Fut + Send + Sync, 135 | Fut: Future, NailsError>> + Send + 'static, 136 | Req: Preroute + Send, 137 | { 138 | type Ctx = Ctx; 139 | 140 | fn path_prefix_hint(&self) -> &str { 141 | Req::path_prefix_hint() 142 | } 143 | 144 | fn match_path(&self, method: &Method, path: &str) -> bool { 145 | Req::match_path(method, path) 146 | } 147 | 148 | async fn respond( 149 | &self, 150 | ctx: &Self::Ctx, 151 | req: Request, 152 | ) -> Result, NailsError> { 153 | let req = Preroute::from_request(req).await?; 154 | (self.f)(ctx.clone(), req).await 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /lib/nails/src/service.rs: -------------------------------------------------------------------------------- 1 | use futures::prelude::*; 2 | 3 | use std::sync::Arc; 4 | 5 | use contextful::Context; 6 | use futures::task::Poll; 7 | use hyper::client::service::Service as HyperService; 8 | use hyper::{Body, Method, Request, Response, StatusCode}; 9 | 10 | use crate::error::NailsError; 11 | use crate::request::Preroute; 12 | use crate::routing::{Routable, Router}; 13 | 14 | #[derive(Debug)] 15 | pub struct ServiceWithContext 16 | where 17 | Ctx: Context + Send + Sync + 'static, 18 | { 19 | pub service: Service, 20 | pub ctx: Ctx, 21 | } 22 | 23 | impl Clone for ServiceWithContext 24 | where 25 | Ctx: Context + Send + Sync + 'static, 26 | { 27 | fn clone(&self) -> Self { 28 | Self { 29 | service: self.service.clone(), 30 | ctx: self.ctx.clone(), 31 | } 32 | } 33 | } 34 | 35 | impl<'a, Ctx, HyperCtx> HyperService<&'a HyperCtx> for ServiceWithContext 36 | where 37 | Ctx: Context + Send + Sync + 'static, 38 | { 39 | type Response = ServiceWithContext; 40 | type Error = Box; 41 | type Future = future::Ready>; 42 | 43 | fn call(&mut self, _ctx: &HyperCtx) -> Self::Future { 44 | future::ok(self.clone()) 45 | } 46 | 47 | // TODO: implement poll_ready for better connection throttling 48 | fn poll_ready( 49 | &mut self, 50 | _cx: &mut futures::task::Context<'_>, 51 | ) -> Poll> { 52 | Poll::Ready(Ok(())) 53 | } 54 | } 55 | 56 | impl<'a, Ctx> HyperService> for ServiceWithContext 57 | where 58 | Ctx: Context + Send + Sync + 'static, 59 | { 60 | type Response = Response; 61 | type Error = Box; 62 | type Future = future::BoxFuture<'static, Result>; 63 | 64 | fn call(&mut self, req: Request) -> Self::Future { 65 | let inner = self.service.inner.clone(); 66 | let ctx = self.ctx.clone(); 67 | async move { inner.respond(&ctx, req).await }.boxed() 68 | } 69 | 70 | // TODO: implement poll_ready for better connection throttling 71 | fn poll_ready( 72 | &mut self, 73 | _cx: &mut futures::task::Context<'_>, 74 | ) -> Poll> { 75 | Poll::Ready(Ok(())) 76 | } 77 | } 78 | 79 | #[derive(Debug)] 80 | pub struct Service 81 | where 82 | Ctx: Context + Send + Sync + 'static, 83 | { 84 | inner: Arc>, 85 | } 86 | 87 | impl Service 88 | where 89 | Ctx: Context + Send + Sync + 'static, 90 | { 91 | pub fn builder() -> Builder { 92 | Builder::new() 93 | } 94 | 95 | pub fn with_context(self, ctx: &Ctx) -> ServiceWithContext { 96 | ServiceWithContext { 97 | service: self, 98 | ctx: ctx.clone(), 99 | } 100 | } 101 | } 102 | 103 | impl Clone for Service 104 | where 105 | Ctx: Context + Send + Sync + 'static, 106 | { 107 | fn clone(&self) -> Self { 108 | Self { 109 | inner: self.inner.clone(), 110 | } 111 | } 112 | } 113 | 114 | #[derive(Debug)] 115 | pub struct Builder 116 | where 117 | Ctx: Context + Send + Sync + 'static, 118 | { 119 | inner: Option>, 120 | } 121 | 122 | impl Builder 123 | where 124 | Ctx: Context + Send + Sync + 'static, 125 | { 126 | pub fn new() -> Self { 127 | Self { 128 | inner: Some(ServiceInner { 129 | router: Router::new(), 130 | }), 131 | } 132 | } 133 | 134 | pub fn finish(&mut self) -> Service { 135 | let inner = self.inner.take().expect("Builder::finish called twice"); 136 | Service { 137 | inner: Arc::new(inner), 138 | } 139 | } 140 | 141 | fn inner_mut(&mut self) -> &mut ServiceInner { 142 | self.inner 143 | .as_mut() 144 | .expect("this builder is already finished") 145 | } 146 | 147 | pub fn add_route(&mut self, route: R) -> &mut Self 148 | where 149 | R: Routable + Send + Sync + 'static, 150 | { 151 | self.inner_mut().router.add_route(route); 152 | self 153 | } 154 | 155 | pub fn add_function_route(&mut self, route: F) -> &mut Self 156 | where 157 | F: Fn(Ctx, Req) -> Fut + Send + Sync + 'static, 158 | Fut: Future, NailsError>> + Send + 'static, 159 | Req: Preroute + Send + 'static, 160 | { 161 | self.inner_mut().router.add_function_route(route); 162 | self 163 | } 164 | } 165 | 166 | #[derive(Debug)] 167 | struct ServiceInner 168 | where 169 | Ctx: Context + Send + Sync + 'static, 170 | { 171 | router: Router, 172 | } 173 | 174 | impl ServiceInner 175 | where 176 | Ctx: Context + Send + Sync + 'static, 177 | { 178 | async fn respond( 179 | &self, 180 | ctx: &Ctx, 181 | req: Request, 182 | ) -> Result, Box> { 183 | if req.method() == Method::OPTIONS && req.headers().get("Origin").is_some() { 184 | // CORS hack. 185 | // TODO: move this out to middleware 186 | return Ok(Response::builder() 187 | .status(StatusCode::OK) 188 | .header("Access-Control-Allow-Origin", "*") 189 | .header("Access-Control-Allow-Methods", "*") 190 | .header("Access-Control-Allow-Headers", "*") 191 | .body(Body::empty()) 192 | .unwrap()); 193 | } 194 | let resp = if self.router.match_path(req.method(), req.uri().path()) { 195 | match self.router.respond(ctx, req).await { 196 | Ok(resp) => resp, 197 | Err(e) => e.to_response(), 198 | } 199 | } else { 200 | Response::builder() 201 | .status(StatusCode::NOT_FOUND) 202 | .body(Body::from("Not Found")) 203 | .unwrap() 204 | }; 205 | let resp = { 206 | let mut resp = resp; 207 | // CORS hack. 208 | // TODO: move this out to middleware 209 | resp.headers_mut() 210 | .append("Access-Control-Allow-Origin", "*".parse().unwrap()); 211 | resp 212 | }; 213 | Ok(resp) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /lib/nails/src/request.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::Entry; 2 | use std::collections::HashMap; 3 | use std::slice; 4 | 5 | use async_trait::async_trait; 6 | use hyper::{Body, Method, Request}; 7 | use serde::de::DeserializeOwned; 8 | 9 | use crate::error::{BodyError, ContentTypeError, JsonBodyError, NailsError, QueryError}; 10 | 11 | pub use nails_derive::Preroute; 12 | 13 | #[async_trait] 14 | pub trait Preroute: Sized { 15 | fn path_prefix_hint() -> &'static str { 16 | "" 17 | } 18 | fn match_path(method: &Method, path: &str) -> bool; 19 | 20 | // TODO: Request -> RoutableRequest 21 | async fn from_request(req: Request) -> Result; 22 | } 23 | 24 | #[async_trait] 25 | pub trait FromBody: Sized { 26 | async fn from_body(req: Request) -> Result; 27 | } 28 | 29 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] 30 | pub struct JsonBody(pub T); 31 | 32 | #[async_trait] 33 | impl FromBody for JsonBody 34 | where 35 | T: DeserializeOwned, 36 | { 37 | async fn from_body(req: Request) -> Result { 38 | if let Some(content_type) = req.headers().get("Content-Type") { 39 | if content_type != "application/json" { 40 | let content_type = String::from_utf8_lossy(content_type.as_bytes()).into_owned(); 41 | return Err(ContentTypeError { 42 | expected: vec!["application/json".to_owned()], 43 | got: Some(content_type), 44 | } 45 | .into()); 46 | } 47 | } else { 48 | return Err(ContentTypeError { 49 | expected: vec!["application/json".to_owned()], 50 | got: None, 51 | } 52 | .into()); 53 | } 54 | let mut body = req.into_body(); 55 | let mut buf = Vec::new(); 56 | while let Some(chunk) = body.next().await { 57 | let chunk = chunk.map_err(BodyError)?; 58 | buf.extend_from_slice(chunk.as_ref()); 59 | } 60 | let data = serde_json::from_slice(&buf).map_err(JsonBodyError)?; 61 | Ok(JsonBody(data)) 62 | } 63 | } 64 | 65 | pub trait FromPath: Sized { 66 | fn from_path(path_component: &str) -> Result; 67 | 68 | fn matches(path_component: &str) -> bool { 69 | Self::from_path(path_component).is_ok() 70 | } 71 | } 72 | 73 | impl FromPath for String { 74 | fn from_path(path_component: &str) -> Result { 75 | Ok(path_component.to_owned()) 76 | } 77 | fn matches(_path_component: &str) -> bool { 78 | true 79 | } 80 | } 81 | 82 | macro_rules! from_path_int_matcher { 83 | ($($int:ty)*) => { 84 | $( 85 | impl FromPath for $int { 86 | fn from_path(path_component: &str) -> Result { 87 | path_component.parse::<$int>().map_err(|_| ()) 88 | } 89 | } 90 | )* 91 | }; 92 | } 93 | from_path_int_matcher!(u8 u16 u32 u64 u128 i8 i16 i32 i64 i128); 94 | 95 | pub trait FromQuery: Sized { 96 | // TODO: Result 97 | fn from_query(values: &[String]) -> Result; 98 | } 99 | 100 | fn require_one(values: &[String]) -> Result<&str, QueryError> { 101 | if values.len() > 1 { 102 | return Err(QueryError::MultipleQuery); 103 | } else if values.len() < 1 { 104 | return Err(QueryError::NoQuery); 105 | } 106 | Ok(&values[0]) 107 | } 108 | 109 | impl FromQuery for Vec 110 | where 111 | T: FromQuery, 112 | { 113 | fn from_query(values: &[String]) -> Result { 114 | values 115 | .iter() 116 | .map(|x| T::from_query(slice::from_ref(x))) 117 | .collect() 118 | } 119 | } 120 | 121 | impl FromQuery for Option 122 | where 123 | T: FromQuery, 124 | { 125 | fn from_query(values: &[String]) -> Result { 126 | if values.is_empty() { 127 | Ok(None) 128 | } else { 129 | Ok(Some(T::from_query(values)?)) 130 | } 131 | } 132 | } 133 | 134 | impl FromQuery for String { 135 | fn from_query(values: &[String]) -> Result { 136 | Ok(require_one(values)?.to_owned()) 137 | } 138 | } 139 | 140 | macro_rules! impl_int_from_query { 141 | ($T:ty) => { 142 | impl FromQuery for $T { 143 | fn from_query(values: &[String]) -> Result { 144 | Ok(require_one(values)?.parse()?) 145 | } 146 | } 147 | }; 148 | } 149 | impl_int_from_query!(i8); 150 | impl_int_from_query!(i16); 151 | impl_int_from_query!(i32); 152 | impl_int_from_query!(i64); 153 | impl_int_from_query!(i128); 154 | impl_int_from_query!(isize); 155 | impl_int_from_query!(u8); 156 | impl_int_from_query!(u16); 157 | impl_int_from_query!(u32); 158 | impl_int_from_query!(u64); 159 | impl_int_from_query!(u128); 160 | impl_int_from_query!(usize); 161 | 162 | // TODO: rails-like decoding 163 | // TODO: consider less-allocation way to decode query 164 | // TODO: handle illformed keys and values 165 | pub fn parse_query(query: &str) -> HashMap> { 166 | let mut hash: HashMap> = HashMap::new(); 167 | for pair in query.split("&") { 168 | let (key, value) = if let Some(pair) = parse_query_pair(pair) { 169 | pair 170 | } else { 171 | // TODO: handle errors 172 | continue; 173 | }; 174 | match hash.entry(key) { 175 | Entry::Occupied(mut entry) => { 176 | entry.get_mut().push(value); 177 | } 178 | Entry::Vacant(entry) => { 179 | entry.insert(vec![value]); 180 | } 181 | } 182 | } 183 | hash 184 | } 185 | 186 | // TODO: optimize 187 | // TODO: better error handling 188 | fn parse_query_pair(pair: &str) -> Option<(String, String)> { 189 | let mut kv_iter = pair.split("="); 190 | let key = kv_iter.next().unwrap(); // Split always yields some element 191 | let value = kv_iter.next()?; 192 | if kv_iter.next().is_some() { 193 | return None; 194 | } 195 | let key = parse_percent_encoding(key)?; 196 | let value = parse_percent_encoding(value)?; 197 | Some((key, value)) 198 | } 199 | 200 | // TODO: optimize 201 | // TODO: better error handling 202 | fn parse_percent_encoding(input: &str) -> Option { 203 | let input = input.as_bytes(); 204 | let mut output = Vec::new(); 205 | let mut i = 0; 206 | while i < input.len() { 207 | if input[i] == b'%' { 208 | if i + 3 > input.len() { 209 | return None; 210 | } 211 | let d0 = input[i + 1]; 212 | let d1 = input[i + 2]; 213 | if !(d0.is_ascii_hexdigit() && d1.is_ascii_hexdigit()) { 214 | return None; 215 | } 216 | let d0 = (d0 as char).to_digit(16).unwrap(); 217 | let d1 = (d1 as char).to_digit(16).unwrap(); 218 | output.push((d0 * 16 + d1) as u8); 219 | i += 3; 220 | } else { 221 | output.push(input[i]); 222 | i += 1; 223 | } 224 | } 225 | String::from_utf8(output).ok() 226 | } 227 | 228 | #[cfg(test)] 229 | #[cfg_attr(tarpaulin, skip)] 230 | mod tests { 231 | use super::*; 232 | 233 | macro_rules! hash { 234 | ($($e:expr),*) => { 235 | vec![$($e,)*].into_iter().collect::>() 236 | }; 237 | ($($e:expr,)*) => { 238 | vec![$($e,)*].into_iter().collect::>() 239 | }; 240 | } 241 | 242 | #[test] 243 | fn test_from_path() { 244 | assert_eq!(String::from_path("%20あ"), Ok(S("%20あ"))); 245 | assert_eq!(i32::from_path("20"), Ok(20)); 246 | assert_eq!(i32::from_path("08"), Ok(8)); 247 | assert_eq!(i32::from_path("-2"), Ok(-2)); 248 | assert_eq!(i32::from_path(" 5"), Err(())); 249 | assert_eq!(i8::from_path("200"), Err(())); 250 | assert_eq!(u32::from_path("-1"), Err(())); 251 | 252 | assert!(String::matches("%20あ")); 253 | assert!(i32::matches("20")); 254 | assert!(i32::matches("08")); 255 | assert!(i32::matches("-2")); 256 | assert!(!i32::matches(" 5")); 257 | assert!(!i8::matches("200")); 258 | assert!(!u32::matches("-1")); 259 | } 260 | 261 | #[test] 262 | fn test_from_query() { 263 | assert_eq!(String::from_query(&[]).ok(), None); 264 | assert_eq!(String::from_query(&[S("foo")]).ok(), Some(S("foo"))); 265 | assert_eq!(String::from_query(&[S("foo"), S("bar")]).ok(), None); 266 | assert_eq!(Option::::from_query(&[]).ok(), Some(None)); 267 | assert_eq!( 268 | Option::::from_query(&[S("foo")]).ok(), 269 | Some(Some(S("foo"))) 270 | ); 271 | assert_eq!( 272 | Option::::from_query(&[S("foo"), S("bar")]).ok(), 273 | None 274 | ); 275 | assert_eq!(i32::from_query(&[S("42")]).ok(), Some(42)); 276 | assert_eq!(i32::from_query(&[S("42"), S("42")]).ok(), None); 277 | assert_eq!(i32::from_query(&[S("4x2")]).ok(), None); 278 | assert_eq!(i32::from_query(&[]).ok(), None); 279 | assert_eq!(Vec::::from_query(&[]).ok(), Some(vec![])); 280 | assert_eq!(Vec::::from_query(&[S("42")]).ok(), Some(vec![42])); 281 | assert_eq!( 282 | Vec::::from_query(&[S("42"), S("42")]).ok(), 283 | Some(vec![42, 42]) 284 | ); 285 | } 286 | 287 | #[test] 288 | fn test_parse_query() { 289 | assert_eq!(parse_query("foo=bar"), hash![(S("foo"), vec![S("bar")])]); 290 | assert_eq!( 291 | parse_query("foo=bar&foo=baz"), 292 | hash![(S("foo"), vec![S("bar"), S("baz")])], 293 | ); 294 | assert_eq!( 295 | parse_query("foo=bar&foo2=baz"), 296 | hash![(S("foo"), vec![S("bar")]), (S("foo2"), vec![S("baz")])], 297 | ); 298 | assert_eq!(parse_query("foo&foo=foo=foo&f%oo=1&1=%E3"), hash![]); 299 | } 300 | 301 | #[test] 302 | fn test_parse_percent_encoding() { 303 | assert_eq!(parse_percent_encoding("foo"), Some(S("foo"))); 304 | assert_eq!(parse_percent_encoding("f%6F%6f"), Some(S("foo"))); 305 | assert_eq!(parse_percent_encoding("あ"), Some(S("あ"))); 306 | assert_eq!(parse_percent_encoding("%E3%81%82"), Some(S("あ"))); 307 | } 308 | 309 | #[test] 310 | fn test_parse_percent_encoding_failing() { 311 | assert_eq!(parse_percent_encoding("fo%"), None); 312 | assert_eq!(parse_percent_encoding("%kv"), None); 313 | assert_eq!(parse_percent_encoding("%E3%81"), None); 314 | } 315 | 316 | #[allow(non_snake_case)] 317 | fn S(s: &'static str) -> String { 318 | s.to_owned() 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /lib/nails/src/error.rs: -------------------------------------------------------------------------------- 1 | use hyper::{Body, Response, StatusCode}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::any::Any; 4 | use std::fmt; 5 | 6 | pub trait ServiceError: std::error::Error + Any + Send + Sync { 7 | fn status(&self) -> StatusCode; 8 | fn class_name(&self) -> &str; 9 | fn has_public_message(&self) -> bool { 10 | false 11 | } 12 | fn fmt_public_message(&self, f: &mut fmt::Formatter) -> fmt::Result { 13 | drop(f); 14 | Ok(()) 15 | } 16 | } 17 | 18 | pub trait ServiceErrorExt: ServiceError { 19 | fn public_message(&self) -> Option> { 20 | if self.has_public_message() { 21 | Some(PublicMessage(self)) 22 | } else { 23 | None 24 | } 25 | } 26 | } 27 | impl ServiceErrorExt for T {} 28 | 29 | pub struct PublicMessage<'a, E: ServiceError + ?Sized>(&'a E); 30 | 31 | impl fmt::Display for PublicMessage<'_, E> { 32 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 33 | self.0.fmt_public_message(f) 34 | } 35 | } 36 | 37 | #[derive(Debug)] 38 | pub enum NailsError { 39 | ContentTypeError(ContentTypeError), 40 | JsonBodyError(JsonBodyError), 41 | BodyError(BodyError), 42 | QueryError(QueryError), 43 | AnyError(Box), 44 | } 45 | 46 | impl NailsError { 47 | // TODO: use Accept header from request 48 | pub fn to_response(&self) -> Response { 49 | Response::builder() 50 | .header("Content-Type", "application/json") 51 | .body(Body::from( 52 | serde_json::to_string(&ErrorBody { 53 | error: self.class_name().to_owned(), 54 | message: self 55 | .public_message() 56 | .map(|m| m.to_string()) 57 | .unwrap_or_else(|| "error".to_string()), 58 | }) 59 | .unwrap(), 60 | )) 61 | .unwrap() 62 | } 63 | } 64 | 65 | impl ServiceError for NailsError { 66 | fn status(&self) -> StatusCode { 67 | use NailsError::*; 68 | match self { 69 | ContentTypeError(e) => e.status(), 70 | JsonBodyError(e) => e.status(), 71 | BodyError(e) => e.status(), 72 | QueryError(e) => e.status(), 73 | AnyError(e) => e.status(), 74 | } 75 | } 76 | fn class_name(&self) -> &str { 77 | use NailsError::*; 78 | match self { 79 | ContentTypeError(e) => e.class_name(), 80 | JsonBodyError(e) => e.class_name(), 81 | BodyError(e) => e.class_name(), 82 | QueryError(e) => e.class_name(), 83 | AnyError(e) => e.class_name(), 84 | } 85 | } 86 | fn has_public_message(&self) -> bool { 87 | use NailsError::*; 88 | match self { 89 | ContentTypeError(e) => e.has_public_message(), 90 | JsonBodyError(e) => e.has_public_message(), 91 | BodyError(e) => e.has_public_message(), 92 | QueryError(e) => e.has_public_message(), 93 | AnyError(e) => e.has_public_message(), 94 | } 95 | } 96 | fn fmt_public_message(&self, f: &mut fmt::Formatter) -> fmt::Result { 97 | use NailsError::*; 98 | match self { 99 | ContentTypeError(e) => e.fmt_public_message(f), 100 | JsonBodyError(e) => e.fmt_public_message(f), 101 | BodyError(e) => e.fmt_public_message(f), 102 | QueryError(e) => e.fmt_public_message(f), 103 | AnyError(e) => e.fmt_public_message(f), 104 | } 105 | } 106 | } 107 | 108 | impl std::error::Error for NailsError { 109 | fn description(&self) -> &str { 110 | use NailsError::*; 111 | match self { 112 | ContentTypeError(e) => e.description(), 113 | JsonBodyError(e) => e.description(), 114 | BodyError(e) => e.description(), 115 | QueryError(e) => e.description(), 116 | AnyError(e) => e.description(), 117 | } 118 | } 119 | 120 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 121 | use NailsError::*; 122 | match self { 123 | ContentTypeError(e) => e.source(), 124 | JsonBodyError(e) => e.source(), 125 | BodyError(e) => e.source(), 126 | QueryError(e) => e.source(), 127 | AnyError(e) => e.source(), 128 | } 129 | } 130 | } 131 | 132 | impl fmt::Display for NailsError { 133 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 134 | use NailsError::*; 135 | match self { 136 | ContentTypeError(e) => e.fmt(f), 137 | JsonBodyError(e) => e.fmt(f), 138 | BodyError(e) => e.fmt(f), 139 | QueryError(e) => e.fmt(f), 140 | AnyError(e) => e.fmt(f), 141 | } 142 | } 143 | } 144 | 145 | impl From for NailsError { 146 | fn from(e: ContentTypeError) -> Self { 147 | NailsError::ContentTypeError(e) 148 | } 149 | } 150 | 151 | impl From for NailsError { 152 | fn from(e: JsonBodyError) -> Self { 153 | NailsError::JsonBodyError(e) 154 | } 155 | } 156 | 157 | impl From for NailsError { 158 | fn from(e: QueryError) -> Self { 159 | NailsError::QueryError(e) 160 | } 161 | } 162 | 163 | impl From for NailsError { 164 | fn from(e: BodyError) -> Self { 165 | NailsError::BodyError(e) 166 | } 167 | } 168 | 169 | #[derive(Debug, Clone, Serialize, Deserialize)] 170 | struct ErrorBody { 171 | error: String, 172 | message: String, 173 | } 174 | 175 | #[derive(Debug)] 176 | pub struct ContentTypeError { 177 | pub expected: Vec, 178 | pub got: Option, 179 | } 180 | 181 | impl ServiceError for ContentTypeError { 182 | fn status(&self) -> StatusCode { 183 | StatusCode::UNSUPPORTED_MEDIA_TYPE 184 | } 185 | fn class_name(&self) -> &str { 186 | "nails::error::ContentTypeError" 187 | } 188 | fn has_public_message(&self) -> bool { 189 | true 190 | } 191 | fn fmt_public_message(&self, f: &mut fmt::Formatter) -> fmt::Result { 192 | fmt::Display::fmt(self, f) 193 | } 194 | } 195 | 196 | impl fmt::Display for ContentTypeError { 197 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 198 | let Self { expected, got } = self; 199 | write!(f, "Invalid Content-Type: expected ")?; 200 | if expected.is_empty() { 201 | write!(f, "nothing")?; 202 | } else if expected.len() == 1 { 203 | write!(f, "{:?}", expected[0])?; 204 | } else { 205 | for ct in &expected[..expected.len() - 2] { 206 | write!(f, "{:?}, ", ct)?; 207 | } 208 | write!( 209 | f, 210 | "{:?} and {:?}", 211 | expected[expected.len() - 2], 212 | expected[expected.len() - 1], 213 | )?; 214 | } 215 | if let Some(got) = got { 216 | write!(f, ", got {:?}", got)?; 217 | } else { 218 | write!(f, ", got nothing")?; 219 | } 220 | Ok(()) 221 | } 222 | } 223 | 224 | impl std::error::Error for ContentTypeError { 225 | fn description(&self) -> &str { 226 | "Invalid Content-Type" 227 | } 228 | } 229 | 230 | #[derive(Debug)] 231 | pub struct JsonBodyError(pub serde_json::Error); 232 | 233 | impl ServiceError for JsonBodyError { 234 | fn status(&self) -> StatusCode { 235 | StatusCode::BAD_REQUEST 236 | } 237 | fn class_name(&self) -> &str { 238 | "nails::error::JsonBodyError" 239 | } 240 | fn has_public_message(&self) -> bool { 241 | true 242 | } 243 | fn fmt_public_message(&self, f: &mut fmt::Formatter) -> fmt::Result { 244 | fmt::Display::fmt(self, f) 245 | } 246 | } 247 | 248 | impl fmt::Display for JsonBodyError { 249 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 250 | write!(f, "Error in JSON Body: {}", self.0) 251 | } 252 | } 253 | 254 | impl std::error::Error for JsonBodyError { 255 | fn description(&self) -> &str { 256 | "Error in JSON Body" 257 | } 258 | 259 | fn cause(&self) -> Option<&dyn std::error::Error> { 260 | Some(&self.0) 261 | } 262 | 263 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 264 | Some(&self.0) 265 | } 266 | } 267 | 268 | #[derive(Debug)] 269 | pub struct BodyError(pub hyper::Error); 270 | 271 | impl ServiceError for BodyError { 272 | fn status(&self) -> StatusCode { 273 | StatusCode::INTERNAL_SERVER_ERROR 274 | } 275 | fn class_name(&self) -> &str { 276 | "nails::error::BodyError" 277 | } 278 | } 279 | 280 | impl fmt::Display for BodyError { 281 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 282 | write!(f, "Error reading request body: {}", self.0) 283 | } 284 | } 285 | 286 | impl std::error::Error for BodyError { 287 | fn description(&self) -> &str { 288 | "Error reading request body" 289 | } 290 | 291 | fn cause(&self) -> Option<&dyn std::error::Error> { 292 | Some(&self.0) 293 | } 294 | 295 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 296 | Some(&self.0) 297 | } 298 | } 299 | 300 | #[derive(Debug)] 301 | pub enum QueryError { 302 | MultipleQuery, 303 | NoQuery, 304 | ParseIntError(std::num::ParseIntError), 305 | ParseFloatError(std::num::ParseFloatError), 306 | AnyError(failure::Error), 307 | } 308 | 309 | impl ServiceError for QueryError { 310 | fn status(&self) -> StatusCode { 311 | StatusCode::BAD_REQUEST 312 | } 313 | fn class_name(&self) -> &str { 314 | "nails::error::QueryError" 315 | } 316 | fn has_public_message(&self) -> bool { 317 | true 318 | } 319 | fn fmt_public_message(&self, f: &mut fmt::Formatter) -> fmt::Result { 320 | fmt::Display::fmt(self, f) 321 | } 322 | } 323 | 324 | impl fmt::Display for QueryError { 325 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 326 | use QueryError::*; 327 | match self { 328 | MultipleQuery => write!(f, "multiple query values found"), 329 | NoQuery => write!(f, "no query value found"), 330 | ParseIntError(e) => write!(f, "{}", e), 331 | ParseFloatError(e) => write!(f, "{}", e), 332 | AnyError(e) => write!(f, "{}", e), 333 | } 334 | } 335 | } 336 | 337 | impl std::error::Error for QueryError { 338 | fn description(&self) -> &str { 339 | use QueryError::*; 340 | match self { 341 | MultipleQuery => "multiple query values found", 342 | NoQuery => "no query value found", 343 | ParseIntError(e) => e.description(), 344 | ParseFloatError(e) => e.description(), 345 | AnyError(_) => "some error", 346 | } 347 | } 348 | } 349 | 350 | impl From for QueryError { 351 | fn from(e: std::num::ParseIntError) -> Self { 352 | QueryError::ParseIntError(e) 353 | } 354 | } 355 | 356 | impl From for QueryError { 357 | fn from(e: std::num::ParseFloatError) -> Self { 358 | QueryError::ParseFloatError(e) 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /lib/nails_derive/src/attrs.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use syn::spanned::Spanned; 3 | use syn::{Attribute, Lit, LitStr, Meta, NestedMeta}; 4 | 5 | #[cfg(feature = "proc_macro_diagnostics")] 6 | macro_rules! if_proc_macro_diagnostics { 7 | ($($x:tt)*) => { $($x)* }; 8 | } 9 | #[cfg(not(feature = "proc_macro_diagnostics"))] 10 | macro_rules! if_proc_macro_diagnostics { 11 | ($($x:tt)*) => {}; 12 | } 13 | 14 | #[derive(Debug, Clone, PartialEq, Eq)] 15 | pub(crate) struct StructAttrs { 16 | pub(crate) path: Option, 17 | pub(crate) method: Option, 18 | } 19 | 20 | impl StructAttrs { 21 | pub(crate) fn parse(attrs: &[Attribute]) -> syn::Result { 22 | let mut ret = Self { 23 | path: None, 24 | method: None, 25 | }; 26 | for attr in attrs { 27 | if !attr.path.is_ident("nails") { 28 | continue; 29 | } 30 | let meta = attr.parse_meta()?; 31 | let list = match meta { 32 | Meta::Path(path) => { 33 | return Err(syn::Error::new( 34 | path.span(), 35 | "#[nails] must have an argument list", 36 | )); 37 | } 38 | Meta::NameValue(nv) => { 39 | return Err(syn::Error::new( 40 | nv.span(), 41 | "#[nails] must have an argument list", 42 | )); 43 | } 44 | Meta::List(list) => list, 45 | }; 46 | if_proc_macro_diagnostics! { 47 | if list.nested.is_empty() { 48 | list.paren_token.span.unwrap().warning("#[nails()] is meaningless").emit(); 49 | } 50 | } 51 | for item in &list.nested { 52 | match item { 53 | NestedMeta::Meta(meta) => { 54 | ret.parse_inner(meta)?; 55 | } 56 | NestedMeta::Lit(lit) => { 57 | return Err(syn::Error::new(lit.span(), "unexpected literal")); 58 | } 59 | } 60 | } 61 | } 62 | Ok(ret) 63 | } 64 | 65 | fn parse_inner(&mut self, meta: &Meta) -> syn::Result<()> { 66 | let name = meta.path(); 67 | if name.is_ident("path") { 68 | self.parse_path(meta) 69 | } else if name.is_ident("method") { 70 | self.parse_method(meta) 71 | } else { 72 | return Err(syn::Error::new( 73 | meta.span(), 74 | format_args!("unknown option: `{}`", path_to_string(name)), 75 | )); 76 | } 77 | } 78 | 79 | fn parse_path(&mut self, meta: &Meta) -> syn::Result<()> { 80 | let lit = match meta { 81 | Meta::Path(path) => { 82 | return Err(syn::Error::new( 83 | path.span(), 84 | "string value expected in #[nails(path)]", 85 | )); 86 | } 87 | Meta::List(list) => { 88 | return Err(syn::Error::new( 89 | list.paren_token.span, 90 | "extra parentheses in #[nails(path)]", 91 | )); 92 | } 93 | Meta::NameValue(nv) => &nv.lit, 94 | }; 95 | if let Lit::Str(lit) = lit { 96 | if self.path.is_some() { 97 | return Err(syn::Error::new( 98 | lit.span(), 99 | "multiple #[nails(path)] definitions", 100 | )); 101 | } 102 | self.path = Some(PathInfo { path: lit.clone() }); 103 | Ok(()) 104 | } else { 105 | return Err(syn::Error::new( 106 | lit.span(), 107 | "string value expected in #[nails(path)]", 108 | )); 109 | } 110 | } 111 | 112 | fn parse_method(&mut self, meta: &Meta) -> syn::Result<()> { 113 | let lit = match meta { 114 | Meta::Path(path) => { 115 | return Err(syn::Error::new( 116 | path.span(), 117 | "string value expected in #[nails(method)]", 118 | )); 119 | } 120 | Meta::List(list) => { 121 | return Err(syn::Error::new( 122 | list.paren_token.span, 123 | "extra parentheses in #[nails(method)]", 124 | )); 125 | } 126 | Meta::NameValue(nv) => &nv.lit, 127 | }; 128 | if let Lit::Str(lit) = lit { 129 | if self.method.is_some() { 130 | return Err(syn::Error::new( 131 | lit.span(), 132 | "multiple #[nails(method)] definitions", 133 | )); 134 | } 135 | let lit_str = lit.value(); 136 | let kind = match lit_str.as_str() { 137 | "GET" => MethodKind::Get, 138 | "GET_ONLY" => MethodKind::GetOnly, 139 | "HEAD" => MethodKind::Head, 140 | "POST" => MethodKind::Post, 141 | "PUT" => MethodKind::Put, 142 | "DELETE" => MethodKind::Delete, 143 | "OPTIONS" => MethodKind::Options, 144 | "PATCH" => MethodKind::Patch, 145 | _ => return Err(syn::Error::new(lit.span(), "Unknown method type")), 146 | }; 147 | self.method = Some(MethodInfo { 148 | lit: lit.clone(), 149 | kind, 150 | }); 151 | Ok(()) 152 | } else { 153 | return Err(syn::Error::new( 154 | lit.span(), 155 | "string value expected in #[nails(path)]", 156 | )); 157 | } 158 | } 159 | } 160 | 161 | #[derive(Debug, Clone, PartialEq, Eq)] 162 | pub(crate) struct PathInfo { 163 | pub(crate) path: LitStr, 164 | } 165 | 166 | #[derive(Debug, Clone, PartialEq, Eq)] 167 | pub(crate) struct MethodInfo { 168 | pub(crate) lit: LitStr, 169 | pub(crate) kind: MethodKind, 170 | } 171 | 172 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 173 | pub(crate) enum MethodKind { 174 | Get, 175 | GetOnly, 176 | Head, 177 | Post, 178 | Put, 179 | Delete, 180 | Options, 181 | Patch, 182 | } 183 | 184 | #[derive(Debug, Clone, PartialEq, Eq)] 185 | pub(crate) struct FieldAttrs { 186 | pub(crate) query: Option, 187 | pub(crate) path: Option, 188 | pub(crate) body: Option, 189 | } 190 | 191 | impl FieldAttrs { 192 | pub(crate) fn parse(attrs: &[Attribute]) -> syn::Result { 193 | let mut ret = Self { 194 | query: None, 195 | path: None, 196 | body: None, 197 | }; 198 | for attr in attrs { 199 | if !attr.path.is_ident("nails") { 200 | continue; 201 | } 202 | let meta = attr.parse_meta()?; 203 | let list = match meta { 204 | Meta::Path(path) => { 205 | return Err(syn::Error::new( 206 | path.span(), 207 | "#[nails] must have an argument list", 208 | )); 209 | } 210 | Meta::NameValue(nv) => { 211 | return Err(syn::Error::new( 212 | nv.span(), 213 | "#[nails] must have an argument list", 214 | )); 215 | } 216 | Meta::List(list) => list, 217 | }; 218 | if_proc_macro_diagnostics! { 219 | if list.nested.is_empty() { 220 | list.paren_token.span.unwrap().warning("#[nails()] is meaningless").emit(); 221 | } 222 | } 223 | for item in &list.nested { 224 | match item { 225 | NestedMeta::Meta(meta) => { 226 | ret.parse_inner(meta)?; 227 | } 228 | NestedMeta::Lit(lit) => { 229 | return Err(syn::Error::new(lit.span(), "unexpected literal")); 230 | } 231 | } 232 | } 233 | } 234 | Ok(ret) 235 | } 236 | 237 | fn parse_inner(&mut self, meta: &Meta) -> syn::Result<()> { 238 | let name = meta.path(); 239 | if name.is_ident("query") { 240 | self.parse_query(meta) 241 | } else if name.is_ident("path") { 242 | self.parse_path(meta) 243 | } else if name.is_ident("body") { 244 | self.parse_body(meta) 245 | } else { 246 | return Err(syn::Error::new( 247 | meta.span(), 248 | format_args!("unknown option: `{}`", path_to_string(name)), 249 | )); 250 | } 251 | } 252 | 253 | fn parse_query(&mut self, meta: &Meta) -> syn::Result<()> { 254 | let (lit, span) = match meta { 255 | Meta::Path(path) => (None, path.span()), 256 | Meta::List(list) => { 257 | return Err(syn::Error::new( 258 | list.paren_token.span, 259 | "extra parentheses in #[nails(query)]", 260 | )); 261 | } 262 | Meta::NameValue(nv) => { 263 | if let Lit::Str(lit) = &nv.lit { 264 | (Some(lit.clone()), nv.span()) 265 | } else { 266 | return Err(syn::Error::new( 267 | nv.lit.span(), 268 | "string value or no value expected in #[nails(query)]", 269 | )); 270 | } 271 | } 272 | }; 273 | if self.query.is_some() { 274 | return Err(syn::Error::new( 275 | lit.span(), 276 | "multiple #[nails(query)] definitions", 277 | )); 278 | } 279 | self.query = Some(QueryFieldInfo { name: lit, span }); 280 | Ok(()) 281 | } 282 | 283 | fn parse_path(&mut self, meta: &Meta) -> syn::Result<()> { 284 | let (lit, span) = match meta { 285 | Meta::Path(path) => (None, path.span()), 286 | Meta::List(list) => { 287 | return Err(syn::Error::new( 288 | list.paren_token.span, 289 | "extra parentheses in #[nails(path)]", 290 | )); 291 | } 292 | Meta::NameValue(nv) => { 293 | if let Lit::Str(lit) = &nv.lit { 294 | (Some(lit.clone()), nv.span()) 295 | } else { 296 | return Err(syn::Error::new( 297 | nv.lit.span(), 298 | "string value or no value expected in #[nails(path)]", 299 | )); 300 | } 301 | } 302 | }; 303 | if self.path.is_some() { 304 | return Err(syn::Error::new( 305 | lit.span(), 306 | "multiple #[nails(path)] definitions", 307 | )); 308 | } 309 | self.path = Some(PathFieldInfo { name: lit, span }); 310 | Ok(()) 311 | } 312 | 313 | fn parse_body(&mut self, meta: &Meta) -> syn::Result<()> { 314 | let span = match meta { 315 | Meta::Path(path) => path.span(), 316 | Meta::List(list) => { 317 | return Err(syn::Error::new( 318 | list.paren_token.span, 319 | "extra parentheses in #[nails(body)]", 320 | )); 321 | } 322 | Meta::NameValue(nv) => { 323 | return Err(syn::Error::new( 324 | nv.lit.span(), 325 | "no value expected in #[nails(body)]", 326 | )); 327 | } 328 | }; 329 | if self.body.is_some() { 330 | return Err(syn::Error::new(span, "multiple #[nails(body)] definitions")); 331 | } 332 | self.body = Some(BodyFieldInfo { span }); 333 | Ok(()) 334 | } 335 | } 336 | 337 | #[derive(Debug, Clone)] 338 | pub(crate) struct QueryFieldInfo { 339 | pub(crate) name: Option, 340 | pub(crate) span: Span, 341 | } 342 | 343 | impl PartialEq for QueryFieldInfo { 344 | fn eq(&self, other: &Self) -> bool { 345 | self.name == other.name 346 | } 347 | } 348 | impl Eq for QueryFieldInfo {} 349 | 350 | #[derive(Debug, Clone)] 351 | pub(crate) struct PathFieldInfo { 352 | pub(crate) name: Option, 353 | pub(crate) span: Span, 354 | } 355 | 356 | impl PartialEq for PathFieldInfo { 357 | fn eq(&self, other: &Self) -> bool { 358 | self.name == other.name 359 | } 360 | } 361 | impl Eq for PathFieldInfo {} 362 | 363 | #[derive(Debug, Clone)] 364 | pub(crate) struct BodyFieldInfo { 365 | pub(crate) span: Span, 366 | } 367 | 368 | impl PartialEq for BodyFieldInfo { 369 | fn eq(&self, _other: &Self) -> bool { 370 | true 371 | } 372 | } 373 | impl Eq for BodyFieldInfo {} 374 | 375 | fn path_to_string(path: &syn::Path) -> String { 376 | use std::fmt::Write; 377 | 378 | let mut s = String::new(); 379 | if path.leading_colon.is_some() { 380 | s.push_str("::"); 381 | } 382 | for pair in path.segments.pairs() { 383 | match pair { 384 | syn::punctuated::Pair::Punctuated(seg, _) => { 385 | write!(s, "{}::", seg.ident).ok(); 386 | } 387 | syn::punctuated::Pair::End(seg) => { 388 | write!(s, "{}", seg.ident).ok(); 389 | } 390 | } 391 | } 392 | s 393 | } 394 | -------------------------------------------------------------------------------- /lib/nails_derive/src/path.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::fmt; 3 | use std::str::FromStr; 4 | 5 | use proc_macro2::{Span, TokenStream}; 6 | use quote::quote; 7 | 8 | // TODO: support recursive glob like `/admin/sidekiq/{path*}` 9 | #[derive(Debug, Clone, PartialEq, Eq)] 10 | pub(crate) struct PathPattern { 11 | components: Vec, 12 | bindings: HashSet, 13 | } 14 | 15 | impl PathPattern { 16 | pub(crate) fn path_prefix(&self) -> String { 17 | let mut prefix = String::from(""); 18 | for comp in &self.components { 19 | match comp { 20 | ComponentMatcher::String(s) => { 21 | prefix.push_str("/"); 22 | prefix.push_str(s); 23 | } 24 | ComponentMatcher::Var(_) => { 25 | prefix.push_str("/"); 26 | return prefix; 27 | } 28 | } 29 | } 30 | prefix 31 | } 32 | 33 | pub(crate) fn gen_path_condition( 34 | &self, 35 | path: TokenStream, 36 | fields: &HashMap, 37 | ) -> TokenStream { 38 | let conditions = self 39 | .components 40 | .iter() 41 | .map(|comp| match comp { 42 | ComponentMatcher::String(s) => { 43 | quote! { 44 | path_iter.next().map(|comp| comp == #s).unwrap_or(false) && 45 | } 46 | } 47 | ComponentMatcher::Var(var) => { 48 | let field_ty = &fields[var].ty; 49 | quote! { 50 | path_iter.next().map(|comp| { 51 | <#field_ty as nails::__rt::FromPath>::matches(comp) 52 | }).unwrap_or(false) && 53 | } 54 | } 55 | }) 56 | .collect::(); 57 | quote! {( 58 | #path.starts_with("/") && { 59 | let mut path_iter = #path[1..].split("/"); 60 | #conditions 61 | path_iter.next().is_none() 62 | } 63 | )} 64 | } 65 | 66 | pub(crate) fn gen_path_extractor( 67 | &self, 68 | path: TokenStream, 69 | fields: &HashMap, 70 | ) -> (TokenStream, HashMap) { 71 | let mut vars = HashMap::new(); 72 | let extractors = self 73 | .components 74 | .iter() 75 | .map(|comp| match comp { 76 | ComponentMatcher::String(_) => { 77 | quote! { 78 | path_iter.next(); 79 | } 80 | } 81 | ComponentMatcher::Var(var) => { 82 | let var_ident = format!("pathcomp_{}", var); 83 | let var_ident = syn::Ident::new(&var_ident, Span::call_site()); 84 | vars.insert(var.clone(), var_ident.clone()); 85 | let field_ty = &fields[var].ty; 86 | quote! { 87 | let #var_ident = <#field_ty as nails::__rt::FromPath>::from_path( 88 | path_iter.next().expect("internal error: invalid path given") 89 | ).expect("internal error: invalid path given"); 90 | } 91 | } 92 | }) 93 | .collect::(); 94 | let extractor = quote! { 95 | let mut path_iter = #path[1..].split("/"); 96 | #extractors 97 | }; 98 | (extractor, vars) 99 | } 100 | 101 | pub(crate) fn bindings(&self) -> &HashSet { 102 | &self.bindings 103 | } 104 | } 105 | 106 | impl FromStr for PathPattern { 107 | type Err = ParseError; 108 | 109 | fn from_str(path: &str) -> Result { 110 | if !path.starts_with("/") { 111 | return Err(ParseError::new(path, "must start with slash")); 112 | } 113 | let components = path[1..] 114 | .split("/") 115 | .map(|c| -> Result<_, Self::Err> { 116 | // TODO: support `{{`, `}}` 117 | if c.contains("{") || c.contains("}") { 118 | if !c.starts_with("{") || !c.ends_with("}") { 119 | return Err(ParseError::new( 120 | path, 121 | "variable must span the whole path component", 122 | )); 123 | } 124 | let c = &c[1..c.len() - 1]; 125 | if c.contains("{") || c.contains("}") { 126 | return Err(ParseError::new( 127 | path, 128 | "variable must span the whole path component", 129 | )); 130 | } 131 | if c == "" { 132 | return Err(ParseError::new(path, "variable must contain variable name")); 133 | } 134 | if !is_ident(c) { 135 | return Err(ParseError::new( 136 | path, 137 | "variable must be /[a-zA-Z_][a-zA-Z0-9_]*/", 138 | )); 139 | } 140 | Ok(ComponentMatcher::Var(c.to_owned())) 141 | } else { 142 | // TODO: more sanity check 143 | Ok(ComponentMatcher::String(c.to_owned())) 144 | } 145 | }) 146 | .collect::, _>>()?; 147 | 148 | let mut bindings = HashSet::new(); 149 | for comp in &components { 150 | match comp { 151 | ComponentMatcher::String(_) => {} 152 | ComponentMatcher::Var(name) => { 153 | let success = bindings.insert(name.clone()); 154 | if !success { 155 | return Err(ParseError::new( 156 | path, 157 | &format!("duplicate name: `{}`", name), 158 | )); 159 | } 160 | } 161 | } 162 | } 163 | 164 | Ok(Self { 165 | components, 166 | bindings, 167 | }) 168 | } 169 | } 170 | 171 | #[derive(Debug, Clone, PartialEq, Eq)] 172 | enum ComponentMatcher { 173 | String(String), 174 | Var(String), 175 | } 176 | 177 | #[derive(Debug, Clone)] 178 | pub(crate) struct ParseError { 179 | path: String, 180 | message: String, 181 | } 182 | 183 | impl ParseError { 184 | fn new(path: &str, message: &str) -> Self { 185 | Self { 186 | path: path.to_owned(), 187 | message: message.to_owned(), 188 | } 189 | } 190 | } 191 | 192 | impl fmt::Display for ParseError { 193 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 194 | write!( 195 | f, 196 | "error while parsing path matcher `{}`: {}", 197 | self.path, self.message 198 | ) 199 | } 200 | } 201 | 202 | impl std::error::Error for ParseError { 203 | fn description(&self) -> &str { 204 | "error while parsing path matcher" 205 | } 206 | } 207 | 208 | fn is_ident(s: &str) -> bool { 209 | let s = s.as_bytes(); 210 | s.len() > 0 211 | && s != b"_" 212 | && (s[0].is_ascii_alphabetic() || s[0] == b'_') 213 | && s.iter().all(|&c| c.is_ascii_alphanumeric() || c == b'_') 214 | } 215 | 216 | #[cfg(test)] 217 | #[cfg_attr(tarpaulin, skip)] 218 | mod tests { 219 | use super::*; 220 | 221 | use crate::assert_ts_eq; 222 | use syn::parse::Parser; 223 | 224 | macro_rules! hash { 225 | [$($e:expr),*] => { 226 | vec![$($e,)*].into_iter().collect::>() 227 | }; 228 | [$($e:expr)*,] => { 229 | vec![$($e,)*].into_iter().collect::>() 230 | }; 231 | } 232 | macro_rules! hash_set { 233 | [$($e:expr),*] => { 234 | vec![$($e,)*].into_iter().collect::>() 235 | }; 236 | [$($e:expr)*,] => { 237 | vec![$($e,)*].into_iter().collect::>() 238 | }; 239 | } 240 | 241 | #[test] 242 | fn test_is_ident() { 243 | assert!(is_ident("foo_bar2")); 244 | assert!(is_ident("_foo_bar2")); 245 | assert!(!is_ident("_")); 246 | assert!(!is_ident("1st")); 247 | assert!(!is_ident("foo-bar")); 248 | } 249 | 250 | #[test] 251 | fn test_path_prefix() { 252 | let parse = ::from_str; 253 | let parse = |s| parse(s).unwrap(); 254 | assert_eq!(parse("/").path_prefix(), "/"); 255 | assert_eq!(parse("/api/posts/{id}").path_prefix(), "/api/posts/"); 256 | assert_eq!( 257 | parse("/api/posts/{post_id}/comments/{id}").path_prefix(), 258 | "/api/posts/", 259 | ); 260 | } 261 | 262 | #[test] 263 | fn test_gen_path_condition() { 264 | let parse = ::from_str; 265 | let parse = |s| parse(s).unwrap(); 266 | assert_ts_eq!( 267 | parse("/").gen_path_condition(quote! { path }, &hash![]), 268 | quote! { 269 | (path.starts_with("/") && { 270 | let mut path_iter = path[1..].split("/"); 271 | path_iter.next().map(|comp| comp == "").unwrap_or(false) 272 | && path_iter.next().is_none() 273 | }) 274 | }, 275 | ); 276 | 277 | let field = syn::Field::parse_named 278 | .parse2(quote! { id: String }) 279 | .unwrap(); 280 | assert_ts_eq!( 281 | parse("/api/posts/{id}") 282 | .gen_path_condition(quote! { path }, &hash![(S("id"), &field),]), 283 | quote! { 284 | (path.starts_with("/") && { 285 | let mut path_iter = path[1..].split("/"); 286 | path_iter.next().map(|comp| comp == "api").unwrap_or(false) 287 | && path_iter.next().map(|comp| comp == "posts").unwrap_or(false) 288 | && path_iter.next().map(|comp| { 289 | ::matches(comp) 290 | }).unwrap_or(false) 291 | && path_iter.next().is_none() 292 | }) 293 | }, 294 | ); 295 | } 296 | 297 | #[test] 298 | fn test_gen_path_extractor() { 299 | let parse = ::from_str; 300 | let parse = |s| parse(s).unwrap(); 301 | 302 | let (extractor, vars) = parse("/").gen_path_extractor(quote! { path }, &hash![]); 303 | assert_ts_eq!( 304 | extractor, 305 | quote! { 306 | let mut path_iter = path[1..].split("/"); 307 | path_iter.next(); 308 | }, 309 | ); 310 | assert_eq!(vars.len(), 0); 311 | 312 | let field = syn::Field::parse_named 313 | .parse2(quote! { id: String }) 314 | .unwrap(); 315 | let (extractor, vars) = parse("/api/posts/{id}") 316 | .gen_path_extractor(quote! { path }, &hash![(S("id"), &field),]); 317 | assert_ts_eq!( 318 | extractor, 319 | quote! { 320 | let mut path_iter = path[1..].split("/"); 321 | path_iter.next(); 322 | path_iter.next(); 323 | let pathcomp_id = ::from_path( 324 | path_iter.next().expect("internal error: invalid path given") 325 | ).expect("internal error: invalid path given"); 326 | }, 327 | ); 328 | assert_eq!(vars.len(), 1); 329 | assert_eq!(vars["id"], "pathcomp_id"); 330 | } 331 | 332 | #[test] 333 | fn test_parse() { 334 | let parse = ::from_str; 335 | let parse = |s| parse(s).unwrap(); 336 | assert_eq!( 337 | parse("/"), 338 | PathPattern { 339 | components: vec![ComponentMatcher::String(S("")),], 340 | bindings: hash_set![], 341 | }, 342 | ); 343 | assert_eq!( 344 | parse("/ping"), 345 | PathPattern { 346 | components: vec![ComponentMatcher::String(S("ping")),], 347 | bindings: hash_set![], 348 | }, 349 | ); 350 | assert_eq!( 351 | parse("/api/"), 352 | PathPattern { 353 | components: vec![ 354 | ComponentMatcher::String(S("api")), 355 | ComponentMatcher::String(S("")), 356 | ], 357 | bindings: hash_set![], 358 | }, 359 | ); 360 | assert_eq!( 361 | parse("/api/posts/{id}"), 362 | PathPattern { 363 | components: vec![ 364 | ComponentMatcher::String(S("api")), 365 | ComponentMatcher::String(S("posts")), 366 | ComponentMatcher::Var(S("id")), 367 | ], 368 | bindings: hash_set![S("id")], 369 | }, 370 | ); 371 | assert_eq!( 372 | parse("/api/posts/{post_id}/comments/{id}"), 373 | PathPattern { 374 | components: vec![ 375 | ComponentMatcher::String(S("api")), 376 | ComponentMatcher::String(S("posts")), 377 | ComponentMatcher::Var(S("post_id")), 378 | ComponentMatcher::String(S("comments")), 379 | ComponentMatcher::Var(S("id")), 380 | ], 381 | bindings: hash_set![S("post_id"), S("id")], 382 | }, 383 | ); 384 | } 385 | 386 | #[test] 387 | fn test_parse_error() { 388 | let parse = ::from_str; 389 | let parse_err = |s| parse(s).unwrap_err().message; 390 | assert_eq!(parse_err(""), "must start with slash"); 391 | assert_eq!(parse_err("api/posts/{id}"), "must start with slash",); 392 | assert_eq!( 393 | parse_err("/api/posts/post_{id}"), 394 | "variable must span the whole path component", 395 | ); 396 | assert_eq!( 397 | parse_err("/api/posts/{foo}_{bar}"), 398 | "variable must span the whole path component", 399 | ); 400 | assert_eq!( 401 | parse_err("/api/posts/}/"), 402 | "variable must span the whole path component", 403 | ); 404 | assert_eq!( 405 | parse_err("/api/posts/{barrr/"), 406 | "variable must span the whole path component", 407 | ); 408 | assert_eq!( 409 | parse_err("/api/posts/}foo{/"), 410 | "variable must span the whole path component", 411 | ); 412 | assert_eq!( 413 | parse_err("/api/posts/{}/"), 414 | "variable must contain variable name", 415 | ); 416 | assert_eq!( 417 | parse_err("/api/posts/{1}/"), 418 | "variable must be /[a-zA-Z_][a-zA-Z0-9_]*/", 419 | ); 420 | assert_eq!( 421 | parse_err("/api/posts/{id}/comments/{id}"), 422 | "duplicate name: `id`", 423 | ); 424 | } 425 | 426 | #[test] 427 | fn test_parse_error_message() { 428 | let parse = ::from_str; 429 | let parse_err = |s| parse(s).unwrap_err().to_string(); 430 | assert_eq!( 431 | parse_err(""), 432 | "error while parsing path matcher ``: must start with slash", 433 | ); 434 | assert_eq!( 435 | parse_err("api/posts/{id}"), 436 | "error while parsing path matcher `api/posts/{id}`: must start with slash", 437 | ); 438 | 439 | { 440 | use std::error::Error as _; 441 | assert_eq!( 442 | parse("").unwrap_err().description(), 443 | "error while parsing path matcher", 444 | ); 445 | } 446 | } 447 | 448 | #[allow(non_snake_case)] 449 | fn S(s: &'static str) -> String { 450 | s.to_owned() 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /lib/nails_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "proc_macro_diagnostics", feature(proc_macro_diagnostics))] 2 | 3 | extern crate proc_macro; 4 | 5 | use std::collections::{HashMap, HashSet}; 6 | 7 | use proc_macro2::{Span, TokenStream}; 8 | use quote::quote; 9 | use syn::spanned::Spanned; 10 | use syn::DeriveInput; 11 | 12 | use crate::attrs::{FieldAttrs, StructAttrs}; 13 | use crate::path::PathPattern; 14 | use crate::utils::FieldsExt; 15 | 16 | mod attrs; 17 | mod path; 18 | mod utils; 19 | 20 | #[cfg(test)] 21 | #[macro_use] 22 | mod test_utils; 23 | 24 | #[proc_macro_derive(Preroute, attributes(nails))] 25 | pub fn derive_preroute(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 26 | derive_preroute2(input.into()) 27 | .unwrap_or_else(|e| e.to_compile_error()) 28 | .into() 29 | } 30 | 31 | fn derive_preroute2(input: TokenStream) -> syn::Result { 32 | let input = syn::parse2::(input)?; 33 | 34 | let data = if let syn::Data::Struct(data) = &input.data { 35 | data 36 | } else { 37 | return Err(syn::Error::new( 38 | input.span(), 39 | "Preroute cannot be derived for enums or unions", 40 | )); 41 | }; 42 | let attrs = StructAttrs::parse(&input.attrs)?; 43 | let field_attrs = data 44 | .fields 45 | .iter() 46 | .map(|field| FieldAttrs::parse(&field.attrs)) 47 | .collect::, _>>()?; 48 | 49 | let path = attrs 50 | .path 51 | .clone() 52 | .ok_or_else(|| syn::Error::new(input.span(), "#[nails(path)] is needed"))?; 53 | let path_span = path.path.span(); 54 | let path = path 55 | .path 56 | .value() 57 | .parse::() 58 | .map_err(|e| syn::Error::new(path_span, e))?; 59 | 60 | let field_len = data.fields.iter().len(); 61 | let field_kinds = data 62 | .fields 63 | .iter() 64 | .zip(&field_attrs) 65 | .enumerate() 66 | .map(|(i, (field, attrs))| { 67 | FieldKind::parse_from(field, i + 1 == field_len, attrs, path.bindings()) 68 | }) 69 | .collect::, _>>()?; 70 | 71 | let path_fields = { 72 | let mut path_fields = HashMap::new(); 73 | for (idx, field) in data.fields.iter().enumerate() { 74 | if let FieldKind::Path { var } = &field_kinds[idx] { 75 | if let Some(_dup_field) = path_fields.get(var) { 76 | let span = if let Some(path) = &field_attrs[idx].path { 77 | path.span 78 | } else { 79 | field.span() 80 | }; 81 | return Err(syn::Error::new(span, "Duplicate path names")); 82 | } 83 | path_fields.insert(var.clone(), field); 84 | } 85 | } 86 | path_fields 87 | }; 88 | for binding in path.bindings() { 89 | if !path_fields.contains_key(binding) { 90 | return Err(syn::Error::new( 91 | path_span, 92 | format_args!("Missing field for binding name from {{{}}}", binding), 93 | )); 94 | } 95 | } 96 | 97 | let path_prefix = path.path_prefix(); 98 | let path_condition = path.gen_path_condition(quote! { path }, &path_fields); 99 | let (path_extractor, path_vars) = path.gen_path_extractor(quote! { path }, &path_fields); 100 | 101 | let construct = data.fields.try_construct(&input.ident, |field, idx| { 102 | field_kinds[idx].gen_parser(field, &path_vars) 103 | })?; 104 | 105 | let method_cond = if let Some(method) = attrs.method { 106 | method.kind 107 | } else { 108 | attrs::MethodKind::Get 109 | } 110 | .gen_condition(quote! { method }); 111 | 112 | let name = &input.ident; 113 | let (impl_generics, ty_generics, where_clause) = &input.generics.split_for_impl(); 114 | Ok(quote! { 115 | impl #impl_generics nails::__rt::Preroute for #name #ty_generics #where_clause { 116 | fn path_prefix_hint() -> &'static str { 117 | #path_prefix 118 | } 119 | fn match_path(method: &nails::__rt::Method, path: &str) -> bool { 120 | #method_cond && #path_condition 121 | } 122 | 123 | fn from_request<'a>( 124 | req: nails::__rt::Request 125 | ) -> nails::__rt::BoxFuture<'a, Result> { 126 | nails::__rt::box_future(async move { 127 | let query_hash = nails::__rt::parse_query(req.uri().query().unwrap_or("")); 128 | let path = req.uri().path(); 129 | #path_extractor 130 | Ok(#construct) 131 | }) 132 | } 133 | } 134 | }) 135 | } 136 | 137 | impl attrs::MethodKind { 138 | fn gen_condition(&self, method_var: TokenStream) -> TokenStream { 139 | use attrs::MethodKind::*; 140 | 141 | let method_const = match *self { 142 | Get => { 143 | return quote! { 144 | (*#method_var == nails::__rt::Method::GET || *#method_var == nails::__rt::Method::HEAD) 145 | } 146 | } 147 | GetOnly => "GET", 148 | Head => "HEAD", 149 | Post => "POST", 150 | Put => "PUT", 151 | Delete => "DELETE", 152 | Options => "OPTIONS", 153 | Patch => "PATCH", 154 | }; 155 | let method_const = syn::Ident::new(method_const, Span::call_site()); 156 | quote! { 157 | (*#method_var == nails::__rt::Method::#method_const) 158 | } 159 | } 160 | } 161 | 162 | #[derive(Debug)] 163 | enum FieldKind { 164 | Path { var: String }, 165 | Query { name: String }, 166 | Body, 167 | } 168 | 169 | impl FieldKind { 170 | fn parse_from( 171 | field: &syn::Field, 172 | is_last: bool, 173 | attrs: &FieldAttrs, 174 | path_bindings: &HashSet, 175 | ) -> syn::Result { 176 | let mut specs = Vec::new(); 177 | if let Some(query) = &attrs.query { 178 | specs.push(("query", query.span)); 179 | } 180 | if let Some(path) = &attrs.path { 181 | specs.push(("path", path.span)); 182 | } 183 | if let Some(body) = &attrs.body { 184 | specs.push(("body", body.span)); 185 | } 186 | if specs.len() > 1 { 187 | return Err(syn::Error::new( 188 | specs[1].1, 189 | format_args!( 190 | "Cannot have both #[nails({})] and #[nails({})]", 191 | specs[0].0, specs[1].0, 192 | ), 193 | )); 194 | } 195 | if let Some(query) = &attrs.query { 196 | let query_name = if let Some(query_name) = &query.name { 197 | query_name.value() 198 | } else if let Some(ident) = &field.ident { 199 | ident.to_string() 200 | } else { 201 | return Err(syn::Error::new( 202 | query.span, 203 | "Specify name with #[nails(query = \"\")]", 204 | )); 205 | }; 206 | return Ok(FieldKind::Query { name: query_name }); 207 | } 208 | 209 | if let Some(path) = &attrs.path { 210 | let path_name = if let Some(path_name) = &path.name { 211 | path_name.value() 212 | } else if let Some(ident) = &field.ident { 213 | ident.to_string() 214 | } else { 215 | return Err(syn::Error::new( 216 | path.span, 217 | "Specify name with #[nails(path = \"\")]", 218 | )); 219 | }; 220 | if !path_bindings.contains(&path_name) { 221 | return Err(syn::Error::new( 222 | path.span, 223 | "This name doesn't exist in the endpoint path", 224 | )); 225 | } 226 | return Ok(FieldKind::Path { var: path_name }); 227 | } 228 | 229 | if let Some(body) = &attrs.body { 230 | if !is_last { 231 | return Err(syn::Error::new( 232 | body.span, 233 | "Only the last field can have #[nails(body)]", 234 | )); 235 | } 236 | return Ok(FieldKind::Body); 237 | } 238 | 239 | // ident-based fallback 240 | let ident = field.ident.as_ref().ok_or_else(|| { 241 | syn::Error::new( 242 | field.span(), 243 | "Specify name with #[nails(query = \"\")] or alike", 244 | ) 245 | })?; 246 | let ident_name = ident.to_string(); 247 | if path_bindings.contains(&ident_name) { 248 | // fallback to path 249 | Ok(FieldKind::Path { var: ident_name }) 250 | } else { 251 | // fallback to query 252 | Ok(FieldKind::Query { name: ident_name }) 253 | } 254 | } 255 | 256 | fn gen_parser( 257 | &self, 258 | _field: &syn::Field, 259 | path_vars: &HashMap, 260 | ) -> syn::Result { 261 | Ok(match self { 262 | FieldKind::Path { var } => { 263 | let path_var = &path_vars[var]; 264 | quote! { #path_var } 265 | } 266 | FieldKind::Query { name } => quote! { 267 | nails::__rt::FromQuery::from_query( 268 | if let Some(values) = query_hash.get(#name) { 269 | values.as_slice() 270 | } else { 271 | &[] 272 | } 273 | )? 274 | }, 275 | FieldKind::Body => quote! { 276 | nails::__rt::FromBody::from_body( 277 | req // TODO: abstract over ident name 278 | ).await? 279 | }, 280 | }) 281 | } 282 | } 283 | 284 | #[cfg(test)] 285 | #[cfg_attr(tarpaulin, skip)] 286 | mod tests { 287 | use super::*; 288 | 289 | #[test] 290 | fn test_derive1() { 291 | assert_ts_eq!( 292 | derive_preroute2(quote! { 293 | #[nails(path = "/api/posts/{id}")] 294 | struct GetPostRequest { 295 | id: String, 296 | #[nails(query)] 297 | param1: String, 298 | #[nails(query = "param2rename")] 299 | param2: String, 300 | param3: String, 301 | } 302 | }) 303 | .unwrap(), 304 | quote! { 305 | impl nails::__rt::Preroute for GetPostRequest { 306 | fn path_prefix_hint() -> &'static str { "/api/posts/" } 307 | fn match_path(method: &nails::__rt::Method, path: &str) -> bool { 308 | (*method == nails::__rt::Method::GET || *method == nails::__rt::Method::HEAD) && ( 309 | path.starts_with("/") && { 310 | let mut path_iter = path[1..].split("/"); 311 | path_iter.next().map(|comp| comp == "api").unwrap_or(false) 312 | && path_iter.next().map(|comp| comp == "posts").unwrap_or(false) 313 | && path_iter.next().map(|comp| { 314 | ::matches(comp) 315 | }).unwrap_or(false) 316 | && path_iter.next().is_none() 317 | } 318 | ) 319 | } 320 | fn from_request<'a>( 321 | req: nails::__rt::Request 322 | ) -> nails::__rt::BoxFuture<'a, Result> { 323 | nails::__rt::box_future(async move { 324 | let query_hash = nails::__rt::parse_query(req.uri().query().unwrap_or("")); 325 | let path = req.uri().path(); 326 | let mut path_iter = path[1..].split("/"); 327 | path_iter.next(); 328 | path_iter.next(); 329 | let pathcomp_id = ::from_path( 330 | path_iter.next().expect("internal error: invalid path given") 331 | ).expect("internal error: invalid path given"); 332 | Ok(GetPostRequest { 333 | id: pathcomp_id, 334 | param1: nails::__rt::FromQuery::from_query( 335 | if let Some(values) = query_hash.get("param1") { 336 | values.as_slice() 337 | } else { 338 | &[] 339 | } 340 | )?, 341 | param2: nails::__rt::FromQuery::from_query( 342 | if let Some(values) = query_hash.get("param2rename") { 343 | values.as_slice() 344 | } else { 345 | &[] 346 | } 347 | )?, 348 | param3: nails::__rt::FromQuery::from_query( 349 | if let Some(values) = query_hash.get("param3") { 350 | values.as_slice() 351 | } else { 352 | &[] 353 | } 354 | )?, 355 | }) 356 | }) 357 | } 358 | } 359 | }, 360 | ); 361 | } 362 | 363 | #[test] 364 | fn test_derive_post() { 365 | assert_ts_eq!( 366 | derive_preroute2(quote! { 367 | #[nails(path = "/api/posts", method = "POST")] 368 | struct CreatePostRequest; 369 | }) 370 | .unwrap(), 371 | quote! { 372 | impl nails::__rt::Preroute for CreatePostRequest { 373 | fn path_prefix_hint() -> &'static str { "/api/posts" } 374 | fn match_path(method: &nails::__rt::Method, path: &str) -> bool { 375 | (*method == nails::__rt::Method::POST) && ( 376 | path.starts_with("/") && { 377 | let mut path_iter = path[1..].split("/"); 378 | path_iter.next().map(|comp| comp == "api").unwrap_or(false) 379 | && path_iter.next().map(|comp| comp == "posts").unwrap_or(false) 380 | && path_iter.next().is_none() 381 | } 382 | ) 383 | } 384 | fn from_request<'a>( 385 | req: nails::__rt::Request 386 | ) -> nails::__rt::BoxFuture<'a, Result> { 387 | nails::__rt::box_future(async move { 388 | let query_hash = nails::__rt::parse_query(req.uri().query().unwrap_or("")); 389 | let path = req.uri().path(); 390 | let mut path_iter = path[1..].split("/"); 391 | path_iter.next(); 392 | path_iter.next(); 393 | Ok(CreatePostRequest) 394 | }) 395 | } 396 | } 397 | }, 398 | ); 399 | } 400 | 401 | #[test] 402 | #[should_panic(expected = "Preroute cannot be derived for enums or unions")] 403 | fn test_derive_enum() { 404 | derive_preroute2(quote! { 405 | #[nails(path = "/api/posts/{id}")] 406 | enum GetPostRequest { 407 | Foo {} 408 | } 409 | }) 410 | .unwrap(); 411 | } 412 | 413 | #[test] 414 | fn test_derive_tuple() { 415 | assert_ts_eq!( 416 | derive_preroute2(quote! { 417 | #[nails(path = "/api/posts/{id}")] 418 | struct GetPostRequest( 419 | #[nails(path = "id")] 420 | String, 421 | #[nails(query = "param1")] 422 | String, 423 | ); 424 | }) 425 | .unwrap(), 426 | quote! { 427 | impl nails::__rt::Preroute for GetPostRequest { 428 | fn path_prefix_hint() -> &'static str { "/api/posts/" } 429 | fn match_path(method: &nails::__rt::Method, path: &str) -> bool { 430 | (*method == nails::__rt::Method::GET || *method == nails::__rt::Method::HEAD) && ( 431 | path.starts_with("/") && { 432 | let mut path_iter = path[1..].split("/"); 433 | path_iter.next().map(|comp| comp == "api").unwrap_or(false) 434 | && path_iter.next().map(|comp| comp == "posts").unwrap_or(false) 435 | && path_iter.next().map(|comp| { 436 | ::matches(comp) 437 | }).unwrap_or(false) 438 | && path_iter.next().is_none() 439 | } 440 | ) 441 | } 442 | fn from_request<'a>( 443 | req: nails::__rt::Request 444 | ) -> nails::__rt::BoxFuture<'a, Result> { 445 | nails::__rt::box_future(async move { 446 | let query_hash = nails::__rt::parse_query(req.uri().query().unwrap_or("")); 447 | let path = req.uri().path(); 448 | let mut path_iter = path[1..].split("/"); 449 | path_iter.next(); 450 | path_iter.next(); 451 | let pathcomp_id = ::from_path( 452 | path_iter.next().expect("internal error: invalid path given") 453 | ).expect("internal error: invalid path given"); 454 | Ok(GetPostRequest( 455 | pathcomp_id, 456 | nails::__rt::FromQuery::from_query( 457 | if let Some(values) = query_hash.get("param1") { 458 | values.as_slice() 459 | } else { 460 | &[] 461 | } 462 | )?, 463 | )) 464 | }) 465 | } 466 | } 467 | }, 468 | ); 469 | } 470 | 471 | #[test] 472 | fn test_derive_unit() { 473 | assert_ts_eq!( 474 | derive_preroute2(quote! { 475 | #[nails(path = "/ping")] 476 | struct PingRequest; 477 | }) 478 | .unwrap(), 479 | quote! { 480 | impl nails::__rt::Preroute for PingRequest { 481 | fn path_prefix_hint() -> &'static str { "/ping" } 482 | fn match_path(method: &nails::__rt::Method, path: &str) -> bool { 483 | (*method == nails::__rt::Method::GET || *method == nails::__rt::Method::HEAD) && ( 484 | path.starts_with("/") && { 485 | let mut path_iter = path[1..].split("/"); 486 | path_iter.next().map(|comp| comp == "ping").unwrap_or(false) 487 | && path_iter.next().is_none() 488 | } 489 | ) 490 | } 491 | fn from_request<'a>( 492 | req: nails::__rt::Request 493 | ) -> nails::__rt::BoxFuture<'a, Result> { 494 | nails::__rt::box_future(async move { 495 | let query_hash = nails::__rt::parse_query(req.uri().query().unwrap_or("")); 496 | let path = req.uri().path(); 497 | let mut path_iter = path[1..].split("/"); 498 | path_iter.next(); 499 | Ok(PingRequest) 500 | }) 501 | } 502 | } 503 | }, 504 | ); 505 | } 506 | 507 | #[test] 508 | #[should_panic(expected = "multiple #[nails(path)] definitions")] 509 | fn test_derive_double_paths() { 510 | derive_preroute2(quote! { 511 | #[nails(path = "/api/posts/{id}")] 512 | #[nails(path = "/api/posts/{idd}")] 513 | struct GetPostRequest {} 514 | }) 515 | .unwrap(); 516 | } 517 | 518 | #[test] 519 | #[should_panic(expected = "multiple #[nails(path)] definitions")] 520 | fn test_derive_double_paths2() { 521 | derive_preroute2(quote! { 522 | #[nails(path = "/api/posts/{id}", path = "/api/posts/{idd}")] 523 | struct GetPostRequest {} 524 | }) 525 | .unwrap(); 526 | } 527 | 528 | #[test] 529 | #[should_panic(expected = "string value expected in #[nails(path)]")] 530 | fn test_derive_integer_path() { 531 | derive_preroute2(quote! { 532 | #[nails(path = 1)] 533 | struct GetPostRequest {} 534 | }) 535 | .unwrap(); 536 | } 537 | 538 | #[test] 539 | #[should_panic(expected = "#[nails(path)] is needed")] 540 | fn test_derive_missing_path() { 541 | derive_preroute2(quote! { 542 | struct GetPostRequest {} 543 | }) 544 | .unwrap(); 545 | } 546 | 547 | #[test] 548 | #[should_panic(expected = "unknown option: `foo`")] 549 | fn test_derive_unknown_struct_attr() { 550 | derive_preroute2(quote! { 551 | #[nails(path = "/api/posts/{id}", foo)] 552 | struct GetPostRequest {} 553 | }) 554 | .unwrap(); 555 | } 556 | 557 | #[test] 558 | #[should_panic(expected = "multiple #[nails(query)] definitions")] 559 | fn test_derive_double_queries() { 560 | derive_preroute2(quote! { 561 | #[nails(path = "/api/posts/{id}")] 562 | struct GetPostRequest { 563 | #[nails(query = "query1rename")] 564 | #[nails(query = "query1renamerename")] 565 | query1: String, 566 | } 567 | }) 568 | .unwrap(); 569 | } 570 | 571 | #[test] 572 | #[should_panic(expected = "multiple #[nails(query)] definitions")] 573 | fn test_derive_double_queries2() { 574 | derive_preroute2(quote! { 575 | #[nails(path = "/api/posts/{id}")] 576 | struct GetPostRequest { 577 | #[nails(query = "query1rename", query = "query1renamerename")] 578 | query1: String, 579 | } 580 | }) 581 | .unwrap(); 582 | } 583 | 584 | #[test] 585 | #[should_panic(expected = "multiple #[nails(path)] definitions")] 586 | fn test_derive_double_path_kinds() { 587 | derive_preroute2(quote! { 588 | #[nails(path = "/api/posts/{id1}/{id2}")] 589 | struct GetPostRequest { 590 | #[nails(path = "id1", path = "id2")] 591 | id: String, 592 | } 593 | }) 594 | .unwrap(); 595 | } 596 | 597 | #[test] 598 | #[should_panic(expected = "Duplicate path names")] 599 | fn test_derive_duplicate_path_names() { 600 | derive_preroute2(quote! { 601 | #[nails(path = "/api/posts/{id}")] 602 | struct GetPostRequest { 603 | #[nails(path)] 604 | id: String, 605 | #[nails(path = "id")] 606 | id2: String, 607 | } 608 | }) 609 | .unwrap(); 610 | } 611 | 612 | #[test] 613 | #[should_panic(expected = "Duplicate path names")] 614 | fn test_derive_duplicate_path_names2() { 615 | derive_preroute2(quote! { 616 | #[nails(path = "/api/posts/{id}")] 617 | struct GetPostRequest { 618 | #[nails(path = "id")] 619 | id2: String, 620 | id: String, 621 | } 622 | }) 623 | .unwrap(); 624 | } 625 | 626 | #[test] 627 | #[should_panic(expected = "string value or no value expected in #[nails(query)]")] 628 | fn test_derive_integer_query_name() { 629 | derive_preroute2(quote! { 630 | #[nails(path = "/api/posts/{id}")] 631 | struct GetPostRequest { 632 | #[nails(query = 1)] 633 | query1: String, 634 | } 635 | }) 636 | .unwrap(); 637 | } 638 | 639 | #[test] 640 | #[should_panic(expected = "string value or no value expected in #[nails(path)]")] 641 | fn test_derive_integer_path_name() { 642 | derive_preroute2(quote! { 643 | #[nails(path = "/api/posts/{id}")] 644 | struct GetPostRequest { 645 | #[nails(path = 1)] 646 | id: String, 647 | } 648 | }) 649 | .unwrap(); 650 | } 651 | 652 | #[test] 653 | #[should_panic(expected = "This name doesn\\'t exist in the endpoint path")] 654 | fn test_derive_non_captured_path_name1() { 655 | derive_preroute2(quote! { 656 | #[nails(path = "/api/posts/{id}")] 657 | struct GetPostRequest { 658 | #[nails(path = "idd")] 659 | id: String, 660 | } 661 | }) 662 | .unwrap(); 663 | } 664 | 665 | #[test] 666 | #[should_panic(expected = "This name doesn\\'t exist in the endpoint path")] 667 | fn test_derive_non_captured_path_name2() { 668 | derive_preroute2(quote! { 669 | #[nails(path = "/api/posts/{id}")] 670 | struct GetPostRequest { 671 | #[nails(path)] 672 | idd: String, 673 | } 674 | }) 675 | .unwrap(); 676 | } 677 | 678 | #[test] 679 | #[should_panic(expected = "Missing field for binding name from {id}")] 680 | fn test_derive_missing_path_names() { 681 | derive_preroute2(quote! { 682 | #[nails(path = "/api/posts/{id}")] 683 | struct GetPostRequest; 684 | }) 685 | .unwrap(); 686 | } 687 | 688 | #[test] 689 | #[should_panic(expected = "Specify name with #[nails(query = \\\"\\\")] or alike")] 690 | fn test_derive_missing_field_kind_for_position_field() { 691 | derive_preroute2(quote! { 692 | #[nails(path = "/api/posts/{id}")] 693 | struct GetPostRequest( 694 | String, 695 | ); 696 | }) 697 | .unwrap(); 698 | } 699 | 700 | #[test] 701 | #[should_panic(expected = "Specify name with #[nails(query = \\\"\\\")]")] 702 | fn test_derive_missing_query_name_for_position_field() { 703 | derive_preroute2(quote! { 704 | #[nails(path = "/api/posts/{id}")] 705 | struct GetPostRequest( 706 | #[nails(query)] 707 | String, 708 | ); 709 | }) 710 | .unwrap(); 711 | } 712 | 713 | #[test] 714 | #[should_panic(expected = "Specify name with #[nails(path = \\\"\\\")]")] 715 | fn test_derive_missing_path_name_for_position_field() { 716 | derive_preroute2(quote! { 717 | #[nails(path = "/api/posts/{id}")] 718 | struct GetPostRequest( 719 | #[nails(path)] 720 | String, 721 | ); 722 | }) 723 | .unwrap(); 724 | } 725 | 726 | #[test] 727 | #[should_panic(expected = "unknown option: `foo`")] 728 | fn test_derive_unknown_field_attr() { 729 | derive_preroute2(quote! { 730 | #[nails(path = "/api/posts/{id}")] 731 | struct GetPostRequest { 732 | #[nails(query, foo)] 733 | query1: String, 734 | } 735 | }) 736 | .unwrap(); 737 | } 738 | } 739 | --------------------------------------------------------------------------------