Skip to main content

alien_core/
stack_input.rs

1use serde::{Deserialize, Serialize};
2
3use crate::Platform;
4
5/// Who can provide a stack input value.
6#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
7#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
8#[serde(rename_all = "camelCase")]
9pub enum StackInputProvider {
10    /// Value is provided by the developer before a deployment link is created.
11    Developer,
12    /// Value is provided by the deployer during setup.
13    Deployer,
14}
15
16/// Primitive stack input kind.
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
19#[serde(rename_all = "camelCase")]
20pub enum StackInputKind {
21    /// Plain string input.
22    String,
23    /// Secret string input.
24    Secret,
25    /// Floating point number input.
26    Number,
27    /// Integer input.
28    Integer,
29    /// Boolean input.
30    Boolean,
31    /// String enum input.
32    Enum,
33    /// List of strings.
34    StringList,
35}
36
37/// How a resolved stack input is injected into runtime environment variables.
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
40#[serde(rename_all = "camelCase")]
41pub struct StackInputEnvironmentMapping {
42    /// Environment variable name.
43    pub name: String,
44    /// Target resource IDs or patterns. None means every env-capable resource.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub target_resources: Option<Vec<String>>,
47    /// Whether this env var is plain or secret. Defaults from the input kind.
48    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
49    pub var_type: Option<StackInputEnvironmentVariableType>,
50}
51
52/// Environment variable handling for a stack input mapping.
53#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
54#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
55#[serde(rename_all = "lowercase")]
56pub enum StackInputEnvironmentVariableType {
57    /// Plain value injected directly.
58    Plain,
59    /// Secret value routed through secret handling.
60    Secret,
61}
62
63/// Portable stack input validation constraints.
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
66#[serde(rename_all = "camelCase")]
67pub struct StackInputValidation {
68    /// Minimum string length.
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub min_length: Option<u32>,
71    /// Maximum string length.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub max_length: Option<u32>,
74    /// Portable whole-value regex pattern.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub pattern: Option<String>,
77    /// Semantic format hint such as url.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub format: Option<String>,
80    /// Minimum number.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub min: Option<String>,
83    /// Maximum number.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub max: Option<String>,
86    /// Allowed string enum values.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub values: Option<Vec<String>>,
89    /// Minimum string-list items.
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub min_items: Option<u32>,
92    /// Maximum string-list items.
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub max_items: Option<u32>,
95}
96
97/// Stack input default value.
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
99#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
100#[serde(rename_all = "camelCase", tag = "type", content = "value")]
101pub enum StackInputDefaultValue {
102    /// String default.
103    String(String),
104    /// Number default.
105    Number(String),
106    /// Boolean default.
107    Boolean(bool),
108    /// String list default.
109    StringList(Vec<String>),
110}
111
112/// Stack input definition serialized into a release stack.
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
115#[serde(rename_all = "camelCase")]
116pub struct StackInputDefinition {
117    /// Stable input ID used by CLI/API calls.
118    pub id: String,
119    /// Input primitive kind.
120    pub kind: StackInputKind,
121    /// Who can provide this value.
122    pub provided_by: Vec<StackInputProvider>,
123    /// Whether a resolved value is required before deployment can proceed.
124    pub required: bool,
125    /// Human-facing field label.
126    pub label: String,
127    /// Human-facing helper text.
128    pub description: String,
129    /// Example placeholder shown in UI.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub placeholder: Option<String>,
132    /// Default value for optional/plain inputs.
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub default: Option<StackInputDefaultValue>,
135    /// Platforms where this input applies.
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub platforms: Option<Vec<Platform>>,
138    /// Portable validation constraints.
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub validation: Option<StackInputValidation>,
141    /// Runtime env-var mappings for v1 input resolution.
142    #[serde(default, skip_serializing_if = "Vec::is_empty")]
143    pub env: Vec<StackInputEnvironmentMapping>,
144}
145
146impl StackInputDefinition {
147    /// A deployer-provided boolean input, the shape `.enabled(input)` gates
148    /// on. With a default the input is optional; without one it is required,
149    /// since an unanswered gate would leave the resource's existence
150    /// undecided. This is the Rust-side seam gated-stack tests build inputs
151    /// with.
152    #[doc(hidden)]
153    pub fn deployer_boolean(
154        id: &str,
155        label: &str,
156        description: &str,
157        default: Option<bool>,
158    ) -> Self {
159        Self {
160            id: id.to_string(),
161            kind: StackInputKind::Boolean,
162            provided_by: vec![StackInputProvider::Deployer],
163            required: default.is_none(),
164            label: label.to_string(),
165            description: description.to_string(),
166            placeholder: None,
167            default: default.map(StackInputDefaultValue::Boolean),
168            platforms: None,
169            validation: None,
170            env: Vec::new(),
171        }
172    }
173}
174
175/// Finds the boolean deployer input a gate references, for render-time
176/// re-validation. The compile-time preflight enforces the same two rules;
177/// generators repeat them so a caller that renders without preflights cannot
178/// ship a template whose gate variable is undeclared or non-boolean.
179pub fn find_boolean_gate_input<'a>(
180    inputs: &'a [StackInputDefinition],
181    input_id: &str,
182) -> Result<&'a StackInputDefinition, GateInputIssue> {
183    let input = inputs
184        .iter()
185        .find(|input| input.id == input_id)
186        .ok_or(GateInputIssue::Undeclared)?;
187    if input.kind != StackInputKind::Boolean {
188        return Err(GateInputIssue::NotBoolean(input.kind.clone()));
189    }
190    Ok(input)
191}
192
193/// Why a gate input failed render-time validation; the caller owns the
194/// backend-specific error message.
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub enum GateInputIssue {
197    /// The stack declares no input with the gate's id.
198    Undeclared,
199    /// The input exists but is not a boolean.
200    NotBoolean(StackInputKind),
201}