🏡 index : ~doyle/tinfoil.git

author Jordan Doyle <jordan@doyle.la> 2021-06-20 1:13:54.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-06-20 1:24:36.0 +00: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-macros/src/implementation.rs | 223 ++++++++++++++++++++++++++++++++++++-
 tinfoil-macros/src/lib.rs            |  11 ++-
 tinfoil-tests/Cargo.toml             |  11 ++-
 tinfoil-tests/src/lib.rs             |  53 +++++++++-
 tinfoil/Cargo.toml                   |  11 ++-
 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
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
/target
Cargo.lock
.idea/
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..3cc474a
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,2 @@
[workspace]
members = ["tinfoil", "tinfoil-macros", "tinfoil-tests"]
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8b1a9d8
--- /dev/null
+++ b/LICENSE
@@ -0,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
--- /dev/null
+++ b/README.md
@@ -0,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
--- /dev/null
+++ b/tinfoil-macros/Cargo.toml
@@ -0,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 = ""
\ No newline at end of file
diff --git a/tinfoil-macros/src/implementation.rs b/tinfoil-macros/src/implementation.rs
new file mode 100644
index 0000000..3128509
--- /dev/null
+++ b/tinfoil-macros/src/implementation.rs
@@ -0,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
--- /dev/null
+++ b/tinfoil-macros/src/lib.rs
@@ -0,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/Cargo.toml b/tinfoil-tests/Cargo.toml
new file mode 100644
index 0000000..4b7f64b
--- /dev/null
+++ b/tinfoil-tests/Cargo.toml
@@ -0,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" }
\ No newline at end of file
diff --git a/tinfoil-tests/src/lib.rs b/tinfoil-tests/src/lib.rs
new file mode 100644
index 0000000..52fdb86
--- /dev/null
+++ b/tinfoil-tests/src/lib.rs
@@ -0,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/Cargo.toml b/tinfoil/Cargo.toml
new file mode 100644
index 0000000..382c4af
--- /dev/null
+++ b/tinfoil/Cargo.toml
@@ -0,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"
\ No newline at end of file
diff --git a/tinfoil/src/internals.rs b/tinfoil/src/internals.rs
new file mode 100644
index 0000000..54b4681
--- /dev/null
+++ b/tinfoil/src/internals.rs
@@ -0,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
--- /dev/null
+++ b/tinfoil/src/lib.rs
@@ -0,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)
//     }
// }