Skip to main content

webots_proto_ast/proto/
ast.rs

1use super::span::Span;
2use derive_new::new;
3use derive_setters::Setters;
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7/// Represents a complete PROTO document.
8#[derive(Debug, Clone, PartialEq, Default, Setters, Serialize, Deserialize)]
9#[setters(prefix = "with_", strip_option)]
10pub struct Proto {
11    /// The VRML/PROTO header.
12    pub header: Option<Header>,
13    /// EXTERNPROTO declarations.
14    pub externprotos: Vec<ExternProto>,
15    /// The PROTO definition itself.
16    pub proto: Option<ProtoDefinition>,
17    /// Root nodes (if this is a .wbt or a PROTO without a wrapper).
18    pub root_nodes: Vec<AstNode>,
19    /// Absolute source file path when loaded from disk.
20    #[serde(skip)]
21    pub source_path: Option<PathBuf>,
22    /// Original source content when loaded from disk.
23    #[serde(skip)]
24    pub source_content: Option<String>,
25}
26
27impl Proto {
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    pub fn new_with_header(header: Header) -> Self {
33        Self {
34            header: Some(header),
35            ..Self::default()
36        }
37    }
38}
39
40/// The file header, e.g., `#VRML_SIM R2025a utf8`.
41#[derive(Debug, Clone, PartialEq, Default, new, Setters, Serialize, Deserialize)]
42#[setters(prefix = "with_", strip_option)]
43pub struct Header {
44    pub version: String,
45    pub encoding: String,
46    pub raw: Option<String>,
47    pub span: Span,
48}
49
50/// An EXTERNPROTO declaration.
51#[derive(Debug, Clone, PartialEq, new, Setters, Serialize, Deserialize)]
52#[setters(prefix = "with_", strip_option)]
53pub struct ExternProto {
54    pub url: String,
55    pub alias: Option<String>,
56    pub span: Span,
57}
58
59/// The main PROTO definition block.
60#[derive(Debug, Clone, PartialEq, Default, new, Setters, Serialize, Deserialize)]
61#[setters(prefix = "with_", strip_option)]
62pub struct ProtoDefinition {
63    pub name: String,
64    #[new(default)]
65    pub fields: Vec<ProtoField>,
66    #[new(default)]
67    pub body: Vec<ProtoBodyItem>,
68    pub span: Span,
69}
70
71/// A field declaration in the PROTO interface.
72#[derive(Debug, Clone, PartialEq, new, Setters, Serialize, Deserialize)]
73#[setters(prefix = "with_", strip_option)]
74pub struct ProtoField {
75    pub name: String,
76    pub field_type: FieldType,
77    #[new(default)]
78    pub default_value: Option<FieldValue>,
79    /// e.g. "field", "vrmlField", "hiddenField", "deprecatedField"
80    pub keyword: FieldKeyword,
81    /// Optional list of allowed values for this field (e.g. `field SFString { "a", "b" } name "a"`)
82    #[new(default)]
83    pub restrictions: Option<Vec<FieldValue>>,
84    pub span: Span,
85}
86
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub enum FieldKeyword {
89    Field,
90    VrmlField,
91    HiddenField,
92    DeprecatedField,
93}
94
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub enum FieldType {
97    SFBool,
98    SFInt32,
99    SFFloat,
100    SFString,
101    SFVec2f,
102    SFVec3f,
103    SFRotation,
104    SFColor,
105    SFNode,
106    MFBool,
107    MFInt32,
108    MFFloat,
109    MFString,
110    MFVec2f,
111    MFVec3f,
112    MFRotation,
113    MFColor,
114    MFNode,
115    Unknown(String),
116}
117
118/// An item inside the PROTO body: either a Node or a Template Block.
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120pub enum ProtoBodyItem {
121    Node(AstNode),
122    Template(TemplateBlock),
123}
124
125/// A template block `%< ... >%` or expression `%<= ... >%`.
126#[derive(Debug, Clone, PartialEq, new, Setters, Serialize, Deserialize)]
127#[setters(prefix = "with_", strip_option)]
128pub struct TemplateBlock {
129    pub content: String,
130    /// If true, it is an expression `%<= ... >%`.
131    pub is_expression: bool,
132    pub span: Span,
133}
134
135/// A node in the AST (e.g., `Robot { ... }` or `USE name`).
136#[derive(Debug, Clone, PartialEq, new, Setters, Serialize, Deserialize)]
137#[setters(prefix = "with_", strip_option)]
138pub struct AstNode {
139    pub kind: AstNodeKind,
140    pub span: Span,
141}
142
143impl Default for AstNode {
144    fn default() -> Self {
145        Self {
146            kind: AstNodeKind::Node {
147                type_name: "Group".to_string(),
148                def_name: None,
149                fields: vec![],
150            },
151            span: Span::default(),
152        }
153    }
154}
155
156impl From<AstNode> for FieldValue {
157    fn from(node: AstNode) -> Self {
158        FieldValue::Node(Box::new(node))
159    }
160}
161
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub enum AstNodeKind {
164    /// A standard node definition: `[DEF name] Type { fields }`
165    Node {
166        type_name: String,
167        def_name: Option<String>,
168        fields: Vec<NodeBodyElement>,
169    },
170    /// A USE node: `USE name`
171    Use { use_name: String },
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub enum NodeBodyElement {
176    Field(NodeField),
177    Template(TemplateBlock),
178    /// Represents an unexpected token inside the body that we keep to be lossless.
179    /// This happens when template blocks mess up the structure (e.g., unbalanced braces).
180    Raw(RawSyntax),
181}
182
183#[derive(Debug, Clone, PartialEq, new, Setters, Serialize, Deserialize)]
184#[setters(prefix = "with_", strip_option)]
185pub struct RawSyntax {
186    pub text: String,
187    pub span: Span,
188}
189
190/// A field assignment inside a node (e.g., `translation 0 1 0` or `children IS bodySlot`).
191#[derive(Debug, Clone, PartialEq, new, Setters, Serialize, Deserialize)]
192#[setters(prefix = "with_", strip_option)]
193pub struct NodeField {
194    pub name: String,
195    pub value: FieldValue,
196    pub span: Span,
197}
198
199/// The value of a field.
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201pub enum FieldValue {
202    Bool(bool),
203    Int(i64, Option<String>),
204    Float(f64, Option<String>),
205    String(String),
206    Vec2f([f64; 2]),
207    Vec3f([f64; 3]),
208    Rotation([f64; 4]),
209    Color([f64; 3]),
210    Node(Box<AstNode>),
211    Array(ArrayValue),
212    /// A sequence of numbers without brackets (e.g. `0 0 1`).
213    NumberSequence(NumberSequence),
214    /// `IS fieldName`
215    Is(String),
216    /// `NULL` literal
217    Null,
218    /// Template expression as a value `%<= ... >%`
219    Template(TemplateBlock),
220    /// For unknown or complex types not fully parsed yet
221    Raw(String),
222}
223
224/// A bracketed array value.
225#[derive(Debug, Clone, PartialEq, new, Setters, Serialize, Deserialize)]
226#[setters(prefix = "with_", strip_option)]
227pub struct ArrayValue {
228    #[new(default)]
229    pub elements: Vec<ArrayElement>,
230}
231
232/// An array element.
233#[derive(Debug, Clone, PartialEq, new, Setters, Serialize, Deserialize)]
234#[setters(prefix = "with_", strip_option)]
235pub struct ArrayElement {
236    pub value: FieldValue,
237}
238
239/// A sequence of numeric values.
240#[derive(Debug, Clone, PartialEq, new, Setters, Serialize, Deserialize)]
241#[setters(prefix = "with_", strip_option)]
242pub struct NumberSequence {
243    #[new(default)]
244    pub elements: Vec<NumberSequenceElement>,
245}
246
247/// A number sequence element.
248#[derive(Debug, Clone, PartialEq, new, Setters, Serialize, Deserialize)]
249#[setters(prefix = "with_", strip_option)]
250pub struct NumberSequenceElement {
251    pub value: FieldValue,
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use crate::proto::span::Span;
258
259    #[test]
260    fn test_manual_ast_construction() {
261        let span = Span::default();
262
263        let header = Header::new("R2025a".to_string(), "utf8".to_string(), None, span.clone());
264
265        let extern_proto =
266            ExternProto::new("PedestrianTorso.proto".to_string(), None, span.clone());
267
268        let translation_field = ProtoField::new(
269            "translation".to_string(),
270            FieldType::SFVec3f,
271            FieldKeyword::Field,
272            span.clone(),
273        )
274        .with_default_value(FieldValue::Vec3f([0.0, 0.0, 1.27]));
275
276        let rotation_field = ProtoField::new(
277            "rotation".to_string(),
278            FieldType::SFRotation,
279            FieldKeyword::Field,
280            span.clone(),
281        )
282        .with_default_value(FieldValue::Rotation([0.0, 0.0, 1.0, 0.0]));
283
284        let template_statement = ProtoBodyItem::Template(TemplateBlock::new(
285            " const rigid = fields.controllerArgs.value.length == 0; ".to_string(),
286            false,
287            span.clone(),
288        ));
289
290        let robot_node = AstNode::new(
291            AstNodeKind::Node {
292                type_name: "Robot".to_string(),
293                def_name: None,
294                fields: vec![],
295            },
296            span.clone(),
297        );
298
299        let proto_def = ProtoDefinition::new("Pedestrian".to_string(), span.clone())
300            .with_fields(vec![translation_field, rotation_field])
301            .with_body(vec![template_statement, ProtoBodyItem::Node(robot_node)]);
302
303        let document = Proto::new()
304            .with_header(header)
305            .with_externprotos(vec![extern_proto])
306            .with_proto(proto_def);
307
308        assert_eq!(document.header.as_ref().unwrap().version, "R2025a");
309        assert_eq!(document.externprotos.len(), 1);
310        assert_eq!(document.externprotos[0].url, "PedestrianTorso.proto");
311
312        let proto = document.proto.as_ref().unwrap();
313        assert_eq!(proto.name, "Pedestrian");
314        assert_eq!(proto.fields.len(), 2);
315        assert_eq!(proto.fields[0].name, "translation");
316        if let Some(FieldValue::Vec3f(val)) = &proto.fields[0].default_value {
317            assert_eq!(*val, [0.0, 0.0, 1.27]);
318        } else {
319            panic!("Expected Vec3f default value");
320        }
321
322        assert_eq!(proto.body.len(), 2);
323        if let ProtoBodyItem::Template(block) = &proto.body[0] {
324            assert_eq!(
325                block.content,
326                " const rigid = fields.controllerArgs.value.length == 0; "
327            );
328            assert!(!block.is_expression);
329        } else {
330            panic!("Expected TemplateBlock");
331        }
332    }
333}