oslquery_petite/
query.rs

1//! Query API using the fully type-safe parameter system.
2
3use std::path::Path;
4
5use crate::parser::ParseError;
6use crate::types::{Metadata, Parameter};
7
8/// Main structure for querying OSL shader information.
9#[derive(Debug, Clone, PartialEq)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct OslQuery {
12    /// Shader name
13    shader_name: String,
14    /// Shader type (surface, displacement, volume, etc.)
15    shader_type: String,
16    /// List of shader parameters
17    parameters: Vec<Parameter>,
18    /// Global shader metadata
19    metadata: Vec<Metadata>,
20}
21
22impl OslQuery {
23    /// Create a new empty OslQuery.
24    pub fn new() -> Self {
25        OslQuery {
26            shader_name: String::new(),
27            shader_type: String::new(),
28            parameters: Vec::new(),
29            metadata: Vec::new(),
30        }
31    }
32
33    /// Open and parse an OSO file from disk.
34    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, ParseError> {
35        Self::open_with_searchpath(path, "")
36    }
37
38    /// Open and parse an OSO file with search path support.
39    pub fn open_with_searchpath<P: AsRef<Path>>(
40        path: P,
41        searchpath: &str,
42    ) -> Result<Self, ParseError> {
43        let path = path.as_ref();
44
45        // Check if file has .oso extension
46        if path.extension().and_then(|s| s.to_str()) != Some("oso") {
47            // Append .oso extension
48            let mut path_with_ext = path.to_path_buf();
49            path_with_ext.set_extension("oso");
50
51            if path_with_ext.exists() {
52                return crate::parser::OsoReader::new().parse_file(path_with_ext);
53            }
54        }
55
56        // Try direct path first
57        if path.exists() {
58            return crate::parser::OsoReader::new().parse_file(path);
59        }
60
61        // Try searchpath if provided
62        if !searchpath.is_empty() {
63            for search_dir in searchpath.split(':') {
64                let search_path = Path::new(search_dir).join(path);
65                if search_path.exists() {
66                    return crate::parser::OsoReader::new().parse_file(search_path);
67                }
68
69                // Also try with .oso extension
70                let mut search_path_with_ext = search_path.clone();
71                search_path_with_ext.set_extension("oso");
72                if search_path_with_ext.exists() {
73                    return crate::parser::OsoReader::new().parse_file(search_path_with_ext);
74                }
75            }
76        }
77
78        Err(ParseError::Io(format!("Shader file not found: {:?}", path)))
79    }
80
81    /// Parse OSO content from a string.
82    pub fn from_string(content: &str) -> Result<Self, ParseError> {
83        crate::parser::OsoReader::new().parse_string(content)
84    }
85
86    // Internal methods for the parser
87
88    pub(crate) fn set_shader_info(&mut self, shader_type: &str, shader_name: String) {
89        self.shader_type = shader_type.to_string();
90        self.shader_name = shader_name;
91    }
92
93    pub(crate) fn add_parameter(&mut self, param: Parameter) {
94        self.parameters.push(param);
95    }
96
97    pub(crate) fn add_metadata(&mut self, meta: Metadata) {
98        self.metadata.push(meta);
99    }
100
101    /// Get the shader name.
102    pub fn shader_name(&self) -> &str {
103        &self.shader_name
104    }
105
106    /// Get the shader type.
107    pub fn shader_type(&self) -> &str {
108        &self.shader_type
109    }
110
111    /// Get the number of parameters.
112    pub fn param_count(&self) -> usize {
113        self.parameters.len()
114    }
115
116    /// Get a parameter by index.
117    pub fn param_at(&self, index: usize) -> Option<&Parameter> {
118        self.parameters.get(index)
119    }
120
121    /// Get a parameter by name.
122    pub fn param_by_name(&self, name: &str) -> Option<&Parameter> {
123        self.parameters.iter().find(|p| p.name.as_str() == name)
124    }
125
126    /// Get all parameters.
127    pub fn params(&self) -> &[Parameter] {
128        &self.parameters
129    }
130
131    /// Get input parameters only.
132    pub fn input_params(&self) -> impl Iterator<Item = &Parameter> {
133        self.parameters.iter().filter(|p| !p.is_output())
134    }
135
136    /// Get output parameters only.
137    pub fn output_params(&self) -> impl Iterator<Item = &Parameter> {
138        self.parameters.iter().filter(|p| p.is_output())
139    }
140
141    /// Get global metadata.
142    pub fn metadata(&self) -> &[Metadata] {
143        &self.metadata
144    }
145
146    /// Find global metadata by name.
147    pub fn find_metadata(&self, name: &str) -> Option<&Metadata> {
148        self.metadata.iter().find(|m| m.name.as_str() == name)
149    }
150
151    /// Check if the query is valid (has been successfully parsed).
152    pub fn is_valid(&self) -> bool {
153        !self.shader_name.is_empty() && !self.shader_type.is_empty()
154    }
155}
156
157impl Default for OslQuery {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::types::TypedParameter;
167
168    #[test]
169    fn test_empty_query() {
170        let query = OslQuery::new();
171        assert!(!query.is_valid());
172        assert_eq!(query.param_count(), 0);
173        assert_eq!(query.shader_name(), "");
174        assert_eq!(query.shader_type(), "");
175    }
176
177    #[test]
178    fn test_from_string() {
179        let oso_content = r#"
180OpenShadingLanguage 1.12
181surface "test_shader"
182param float Kd 0.5
183code ___main___
184"#;
185
186        let query = OslQuery::from_string(oso_content).unwrap();
187        assert!(query.is_valid());
188        assert_eq!(query.shader_name(), "test_shader");
189        assert_eq!(query.shader_type(), "surface");
190        assert_eq!(query.param_count(), 1);
191
192        let param = query.param_by_name("Kd");
193        assert!(param.is_some());
194        let param = param.unwrap();
195        assert_eq!(param.name.as_str(), "Kd");
196        assert!(!param.is_output());
197
198        // Check the typed parameter - it should be a Float with default 0.5
199        match param.typed_param() {
200            TypedParameter::Float { default: Some(val) } => {
201                assert_eq!(*val, 0.5);
202            }
203            _ => panic!("Expected Float parameter with default"),
204        }
205    }
206
207    #[test]
208    fn test_type_safety() {
209        let oso_content = r#"
210OpenShadingLanguage 1.12
211shader test
212param color rgb 1 0 0
213param int count 42
214param float[3] values 1.0 2.0 3.0
215code ___main___
216"#;
217
218        let query = OslQuery::from_string(oso_content).unwrap();
219
220        // Color parameter - exactly 3 floats
221        let rgb = query.param_by_name("rgb").unwrap();
222        match rgb.typed_param() {
223            TypedParameter::Color {
224                default: Some([r, g, b]),
225                ..
226            } => {
227                assert_eq!(*r, 1.0);
228                assert_eq!(*g, 0.0);
229                assert_eq!(*b, 0.0);
230            }
231            _ => panic!("Expected Color parameter"),
232        }
233
234        // Int parameter - exactly 1 int
235        let count = query.param_by_name("count").unwrap();
236        match count.typed_param() {
237            TypedParameter::Int { default: Some(val) } => {
238                assert_eq!(*val, 42);
239            }
240            _ => panic!("Expected Int parameter"),
241        }
242
243        // Float array - exactly the right size
244        let values = query.param_by_name("values").unwrap();
245        match values.typed_param() {
246            TypedParameter::FloatArray {
247                size: 3,
248                default: Some(vals),
249            } => {
250                assert_eq!(vals, &vec![1.0, 2.0, 3.0]);
251            }
252            _ => panic!("Expected FloatArray[3] parameter"),
253        }
254    }
255
256    #[test]
257    fn test_input_output_separation() {
258        let oso_content = r#"
259OpenShadingLanguage 1.12
260surface test
261param float input1 0.5
262param color input2 1 0 0
263oparam color result
264code ___main___
265"#;
266
267        let query = OslQuery::from_string(oso_content).unwrap();
268
269        let inputs: Vec<_> = query.input_params().collect();
270        let outputs: Vec<_> = query.output_params().collect();
271
272        assert_eq!(inputs.len(), 2);
273        assert_eq!(outputs.len(), 1);
274
275        // Output should have no default value
276        let result = outputs[0];
277        match result.typed_param() {
278            TypedParameter::Color { default, .. } => {
279                assert!(default.is_none(), "Output parameter should have no default");
280            }
281            _ => panic!("Expected Color output parameter"),
282        }
283    }
284}