Skip to content

Commit 4df1ca7

Browse files
committed
initial commit
0 parents  commit 4df1ca7

16 files changed

+788
-0
lines changed

.github/workflows/rust.yaml

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Rust
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ master ]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
12+
jobs:
13+
build:
14+
name: build
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout sources
18+
uses: actions/checkout@v2
19+
20+
- name: Install stable toolchain
21+
uses: actions-rs/toolchain@v1
22+
with:
23+
profile: minimal
24+
toolchain: stable
25+
override: true
26+
components: rustfmt, clippy
27+
28+
- name: Run cargo fmt
29+
uses: actions-rs/cargo@v1
30+
with:
31+
command: fmt
32+
args: --all -- --check
33+
34+
- name: Run cargo clippy
35+
uses: actions-rs/cargo@v1
36+
with:
37+
command: clippy
38+
args: --tests --all-targets --all-features -- -D warnings
39+
40+
- name: Run cargo build
41+
uses: actions-rs/cargo@v1
42+
with:
43+
command: build
44+
args: --all-targets --all-features
45+
46+
- name: Run cargo test
47+
uses: actions-rs/cargo@v1
48+
with:
49+
command: test
50+
args: --all-features

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/target
2+
Cargo.lock
3+
todo

Cargo.toml

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "choices"
3+
version = "0.1.0"
4+
authors = ["Trisfald <[email protected]>"]
5+
description = "HTTP configuration service by defining a struct."
6+
documentation = "https://docs.rs/choices"
7+
repository = "https://github.com/trisfald/choices"
8+
keywords = ["configuration", "derive", "http"]
9+
categories = ["configuration", "web-programming"]
10+
edition = "2018"
11+
license = "MIT"
12+
readme = "README.md"
13+
14+
[dependencies]
15+
choices-derive = { path = "choices-derive", version = "=0.1.0" }
16+
async-trait = "0.1"
17+
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
18+
warp = "0.3"
19+
20+
[dev-dependencies]
21+
lazy_static = "1.4"
22+
reqwest = "0.11"

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Trisfald
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# `choices`
2+
3+
[![works badge](https://cdn.jsdelivr.net/gh/nikku/[email protected]/badge.svg)](https://github.com/nikku/works-on-my-machine)
4+
5+
Do you like `structops` and `clap`?
6+
Do you write `microservices`?
7+
Continue reading!
8+
9+
`choices` is a library that lets you expose your application's configuration
10+
over HTTP with a simple struct!
11+
12+
## Look, it's easy
13+
14+
Given the following code:
15+
16+
```rust
17+
use choices::Choices;
18+
use lazy_static::lazy_static;
19+
20+
#[derive(Choices)]
21+
struct Config {
22+
debug: bool,
23+
id: Option<i32>,
24+
log_file: String,
25+
}
26+
27+
lazy_static! {
28+
static ref CONFIG: Config = {
29+
Config {
30+
debug: false,
31+
id: Some(3),
32+
log_file: "log.txt".to_string()
33+
}
34+
};
35+
}
36+
37+
#[tokio::main]
38+
async fn main() {
39+
CONFIG.run(([127, 0, 0, 1], 8081)).await;
40+
}
41+
```
42+
43+
You can see all configuration fields at `localhost:8081/config`
44+
and the individual fields' values at `localhost:8081/config/<field name>`.
45+
46+
More examples in [examples](/examples).

choices-derive/Cargo.toml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "choices-derive"
3+
version = "0.1.0"
4+
authors = ["Trisfald <[email protected]>"]
5+
description = "HTTP configuration service by defining a struct, derive crate."
6+
documentation = "https://docs.rs/choices-derive"
7+
repository = "https://github.com/trisfald/choices"
8+
keywords = ["configuration", "derive", "http"]
9+
categories = ["configuration", "web-programming"]
10+
edition = "2018"
11+
license = "MIT"
12+
13+
[dependencies]
14+
syn = { version = "1", features = ["full"] }
15+
quote = "1"
16+
proc-macro2 = "1"
17+
proc-macro-error = "1"
18+
19+
[lib]
20+
proc-macro = true

choices-derive/src/lib.rs

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
//! Proc macros for the `choices` crate.
2+
3+
#![forbid(unsafe_code)]
4+
#![deny(missing_docs)]
5+
6+
extern crate proc_macro;
7+
8+
use proc_macro2::TokenStream;
9+
use proc_macro_error::{abort_call_site, proc_macro_error, set_dummy};
10+
use quote::quote;
11+
use syn::{punctuated::Punctuated, token::Comma, *};
12+
13+
const BASE_PATH: &str = "config";
14+
15+
/// Generates the `Choices` impl.
16+
#[proc_macro_derive(Choices)]
17+
#[proc_macro_error]
18+
pub fn choices(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
19+
let input: DeriveInput = syn::parse(input).unwrap();
20+
let gen = impl_choices(&input);
21+
gen.into()
22+
}
23+
24+
fn impl_choices(input: &DeriveInput) -> TokenStream {
25+
use syn::Data::*;
26+
27+
let struct_name = &input.ident;
28+
29+
set_dummy(quote! {
30+
#[async_trait::async_trait]
31+
impl ::choices::Choices for #struct_name {
32+
unimplemented!();
33+
}
34+
});
35+
36+
match input.data {
37+
Struct(DataStruct {
38+
fields: syn::Fields::Named(ref fields),
39+
..
40+
}) => impl_choices_for_struct(struct_name, &fields.named, &input.attrs),
41+
_ => abort_call_site!("choices only supports non-tuple structs"),
42+
}
43+
}
44+
45+
fn impl_choices_for_struct(
46+
name: &Ident,
47+
fields: &Punctuated<Field, Comma>,
48+
_attrs: &[Attribute],
49+
) -> TokenStream {
50+
let (macros, implementation, choices) = gen_choices_warp(fields);
51+
52+
quote! {
53+
#macros
54+
55+
#[allow(unused_variables)]
56+
#[allow(unknown_lints)]
57+
#[allow(
58+
clippy::style,
59+
clippy::complexity,
60+
clippy::pedantic,
61+
clippy::restriction,
62+
clippy::perf,
63+
clippy::deprecated,
64+
clippy::nursery,
65+
clippy::cargo
66+
)]
67+
#[deny(clippy::correctness)]
68+
#[allow(dead_code, unreachable_code)]
69+
impl #name {
70+
#implementation
71+
}
72+
73+
#[allow(unused_variables)]
74+
#[allow(unknown_lints)]
75+
#[allow(
76+
clippy::style,
77+
clippy::complexity,
78+
clippy::pedantic,
79+
clippy::restriction,
80+
clippy::perf,
81+
clippy::deprecated,
82+
clippy::nursery,
83+
clippy::cargo
84+
)]
85+
#[deny(clippy::correctness)]
86+
#[allow(dead_code, unreachable_code)]
87+
#[async_trait::async_trait]
88+
impl ::choices::Choices for #name {
89+
#choices
90+
}
91+
}
92+
}
93+
94+
fn gen_choices_warp(fields: &Punctuated<Field, Comma>) -> (TokenStream, TokenStream, TokenStream) {
95+
let fields_populators = fields.iter().map(|field| {
96+
let field_ident = field
97+
.ident
98+
.as_ref()
99+
.expect("unnamed fields are not supported!");
100+
let field_name = field_ident.to_string();
101+
Some(quote! {
102+
warp::path!(#BASE_PATH / #field_name).map(move || format!("{}", $self.#field_ident.body_string()) )
103+
})
104+
});
105+
106+
let index_string = compute_index_string(fields);
107+
108+
let macro_tk = quote! {
109+
macro_rules! create_filter {
110+
($self:ident) => {{
111+
use warp::Filter;
112+
#[allow(unused_imports)]
113+
use choices::ChoicesOutput;
114+
115+
let index = warp::path(#BASE_PATH).map(|| #index_string);
116+
warp::get().and(
117+
index.and(warp::path::end())
118+
#( .or(#fields_populators) )*
119+
)
120+
}};
121+
}
122+
};
123+
124+
let implementation_tk = quote! {
125+
/// If you want more control over the http server instance you can use this
126+
/// function to retrieve the configuration's `warp::Filter`.
127+
fn filter(&'static self) -> warp::filters::BoxedFilter<(impl warp::Reply,)> {
128+
use warp::Filter;
129+
create_filter!(self).boxed()
130+
}
131+
};
132+
133+
let trait_tk = quote! {
134+
async fn run<T: Into<std::net::SocketAddr> + Send>(&'static self, addr: T) {
135+
let filter = create_filter!(self);
136+
warp::serve(filter).run(addr).await
137+
}
138+
};
139+
140+
(macro_tk, implementation_tk, trait_tk)
141+
}
142+
143+
fn compute_index_string(fields: &Punctuated<Field, Comma>) -> String {
144+
let mut index = "Available configuration options:\n".to_string();
145+
fields.iter().for_each(|field| {
146+
let field_ident = field
147+
.ident
148+
.as_ref()
149+
.expect("unnamed fields are not supported!");
150+
let type_name = compute_type_string(&field.ty);
151+
index += &format!(" - {}: {}\n", &field_ident.to_string(), type_name);
152+
});
153+
index
154+
}
155+
156+
fn compute_type_string(ty: &Type) -> String {
157+
match ty {
158+
Type::Path(ref typepath) if typepath.qself.is_none() => typepath
159+
.path
160+
.segments
161+
.iter()
162+
.into_iter()
163+
.fold(String::new(), |mut acc, v| {
164+
acc.push_str(&v.ident.to_string());
165+
if let PathArguments::AngleBracketed(ref arguments) = &v.arguments {
166+
if arguments.args.len() > 1 {
167+
abort_call_site!(
168+
"generic types parameterized on more than one type are not supported"
169+
)
170+
}
171+
if let Some(args) = arguments.args.first() {
172+
if let GenericArgument::Type(inner_type) = args {
173+
acc.push_str(
174+
&("<".to_owned() + &compute_type_string(inner_type) + ">"),
175+
);
176+
}
177+
}
178+
}
179+
acc
180+
}),
181+
_ => abort_call_site!("choices supports only simple types (syn::Type::Path) for fields"),
182+
}
183+
}

examples/README.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Many examples about how to use `choices`
2+
3+
### [Simple read only](simple_readonly.rs)
4+
5+
Create a simple read only configuration.
6+
7+
### [Custom warp server](custom_warp_server.rs)
8+
9+
Run the configuration service on your own server instance.

examples/custom_warp_server.rs

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//! Example of using an existing server for configuration.
2+
3+
// Run the example with `cargo run --example custom_warp_server` then try the following commands:
4+
// `curl localhost:8081/config/user`
5+
// `curl localhost:8081/hello`
6+
7+
use choices::Choices;
8+
use lazy_static::lazy_static;
9+
use warp::Filter;
10+
11+
#[derive(Choices)]
12+
struct Config {
13+
user: String,
14+
}
15+
16+
lazy_static! {
17+
static ref CONFIG: Config = {
18+
Config {
19+
user: "Matt".to_string(),
20+
}
21+
};
22+
}
23+
24+
#[tokio::main]
25+
async fn main() {
26+
let routes = CONFIG.filter().or(warp::path("hello").map(|| "Hello!"));
27+
warp::serve(routes).run(([127, 0, 0, 1], 8081)).await;
28+
}

0 commit comments

Comments
 (0)