use helpers::generate_helpers;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{Data, DataStruct, DeriveInput, Fields, spanned::Spanned};
mod helpers;
use crate::attributes::{ActionsAttribute, ActionsAttributeKeys, ActionsAttributeValue};
use ghactions_core::{
ActionInput, ActionYML,
actions::models::{ActionBranding, ActionMode, ActionOutput, ActionRunUsing},
};
pub(crate) fn derive_parser(ast: &DeriveInput) -> Result<TokenStream, syn::Error> {
let name = &ast.ident;
let (_, attributes) = ActionsAttribute::parse_all(&ast.attrs)?;
let mut action = load_actionyaml(&attributes)?;
match ast.data {
Data::Struct(DataStruct {
fields: Fields::Named(ref fields),
..
}) => {
for field in fields.named.iter() {
let field_name = field.ident.as_ref().unwrap();
let field_type = &field.ty;
let (name, field_attributes) = ActionsAttribute::parse_all(&field.attrs)?;
match name.as_str() {
"input" => {
let mut input = ActionInput {
field_name: field_name.to_string(),
r#type: field_type.to_token_stream().to_string(),
..Default::default()
};
field_attributes.iter().for_each(|attr| match attr {
ActionsAttribute {
key: Some(ActionsAttributeKeys::Name),
value: Some(ActionsAttributeValue::String(name)),
..
} => {
input.action_name = name.to_string();
}
ActionsAttribute {
key: Some(ActionsAttributeKeys::Description),
value: Some(ActionsAttributeValue::String(description)),
..
} => {
input.description = Some(description.clone());
}
ActionsAttribute {
key: Some(ActionsAttributeKeys::Required),
value: Some(ActionsAttributeValue::Bool(required)),
..
} => {
input.required = Some(*required);
}
ActionsAttribute {
key: Some(ActionsAttributeKeys::Default),
value: Some(ActionsAttributeValue::String(default)),
..
} => {
input.default = Some(default.clone());
}
ActionsAttribute {
key: Some(ActionsAttributeKeys::Separator),
value: Some(ActionsAttributeValue::String(separator)),
..
} => {
input.separator = Some(separator.clone());
}
_ => {}
});
if input.action_name.is_empty() {
input.action_name = field_name.to_string();
}
action.inputs.insert(input.action_name.to_string(), input);
}
"output" => {
let mut output = ActionOutput::default();
if let Some(ref step_id) = action.output_value_step_id {
output.value = Some(format!(
"${{{{ steps.{}.outputs.{} }}}}",
step_id, field_name
));
}
if let Some(ActionsAttribute {
value: Some(ActionsAttributeValue::String(description)),
..
}) = field_attributes
.iter()
.find(|attr| attr.key == Some(ActionsAttributeKeys::Description))
{
output.description = Some(description.clone());
}
action.outputs.insert(field_name.to_string(), output);
}
_ => {}
}
}
let mut tokens = generate_traits(name, fields, &ast.generics, &action)?;
tokens.extend(generate_helpers(name, fields, &ast.generics, &action)?);
#[cfg(feature = "generate")]
{
if action.path.is_some() {
action
.write()
.map_err(|e| syn::Error::new(ast.span(), e.to_string()))?;
}
}
Ok(tokens)
}
_ => Ok(
syn::Error::new(ast.span(), "Only structs with named fields are supported")
.to_compile_error(),
),
}
}
pub(crate) fn generate_traits(
ident: &syn::Ident,
_fields: &syn::FieldsNamed,
generics: &syn::Generics,
action: &ActionYML,
) -> Result<TokenStream, syn::Error> {
let mut stream = TokenStream::new();
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let mut selfstream = TokenStream::new();
for (action_name, input) in action.inputs.iter() {
let input_name = format!("INPUT_{}", input.action_name.to_uppercase());
let ident_input = syn::Ident::new(&input.field_name.clone(), ident.span());
let required = if input.required.unwrap_or(false) {
quote! { ? }
} else {
quote! { .unwrap_or_default() }
};
match input.r#type.as_str() {
"String" | "&str" => {
selfstream.extend(quote! {
#ident_input: Self::get_input(#input_name)
#required,
});
}
"bool" => {
selfstream.extend(quote! {
#ident_input: Self::get_input_bool(#input_name)
#required,
});
}
"i32" | "i64" | "u32" | "u64" => {
selfstream.extend(quote! {
#ident_input: Self::get_input_int(#input_name)
#required,
});
}
"Vec < String >" => {
let separator = input.separator.clone().unwrap_or_else(|| ",".to_string());
selfstream.extend(quote! {
#ident_input: Self::get_input_vec(#input_name, #separator)
#required,
});
}
_ => {
return Err(syn::Error::new(
ident.span(),
format!(
"Unsupported type for input {} ({})",
action_name, input.r#type
),
));
}
}
}
for (name, _output) in action.outputs.iter() {
let ident_output = syn::Ident::new(name, ident.span());
selfstream.extend(quote! {
#ident_output: String::new(),
});
}
let action_name = action.name.clone().unwrap_or_default();
let action_description = action.description.clone().unwrap_or_default();
let dotenv = match cfg!(feature = "dotenvy") {
true => quote! {
::dotenvy::dotenv().ok();
},
false => quote! {},
};
let log = match cfg!(feature = "log") {
true => quote! {
::ghactions::init_logger().init();
},
false => quote! {},
};
stream.extend(quote! {
#[automatically_derived]
impl #impl_generics ::ghactions::ActionTrait for #ident #ty_generics #where_clause {
fn init() -> Result<Self, ::ghactions::ActionsError> {
#dotenv
#log
Ok(Self {
#selfstream
})
}
fn name(&self) -> &str {
#action_name
}
fn description(&self) -> &str {
#action_description
}
}
});
Ok(stream)
}
fn load_actionyaml(attributes: &[ActionsAttribute]) -> Result<ActionYML, syn::Error> {
let mut action = ActionYML::default();
for attr in attributes.iter() {
match attr.key {
Some(ActionsAttributeKeys::Path) => {
if let Some(ActionsAttributeValue::Path(ref value)) = attr.value {
action.path = Some(value.clone());
}
}
Some(ActionsAttributeKeys::Name) => {
if let Some(ActionsAttributeValue::String(ref value)) = attr.value {
action.name = Some(value.clone());
}
}
Some(ActionsAttributeKeys::Description) => {
if let Some(ActionsAttributeValue::String(ref value)) = attr.value {
action.description = Some(value.clone());
}
}
Some(ActionsAttributeKeys::Author) => {
if let Some(ActionsAttributeValue::String(ref value)) = attr.value {
action.author = Some(value.clone());
}
}
Some(ActionsAttributeKeys::BrandingIcon) => {
if let Some(ActionsAttributeValue::String(ref value)) = attr.value {
if let Some(ref mut branding) = action.branding {
branding.icon = Some(value.clone());
} else {
action.branding = Some(ActionBranding {
icon: Some(value.clone()),
color: None,
});
}
}
}
Some(ActionsAttributeKeys::BrandingColor) => {
if let Some(ActionsAttributeValue::String(ref value)) = attr.value {
if let Some(ref mut branding) = action.branding {
branding.color = Some(value.clone());
} else {
action.branding = Some(ActionBranding {
icon: None,
color: Some(value.clone()),
});
}
}
}
Some(ActionsAttributeKeys::Image) => {
action.mode = ActionMode::Container;
if let Some(ActionsAttributeValue::Path(ref value)) = attr.value {
action.set_container_image(value.display().to_string());
} else if let Some(ActionsAttributeValue::String(ref value)) = attr.value {
if value.is_empty() {
return Err(syn::Error::new(
attr.value_span.unwrap(),
"Image attribute cannot be empty",
));
}
action.set_container_image(value.clone());
}
}
Some(ActionsAttributeKeys::Entrypoint) => {
action.mode = ActionMode::Entrypoint;
if action.runs.steps.is_none() {
action.runs.steps = Some(vec![]);
}
if let Some(ActionsAttributeValue::String(ref value)) = attr.value {
if value.is_empty() {
return Err(syn::Error::new(
attr.value_span.unwrap(),
"Entrypoint cannot be empty",
));
}
if action.runs.using == ActionRunUsing::Docker {
action.runs.args = Some(vec![value.clone()]);
}
} else if let Some(ActionsAttributeValue::Path(ref value)) = attr.value {
action.runs.using = ActionRunUsing::Composite;
let shell = if value.extension().unwrap_or_default() == "ps1" {
"pwsh".to_string()
} else {
"bash".to_string()
};
let entrypoint = value.display().to_string().replace("./", "");
let run: String = format!("${{{{ github.action_path }}}}/{}", entrypoint);
action.runs.steps =
Some(vec![ghactions_core::actions::models::ActionRunStep {
id: Some("entrypoint-script".to_string()),
shell: Some(shell),
run: Some(run),
..Default::default()
}]);
action.output_value_step_id = Some("entrypoint-script".to_string());
}
}
Some(ActionsAttributeKeys::Installer) => {
action.mode = ActionMode::Installer;
if let Some(ActionsAttributeValue::Path(ref value)) = attr.value {
let installer_script = std::fs::read_to_string(value)
.map_err(|e| syn::Error::new(attr.value_span.unwrap(), e.to_string()))?;
action.add_script(installer_script.as_str(), Some("install-script"));
} else if let Some(ActionsAttributeValue::String(ref value)) = attr.value {
action.add_cargo_install_step(value);
} else {
action.add_installer_step();
action.add_script_run();
}
}
_ => {}
}
}
Ok(action)
}