diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..4b55557f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: "rust" +notifications: + email: false + irc: + channels: + - secure: "FiHwNDkLqlzn+fZnn42uZ+GWm59S9OJreUIz9r7+nXrxUBeBcthQlqamJUiuYryVohzqLydBVv6xmT5wgS/LxRnj4f363eBpKejuSktglnf2rl8JjuSXZVgrPMDmrfgkBdC+aMCPzdw2fIHSWmvQMr/t9kGW9cHl0VlLxPAhnAsry+E1Kxrrz4IuOJmyb43VqPf/GO6VCDzTpHiKHKe5Rp7i2IkbGus2GiSD/UMrgUTWmMOFoejl7fWX7SH9kvSrN/SCYldVOYA4nazeZfaHv7mCX6G8U3GGXTHwjAVAluXyYgUCYpsYKC5KGkUJFcLhjaBu5qpmlI0EZd/rsgscOBzqfJ0D/WkahWiKtlQEKZ7UEAhA3SaAhcrSh2kSQFf2GW1T8kfzqlnBtjpqSvCFuOpY5XQcSYEEX7qxT1aiK2UBi9iAKgMnG1SDEfeFERekw0KJPKbwJDMV7NhCg9kYVBHG1hxvFeYqMmnFrjLlRDQQrbDHrP9Avdtg0FScolsFVmT+uatBuRXDcqunssolfnWguyrQ0Z9KGauv0iqkwFwO7jQSA9f87wgsuzqlzstHRxoGGlPtGt4J/+MhyA3lOEXwBa5eotjILI7iykK+ykJ33cOTGcqyXbkWoYRZ6+fS2guI+f2CxxsYWUOK2UgMyYKEwtraC3duVIGtQR+zuvc=" + use_notice: true +rust: + - "nightly" diff --git a/Cargo.toml b/Cargo.toml index 38874d7a..6fe48d5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,5 +25,4 @@ serde_urlencoded = "0.5.1" url = "1.5.1" [lib] -doctest = false proc-macro = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..8a75e6ee --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017 Jimmy Cuadra + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..5b16942d --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# ruma-api-macros + +[![Build Status](https://travis-ci.org/ruma/ruma-api-macros.svg?branch=master)](https://travis-ci.org/ruma/ruma-api-macros) + +**ruma-api-macros** provides a procedural macro for easily generating [ruma-api](https://github.com/ruma/ruma-api)-compatible API endpoints. +You define the endpoint's metadata, request fields, and response fields, and the macro generates all the necessary types and implements all the necessary traits. + +## Usage + +Here is an example that shows most of the macro's functionality. + +``` rust +#![feature(associated_consts, proc_macro, try_from)] + +extern crate futures; +extern crate hyper; +extern crate ruma_api; +extern crate ruma_api_macros; +extern crate serde; +#[macro_use] extern crate serde_derive; +extern crate serde_json; +extern crate serde_urlencoded; +extern crate url; + +pub mod some_endpoint { + use ruma_api_macros::ruma_api; + + ruma_api! { + metadata { + description: "Does something.", + method: Method::Get, // A `hyper::Method` value. No need to import the name. + name: "some_endpoint", + path: "/_matrix/some/endpoint/:baz", + rate_limited: false, + requires_authentication: false, + } + + request { + // With no attribute on the field, it will be put into the body of the request. + pub foo: String, + + // This value will be put into the "Content-Type" HTTP header. + #[ruma_api(header)] + pub content_type: ContentType, + + // This value will be put into the query string of the request's URL. + #[ruma_api(query)] + pub bar: String, + + // This value will be inserted into the request's URL in place of the + // ":baz" path component. + #[ruma_api(path)] + pub baz: String, + } + + response { + // This value will be extracted from the "Content-Type" HTTP header. + #[ruma_api(header)] + pub content_type: ContentType, + + // With no attribute on the field, it will be extracted from the body of the response. + pub value: String, + } + } +} +``` + +## Documentation + +ruma-api-macros has [comprehensive documentation](https://docs.rs/ruma-api-macros) available on docs.rs. + +## License + +[MIT](http://opensource.org/licenses/MIT) diff --git a/src/lib.rs b/src/lib.rs index 9ce90231..539d8258 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ //! Crate `ruma-api-macros` provides a procedural macro for easily generating //! [ruma-api](https://github.com/ruma/ruma-api)-compatible endpoints. +//! +//! See the documentation for the `ruma_api!` macro for usage details. #![deny(missing_debug_implementations)] #![feature(proc_macro)] @@ -21,7 +23,177 @@ use parse::parse_entries; mod api; mod parse; -/// Generates a `ruma-api` endpoint. +/// Generates a `ruma_api::Endpoint` from a concise definition. +/// +/// The macro expects the following structure as input: +/// +/// ```text +/// ruma_api! { +/// metadata { +/// description: &'static str +/// method: hyper::Method, +/// name: &'static str, +/// path: &'static str, +/// rate_limited: bool, +/// requires_authentication: bool, +/// } +/// +/// request { +/// // Struct fields for each piece of data required +/// // to make a request to this API endpoint. +/// } +/// +/// response { +/// // Struct fields for each piece of data expected +/// // in the response from this API endpoint. +/// } +/// } +/// # } +/// ``` +/// +/// This will generate a `ruma_api::Metadata` value to be used for the `ruma_api::Endpoint`'s +/// associated constant, single `Request` and `Response` structs, and the necessary trait +/// implementations to convert the request into a `hyper::Request` and to create a response from a +/// `hyper::response`. +/// +/// The details of each of the three sections of the macros are documented below. +/// +/// ## Metadata +/// +/// * `description`: A short description of what the endpoint does. +/// * `method`: The HTTP method used for requests to the endpoint. +/// It's not necessary to import `hyper::Method`, you just write the value as if it was +/// imported, e.g. `Method::Get`. +/// * `name`: A unique name for the endpoint. +/// Generally this will be the same as the containing module. +/// * `path`: The path component of the URL for the endpoint, e.g. "/foo/bar". +/// Components of the path that are parameterized can indicate a varible by using a Rust +/// identifier prefixed with a colon, e.g. `/foo/:some_parameter`. +/// A corresponding query string parameter will be expected in the request struct (see below +/// for details). +/// * `rate_limited`: Whether or not the endpoint enforces rate limiting on requests. +/// * `requires_authentication`: Whether or not the endpoint requires a valid access token. +/// +/// ## Request +/// +/// The request block contains normal struct field definitions. +/// Doc comments and attributes are allowed as normal. +/// There are also a few special attributes available to control how the struct is converted into a +/// `hyper::Request`: +/// +/// * `#[ruma_api(header)]`: Fields with this attribute will be treated as HTTP headers on the +/// request. +/// The value must implement `hyper::header::Header`. +/// * `#[ruma_api(path)]`: Fields with this attribute will be inserted into the matching path +/// component of the request URL. +/// * `#[ruma_api(query)]`: Fields with this attribute will be inserting into the URL's query +/// string. +/// +/// Any field that does not include one of these attributes will be part of the request's JSON +/// body. +/// +/// ## Response +/// +/// Like the request block, the response block consists of normal struct field definitions. +/// Doc comments and attributes are allowed as normal. +/// There is also a special attribute available to control how the struct is created from a +/// `hyper::Request`: +/// +/// * `#[ruma_api(header)]`: Fields with this attribute will be treated as HTTP headers on the +/// response. +/// The value must implement `hyper::header::Header`. +/// +/// Any field that does not include the above attribute will be expected in the response's JSON +/// body. +/// +/// ## Newtype bodies +/// +/// Both the request and response block also support "newtype bodies" by using the +/// `#[ruma_api(body)]` attribute on a field. If present on a field, the entire request or response +/// body will be treated as the value of the field. This allows you to treat the entire request or +/// response body as a specific type, rather than a JSON object with named fields. Only one field in +/// each struct can be marked with this attribute. It is an error to have a newtype body field and +/// normal body fields within the same struct. +/// +/// # Examples +/// +/// ```rust,no_run +/// #![feature(associated_consts, proc_macro, try_from)] +/// +/// extern crate futures; +/// extern crate hyper; +/// extern crate ruma_api; +/// extern crate ruma_api_macros; +/// extern crate serde; +/// #[macro_use] extern crate serde_derive; +/// extern crate serde_json; +/// extern crate serde_urlencoded; +/// extern crate url; +/// +/// # fn main() { +/// pub mod some_endpoint { +/// use hyper::header::ContentType; +/// use ruma_api_macros::ruma_api; +/// +/// ruma_api! { +/// metadata { +/// description: "Does something.", +/// method: Method::Get, +/// name: "some_endpoint", +/// path: "/_matrix/some/endpoint/:baz", +/// rate_limited: false, +/// requires_authentication: false, +/// } +/// +/// request { +/// pub foo: String, +/// #[ruma_api(header)] +/// pub content_type: ContentType, +/// #[ruma_api(query)] +/// pub bar: String, +/// #[ruma_api(path)] +/// pub baz: String, +/// } +/// +/// response { +/// #[ruma_api(header)] +/// pub content_type: ContentType, +/// pub value: String, +/// } +/// } +/// } +/// +/// pub mod newtype_body_endpoint { +/// use ruma_api_macros::ruma_api; +/// +/// #[derive(Debug, Deserialize)] +/// pub struct MyCustomType { +/// pub foo: String, +/// } +/// +/// ruma_api! { +/// metadata { +/// description: "Does something.", +/// method: Method::Get, +/// name: "newtype_body_endpoint", +/// path: "/_matrix/some/newtype/body/endpoint", +/// rate_limited: false, +/// requires_authentication: false, +/// } +/// +/// request { +/// #[ruma_api(body)] +/// pub file: Vec, +/// } +/// +/// response { +/// #[ruma_api(body)] +/// pub my_custom_type: MyCustomType, +/// } +/// } +/// } +/// # } +/// ``` #[proc_macro] pub fn ruma_api(input: TokenStream) -> TokenStream { let entries = parse_entries(&input.to_string()).expect("ruma_api! failed to parse input");