🏡 index : ~doyle/tinfoil.git

author Jordan Doyle <jordan@doyle.la> 2021-06-20 2:13:54.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-06-20 2:24:36.0 +01:00:00
commit
783fdef6f23114c451b8da90a11b4c2e81884eb1 [patch]
tree
b335d0c4b08aab626b2f498b5b46ddde7ddcf104
download
783fdef6f23114c451b8da90a11b4c2e81884eb1.tar.gz

Initial commit



Diff

 .gitignore                           |   3 +++
 Cargo.toml                           |   2 ++
 LICENSE                              |  13 +++++++++++++
 README.md                            |   9 +++++++++
 tinfoil-macros/Cargo.toml            |  15 +++++++++++++++
 tinfoil-tests/Cargo.toml             |  11 +++++++++++
 tinfoil/Cargo.toml                   |  11 +++++++++++
 tinfoil-macros/src/implementation.rs | 223 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 tinfoil-macros/src/lib.rs            |  11 +++++++++++
 tinfoil-tests/src/lib.rs             |  53 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 tinfoil/src/internals.rs             |   2 ++
 tinfoil/src/lib.rs                   |  23 +++++++++++++++++++++++
 12 files changed, 376 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..77147e2 100644
--- /dev/null
+++ a/.gitignore
@@ -1,0 +1,3 @@
/target
Cargo.lock
.idea/
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..3cc474a 100644
--- /dev/null
+++ a/Cargo.toml
@@ -1,0 +1,2 @@
[workspace]
members = ["tinfoil", "tinfoil-macros", "tinfoil-tests"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8b1a9d8 100644
--- /dev/null
+++ a/LICENSE
@@ -1,0 +1,13 @@
           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
                   Version 2, December 2004

Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>

Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.

           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
  TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

 0. You just DO WHAT THE FUCK YOU WANT TO.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f6b1f92 100644
--- /dev/null
+++ a/README.md
@@ -1,0 +1,9 @@
# Tinfoil - Rust dependency injection

Not anywhere close to production ready.

Example: https://github.com/w4/tinfoil/blob/master/tinfoil-tests/src/lib.rs

TODO:
- lazy loading
- generics
diff --git a/tinfoil-macros/Cargo.toml b/tinfoil-macros/Cargo.toml
new file mode 100644
index 0000000..6b55363 100644
--- /dev/null
+++ a/tinfoil-macros/Cargo.toml
@@ -1,0 +1,15 @@
[package]
name = "tinfoil-macros"
version = "0.0.1"
authors = ["Jordan Doyle <jordan@doyle.la>"]
edition = "2018"

[lib]
proc-macro = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
quote = ""
syn = ""
darling = ""
diff --git a/tinfoil-tests/Cargo.toml b/tinfoil-tests/Cargo.toml
new file mode 100644
index 0000000..4b7f64b 100644
--- /dev/null
+++ a/tinfoil-tests/Cargo.toml
@@ -1,0 +1,11 @@
[package]
name = "tinfoil-tests"
version = "0.0.1"
authors = ["Jordan Doyle <jordan@doyle.la>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tinfoil = { path = "../tinfoil" }
tinfoil-macros = { path = "../tinfoil-macros" }
diff --git a/tinfoil/Cargo.toml b/tinfoil/Cargo.toml
new file mode 100644
index 0000000..382c4af 100644
--- /dev/null
+++ a/tinfoil/Cargo.toml
@@ -1,0 +1,11 @@
[package]
name = "tinfoil"
version = "0.0.1"
authors = ["Jordan Doyle <jordan@doyle.la>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
daggy = "0.7"
petgraph = "0.5"
diff --git a/tinfoil-macros/src/implementation.rs b/tinfoil-macros/src/implementation.rs
new file mode 100644
index 0000000..3128509 100644
--- /dev/null
+++ a/tinfoil-macros/src/implementation.rs
@@ -1,0 +1,223 @@
use darling::FromField;
use quote::quote;
use syn::{spanned::Spanned, Data, DeriveInput, Type};

type Result<T> = std::result::Result<T, proc_macro::TokenStream>;

fn replace_lifetimes_with_static(ty: &Type) -> Type {
    match ty {
        syn::Type::Reference(v) => {
            let mut v = v.clone();
            v.lifetime = Some(syn::Lifetime::new("'static", v.span()));
            if let syn::Type::Path(path) = v.elem.as_mut() {
                for segment in &mut path.path.segments {
                    if let syn::PathArguments::AngleBracketed(bracketed) = &mut segment.arguments {
                        for arg in &mut bracketed.args {
                            if let syn::GenericArgument::Lifetime(lifetime) = arg {
                                *lifetime = syn::Lifetime::new("'static", ty.span());
                            }
                        }
                    }
                }
            }
            syn::Type::Reference(v)
        }
        v => v.clone(),
    }
}

pub fn tinfoil(input: proc_macro::TokenStream) -> Result<proc_macro::TokenStream> {
    let input: DeriveInput = syn::parse(input).map_err(|e| e.to_compile_error())?;

    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let s = match input.data {
        Data::Struct(v) => v,
        _ => panic!("not a struct"),
    };
    let types = s
        .fields
        .iter()
        .map(|v| replace_lifetimes_with_static(&v.ty));
    let fields = s.fields.iter().map(|v| &v.ident);

    let expanded = quote! {
        #[allow(missing_docs, single_use_lifetimes)]
        impl #impl_generics Dependency<'a, InjectionContext<'a>> for #name #ty_generics #where_clause {
            const DEPENDENCIES: &'static [std::any::TypeId] = &[
                #(std::any::TypeId::of::<#types>()),*
            ];

            fn instn(context: &'a InjectionContext<'a>) -> Self {
                Self {
                    #(#fields: context.get()),*
                }
            }
        }
    };

    Ok(expanded.into())
}

#[derive(Clone, Debug, FromField)]
#[darling(attributes(tinfoil))]
struct TinfoilContextFieldOpts {
    #[darling(default)]
    parameter: bool,
    #[darling(default)]
    default: bool,
}

pub fn tinfoil_context(input: proc_macro::TokenStream) -> Result<proc_macro::TokenStream> {
    let input: DeriveInput = syn::parse(input).map_err(|e| e.to_compile_error())?;

    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let s = match input.data {
        Data::Struct(ref v) => v,
        _ => panic!("not a struct"),
    };

    let mut parameters = Vec::new();
    let mut parameter_types = Vec::new();

    let mut defaults = Vec::new();
    let mut defaults_types = Vec::new();

    let mut instantiate_from_ctx = Vec::new();
    let mut instantiate_from_ctx_types = Vec::new();

    for field in s.fields.iter() {
        let ident = field.ident.clone().unwrap();
        let field_opts = TinfoilContextFieldOpts::from_field(field).unwrap();

        if field_opts.parameter {
            parameters.push(ident);
            parameter_types.push(field.ty.clone());
        } else if field_opts.default {
            defaults.push(ident);
            defaults_types.push(field.ty.clone());
        } else if ident != "_pin" {
            match &field.ty {
                syn::Type::Path(v) => {
                    let segment = v.path.segments.first().unwrap();
                    if segment.ident != "MaybeUninit" {
                        return Err(syn::Error::new(
                            v.span(),
                            "values instantiated via a context must be wrapped in MaybeUninit",
                        )
                        .into_compile_error()
                        .into());
                    }
                    if let syn::PathArguments::AngleBracketed(
                        syn::AngleBracketedGenericArguments { args, .. },
                    ) = &segment.arguments
                    {
                        match args.first().unwrap() {
                            syn::GenericArgument::Type(syn::Type::Path(t)) => {
                                instantiate_from_ctx_types
                                    .push(t.path.segments.first().unwrap().clone().ident);
                            }
                            _ => panic!("invalid type"),
                        }
                    }
                }
                v => {
                    return Err(syn::Error::new(v.span(), "value must be owned")
                        .into_compile_error()
                        .into())
                }
            }

            instantiate_from_ctx.push(ident);
        }
    }

    let expanded = quote! {
        impl #impl_generics #name #ty_generics #where_clause {
            pub fn new(#(#parameters: #parameter_types),*) -> Pin<Box<Self>> {
                let mut context = Box::pin(InjectionContext {
                    #(#parameters,)*
                    #(#defaults: Default::default(),)*
                    #(#instantiate_from_ctx: MaybeUninit::uninit(),)*
                    _pin: PhantomPinned,
                });

                let mut dag: tinfoil::internals::Dag<i32, i32, usize> = tinfoil::internals::Dag::new();
                let initial = dag.add_node(0);
                let mut nodes = std::collections::HashMap::new();
                // TODO: fix generics & run through `replace_lifetimes_with_static`
                #(nodes.insert(std::any::TypeId::of::<&'static #instantiate_from_ctx_types<'static>>(), {
                    let node = dag.add_node(0);
                    dag.add_edge(initial, node, 0);
                    node
                });)*

                #(println!("{:?} - {}", std::any::TypeId::of::<&'static #instantiate_from_ctx_types<'static>>(), stringify!(#instantiate_from_ctx_types));)*

                #(
                    let node = nodes.get(&std::any::TypeId::of::<&'static #instantiate_from_ctx_types<'static>>()).unwrap();

                    for dependency in #instantiate_from_ctx_types::DEPENDENCIES {
                        eprintln!("{:#?}", nodes);
                        if let Some(dependency_node) = nodes.get(dependency) {
                            dag.add_edge(node.clone(), dependency_node.clone(), 0).expect("dependency cycle");
                        }
                    }
                )*

                eprintln!("{:?}", tinfoil::internals::petgraph::dot::Dot::with_config(dag.graph(), &[tinfoil::internals::petgraph::dot::Config::EdgeIndexLabel]));

                let mut bfs = tinfoil::internals::petgraph::visit::Dfs::new(dag.graph(), initial);
                while let Some(nx) = bfs.next(dag.graph()) {
                    // skip root
                    if nx == initial { continue; }

                    let ty = *nodes.iter().find(|(k, v)| **v == nx).expect("couldn't find index").0;

                    if false {}
                    #(else if ty == std::any::TypeId::of::<&'static #instantiate_from_ctx_types<'static>>() {
                        // this is safe because we know MyCoolValue is initialised and will be for the lifetime of
                        // InjectionContext
                        let value = MaybeUninit::new(#instantiate_from_ctx_types::instn(context.as_ref().get_ref()));

                        // this is safe because we don't move any values
                        unsafe {
                            // let mut_ref: Pin<&mut InjectionContext> = Pin::as_mut(&mut context);
                            let mut_ref: Pin<&mut InjectionContext> = std::mem::transmute(context.as_ref());
                            Pin::get_unchecked_mut(mut_ref).#instantiate_from_ctx = value;
                        }
                    })*
                    else {
                        panic!("unknown type");
                    }
                }

                context
            }
        }

        #(impl #impl_generics Provider<'a, &'a #parameter_types> for #name #ty_generics #where_clause {
            fn get(&'a self) -> &'a #parameter_types {
                &self.#parameters
            }
        })*

        #(impl #impl_generics Provider<'a, &'a #defaults_types> for #name #ty_generics #where_clause {
            fn get(&'a self) -> &'a #defaults_types {
                &self.#defaults
            }
        })*


        #(impl #impl_generics Provider<'a, &'a #instantiate_from_ctx_types<'a>> for #name #ty_generics #where_clause {
            fn get(&'a self) -> &'a #instantiate_from_ctx_types<'a> {
                unsafe { &*self.#instantiate_from_ctx.as_ptr() }
            }
        })*
    };

    Ok(expanded.into())
}
diff --git a/tinfoil-macros/src/lib.rs b/tinfoil-macros/src/lib.rs
new file mode 100644
index 0000000..e70fbc6 100644
--- /dev/null
+++ a/tinfoil-macros/src/lib.rs
@@ -1,0 +1,11 @@
mod implementation;

