blueprint_engine_core/value/
structs.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use indexmap::IndexMap;
5
6use super::Value;
7use crate::error::{BlueprintError, Result};
8
9#[derive(Debug, Clone, PartialEq)]
10pub enum TypeAnnotation {
11    Simple(String),
12    Parameterized(String, Vec<TypeAnnotation>),
13    Optional(Box<TypeAnnotation>),
14    Any,
15}
16
17impl TypeAnnotation {
18    pub fn matches(&self, value: &Value) -> bool {
19        match self {
20            TypeAnnotation::Any => true,
21            TypeAnnotation::Simple(name) => match name.as_str() {
22                "int" => matches!(value, Value::Int(_)),
23                "float" => matches!(value, Value::Float(_) | Value::Int(_)),
24                "str" => matches!(value, Value::String(_)),
25                "bool" => matches!(value, Value::Bool(_)),
26                "list" => matches!(value, Value::List(_)),
27                "dict" => matches!(value, Value::Dict(_)),
28                "tuple" => matches!(value, Value::Tuple(_)),
29                "None" | "NoneType" => matches!(value, Value::None),
30                struct_name => {
31                    if let Value::StructInstance(inst) = value {
32                        inst.struct_type.name == struct_name
33                    } else {
34                        false
35                    }
36                }
37            },
38            TypeAnnotation::Parameterized(name, _params) => match name.as_str() {
39                "list" => matches!(value, Value::List(_)),
40                "dict" => matches!(value, Value::Dict(_)),
41                _ => false,
42            },
43            TypeAnnotation::Optional(inner) => matches!(value, Value::None) || inner.matches(value),
44        }
45    }
46
47    pub fn type_name(&self) -> String {
48        match self {
49            TypeAnnotation::Any => "any".to_string(),
50            TypeAnnotation::Simple(name) => name.clone(),
51            TypeAnnotation::Parameterized(name, params) => {
52                let param_strs: Vec<String> = params.iter().map(|p| p.type_name()).collect();
53                format!("{}[{}]", name, param_strs.join(", "))
54            }
55            TypeAnnotation::Optional(inner) => format!("{}?", inner.type_name()),
56        }
57    }
58}
59
60#[derive(Debug, Clone)]
61pub struct StructField {
62    pub name: String,
63    pub typ: TypeAnnotation,
64    pub default: Option<Value>,
65}
66
67#[derive(Debug, Clone)]
68pub struct StructType {
69    pub name: String,
70    pub fields: Vec<StructField>,
71}
72
73impl StructType {
74    pub fn instantiate(
75        &self,
76        args: Vec<Value>,
77        kwargs: HashMap<String, Value>,
78    ) -> Result<StructInstance> {
79        let mut field_values: IndexMap<String, Value> = IndexMap::new();
80
81        let mut positional_idx = 0;
82        for field in &self.fields {
83            let value = if let Some(v) = kwargs.get(&field.name) {
84                v.clone()
85            } else if positional_idx < args.len() {
86                let v = args[positional_idx].clone();
87                positional_idx += 1;
88                v
89            } else if let Some(default) = &field.default {
90                default.clone()
91            } else {
92                return Err(BlueprintError::ArgumentError {
93                    message: format!(
94                        "{}() missing required argument: '{}'",
95                        self.name, field.name
96                    ),
97                });
98            };
99
100            if !field.typ.matches(&value) {
101                return Err(BlueprintError::TypeError {
102                    expected: format!(
103                        "{} for field '{}' in {}()",
104                        field.typ.type_name(),
105                        field.name,
106                        self.name
107                    ),
108                    actual: value.type_name().to_string(),
109                });
110            }
111
112            field_values.insert(field.name.clone(), value);
113        }
114
115        if positional_idx < args.len() {
116            return Err(BlueprintError::ArgumentError {
117                message: format!(
118                    "{}() takes {} positional arguments but {} were given",
119                    self.name,
120                    self.fields.len(),
121                    args.len()
122                ),
123            });
124        }
125
126        for key in kwargs.keys() {
127            if !self.fields.iter().any(|f| &f.name == key) {
128                return Err(BlueprintError::ArgumentError {
129                    message: format!("{}() got unexpected keyword argument '{}'", self.name, key),
130                });
131            }
132        }
133
134        Ok(StructInstance {
135            struct_type: Arc::new(self.clone()),
136            fields: field_values,
137        })
138    }
139}
140
141#[derive(Debug, Clone)]
142pub struct StructInstance {
143    pub struct_type: Arc<StructType>,
144    pub fields: IndexMap<String, Value>,
145}
146
147impl StructInstance {
148    pub fn get_field(&self, name: &str) -> Option<Value> {
149        self.fields.get(name).cloned()
150    }
151
152    pub fn to_display_string(&self) -> String {
153        let field_strs: Vec<String> = self
154            .struct_type
155            .fields
156            .iter()
157            .map(|f| {
158                let val = self
159                    .fields
160                    .get(&f.name)
161                    .map(|v| v.repr())
162                    .unwrap_or_default();
163                format!("{}={}", f.name, val)
164            })
165            .collect();
166        format!("{}({})", self.struct_type.name, field_strs.join(", "))
167    }
168}