From 783fdef6f23114c451b8da90a11b4c2e81884eb1 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sun, 20 Jun 2021 02:13:54 +0100 Subject: [PATCH] Initial commit --- .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(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 tinfoil-macros/Cargo.toml create mode 100644 tinfoil-macros/src/implementation.rs create mode 100644 tinfoil-macros/src/lib.rs create mode 100644 tinfoil-tests/Cargo.toml create mode 100644 tinfoil-tests/src/lib.rs create mode 100644 tinfoil/Cargo.toml create mode 100644 tinfoil/src/internals.rs create mode 100644 tinfoil/src/lib.rs 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 + +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 "] +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 = std::result::Result; + +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 { + 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 { + 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> { + let mut context = Box::pin(InjectionContext { + #(#parameters,)* + #(#defaults: Default::default(),)* + #(#instantiate_from_ctx: MaybeUninit::uninit(),)* + _pin: PhantomPinned, + }); + + let mut dag: tinfoil::internals::Dag = 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 "] +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>, + #[tinfoil(parameter)] + pub cool_value: MyCoolValue, + #[tinfoil(default)] + pub other_cool_value: MyOtherCoolValue, + pub cool_dependency: MaybeUninit>, + pub _pin: PhantomPinned, +} + +fn get_context<'a>() -> Pin>> { + 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 "] +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 { +// >::get(self) +// } +// } -- libgit2 1.7.2