1#[cfg(feature = "validation")]
2mod imports;
3
4#[cfg(feature = "validation")]
5use std::collections::HashSet;
6
7use thiserror::Error;
8
9pub use webots_proto_ast as ast;
10#[cfg(feature = "resolver")]
11pub use webots_proto_resolver as resolver;
12#[cfg(feature = "schema")]
13pub use webots_proto_schema as schema;
14#[cfg(feature = "schema")]
15pub use webots_proto_schema::types;
16
17pub use webots_proto_ast::{AstNode, FieldValue, Proto};
18#[cfg(feature = "resolver")]
19pub use webots_proto_resolver::{ProtoResolver, ResolveOptions};
20#[cfg(feature = "r2025a")]
21pub use webots_proto_schema::r2025a;
22#[cfg(feature = "schema")]
23pub use webots_proto_schema::{
24 Diagnostic, DiagnosticSet, Error as SchemaError, NodeSchema, Result as SchemaResult,
25 SchemaField, Severity, ValidationContext, ast_to_r2025a_node, r2025a_node_to_ast,
26};
27#[cfg(feature = "template")]
28pub use webots_proto_template::{
29 RenderContext, RenderOptions, RenderWebotsVersion, TemplateContext, TemplateError,
30 TemplateEvaluator, TemplateField, TemplateFieldBinding, TemplateWebotsVersion,
31};
32
33#[derive(Debug, Error)]
34pub enum Error {
35 #[error(transparent)]
36 Ast(#[from] webots_proto_ast::ProtoError),
37 #[cfg(feature = "template")]
38 #[error(transparent)]
39 Template(#[from] webots_proto_template::TemplateError),
40 #[cfg(feature = "resolver")]
41 #[error(transparent)]
42 Resolver(#[from] webots_proto_resolver::ProtoError),
43 #[error(transparent)]
44 Io(#[from] std::io::Error),
45}
46
47pub type Result<T> = std::result::Result<T, Error>;
48
49#[cfg(feature = "validation")]
50#[derive(Debug, Clone, Default)]
51pub struct ValidationOptions {
52 pub local_externproto_naming: bool,
53 pub runtime_semantics: bool,
54}
55
56#[cfg(feature = "validation")]
57impl ValidationOptions {
58 pub fn new() -> Self {
59 Self {
60 local_externproto_naming: true,
61 runtime_semantics: true,
62 }
63 }
64
65 pub fn with_local_externproto_naming(mut self, enabled: bool) -> Self {
66 self.local_externproto_naming = enabled;
67 self
68 }
69
70 pub fn with_runtime_semantics(mut self, enabled: bool) -> Self {
71 self.runtime_semantics = enabled;
72 self
73 }
74}
75
76#[cfg(any(feature = "template", feature = "validation"))]
77pub trait ProtoExt {
78 #[cfg(feature = "template")]
79 fn render(&self, options: &RenderOptions) -> Result<String>;
80 #[cfg(feature = "validation")]
81 fn validate(&self) -> Result<DiagnosticSet>;
82 #[cfg(feature = "validation")]
83 fn validate_with_options(&self, options: &ValidationOptions) -> Result<DiagnosticSet>;
84}
85
86#[cfg(any(feature = "template", feature = "validation"))]
87impl ProtoExt for Proto {
88 #[cfg(feature = "template")]
89 fn render(&self, options: &RenderOptions) -> Result<String> {
90 Ok(webots_proto_template::render(self, options)?)
91 }
92
93 #[cfg(feature = "validation")]
94 fn validate(&self) -> Result<DiagnosticSet> {
95 self.validate_with_options(&ValidationOptions::new())
96 }
97
98 #[cfg(feature = "validation")]
99 fn validate_with_options(&self, options: &ValidationOptions) -> Result<DiagnosticSet> {
100 validate_with_options(self, options)
101 }
102}
103
104#[cfg(feature = "validation")]
105pub fn validate(proto: &Proto) -> Result<DiagnosticSet> {
106 validate_with_options(proto, &ValidationOptions::new())
107}
108
109#[cfg(feature = "validation")]
110pub fn validate_with_options(proto: &Proto, options: &ValidationOptions) -> Result<DiagnosticSet> {
111 let mut diagnostics = schema::validate(proto);
112
113 if options.local_externproto_naming
114 && let Some(source_path) = &proto.source_path
115 {
116 diagnostics.extend(imports::validate_local_externproto_naming(
117 source_path,
118 proto,
119 )?);
120 }
121
122 if options.runtime_semantics
123 && let Some(source_path) = &proto.source_path
124 {
125 let content = proto
126 .source_content
127 .clone()
128 .map(Ok)
129 .unwrap_or_else(|| std::fs::read_to_string(source_path).map_err(Error::from))?;
130
131 if let Some(base_path) = source_path.parent() {
132 let expanded_root = ProtoResolver::new(ResolveOptions::new())
133 .to_root_node(&content, Some(base_path))?;
134 diagnostics.extend(schema::validate_runtime_semantics(&expanded_root));
135 }
136 }
137
138 Ok(dedupe_diagnostics(diagnostics))
139}
140
141#[cfg(feature = "validation")]
142fn dedupe_diagnostics(diagnostics: DiagnosticSet) -> DiagnosticSet {
143 let mut deduped = DiagnosticSet::new();
144 let mut seen = HashSet::new();
145 for diagnostic in diagnostics.iter() {
146 if seen.insert(diagnostic.clone()) {
147 deduped.add(diagnostic.clone());
148 }
149 }
150 deduped
151}