Skip to main content

pipedash_plugin_actions/
lib.rs

1mod log_dialect;
2
3pub use log_dialect::ActionsLogDialect;
4use pipedash_plugin_api::{
5    PluginError,
6    PluginResult,
7    WorkflowParameter,
8    WorkflowParameterType,
9};
10use serde::de::{
11    Deserializer,
12    IgnoredAny,
13    MapAccess,
14    Visitor,
15};
16use serde::Deserialize;
17use serde_json::Value;
18
19#[derive(Deserialize)]
20struct WorkflowFile {
21    #[serde(default)]
22    on: Option<Triggers>,
23}
24
25#[derive(Deserialize)]
26#[serde(untagged)]
27enum Triggers {
28    Map {
29        #[serde(default)]
30        workflow_dispatch: Option<Dispatch>,
31    },
32    Other(IgnoredAny),
33}
34
35#[derive(Deserialize)]
36struct Dispatch {
37    #[serde(default)]
38    inputs: Option<Inputs>,
39}
40
41struct Inputs(Vec<(String, Value)>);
42
43impl<'de> Deserialize<'de> for Inputs {
44    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
45        struct OrderedMap;
46
47        impl<'de> Visitor<'de> for OrderedMap {
48            type Value = Inputs;
49
50            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
51                formatter.write_str("a map of workflow inputs")
52            }
53
54            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Inputs, A::Error> {
55                let mut entries = Vec::new();
56                while let Some(entry) = map.next_entry()? {
57                    entries.push(entry);
58                }
59                Ok(Inputs(entries))
60            }
61        }
62
63        deserializer.deserialize_map(OrderedMap)
64    }
65}
66
67const INPUT_PREFIX: &str = "inputs.";
68
69pub fn dispatch_inputs(workflow_source: &str) -> PluginResult<Vec<WorkflowParameter>> {
70    let file: WorkflowFile = serde_saphyr::from_str(workflow_source)
71        .map_err(|e| PluginError::ApiError(format!("Failed to parse workflow file: {e}")))?;
72    let inputs = match file.on {
73        Some(Triggers::Map {
74            workflow_dispatch: Some(Dispatch {
75                inputs: Some(inputs),
76            }),
77        }) => inputs.0,
78        _ => Vec::new(),
79    };
80
81    Ok(inputs
82        .into_iter()
83        .map(|(name, input)| WorkflowParameter {
84            name: format!("{INPUT_PREFIX}{name}"),
85            label: Some(name),
86            description: input
87                .get("description")
88                .and_then(Value::as_str)
89                .map(str::to_string),
90            param_type: input_type(&input),
91            required: input
92                .get("required")
93                .and_then(Value::as_bool)
94                .unwrap_or(false),
95        })
96        .collect())
97}
98
99pub fn dispatch_body(inputs: Option<Value>) -> Value {
100    let mut values = match inputs {
101        Some(Value::Object(values)) => values,
102        _ => serde_json::Map::new(),
103    };
104    let ref_value = match values.remove("ref") {
105        Some(Value::String(value)) => value,
106        _ => "main".to_string(),
107    };
108    let inputs: serde_json::Map<String, Value> = values
109        .into_iter()
110        .map(|(mut key, value)| {
111            if key.starts_with(INPUT_PREFIX) {
112                key.replace_range(..INPUT_PREFIX.len(), "");
113            }
114            (key, value)
115        })
116        .collect();
117
118    let mut body = serde_json::json!({ "ref": ref_value });
119    if !inputs.is_empty() {
120        body["inputs"] = Value::Object(inputs);
121    }
122    body
123}
124
125fn input_type(input: &Value) -> WorkflowParameterType {
126    let default = input.get("default");
127    let text = |value: &Value| match value {
128        Value::String(s) => s.clone(),
129        other => other.to_string(),
130    };
131    match input
132        .get("type")
133        .and_then(Value::as_str)
134        .unwrap_or("string")
135    {
136        "boolean" => WorkflowParameterType::Boolean {
137            default: default
138                .is_some_and(|d| d.as_bool() == Some(true) || d.as_str() == Some("true")),
139        },
140        "number" => WorkflowParameterType::Number {
141            default: default.and_then(|d| d.as_f64().or_else(|| d.as_str()?.parse().ok())),
142        },
143        "choice" => WorkflowParameterType::Choice {
144            options: input
145                .get("options")
146                .and_then(Value::as_array)
147                .map(|options| options.iter().map(text).collect())
148                .unwrap_or_default(),
149            default: default.map(text),
150        },
151        _ => WorkflowParameterType::String {
152            default: default.map(text),
153        },
154    }
155}
156
157#[cfg(test)]
158mod tests;