#[proc_macro_derive(Tinfoil)]
pub fn tinfoil(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    crate::implementation::tinfoil(input).unwrap_or_else(|e| e)
}

#[proc_macro_derive(TinfoilContext, attributes(tinfoil))]
pub fn tinfoil_context(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    crate::implementation::tinfoil_context(input).unwrap_or_else(|e| e)
}
diff --git a/tinfoil-tests/src/lib.rs b/tinfoil-tests/src/lib.rs
new file mode 100644
index 0000000..52fdb86 100644
--- /dev/null
+++ a/tinfoil-tests/src/lib.rs
@@ -1,0 +1,53 @@
#![feature(const_type_id)]
#![cfg(test)]

use std::marker::PhantomPinned;
use std::mem::MaybeUninit;
use std::pin::Pin;
use std::ptr::NonNull;
use tinfoil::{Dependency, Provider};
use tinfoil_macros::{Tinfoil, TinfoilContext};

pub struct MyCoolValue(pub String);

pub struct MyOtherCoolValue(pub u64);

impl Default for MyOtherCoolValue {
    fn default() -> MyOtherCoolValue {
        MyOtherCoolValue(32)
    }
}

#[derive(Tinfoil)]
pub struct CoolDependency<'a> {
    pub cool: &'a MyCoolValue,
}

