From 7d28815065a6d78b121b73d80f10b3ede2a2151c Mon Sep 17 00:00:00 2001 From: Nilstrieb <48135649+Nilstrieb@users.noreply.github.com> Date: Mon, 15 Apr 2024 19:48:17 +0200 Subject: [PATCH] Derive macro getting works --- Cargo.lock | 2 +- terraform-provider-example/Cargo.toml | 1 - terraform-provider-example/src/main.rs | 33 ++++---- terustform-macros/src/lib.rs | 106 ++++++++++++++++++++++++- terustform/Cargo.toml | 2 + terustform/src/framework/mod.rs | 88 +++++++++++++++++++- terustform/src/framework/value.rs | 1 - terustform/src/lib.rs | 29 ++++--- terustform/src/values.rs | 24 ++++-- 9 files changed, 245 insertions(+), 41 deletions(-) delete mode 100644 terustform/src/framework/value.rs diff --git a/Cargo.lock b/Cargo.lock index 18fdc6d..50ba838 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1083,7 +1083,6 @@ version = "0.1.0" dependencies = [ "eyre", "terustform", - "terustform-macros", "tokio", ] @@ -1100,6 +1099,7 @@ dependencies = [ "serde", "serde_json", "tempfile", + "terustform-macros", "time", "tokio", "tokio-stream", diff --git a/terraform-provider-example/Cargo.toml b/terraform-provider-example/Cargo.toml index 060acba..6886fb8 100644 --- a/terraform-provider-example/Cargo.toml +++ b/terraform-provider-example/Cargo.toml @@ -6,6 +6,5 @@ edition = "2021" [dependencies] eyre = "0.6.12" terustform = { path = "../terustform" } -terustform-macros = { path = "../terustform-macros" } tokio = { version = "1.37.0", features = ["full"] } diff --git a/terraform-provider-example/src/main.rs b/terraform-provider-example/src/main.rs index ce6809b..6cc8028 100644 --- a/terraform-provider-example/src/main.rs +++ b/terraform-provider-example/src/main.rs @@ -4,8 +4,7 @@ use terustform::{ framework::{ datasource::{self, DataSource}, provider::Provider, - value::StringValue, - DResult, + AttrPath, DResult, Diagnostics, StringValue, ValueModel, }, values::{Value, ValueKind}, }; @@ -29,11 +28,6 @@ impl Provider for ExampleProvider { struct ExampleDataSource {} -#[derive(terustform_macros::DataSourceModel)] -struct _ExampleDataSourceModel { - name: StringValue, -} - impl DataSource for ExampleDataSource { fn name(&self, provider_name: &str) -> String { format!("{provider_name}_kitty") @@ -72,20 +66,20 @@ impl DataSource for ExampleDataSource { } fn read(&self, config: Value) -> DResult { - let name = match config { - Value::Known(ValueKind::Object(mut obj)) => obj.remove("name").unwrap(), - _ => unreachable!(), - }; - let name_str = match &name { - Value::Known(ValueKind::String(s)) => s.clone(), - _ => unreachable!(), + let model = ExampleDataSourceModel::from_value(config, &AttrPath::root())?; + + let StringValue::Known(name_str) = &model.name else { + return Err(Diagnostics::error_string( + "model name must be known".to_owned(), + )); }; + let meow = format!("mrrrrr i am {name_str}"); Ok(Value::Known(ValueKind::Object(BTreeMap::from([ - ("name".to_owned(), name), + ("name".to_owned(), model.name.to_value()), ( "meow".to_owned(), - Value::Known(ValueKind::String(format!("mrrrrr i am {name_str}"))), + Value::Known(ValueKind::String(meow)), ), ( "id".to_owned(), @@ -94,3 +88,10 @@ impl DataSource for ExampleDataSource { ])))) } } + +#[derive(terustform::DataSourceModel)] +struct ExampleDataSourceModel { + name: StringValue, + meow: StringValue, + id: StringValue, +} diff --git a/terustform-macros/src/lib.rs b/terustform-macros/src/lib.rs index 023dd78..ab9f3b8 100644 --- a/terustform-macros/src/lib.rs +++ b/terustform-macros/src/lib.rs @@ -1,6 +1,104 @@ +use quote::quote; +use syn::spanned::Spanned; + +// This macro should only reference items in `terustform::__derive_private`. + #[proc_macro_derive(DataSourceModel)] -pub fn data_source_model( - _input: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - Default::default() +pub fn data_source_model(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = syn::parse_macro_input!(input as syn::DeriveInput); + match data_source_model_inner(input) { + Ok(ts) => ts.into(), + Err(err) => err.into_compile_error().into(), + } +} + +fn data_source_model_inner( + input: syn::DeriveInput, +) -> Result { + let struct_name = input.ident; + + let syn::Data::Struct(data) = input.data else { + return Err(syn::Error::new( + struct_name.span(), + "models must be structs", + )); + }; + let syn::Fields::Named(fields) = data.fields else { + return Err(syn::Error::new( + struct_name.span(), + "models must have named fields", + )); + }; + + let terustform = quote!(::terustform::__derive_private); + + let fields = fields + .named + .into_iter() + .map(|field| { + let Some(name) = field.ident else { + return Err(syn::Error::new(field.span(), "field must be named")); + }; + + Ok((name, field.ty)) + }) + .collect::, _>>()?; + + + let field_extractions = fields.iter().map(|(name, ty)| { + let name_str = proc_macro2::Literal::string(&name.to_string()); + quote! { + let #terustform::Some(#name) = obj.remove(#name_str) else { + return #terustform::Err( + #terustform::Diagnostics::error_string( + format!("Expected property '{}', which was not present", #name_str), + ).with_path(path.clone()) + ); + }; + let #name = <#ty as #terustform::ValueModel>::from_value( + #name, + &path.append_attribute_name(#terustform::ToOwned::to_owned(#name_str)) + )?; + } + }); + let constructor_fields = fields.iter().map(|(name, _)| quote! { #name, }); + + let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl(); + + Ok(quote! { + #[automatically_derived] + impl #impl_generics #terustform::ValueModel + for #struct_name #type_generics #where_clause + { + fn from_value(v: #terustform::Value, path: &#terustform::AttrPath) -> #terustform::DResult { + match v { + #terustform::BaseValue::Unknown => { + return #terustform::Err(#terustform::Diagnostics::with_path( + #terustform::Diagnostics::error_string(#terustform::ToOwned::to_owned("Expected object, found unknown value")), + #terustform::Clone::clone(&path), + )); + }, + #terustform::BaseValue::Null => { + return #terustform::Err(#terustform::Diagnostics::with_path( + #terustform::Diagnostics::error_string(#terustform::ToOwned::to_owned("Expected object, found null value")), + #terustform::Clone::clone(&path), + )); + }, + #terustform::BaseValue::Known(#terustform::ValueKind::Object(mut obj)) => { + #(#field_extractions)* + + Ok(#struct_name { + #(#constructor_fields)* + }) + }, + #terustform::BaseValue::Known(v) => { + return #terustform::Err(#terustform::Diagnostics::with_path( + #terustform::Diagnostics::error_string(format!("Expected object, found {} value", v.diagnostic_type_str())), + #terustform::Clone::clone(&path), + )); + }, + } + } + } + }) } diff --git a/terustform/Cargo.toml b/terustform/Cargo.toml index 4332dd1..f6c557e 100644 --- a/terustform/Cargo.toml +++ b/terustform/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +terustform-macros = { path = "../terustform-macros" } + base64 = "0.22.0" eyre = "0.6.12" prost = "0.12.4" diff --git a/terustform/src/framework/mod.rs b/terustform/src/framework/mod.rs index 8f35126..b4e833b 100644 --- a/terustform/src/framework/mod.rs +++ b/terustform/src/framework/mod.rs @@ -2,12 +2,16 @@ pub mod datasource; pub mod provider; -pub mod value; + +use crate::values::{Value, ValueKind}; use self::datasource::DataSource; +#[derive(Debug, Default)] pub struct Diagnostics { pub(crate) errors: Vec, + pub(crate) attr: Option, + // note: lol this cannot contain warnings that would be fucked oops } pub type DResult = Result; @@ -16,8 +20,18 @@ impl Diagnostics { pub fn error_string(msg: String) -> Self { Self { errors: vec![msg], + attr: None, } } + + pub fn with_path(mut self, path: AttrPath) -> Self { + self.attr = Some(path); + self + } + + pub fn has_errors(&self) -> bool { + !self.errors.is_empty() + } } impl From for Diagnostics { @@ -25,3 +39,75 @@ impl From for Diagnostics { Self::error_string(format!("{:?}", value)) } } + +// TODO: this could probably be a clever 0-alloc &-based linked list! + +#[derive(Debug, Clone, Default)] +pub struct AttrPath(Vec); + +#[derive(Debug, Clone)] +pub enum AttrPathSegment { + AttributeName(String), + ElementKeyString(String), + ElementKeyInt(i64), +} + +impl AttrPath { + pub fn root() -> Self { + Self::default() + } + pub fn append_attribute_name(&self, name: String) -> Self { + let mut p = self.clone(); + p.0.push(AttrPathSegment::AttributeName(name)); + p + } +} + +pub type StringValue = BaseValue; +pub type I64Value = BaseValue; + +#[derive(Debug)] +pub enum BaseValue { + Unknown, + Null, + Known(T), +} + +impl BaseValue { + fn map(self, f: impl FnOnce(T) -> U) -> BaseValue { + self.try_map(|v| Ok(f(v))).unwrap() + } + + fn try_map(self, f: impl FnOnce(T) -> DResult) -> DResult> { + Ok(match self { + Self::Unknown => BaseValue::Unknown, + Self::Null => BaseValue::Null, + Self::Known(v) => BaseValue::Known(f(v)?), + }) + } +} + +pub trait ValueModel: Sized { + fn from_value(v: Value, path: &AttrPath) -> DResult; + + fn to_value(self) -> Value { + todo!() + } +} + +impl ValueModel for StringValue { + fn from_value(v: Value, path: &AttrPath) -> DResult { + v.try_map(|v| match v { + ValueKind::String(s) => Ok(s), + _ => Err(Diagnostics::error_string(format!( + "expected string, found {}", + v.diagnostic_type_str() + )) + .with_path(path.clone())), + }) + } + + fn to_value(self) -> Value { + self.map(ValueKind::String) + } +} diff --git a/terustform/src/framework/value.rs b/terustform/src/framework/value.rs deleted file mode 100644 index 096c6e9..0000000 --- a/terustform/src/framework/value.rs +++ /dev/null @@ -1 +0,0 @@ -pub struct StringValue; \ No newline at end of file diff --git a/terustform/src/lib.rs b/terustform/src/lib.rs index 4ee5ab4..f78e426 100644 --- a/terustform/src/lib.rs +++ b/terustform/src/lib.rs @@ -3,6 +3,8 @@ pub mod framework; mod server; pub mod values; +pub use terustform_macros::DataSourceModel; + use std::{env, path::PathBuf}; use base64::Engine; @@ -48,16 +50,13 @@ pub async fn serve(provider: &dyn Provider) -> eyre::Result<()> { let server = tonic::transport::Server::builder() .tls_config(tls) .wrap_err("invalid TLS config")? - .add_service(server::ProviderServer::new( - server::ProviderHandler::new(shutdown.clone(), provider), - )) - .add_service( - server::GrpcControllerServer::new( - server::Controller { - shutdown: shutdown.clone(), - }, - ), - ) + .add_service(server::ProviderServer::new(server::ProviderHandler::new( + shutdown.clone(), + provider, + ))) + .add_service(server::GrpcControllerServer::new(server::Controller { + shutdown: shutdown.clone(), + })) .serve_with_incoming(uds_stream); tokio::select! { @@ -101,3 +100,13 @@ async fn init_handshake(server_cert: &rcgen::Certificate) -> Result<(tempfile::T Ok((tmpdir, socket)) } + +/// Private, only for use for with the derive macro. +#[doc(hidden)] +pub mod __derive_private { + pub use crate::framework::{ + AttrPath, AttrPathSegment, BaseValue, DResult, Diagnostics, ValueModel, + }; + pub use crate::values::{Value, ValueKind}; + pub use {Clone, Result::Err, Option::Some, ToOwned}; +} diff --git a/terustform/src/values.rs b/terustform/src/values.rs index 1b69f92..69735cd 100644 --- a/terustform/src/values.rs +++ b/terustform/src/values.rs @@ -6,7 +6,7 @@ use std::{ io::{self, Read}, }; -use crate::framework::{DResult, Diagnostics}; +use crate::framework::{BaseValue, DResult, Diagnostics}; #[derive(Debug)] pub enum Type { @@ -79,12 +79,7 @@ impl Type { } } -#[derive(Debug)] -pub enum Value { - Known(ValueKind), - Unknown, - Null, -} +pub type Value = BaseValue; #[derive(Debug)] pub enum ValueKind { @@ -98,6 +93,21 @@ pub enum ValueKind { Object(BTreeMap), } +impl ValueKind { + pub fn diagnostic_type_str(&self) -> &'static str { + match self { + ValueKind::String(_) => "string", + ValueKind::Number(_) => "number", + ValueKind::Bool(_) => "bool", + ValueKind::List(_) => "list", + ValueKind::Set(_) => "set", + ValueKind::Map(_) => "map", + ValueKind::Tuple(_) => "tuple", + ValueKind::Object(_) => "object", + } + } +} + // marshal msg pack // tftypes/value.go:MarshalMsgPack