Implement blueprint schema & data validations.
This commit is contained in:
parent
ee220881b6
commit
d58ef1a079
|
@ -53,6 +53,12 @@ impl<T> Definitions<T> {
|
|||
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<F, E>(
|
||||
|
|
|
@ -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<DeBruijn>,
|
||||
},
|
||||
|
||||
#[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 {}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
pub mod definitions;
|
||||
pub mod error;
|
||||
pub mod parameter;
|
||||
pub mod schema;
|
||||
pub mod validator;
|
||||
|
||||
|
|
|
@ -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<String>,
|
||||
|
||||
pub schema: Reference,
|
||||
}
|
||||
|
||||
type Instance = Term<DeBruijn>;
|
||||
|
||||
impl From<Reference> for Parameter {
|
||||
fn from(schema: Reference) -> Parameter {
|
||||
Parameter {
|
||||
title: None,
|
||||
schema,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parameter {
|
||||
pub fn validate(
|
||||
&self,
|
||||
definitions: &Definitions<Annotated<Schema>>,
|
||||
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<Annotated<Schema>>,
|
||||
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::<Result<Vec<_>, _>>()?;
|
||||
|
||||
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<Annotated<Schema>>,
|
||||
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::<Result<Vec<_>, _>>()?;
|
||||
|
||||
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::<Result<_, _>>()
|
||||
.map(|fields| (constructor.annotated.index, fields))
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
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<Vec<Instance>, 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<Vec<(Instance, Instance)>, 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<Vec<Instance>, 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<Vec<Instance>, 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(),
|
||||
})
|
||||
}
|
|
@ -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<T> {
|
|||
Inline(Box<T>),
|
||||
}
|
||||
|
||||
impl<'a, T> Declaration<T> {
|
||||
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<Annotated<Schema>>,
|
||||
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<Data> {
|
||||
pub fn schema(&'a self, definitions: &'a Definitions<Annotated<Schema>>) -> Option<&'a Data> {
|
||||
self.try_schema(definitions, |s| match s {
|
||||
Schema::Data(data) => Some(data),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Declaration<Schema> {
|
||||
pub fn schema(&'a self, definitions: &'a Definitions<Annotated<Schema>>) -> 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<T> {
|
|||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Constructor {
|
||||
pub index: usize,
|
||||
// TODO: Generalize to work with either Reference or Data
|
||||
pub fields: Vec<Annotated<Declaration<Data>>>,
|
||||
}
|
||||
|
||||
|
@ -85,50 +120,6 @@ impl<T> From<T> for Annotated<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T> TryFrom<&'a Term<T>> for Schema {
|
||||
type Error = &'a str;
|
||||
|
||||
fn try_from(term: &'a Term<T>) -> Result<Schema, Self::Error> {
|
||||
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<Schema> {
|
||||
pub fn as_wrapped_redeemer(
|
||||
definitions: &mut Definitions<Annotated<Schema>>,
|
||||
|
|
|
@ -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<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub datum: Option<Argument>,
|
||||
pub datum: Option<Parameter>,
|
||||
|
||||
pub redeemer: Argument,
|
||||
pub redeemer: Parameter,
|
||||
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
#[serde(default)]
|
||||
pub parameters: Vec<Argument>,
|
||||
pub parameters: Vec<Parameter>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub program: Program<DeBruijn>,
|
||||
|
@ -38,56 +37,6 @@ pub struct Validator {
|
|||
pub definitions: Definitions<Annotated<Schema>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Argument {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
|
||||
pub schema: Reference,
|
||||
}
|
||||
|
||||
impl From<Reference> for Argument {
|
||||
fn from(schema: Reference) -> Argument {
|
||||
Argument {
|
||||
title: None,
|
||||
schema,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Argument {
|
||||
pub fn validate(
|
||||
&self,
|
||||
definitions: &Definitions<Annotated<Schema>>,
|
||||
term: &Term<DeBruijn>,
|
||||
) -> 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<Annotated<Schema>> {
|
||||
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 { .. }))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue