pkg_rs/
input.rs

1use std::{collections::HashMap, fs, path::PathBuf};
2
3use kdl::{KdlDocument, KdlError, KdlNode};
4use miette::{Diagnostic, IntoDiagnostic, Report, Result};
5use thiserror::Error;
6
7#[derive(Debug)]
8pub enum PkgType {
9    SingleExecutable, // so the entry point is the pkg path itself
10    Folder(PathBuf),  // the entry point of the package
11}
12
13#[derive(Debug, Clone, PartialEq)]
14pub enum AttributeValue {
15    String(String),
16    Integer(i64),
17    Float(f64),
18    Boolean(bool),
19}
20
21#[derive(Debug, Clone, PartialEq)]
22pub struct PkgDeclaration {
23    pub name: String,
24    pub input: String,
25    pub attributes: HashMap<String, AttributeValue>,
26}
27
28#[derive(Debug)]
29pub struct Bridge {
30    pub name: String,
31    pub pkgs: Vec<PkgDeclaration>,
32}
33
34#[derive(Debug)]
35pub struct Input {
36    pub path: PathBuf,
37    pub bridges: Vec<Bridge>,
38}
39
40#[derive(Error, Debug, Diagnostic)]
41pub enum InputError {
42    #[error(transparent)]
43    #[diagnostic(code(input::io_error))]
44    IoError(#[from] std::io::Error),
45
46    #[error(transparent)]
47    #[diagnostic(code(input::parse_error))]
48    KdlError(#[from] KdlError),
49
50    #[error("Unsupported attribute type {0}")]
51    #[diagnostic(code(input::wrong_value))]
52    UnSupportedAttributeType(String),
53
54    #[error("Missing required field")]
55    #[diagnostic(code(input::missing_field))]
56    MissingField,
57
58    #[error("Invalid attribute format")]
59    #[diagnostic(code(input::invalid_attribute))]
60    InvalidAttribute,
61
62    #[error("Duplicate package declaration: {0}")]
63    #[diagnostic(code(input::duplicate_pkg))]
64    DuplicatePkgDeclaration(String),
65}
66
67fn detect_pkg_kdl_files(path: &PathBuf) -> Result<Vec<PathBuf>> {
68    let mut inputs_paths = Vec::new();
69    for entry in fs::read_dir(path).into_diagnostic()? {
70        let entry = entry.into_diagnostic()?;
71        let path = entry.path();
72
73        if path.is_file() {
74            if let Some(ext) = path.extension().and_then(|e| e.to_str())
75                && ext.eq_ignore_ascii_case("kdl")
76                && let Some(file_name) = path.file_name().and_then(|n| n.to_str())
77                && !file_name.starts_with('.')
78            {
79                inputs_paths.push(path);
80            }
81        } else if path.is_dir() {
82            inputs_paths.extend(detect_pkg_kdl_files(&path)?);
83        }
84    }
85    Ok(inputs_paths)
86}
87
88fn parse_inputs_kdl(inputs_paths: &[PathBuf]) -> Result<Vec<KdlDocument>> {
89    inputs_paths
90        .iter()
91        .map(|path| {
92            fs::read_to_string(path)
93                .into_diagnostic()?
94                .parse::<KdlDocument>()
95                .into_diagnostic()
96        })
97        .collect()
98}
99
100fn parse_attributes(node: &KdlNode) -> Result<HashMap<String, AttributeValue>, InputError> {
101    let mut attributes = HashMap::new();
102
103    for entry in node.entries().iter().skip(1) {
104        // Skip first entry which is the input
105        let name = entry.name().ok_or(InputError::MissingField)?;
106        let value = entry.value();
107
108        let attr_value = if value.is_string() {
109            AttributeValue::String(value.as_string().unwrap().to_string())
110        } else if value.is_integer() {
111            AttributeValue::Integer(value.as_integer().unwrap() as i64)
112        } else if value.is_bool() {
113            AttributeValue::Boolean(value.as_bool().unwrap())
114        } else if value.is_float() {
115            AttributeValue::Float(value.as_float().unwrap())
116        } else {
117            return Err(InputError::UnSupportedAttributeType(value.to_string()));
118        };
119
120        attributes.insert(name.to_string(), attr_value);
121    }
122
123    Ok(attributes)
124}
125
126fn parse_bridges(kdl_docs: &[KdlDocument]) -> Result<Vec<Bridge>> {
127    let mut bridges = Vec::<Bridge>::new();
128
129    for doc in kdl_docs {
130        for bridge_node in doc.nodes() {
131            let bridge_name = bridge_node.name().to_string();
132            let mut bridge = Bridge {
133                name: bridge_name.clone(),
134                pkgs: Vec::new(),
135            };
136
137            let children = bridge_node.children().ok_or(InputError::MissingField)?;
138
139            for pkg_decl_node in children.nodes() {
140                let input = pkg_decl_node
141                    .entries()
142                    .first()
143                    .map(|entry| {
144                        entry
145                            .value()
146                            .as_string()
147                            .ok_or(InputError::InvalidAttribute)
148                            .map(|s| s.to_string())
149                    })
150                    .unwrap_or_else(|| Ok(pkg_decl_node.name().to_string()))?;
151
152                let pkg_decl = PkgDeclaration {
153                    name: pkg_decl_node.name().to_string(),
154                    input,
155                    attributes: parse_attributes(pkg_decl_node)?,
156                };
157
158                if bridges.iter().any(|b: &Bridge| {
159                    b.pkgs
160                        .iter()
161                        .any(|p: &PkgDeclaration| p.name == pkg_decl.name)
162                }) {
163                    return Err(Report::new(InputError::DuplicatePkgDeclaration(
164                        pkg_decl.name.clone(),
165                    )));
166                }
167
168                bridge.pkgs.push(pkg_decl);
169            }
170            if let Some(existing_bridge) = bridges.iter_mut().find(|b| b.name == bridge.name) {
171                existing_bridge.pkgs.extend(bridge.pkgs);
172            } else {
173                bridges.push(bridge);
174            }
175        }
176    }
177
178    Ok(bridges)
179}
180
181impl Input {
182    pub fn load(path: &PathBuf) -> Result<Self> {
183        let inputs_paths = detect_pkg_kdl_files(path)?;
184        let kdl_docs = parse_inputs_kdl(&inputs_paths)?;
185        let bridges = parse_bridges(&kdl_docs)?;
186
187        Ok(Self {
188            path: path.clone(),
189            bridges,
190        })
191    }
192}