Skip to main content

txtx_addon_kit/types/
mod.rs

1use std::collections::HashMap;
2use std::env;
3use std::fmt::Display;
4use std::path::PathBuf;
5
6use commands::{PostConditionEvaluatableInput, PreConditionEvaluatableInput};
7use diagnostics::Diagnostic;
8use dyn_clone::DynClone;
9use hcl_edit::expr::Expression;
10use hcl_edit::structure::Block;
11use serde::de::Error;
12use serde::{Deserialize, Deserializer, Serialize, Serializer};
13use sha2::{Digest, Sha256};
14use types::{ObjectDefinition, ObjectProperty, Type};
15use uuid::Uuid;
16
17use crate::helpers::fs::FileLocation;
18
19pub mod block_id;
20pub mod cloud_interface;
21pub mod commands;
22pub mod construct_type;
23pub mod typed_block;
24pub mod diagnostic_types;
25pub mod diagnostics;
26
27// Re-export common diagnostic types for convenience
28pub use diagnostic_types::{DiagnosticLevel, DiagnosticSpan, RelatedLocation};
29
30pub mod embedded_runbooks;
31pub mod frontend;
32pub mod functions;
33pub mod package;
34pub mod signers;
35pub mod stores;
36pub mod types;
37
38pub const CACHED_NONCE: &str = "cached_nonce";
39
40#[cfg(test)]
41mod tests;
42
43#[derive(Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
44pub struct Did(pub [u8; 32]);
45
46impl Did {
47    pub fn from_components(comps: Vec<impl AsRef<[u8]>>) -> Self {
48        let mut hasher = Sha256::new();
49        for comp in comps {
50            hasher.update(comp);
51        }
52        let hash = hasher.finalize();
53        Did(hash.into())
54    }
55
56    pub fn from_hex_string(source_bytes_str: &str) -> Self {
57        let bytes = hex::decode(source_bytes_str).expect("invalid hex_string");
58        Self::from_bytes(&bytes)
59    }
60
61    pub fn from_bytes(source_bytes: &Vec<u8>) -> Self {
62        let mut bytes = [0u8; 32];
63        bytes.copy_from_slice(&source_bytes);
64        Did(bytes)
65    }
66
67    pub fn zero() -> Self {
68        Self([0u8; 32])
69    }
70
71    pub fn to_string(&self) -> String {
72        hex::encode(self.0)
73    }
74
75    pub fn as_bytes(&self) -> &[u8] {
76        self.0.as_slice()
77    }
78
79    pub fn as_uuid(&self) -> Uuid {
80        Uuid::from_bytes(self.0[0..16].try_into().unwrap())
81    }
82}
83
84impl Serialize for Did {
85    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
86    where
87        S: Serializer,
88    {
89        serializer.serialize_str(&format!("0x{}", self))
90    }
91}
92
93impl<'de> Deserialize<'de> for Did {
94    fn deserialize<D>(deserializer: D) -> Result<Did, D::Error>
95    where
96        D: Deserializer<'de>,
97    {
98        let bytes_hex: String = serde::Deserialize::deserialize(deserializer)?;
99        let bytes = hex::decode(&bytes_hex[2..]).map_err(|e| D::Error::custom(e.to_string()))?;
100        Ok(Did::from_bytes(&bytes))
101    }
102}
103
104impl std::fmt::Display for Did {
105    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
106        write!(f, "{}", self.to_string())
107    }
108}
109
110impl std::fmt::Debug for Did {
111    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
112        write!(f, "0x{}", self.to_string())
113    }
114}
115
116#[derive(Clone, Debug, PartialEq, Eq, Hash)]
117pub struct RunbookDid(pub Did);
118
119impl RunbookDid {
120    pub fn value(&self) -> Did {
121        self.0.clone()
122    }
123
124    pub fn as_bytes(&self) -> &[u8] {
125        self.0.as_bytes()
126    }
127
128    pub fn to_string(&self) -> String {
129        self.0.to_string()
130    }
131}
132
133#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
134pub struct RunbookId {
135    /// Canonical name of the org authoring the workspace
136    pub org: Option<String>,
137    /// Canonical name of the workspace supporting the runbook
138    pub workspace: Option<String>,
139    /// Canonical name of the runbook
140    pub name: String,
141}
142
143impl RunbookId {
144    pub fn new(org: Option<String>, workspace: Option<String>, name: &str) -> RunbookId {
145        RunbookId { org, workspace, name: name.into() }
146    }
147    pub fn did(&self) -> RunbookDid {
148        let mut comps = vec![];
149        if let Some(ref org) = self.org {
150            comps.push(org.as_bytes());
151        }
152        if let Some(ref workspace) = self.workspace {
153            comps.push(workspace.as_bytes());
154        }
155        comps.push(self.name.as_bytes());
156        let did = Did::from_components(comps);
157        RunbookDid(did)
158    }
159
160    pub fn zero() -> RunbookId {
161        RunbookId { org: None, workspace: None, name: "".into() }
162    }
163}
164
165pub struct RunbookInstanceContext {
166    pub runbook_id: RunbookId,
167    pub workspace_location: FileLocation,
168    pub environment_selector: Option<String>,
169}
170
171impl RunbookInstanceContext {
172    pub fn get_workspace_root(&self) -> Result<FileLocation, String> {
173        self.workspace_location.get_parent_location()
174    }
175    pub fn environment_selector<'a>(&'a self, default: &'a str) -> &'a str {
176        self.environment_selector.as_deref().unwrap_or(default)
177    }
178}
179
180#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
181pub struct PackageDid(pub Did);
182
183impl PackageDid {
184    pub fn as_bytes(&self) -> &[u8] {
185        self.0.as_bytes()
186    }
187
188    pub fn to_string(&self) -> String {
189        self.0.to_string()
190    }
191}
192
193#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
194pub struct PackageId {
195    /// Id of the Runbook
196    pub runbook_id: RunbookId,
197    /// Location of the package within the workspace
198    pub package_location: FileLocation,
199    /// Name of the package
200    pub package_name: String,
201}
202
203impl PackageId {
204    pub fn did(&self) -> PackageDid {
205        let did = Did::from_components(vec![
206            self.runbook_id.did().as_bytes(),
207            self.package_name.to_string().as_bytes(),
208            // todo(lgalabru): This should be done upstream.
209            // Serializing is allowing us to get a canonical location.
210            serde_json::json!(self.package_location).to_string().as_bytes(),
211        ]);
212        PackageDid(did)
213    }
214
215    pub fn zero() -> PackageId {
216        PackageId {
217            runbook_id: RunbookId::zero(),
218            package_location: FileLocation::working_dir(),
219            package_name: "".into(),
220        }
221    }
222    pub fn from_file(
223        location: &FileLocation,
224        runbook_id: &RunbookId,
225        package_name: &str,
226    ) -> Result<Self, Diagnostic> {
227        let package_location = location.get_parent_location().map_err(|e| {
228            Diagnostic::error_from_string(format!("{}", e.to_string())).location(&location)
229        })?;
230        Ok(PackageId {
231            runbook_id: runbook_id.clone(),
232            package_location: package_location.clone(),
233            package_name: package_name.to_string(),
234        })
235    }
236}
237
238#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd)]
239pub struct ConstructDid(pub Did);
240
241impl ConstructDid {
242    pub fn value(&self) -> Did {
243        self.0.clone()
244    }
245
246    pub fn as_bytes(&self) -> &[u8] {
247        self.0.as_bytes()
248    }
249
250    pub fn to_string(&self) -> String {
251        self.0.to_string()
252    }
253
254    pub fn from_hex_string(did_str: &str) -> Self {
255        ConstructDid(Did::from_hex_string(did_str))
256    }
257
258    pub fn as_uuid(&self) -> Uuid {
259        self.0.as_uuid()
260    }
261}
262
263impl Display for ConstructDid {
264    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
265        write!(f, "{}", self.to_string())
266    }
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct ConstructId {
271    /// Id of the Package
272    pub package_id: PackageId,
273    /// Location of the file enclosing the construct
274    pub construct_location: FileLocation,
275    /// Type of construct (e.g. `variable` in `variable.value``)
276    pub construct_type: construct_type::ConstructType,
277    /// Name of construct (e.g. `value` in `variable.value``)
278    pub construct_name: String,
279}
280
281impl ConstructId {
282    pub fn did(&self) -> ConstructDid {
283        let did = Did::from_components(vec![
284            self.package_id.did().as_bytes(),
285            self.construct_type.as_ref().as_bytes(),  // Zero-cost conversion via AsRef trait
286            self.construct_name.to_string().as_bytes(),
287            // todo(lgalabru): This should be done upstream.
288            // Serializing is allowing us to get a canonical location.
289            serde_json::json!(self.construct_location).to_string().as_bytes(),
290        ]);
291        ConstructDid(did)
292    }
293
294    /// Get the construct type as a string reference.
295    ///
296    /// This is a zero-cost conversion that returns a reference to a static string.
297    pub fn construct_type_str(&self) -> &str {
298        self.construct_type.as_ref()
299    }
300}
301
302#[derive(Debug, Clone)]
303pub struct Construct {
304    /// Id of the Construct
305    pub construct_id: ConstructId,
306}
307
308#[derive(Debug, Clone)]
309pub struct AuthorizationContext {
310    pub workspace_location: FileLocation,
311}
312
313impl AuthorizationContext {
314    pub fn new(workspace_location: FileLocation) -> Self {
315        Self { workspace_location }
316    }
317
318    pub fn empty() -> Self {
319        Self { workspace_location: FileLocation::working_dir() }
320    }
321
322    pub fn get_file_location_from_path_buf(&self, input: &PathBuf) -> Result<FileLocation, String> {
323        let path_str = input.to_string_lossy();
324
325        let loc = if let Some(stripped) = path_str.strip_prefix("~/") {
326            let home = PathBuf::from(get_home_dir());
327            FileLocation::from_path(home.join(stripped))
328        }
329        // If absolute, use as-is
330        else if input.is_absolute() {
331            FileLocation::from_path(input.clone())
332        } else {
333            let mut workspace_loc = self
334                .workspace_location
335                .get_parent_location()
336                .map_err(|e| format!("unable to read workspace location: {e}"))?;
337
338            workspace_loc
339                .append_path(&path_str.to_string())
340                .map_err(|e| format!("invalid path: {}", e))?;
341            workspace_loc
342        };
343
344        Ok(loc)
345    }
346}
347
348/// Gets the user's home directory, accounting for the Snap confinement environment.
349/// We set out snap build to set this environment variable to the real home directory,
350/// because by default, snaps run in a confined environment where the home directory is not
351/// the user's actual home directory.
352fn get_home_dir() -> String {
353    if let Ok(real_home) = env::var("SNAP_REAL_HOME") {
354        let path_buf = PathBuf::from(real_home);
355        path_buf.display().to_string()
356    } else {
357        dirs::home_dir().unwrap().display().to_string()
358    }
359}
360
361#[derive(Debug)]
362pub enum ContractSourceTransform {
363    FindAndReplace(String, String),
364    RemapDownstreamDependencies(String, String),
365}
366
367pub struct AddonPostProcessingResult {
368    pub dependencies: HashMap<ConstructDid, Vec<ConstructDid>>,
369    pub transforms: HashMap<ConstructDid, Vec<ContractSourceTransform>>,
370}
371
372impl AddonPostProcessingResult {
373    pub fn new() -> AddonPostProcessingResult {
374        AddonPostProcessingResult { dependencies: HashMap::new(), transforms: HashMap::new() }
375    }
376}
377
378#[derive(Debug, Clone)]
379pub struct AddonInstance {
380    pub addon_id: String,
381    pub package_id: PackageId,
382    pub block: Block,
383}
384
385pub trait WithEvaluatableInputs {
386    fn name(&self) -> String;
387    fn block(&self) -> &Block;
388    fn get_expression_from_input(&self, input_name: &str) -> Option<Expression>;
389    fn get_blocks_for_map(
390        &self,
391        input_name: &str,
392        input_typing: &Type,
393        input_optional: bool,
394    ) -> Result<Option<Vec<Block>>, Vec<Diagnostic>>;
395    fn get_expression_from_block(&self, block: &Block, prop: &ObjectProperty)
396        -> Option<Expression>;
397    fn get_expression_from_object(
398        &self,
399        input_name: &str,
400        input_typing: &Type,
401    ) -> Result<Option<Expression>, Vec<Diagnostic>>;
402    fn get_expression_from_object_property(
403        &self,
404        input_name: &str,
405        prop: &ObjectProperty,
406    ) -> Option<Expression>;
407
408    /// Defines the inputs for this trait type with evaluatable inputs
409    fn _spec_inputs(&self) -> Vec<Box<dyn EvaluatableInput>>;
410
411    // Merges some default inputs that are available for all commands
412    // with those defined specifically for the implementer of this trait
413    fn spec_inputs(&self) -> Vec<Box<dyn EvaluatableInput>> {
414        let mut spec_inputs = self._spec_inputs();
415        spec_inputs.push(Box::new(PreConditionEvaluatableInput::new()));
416        spec_inputs
417    }
418
419    fn self_referencing_inputs(&self) -> Vec<Box<dyn EvaluatableInput>> {
420        vec![Box::new(PostConditionEvaluatableInput::new())]
421    }
422}
423
424pub trait EvaluatableInput: DynClone {
425    fn documentation(&self) -> String;
426    fn optional(&self) -> bool;
427    fn typing(&self) -> &Type;
428    fn name(&self) -> String;
429    fn as_object(&self) -> Option<&ObjectDefinition> {
430        self.typing().as_object()
431    }
432    fn as_array(&self) -> Option<&Box<Type>> {
433        self.typing().as_array()
434    }
435    fn as_action(&self) -> Option<&String> {
436        self.typing().as_action()
437    }
438    fn as_map(&self) -> Option<&ObjectDefinition> {
439        self.typing().as_map()
440    }
441}
442
443dyn_clone::clone_trait_object!(EvaluatableInput);