use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
Attribute, Ident, LitBool, LitInt, LitStr, Token,
parse::{Parse, ParseStream},
punctuated::Punctuated,
spanned::Spanned,
};
const ALLOWED_COLOURS: [&str; 9] = [
"white",
"black",
"yellow",
"blue",
"green",
"orange",
"red",
"purple",
"gray-dark",
];
#[derive(Debug, Clone)]
pub(crate) struct ActionsAttribute {
#[allow(dead_code)]
pub(crate) span: Ident,
pub(crate) key: Option<ActionsAttributeKeys>,
pub(crate) value: Option<ActionsAttributeValue>,
pub(crate) value_span: Option<Span>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) enum ActionsAttributeKeys {
Actions,
Input,
Output,
OutputStepName,
Path,
Name,
Description,
Author,
BrandingIcon,
BrandingColor,
Default,
Expression,
GitHub,
Required,
Image,
Separator,
Entrypoint,
Composite,
Installer,
}
#[derive(Debug, Clone)]
pub(crate) enum ActionsAttributeValue {
#[allow(dead_code)]
String(String),
#[allow(dead_code)]
Int(i64),
#[allow(dead_code)]
Bool(bool),
#[allow(dead_code)]
Path(std::path::PathBuf),
}
impl Parse for ActionsAttribute {
#[allow(irrefutable_let_patterns)]
fn parse(input: ParseStream) -> syn::Result<Self> {
let name: Ident = input.parse()?;
let name_str = name.to_string();
let key: Option<ActionsAttributeKeys> = match name_str.as_str() {
"actions" => Some(ActionsAttributeKeys::Actions),
"input" => Some(ActionsAttributeKeys::Input),
"output" => Some(ActionsAttributeKeys::Output),
"output-step" | "output_step" => Some(ActionsAttributeKeys::OutputStepName),
"path" => Some(ActionsAttributeKeys::Path),
"name" | "rename" => Some(ActionsAttributeKeys::Name),
"description" => Some(ActionsAttributeKeys::Description),
"author" => Some(ActionsAttributeKeys::Author),
"branding_icon" | "icon" => Some(ActionsAttributeKeys::BrandingIcon),
"branding_color" | "color" => Some(ActionsAttributeKeys::BrandingColor),
"default" => Some(ActionsAttributeKeys::Default),
"expression" => Some(ActionsAttributeKeys::Expression),
"required" => Some(ActionsAttributeKeys::Required),
"image" => Some(ActionsAttributeKeys::Image),
"entrypoint" => Some(ActionsAttributeKeys::Entrypoint),
"composite" => Some(ActionsAttributeKeys::Composite),
"installer" => Some(ActionsAttributeKeys::Installer),
"separator" | "split" => Some(ActionsAttributeKeys::Separator),
_ => {
return Err(syn::Error::new(
name.span(),
format!("Unknown attribute: {}", name),
));
}
};
let mut value_span: Option<Span> = None;
let value = if input.peek(Token![=]) {
let _assign_token = input.parse::<Token![=]>()?;
if input.peek(LitStr) {
let lit: LitStr = input.parse()?;
if lit.value().starts_with("docker://") {
value_span = Some(lit.span());
Some(ActionsAttributeValue::String(lit.value()))
} else if lit.value().starts_with("..")
|| lit.value().starts_with("./")
|| (lit.value().starts_with("/"))
{
value_span = Some(lit.span());
Some(ActionsAttributeValue::Path(lit.value().into()))
} else {
value_span = Some(lit.span());
Some(ActionsAttributeValue::String(lit.value()))
}
} else if input.peek(LitInt) {
let lit: LitInt = input.parse()?;
value_span = Some(lit.span());
Some(ActionsAttributeValue::Int(lit.base10_parse().unwrap()))
} else if input.peek(LitBool) {
let lit: LitBool = input.parse()?;
Some(ActionsAttributeValue::Bool(lit.value))
} else {
None
}
} else {
None
};
Ok(Self {
span: name,
key,
value,
value_span,
})
}
}
impl ActionsAttribute {
pub(crate) fn parse_all(all_attrs: &[Attribute]) -> Result<(String, Vec<Self>), syn::Error> {
let mut name = String::new();
let mut parsed = Vec::new();
for attribute in all_attrs {
if attribute.path().is_ident("action") {
name = String::from("action");
for attr in attribute
.parse_args_with(Punctuated::<ActionsAttribute, Token![,]>::parse_terminated)?
{
attr.validate()?;
parsed.push(attr);
}
} else if attribute.path().is_ident("input") {
name = String::from("input");
for attr in attribute
.parse_args_with(Punctuated::<ActionsAttribute, Token![,]>::parse_terminated)?
{
attr.validate()?;
parsed.push(attr);
}
} else if attribute.path().is_ident("output") {
name = String::from("output");
for attr in attribute
.parse_args_with(Punctuated::<ActionsAttribute, Token![,]>::parse_terminated)?
{
attr.validate()?;
parsed.push(attr);
}
} else {
continue;
};
}
Ok((name, parsed))
}
#[allow(irrefutable_let_patterns)]
pub(crate) fn validate(&self) -> Result<(), syn::Error> {
match self.key {
Some(ActionsAttributeKeys::Path) => {
if let Some(ActionsAttributeValue::Path(_)) = &self.value {
Ok(())
} else if let Some(ActionsAttributeValue::String(_)) = &self.value {
Err(syn::Error::new(
self.value_span.unwrap(),
"Path attribute must start with `.` or `/` (e.g. `./action.yml`)",
))
} else {
Err(syn::Error::new(
self.span.span(),
"Path attribute must have a string value",
))
}
}
Some(ActionsAttributeKeys::Name) => {
if let Some(ActionsAttributeValue::String(_)) = &self.value {
Ok(())
} else {
Err(syn::Error::new(
self.span.span(),
"Name attribute must have a string value",
))
}
}
Some(ActionsAttributeKeys::BrandingColor) => {
if let Some(ActionsAttributeValue::String(data)) = &self.value {
if ALLOWED_COLOURS.contains(&data.as_str()) {
Ok(())
} else {
Err(syn::Error::new(
self.value_span.unwrap(),
"Invalid color value, please check the documentation for allowed values",
))
}
} else {
Err(syn::Error::new(
self.span.span(),
"Color attribute must have a string value",
))
}
}
Some(ActionsAttributeKeys::Required) => {
if let Some(ActionsAttributeValue::Bool(_)) = &self.value {
Ok(())
} else {
Err(syn::Error::new(
self.value_span.unwrap(),
"Required attribute must have a boolean value",
))
}
}
Some(ActionsAttributeKeys::Image) => {
if let Some(ActionsAttributeValue::Path(path)) = &self.value {
if path.exists() {
Ok(())
} else {
Err(syn::Error::new(
self.value_span.unwrap(),
"Image attribute must have a valid path value (file not found)",
))
}
} else if let Some(ActionsAttributeValue::String(image_ref)) = &self.value {
if image_ref.starts_with("docker://") {
Ok(())
} else {
Err(syn::Error::new(
self.value_span.unwrap(),
"Image attribute must start with 'docker://'",
))
}
} else {
Err(syn::Error::new(
self.span.span(),
"Image attribute must have a string or path value",
))
}
}
Some(ActionsAttributeKeys::Entrypoint) => {
if let Some(ActionsAttributeValue::Path(path)) = &self.value {
if path.exists() {
Ok(())
} else {
Err(syn::Error::new(
self.value_span.unwrap(),
"Entrypoint attribute must have a valid path value (file not found)",
))
}
} else if let Some(ActionsAttributeValue::String(_)) = &self.value {
Ok(())
} else {
Err(syn::Error::new(
self.span.span(),
"Entrypoint attribute must have a string value",
))
}
}
Some(ActionsAttributeKeys::Composite) => {
if let Some(ActionsAttributeValue::Bool(_)) = &self.value {
Ok(())
} else {
Err(syn::Error::new(
self.value_span.unwrap(),
"Composite attribute must have a boolean value",
))
}
}
Some(ActionsAttributeKeys::Installer) => {
if let Some(ActionsAttributeValue::Path(path)) = &self.value {
if path.exists() {
Ok(())
} else {
Err(syn::Error::new(
self.value_span.unwrap(),
"Installer attribute must have a valid path value (file not found)",
))
}
} else if let Some(ActionsAttributeValue::Int(_)) = &self.value {
Err(syn::Error::new(
self.value_span.unwrap(),
"Installer attribute must have a string value",
))
} else {
Ok(())
}
}
Some(ActionsAttributeKeys::Separator) => {
if let Some(ActionsAttributeValue::String(_)) = &self.value {
Ok(())
} else {
Err(syn::Error::new(
self.span.span(),
"Separator attribute must have a string value",
))
}
}
_ => Ok(()),
}
}
}