diff --git a/crates/aiken-project/src/blueprint/definitions.rs b/crates/aiken-project/src/blueprint/definitions.rs index 047e3f68..b72b3a3c 100644 --- a/crates/aiken-project/src/blueprint/definitions.rs +++ b/crates/aiken-project/src/blueprint/definitions.rs @@ -53,6 +53,12 @@ impl Definitions { self.inner.remove(reference.as_key()); } + /// Insert a new definition + pub fn insert(&mut self, reference: &Reference, schema: T) { + self.inner + .insert(reference.as_key().to_string(), Some(schema)); + } + /// Register a new definition only if it doesn't exist. This uses a strategy of /// mark-and-insert such that recursive definitions are only built once. pub fn register( diff --git a/crates/aiken-project/src/blueprint/error.rs b/crates/aiken-project/src/blueprint/error.rs index 25e4f331..27c7d5f4 100644 --- a/crates/aiken-project/src/blueprint/error.rs +++ b/crates/aiken-project/src/blueprint/error.rs @@ -6,6 +6,7 @@ use aiken_lang::ast::Span; use miette::{Diagnostic, NamedSource}; use owo_colors::{OwoColorize, Stream::Stdout}; use std::fmt::Debug; +use uplc::ast::{DeBruijn, Term}; #[derive(Debug, thiserror::Error, Diagnostic)] pub enum Error { @@ -65,11 +66,25 @@ pub enum Error { #[error("I caught a parameter application that seems off.")] #[diagnostic(code("aiken::blueprint::apply::mismatch"))] #[diagnostic(help( - "When applying parameters to a validator, I control that the shape of the parameter you give me matches what is specified in the blueprint. Unfortunately, schemas didn't match in this case.\n\nI am expecting the following:\n\n{}But I've inferred the following schema from your input:\n\n{}", - serde_json::to_string_pretty(&expected).unwrap().if_supports_color(Stdout, |s| s.green()), - serde_json::to_string_pretty(&inferred).unwrap().if_supports_color(Stdout, |s| s.red()), + "When applying parameters to a validator, I control that the shape of the parameter you give me matches what is specified in the blueprint. Unfortunately, schemas didn't match in this case.\n\nI am expecting something of the shape:\n\n{}Which I couldn't match against the following term:\n\n{}\n\nNote that this may only represent part of a bigger whole.", + serde_json::to_string_pretty(&schema).unwrap().if_supports_color(Stdout, |s| s.green()), + term.to_pretty().if_supports_color(Stdout, |s| s.red()), ))] - SchemaMismatch { expected: Schema, inferred: Schema }, + SchemaMismatch { + schema: Schema, + term: Term, + }, + + #[error( + "I discovered a discrepancy of elements between a given tuple and its declared schema." + )] + #[diagnostic(code("aiken::blueprint::apply::tuple::mismatch"))] + #[diagnostic(help( + "When validating a list-like schema with multiple 'items' schemas, I try to match each element of the instance with each item schema (by their position). Hence, I expect to be as many items in the declared schema ({expected}) than there are items in the instance ({found}).", + expected = expected.if_supports_color(Stdout, |s| s.green()), + found = found.if_supports_color(Stdout, |s| s.red()), + ))] + TupleItemsMismatch { expected: usize, found: usize }, } unsafe impl Send for Error {} diff --git a/crates/aiken-project/src/blueprint/mod.rs b/crates/aiken-project/src/blueprint/mod.rs index 11ee91a1..168953c8 100644 --- a/crates/aiken-project/src/blueprint/mod.rs +++ b/crates/aiken-project/src/blueprint/mod.rs @@ -1,5 +1,6 @@ pub mod definitions; pub mod error; +pub mod parameter; pub mod schema; pub mod validator; diff --git a/crates/aiken-project/src/blueprint/parameter.rs b/crates/aiken-project/src/blueprint/parameter.rs new file mode 100644 index 00000000..fe682077 --- /dev/null +++ b/crates/aiken-project/src/blueprint/parameter.rs @@ -0,0 +1,478 @@ +use super::{ + definitions::{Definitions, Reference}, + error::Error, + schema::{Annotated, Constructor, Data, Declaration, Items, Schema}, +}; +use std::{iter, ops::Deref, rc::Rc}; +use uplc::{ + ast::{Constant, Data as UplcData, DeBruijn, Term}, + PlutusData, +}; + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub struct Parameter { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + + pub schema: Reference, +} + +type Instance = Term; + +impl From for Parameter { + fn from(schema: Reference) -> Parameter { + Parameter { + title: None, + schema, + } + } +} + +impl Parameter { + pub fn validate( + &self, + definitions: &Definitions>, + term: &Instance, + ) -> Result<(), Error> { + let schema = &definitions + .lookup(&self.schema) + .map(Ok) + .unwrap_or_else(|| { + Err(Error::UnresolvedSchemaReference { + reference: self.schema.clone(), + }) + })? + .annotated; + + validate_schema(schema, definitions, term) + } +} + +fn validate_schema( + schema: &Schema, + definitions: &Definitions>, + term: &Instance, +) -> Result<(), Error> { + match schema { + Schema::Data(data) => validate_data(data, definitions, term), + + Schema::Unit => expect_unit(term), + + Schema::Integer => expect_integer(term), + + Schema::Bytes => expect_bytes(term), + + Schema::String => expect_string(term), + + Schema::Boolean => expect_boolean(term), + + Schema::Pair(left, right) => { + let (term_left, term_right) = expect_pair(term)?; + + let left = + left.schema(definitions) + .ok_or_else(|| Error::UnresolvedSchemaReference { + reference: left.reference().unwrap().clone(), + })?; + validate_schema(left, definitions, &term_left)?; + + let right = + right + .schema(definitions) + .ok_or_else(|| Error::UnresolvedSchemaReference { + reference: right.reference().unwrap().clone(), + })?; + validate_schema(right, definitions, &term_right)?; + + Ok(()) + } + + Schema::List(Items::One(item)) => { + let terms = expect_list(term)?; + + let item = + item.schema(definitions) + .ok_or_else(|| Error::UnresolvedSchemaReference { + reference: item.reference().unwrap().clone(), + })?; + + for ref term in terms { + validate_schema(item, definitions, term)?; + } + + Ok(()) + } + + Schema::List(Items::Many(items)) => { + let terms = expect_list(term)?; + + let items = items + .iter() + .map(|item| { + item.schema(definitions) + .ok_or_else(|| Error::UnresolvedSchemaReference { + reference: item.reference().unwrap().clone(), + }) + }) + .collect::, _>>()?; + + if terms.len() != items.len() { + return Err(Error::TupleItemsMismatch { + expected: items.len(), + found: terms.len(), + }); + } + + for (item, ref term) in iter::zip(items, terms) { + validate_schema(item, definitions, term)?; + } + + Ok(()) + } + } +} + +fn validate_data( + data: &Data, + definitions: &Definitions>, + term: &Instance, +) -> Result<(), Error> { + match data { + Data::Opaque => expect_data(term), + + Data::Integer => expect_data_integer(term), + + Data::Bytes => expect_data_bytes(term), + + Data::List(Items::One(item)) => { + let terms = expect_data_list(term)?; + + let item = + item.schema(definitions) + .ok_or_else(|| Error::UnresolvedSchemaReference { + reference: item.reference().unwrap().clone(), + })?; + + for ref term in terms { + validate_data(item, definitions, term)?; + } + + Ok(()) + } + + Data::List(Items::Many(items)) => { + let terms = expect_data_list(term)?; + + let items = items + .iter() + .map(|item| { + item.schema(definitions) + .ok_or_else(|| Error::UnresolvedSchemaReference { + reference: item.reference().unwrap().clone(), + }) + }) + .collect::, _>>()?; + + if terms.len() != items.len() { + return Err(Error::TupleItemsMismatch { + expected: items.len(), + found: terms.len(), + }); + } + + for (item, ref term) in iter::zip(items, terms) { + validate_data(item, definitions, term)?; + } + + Ok(()) + } + + Data::Map(keys, values) => { + let terms = expect_data_map(term)?; + + let keys = + keys.schema(definitions) + .ok_or_else(|| Error::UnresolvedSchemaReference { + reference: keys.reference().unwrap().clone(), + })?; + + let values = + values + .schema(definitions) + .ok_or_else(|| Error::UnresolvedSchemaReference { + reference: values.reference().unwrap().clone(), + })?; + + for (ref k, ref v) in terms { + validate_data(keys, definitions, k)?; + validate_data(values, definitions, v)?; + } + + Ok(()) + } + + Data::AnyOf(constructors) => { + let constructors: Vec<(usize, Vec<&Data>)> = constructors + .iter() + .map(|constructor| { + constructor + .annotated + .fields + .iter() + .map(|field| { + field.annotated.schema(definitions).ok_or_else(|| { + Error::UnresolvedSchemaReference { + reference: field.annotated.reference().unwrap().clone(), + } + }) + }) + .collect::>() + .map(|fields| (constructor.annotated.index, fields)) + }) + .collect::>()?; + + for (index, fields_schema) in constructors.iter() { + if let Ok(fields) = expect_data_constr(term, *index) { + if fields_schema.len() != fields.len() { + panic!("fields length different"); + } + + for (instance, schema) in iter::zip(fields, fields_schema) { + validate_data(schema, definitions, &instance)?; + } + + return Ok(()); + } + } + + Err(Error::SchemaMismatch { + schema: Schema::Data(Data::AnyOf( + constructors + .iter() + .map(|(index, fields)| { + Constructor { + index: *index, + fields: fields + .iter() + .map(|_| Declaration::Inline(Box::new(Data::Opaque)).into()) + .collect(), + } + .into() + }) + .collect(), + )), + term: term.clone(), + }) + } + } +} + +fn expect_data(term: &Instance) -> Result<(), Error> { + if let Term::Constant(constant) = term { + if matches!(constant.deref(), Constant::Data(..)) { + return Ok(()); + } + } + + Err(Error::SchemaMismatch { + schema: Schema::Data(Data::Opaque), + term: term.clone(), + }) +} + +fn expect_data_integer(term: &Instance) -> Result<(), Error> { + if let Term::Constant(constant) = term { + if let Constant::Data(data) = constant.deref() { + if matches!(data, PlutusData::BigInt(..)) { + return Ok(()); + } + } + } + + Err(Error::SchemaMismatch { + schema: Schema::Data(Data::Integer), + term: term.clone(), + }) +} + +fn expect_data_bytes(term: &Instance) -> Result<(), Error> { + if let Term::Constant(constant) = term { + if let Constant::Data(data) = constant.deref() { + if matches!(data, PlutusData::BoundedBytes(..)) { + return Ok(()); + } + } + } + + Err(Error::SchemaMismatch { + schema: Schema::Data(Data::Bytes), + term: term.clone(), + }) +} + +fn expect_data_list(term: &Instance) -> Result, Error> { + if let Term::Constant(constant) = term { + if let Constant::Data(PlutusData::Array(elems)) = constant.deref() { + return Ok(elems + .iter() + .map(|elem| Term::Constant(Rc::new(Constant::Data(elem.to_owned())))) + .collect()); + } + } + + let inner_schema = Items::One(Declaration::Inline(Box::new(Data::Opaque))); + + Err(Error::SchemaMismatch { + schema: Schema::Data(Data::List(inner_schema)), + term: term.clone(), + }) +} + +fn expect_data_map(term: &Instance) -> Result, Error> { + if let Term::Constant(constant) = term { + if let Constant::Data(PlutusData::Map(pairs)) = constant.deref() { + return Ok(pairs + .iter() + .map(|(k, v)| { + ( + Term::Constant(Rc::new(Constant::Data(k.to_owned()))), + Term::Constant(Rc::new(Constant::Data(v.to_owned()))), + ) + }) + .collect()); + } + } + + let key_schema = Declaration::Inline(Box::new(Data::Opaque)); + let value_schema = Declaration::Inline(Box::new(Data::Opaque)); + + Err(Error::SchemaMismatch { + schema: Schema::Data(Data::Map(key_schema, value_schema)), + term: term.clone(), + }) +} + +fn expect_data_constr(term: &Instance, index: usize) -> Result, Error> { + if let Term::Constant(constant) = term { + if let Constant::Data(PlutusData::Constr(constr)) = constant.deref() { + if let PlutusData::Constr(expected) = UplcData::constr(index as u64, vec![]) { + if expected.tag == constr.tag && expected.any_constructor == constr.any_constructor + { + return Ok(constr + .fields + .iter() + .map(|field| Term::Constant(Rc::new(Constant::Data(field.to_owned())))) + .collect()); + } + } + } + } + + Err(Error::SchemaMismatch { + schema: Schema::Data(Data::AnyOf(vec![Constructor { + index, + fields: vec![], + } + .into()])), + term: term.clone(), + }) +} + +fn expect_unit(term: &Instance) -> Result<(), Error> { + if let Term::Constant(constant) = term { + if matches!(constant.deref(), Constant::Unit) { + return Ok(()); + } + } + + Err(Error::SchemaMismatch { + schema: Schema::Unit, + term: term.clone(), + }) +} + +fn expect_integer(term: &Instance) -> Result<(), Error> { + if let Term::Constant(constant) = term { + if matches!(constant.deref(), Constant::Integer(..)) { + return Ok(()); + } + } + + Err(Error::SchemaMismatch { + schema: Schema::Integer, + term: term.clone(), + }) +} + +fn expect_bytes(term: &Instance) -> Result<(), Error> { + if let Term::Constant(constant) = term { + if matches!(constant.deref(), Constant::ByteString(..)) { + return Ok(()); + } + } + + Err(Error::SchemaMismatch { + schema: Schema::Bytes, + term: term.clone(), + }) +} + +fn expect_string(term: &Instance) -> Result<(), Error> { + if let Term::Constant(constant) = term { + if matches!(constant.deref(), Constant::String(..)) { + return Ok(()); + } + } + + Err(Error::SchemaMismatch { + schema: Schema::String, + term: term.clone(), + }) +} + +fn expect_boolean(term: &Instance) -> Result<(), Error> { + if let Term::Constant(constant) = term { + if matches!(constant.deref(), Constant::Bool(..)) { + return Ok(()); + } + } + + Err(Error::SchemaMismatch { + schema: Schema::Boolean, + term: term.clone(), + }) +} + +fn expect_pair(term: &Instance) -> Result<(Instance, Instance), Error> { + if let Term::Constant(constant) = term { + if let Constant::ProtoPair(_, _, left, right) = constant.deref() { + return Ok((Term::Constant(left.clone()), Term::Constant(right.clone()))); + } + } + + let left_schema = Declaration::Inline(Box::new(Schema::Data(Data::Opaque))); + let right_schema = Declaration::Inline(Box::new(Schema::Data(Data::Opaque))); + + Err(Error::SchemaMismatch { + schema: Schema::Pair(left_schema, right_schema), + term: term.clone(), + }) +} + +fn expect_list(term: &Instance) -> Result, Error> { + if let Term::Constant(constant) = term { + if let Constant::ProtoList(_, elems) = constant.deref() { + return Ok(elems + .iter() + .map(|elem| Term::Constant(Rc::new(elem.to_owned()))) + .collect()); + } + } + + let inner_schema = Items::One(Declaration::Inline(Box::new(Schema::Data(Data::Opaque)))); + + Err(Error::SchemaMismatch { + schema: Schema::List(inner_schema), + term: term.clone(), + }) +} diff --git a/crates/aiken-project/src/blueprint/schema.rs b/crates/aiken-project/src/blueprint/schema.rs index 6ba0700b..c35202a3 100644 --- a/crates/aiken-project/src/blueprint/schema.rs +++ b/crates/aiken-project/src/blueprint/schema.rs @@ -13,7 +13,6 @@ use serde::{ }; use serde_json as json; use std::{collections::HashMap, fmt, ops::Deref, sync::Arc}; -use uplc::ast::Term; // NOTE: Can be anything BUT 0 pub const REDEEMER_DISCRIMINANT: usize = 1; @@ -35,6 +34,43 @@ pub enum Declaration { Inline(Box), } +impl<'a, T> Declaration { + pub fn reference(&'a self) -> Option<&'a Reference> { + match self { + Declaration::Referenced(reference) => Some(reference), + Declaration::Inline(..) => None, + } + } + + fn try_schema( + &'a self, + definitions: &'a Definitions>, + cast: fn(&'a Schema) -> Option<&'a T>, + ) -> Option<&'a T> { + match self { + Declaration::Inline(inner) => Some(inner.deref()), + Declaration::Referenced(reference) => definitions + .lookup(reference) + .and_then(|s| cast(&s.annotated)), + } + } +} + +impl<'a> Declaration { + pub fn schema(&'a self, definitions: &'a Definitions>) -> Option<&'a Data> { + self.try_schema(definitions, |s| match s { + Schema::Data(data) => Some(data), + _ => None, + }) + } +} + +impl<'a> Declaration { + pub fn schema(&'a self, definitions: &'a Definitions>) -> Option<&'a Schema> { + self.try_schema(definitions, Some) + } +} + /// A schema for low-level UPLC primitives. #[derive(Debug, PartialEq, Eq, Clone)] pub enum Schema { @@ -71,7 +107,6 @@ pub enum Items { #[derive(Debug, PartialEq, Eq, Clone)] pub struct Constructor { pub index: usize, - // TODO: Generalize to work with either Reference or Data pub fields: Vec>>, } @@ -85,50 +120,6 @@ impl From for Annotated { } } -impl<'a, T> TryFrom<&'a Term> for Schema { - type Error = &'a str; - - fn try_from(term: &'a Term) -> Result { - use uplc::{ast::Constant, Constr, PlutusData}; - - match term { - Term::Constant(constant) => match constant.deref() { - Constant::Integer(..) => Ok(Schema::Integer), - Constant::Bool(..) => Ok(Schema::Boolean), - Constant::ByteString(..) => Ok(Schema::Bytes), - Constant::String(..) => Ok(Schema::String), - Constant::Unit => Ok(Schema::Unit), - Constant::ProtoList{..} => todo!("can't convert from ProtoList to Schema; note that you probably want to use a Data's list instead anyway."), - Constant::ProtoPair{..} => todo!("can't convert from ProtoPair to Schema; note that you probably want to use a Data's list instead anyway."), - Constant::Data(data) => Ok(Schema::Data(match data { - PlutusData::BigInt(..) => { - Data::Integer - } - PlutusData::BoundedBytes(..) => { - Data::Bytes - } - PlutusData::Array(elems) => { - todo!() - } - PlutusData::Map(keyValuePair) => { - todo!() - } - PlutusData::Constr(Constr{ tag, fields, any_constructor }) => { - todo!() - } - })) - }, - Term::Delay(..) - | Term::Lambda { .. } - | Term::Var(..) - | Term::Apply { .. } - | Term::Force(..) - | Term::Error - | Term::Builtin(..) => Err("not a UPLC constant"), - } - } -} - impl Annotated { pub fn as_wrapped_redeemer( definitions: &mut Definitions>, diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs index 89310cac..7d8d87d5 100644 --- a/crates/aiken-project/src/blueprint/validator.rs +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -1,18 +1,17 @@ use super::{ - definitions::{Definitions, Reference}, + definitions::Definitions, error::Error, + parameter::Parameter, schema::{Annotated, Schema}, }; use crate::module::{CheckedModule, CheckedModules}; use aiken_lang::{ ast::{TypedArg, TypedFunction, TypedValidator}, - builtins, gen_uplc::CodeGenerator, }; use miette::NamedSource; use serde; -use std::{borrow::Borrow, collections::HashMap}; -use uplc::ast::{Constant, DeBruijn, Program, Term}; +use uplc::ast::{DeBruijn, Program, Term}; #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct Validator { @@ -22,13 +21,13 @@ pub struct Validator { pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub datum: Option, + pub datum: Option, - pub redeemer: Argument, + pub redeemer: Parameter, #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)] - pub parameters: Vec, + pub parameters: Vec, #[serde(flatten)] pub program: Program, @@ -38,56 +37,6 @@ pub struct Validator { pub definitions: Definitions>, } -#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] -pub struct Argument { - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - - pub schema: Reference, -} - -impl From for Argument { - fn from(schema: Reference) -> Argument { - Argument { - title: None, - schema, - } - } -} - -impl Argument { - pub fn validate( - &self, - definitions: &Definitions>, - term: &Term, - ) -> Result<(), Error> { - let expected_schema = &definitions - .lookup(&self.schema) - .map(Ok) - .unwrap_or_else(|| { - Err(Error::UnresolvedSchemaReference { - reference: self.schema.clone(), - }) - })? - .annotated; - - let inferred_schema: Schema = - term.try_into() - .map_err(|hint: &str| Error::UnableToInferArgumentSchema { - hint: hint.to_owned(), - })?; - - if expected_schema != &inferred_schema { - Err(Error::SchemaMismatch { - expected: expected_schema.to_owned(), - inferred: inferred_schema, - }) - } else { - Ok(()) - } - } -} - impl Validator { pub fn from_checked_module( modules: &CheckedModules, @@ -146,7 +95,7 @@ impl Validator { .iter() .map(|param| { Annotated::from_type(modules.into(), ¶m.tipo, &mut definitions) - .map(|schema| Argument { + .map(|schema| Parameter { title: Some(param.arg_name.get_label()), schema, }) @@ -174,7 +123,7 @@ impl Validator { ) }) .transpose()? - .map(|schema| Argument { + .map(|schema| Parameter { title: datum.map(|datum| datum.arg_name.get_label()), schema, }), @@ -187,7 +136,7 @@ impl Validator { module.code.clone(), ), }) - .map(|schema| Argument { + .map(|schema| Parameter { title: Some(redeemer.arg_name.get_label()), schema: match datum { Some(..) if is_multi_validator => Annotated::as_wrapped_redeemer( @@ -224,9 +173,9 @@ impl Validator { mod test { use super::{ super::{ - definitions::Definitions, + definitions::{Definitions, Reference}, error::Error, - schema::{Annotated, Data, Declaration, Items, Schema}, + schema::{Annotated, Constructor, Data, Declaration, Items, Schema}, }, *, }; @@ -369,18 +318,63 @@ mod test { fn fixture_definitions() -> Definitions> { let mut definitions = Definitions::new(); + // #/definitions/Int + // + // { + // "dataType": "integer" + // } definitions .register::<_, Error>(&builtins::int(), &HashMap::new(), |_| { Ok(Schema::Data(Data::Integer).into()) }) .unwrap(); + // #/definitions/ByteArray + // + // { + // "dataType": "bytes" + // } definitions .register::<_, Error>(&builtins::byte_array(), &HashMap::new(), |_| { Ok(Schema::Data(Data::Bytes).into()) }) .unwrap(); + // #/definitions/Bool + // + // { + // "anyOf": [ + // { + // "dataType": "constructor", + // "index": 0, + // "fields": [] + // }, + // { + // "dataType": "constructor", + // "index": 1, + // "fields": [] + // }, + // ] + // } + definitions.insert( + &Reference::new("Bool"), + Schema::Data(Data::AnyOf(vec![ + // False + Constructor { + index: 0, + fields: vec![], + } + .into(), + // True + Constructor { + index: 1, + fields: vec![], + } + .into(), + ])) + .into(), + ); + definitions } @@ -1165,24 +1159,263 @@ mod test { #[test] fn validate_arguments_integer() { - let term = Term::data(uplc::Data::integer(42.into())); let definitions = fixture_definitions(); - let arg = Argument { + + let term = Term::data(uplc::Data::integer(42.into())); + + let param = Parameter { title: None, schema: Reference::new("Int"), }; - assert!(matches!(arg.validate(&definitions, &term), Ok { .. })) + assert!(matches!(param.validate(&definitions, &term), Ok { .. })) } + #[test] fn validate_arguments_bytestring() { - let term = Term::data(uplc::Data::bytestring(vec![102, 111, 111])); let definitions = fixture_definitions(); - let arg = Argument { + + let term = Term::data(uplc::Data::bytestring(vec![102, 111, 111])); + + let param = Parameter { title: None, schema: Reference::new("ByteArray"), }; - assert!(matches!(arg.validate(&definitions, &term), Ok { .. })) + assert!(matches!(param.validate(&definitions, &term), Ok { .. })) + } + + #[test] + fn validate_arguments_list_inline() { + let schema = Reference::new("List$Int"); + + // #/definitions/List$Int + // + // { + // "dataType": "list", + // "items": { "dataType": "integer" } + // } + let mut definitions = fixture_definitions(); + definitions.insert( + &schema, + Schema::Data(Data::List(Items::One(Declaration::Inline(Box::new( + Data::Integer, + ))))) + .into(), + ); + + let term = Term::data(uplc::Data::list(vec![ + uplc::Data::integer(42.into()), + uplc::Data::integer(14.into()), + ])); + + let param: Parameter = schema.into(); + + assert!(matches!(param.validate(&definitions, &term), Ok { .. })) + } + + #[test] + fn validate_arguments_list_ref() { + let schema = Reference::new("List$ByteArray"); + + // #/definitions/List$ByteArray + // + // { + // "dataType": "list", + // "items": { "$ref": "#/definitions/ByteArray" } + // } + let mut definitions = fixture_definitions(); + definitions.insert( + &schema, + Schema::Data(Data::List(Items::One(Declaration::Referenced( + Reference::new("ByteArray"), + )))) + .into(), + ); + + let term = Term::data(uplc::Data::list(vec![uplc::Data::bytestring(vec![ + 102, 111, 111, + ])])); + + let param: Parameter = schema.into(); + + assert!(matches!(param.validate(&definitions, &term), Ok { .. })) + } + + #[test] + fn validate_arguments_tuple() { + let schema = Reference::new("Tuple$Int_ByteArray"); + + // #/definitions/Tuple$Int_ByteArray + // + // { + // "dataType": "list", + // "items": [ + // { "$ref": "#/definitions/Int" } + // { "$ref": "#/definitions/ByteArray" } + // ] + // } + let mut definitions = fixture_definitions(); + definitions.insert( + &schema, + Schema::Data(Data::List(Items::Many(vec![ + Declaration::Referenced(Reference::new("Int")), + Declaration::Referenced(Reference::new("ByteArray")), + ]))) + .into(), + ); + + let term = Term::data(uplc::Data::list(vec![ + uplc::Data::integer(42.into()), + uplc::Data::bytestring(vec![102, 111, 111]), + ])); + + let param: Parameter = schema.into(); + + assert!(matches!(param.validate(&definitions, &term), Ok { .. })) + } + + #[test] + fn validate_arguments_dict() { + let schema = Reference::new("Dict$ByteArray_Int"); + + // #/definitions/Dict$Int_ByteArray + // + // { + // "dataType": "map", + // "keys": { "dataType": "bytes" }, + // "values": { "dataType": "integer" } + // } + let mut definitions = fixture_definitions(); + definitions.insert( + &Reference::new("Dict$ByteArray_Int"), + Schema::Data(Data::Map( + Declaration::Inline(Box::new(Data::Bytes)), + Declaration::Inline(Box::new(Data::Integer)), + )) + .into(), + ); + + let term = Term::data(uplc::Data::map(vec![( + uplc::Data::bytestring(vec![102, 111, 111]), + uplc::Data::integer(42.into()), + )])); + + let param: Parameter = schema.into(); + + assert!(matches!(param.validate(&definitions, &term), Ok { .. })) + } + + #[test] + fn validate_arguments_constr_nullary() { + let schema = Reference::new("Bool"); + + let definitions = fixture_definitions(); + + let term = Term::data(uplc::Data::constr(1, vec![])); + + let param: Parameter = schema.into(); + + assert!(matches!(param.validate(&definitions, &term), Ok { .. })) + } + + #[test] + fn validate_arguments_constr_n_ary() { + let schema = Reference::new("Foo"); + + // #/definitions/Foo + // + // { + // "anyOf": [ + // { + // "dataType": "constructor", + // "index": 0, + // "fields": [{ + // "$ref": "#/definitions/Bool + // }] + // }, + // ] + // } + let mut definitions = fixture_definitions(); + definitions.insert( + &schema, + Schema::Data(Data::AnyOf(vec![Constructor { + index: 0, + fields: vec![Declaration::Referenced(Reference::new("Bool")).into()], + } + .into()])) + .into(), + ); + + let term = Term::data(uplc::Data::constr(0, vec![uplc::Data::constr(0, vec![])])); + + let param: Parameter = schema.into(); + + assert!(matches!(param.validate(&definitions, &term), Ok { .. })) + } + + #[test] + fn validate_arguments_constr_recursive() { + let schema = Reference::new("LinkedList$Int"); + + // #/definitions/LinkedList$Int + // + // { + // "anyOf": [ + // { + // "dataType": "constructor", + // "index": 0, + // "fields": [] + // }, + // { + // "dataType": "constructor", + // "index": 1, + // "fields": [{ + // "$ref": "#/definitions/Int + // "$ref": "#/definitions/LinkedList$Int + // }] + // }, + // ] + // } + let mut definitions = fixture_definitions(); + definitions.insert( + &schema, + Schema::Data(Data::AnyOf(vec![ + // Empty + Constructor { + index: 0, + fields: vec![], + } + .into(), + // Node + Constructor { + index: 1, + fields: vec![ + Declaration::Referenced(Reference::new("Int")).into(), + Declaration::Referenced(Reference::new("LinkedList$Int")).into(), + ], + } + .into(), + ])) + .into(), + ); + + let term = Term::data(uplc::Data::constr( + 1, + vec![ + uplc::Data::integer(14.into()), + uplc::Data::constr( + 1, + vec![ + uplc::Data::integer(42.into()), + uplc::Data::constr(0, vec![]), + ], + ), + ], + )); + + let param: Parameter = schema.into(); + + assert!(matches!(param.validate(&definitions, &term), Ok { .. })) } }