1use crate::ir::{classic, v4};
8use crate::metadata::ObjectTerm;
9use crate::naming::FQName;
10use serde_json::Value;
11use std::collections::{HashMap, HashSet};
12
13mod declarations;
14use declarations::{
15 classic_path, collect_v3_definitions, collect_v3_specifications, collect_v4_definitions,
16 collect_v4_specifications, v3_shape, v4_shape,
17};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum DataValueErrorKind {
22 Mismatch,
24 UnknownConstructor,
26 UnsupportedType,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
32#[error("{message} at {path}")]
33pub struct DataValueError {
34 pub kind: DataValueErrorKind,
36 pub path: String,
38 pub message: String,
40}
41
42fn error(kind: DataValueErrorKind, path: &str, message: impl Into<String>) -> DataValueError {
43 DataValueError {
44 kind,
45 path: path.to_owned(),
46 message: message.into(),
47 }
48}
49
50fn mismatch(path: &str, message: impl Into<String>) -> DataValueError {
51 error(DataValueErrorKind::Mismatch, path, message)
52}
53
54fn unsupported(path: &str, message: impl Into<String>) -> DataValueError {
55 error(DataValueErrorKind::UnsupportedType, path, message)
56}
57
58#[derive(Clone, PartialEq, Eq, Hash)]
59enum Shape {
60 Unit,
61 Variable(String),
62 Reference(String, Vec<Self>),
63 Record(Vec<(String, Self)>),
64 Tuple(Vec<Self>),
65 Unsupported(&'static str),
66}
67
68#[derive(Clone)]
69enum Definition {
70 Alias {
71 params: Vec<String>,
72 body: Shape,
73 },
74 Custom {
75 params: Vec<String>,
76 constructors: HashMap<String, Vec<Shape>>,
77 },
78 Unsupported(&'static str),
79}
80
81pub struct DataValueValidator {
110 definitions: HashMap<String, Definition>,
111}
112
113impl DataValueValidator {
114 pub fn v4(distribution: &v4::Distribution) -> Result<Self, DataValueError> {
116 let mut definitions = HashMap::new();
117 match distribution {
118 v4::Distribution::Library(library) => {
119 collect_v4_definitions(
120 &mut definitions,
121 &library.package_name.to_canonical_string(),
122 &library.def,
123 )?;
124 for (package, spec) in &library.dependencies {
125 collect_v4_specifications(&mut definitions, package, spec)?;
126 }
127 }
128 v4::Distribution::Specs(specs) => {
129 collect_v4_specifications(
130 &mut definitions,
131 &specs.package_name.to_canonical_string(),
132 &specs.spec,
133 )?;
134 for (package, spec) in &specs.dependencies {
135 collect_v4_specifications(&mut definitions, package, spec)?;
136 }
137 }
138 v4::Distribution::Application(application) => {
139 collect_v4_definitions(
140 &mut definitions,
141 &application.package_name.to_canonical_string(),
142 &application.def,
143 )?;
144 for (package, def) in &application.dependencies {
145 collect_v4_definitions(&mut definitions, package, def)?;
146 }
147 }
148 }
149 Ok(Self { definitions })
150 }
151
152 pub fn v3(distribution: &classic::Distribution) -> Result<Self, DataValueError> {
154 if distribution.format_version != 3 {
155 return Err(unsupported("$", "data type IR must be V3"));
156 }
157 let mut definitions = HashMap::new();
158 let dependencies = match &distribution.distribution {
159 classic::DistributionBody::Library(package, dependencies, definition) => {
160 collect_v3_definitions(&mut definitions, &classic_path(package), definition)?;
161 dependencies
162 }
163 classic::DistributionBody::Specs(package, dependencies, specification) => {
164 collect_v3_specifications(&mut definitions, &classic_path(package), specification)?;
165 dependencies
166 }
167 };
168 for (package, specification) in dependencies {
169 collect_v3_specifications(&mut definitions, &classic_path(package), specification)?;
170 }
171 Ok(Self { definitions })
172 }
173
174 pub fn validate_v4_data(&self, ty: &v4::Type, value: &Value) -> Result<(), DataValueError> {
176 self.validate(&v4_shape(ty), value, &HashMap::new(), "$", 0)
177 }
178
179 pub fn validate_v3_data(
181 &self,
182 ty: &classic::Type<classic::Attrs>,
183 value: &Value,
184 ) -> Result<(), DataValueError> {
185 self.validate(&v3_shape(ty), value, &HashMap::new(), "$", 0)
186 }
187
188 pub fn validate_v4_object(
190 &self,
191 ty: &v4::Type,
192 object: &ObjectTerm,
193 ) -> Result<(), DataValueError> {
194 match object {
195 ObjectTerm::Value(value) => self.validate_v4_data(ty, value.value()),
196 ObjectTerm::NodeRef(_) => Err(mismatch("$", "expected data, found node reference")),
197 }
198 }
199
200 pub fn validate_reference(&self, name: &FQName, value: &Value) -> Result<(), DataValueError> {
202 self.validate(
203 &Shape::Reference(name.to_canonical_string(), vec![]),
204 value,
205 &HashMap::new(),
206 "$",
207 0,
208 )
209 }
210
211 fn validate(
212 &self,
213 shape: &Shape,
214 value: &Value,
215 vars: &HashMap<String, Shape>,
216 path: &str,
217 depth: usize,
218 ) -> Result<(), DataValueError> {
219 if depth > 128 {
220 return Err(unsupported(path, "data type/value nesting exceeds 128"));
221 }
222 let next = depth + 1;
223 match shape {
224 Shape::Unit if value.is_null() => Ok(()),
225 Shape::Unit => Err(mismatch(path, "expected unit (null)")),
226 Shape::Variable(name) => {
227 let bound = vars
228 .get(name)
229 .ok_or_else(|| unsupported(path, format!("unbound type variable {name}")))?;
230 self.validate(bound, value, vars, path, next)
231 }
232 Shape::Reference(name, args) => {
233 if let Some((module, local)) = sdk_type(name) {
234 return self.validate_sdk((module, local), args, value, vars, path, next);
235 }
236 let definition = self.definitions.get(name).ok_or_else(|| {
237 unsupported(
238 path,
239 format!("referenced Morphir type {name} is unavailable"),
240 )
241 })?;
242 match definition {
243 Definition::Alias { params, body } => {
244 let bound = bind_type_args(params, args, vars, path)?;
245 self.validate(body, value, &bound, path, next)
246 }
247 Definition::Custom {
248 params,
249 constructors,
250 } => {
251 let bound = bind_type_args(params, args, vars, path)?;
252 let items = value
253 .as_array()
254 .ok_or_else(|| mismatch(path, "expected constructor array"))?;
255 let tag = items.first().and_then(Value::as_str).ok_or_else(|| {
256 mismatch(path, "constructor array needs a string tag")
257 })?;
258 let fields = constructors.get(tag).ok_or_else(|| {
259 error(
260 DataValueErrorKind::UnknownConstructor,
261 path,
262 format!("unknown constructor {tag} for {name}"),
263 )
264 })?;
265 if fields.len() + 1 != items.len() {
266 return Err(mismatch(
267 path,
268 format!("constructor {tag} expects {} arguments", fields.len()),
269 ));
270 }
271 for (index, (field_type, field_value)) in
272 fields.iter().zip(&items[1..]).enumerate()
273 {
274 self.validate(
275 field_type,
276 field_value,
277 &bound,
278 &format!("{path}[{}]", index + 1),
279 next,
280 )?;
281 }
282 Ok(())
283 }
284 Definition::Unsupported(kind) => Err(unsupported(
285 path,
286 format!("{kind} has no closed JSON data mapping"),
287 )),
288 }
289 }
290 Shape::Record(fields) => {
291 let members = value
292 .as_object()
293 .ok_or_else(|| mismatch(path, "expected record object"))?;
294 for (name, field_type) in fields {
295 let field = members
296 .get(name)
297 .ok_or_else(|| mismatch(path, format!("missing field {name}")))?;
298 self.validate(field_type, field, vars, &member_path(path, name), next)?;
299 }
300 if members.len() != fields.len() {
301 return Err(mismatch(path, "record has unknown fields"));
302 }
303 Ok(())
304 }
305 Shape::Tuple(elements) => {
306 let items = value
307 .as_array()
308 .ok_or_else(|| mismatch(path, "expected tuple array"))?;
309 if items.len() != elements.len() {
310 return Err(mismatch(
311 path,
312 format!("tuple expects {} elements", elements.len()),
313 ));
314 }
315 for (index, (item_type, item)) in elements.iter().zip(items).enumerate() {
316 self.validate(item_type, item, vars, &format!("{path}[{index}]"), next)?;
317 }
318 Ok(())
319 }
320 Shape::Unsupported(kind) => Err(unsupported(
321 path,
322 format!("{kind} has no closed JSON data mapping"),
323 )),
324 }
325 }
326
327 fn validate_sdk(
328 &self,
329 sdk: (&str, &str),
330 args: &[Shape],
331 value: &Value,
332 vars: &HashMap<String, Shape>,
333 path: &str,
334 next: usize,
335 ) -> Result<(), DataValueError> {
336 match (sdk.0, sdk.1, args) {
337 ("basics", "bool", []) => expect_primitive(value.is_boolean(), path, "Bool"),
338 ("basics", "int", []) => expect_primitive(
339 v4::serde_tagged::integer_from_json(value).is_some(),
340 path,
341 "Int",
342 ),
343 ("basics", "float", []) => expect_primitive(value.is_number(), path, "Float"),
344 ("string", "string", []) => expect_primitive(value.is_string(), path, "String"),
345 ("char", "char", []) => expect_primitive(
346 value.as_str().is_some_and(|text| text.chars().count() == 1),
347 path,
348 "Char",
349 ),
350 ("decimal", "decimal", []) => expect_primitive(
351 value
352 .as_str()
353 .is_some_and(crate::ir::decimal::is_decimal_lexeme),
354 path,
355 "Decimal",
356 ),
357 ("list", "list", [item_type]) => {
358 let items = value
359 .as_array()
360 .ok_or_else(|| mismatch(path, "expected list"))?;
361 for (index, item) in items.iter().enumerate() {
362 self.validate(item_type, item, vars, &format!("{path}[{index}]"), next)?;
363 }
364 Ok(())
365 }
366 ("maybe", "maybe", [_]) if value.is_null() => Ok(()),
367 ("maybe", "maybe", [item_type]) => self.validate(item_type, value, vars, path, next),
368 ("dict", "dict", [key_type, value_type]) => {
369 if !self.is_string_key_type(key_type, vars, path, &mut HashSet::new(), 0)? {
370 return Err(unsupported(
371 path,
372 "Dict keys other than String have no JSON object mapping",
373 ));
374 }
375 let entries = value
376 .as_object()
377 .ok_or_else(|| mismatch(path, "expected Dict String object"))?;
378 for (key, entry) in entries {
379 self.validate(value_type, entry, vars, &member_path(path, key), next)?;
380 }
381 Ok(())
382 }
383 _ => Err(unsupported(
384 path,
385 format!(
386 "SDK type {}#{} has no closed JSON data mapping with {} arguments",
387 sdk.0,
388 sdk.1,
389 args.len()
390 ),
391 )),
392 }
393 }
394
395 fn is_string_key_type(
396 &self,
397 key_type: &Shape,
398 vars: &HashMap<String, Shape>,
399 path: &str,
400 visiting: &mut HashSet<(String, Vec<Shape>)>,
401 depth: usize,
402 ) -> Result<bool, DataValueError> {
403 if depth > 128 {
404 return Err(unsupported(path, "Dict key alias nesting exceeds 128"));
405 }
406 let resolved = substitute_type(key_type, vars, &mut HashSet::new(), path)?;
407 let Shape::Reference(name, args) = resolved else {
408 return Ok(false);
409 };
410 if let Some((module, local)) = sdk_type(&name) {
411 return Ok((module, local) == ("string", "string") && args.is_empty());
412 }
413 let Some(Definition::Alias { params, body }) = self.definitions.get(&name) else {
414 return Ok(false);
415 };
416 let instance = (name.clone(), args.clone());
417 if !visiting.insert(instance.clone()) {
418 return Err(unsupported(path, format!("cyclic Dict key alias {name}")));
419 }
420 let result = bind_type_args(params, &args, vars, path)
421 .and_then(|bound| self.is_string_key_type(body, &bound, path, visiting, depth + 1));
422 visiting.remove(&instance);
423 result
424 }
425}
426
427fn expect_primitive(valid: bool, path: &str, name: &str) -> Result<(), DataValueError> {
428 if valid {
429 Ok(())
430 } else {
431 Err(mismatch(path, format!("expected {name}")))
432 }
433}
434
435fn member_path(path: &str, name: &str) -> String {
436 let mut bytes = name.bytes();
437 let safe_start = bytes
438 .next()
439 .is_some_and(|first| first.is_ascii_alphabetic() || first == b'_');
440 if safe_start && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_') {
441 format!("{path}.{name}")
442 } else {
443 let quoted = serde_json::to_string(name).expect("JSON member names serialize");
444 format!("{path}[{quoted}]")
445 }
446}
447
448fn sdk_type(name: &str) -> Option<(&str, &str)> {
449 let (package, rest) = name.split_once(':')?;
450 if package != "morphir/SDK" && package != "morphir/s-d-k" {
451 return None;
452 }
453 rest.split_once('#')
454}
455
456fn bind_type_args(
457 params: &[String],
458 args: &[Shape],
459 outer: &HashMap<String, Shape>,
460 path: &str,
461) -> Result<HashMap<String, Shape>, DataValueError> {
462 if params.len() != args.len() {
463 return Err(unsupported(
464 path,
465 format!("type expects {} arguments", params.len()),
466 ));
467 }
468 let arguments = args
469 .iter()
470 .map(|arg| substitute_type(arg, outer, &mut HashSet::new(), path))
471 .collect::<Result<Vec<_>, _>>()?;
472 let mut bound = outer.clone();
473 bound.extend(params.iter().cloned().zip(arguments));
474 Ok(bound)
475}
476
477fn substitute_type(
478 shape: &Shape,
479 vars: &HashMap<String, Shape>,
480 visiting: &mut HashSet<String>,
481 path: &str,
482) -> Result<Shape, DataValueError> {
483 match shape {
484 Shape::Variable(name) => {
485 let Some(bound) = vars.get(name) else {
486 return Ok(shape.clone());
487 };
488 if !visiting.insert(name.clone()) {
489 return Err(unsupported(path, format!("cyclic type variable {name}")));
490 }
491 let resolved = substitute_type(bound, vars, visiting, path);
492 visiting.remove(name);
493 resolved
494 }
495 Shape::Reference(name, args) => Ok(Shape::Reference(
496 name.clone(),
497 args.iter()
498 .map(|arg| substitute_type(arg, vars, visiting, path))
499 .collect::<Result<_, _>>()?,
500 )),
501 Shape::Record(fields) => Ok(Shape::Record(
502 fields
503 .iter()
504 .map(|(name, shape)| {
505 substitute_type(shape, vars, visiting, path).map(|shape| (name.clone(), shape))
506 })
507 .collect::<Result<_, _>>()?,
508 )),
509 Shape::Tuple(elements) => Ok(Shape::Tuple(
510 elements
511 .iter()
512 .map(|shape| substitute_type(shape, vars, visiting, path))
513 .collect::<Result<_, _>>()?,
514 )),
515 Shape::Unit | Shape::Unsupported(_) => Ok(shape.clone()),
516 }
517}