1440 lines
50 KiB
Rust
1440 lines
50 KiB
Rust
use crate::{
|
|
blueprint::definitions::{Definitions, Reference},
|
|
CheckedModule,
|
|
};
|
|
use aiken_lang::{
|
|
ast::{Definition, TypedDataType, TypedDefinition},
|
|
builtins::wrapped_redeemer,
|
|
tipo::{pretty, Type, TypeVar},
|
|
};
|
|
use owo_colors::{OwoColorize, Stream::Stdout};
|
|
use serde::{
|
|
self,
|
|
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
|
|
ser::{Serialize, SerializeStruct, Serializer},
|
|
};
|
|
use serde_json as json;
|
|
use std::{collections::HashMap, fmt, ops::Deref, rc::Rc};
|
|
|
|
// NOTE: Can be anything BUT 0
|
|
pub const REDEEMER_DISCRIMINANT: usize = 1;
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
|
|
pub struct Annotated<T> {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub title: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub description: Option<String>,
|
|
#[serde(flatten)]
|
|
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.
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
pub enum Schema {
|
|
Unit,
|
|
Boolean,
|
|
Integer,
|
|
Bytes,
|
|
String,
|
|
Pair(Declaration<Schema>, Declaration<Schema>),
|
|
List(Items<Schema>),
|
|
Data(Data),
|
|
}
|
|
|
|
/// A schema for Plutus' Data.
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
pub enum Data {
|
|
Integer,
|
|
Bytes,
|
|
List(Items<Data>),
|
|
Map(Declaration<Data>, Declaration<Data>),
|
|
AnyOf(Vec<Annotated<Constructor>>),
|
|
Opaque,
|
|
}
|
|
|
|
/// A structure that represents either one or many elements.
|
|
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum Items<T> {
|
|
One(Declaration<T>),
|
|
Many(Vec<Declaration<T>>),
|
|
}
|
|
|
|
/// Captures a single UPLC constructor with its
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
pub struct Constructor {
|
|
pub index: usize,
|
|
pub fields: Vec<Annotated<Declaration<Data>>>,
|
|
}
|
|
|
|
impl<T> From<T> for Annotated<T> {
|
|
fn from(annotated: T) -> Self {
|
|
Annotated {
|
|
title: None,
|
|
description: None,
|
|
annotated,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Annotated<Schema> {
|
|
pub fn as_wrapped_redeemer(
|
|
definitions: &mut Definitions<Annotated<Schema>>,
|
|
schema: Reference,
|
|
type_info: Rc<Type>,
|
|
) -> Reference {
|
|
definitions
|
|
.register(
|
|
&wrapped_redeemer(type_info),
|
|
&HashMap::new(),
|
|
|_| {
|
|
Ok::<_, Error>(Annotated {
|
|
title: Some("Wrapped Redeemer".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 {
|
|
index: REDEEMER_DISCRIMINANT,
|
|
fields: vec![Declaration::Referenced(schema).into()],
|
|
}
|
|
.into()])),
|
|
})
|
|
},
|
|
)
|
|
.expect("cannot fail because Ok")
|
|
}
|
|
|
|
pub fn from_type(
|
|
modules: &HashMap<String, CheckedModule>,
|
|
type_info: &Type,
|
|
definitions: &mut Definitions<Self>,
|
|
) -> Result<Reference, Error> {
|
|
Annotated::do_from_type(type_info, modules, &mut HashMap::new(), definitions)
|
|
}
|
|
|
|
fn do_from_type(
|
|
type_info: &Type,
|
|
modules: &HashMap<String, CheckedModule>,
|
|
type_parameters: &mut HashMap<u64, Rc<Type>>,
|
|
definitions: &mut Definitions<Self>,
|
|
) -> Result<Reference, Error> {
|
|
match type_info {
|
|
Type::App {
|
|
module: module_name,
|
|
name: type_name,
|
|
args,
|
|
..
|
|
} if module_name.is_empty() => {
|
|
definitions.register(type_info, &type_parameters.clone(), |definitions| {
|
|
match &type_name[..] {
|
|
"Data" => Ok(Annotated {
|
|
title: Some("Data".to_string()),
|
|
description: Some("Any Plutus data.".to_string()),
|
|
annotated: Schema::Data(Data::Opaque),
|
|
}),
|
|
|
|
"ByteArray" => Ok(Schema::Data(Data::Bytes).into()),
|
|
|
|
"Int" => Ok(Schema::Data(Data::Integer).into()),
|
|
|
|
"String" => Ok(Schema::String.into()),
|
|
|
|
"Void" => Ok(Annotated {
|
|
title: Some("Unit".to_string()),
|
|
description: Some("The nullary constructor.".to_string()),
|
|
annotated: Schema::Data(Data::AnyOf(vec![Annotated {
|
|
title: None,
|
|
description: None,
|
|
annotated: Constructor {
|
|
index: 0,
|
|
fields: vec![],
|
|
},
|
|
}])),
|
|
}),
|
|
|
|
"Bool" => Ok(Annotated {
|
|
title: Some("Bool".to_string()),
|
|
description: None,
|
|
annotated: Schema::Data(Data::AnyOf(vec![
|
|
Annotated {
|
|
title: Some("False".to_string()),
|
|
description: None,
|
|
annotated: Constructor {
|
|
index: 0,
|
|
fields: vec![],
|
|
},
|
|
},
|
|
Annotated {
|
|
title: Some("True".to_string()),
|
|
description: None,
|
|
annotated: Constructor {
|
|
index: 1,
|
|
fields: vec![],
|
|
},
|
|
},
|
|
])),
|
|
}),
|
|
|
|
"Ordering" => Ok(Annotated {
|
|
title: Some("Ordering".to_string()),
|
|
description: None,
|
|
annotated: Schema::Data(Data::AnyOf(vec![
|
|
Annotated {
|
|
title: Some("Less".to_string()),
|
|
description: None,
|
|
annotated: Constructor {
|
|
index: 0,
|
|
fields: vec![],
|
|
},
|
|
},
|
|
Annotated {
|
|
title: Some("Equal".to_string()),
|
|
description: None,
|
|
annotated: Constructor {
|
|
index: 1,
|
|
fields: vec![],
|
|
},
|
|
},
|
|
Annotated {
|
|
title: Some("Greater".to_string()),
|
|
description: None,
|
|
annotated: Constructor {
|
|
index: 2,
|
|
fields: vec![],
|
|
},
|
|
},
|
|
])),
|
|
}),
|
|
|
|
"Option" => {
|
|
let generic = Annotated::do_from_type(
|
|
args.first()
|
|
.expect("Option types have always one generic argument"),
|
|
modules,
|
|
type_parameters,
|
|
definitions,
|
|
)?;
|
|
|
|
Ok(Annotated {
|
|
title: Some("Optional".to_string()),
|
|
description: None,
|
|
annotated: Schema::Data(Data::AnyOf(vec![
|
|
Annotated {
|
|
title: Some("Some".to_string()),
|
|
description: Some("An optional value.".to_string()),
|
|
annotated: Constructor {
|
|
index: 0,
|
|
fields: vec![Declaration::Referenced(generic).into()],
|
|
},
|
|
},
|
|
Annotated {
|
|
title: Some("None".to_string()),
|
|
description: Some("Nothing.".to_string()),
|
|
annotated: Constructor {
|
|
index: 1,
|
|
fields: vec![],
|
|
},
|
|
},
|
|
])),
|
|
})
|
|
}
|
|
|
|
"List" => {
|
|
let generic = Annotated::do_from_type(
|
|
args.first()
|
|
.expect("List types have always one generic argument"),
|
|
modules,
|
|
type_parameters,
|
|
definitions,
|
|
)?;
|
|
|
|
// NOTE: Lists of 2-tuples are treated as Maps. This is an oddity we inherit
|
|
// from the PlutusTx / LedgerApi Haskell codebase, which encodes some elements
|
|
// as such. We don't have a concept of language maps in Aiken, so we simply
|
|
// make all types abide by this convention.
|
|
let data = match definitions.try_lookup(&generic).cloned() {
|
|
Some(Annotated {
|
|
annotated: Schema::Data(Data::List(Items::Many(xs))),
|
|
..
|
|
}) if xs.len() == 2 => {
|
|
definitions.remove(&generic);
|
|
Data::Map(
|
|
xs.first()
|
|
.expect("length (== 2) checked in pattern clause")
|
|
.to_owned(),
|
|
xs.last()
|
|
.expect("length (== 2) checked in pattern clause")
|
|
.to_owned(),
|
|
)
|
|
}
|
|
|
|
_ => Data::List(Items::One(Declaration::Referenced(generic))),
|
|
};
|
|
|
|
Ok(Schema::Data(data).into())
|
|
}
|
|
|
|
_ => Err(Error::new(ErrorContext::UnsupportedType, type_info)),
|
|
}
|
|
})
|
|
}
|
|
|
|
Type::App {
|
|
name, module, args, ..
|
|
} => definitions.register(type_info, &type_parameters.clone(), |definitions| {
|
|
let module = modules
|
|
.get(module)
|
|
.unwrap_or_else(|| panic!("unknown module '{module}'\n\n{modules:?}"));
|
|
|
|
let data_type =
|
|
find_data_type(name, &module.ast.definitions).unwrap_or_else(|| {
|
|
panic!(
|
|
"unknown data-type for '{name:?}' \n\n{definitions:?}",
|
|
definitions = module.ast.definitions
|
|
)
|
|
});
|
|
|
|
collect_type_parameters(type_parameters, &data_type.typed_parameters, args);
|
|
|
|
let annotated = Schema::Data(
|
|
Data::from_data_type(&data_type, modules, type_parameters, definitions)
|
|
.map_err(|e| e.backtrack(type_info))?,
|
|
);
|
|
|
|
Ok(Annotated {
|
|
title: Some(data_type.name.clone()),
|
|
description: data_type.doc.clone().map(|s| s.trim().to_string()),
|
|
annotated,
|
|
})
|
|
}),
|
|
Type::Tuple { elems, .. } => {
|
|
definitions.register(type_info, &type_parameters.clone(), |definitions| {
|
|
let elems = elems
|
|
.iter()
|
|
.map(|elem| {
|
|
Annotated::do_from_type(elem, modules, type_parameters, definitions)
|
|
.map(Declaration::Referenced)
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.map_err(|e| e.backtrack(type_info))?;
|
|
|
|
Ok(Annotated {
|
|
title: Some("Tuple".to_owned()),
|
|
description: None,
|
|
annotated: Schema::Data(Data::List(Items::Many(elems))),
|
|
})
|
|
})
|
|
}
|
|
Type::Var { tipo, .. } => match tipo.borrow().deref() {
|
|
TypeVar::Link { tipo } => {
|
|
Annotated::do_from_type(tipo, modules, type_parameters, definitions)
|
|
}
|
|
TypeVar::Generic { id } => {
|
|
let tipo = type_parameters
|
|
.get(id)
|
|
.ok_or_else(|| Error::new(ErrorContext::FreeTypeVariable, type_info))?
|
|
.clone();
|
|
Annotated::do_from_type(&tipo, modules, type_parameters, definitions)
|
|
}
|
|
TypeVar::Unbound { .. } => {
|
|
Err(Error::new(ErrorContext::UnboundTypeVariable, type_info))
|
|
}
|
|
},
|
|
Type::Fn { .. } => unreachable!(),
|
|
Type::Pair { .. } => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Data {
|
|
fn from_data_type(
|
|
data_type: &TypedDataType,
|
|
modules: &HashMap<String, CheckedModule>,
|
|
type_parameters: &mut HashMap<u64, Rc<Type>>,
|
|
definitions: &mut Definitions<Annotated<Schema>>,
|
|
) -> Result<Self, Error> {
|
|
if data_type.opaque {
|
|
// NOTE: No breadcrumbs here which is *okay*, as the caller will backtrack
|
|
// and add the necessary type information.
|
|
return Err(Error {
|
|
context: ErrorContext::IllegalOpaqueType,
|
|
breadcrumbs: vec![],
|
|
});
|
|
}
|
|
|
|
let mut variants = vec![];
|
|
|
|
for (index, constructor) in data_type.constructors.iter().enumerate() {
|
|
let mut fields = vec![];
|
|
|
|
for field in constructor.arguments.iter() {
|
|
let reference =
|
|
Annotated::do_from_type(&field.tipo, modules, type_parameters, definitions)?;
|
|
|
|
fields.push(Annotated {
|
|
title: field.label.clone(),
|
|
description: field.doc.clone().map(|s| s.trim().to_string()),
|
|
annotated: Declaration::Referenced(reference),
|
|
});
|
|
}
|
|
|
|
let variant = Annotated {
|
|
title: Some(constructor.name.clone()),
|
|
description: constructor.doc.clone().map(|s| s.trim().to_string()),
|
|
annotated: Constructor { index, fields },
|
|
};
|
|
|
|
variants.push(variant);
|
|
}
|
|
|
|
Ok(Data::AnyOf(variants))
|
|
}
|
|
}
|
|
|
|
fn collect_type_parameters<'a>(
|
|
type_parameters: &'a mut HashMap<u64, Rc<Type>>,
|
|
generics: &'a [Rc<Type>],
|
|
applications: &'a [Rc<Type>],
|
|
) {
|
|
for (index, generic) in generics.iter().enumerate() {
|
|
match &**generic {
|
|
Type::Var { tipo, .. } => match *tipo.borrow() {
|
|
TypeVar::Generic { id } => {
|
|
type_parameters.insert(
|
|
id,
|
|
applications
|
|
.get(index)
|
|
.unwrap_or_else(|| panic!("Couldn't find generic identifier ({id}) in applied types: {applications:?}"))
|
|
.to_owned()
|
|
);
|
|
}
|
|
_ => unreachable!(),
|
|
},
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn find_data_type(name: &str, definitions: &[TypedDefinition]) -> Option<TypedDataType> {
|
|
for def in definitions {
|
|
match def {
|
|
Definition::DataType(data_type) if name == data_type.name => {
|
|
return Some(data_type.clone());
|
|
}
|
|
Definition::Fn { .. }
|
|
| Definition::Validator { .. }
|
|
| Definition::DataType { .. }
|
|
| Definition::TypeAlias { .. }
|
|
| Definition::Use { .. }
|
|
| Definition::ModuleConstant { .. }
|
|
| Definition::Test { .. } => continue,
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
// Needed because of Blueprint's default, but actually never used.
|
|
impl Default for Annotated<Schema> {
|
|
fn default() -> Self {
|
|
Schema::Data(Data::Opaque).into()
|
|
}
|
|
}
|
|
|
|
impl Serialize for Schema {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
match self {
|
|
Schema::Unit => {
|
|
let mut s = serializer.serialize_struct("Unit", 1)?;
|
|
s.serialize_field("dataType", "#unit")?;
|
|
s.end()
|
|
}
|
|
Schema::Boolean => {
|
|
let mut s = serializer.serialize_struct("Boolean", 1)?;
|
|
s.serialize_field("dataType", "#boolean")?;
|
|
s.end()
|
|
}
|
|
Schema::Integer => {
|
|
let mut s = serializer.serialize_struct("Integer", 1)?;
|
|
s.serialize_field("dataType", "#integer")?;
|
|
s.end()
|
|
}
|
|
Schema::Bytes => {
|
|
let mut s = serializer.serialize_struct("Bytes", 1)?;
|
|
s.serialize_field("dataType", "#bytes")?;
|
|
s.end()
|
|
}
|
|
Schema::String => {
|
|
let mut s = serializer.serialize_struct("String", 1)?;
|
|
s.serialize_field("dataType", "#string")?;
|
|
s.end()
|
|
}
|
|
Schema::Pair(left, right) => {
|
|
let mut s = serializer.serialize_struct("Pair", 3)?;
|
|
s.serialize_field("dataType", "#pair")?;
|
|
s.serialize_field("left", &left)?;
|
|
s.serialize_field("right", &right)?;
|
|
s.end()
|
|
}
|
|
Schema::List(items) => {
|
|
let mut s = serializer.serialize_struct("List", 2)?;
|
|
s.serialize_field("dataType", "#list")?;
|
|
s.serialize_field("items", &items)?;
|
|
s.end()
|
|
}
|
|
Schema::Data(data) => data.serialize(serializer),
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
match self {
|
|
Data::Opaque => {
|
|
let s = serializer.serialize_struct("Opaque", 0)?;
|
|
s.end()
|
|
}
|
|
Data::Integer => {
|
|
let mut s = serializer.serialize_struct("Integer", 1)?;
|
|
s.serialize_field("dataType", "integer")?;
|
|
s.end()
|
|
}
|
|
Data::Bytes => {
|
|
let mut s = serializer.serialize_struct("Bytes", 1)?;
|
|
s.serialize_field("dataType", "bytes")?;
|
|
s.end()
|
|
}
|
|
Data::List(items) => {
|
|
let mut s = serializer.serialize_struct("List", 2)?;
|
|
s.serialize_field("dataType", "list")?;
|
|
s.serialize_field("items", &items)?;
|
|
s.end()
|
|
}
|
|
Data::Map(keys, values) => {
|
|
let mut s = serializer.serialize_struct("Map", 3)?;
|
|
s.serialize_field("dataType", "map")?;
|
|
s.serialize_field("keys", &keys)?;
|
|
s.serialize_field("values", &values)?;
|
|
s.end()
|
|
}
|
|
Data::AnyOf(constructors) => {
|
|
let mut s = serializer.serialize_struct("AnyOf", 1)?;
|
|
s.serialize_field("anyOf", &constructors)?;
|
|
s.end()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
let mut s = serializer.serialize_struct("Constructor", 3)?;
|
|
s.serialize_field("dataType", "constructor")?;
|
|
s.serialize_field("index", &self.index)?;
|
|
s.serialize_field("fields", &self.fields)?;
|
|
s.end()
|
|
}
|
|
}
|
|
|
|
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)]
|
|
#[error("{}", context)]
|
|
pub struct Error {
|
|
context: ErrorContext,
|
|
breadcrumbs: Vec<Type>,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Clone, thiserror::Error)]
|
|
pub enum ErrorContext {
|
|
#[error(
|
|
"I failed at my own job and couldn't figure out how to generate a specification for a type."
|
|
)]
|
|
UnsupportedType,
|
|
|
|
#[error("I discovered a type hole where I would expect a concrete type.")]
|
|
UnboundTypeVariable,
|
|
|
|
#[error("I caught a free variable in the contract's interface boundary.")]
|
|
FreeTypeVariable,
|
|
|
|
#[error("I had the misfortune to find an invalid type in an interface boundary.")]
|
|
ExpectedData,
|
|
|
|
#[error("I figured you tried to export a function in your contract's binary interface.")]
|
|
UnexpectedFunction,
|
|
|
|
#[error("I caught an opaque type trying to escape")]
|
|
IllegalOpaqueType,
|
|
}
|
|
|
|
impl Error {
|
|
pub fn new(context: ErrorContext, type_info: &Type) -> Self {
|
|
Error {
|
|
context,
|
|
breadcrumbs: vec![type_info.clone()],
|
|
}
|
|
}
|
|
|
|
pub fn backtrack(self, type_info: &Type) -> Self {
|
|
let mut breadcrumbs = vec![type_info.clone()];
|
|
breadcrumbs.extend(self.breadcrumbs);
|
|
Error {
|
|
context: self.context,
|
|
breadcrumbs,
|
|
}
|
|
}
|
|
|
|
pub fn help(&self) -> String {
|
|
match self.context {
|
|
ErrorContext::IllegalOpaqueType => format!(
|
|
r#"Opaque types cannot figure anywhere in an outward-facing type like a validator's redeemer or datum. This is because an {opaque} type hides its implementation details, and likely enforce invariants that cannot be expressed only structurally. In particular, the {opaque} type {signature} cannot be safely constructed from any Plutus Data.
|
|
|
|
Hence, {opaque} types are forbidden from interface points with the off-chain world. Instead, use an intermediate representation and construct the {opaque} type at runtime using constructors and methods provided for that type (e.g. {Dict}.{from_list}, {Rational}.{new}, ...)."#,
|
|
opaque = "opaque".if_supports_color(Stdout, |s| s.purple()),
|
|
signature = Error::fmt_breadcrumbs(&[self
|
|
.breadcrumbs
|
|
.last()
|
|
.expect("always at least one breadcrumb")
|
|
.to_owned()]),
|
|
Dict = "Dict"
|
|
.if_supports_color(Stdout, |s| s.bright_blue())
|
|
.if_supports_color(Stdout, |s| s.bold()),
|
|
from_list = "from_list".if_supports_color(Stdout, |s| s.blue()),
|
|
Rational = "Rational"
|
|
.if_supports_color(Stdout, |s| s.bright_blue())
|
|
.if_supports_color(Stdout, |s| s.bold()),
|
|
new = "new".if_supports_color(Stdout, |s| s.blue()),
|
|
),
|
|
|
|
ErrorContext::UnsupportedType => format!(
|
|
r#"I do not know how to generate a portable Plutus specification for the following type:
|
|
|
|
╰─▶ {signature}
|
|
|
|
This is likely a bug. I should know. May you be kind enough and report this on <https://github.com/aiken-lang/aiken>."#,
|
|
signature = Error::fmt_breadcrumbs(&[self
|
|
.breadcrumbs
|
|
.last()
|
|
.expect("always at least one breadcrumb")
|
|
.to_owned()]),
|
|
),
|
|
|
|
ErrorContext::FreeTypeVariable => format!(
|
|
r#"There can't be any free type variable at the contract's boundary (i.e. in types used as datum and/or redeemer). Indeed, the validator can only be invoked with (very) concrete types. Since there's no reflexion possible inside a validator, it simply isn't possible to have any remaining free type variable in any of the datum or redeemer.
|
|
|
|
I got there when trying to generate a blueprint specification of the following type:
|
|
|
|
╰─▶ {breadcrumbs}"#,
|
|
breadcrumbs = Error::fmt_breadcrumbs(&self.breadcrumbs)
|
|
),
|
|
|
|
ErrorContext::UnboundTypeVariable => format!(
|
|
r#"There cannot be any unbound type variable at the contract's boundary (i.e. in types used as datum and/or redeemer). Indeed, in order to generate an outward-facing specification of the contract's interface, I need to know what concrete representations will the datum and/or the redeemer have.
|
|
|
|
If your contract doesn't need datum or redeemer, you can always give them the type {type_Void} to indicate this. It is very concrete and will help me progress forward."#,
|
|
type_Void = "Void"
|
|
.if_supports_color(Stdout, |s| s.bright_blue())
|
|
.if_supports_color(Stdout, |s| s.bold())
|
|
),
|
|
|
|
ErrorContext::ExpectedData => format!(
|
|
r#"While figuring out the outward-facing specification for your contract, I found a type that cannot actually be represented as valid Untyped Plutus Core (the low-level language Cardano uses to execute smart-contracts. For example, it isn't possible to have a list or a tuple of {type_String} because the underlying execution engine doesn't allow it.
|
|
|
|
There are few restrictions like this one. In this instance, here's the types I followed and that led me to this problem:
|
|
|
|
╰─▶ {breadcrumbs}"#,
|
|
type_String = "String"
|
|
.if_supports_color(Stdout, |s| s.bright_blue())
|
|
.if_supports_color(Stdout, |s| s.bold()),
|
|
breadcrumbs = Error::fmt_breadcrumbs(&self.breadcrumbs)
|
|
),
|
|
|
|
ErrorContext::UnexpectedFunction => format!(
|
|
r#"I can't allow that. Functions aren't serializable as data on-chain and thus cannot be used within your datum and/or redeemer types.
|
|
|
|
Here's the types I followed and that led me to this problem:
|
|
|
|
╰─▶ {breadcrumbs}"#,
|
|
breadcrumbs = Error::fmt_breadcrumbs(&self.breadcrumbs)
|
|
),
|
|
}
|
|
}
|
|
|
|
fn fmt_breadcrumbs(breadcrumbs: &[Type]) -> String {
|
|
breadcrumbs
|
|
.iter()
|
|
.map(|type_info| {
|
|
pretty::Printer::new()
|
|
.print(type_info)
|
|
.to_pretty_string(70)
|
|
.if_supports_color(Stdout, |s| s.bright_blue())
|
|
.if_supports_color(Stdout, |s| s.bold())
|
|
.to_string()
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" → ")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod tests {
|
|
use super::*;
|
|
use proptest::prelude::*;
|
|
use serde_json::{self, json, Value};
|
|
|
|
pub fn assert_json(schema: &impl Serialize, expected: Value) {
|
|
assert_eq!(serde_json::to_value(schema).unwrap(), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_data_integer() {
|
|
let schema = Schema::Data(Data::Integer);
|
|
assert_json(
|
|
&schema,
|
|
json!({
|
|
"dataType": "integer"
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_data_bytes() {
|
|
let schema = Schema::Data(Data::Bytes);
|
|
assert_json(
|
|
&schema,
|
|
json!({
|
|
"dataType": "bytes"
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_data_list_1() {
|
|
let ref_integer = Reference::new("Int");
|
|
let schema = Schema::Data(Data::List(Items::One(Declaration::Referenced(ref_integer))));
|
|
assert_json(
|
|
&schema,
|
|
json!({
|
|
"dataType": "list",
|
|
"items": {
|
|
"$ref": "#/definitions/Int"
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_data_list_2() {
|
|
let ref_list_integer = Reference::new("List$Int");
|
|
let schema = Schema::Data(Data::List(Items::One(Declaration::Referenced(
|
|
ref_list_integer,
|
|
))));
|
|
assert_json(
|
|
&schema,
|
|
json!({
|
|
"dataType": "list",
|
|
"items": {
|
|
"$ref": "#/definitions/List$Int"
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_data_map_1() {
|
|
let ref_integer = Declaration::Referenced(Reference::new("Int"));
|
|
let ref_bytes = Declaration::Referenced(Reference::new("ByteArray"));
|
|
let schema = Schema::Data(Data::Map(ref_integer, ref_bytes));
|
|
assert_json(
|
|
&schema,
|
|
json!({
|
|
"dataType": "map",
|
|
"keys": {
|
|
"$ref": "#/definitions/Int"
|
|
},
|
|
"values": {
|
|
"$ref": "#/definitions/ByteArray"
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_data_constr_1() {
|
|
let schema = Schema::Data(Data::AnyOf(vec![Constructor {
|
|
index: 0,
|
|
fields: vec![],
|
|
}
|
|
.into()]));
|
|
assert_json(
|
|
&schema,
|
|
json!({
|
|
"anyOf": [{
|
|
"dataType": "constructor",
|
|
"index": 0,
|
|
"fields": []
|
|
}]
|
|
}),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_data_constr_2() {
|
|
let schema = Schema::Data(Data::AnyOf(vec![
|
|
Constructor {
|
|
index: 0,
|
|
fields: vec![Declaration::Referenced(Reference::new("Int")).into()],
|
|
}
|
|
.into(),
|
|
Constructor {
|
|
index: 1,
|
|
fields: vec![Declaration::Referenced(Reference::new("Bytes")).into()],
|
|
}
|
|
.into(),
|
|
]));
|
|
assert_json(
|
|
&schema,
|
|
json!({
|
|
"anyOf": [
|
|
{
|
|
"dataType": "constructor",
|
|
"index": 0,
|
|
"fields": [{ "$ref": "#/definitions/Int" }]
|
|
},
|
|
{
|
|
"dataType": "constructor",
|
|
"index": 1,
|
|
"fields": [{ "$ref": "#/definitions/Bytes" }]
|
|
}
|
|
]
|
|
}),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_empty_data() {
|
|
let schema = Schema::Data(Data::Opaque);
|
|
assert_json(&schema, json!({}))
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_annotated_1() {
|
|
let schema = Annotated {
|
|
title: Some("foo".to_string()),
|
|
description: None,
|
|
annotated: Schema::Integer,
|
|
};
|
|
assert_json(
|
|
&schema,
|
|
json!({
|
|
"title": "foo",
|
|
"dataType": "#integer"
|
|
}),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_annotated_2() {
|
|
let schema = Annotated {
|
|
title: Some("foo".to_string()),
|
|
description: Some("Lorem Ipsum".to_string()),
|
|
annotated: Schema::String,
|
|
};
|
|
assert_json(
|
|
&schema,
|
|
json!({
|
|
"title": "foo",
|
|
"description": "Lorem Ipsum",
|
|
"dataType": "#string"
|
|
}),
|
|
)
|
|
}
|
|
|
|
#[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()
|
|
)
|
|
}
|
|
|
|
#[allow(clippy::arc_with_non_send_sync)]
|
|
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)
|
|
]
|
|
})
|
|
}
|
|
|
|
#[allow(clippy::arc_with_non_send_sync)]
|
|
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",
|
|
)
|
|
}
|
|
}
|
|
}
|