#[derive(Tinfoil)]
pub struct OtherDependency<'a> {
    pub cool_dep: &'a CoolDependency<'a>,
}

#[derive(TinfoilContext)]
pub struct InjectionContext<'a> {
    pub other_dependency: MaybeUninit<OtherDependency<'a>>,
    #[tinfoil(parameter)]
    pub cool_value: MyCoolValue,
    #[tinfoil(default)]
    pub other_cool_value: MyOtherCoolValue,
    pub cool_dependency: MaybeUninit<CoolDependency<'a>>,
    pub _pin: PhantomPinned,
}

fn get_context<'a>() -> Pin<Box<InjectionContext<'a>>> {
    let cool_value = MyCoolValue("yo".to_string());
    InjectionContext::new(cool_value)
}

#[test]
fn it_works() {
    let context = get_context();

    let c: &CoolDependency = context.get();
    panic!("{}", &c.cool.0)
}
diff --git a/tinfoil/src/internals.rs b/tinfoil/src/internals.rs
new file mode 100644
index 0000000..54b4681 100644
--- /dev/null
+++ a/tinfoil/src/internals.rs
@@ -1,0 +1,2 @@
pub use daggy::Dag;
pub use petgraph;
diff --git a/tinfoil/src/lib.rs b/tinfoil/src/lib.rs
new file mode 100644
index 0000000..dea8e3d 100644
--- /dev/null
+++ a/tinfoil/src/lib.rs
@@ -1,0 +1,23 @@
#![feature(const_type_id)]

pub mod internals;

use std::any::TypeId;

pub trait Dependency<'a, C> {
    const DEPENDENCIES: &'static [TypeId];

    fn instn(context: &'a C) -> Self;
}

///////////////////

pub trait Provider<'a, T> {
    fn get(&'a self) -> T;
}

// impl<'a, T: Copy, P: Provider<'a, &'a T>> Provider<'a, T> for P {
//     fn get(&self) -> T {
//         <Self as Provider<&T>>::get(self)
//     }
// }