~lthms/typed-urls

b6e74d6d2614ad556fb0d8d49938d1be9e703f66 — Thomas Letan 11 months ago
Initial commit
7 files changed, 401 insertions(+), 0 deletions(-)

A .gitignore
A Cargo.toml
A routes-derive/Cargo.toml
A routes-derive/src/lib.rs
A rustfmt.toml
A src/lib.rs
A tests/simple.rs
A  => .gitignore +2 -0
@@ 1,2 @@
/target
Cargo.lock

A  => Cargo.toml +12 -0
@@ 1,12 @@
[workspace]
members = ['routes-derive']

[package]
name = "routes"
version = "0.1.0"
authors = ["Thomas Letan <lthms@soap.coffee>"]
edition = "2018"

[dependencies]
serde_json = "*"
routes-derive = { path = "routes-derive/" }
\ No newline at end of file

A  => routes-derive/Cargo.toml +13 -0
@@ 1,13 @@
[package]
name = "routes-derive"
version = "0.1.0"
authors = ["Thomas Letan <lthms@soap.coffee>"]
edition = "2018"

[lib]
proc-macro = true

[dependencies]
syn = "1.0"
quote = "1.0"
serde_json = "*"
\ No newline at end of file

A  => routes-derive/src/lib.rs +313 -0
@@ 1,313 @@
#[macro_use]
extern crate syn;
#[macro_use]
extern crate quote;

use std::collections::HashMap;

use proc_macro::TokenStream;
use syn::export::{Span, TokenStream2 as Quote};
use syn::{
    Attribute, Data, DataEnum, DeriveInput, Fields, Ident, Lit, LitStr, Meta,
    MetaNameValue, Type, Variant,
};

#[proc_macro_derive(Router, attributes(routefmt))]
pub fn derive_routes_main(input : TokenStream) -> TokenStream {
    let ast : DeriveInput = parse_macro_input!(input as DeriveInput);

    let res = derive_routes_boilerplate(&ast);

    res.into()
}

fn find_format(attrs : &Vec<Attribute>) -> LitStr {
    for attr in attrs {
        if attr.path.is_ident("routefmt") {
            match attr.parse_meta().unwrap() {
                Meta::NameValue(MetaNameValue {
                    lit: Lit::Str(str), ..
                }) => return str,
                _ => break,
            }
        } else {
            continue;
        }
    }

    panic!("Missing attribute `routefmt'")
}

fn format_table(ast : &DataEnum) -> HashMap<Ident, LitStr> {
    let mut res = HashMap::with_capacity(ast.variants.len());

    for variant in ast.variants.iter() {
        let ident = variant.ident.clone();
        let fmt = find_format(&variant.attrs);

        res.insert(ident, fmt);
    }

    res
}

fn get_fields_name(ast : &Variant) -> Vec<Ident> {
    match ast.fields {
        Fields::Named(ref named_fields) => named_fields
            .named
            .iter()
            .map(|x| x.ident.clone().unwrap())
            .collect::<Vec<Ident>>(),
        Fields::Unit => Vec::new(),
        _ => panic!("Unnamed fields are not supported"),
    }
}

fn get_typed_fields(ast : &Variant) -> Vec<(Type, Ident)> {
    match ast.fields {
        Fields::Named(ref named_fields) => named_fields
            .named
            .iter()
            .map(|x| (x.ty.clone(), x.ident.clone().unwrap()))
            .collect::<Vec<_>>(),
        Fields::Unit => Vec::new(),
        _ => panic!("Unnamed fields are not supported"),
    }
}

fn fields_table(ast : &DataEnum) -> HashMap<Ident, Vec<Ident>> {
    let mut res = HashMap::with_capacity(ast.variants.len());

    for variant in ast.variants.iter() {
        let ident = variant.ident.clone();
        let fields = get_fields_name(&variant);

        res.insert(ident, fields);
    }

    res
}

fn typed_fields_table(ast : &DataEnum) -> HashMap<Ident, Vec<(Type, Ident)>> {
    let mut res = HashMap::with_capacity(ast.variants.len());

    for variant in ast.variants.iter() {
        let ident = variant.ident.clone();
        let fields = get_typed_fields(&variant);

        res.insert(ident, fields);
    }

    res
}

fn derive_ast_to_string(
    name : &Ident,
    ast : &DataEnum,
    fmts : &HashMap<Ident, LitStr>,
    fields : &HashMap<Ident, Vec<Ident>>,
) -> Quote {
    let mut match_bodies = vec![];

    for variant in ast.variants.iter() {
        let ident = variant.ident.clone();
        let fmt = fmts.get(&ident).unwrap();
        let flds = fields.get(&ident).unwrap();

        match_bodies.push(quote! {
            #name::#ident { #(#flds),* } => format!(#fmt, #(#flds),*)
        });
    }

    quote! { impl #name {
        fn to_string(&self) -> String {
            match self {
                #(#match_bodies),*
            }
        }
    }}
}

fn ident_to_litstr(
    prefix : &str,
    id : &Ident,
    suffix : &str,
) -> LitStr {
    LitStr::new(&format!("{}{}{}", prefix, id, suffix), Span::call_site())
}

fn derive_route_to_string(
    ast : &DataEnum,
    fmts : &HashMap<Ident, LitStr>,
    fields : &HashMap<Ident, Vec<Ident>>,
) -> Quote {
    let mut match_bodies = vec![];

    for variant in ast.variants.iter() {
        let ident = variant.ident.clone();
        let fmt = fmts.get(&ident).unwrap();
        let flds = fields
            .get(&ident)
            .unwrap()
            .iter()
            .map(|x| ident_to_litstr("{", x, "}"))
            .collect::<Vec<LitStr>>();

        match_bodies.push(quote! {
            Route::#ident => format!(#fmt, #(#flds),*)
        });
    }

    quote! {
        fn to_string(&self) -> String {
            match self {
                #(#match_bodies),*
            }
        }
    }
}

