use proc_macro2::{Ident, Span, TokenStream}; use syn::{self, Data, LitStr}; extern crate quote; pub(crate) fn builder_fn(ast: &syn::DeriveInput) -> TokenStream { let builder_name = format!("{}{}", &ast.ident.to_string(), "Builder"); let builder_ident = Ident::new(&builder_name, Span::call_site()); let ident = &ast.ident; quote! { impl #ident{ pub fn builder() -> #builder_ident { #builder_ident::new() } } } } pub(crate) fn build_struct(ast: &syn::DeriveInput) -> TokenStream { let name = format!("{}{}", &ast.ident.to_string(), "Builder"); let build_ident = Ident::new(&name, Span::call_site()); let data = match &ast.data { Data::Struct(data) => data, _ => panic!("configuration-derive only supports structs"), }; let crd_type = ast .attrs .iter() .filter_map(|v| { v.parse_meta().ok().map(|meta| { if meta.path().is_ident("crd") { v.parse_args::().ok() } else { None } }) }) .last() .flatten() .expect("`crd` is a required attribute (Test, Resource)") .value(); // Get a list of fields and their types let fields = data.fields.iter().filter_map(|field| { let attrs = field.attrs.iter().filter(|v| { v.parse_meta() .map(|meta| meta.path().is_ident("doc") || meta.path().is_ident("serde")) .unwrap_or(false) }); let field_name = match field.ident.as_ref() { Some(ident) => ident.to_string(), None => return None, }; let field_ident = Ident::new(&field_name, Span::call_site()); let ty = field.ty.clone(); Some(quote! { #(#attrs)* #field_ident: testsys_model::ConfigValue<#ty>, }) }); // Create the setters for each field, one for typed values and one for templated strings let setters = data.fields.iter().filter_map(|field| { let doc = field.attrs.iter().filter(|v| { v.parse_meta() .map(|meta| meta.path().is_ident("doc")) .unwrap_or(false) }); let field_name = match field.ident.as_ref() { Some(ident) => ident.to_string(), None => return None, }; let field_ident = Ident::new(&field_name, Span::call_site()); let template_field_name = format!("{}{}", field_name, "_template"); let template_ident = Ident::new(&template_field_name, Span::call_site()); let ty = field.ty.clone(); Some(quote! { #(#doc)* #[inline(always)] pub fn #field_ident(&mut self, #field_ident: T) -> &mut Self where T: Into<#ty>{ self.#field_ident = testsys_model::ConfigValue::Value(#field_ident.into()); self } #[inline(always)] pub fn #template_ident(&mut self, resource: S1, field: S2) -> &mut Self where S1: Into, S2: Into, { self.#field_ident = testsys_model::ConfigValue::TemplatedString(format!("${{{}.{}}}", resource.into(), field.into())); self } }) }); let fns = quote! { #(#setters)* }; // Add the build function to the builders. let build = match crd_type.as_str() { "Test" => { quote! { #[derive(Debug, Default, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct #build_ident{ #(#fields)* #[serde(skip)] depends_on: Vec, #[serde(skip)] resources: Vec, #[serde(skip)] labels: std::collections::BTreeMap, #[serde(skip)] image: Option, #[serde(skip)] image_pull_secret: Option, #[serde(skip)] secrets: std::collections::BTreeMap, #[serde(skip)] retries: Option, #[serde(skip)] keep_running: Option, #[serde(skip)] capabilities: Vec, #[serde(skip)] privileged: Option, } impl #build_ident{ pub fn new() -> #build_ident { Default::default() } #fns pub fn depends_on(&mut self, depends_on: S1) -> &mut Self where S1: Into { self.depends_on.push(depends_on.into()); self } pub fn set_depends_on(&mut self, depends_on: Option>) -> &mut Self { self.depends_on = depends_on.unwrap_or_default(); self } pub fn resources(&mut self, resources: S1) -> &mut Self where S1: Into { self.resources.push(resources.into()); self } pub fn set_resources(&mut self, resources: Option>) -> &mut Self { self.resources = resources.unwrap_or_default(); self } pub fn labels(&mut self, key: S1, value: S2) -> &mut Self where S1: Into, S2: Into { self.labels.insert(key.into(), value.into()); self } pub fn set_labels(&mut self, labels: Option>) -> &mut Self { self.labels = labels.unwrap_or_default(); self } pub fn image(&mut self, image: S1) -> &mut Self where S1: Into { self.image = Some(image.into()); self } pub fn set_image(&mut self, image: Option) -> &mut Self { self.image = image; self } pub fn image_pull_secret(&mut self, image_pull_secret: S1) -> &mut Self where S1: Into { self.image_pull_secret = Some(image_pull_secret.into()); self } pub fn set_image_pull_secret(&mut self, image_pull_secret: Option) -> &mut Self { self.image_pull_secret = image_pull_secret; self } pub fn secrets(&mut self, key: S1, value: testsys_model::SecretName) -> &mut Self where S1: Into{ self.secrets.insert(key.into(), value.into()); self } pub fn set_secrets(&mut self, secrets: Option>) -> &mut Self { self.secrets = secrets.unwrap_or_default(); self } pub fn retries(&mut self, retries: u32) -> &mut Self { self.retries = Some(retries); self } pub fn set_retries(&mut self, retries: Option) -> &mut Self { self.retries = retries; self } pub fn keep_running(&mut self, keep_running: bool) -> &mut Self { self.keep_running = Some(keep_running); self } pub fn set_keep_running(&mut self, keep_running: Option) -> &mut Self { self.keep_running = keep_running; self } pub fn capabilities(&mut self, capabilities: S1) -> &mut Self where S1: Into { self.capabilities.push(capabilities.into()); self } pub fn set_capabilities(&mut self, capabilities: Option>) -> &mut Self { self.capabilities = capabilities.unwrap_or_default(); self } pub fn privileged(&mut self, privileged: bool) -> &mut Self { self.privileged = Some(privileged); self } pub fn set_privileged(&mut self, privileged: Option) -> &mut Self { self.privileged = privileged; self } pub fn build(&self, name: S1) -> Result> where S1: Into, { let configuration = match serde_json::to_value(self) { Ok(serde_json::Value::Object(map)) => map, Err(error) => return Err(format!("Unable to serialize config: {}", error).into()), _ => return Err("Configuration must be a map".into()), }; Ok(testsys_model::create_test_crd(name, Some(&self.labels), testsys_model::TestSpec { resources: self.resources.clone(), depends_on: Some(self.depends_on.clone()), retries: Some(self.retries.as_ref().cloned().unwrap_or(5)), agent: testsys_model::Agent { name: "agent".to_string(), image: self.image.as_ref().cloned().ok_or_else(|| "Image is required to build a test".to_string())?, pull_secret: self.image_pull_secret.as_ref().cloned(), keep_running: self.keep_running.as_ref().cloned().unwrap_or(true), configuration: Some(configuration), secrets: Some(self.secrets.clone()), capabilities: Some(self.capabilities.clone()), privileged: self.privileged, timeout: None }, }, )) } } } } "Resource" => { quote! { #[derive(Debug, Default, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct #build_ident{ #(#fields)* #[serde(skip)] depends_on: Vec, #[serde(skip)] conflicts_with: Vec, #[serde(skip)] labels: std::collections::BTreeMap, #[serde(skip)] image: Option, #[serde(skip)] image_pull_secret: Option, #[serde(skip)] secrets: std::collections::BTreeMap, #[serde(skip)] keep_running: Option, #[serde(skip)] capabilities: Vec, #[serde(skip)] destruction_policy: Option, #[serde(skip)] privileged: Option, } impl #build_ident{ pub fn new() -> #build_ident { Default::default() } #fns pub fn depends_on(&mut self, depends_on: S1) -> &mut Self where S1: Into { self.depends_on.push(depends_on.into()); self } pub fn set_depends_on(&mut self, depends_on: Option>) -> &mut Self { self.depends_on = depends_on.unwrap_or_default(); self } pub fn conflicts_with(&mut self, conflicts_with: S1) -> &mut Self where S1: Into { self.conflicts_with.push(conflicts_with.into()); self } pub fn set_conflicts_with(&mut self, conflicts_with: Option>) -> &mut Self { self.conflicts_with = conflicts_with.unwrap_or_default(); self } pub fn labels(&mut self, key: S1, value: S2) -> &mut Self where S1: Into, S2: Into { self.labels.insert(key.into(), value.into()); self } pub fn set_labels(&mut self, labels: Option>) -> &mut Self { self.labels = labels.unwrap_or_default(); self } pub fn image(&mut self, image: S1) -> &mut Self where S1: Into { self.image = Some(image.into()); self } pub fn set_image(&mut self, image: Option) -> &mut Self { self.image = image; self } pub fn image_pull_secret(&mut self, image_pull_secret: S1) -> &mut Self where S1: Into { self.image_pull_secret = Some(image_pull_secret.into()); self } pub fn set_image_pull_secret(&mut self, image_pull_secret: Option) -> &mut Self { self.image_pull_secret = image_pull_secret; self } pub fn secrets(&mut self, key: S1, value: testsys_model::SecretName) -> &mut Self where S1: Into{ self.secrets.insert(key.into(), value.into()); self } pub fn set_secrets(&mut self, secrets: Option>) -> &mut Self { self.secrets = secrets.unwrap_or_default(); self } pub fn keep_running(&mut self, keep_running: bool) -> &mut Self { self.keep_running = Some(keep_running); self } pub fn set_keep_running(&mut self, keep_running: Option) -> &mut Self { self.keep_running = keep_running; self } pub fn capabilities(&mut self, capabilities: S1) -> &mut Self where S1: Into { self.capabilities.push(capabilities.into()); self } pub fn set_capabilities(&mut self, capabilities: Option>) -> &mut Self { self.capabilities = capabilities.unwrap_or_default(); self } pub fn destruction_policy(&mut self, destruction_policy: testsys_model::DestructionPolicy) -> &mut Self { self.destruction_policy = Some(destruction_policy); self } pub fn set_destruction_policy(&mut self, destruction_policy: Option) -> &mut Self { self.destruction_policy = destruction_policy; self } pub fn privileged(&mut self, privileged: bool) -> &mut Self { self.privileged = Some(privileged); self } pub fn set_privileged(&mut self, privileged: Option) -> &mut Self { self.privileged = privileged; self } pub fn build(&self, name: S1) -> Result> where S1: Into, { let configuration = match serde_json::to_value(self) { Ok(serde_json::Value::Object(map)) => map, Err(error) => return Err(format!("Unable to serialize config: {}", error).into()), _ => return Err("Configuration must be a map".to_string().into()), }; Ok(testsys_model::create_resource_crd(name, Some(&self.labels), testsys_model::ResourceSpec { conflicts_with: Some(self.conflicts_with.clone()), depends_on: Some(self.depends_on.clone()), agent: testsys_model::Agent { name: "agent".to_string(), image: self.image.as_ref().cloned().ok_or_else(|| "Image is required to build a test".to_string())?, pull_secret: self.image_pull_secret.as_ref().cloned(), keep_running: self.keep_running.as_ref().cloned().unwrap_or(true), configuration: Some(configuration), secrets: Some(self.secrets.clone()), capabilities: Some(self.capabilities.clone()), timeout: None, privileged: self.privileged, }, destruction_policy: self.destruction_policy.as_ref().cloned().unwrap_or_default() }, )) } } } } _ => panic!( "Unexpected crd type '{}'. Crd type must be `Test` or `Resource`", crd_type ), }; quote! { #build } }