Merge pull request #477 from aiken-lang/blueprint-json-deserializers
Blueprint schema validation
This commit is contained in:
commit
ee8509956d
File diff suppressed because it is too large
Load Diff
|
@ -1,13 +1,10 @@
|
||||||
use std::{cell::RefCell, collections::HashMap, ops::Deref, sync::Arc};
|
use self::{environment::Environment, pretty::Printer};
|
||||||
|
|
||||||
use uplc::{ast::Type as UplcType, builtins::DefaultFunction};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::{Constant, DefinitionLocation, ModuleKind, Span},
|
ast::{Constant, DefinitionLocation, ModuleKind, Span},
|
||||||
tipo::fields::FieldMap,
|
tipo::fields::FieldMap,
|
||||||
};
|
};
|
||||||
|
use std::{cell::RefCell, collections::HashMap, ops::Deref, sync::Arc};
|
||||||
use self::{environment::Environment, pretty::Printer};
|
use uplc::{ast::Type as UplcType, builtins::DefaultFunction};
|
||||||
|
|
||||||
mod environment;
|
mod environment;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
|
@ -20,6 +20,7 @@ ignore = "0.4.20"
|
||||||
indexmap = "1.9.2"
|
indexmap = "1.9.2"
|
||||||
itertools = "0.10.5"
|
itertools = "0.10.5"
|
||||||
miette = { version = "5.5.0", features = ["fancy"] }
|
miette = { version = "5.5.0", features = ["fancy"] }
|
||||||
|
minicbor = "0.19.1"
|
||||||
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
||||||
pallas = "0.18.0"
|
pallas = "0.18.0"
|
||||||
pallas-traverse = "0.18.0"
|
pallas-traverse = "0.18.0"
|
||||||
|
@ -37,3 +38,6 @@ toml = "0.7.2"
|
||||||
uplc = { path = '../uplc', version = "0.0.29" }
|
uplc = { path = '../uplc', version = "0.0.29" }
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.3.2"
|
||||||
zip = "0.6.4"
|
zip = "0.6.4"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
proptest = "1.1.0"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use aiken_lang::tipo::{Type, TypeVar};
|
use aiken_lang::tipo::{Type, TypeVar};
|
||||||
use serde::{
|
use serde::{
|
||||||
self,
|
self,
|
||||||
|
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
|
||||||
ser::{Serialize, SerializeStruct, Serializer},
|
ser::{Serialize, SerializeStruct, Serializer},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -52,6 +53,12 @@ impl<T> Definitions<T> {
|
||||||
self.inner.remove(reference.as_key());
|
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
|
/// 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.
|
/// mark-and-insert such that recursive definitions are only built once.
|
||||||
pub fn register<F, E>(
|
pub fn register<F, E>(
|
||||||
|
@ -94,14 +101,14 @@ impl Reference {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Turn a reference into a key suitable for lookup.
|
/// Turn a reference into a key suitable for lookup.
|
||||||
fn as_key(&self) -> &str {
|
pub(crate) fn as_key(&self) -> &str {
|
||||||
self.inner.as_str()
|
self.inner.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Turn a reference into a valid JSON pointer. Note that the JSON pointer specification
|
/// Turn a reference into a valid JSON pointer. Note that the JSON pointer specification
|
||||||
/// indicates that '/' must be escaped as '~1' in pointer addresses (as they are otherwise
|
/// indicates that '/' must be escaped as '~1' in pointer addresses (as they are otherwise
|
||||||
/// treated as path delimiter in pointers paths).
|
/// treated as path delimiter in pointers paths).
|
||||||
fn as_json_pointer(&self) -> String {
|
pub(crate) fn as_json_pointer(&self) -> String {
|
||||||
format!("#/definitions/{}", self.as_key().replace('/', "~1"))
|
format!("#/definitions/{}", self.as_key().replace('/', "~1"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,3 +188,55 @@ impl Serialize for Reference {
|
||||||
s.end()
|
s.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> Deserialize<'a> for Reference {
|
||||||
|
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
enum Field {
|
||||||
|
#[serde(rename = "$ref")]
|
||||||
|
Ref,
|
||||||
|
}
|
||||||
|
const FIELDS: &[&str] = &["$ref"];
|
||||||
|
|
||||||
|
struct ReferenceVisitor;
|
||||||
|
|
||||||
|
impl<'a> Visitor<'a> for ReferenceVisitor {
|
||||||
|
type Value = Reference;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("Reference")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<V>(self, mut map: V) -> Result<Reference, V::Error>
|
||||||
|
where
|
||||||
|
V: MapAccess<'a>,
|
||||||
|
{
|
||||||
|
let mut inner = None;
|
||||||
|
|
||||||
|
while let Some(key) = map.next_key()? {
|
||||||
|
match key {
|
||||||
|
Field::Ref => {
|
||||||
|
if inner.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field(FIELDS[0]));
|
||||||
|
}
|
||||||
|
inner = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let inner: String = inner.ok_or_else(|| de::Error::missing_field(FIELDS[0]))?;
|
||||||
|
|
||||||
|
match inner.strip_prefix("#/definitions/") {
|
||||||
|
Some(suffix) => Ok(Reference {
|
||||||
|
inner: suffix.to_string(),
|
||||||
|
}),
|
||||||
|
None => Err(de::Error::custom(
|
||||||
|
"Invalid reference; only local JSON pointer to #/definitions are allowed.",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_struct("Reference", FIELDS, ReferenceVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
use super::schema;
|
use super::{
|
||||||
|
definitions::Reference,
|
||||||
|
schema::{self, Schema},
|
||||||
|
};
|
||||||
use aiken_lang::ast::Span;
|
use aiken_lang::ast::Span;
|
||||||
use miette::{Diagnostic, NamedSource};
|
use miette::{Diagnostic, NamedSource};
|
||||||
|
use minicbor as cbor;
|
||||||
use owo_colors::{OwoColorize, Stream::Stdout};
|
use owo_colors::{OwoColorize, Stream::Stdout};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
use uplc::ast::Constant;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, Diagnostic)]
|
#[derive(Debug, thiserror::Error, Diagnostic)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
@ -37,9 +42,61 @@ pub enum Error {
|
||||||
)]
|
)]
|
||||||
#[diagnostic(code("aiken::blueprint::address::parameterized"))]
|
#[diagnostic(code("aiken::blueprint::address::parameterized"))]
|
||||||
#[diagnostic(help(
|
#[diagnostic(help(
|
||||||
"I can only compute addresses of validators that are fully applied. For example, a {keyword_spend} validator must have exactly 3 arguments: a datum, a redeemer and a context. If it has more, they need to be provided beforehand and applied directly in the validator. Applying parameters change the validator's compiled code, and thus the address.\n\nThis is why I need you to apply parameters first.",
|
"I can only compute addresses of validators that are fully applied. For example, a {keyword_spend} validator must have exactly {spend_arity} arguments: a datum, a redeemer and a context. If it has more, they need to be provided beforehand and applied directly to the validator.\n\nApplying parameters change the validator's compiled code, and thus the address. This is why I need you to apply parameters first using the {blueprint_apply_command} command.",
|
||||||
keyword_spend = "spend".if_supports_color(Stdout, |s| s.purple())))]
|
keyword_spend = "spend".if_supports_color(Stdout, |s| s.yellow()),
|
||||||
|
spend_arity = "3".if_supports_color(Stdout, |s| s.yellow()),
|
||||||
|
blueprint_apply_command = "blueprint apply".if_supports_color(Stdout, |s| s.purple()),
|
||||||
|
))]
|
||||||
ParameterizedValidator { n: usize },
|
ParameterizedValidator { n: usize },
|
||||||
|
|
||||||
|
#[error("I stumble upon something else than a constant when I expected one.")]
|
||||||
|
#[diagnostic(code("aiken:blueprint::apply::malformed::argument"))]
|
||||||
|
#[diagnostic(help(
|
||||||
|
"Parameters applied to blueprints must be constant; they cannot be lambdas or delayed terms."
|
||||||
|
))]
|
||||||
|
NonConstantParameter,
|
||||||
|
|
||||||
|
#[error("I couldn't find a definition corresponding to a reference.")]
|
||||||
|
#[diagnostic(code("aiken::blueprint::apply::unknown::reference"))]
|
||||||
|
#[diagnostic(help(
|
||||||
|
"While resolving a schema definition, I stumbled upon an unknown reference:\n\n→ {reference}\n\nThis is unfortunate, but signals that either the reference is invalid or that the corresponding schema definition is missing. Double-check the blueprint for that reference or definition.",
|
||||||
|
reference = reference.as_json_pointer().if_supports_color(Stdout, |s| s.red())
|
||||||
|
))]
|
||||||
|
UnresolvedSchemaReference { reference: Reference },
|
||||||
|
|
||||||
|
#[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, it didn't match in this case.\n\nI am looking at the following value:\n\n{term}\n\nbut failed to match it against the specified schema:\n\n{expected}\n\n\nNOTE: this may only represent part of a bigger whole as I am validating the parameter incrementally.",
|
||||||
|
expected = serde_json::to_string_pretty(&schema).unwrap().if_supports_color(Stdout, |s| s.green()),
|
||||||
|
term = {
|
||||||
|
let mut buf = vec![];
|
||||||
|
match term {
|
||||||
|
Constant::Data(data) => {
|
||||||
|
cbor::encode(data, &mut buf).unwrap();
|
||||||
|
cbor::display(&buf).to_string()
|
||||||
|
},
|
||||||
|
_ => term.to_pretty()
|
||||||
|
}
|
||||||
|
}.if_supports_color(Stdout, |s| s.red()),
|
||||||
|
))]
|
||||||
|
SchemaMismatch { schema: Schema, term: Constant },
|
||||||
|
|
||||||
|
#[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 },
|
||||||
|
|
||||||
|
#[error("I failed to convert some input into a valid parameter")]
|
||||||
|
#[diagnostic(code("aiken::blueprint::parse::parameter"))]
|
||||||
|
#[diagnostic(help("{hint}"))]
|
||||||
|
MalformedParameter { hint: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe impl Send for Error {}
|
unsafe impl Send for Error {}
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
pub mod definitions;
|
pub mod definitions;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod parameter;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
pub mod validator;
|
pub mod validator;
|
||||||
|
|
||||||
use crate::{config::Config, module::CheckedModules};
|
use crate::{config::Config, module::CheckedModules};
|
||||||
use aiken_lang::gen_uplc::CodeGenerator;
|
use aiken_lang::gen_uplc::CodeGenerator;
|
||||||
use definitions::{Definitions, Reference};
|
use definitions::Definitions;
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use schema::{Annotated, Schema};
|
use schema::{Annotated, Schema};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use validator::Validator;
|
use validator::Validator;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Blueprint<R: Default, S: Default> {
|
pub struct Blueprint {
|
||||||
pub preamble: Preamble,
|
pub preamble: Preamble,
|
||||||
pub validators: Vec<Validator<R, S>>,
|
pub validators: Vec<Validator>,
|
||||||
#[serde(skip_serializing_if = "Definitions::is_empty", default)]
|
#[serde(skip_serializing_if = "Definitions::is_empty", default)]
|
||||||
pub definitions: Definitions<S>,
|
pub definitions: Definitions<Annotated<Schema>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
@ -48,7 +49,7 @@ pub enum LookupResult<'a, T> {
|
||||||
Many,
|
Many,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Blueprint<Reference, Annotated<Schema>> {
|
impl Blueprint {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
modules: &CheckedModules,
|
modules: &CheckedModules,
|
||||||
|
@ -82,12 +83,8 @@ impl Blueprint<Reference, Annotated<Schema>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, S> Blueprint<R, S>
|
impl Blueprint {
|
||||||
where
|
pub fn lookup(&self, title: Option<&String>) -> Option<LookupResult<Validator>> {
|
||||||
R: Clone + Default,
|
|
||||||
S: Clone + Default,
|
|
||||||
{
|
|
||||||
pub fn lookup(&self, title: Option<&String>) -> Option<LookupResult<Validator<R, S>>> {
|
|
||||||
let mut validator = None;
|
let mut validator = None;
|
||||||
|
|
||||||
for v in self.validators.iter() {
|
for v in self.validators.iter() {
|
||||||
|
@ -112,7 +109,7 @@ where
|
||||||
action: F,
|
action: F,
|
||||||
) -> Result<A, E>
|
) -> Result<A, E>
|
||||||
where
|
where
|
||||||
F: Fn(Validator<R, S>) -> Result<A, E>,
|
F: Fn(Validator) -> Result<A, E>,
|
||||||
{
|
{
|
||||||
match self.lookup(title) {
|
match self.lookup(title) {
|
||||||
Some(LookupResult::One(validator)) => action(validator.to_owned()),
|
Some(LookupResult::One(validator)) => action(validator.to_owned()),
|
||||||
|
@ -146,13 +143,13 @@ impl From<&Config> for Preamble {
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use aiken_lang::builtins;
|
use aiken_lang::builtins;
|
||||||
use schema::{Data, Items, Schema};
|
use schema::{Data, Declaration, Items, Schema};
|
||||||
use serde_json::{self, json};
|
use serde_json::{self, json};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_no_description() {
|
fn serialize_no_description() {
|
||||||
let blueprint: Blueprint<Reference, Annotated<Schema>> = Blueprint {
|
let blueprint = Blueprint {
|
||||||
preamble: Preamble {
|
preamble: Preamble {
|
||||||
title: "Foo".to_string(),
|
title: "Foo".to_string(),
|
||||||
description: None,
|
description: None,
|
||||||
|
@ -179,7 +176,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_with_description() {
|
fn serialize_with_description() {
|
||||||
let blueprint: Blueprint<Reference, Annotated<Schema>> = Blueprint {
|
let blueprint = Blueprint {
|
||||||
preamble: Preamble {
|
preamble: Preamble {
|
||||||
title: "Foo".to_string(),
|
title: "Foo".to_string(),
|
||||||
description: Some("Lorem ipsum".to_string()),
|
description: Some("Lorem ipsum".to_string()),
|
||||||
|
@ -222,12 +219,15 @@ mod test {
|
||||||
&HashMap::new(),
|
&HashMap::new(),
|
||||||
|_| Ok(Schema::Data(Data::Bytes).into()),
|
|_| Ok(Schema::Data(Data::Bytes).into()),
|
||||||
)?;
|
)?;
|
||||||
Ok(Schema::Data(Data::List(Items::One(Box::new(ref_bytes)))).into())
|
Ok(
|
||||||
|
Schema::Data(Data::List(Items::One(Declaration::Referenced(ref_bytes))))
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let blueprint: Blueprint<Reference, Annotated<Schema>> = Blueprint {
|
let blueprint = Blueprint {
|
||||||
preamble: Preamble {
|
preamble: Preamble {
|
||||||
title: "Foo".to_string(),
|
title: "Foo".to_string(),
|
||||||
description: None,
|
description: None,
|
||||||
|
|
|
@ -0,0 +1,428 @@
|
||||||
|
use super::{
|
||||||
|
definitions::{Definitions, Reference},
|
||||||
|
error::Error,
|
||||||
|
schema::{Annotated, Constructor, Data, Declaration, Items, Schema},
|
||||||
|
};
|
||||||
|
use std::{iter, ops::Deref};
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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: &Term<DeBruijn>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let schema = &definitions
|
||||||
|
.lookup(&self.schema)
|
||||||
|
.map(Ok)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
Err(Error::UnresolvedSchemaReference {
|
||||||
|
reference: self.schema.clone(),
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.annotated;
|
||||||
|
|
||||||
|
if let Term::Constant(constant) = term {
|
||||||
|
validate_schema(schema, definitions, constant)
|
||||||
|
} else {
|
||||||
|
Err(Error::NonConstantParameter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mismatch(term: &Constant, schema: Schema) -> Error {
|
||||||
|
Error::SchemaMismatch {
|
||||||
|
schema,
|
||||||
|
term: term.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_schema(
|
||||||
|
schema: &Schema,
|
||||||
|
definitions: &Definitions<Annotated<Schema>>,
|
||||||
|
term: &Constant,
|
||||||
|
) -> 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: &Constant,
|
||||||
|
) -> 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(mismatch(
|
||||||
|
term,
|
||||||
|
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(),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_data(term: &Constant) -> Result<(), Error> {
|
||||||
|
if matches!(term, Constant::Data(..)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(term, Schema::Data(Data::Opaque)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_data_integer(term: &Constant) -> Result<(), Error> {
|
||||||
|
if let Constant::Data(data) = term {
|
||||||
|
if matches!(data, PlutusData::BigInt(..)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(term, Schema::Data(Data::Integer)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_data_bytes(term: &Constant) -> Result<(), Error> {
|
||||||
|
if let Constant::Data(data) = term {
|
||||||
|
if matches!(data, PlutusData::BoundedBytes(..)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(term, Schema::Data(Data::Bytes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_data_list(term: &Constant) -> Result<Vec<Constant>, Error> {
|
||||||
|
if let Constant::Data(PlutusData::Array(elems)) = term {
|
||||||
|
return Ok(elems
|
||||||
|
.iter()
|
||||||
|
.map(|elem| Constant::Data(elem.to_owned()))
|
||||||
|
.collect());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(
|
||||||
|
term,
|
||||||
|
Schema::Data(Data::List(Items::One(Declaration::Inline(Box::new(
|
||||||
|
Data::Opaque,
|
||||||
|
))))),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_data_map(term: &Constant) -> Result<Vec<(Constant, Constant)>, Error> {
|
||||||
|
if let Constant::Data(PlutusData::Map(pairs)) = term {
|
||||||
|
return Ok(pairs
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (Constant::Data(k.to_owned()), Constant::Data(v.to_owned())))
|
||||||
|
.collect());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(
|
||||||
|
term,
|
||||||
|
Schema::Data(Data::Map(
|
||||||
|
Declaration::Inline(Box::new(Data::Opaque)),
|
||||||
|
Declaration::Inline(Box::new(Data::Opaque)),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_data_constr(term: &Constant, index: usize) -> Result<Vec<Constant>, Error> {
|
||||||
|
if let Constant::Data(PlutusData::Constr(constr)) = term {
|
||||||
|
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| Constant::Data(field.to_owned()))
|
||||||
|
.collect());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(
|
||||||
|
term,
|
||||||
|
Schema::Data(Data::AnyOf(vec![Constructor {
|
||||||
|
index,
|
||||||
|
fields: vec![],
|
||||||
|
}
|
||||||
|
.into()])),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_unit(term: &Constant) -> Result<(), Error> {
|
||||||
|
if matches!(term, Constant::Unit) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(term, Schema::Unit))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_integer(term: &Constant) -> Result<(), Error> {
|
||||||
|
if matches!(term, Constant::Integer(..)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(term, Schema::Integer))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_bytes(term: &Constant) -> Result<(), Error> {
|
||||||
|
if matches!(term, Constant::ByteString(..)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(term, Schema::Bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_string(term: &Constant) -> Result<(), Error> {
|
||||||
|
if matches!(term, Constant::String(..)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(term, Schema::String))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_boolean(term: &Constant) -> Result<(), Error> {
|
||||||
|
if matches!(term, Constant::Bool(..)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(term, Schema::Boolean))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_pair(term: &Constant) -> Result<(Constant, Constant), Error> {
|
||||||
|
if let Constant::ProtoPair(_, _, left, right) = term {
|
||||||
|
return Ok((left.deref().clone(), right.deref().clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(
|
||||||
|
term,
|
||||||
|
Schema::Pair(
|
||||||
|
Declaration::Inline(Box::new(Schema::Data(Data::Opaque))),
|
||||||
|
Declaration::Inline(Box::new(Schema::Data(Data::Opaque))),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_list(term: &Constant) -> Result<Vec<Constant>, Error> {
|
||||||
|
if let Constant::ProtoList(_, elems) = term {
|
||||||
|
return Ok(elems.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(mismatch(
|
||||||
|
term,
|
||||||
|
Schema::List(Items::One(Declaration::Inline(Box::new(Schema::Data(
|
||||||
|
Data::Opaque,
|
||||||
|
))))),
|
||||||
|
))
|
||||||
|
}
|
|
@ -8,10 +8,11 @@ use aiken_lang::{
|
||||||
use owo_colors::{OwoColorize, Stream::Stdout};
|
use owo_colors::{OwoColorize, Stream::Stdout};
|
||||||
use serde::{
|
use serde::{
|
||||||
self,
|
self,
|
||||||
|
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
|
||||||
ser::{Serialize, SerializeStruct, Serializer},
|
ser::{Serialize, SerializeStruct, Serializer},
|
||||||
};
|
};
|
||||||
use std::ops::Deref;
|
use serde_json as json;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
|
||||||
|
|
||||||
// NOTE: Can be anything BUT 0
|
// NOTE: Can be anything BUT 0
|
||||||
pub const REDEEMER_DISCRIMINANT: usize = 1;
|
pub const REDEEMER_DISCRIMINANT: usize = 1;
|
||||||
|
@ -26,6 +27,50 @@ pub struct Annotated<T> {
|
||||||
pub annotated: T,
|
pub annotated: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Declaration<T> {
|
||||||
|
Referenced(Reference),
|
||||||
|
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.
|
/// A schema for low-level UPLC primitives.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub enum Schema {
|
pub enum Schema {
|
||||||
|
@ -34,8 +79,8 @@ pub enum Schema {
|
||||||
Integer,
|
Integer,
|
||||||
Bytes,
|
Bytes,
|
||||||
String,
|
String,
|
||||||
Pair(Data, Data),
|
Pair(Declaration<Schema>, Declaration<Schema>),
|
||||||
List(Vec<Data>),
|
List(Items<Schema>),
|
||||||
Data(Data),
|
Data(Data),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,24 +89,25 @@ pub enum Schema {
|
||||||
pub enum Data {
|
pub enum Data {
|
||||||
Integer,
|
Integer,
|
||||||
Bytes,
|
Bytes,
|
||||||
List(Items<Reference>),
|
List(Items<Data>),
|
||||||
Map(Box<Reference>, Box<Reference>),
|
Map(Declaration<Data>, Declaration<Data>),
|
||||||
AnyOf(Vec<Annotated<Constructor>>),
|
AnyOf(Vec<Annotated<Constructor>>),
|
||||||
Opaque,
|
Opaque,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A structure that represents either one or many elements.
|
/// A structure that represents either one or many elements.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
pub enum Items<T> {
|
pub enum Items<T> {
|
||||||
One(Box<T>),
|
One(Declaration<T>),
|
||||||
Many(Vec<T>),
|
Many(Vec<Declaration<T>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Captures a single UPLC constructor with its
|
/// Captures a single UPLC constructor with its
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Constructor {
|
pub struct Constructor {
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
pub fields: Vec<Annotated<Reference>>,
|
pub fields: Vec<Annotated<Declaration<Data>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<T> for Annotated<T> {
|
impl<T> From<T> for Annotated<T> {
|
||||||
|
@ -90,7 +136,7 @@ impl Annotated<Schema> {
|
||||||
description: Some("A redeemer wrapped in an extra constructor to make multi-validator detection possible on-chain.".to_string()),
|
description: Some("A redeemer wrapped in an extra constructor to make multi-validator detection possible on-chain.".to_string()),
|
||||||
annotated: Schema::Data(Data::AnyOf(vec![Constructor {
|
annotated: Schema::Data(Data::AnyOf(vec![Constructor {
|
||||||
index: REDEEMER_DISCRIMINANT,
|
index: REDEEMER_DISCRIMINANT,
|
||||||
fields: vec![schema.into()],
|
fields: vec![Declaration::Referenced(schema).into()],
|
||||||
}
|
}
|
||||||
.into()])),
|
.into()])),
|
||||||
})
|
})
|
||||||
|
@ -219,7 +265,7 @@ impl Annotated<Schema> {
|
||||||
description: Some("An optional value.".to_string()),
|
description: Some("An optional value.".to_string()),
|
||||||
annotated: Constructor {
|
annotated: Constructor {
|
||||||
index: 0,
|
index: 0,
|
||||||
fields: vec![generic.into()],
|
fields: vec![Declaration::Referenced(generic).into()],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Annotated {
|
Annotated {
|
||||||
|
@ -260,22 +306,15 @@ impl Annotated<Schema> {
|
||||||
} if xs.len() == 2 => {
|
} if xs.len() == 2 => {
|
||||||
definitions.remove(&generic);
|
definitions.remove(&generic);
|
||||||
Data::Map(
|
Data::Map(
|
||||||
Box::new(
|
xs.first()
|
||||||
xs.first()
|
.expect("length (== 2) checked in pattern clause")
|
||||||
.expect("length (== 2) checked in pattern clause")
|
.to_owned(),
|
||||||
.to_owned(),
|
xs.last()
|
||||||
),
|
.expect("length (== 2) checked in pattern clause")
|
||||||
Box::new(
|
.to_owned(),
|
||||||
xs.last()
|
|
||||||
.expect("length (== 2) checked in pattern clause")
|
|
||||||
.to_owned(),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => Data::List(Items::One(Declaration::Referenced(generic))),
|
||||||
// let inner = schema.clone().into_data(type_info)?.annotated;
|
|
||||||
Data::List(Items::One(Box::new(generic)))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Schema::Data(data).into())
|
Ok(Schema::Data(data).into())
|
||||||
|
@ -320,6 +359,7 @@ impl Annotated<Schema> {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|elem| {
|
.map(|elem| {
|
||||||
Annotated::do_from_type(elem, modules, type_parameters, definitions)
|
Annotated::do_from_type(elem, modules, type_parameters, definitions)
|
||||||
|
.map(Declaration::Referenced)
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map_err(|e| e.backtrack(type_info))?;
|
.map_err(|e| e.backtrack(type_info))?;
|
||||||
|
@ -398,7 +438,7 @@ impl Data {
|
||||||
fields.push(Annotated {
|
fields.push(Annotated {
|
||||||
title: field.label.clone(),
|
title: field.label.clone(),
|
||||||
description: field.doc.clone().map(|s| s.trim().to_string()),
|
description: field.doc.clone().map(|s| s.trim().to_string()),
|
||||||
annotated: reference,
|
annotated: Declaration::Referenced(reference),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,6 +550,245 @@ impl Serialize for Schema {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visit_schema<'a, V>(mut map: V) -> Result<Schema, V::Error>
|
||||||
|
where
|
||||||
|
V: MapAccess<'a>,
|
||||||
|
{
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(field_identifier, rename_all = "camelCase")]
|
||||||
|
enum Field {
|
||||||
|
DataType,
|
||||||
|
Items,
|
||||||
|
Keys,
|
||||||
|
Values,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
AnyOf,
|
||||||
|
OneOf,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data_type: Option<String> = None;
|
||||||
|
let mut items: Option<json::Value> = None; // defer items deserialization to later
|
||||||
|
let mut keys = None;
|
||||||
|
let mut left = None;
|
||||||
|
let mut right = None;
|
||||||
|
let mut values = None;
|
||||||
|
let mut any_of = None;
|
||||||
|
|
||||||
|
while let Some(key) = map.next_key()? {
|
||||||
|
match key {
|
||||||
|
Field::DataType => {
|
||||||
|
if data_type.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("dataType"));
|
||||||
|
}
|
||||||
|
data_type = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::Items => {
|
||||||
|
if items.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("items"));
|
||||||
|
}
|
||||||
|
items = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::Keys => {
|
||||||
|
if keys.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("keys"));
|
||||||
|
}
|
||||||
|
keys = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::Values => {
|
||||||
|
if values.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("values"));
|
||||||
|
}
|
||||||
|
values = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::Left => {
|
||||||
|
if left.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("left"));
|
||||||
|
}
|
||||||
|
left = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::Right => {
|
||||||
|
if right.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("right"));
|
||||||
|
}
|
||||||
|
right = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::AnyOf => {
|
||||||
|
if any_of.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("anyOf/oneOf"));
|
||||||
|
}
|
||||||
|
any_of = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::OneOf => {
|
||||||
|
if any_of.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("anyOf/oneOf"));
|
||||||
|
}
|
||||||
|
any_of = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let expect_data_items = || match &items {
|
||||||
|
Some(items) => serde_json::from_value::<Items<Data>>(items.clone())
|
||||||
|
.map_err(|e| de::Error::custom(e.to_string())),
|
||||||
|
None => Err(de::Error::missing_field("items")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let expect_schema_items = || match &items {
|
||||||
|
Some(items) => serde_json::from_value::<Items<Schema>>(items.clone())
|
||||||
|
.map_err(|e| de::Error::custom(e.to_string())),
|
||||||
|
None => Err(de::Error::missing_field("items")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let expect_no_items = || {
|
||||||
|
if items.is_some() {
|
||||||
|
return Err(de::Error::custom(
|
||||||
|
"unexpected fields 'items' for non-list data-type",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let expect_no_keys = || {
|
||||||
|
if keys.is_some() {
|
||||||
|
return Err(de::Error::custom(
|
||||||
|
"unexpected fields 'keys' for non-map data-type",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let expect_no_values = || {
|
||||||
|
if values.is_some() {
|
||||||
|
return Err(de::Error::custom(
|
||||||
|
"unexpected fields 'values' for non-map data-type",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let expect_no_any_of = || {
|
||||||
|
if any_of.is_some() {
|
||||||
|
return Err(de::Error::custom(
|
||||||
|
"unexpected fields 'anyOf' or 'oneOf'; applicators must singletons",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let expect_no_left_or_right = || {
|
||||||
|
if left.is_some() || right.is_some() {
|
||||||
|
return Err(de::Error::custom(
|
||||||
|
"unexpected field(s) 'left' and/or 'right' for a non-pair data-type",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
match data_type {
|
||||||
|
None => {
|
||||||
|
expect_no_items()?;
|
||||||
|
expect_no_keys()?;
|
||||||
|
expect_no_values()?;
|
||||||
|
expect_no_left_or_right()?;
|
||||||
|
match any_of {
|
||||||
|
None => Ok(Schema::Data(Data::Opaque)),
|
||||||
|
Some(constructors) => Ok(Schema::Data(Data::AnyOf(constructors))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(data_type) if data_type == "list" => {
|
||||||
|
expect_no_keys()?;
|
||||||
|
expect_no_values()?;
|
||||||
|
expect_no_any_of()?;
|
||||||
|
expect_no_left_or_right()?;
|
||||||
|
let items = expect_data_items()?;
|
||||||
|
Ok(Schema::Data(Data::List(items)))
|
||||||
|
}
|
||||||
|
Some(data_type) if data_type == "#list" => {
|
||||||
|
expect_no_keys()?;
|
||||||
|
expect_no_values()?;
|
||||||
|
expect_no_any_of()?;
|
||||||
|
expect_no_left_or_right()?;
|
||||||
|
let items = expect_schema_items()?;
|
||||||
|
Ok(Schema::List(items))
|
||||||
|
}
|
||||||
|
Some(data_type) if data_type == "map" => {
|
||||||
|
expect_no_items()?;
|
||||||
|
expect_no_any_of()?;
|
||||||
|
expect_no_left_or_right()?;
|
||||||
|
match (keys, values) {
|
||||||
|
(Some(keys), Some(values)) => Ok(Schema::Data(Data::Map(keys, values))),
|
||||||
|
(None, _) => Err(de::Error::missing_field("keys")),
|
||||||
|
(Some(..), None) => Err(de::Error::missing_field("values")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(data_type) if data_type == "#pair" => {
|
||||||
|
expect_no_items()?;
|
||||||
|
expect_no_keys()?;
|
||||||
|
expect_no_values()?;
|
||||||
|
expect_no_any_of()?;
|
||||||
|
match (left, right) {
|
||||||
|
(Some(left), Some(right)) => Ok(Schema::Pair(left, right)),
|
||||||
|
(None, _) => Err(de::Error::missing_field("left")),
|
||||||
|
(Some(..), None) => Err(de::Error::missing_field("right")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(data_type) => {
|
||||||
|
expect_no_items()?;
|
||||||
|
expect_no_keys()?;
|
||||||
|
expect_no_values()?;
|
||||||
|
expect_no_any_of()?;
|
||||||
|
expect_no_left_or_right()?;
|
||||||
|
if data_type == "bytes" {
|
||||||
|
Ok(Schema::Data(Data::Bytes))
|
||||||
|
} else if data_type == "integer" {
|
||||||
|
Ok(Schema::Data(Data::Integer))
|
||||||
|
} else if data_type == "#unit" {
|
||||||
|
Ok(Schema::Unit)
|
||||||
|
} else if data_type == "#integer" {
|
||||||
|
Ok(Schema::Integer)
|
||||||
|
} else if data_type == "#bytes" {
|
||||||
|
Ok(Schema::Bytes)
|
||||||
|
} else if data_type == "#boolean" {
|
||||||
|
Ok(Schema::Boolean)
|
||||||
|
} else if data_type == "#string" {
|
||||||
|
Ok(Schema::String)
|
||||||
|
} else {
|
||||||
|
Err(de::Error::custom("unknown data-type"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deserialize<'a> for Schema {
|
||||||
|
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
struct SchemaVisitor;
|
||||||
|
|
||||||
|
impl<'a> Visitor<'a> for SchemaVisitor {
|
||||||
|
type Value = Schema;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("Schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<V>(self, mut map: V) -> Result<Schema, V::Error>
|
||||||
|
where
|
||||||
|
V: MapAccess<'a>,
|
||||||
|
{
|
||||||
|
visit_schema(&mut map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_struct(
|
||||||
|
"Schema",
|
||||||
|
&[
|
||||||
|
"dataType", "items", "keys", "values", "anyOf", "oneOf", "left", "right",
|
||||||
|
],
|
||||||
|
SchemaVisitor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Serialize for Data {
|
impl Serialize for Data {
|
||||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
match self {
|
match self {
|
||||||
|
@ -527,13 +806,7 @@ impl Serialize for Data {
|
||||||
s.serialize_field("dataType", "bytes")?;
|
s.serialize_field("dataType", "bytes")?;
|
||||||
s.end()
|
s.end()
|
||||||
}
|
}
|
||||||
Data::List(Items::One(item)) => {
|
Data::List(items) => {
|
||||||
let mut s = serializer.serialize_struct("List", 2)?;
|
|
||||||
s.serialize_field("dataType", "list")?;
|
|
||||||
s.serialize_field("items", &item)?;
|
|
||||||
s.end()
|
|
||||||
}
|
|
||||||
Data::List(Items::Many(items)) => {
|
|
||||||
let mut s = serializer.serialize_struct("List", 2)?;
|
let mut s = serializer.serialize_struct("List", 2)?;
|
||||||
s.serialize_field("dataType", "list")?;
|
s.serialize_field("dataType", "list")?;
|
||||||
s.serialize_field("items", &items)?;
|
s.serialize_field("items", &items)?;
|
||||||
|
@ -554,6 +827,38 @@ impl Serialize for Data {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> Deserialize<'a> for Data {
|
||||||
|
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
struct DataVisitor;
|
||||||
|
|
||||||
|
impl<'a> Visitor<'a> for DataVisitor {
|
||||||
|
type Value = Data;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("Data")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<V>(self, mut map: V) -> Result<Data, V::Error>
|
||||||
|
where
|
||||||
|
V: MapAccess<'a>,
|
||||||
|
{
|
||||||
|
let schema = visit_schema(&mut map)?;
|
||||||
|
match schema {
|
||||||
|
Schema::Data(data) => Ok(data),
|
||||||
|
_ => Err(de::Error::custom("not a valid 'data'")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_struct(
|
||||||
|
"Data",
|
||||||
|
&["dataType", "items", "keys", "values", "anyOf", "oneOf"],
|
||||||
|
DataVisitor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Serialize for Constructor {
|
impl Serialize for Constructor {
|
||||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
let mut s = serializer.serialize_struct("Constructor", 3)?;
|
let mut s = serializer.serialize_struct("Constructor", 3)?;
|
||||||
|
@ -564,6 +869,60 @@ impl Serialize for Constructor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> Deserialize<'a> for Constructor {
|
||||||
|
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(field_identifier, rename_all = "camelCase")]
|
||||||
|
enum Field {
|
||||||
|
Index,
|
||||||
|
Fields,
|
||||||
|
}
|
||||||
|
const FIELDS: &[&str] = &["index", "fields"];
|
||||||
|
|
||||||
|
struct ConstructorVisitor;
|
||||||
|
|
||||||
|
impl<'a> Visitor<'a> for ConstructorVisitor {
|
||||||
|
type Value = Constructor;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("Constructor")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<V>(self, mut map: V) -> Result<Constructor, V::Error>
|
||||||
|
where
|
||||||
|
V: MapAccess<'a>,
|
||||||
|
{
|
||||||
|
let mut index = None;
|
||||||
|
let mut fields = None;
|
||||||
|
|
||||||
|
while let Some(key) = map.next_key()? {
|
||||||
|
match key {
|
||||||
|
Field::Index => {
|
||||||
|
if index.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field(FIELDS[0]));
|
||||||
|
}
|
||||||
|
index = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::Fields => {
|
||||||
|
if fields.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field(FIELDS[1]));
|
||||||
|
}
|
||||||
|
fields = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Constructor {
|
||||||
|
index: index.ok_or_else(|| de::Error::missing_field(FIELDS[0]))?,
|
||||||
|
fields: fields.ok_or_else(|| de::Error::missing_field(FIELDS[1]))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_struct("Constructor", FIELDS, ConstructorVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, thiserror::Error)]
|
#[derive(Debug, PartialEq, Clone, thiserror::Error)]
|
||||||
#[error("{}", context)]
|
#[error("{}", context)]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
|
@ -681,6 +1040,7 @@ Here's the types I followed and that led me to this problem:
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
use serde_json::{self, json, Value};
|
use serde_json::{self, json, Value};
|
||||||
|
|
||||||
pub fn assert_json(schema: &impl Serialize, expected: Value) {
|
pub fn assert_json(schema: &impl Serialize, expected: Value) {
|
||||||
|
@ -712,7 +1072,7 @@ pub mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_data_list_1() {
|
fn serialize_data_list_1() {
|
||||||
let ref_integer = Reference::new("Int");
|
let ref_integer = Reference::new("Int");
|
||||||
let schema = Schema::Data(Data::List(Items::One(Box::new(ref_integer))));
|
let schema = Schema::Data(Data::List(Items::One(Declaration::Referenced(ref_integer))));
|
||||||
assert_json(
|
assert_json(
|
||||||
&schema,
|
&schema,
|
||||||
json!({
|
json!({
|
||||||
|
@ -727,7 +1087,9 @@ pub mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_data_list_2() {
|
fn serialize_data_list_2() {
|
||||||
let ref_list_integer = Reference::new("List$Int");
|
let ref_list_integer = Reference::new("List$Int");
|
||||||
let schema = Schema::Data(Data::List(Items::One(Box::new(ref_list_integer))));
|
let schema = Schema::Data(Data::List(Items::One(Declaration::Referenced(
|
||||||
|
ref_list_integer,
|
||||||
|
))));
|
||||||
assert_json(
|
assert_json(
|
||||||
&schema,
|
&schema,
|
||||||
json!({
|
json!({
|
||||||
|
@ -741,9 +1103,9 @@ pub mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_data_map_1() {
|
fn serialize_data_map_1() {
|
||||||
let ref_integer = Reference::new("Int");
|
let ref_integer = Declaration::Referenced(Reference::new("Int"));
|
||||||
let ref_bytes = Reference::new("ByteArray");
|
let ref_bytes = Declaration::Referenced(Reference::new("ByteArray"));
|
||||||
let schema = Schema::Data(Data::Map(Box::new(ref_integer), Box::new(ref_bytes)));
|
let schema = Schema::Data(Data::Map(ref_integer, ref_bytes));
|
||||||
assert_json(
|
assert_json(
|
||||||
&schema,
|
&schema,
|
||||||
json!({
|
json!({
|
||||||
|
@ -782,12 +1144,12 @@ pub mod test {
|
||||||
let schema = Schema::Data(Data::AnyOf(vec![
|
let schema = Schema::Data(Data::AnyOf(vec![
|
||||||
Constructor {
|
Constructor {
|
||||||
index: 0,
|
index: 0,
|
||||||
fields: vec![Reference::new("Int").into()],
|
fields: vec![Declaration::Referenced(Reference::new("Int")).into()],
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
Constructor {
|
Constructor {
|
||||||
index: 1,
|
index: 1,
|
||||||
fields: vec![Reference::new("Bytes").into()],
|
fields: vec![Declaration::Referenced(Reference::new("Bytes")).into()],
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
]));
|
]));
|
||||||
|
@ -848,4 +1210,224 @@ pub mod test {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_data_opaque() {
|
||||||
|
assert_eq!(Data::Opaque, serde_json::from_value(json!({})).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_data_integer() {
|
||||||
|
assert_eq!(
|
||||||
|
Data::Integer,
|
||||||
|
serde_json::from_value(json!({
|
||||||
|
"dataType": "integer",
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_data_bytes() {
|
||||||
|
assert_eq!(
|
||||||
|
Data::Bytes,
|
||||||
|
serde_json::from_value(json!({
|
||||||
|
"dataType": "bytes",
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_data_list_one() {
|
||||||
|
assert_eq!(
|
||||||
|
Data::List(Items::One(Declaration::Referenced(Reference::new("foo")))),
|
||||||
|
serde_json::from_value(json!({
|
||||||
|
"dataType": "list",
|
||||||
|
"items": { "$ref": "#/definitions/foo" }
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_data_list_many() {
|
||||||
|
assert_eq!(
|
||||||
|
Data::List(Items::Many(vec![
|
||||||
|
Declaration::Referenced(Reference::new("foo")),
|
||||||
|
Declaration::Referenced(Reference::new("bar"))
|
||||||
|
])),
|
||||||
|
serde_json::from_value(json!({
|
||||||
|
"dataType": "list",
|
||||||
|
"items": [
|
||||||
|
{ "$ref": "#/definitions/foo" },
|
||||||
|
{ "$ref": "#/definitions/bar" }
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_data_map() {
|
||||||
|
assert_eq!(
|
||||||
|
Data::Map(
|
||||||
|
Declaration::Referenced(Reference::new("foo")),
|
||||||
|
Declaration::Referenced(Reference::new("bar"))
|
||||||
|
),
|
||||||
|
serde_json::from_value(json!({
|
||||||
|
"dataType": "map",
|
||||||
|
"keys": { "$ref": "#/definitions/foo" },
|
||||||
|
"values": { "$ref": "#/definitions/bar" }
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_any_of() {
|
||||||
|
assert_eq!(
|
||||||
|
Data::AnyOf(vec![Constructor {
|
||||||
|
index: 0,
|
||||||
|
fields: vec![
|
||||||
|
Declaration::Referenced(Reference::new("foo")).into(),
|
||||||
|
Declaration::Referenced(Reference::new("bar")).into()
|
||||||
|
],
|
||||||
|
}
|
||||||
|
.into()]),
|
||||||
|
serde_json::from_value(json!({
|
||||||
|
"anyOf": [{
|
||||||
|
"index": 0,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/bar",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_one_of() {
|
||||||
|
assert_eq!(
|
||||||
|
Data::AnyOf(vec![Constructor {
|
||||||
|
index: 0,
|
||||||
|
fields: vec![
|
||||||
|
Declaration::Referenced(Reference::new("foo")).into(),
|
||||||
|
Declaration::Referenced(Reference::new("bar")).into()
|
||||||
|
],
|
||||||
|
}
|
||||||
|
.into()]),
|
||||||
|
serde_json::from_value(json!({
|
||||||
|
"oneOf": [{
|
||||||
|
"index": 0,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/bar",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}))
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arbitrary_data() -> impl Strategy<Value = Data> {
|
||||||
|
let leaf = prop_oneof![Just(Data::Opaque), Just(Data::Bytes), Just(Data::Integer)];
|
||||||
|
|
||||||
|
leaf.prop_recursive(3, 8, 3, |inner| {
|
||||||
|
let r = prop_oneof![
|
||||||
|
".*".prop_map(|s| Declaration::Referenced(Reference::new(&s))),
|
||||||
|
inner.prop_map(|s| Declaration::Inline(Box::new(s)))
|
||||||
|
];
|
||||||
|
let constructor =
|
||||||
|
(0..3usize, prop::collection::vec(r.clone(), 0..3)).prop_map(|(index, fields)| {
|
||||||
|
Constructor {
|
||||||
|
index,
|
||||||
|
fields: fields.into_iter().map(|f| f.into()).collect(),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
});
|
||||||
|
|
||||||
|
prop_oneof![
|
||||||
|
(r.clone(), r.clone()).prop_map(|(k, v)| Data::Map(k, v)),
|
||||||
|
r.clone().prop_map(|x| Data::List(Items::One(x))),
|
||||||
|
prop::collection::vec(r, 1..3).prop_map(|xs| Data::List(Items::Many(xs))),
|
||||||
|
prop::collection::vec(constructor, 1..3).prop_map(Data::AnyOf)
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arbitrary_schema() -> impl Strategy<Value = Schema> {
|
||||||
|
prop_compose! {
|
||||||
|
fn data_strategy()(data in arbitrary_data()) -> Schema {
|
||||||
|
Schema::Data(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let leaf = prop_oneof![
|
||||||
|
Just(Schema::Unit),
|
||||||
|
Just(Schema::Boolean),
|
||||||
|
Just(Schema::Bytes),
|
||||||
|
Just(Schema::Integer),
|
||||||
|
Just(Schema::String),
|
||||||
|
data_strategy(),
|
||||||
|
];
|
||||||
|
|
||||||
|
leaf.prop_recursive(3, 8, 3, |inner| {
|
||||||
|
let r = prop_oneof![
|
||||||
|
".*".prop_map(|s| Declaration::Referenced(Reference::new(&s))),
|
||||||
|
inner.prop_map(|s| Declaration::Inline(Box::new(s)))
|
||||||
|
];
|
||||||
|
prop_oneof![
|
||||||
|
(r.clone(), r.clone()).prop_map(|(l, r)| Schema::Pair(l, r)),
|
||||||
|
r.clone().prop_map(|x| Schema::List(Items::One(x))),
|
||||||
|
prop::collection::vec(r, 1..3).prop_map(|xs| Schema::List(Items::Many(xs))),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn data_serialization_roundtrip(data in arbitrary_data()) {
|
||||||
|
let json = serde_json::to_value(data);
|
||||||
|
let pretty = json
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| serde_json::to_string_pretty(v).unwrap())
|
||||||
|
.unwrap_or_else(|_| "invalid".to_string());
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
json.and_then(serde_json::from_value::<Data>),
|
||||||
|
Ok{..}
|
||||||
|
),
|
||||||
|
"\ncounterexample: {pretty}\n",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn schema_serialization_roundtrip(schema in arbitrary_schema()) {
|
||||||
|
let json = serde_json::to_value(schema);
|
||||||
|
let pretty = json
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| serde_json::to_string_pretty(v).unwrap())
|
||||||
|
.unwrap_or_else(|_| "invalid".to_string());
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
json.and_then(serde_json::from_value::<Schema>),
|
||||||
|
Ok{..}
|
||||||
|
),
|
||||||
|
"\ncounterexample: {pretty}\n",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use super::{
|
use super::{
|
||||||
definitions::{Definitions, Reference},
|
definitions::Definitions,
|
||||||
error::Error,
|
error::Error,
|
||||||
|
parameter::Parameter,
|
||||||
schema::{Annotated, Schema},
|
schema::{Annotated, Schema},
|
||||||
};
|
};
|
||||||
use crate::module::{CheckedModule, CheckedModules};
|
use crate::module::{CheckedModule, CheckedModules};
|
||||||
|
@ -13,44 +14,36 @@ use serde;
|
||||||
use uplc::ast::{DeBruijn, Program, Term};
|
use uplc::ast::{DeBruijn, Program, Term};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Validator<R, S> {
|
pub struct Validator {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub datum: Option<Argument<R>>,
|
pub datum: Option<Parameter>,
|
||||||
|
|
||||||
pub redeemer: Argument<R>,
|
pub redeemer: Parameter,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub parameters: Vec<Argument<R>>,
|
pub parameters: Vec<Parameter>,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub program: Program<DeBruijn>,
|
pub program: Program<DeBruijn>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Definitions::is_empty")]
|
#[serde(skip_serializing_if = "Definitions::is_empty")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub definitions: Definitions<S>,
|
pub definitions: Definitions<Annotated<Schema>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
|
impl Validator {
|
||||||
pub struct Argument<T> {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub title: Option<String>,
|
|
||||||
|
|
||||||
pub schema: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Validator<Reference, Annotated<Schema>> {
|
|
||||||
pub fn from_checked_module(
|
pub fn from_checked_module(
|
||||||
modules: &CheckedModules,
|
modules: &CheckedModules,
|
||||||
generator: &mut CodeGenerator,
|
generator: &mut CodeGenerator,
|
||||||
module: &CheckedModule,
|
module: &CheckedModule,
|
||||||
def: &TypedValidator,
|
def: &TypedValidator,
|
||||||
) -> Vec<Result<Validator<Reference, Annotated<Schema>>, Error>> {
|
) -> Vec<Result<Validator, Error>> {
|
||||||
let program = generator.generate(def).try_into().unwrap();
|
let program = generator.generate(def).try_into().unwrap();
|
||||||
|
|
||||||
let is_multi_validator = def.other_fun.is_some();
|
let is_multi_validator = def.other_fun.is_some();
|
||||||
|
@ -85,7 +78,7 @@ impl Validator<Reference, Annotated<Schema>> {
|
||||||
params: &[TypedArg],
|
params: &[TypedArg],
|
||||||
func: &TypedFunction,
|
func: &TypedFunction,
|
||||||
is_multi_validator: bool,
|
is_multi_validator: bool,
|
||||||
) -> Result<Validator<Reference, Annotated<Schema>>, Error> {
|
) -> Result<Validator, Error> {
|
||||||
let mut args = func.arguments.iter().rev();
|
let mut args = func.arguments.iter().rev();
|
||||||
let (_, redeemer, datum) = (args.next(), args.next().unwrap(), args.next());
|
let (_, redeemer, datum) = (args.next(), args.next().unwrap(), args.next());
|
||||||
|
|
||||||
|
@ -102,7 +95,7 @@ impl Validator<Reference, Annotated<Schema>> {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|param| {
|
.map(|param| {
|
||||||
Annotated::from_type(modules.into(), ¶m.tipo, &mut definitions)
|
Annotated::from_type(modules.into(), ¶m.tipo, &mut definitions)
|
||||||
.map(|schema| Argument {
|
.map(|schema| Parameter {
|
||||||
title: Some(param.arg_name.get_label()),
|
title: Some(param.arg_name.get_label()),
|
||||||
schema,
|
schema,
|
||||||
})
|
})
|
||||||
|
@ -130,7 +123,7 @@ impl Validator<Reference, Annotated<Schema>> {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.map(|schema| Argument {
|
.map(|schema| Parameter {
|
||||||
title: datum.map(|datum| datum.arg_name.get_label()),
|
title: datum.map(|datum| datum.arg_name.get_label()),
|
||||||
schema,
|
schema,
|
||||||
}),
|
}),
|
||||||
|
@ -143,7 +136,7 @@ impl Validator<Reference, Annotated<Schema>> {
|
||||||
module.code.clone(),
|
module.code.clone(),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
.map(|schema| Argument {
|
.map(|schema| Parameter {
|
||||||
title: Some(redeemer.arg_name.get_label()),
|
title: Some(redeemer.arg_name.get_label()),
|
||||||
schema: match datum {
|
schema: match datum {
|
||||||
Some(..) if is_multi_validator => Annotated::as_wrapped_redeemer(
|
Some(..) if is_multi_validator => Annotated::as_wrapped_redeemer(
|
||||||
|
@ -160,16 +153,16 @@ impl Validator<Reference, Annotated<Schema>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, S> Validator<R, S>
|
impl Validator {
|
||||||
where
|
pub fn apply(
|
||||||
S: Clone,
|
self,
|
||||||
R: Clone,
|
definitions: &Definitions<Annotated<Schema>>,
|
||||||
{
|
arg: &Term<DeBruijn>,
|
||||||
pub fn apply(self, arg: &Term<DeBruijn>) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
match self.parameters.split_first() {
|
match self.parameters.split_first() {
|
||||||
None => Err(Error::NoParametersToApply),
|
None => Err(Error::NoParametersToApply),
|
||||||
Some((_, tail)) => {
|
Some((head, tail)) => {
|
||||||
// TODO: Ideally, we should control that the applied term matches its schema.
|
head.validate(definitions, arg)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
program: self.program.apply_term(arg),
|
program: self.program.apply_term(arg),
|
||||||
parameters: tail.to_vec(),
|
parameters: tail.to_vec(),
|
||||||
|
@ -182,7 +175,14 @@ where
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::{
|
||||||
|
super::{
|
||||||
|
definitions::{Definitions, Reference},
|
||||||
|
error::Error,
|
||||||
|
schema::{Annotated, Constructor, Data, Declaration, Items, Schema},
|
||||||
|
},
|
||||||
|
*,
|
||||||
|
};
|
||||||
use crate::{module::ParsedModule, PackageName};
|
use crate::{module::ParsedModule, PackageName};
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
self,
|
self,
|
||||||
|
@ -197,6 +197,7 @@ mod test {
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use serde_json::{self, json};
|
use serde_json::{self, json};
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
use uplc::ast as uplc;
|
||||||
|
|
||||||
// TODO: Possible refactor this out of the module and have it used by `Project`. The idea would
|
// TODO: Possible refactor this out of the module and have it used by `Project`. The idea would
|
||||||
// be to make this struct below the actual project, and wrap it in another metadata struct
|
// be to make this struct below the actual project, and wrap it in another metadata struct
|
||||||
|
@ -318,6 +319,69 @@ mod test {
|
||||||
assert_json_eq!(serde_json::to_value(validator).unwrap(), expected);
|
assert_json_eq!(serde_json::to_value(validator).unwrap(), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mint_basic() {
|
fn mint_basic() {
|
||||||
assert_validator(
|
assert_validator(
|
||||||
|
@ -1096,4 +1160,266 @@ mod test {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_arguments_integer() {
|
||||||
|
let definitions = fixture_definitions();
|
||||||
|
|
||||||
|
let term = Term::data(uplc::Data::integer(42.into()));
|
||||||
|
|
||||||
|
let param = Parameter {
|
||||||
|
title: None,
|
||||||
|
schema: Reference::new("Int"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_arguments_bytestring() {
|
||||||
|
let definitions = fixture_definitions();
|
||||||
|
|
||||||
|
let term = Term::data(uplc::Data::bytestring(vec![102, 111, 111]));
|
||||||
|
|
||||||
|
let param = Parameter {
|
||||||
|
title: None,
|
||||||
|
schema: Reference::new("ByteArray"),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 { .. }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,7 @@ pub mod pretty;
|
||||||
pub mod script;
|
pub mod script;
|
||||||
pub mod telemetry;
|
pub mod telemetry;
|
||||||
|
|
||||||
use crate::blueprint::{
|
use crate::blueprint::Blueprint;
|
||||||
definitions::Reference,
|
|
||||||
schema::{Annotated, Schema},
|
|
||||||
Blueprint,
|
|
||||||
};
|
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
ast::{Definition, Function, ModuleKind, Tracing, TypedDataType, TypedFunction},
|
ast::{Definition, Function, ModuleKind, Tracing, TypedDataType, TypedFunction},
|
||||||
builtins,
|
builtins,
|
||||||
|
@ -218,10 +214,7 @@ where
|
||||||
self.compile(options)
|
self.compile(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dump_uplc(
|
pub fn dump_uplc(&self, blueprint: &Blueprint) -> Result<(), Error> {
|
||||||
&self,
|
|
||||||
blueprint: &Blueprint<Reference, Annotated<Schema>>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let dir = self.root.join("artifacts");
|
let dir = self.root.join("artifacts");
|
||||||
|
|
||||||
self.event_listener
|
self.event_listener
|
||||||
|
@ -362,8 +355,7 @@ where
|
||||||
// Read blueprint
|
// Read blueprint
|
||||||
let blueprint = File::open(self.blueprint_path())
|
let blueprint = File::open(self.blueprint_path())
|
||||||
.map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?;
|
.map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?;
|
||||||
let blueprint: Blueprint<serde_json::Value, serde_json::Value> =
|
let blueprint: Blueprint = serde_json::from_reader(BufReader::new(blueprint))?;
|
||||||
serde_json::from_reader(BufReader::new(blueprint))?;
|
|
||||||
|
|
||||||
// Calculate the address
|
// Calculate the address
|
||||||
let when_too_many =
|
let when_too_many =
|
||||||
|
@ -386,12 +378,11 @@ where
|
||||||
&self,
|
&self,
|
||||||
title: Option<&String>,
|
title: Option<&String>,
|
||||||
param: &Term<DeBruijn>,
|
param: &Term<DeBruijn>,
|
||||||
) -> Result<Blueprint<serde_json::Value, serde_json::Value>, Error> {
|
) -> Result<Blueprint, Error> {
|
||||||
// Read blueprint
|
// Read blueprint
|
||||||
let blueprint = File::open(self.blueprint_path())
|
let blueprint = File::open(self.blueprint_path())
|
||||||
.map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?;
|
.map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?;
|
||||||
let mut blueprint: Blueprint<serde_json::Value, serde_json::Value> =
|
let mut blueprint: Blueprint = serde_json::from_reader(BufReader::new(blueprint))?;
|
||||||
serde_json::from_reader(BufReader::new(blueprint))?;
|
|
||||||
|
|
||||||
// Apply parameters
|
// Apply parameters
|
||||||
let when_too_many =
|
let when_too_many =
|
||||||
|
@ -400,7 +391,9 @@ where
|
||||||
|
|
||||||
let applied_validator =
|
let applied_validator =
|
||||||
blueprint.with_validator(title, when_too_many, when_missing, |validator| {
|
blueprint.with_validator(title, when_too_many, when_missing, |validator| {
|
||||||
validator.apply(param).map_err(|e| e.into())
|
validator
|
||||||
|
.apply(&blueprint.definitions, param)
|
||||||
|
.map_err(|e| e.into())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Overwrite validator
|
// Overwrite validator
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
use crate::with_project;
|
use crate::with_project;
|
||||||
use aiken_project::error::Error;
|
use aiken_project::{blueprint, error::Error};
|
||||||
use miette::IntoDiagnostic;
|
use owo_colors::{OwoColorize, Stream::Stderr};
|
||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf, process, rc::Rc};
|
||||||
use uplc::{
|
use uplc::ast::{Constant, DeBruijn, Term};
|
||||||
ast::{DeBruijn, Term},
|
|
||||||
parser,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Apply a parameter to a parameterized validator.
|
/// Apply a parameter to a parameterized validator.
|
||||||
#[derive(clap::Args)]
|
#[derive(clap::Args)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
|
/// The parameter, as a Plutus Data (CBOR, hex-encoded)
|
||||||
|
parameter: String,
|
||||||
|
|
||||||
/// Path to project
|
/// Path to project
|
||||||
directory: Option<PathBuf>,
|
directory: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Output file. Optional, print on stdout when omitted.
|
||||||
|
#[clap(short, long)]
|
||||||
|
out: Option<PathBuf>,
|
||||||
|
|
||||||
/// Name of the validator's module within the project. Optional if there's only one validator.
|
/// Name of the validator's module within the project. Optional if there's only one validator.
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
module: Option<String>,
|
module: Option<String>,
|
||||||
|
@ -20,23 +24,58 @@ pub struct Args {
|
||||||
/// Name of the validator within the module. Optional if there's only one validator.
|
/// Name of the validator within the module. Optional if there's only one validator.
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
validator: Option<String>,
|
validator: Option<String>,
|
||||||
|
|
||||||
/// The parameter, using high-level UPLC-syntax
|
|
||||||
parameter: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exec(
|
pub fn exec(
|
||||||
Args {
|
Args {
|
||||||
|
parameter,
|
||||||
directory,
|
directory,
|
||||||
|
out,
|
||||||
module,
|
module,
|
||||||
validator,
|
validator,
|
||||||
parameter,
|
|
||||||
}: Args,
|
}: Args,
|
||||||
) -> miette::Result<()> {
|
) -> miette::Result<()> {
|
||||||
let term: Term<DeBruijn> = parser::term(¶meter)
|
eprintln!(
|
||||||
.into_diagnostic()?
|
"{} inputs",
|
||||||
.try_into()
|
" Parsing"
|
||||||
.into_diagnostic()?;
|
.if_supports_color(Stderr, |s| s.purple())
|
||||||
|
.if_supports_color(Stderr, |s| s.bold()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let bytes = hex::decode(parameter)
|
||||||
|
.map_err::<Error, _>(|e| {
|
||||||
|
blueprint::error::Error::MalformedParameter {
|
||||||
|
hint: format!("Invalid hex-encoded string: {e}"),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
println!();
|
||||||
|
e.report();
|
||||||
|
process::exit(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = uplc::plutus_data(&bytes)
|
||||||
|
.map_err::<Error, _>(|e| {
|
||||||
|
blueprint::error::Error::MalformedParameter {
|
||||||
|
hint: format!("Invalid Plutus data; malformed CBOR encoding: {e}"),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
println!();
|
||||||
|
e.report();
|
||||||
|
process::exit(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
let term: Term<DeBruijn> = Term::Constant(Rc::new(Constant::Data(data)));
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"{} blueprint",
|
||||||
|
" Analyzing"
|
||||||
|
.if_supports_color(Stderr, |s| s.purple())
|
||||||
|
.if_supports_color(Stderr, |s| s.bold()),
|
||||||
|
);
|
||||||
|
|
||||||
with_project(directory, |p| {
|
with_project(directory, |p| {
|
||||||
let title = module.as_ref().map(|m| {
|
let title = module.as_ref().map(|m| {
|
||||||
|
@ -51,16 +90,35 @@ pub fn exec(
|
||||||
|
|
||||||
let title = title.as_ref().or(validator.as_ref());
|
let title = title.as_ref().or(validator.as_ref());
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"{} parameter",
|
||||||
|
" Applying"
|
||||||
|
.if_supports_color(Stderr, |s| s.purple())
|
||||||
|
.if_supports_color(Stderr, |s| s.bold()),
|
||||||
|
);
|
||||||
|
|
||||||
let blueprint = p.apply_parameter(title, &term)?;
|
let blueprint = p.apply_parameter(title, &term)?;
|
||||||
|
|
||||||
let json = serde_json::to_string_pretty(&blueprint).unwrap();
|
let json = serde_json::to_string_pretty(&blueprint).unwrap();
|
||||||
|
|
||||||
fs::write(p.blueprint_path(), json).map_err(|error| {
|
match out {
|
||||||
Error::FileIo {
|
None => {
|
||||||
|
println!("\n{}\n", json);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Some(ref path) => fs::write(path, json).map_err(|error| Error::FileIo {
|
||||||
error,
|
error,
|
||||||
path: p.blueprint_path(),
|
path: p.blueprint_path(),
|
||||||
}
|
}),
|
||||||
.into()
|
}?;
|
||||||
})
|
|
||||||
|
eprintln!(
|
||||||
|
"{}",
|
||||||
|
" Done"
|
||||||
|
.if_supports_color(Stderr, |s| s.purple())
|
||||||
|
.if_supports_color(Stderr, |s| s.bold()),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ pub fn exec(
|
||||||
.map_err(|_| BlueprintError::InvalidOrMissingFile)
|
.map_err(|_| BlueprintError::InvalidOrMissingFile)
|
||||||
.into_diagnostic()?;
|
.into_diagnostic()?;
|
||||||
|
|
||||||
let blueprint: Blueprint<serde_json::Value, serde_json::Value> =
|
let blueprint: Blueprint =
|
||||||
serde_json::from_reader(BufReader::new(blueprint)).into_diagnostic()?;
|
serde_json::from_reader(BufReader::new(blueprint)).into_diagnostic()?;
|
||||||
|
|
||||||
// Perform the conversion
|
// Perform the conversion
|
||||||
|
|
|
@ -1,23 +1,3 @@
|
||||||
use std::{
|
|
||||||
fmt::{self, Display},
|
|
||||||
hash::{self, Hash},
|
|
||||||
rc::Rc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use num_bigint::BigInt;
|
|
||||||
use serde::{
|
|
||||||
self,
|
|
||||||
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
|
|
||||||
ser::{Serialize, SerializeStruct, Serializer},
|
|
||||||
};
|
|
||||||
|
|
||||||
use pallas_addresses::{Network, ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart};
|
|
||||||
use pallas_primitives::{
|
|
||||||
alonzo::PlutusData,
|
|
||||||
babbage::{self as cardano, Language},
|
|
||||||
};
|
|
||||||
use pallas_traverse::ComputeHash;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtins::DefaultFunction,
|
builtins::DefaultFunction,
|
||||||
debruijn::{self, Converter},
|
debruijn::{self, Converter},
|
||||||
|
@ -28,6 +8,24 @@ use crate::{
|
||||||
Machine,
|
Machine,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use num_bigint::BigInt;
|
||||||
|
use num_traits::ToPrimitive;
|
||||||
|
use pallas_addresses::{Network, ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart};
|
||||||
|
use pallas_primitives::{
|
||||||
|
alonzo::{self as pallas, Constr, PlutusData},
|
||||||
|
babbage::{self as cardano, Language},
|
||||||
|
};
|
||||||
|
use pallas_traverse::ComputeHash;
|
||||||
|
use serde::{
|
||||||
|
self,
|
||||||
|
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
|
||||||
|
ser::{Serialize, SerializeStruct, Serializer},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
fmt::{self, Display},
|
||||||
|
hash::{self, Hash},
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
/// This represents a program in Untyped Plutus Core.
|
/// This represents a program in Untyped Plutus Core.
|
||||||
/// A program contains a version tuple and a term.
|
/// A program contains a version tuple and a term.
|
||||||
|
@ -239,6 +237,61 @@ pub enum Constant {
|
||||||
Data(PlutusData),
|
Data(PlutusData),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Data {}
|
||||||
|
|
||||||
|
// TODO: See about moving these builders upstream to Pallas?
|
||||||
|
impl Data {
|
||||||
|
pub fn integer(i: BigInt) -> PlutusData {
|
||||||
|
match i.to_i64() {
|
||||||
|
Some(i) => PlutusData::BigInt(pallas::BigInt::Int(i.into())),
|
||||||
|
None => {
|
||||||
|
let (sign, bytes) = i.to_bytes_be();
|
||||||
|
match sign {
|
||||||
|
num_bigint::Sign::Minus => {
|
||||||
|
PlutusData::BigInt(pallas::BigInt::BigNInt(bytes.into()))
|
||||||
|
}
|
||||||
|
_ => PlutusData::BigInt(pallas::BigInt::BigUInt(bytes.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bytestring(bytes: Vec<u8>) -> PlutusData {
|
||||||
|
PlutusData::BoundedBytes(bytes.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map(kvs: Vec<(PlutusData, PlutusData)>) -> PlutusData {
|
||||||
|
PlutusData::Map(kvs.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(xs: Vec<PlutusData>) -> PlutusData {
|
||||||
|
PlutusData::Array(xs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn constr(ix: u64, fields: Vec<PlutusData>) -> PlutusData {
|
||||||
|
// NOTE: see https://github.com/input-output-hk/plutus/blob/9538fc9829426b2ecb0628d352e2d7af96ec8204/plutus-core/plutus-core/src/PlutusCore/Data.hs#L139-L155
|
||||||
|
if ix < 7 {
|
||||||
|
PlutusData::Constr(Constr {
|
||||||
|
tag: 121 + ix,
|
||||||
|
any_constructor: None,
|
||||||
|
fields,
|
||||||
|
})
|
||||||
|
} else if ix < 128 {
|
||||||
|
PlutusData::Constr(Constr {
|
||||||
|
tag: 1280 + ix - 7,
|
||||||
|
any_constructor: None,
|
||||||
|
fields,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
PlutusData::Constr(Constr {
|
||||||
|
tag: 102,
|
||||||
|
any_constructor: Some(ix),
|
||||||
|
fields,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Type {
|
pub enum Type {
|
||||||
Bool,
|
Bool,
|
||||||
|
|
|
@ -2,13 +2,14 @@ use crate::{
|
||||||
ast::{Constant, Name, Term, Type},
|
ast::{Constant, Name, Term, Type},
|
||||||
builtins::DefaultFunction,
|
builtins::DefaultFunction,
|
||||||
};
|
};
|
||||||
|
use pallas_primitives::alonzo::PlutusData;
|
||||||
|
|
||||||
pub const CONSTR_FIELDS_EXPOSER: &str = "__constr_fields_exposer";
|
pub const CONSTR_FIELDS_EXPOSER: &str = "__constr_fields_exposer";
|
||||||
pub const CONSTR_INDEX_EXPOSER: &str = "__constr_index_exposer";
|
pub const CONSTR_INDEX_EXPOSER: &str = "__constr_index_exposer";
|
||||||
pub const CONSTR_GET_FIELD: &str = "__constr_get_field";
|
pub const CONSTR_GET_FIELD: &str = "__constr_get_field";
|
||||||
pub const EXPECT_ON_LIST: &str = "__expect_on_list";
|
pub const EXPECT_ON_LIST: &str = "__expect_on_list";
|
||||||
|
|
||||||
impl Term<Name> {
|
impl<T> Term<T> {
|
||||||
pub fn apply(self, arg: Self) -> Self {
|
pub fn apply(self, arg: Self) -> Self {
|
||||||
Term::Apply {
|
Term::Apply {
|
||||||
function: self.into(),
|
function: self.into(),
|
||||||
|
@ -16,13 +17,6 @@ impl Term<Name> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lambda(self, parameter_name: impl ToString) -> Self {
|
|
||||||
Term::Lambda {
|
|
||||||
parameter_name: Name::text(parameter_name).into(),
|
|
||||||
body: self.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn force(self) -> Self {
|
pub fn force(self) -> Self {
|
||||||
Term::Force(self.into())
|
Term::Force(self.into())
|
||||||
}
|
}
|
||||||
|
@ -31,10 +25,6 @@ impl Term<Name> {
|
||||||
Term::Delay(self.into())
|
Term::Delay(self.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn var(name: impl ToString) -> Self {
|
|
||||||
Term::Var(Name::text(name).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn integer(i: num_bigint::BigInt) -> Self {
|
pub fn integer(i: num_bigint::BigInt) -> Self {
|
||||||
Term::Constant(Constant::Integer(i).into())
|
Term::Constant(Constant::Integer(i).into())
|
||||||
}
|
}
|
||||||
|
@ -55,6 +45,10 @@ impl Term<Name> {
|
||||||
Term::Constant(Constant::Unit.into())
|
Term::Constant(Constant::Unit.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn data(d: PlutusData) -> Self {
|
||||||
|
Term::Constant(Constant::Data(d).into())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn empty_list() -> Self {
|
pub fn empty_list() -> Self {
|
||||||
Term::Constant(Constant::ProtoList(Type::Data, vec![]).into())
|
Term::Constant(Constant::ProtoList(Type::Data, vec![]).into())
|
||||||
}
|
}
|
||||||
|
@ -204,7 +198,7 @@ impl Term<Name> {
|
||||||
.force()
|
.force()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn trace(self, msg_term: Term<Name>) -> Self {
|
pub fn trace(self, msg_term: Self) -> Self {
|
||||||
Term::Builtin(DefaultFunction::Trace)
|
Term::Builtin(DefaultFunction::Trace)
|
||||||
.force()
|
.force()
|
||||||
.apply(msg_term)
|
.apply(msg_term)
|
||||||
|
@ -212,41 +206,34 @@ impl Term<Name> {
|
||||||
.force()
|
.force()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assert_on_list(self) -> Term<Name> {
|
pub fn final_wrapper(self) -> Self {
|
||||||
self.lambda(EXPECT_ON_LIST.to_string())
|
|
||||||
.apply(
|
|
||||||
Term::var(EXPECT_ON_LIST.to_string()).apply(Term::var(EXPECT_ON_LIST.to_string())),
|
|
||||||
)
|
|
||||||
.lambda(EXPECT_ON_LIST.to_string())
|
|
||||||
.apply(
|
|
||||||
Term::var("__list_to_check".to_string())
|
|
||||||
.delayed_choose_list(
|
|
||||||
Term::unit(),
|
|
||||||
Term::var("__check_with".to_string())
|
|
||||||
.apply(
|
|
||||||
Term::head_list().apply(Term::var("__list_to_check".to_string())),
|
|
||||||
)
|
|
||||||
.choose_unit(
|
|
||||||
Term::var(EXPECT_ON_LIST.to_string())
|
|
||||||
.apply(Term::var(EXPECT_ON_LIST.to_string()))
|
|
||||||
.apply(
|
|
||||||
Term::tail_list()
|
|
||||||
.apply(Term::var("__list_to_check".to_string())),
|
|
||||||
)
|
|
||||||
.apply(Term::var("__check_with".to_string())),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.lambda("__check_with".to_string())
|
|
||||||
.lambda("__list_to_check".to_string())
|
|
||||||
.lambda(EXPECT_ON_LIST),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn final_wrapper(self: Term<Name>) -> Term<Name> {
|
|
||||||
self.delayed_if_else(Term::unit(), Term::Error)
|
self.delayed_if_else(Term::unit(), Term::Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn constr_fields_exposer(self: Term<Name>) -> Term<Name> {
|
pub fn repeat_tail_list(self, repeat: usize) -> Self {
|
||||||
|
let mut term = self;
|
||||||
|
|
||||||
|
for _ in 0..repeat {
|
||||||
|
term = Term::tail_list().apply(term);
|
||||||
|
}
|
||||||
|
|
||||||
|
term
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Term<Name> {
|
||||||
|
pub fn lambda(self, parameter_name: impl ToString) -> Self {
|
||||||
|
Term::Lambda {
|
||||||
|
parameter_name: Name::text(parameter_name).into(),
|
||||||
|
body: self.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn var(name: impl ToString) -> Self {
|
||||||
|
Term::Var(Name::text(name).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn constr_fields_exposer(self) -> Self {
|
||||||
self.lambda(CONSTR_FIELDS_EXPOSER.to_string()).apply(
|
self.lambda(CONSTR_FIELDS_EXPOSER.to_string()).apply(
|
||||||
Term::snd_pair()
|
Term::snd_pair()
|
||||||
.apply(Term::unconstr_data().apply(Term::var("__constr_var".to_string())))
|
.apply(Term::unconstr_data().apply(Term::var("__constr_var".to_string())))
|
||||||
|
@ -254,7 +241,7 @@ impl Term<Name> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn constr_index_exposer(self: Term<Name>) -> Term<Name> {
|
pub fn constr_index_exposer(self) -> Self {
|
||||||
self.lambda(CONSTR_INDEX_EXPOSER.to_string()).apply(
|
self.lambda(CONSTR_INDEX_EXPOSER.to_string()).apply(
|
||||||
Term::fst_pair()
|
Term::fst_pair()
|
||||||
.apply(Term::unconstr_data().apply(Term::var("__constr_var".to_string())))
|
.apply(Term::unconstr_data().apply(Term::var("__constr_var".to_string())))
|
||||||
|
@ -262,7 +249,7 @@ impl Term<Name> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn constr_get_field(self: Term<Name>) -> Term<Name> {
|
pub fn constr_get_field(self) -> Self {
|
||||||
self.lambda(CONSTR_GET_FIELD.to_string())
|
self.lambda(CONSTR_GET_FIELD.to_string())
|
||||||
.apply(
|
.apply(
|
||||||
Term::var(CONSTR_GET_FIELD.to_string())
|
Term::var(CONSTR_GET_FIELD.to_string())
|
||||||
|
@ -298,13 +285,33 @@ impl Term<Name> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn repeat_tail_list(self: Term<Name>, repeat: usize) -> Term<Name> {
|
pub fn assert_on_list(self) -> Self {
|
||||||
let mut term = self;
|
self.lambda(EXPECT_ON_LIST.to_string())
|
||||||
|
.apply(
|
||||||
for _ in 0..repeat {
|
Term::var(EXPECT_ON_LIST.to_string()).apply(Term::var(EXPECT_ON_LIST.to_string())),
|
||||||
term = Term::tail_list().apply(term);
|
)
|
||||||
}
|
.lambda(EXPECT_ON_LIST.to_string())
|
||||||
|
.apply(
|
||||||
term
|
Term::var("__list_to_check".to_string())
|
||||||
|
.delayed_choose_list(
|
||||||
|
Term::unit(),
|
||||||
|
Term::var("__check_with".to_string())
|
||||||
|
.apply(
|
||||||
|
Term::head_list().apply(Term::var("__list_to_check".to_string())),
|
||||||
|
)
|
||||||
|
.choose_unit(
|
||||||
|
Term::var(EXPECT_ON_LIST.to_string())
|
||||||
|
.apply(Term::var(EXPECT_ON_LIST.to_string()))
|
||||||
|
.apply(
|
||||||
|
Term::tail_list()
|
||||||
|
.apply(Term::var("__list_to_check".to_string())),
|
||||||
|
)
|
||||||
|
.apply(Term::var("__check_with".to_string())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.lambda("__check_with".to_string())
|
||||||
|
.lambda("__list_to_check".to_string())
|
||||||
|
.lambda(EXPECT_ON_LIST),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue