Skip to main content

coil_wasm/manifest/
config.rs

1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, Copy)]
4pub enum ExtensionConfigValueType {
5    String,
6    Integer,
7    Boolean,
8}
9
10impl std::fmt::Display for ExtensionConfigValueType {
11    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12        match self {
13            Self::String => f.write_str("string"),
14            Self::Integer => f.write_str("integer"),
15            Self::Boolean => f.write_str("boolean"),
16        }
17    }
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ExtensionConfigValue {
22    String(String),
23    Integer(i64),
24    Boolean(bool),
25}
26
27impl ExtensionConfigValue {
28    pub fn value_type(&self) -> ExtensionConfigValueType {
29        match self {
30            Self::String(_) => ExtensionConfigValueType::String,
31            Self::Integer(_) => ExtensionConfigValueType::Integer,
32            Self::Boolean(_) => ExtensionConfigValueType::Boolean,
33        }
34    }
35
36    pub(crate) fn validate_for_key(&self, key: &str) -> Result<(), WasmModelError> {
37        if let Self::String(value) = self {
38            if value.trim().is_empty() {
39                return Err(WasmModelError::InvalidConfigValue {
40                    key: key.to_string(),
41                    reason: "string values must not be empty".to_string(),
42                });
43            }
44        }
45
46        Ok(())
47    }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct ExtensionConfigField {
52    pub key: String,
53    pub value_type: ExtensionConfigValueType,
54    pub required: bool,
55    pub default: Option<ExtensionConfigValue>,
56}
57
58impl ExtensionConfigField {
59    pub fn new(
60        key: impl Into<String>,
61        value_type: ExtensionConfigValueType,
62        required: bool,
63    ) -> Result<Self, WasmModelError> {
64        Ok(Self {
65            key: validate_token("extension_config_key", key.into())?,
66            value_type,
67            required,
68            default: None,
69        })
70    }
71
72    pub fn required(
73        key: impl Into<String>,
74        value_type: ExtensionConfigValueType,
75    ) -> Result<Self, WasmModelError> {
76        Self::new(key, value_type, true)
77    }
78
79    pub fn optional(
80        key: impl Into<String>,
81        value_type: ExtensionConfigValueType,
82    ) -> Result<Self, WasmModelError> {
83        Self::new(key, value_type, false)
84    }
85
86    pub fn with_default(mut self, value: ExtensionConfigValue) -> Result<Self, WasmModelError> {
87        if value.value_type() != self.value_type {
88            return Err(WasmModelError::ConfigTypeMismatch {
89                key: self.key.clone(),
90                expected: self.value_type,
91                actual: value.value_type(),
92            });
93        }
94        value.validate_for_key(&self.key)?;
95        self.default = Some(value);
96        Ok(self)
97    }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ExtensionConfigSchema {
102    pub version: u32,
103    pub fields: Vec<ExtensionConfigField>,
104}
105
106impl ExtensionConfigSchema {
107    pub fn new(version: u32, fields: Vec<ExtensionConfigField>) -> Result<Self, WasmModelError> {
108        if version == 0 {
109            return Err(WasmModelError::ZeroSchemaVersion {
110                field: "extension_config_schema_version",
111            });
112        }
113
114        let schema = Self { version, fields };
115        schema.validate()?;
116        Ok(schema)
117    }
118
119    pub fn validate(&self) -> Result<(), WasmModelError> {
120        let mut seen = std::collections::BTreeSet::new();
121        for field in &self.fields {
122            if !seen.insert(field.key.clone()) {
123                return Err(WasmModelError::DuplicateConfigField {
124                    key: field.key.clone(),
125                });
126            }
127
128            if let Some(default) = &field.default {
129                if default.value_type() != field.value_type {
130                    return Err(WasmModelError::ConfigTypeMismatch {
131                        key: field.key.clone(),
132                        expected: field.value_type,
133                        actual: default.value_type(),
134                    });
135                }
136                default.validate_for_key(&field.key)?;
137            }
138        }
139        Ok(())
140    }
141
142    pub fn effective_values(
143        &self,
144        configured: &std::collections::BTreeMap<String, ExtensionConfigValue>,
145    ) -> Result<std::collections::BTreeMap<String, ExtensionConfigValue>, WasmModelError> {
146        self.validate()?;
147
148        for (key, value) in configured {
149            let Some(field) = self.fields.iter().find(|field| field.key == *key) else {
150                return Err(WasmModelError::UnknownConfigField { key: key.clone() });
151            };
152
153            if value.value_type() != field.value_type {
154                return Err(WasmModelError::ConfigTypeMismatch {
155                    key: key.clone(),
156                    expected: field.value_type,
157                    actual: value.value_type(),
158                });
159            }
160
161            value.validate_for_key(key)?;
162        }
163
164        let mut effective = std::collections::BTreeMap::new();
165        for field in &self.fields {
166            if let Some(value) = configured.get(&field.key) {
167                effective.insert(field.key.clone(), value.clone());
168            } else if let Some(default) = &field.default {
169                effective.insert(field.key.clone(), default.clone());
170            } else if field.required {
171                return Err(WasmModelError::MissingRequiredConfigField {
172                    key: field.key.clone(),
173                });
174            }
175        }
176
177        Ok(effective)
178    }
179}