fn derive_routes_boilerplate(ast : &DeriveInput) -> Quote {
    let enum_name = &ast.ident;
    let enum_ast = get_enum_data(&ast);

    let fields = fields_table(&enum_ast);
    let typed_field = typed_fields_table(&enum_ast);
    let fmts = format_table(&enum_ast);

    let route_enum = derive_route_enum(enum_ast);

    let route_routes_impl = derive_route_routes_impl(
        enum_name,
        enum_ast,
        &fmts,
        &typed_field,
        &fields,
    );

    let routes = derive_routes_array(enum_ast);

    let enum_to_string =
        derive_ast_to_string(enum_name, &enum_ast, &fmts, &fields);

    quote! {
        use std::iter::Iterator;
        use routes::Routes;

        #enum_to_string
        #route_enum
        #routes
        #route_routes_impl
    }
}

fn get_enum_data(input : &DeriveInput) -> &DataEnum {
    match input.data {
        Data::Enum(ref data) => data,
        _ => panic!("Should be an enum"),
    }
}

fn derive_route_enum(enum_ast : &DataEnum) -> Quote {
    let idents = enum_ast
        .variants
        .iter()
        .map(|x| {
            let tok = &x.ident;
            quote! { #tok }
        })
        .collect::<Vec<_>>();

    quote! {
        #[derive(Debug, Clone)]
        pub enum Route {
            #(#idents),*
        }
    }
}

fn derive_route_to_url(
    name : &Ident,
    enum_ast : &DataEnum,
    tf : &HashMap<Ident, Vec<(Type, Ident)>>,
    uf : &HashMap<Ident, Vec<Ident>>,
) -> Quote {
    let mut match_bodies = vec![];

    for variant in enum_ast.variants.iter() {
        let ident = variant.ident.clone();
        let typed_fields = tf.get(&ident).unwrap();
        let untyped_fields = uf.get(&ident).unwrap();

        let mut arg_fetch = vec![];

        for (ty, field) in typed_fields.iter() {
            let fname = ident_to_litstr("", field, "");

            arg_fetch.push(quote! {
                let #field = args.get(#fname).and_then(<#ty as Arg>::from_value)?;
            })
        }

        match_bodies.push(quote! {
            Route::#ident => {
                #(#arg_fetch)*
                Some(#name::#ident { #(#untyped_fields),* })
            }
        });
    }

    quote! {
        fn to_url(
            &self,
            args : &std::collections::HashMap<String, serde_json::Value>,
        ) -> Option<#name> {
            use routes::Arg;
            match self {
                #(#match_bodies),*
            }
        }
    }
}

fn derive_route_routes_impl(
    name : &Ident,
    enum_ast : &DataEnum,
    fmts : &HashMap<Ident, LitStr>,
    tf : &HashMap<Ident, Vec<(Type, Ident)>>,
    uf : &HashMap<Ident, Vec<Ident>>,
) -> Quote {
    let to_string_impl = derive_route_to_string(enum_ast, fmts, uf);
    let to_url_impl = derive_route_to_url(name, enum_ast, tf, uf);

    quote! {
        impl Routes for Route {
            type Url = #name;

            #to_string_impl

            #to_url_impl

            fn enumerate() -> std::slice::Iter<'static, Self> {
                ROUTES.iter()
            }
        }
    }
}

fn derive_routes_array(enum_ast : &DataEnum) -> Quote {
    let idents = enum_ast
        .variants
        .iter()
        .map(|x| {
            let tok = &x.ident;
            quote! { #tok }
        })
        .collect::<Vec<_>>();

    quote! {
        const ROUTES : &[Route] = &[
            #(Route::#idents),*
        ];
    }
}

A  => rustfmt.toml +3 -0
@@ 1,3 @@
space_before_colon = true
max_width = 80
fn_args_layout = "Vertical"
\ No newline at end of file

A  => src/lib.rs +35 -0
@@ 1,35 @@
use std::collections::HashMap;
use std::slice::Iter;

use serde_json::Value;

pub trait Routes {
    type Url;

    fn enumerate() -> Iter<'static, Self>
    where
        Self : std::marker::Sized;
    fn to_string(&self) -> String;
    fn to_url(
        &self,
        args : &HashMap<String, Value>,
    ) -> Option<Self::Url>;
}

pub trait Arg {
    fn from_value(value : &Value) -> Option<Self>
    where
        Self : std::marker::Sized;
}

impl Arg for String {
    fn from_value(value : &Value) -> Option<Self> {
        value.as_str().map(|x| x.to_owned())
    }
}

impl Arg for i32 {
    fn from_value(value : &Value) -> Option<Self> {
        value.as_i64().map(|x| x as i32)
    }
}

A  => tests/simple.rs +23 -0
@@ 1,23 @@
#[macro_use]
extern crate routes_derive;

#[derive(Router)]
pub enum Url {
    #[routefmt = "/worlds"]
    Worlds,
    #[routefmt = "/worlds/{}"]
    World { world_key : String },
    #[routefmt = "/worlds/{}/character/{}"]
    Character {
        world_key : String,
        character_key : String,
    },
    #[routefmt = "/worlds/{}/map/{}/tile/{}/{}/{}"]
    Tile {
        world_key : String,
        map_key : String,
        x : i32,
        y : i32,
        z : i32,
    },
}