1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
//! QueryParams is a procedural macro for deriving a [`Hyper`]-centric representation
//! of that struct as query parameters that can be easily appended to query parameters in the Hyper
//! framework. *This crate is only meant to be tested and re-exported by the `QueryParams` crate,
//! and is not meant for direct consumption.*
//!
//! [`Hyper`]: https://crates.io/crates/hyper
use proc_macro::{self, TokenStream};
use quote::quote;
use std::collections::HashSet;
use std::vec::Vec;
use syn::__private::TokenStream2;
use syn::{parse_macro_input, Attribute, DeriveInput, Field, Fields, Ident, LitStr, Path, Type};
#[derive(Debug, Eq, PartialEq, Hash)]
enum FieldAttributes {
Required,
Excluded,
Rename(String),
}
struct FieldDescription<'f> {
pub field: &'f Field,
pub field_name: String,
pub ident: Ident,
pub attributes: HashSet<FieldAttributes>,
}
/// [`QueryParams`] derives `fn to_query_params(&self) -> Vec<(String, String)>` for
/// any struct with field values supporting `.to_string()`.
///
/// Optional values are only included if present,
/// and fields marked `#[query(required)]` must be non-optional. Renaming and excluding of fields is
/// also available, using `#[query(rename = "new_name")]` or `#[query(exclude)]` on the field.
///
/// # Example: Query Params
/// QueryParams supports both required and optional fields, which won't be included in the output
/// if their value is None.
///
/// ```
/// # use query_params_macro::QueryParams;
/// # // trait defined here again since it can't be provided by macro crate
/// # pub trait ToQueryParams {
/// # fn to_query_params(&self) -> Vec<(String, String)>;
/// # }
/// // Eq and PartialEq are just for assertions
/// #[derive(QueryParams, Debug, PartialEq, Eq)]
/// struct ProductRequest {
/// #[query(required)]
/// id: i32,
/// min_price: Option<i32>,
/// max_price: Option<i32>,
/// }
///
/// pub fn main() {
/// let request = ProductRequest {
/// id: 999, // will be included in output
/// min_price: None, // will *not* be included in output
/// max_price: Some(100), // will be included in output
/// };
///
/// let expected = vec![
/// ("id".into(), "999".into()),
/// ("max_price".into(), "100".into())
/// ];
///
/// let query_params = request.to_query_params();
///
/// assert_eq!(expected, query_params);
/// }
/// ```
///
/// ## Attributes
/// QueryParams supports attributes under `#[query(...)]` on individual fields to carry metadata.
/// At this time, the available attributes are:
/// - required -- marks a field as required, meaning it can be `T` instead of `Option<T>` on the struct
/// and will always appear in the resulting `Vec`
/// - rename -- marks a field to be renamed when it is output in the resulting Vec.
/// E.g. `#[query(rename = "newName")]`
/// - exclude -- marks a field to never be included in the output query params
///
/// # Example: Renaming and Excluding
/// In some cases, names of query parameters are not valid identifiers, or don't adhere to Rust's
/// default style of "snake_case". [`QueryParams`] can rename individual fields when creating the
/// query parameters Vec if the attribute with the rename attribute: `#[query(rename = "new_name")]`.
///
/// In the below example, an API expects a type of product and a max price, given as
/// `type=something&maxPrice=123`, which would be and invalid identifier and a non-Rust style
/// field name respectively. A field containing local data that won't be included in the query
/// is also tagged as `#[query(exclude)]` to exclude it.
///
/// ```
/// # use query_params_macro::QueryParams;
/// # // trait defined here again since it can't be provided by macro crate
/// # pub trait ToQueryParams {
/// # fn to_query_params(&self) -> Vec<(String, String)>;
/// # }
/// // Eq and PartialEq are just for assertions
/// #[derive(QueryParams, Debug, PartialEq, Eq)]
/// struct ProductRequest {
/// #[query(required)]
/// id: i32,
/// #[query(rename = "type")]
/// product_type: Option<String>,
/// #[query(rename = "maxPrice")]
/// max_price: Option<i32>,
/// #[query(exclude)]
/// private_data: i32,
/// }
///
/// pub fn main() {
/// let request = ProductRequest {
/// id: 999,
/// product_type: Some("accessory".into()),
/// max_price: Some(100),
/// private_data: 42, // will not be part of the output
/// };
///
/// let expected = vec![
/// ("id".into(), "999".into()),
/// ("type".into(), "accessory".into()),
/// ("maxPrice".into(), "100".into())
/// ];
///
/// let query_params = request.to_query_params();
///
/// assert_eq!(expected, query_params);
/// }
/// ```
#[proc_macro_derive(QueryParams, attributes(query))]
pub fn derive(input: TokenStream) -> TokenStream {
let ast: DeriveInput = parse_macro_input!(input);
let ident = ast.ident;
let fields: &Fields = match ast.data {
syn::Data::Struct(ref s) => &s.fields,
_ => panic!("Can only derive QueryParams for structs."),
};
let named_fields: Vec<&Field> = fields
.iter()
.filter_map(|field| field.ident.as_ref().map(|_ident| field))
.collect();
let field_descriptions = named_fields
.into_iter()
.map(map_field_to_description)
.filter(|field| !field.attributes.contains(&FieldAttributes::Excluded))
.collect::<Vec<FieldDescription>>();
let required_fields: Vec<&FieldDescription> = field_descriptions
.iter()
.filter(|desc| desc.attributes.contains(&FieldAttributes::Required))
.collect();
let req_names: Vec<String> = required_fields
.iter()
.map(|field| field.field_name.clone())
.collect();
let req_idents: Vec<&Ident> = required_fields.iter().map(|field| &field.ident).collect();
let vec_definition = quote! {
let mut query_params: ::std::vec::Vec<(String, String)> =
vec![#((
#req_names.to_string(),
self.#req_idents.to_string()
)),*];
};
let vec_encoded_definition = quote! {
let mut query_params: ::std::vec::Vec<(String, String)> =
vec![#(
(
::to_query_params::urlencoding::encode(#req_names).into_owned(),
::to_query_params::urlencoding::encode(&self.#req_idents.to_string()).into_owned()
)
),*];
};
let optional_fields: Vec<&FieldDescription> = field_descriptions
.iter()
.filter(|desc| !desc.attributes.contains(&FieldAttributes::Required))
.collect();
optional_fields.iter().for_each(validate_optional_field);
let optional_assignments: TokenStream2 = optional_fields
.iter()
.map(|field| {
let ident = &field.ident;
let name = &field.field_name;
quote! {
if let Some(val) = &self.#ident {
query_params.push((
#name.to_string(),
val.to_string()
));
}
}
})
.collect();
let optional_encoded_assignments: TokenStream2 = optional_fields
.iter()
.map(|field| {
let ident = &field.ident;
let name = &field.field_name;
quote! {
if let Some(val) = &self.#ident {
query_params.push(
(
::to_query_params::urlencoding::encode(#name).into_owned(),
::to_query_params::urlencoding::encode(&val.to_string()).into_owned()
)
);
}
}
})
.collect();
let trait_impl = quote! {
#[allow(dead_code)]
impl ToQueryParams for #ident {
fn to_query_params(&self) -> ::std::vec::Vec<(String, String)> {
#vec_definition
#optional_assignments
query_params
}
fn to_encoded_params(&self) -> ::std::vec::Vec<(String, String)> {
#vec_encoded_definition
#optional_encoded_assignments
query_params
}
}
};
trait_impl.into()
}
fn map_field_to_description(field: &Field) -> FieldDescription {
let attributes = field
.attrs
.iter()
.flat_map(parse_query_attributes)
.collect::<HashSet<FieldAttributes>>();
let mut desc = FieldDescription {
field,
field_name: field.ident.as_ref().unwrap().to_string(),
ident: field.ident.clone().unwrap(),
attributes,
};
let name = name_from_field_description(&desc);
desc.field_name = name;
desc
}
fn name_from_field_description(field: &FieldDescription) -> String {
let mut name = field.ident.to_string();
for attribute in field.attributes.iter() {
if let FieldAttributes::Rename(rename) = attribute {
name = (*rename).clone();
}
}
name
}
fn parse_query_attributes(attr: &Attribute) -> Vec<FieldAttributes> {
let mut attrs = Vec::new();
if attr.path().is_ident("query") {
attr.parse_nested_meta(|m| {
if m.path.is_ident("required") {
attrs.push(FieldAttributes::Required);
}
if m.path.is_ident("exclude") {
attrs.push(FieldAttributes::Excluded);
}
if m.path.is_ident("rename") {
let value = m.value().unwrap();
let rename: LitStr = value.parse().unwrap();
attrs.push(FieldAttributes::Rename(rename.value()));
}
Ok(())
})
.expect("Unsupported attribute found in #[query(...)] attribute");
}
attrs
}
fn validate_optional_field(field_desc: &&FieldDescription) {
if let Type::Path(type_path) = &field_desc.field.ty {
if !(type_path.qself.is_none() && path_is_option(&type_path.path)) {
panic!("Non-optional types must be marked with #[query(required)] attribute")
}
}
}
fn path_is_option(path: &Path) -> bool {
path.leading_colon.is_none()
&& path.segments.len() == 1
&& path.segments.iter().next().unwrap().ident == "Option"
}