Generalize schema definition to work from inline schema or reference.

This commit is contained in:
KtorZ 2023-04-06 17:55:11 +02:00
parent d620f6367c
commit ee220881b6
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
3 changed files with 119 additions and 83 deletions

View File

@ -142,7 +142,7 @@ 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;
@ -218,7 +218,10 @@ 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();

View File

@ -11,6 +11,7 @@ use serde::{
de::{self, Deserialize, Deserializer, MapAccess, Visitor}, de::{self, Deserialize, Deserializer, MapAccess, Visitor},
ser::{Serialize, SerializeStruct, Serializer}, ser::{Serialize, SerializeStruct, Serializer},
}; };
use serde_json as json;
use std::{collections::HashMap, fmt, ops::Deref, sync::Arc}; use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
use uplc::ast::Term; use uplc::ast::Term;
@ -27,6 +28,13 @@ 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>),
}
/// 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 {
@ -35,10 +43,8 @@ pub enum Schema {
Integer, Integer,
Bytes, Bytes,
String, String,
// TODO: Generalize to work with either Reference or Data Pair(Declaration<Schema>, Declaration<Schema>),
Pair(Reference, Reference), List(Items<Schema>),
// TODO: Generalize to work with either Reference or Data
List(Items<Reference>),
Data(Data), Data(Data),
} }
@ -47,10 +53,8 @@ pub enum Schema {
pub enum Data { pub enum Data {
Integer, Integer,
Bytes, Bytes,
// TODO: Generalize to work with either Reference or Data List(Items<Data>),
List(Items<Reference>), Map(Declaration<Data>, Declaration<Data>),
// TODO: Generalize to work with either Reference or Data
Map(Box<Reference>, Box<Reference>),
AnyOf(Vec<Annotated<Constructor>>), AnyOf(Vec<Annotated<Constructor>>),
Opaque, Opaque,
} }
@ -59,8 +63,8 @@ pub enum Data {
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
#[serde(untagged)] #[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
@ -68,7 +72,7 @@ pub enum Items<T> {
pub struct Constructor { pub struct Constructor {
pub index: usize, pub index: usize,
// TODO: Generalize to work with either Reference or Data // TODO: Generalize to work with either Reference or Data
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> {
@ -103,10 +107,10 @@ impl<'a, T> TryFrom<&'a Term<T>> for Schema {
PlutusData::BoundedBytes(..) => { PlutusData::BoundedBytes(..) => {
Data::Bytes Data::Bytes
} }
PlutusData::Map(keyValuePair) => { PlutusData::Array(elems) => {
todo!() todo!()
} }
PlutusData::Array(elems) => { PlutusData::Map(keyValuePair) => {
todo!() todo!()
} }
PlutusData::Constr(Constr{ tag, fields, any_constructor }) => { PlutusData::Constr(Constr{ tag, fields, any_constructor }) => {
@ -141,7 +145,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()])),
}) })
@ -270,7 +274,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 {
@ -311,22 +315,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())
@ -371,6 +368,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))?;
@ -449,7 +447,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),
}); });
} }
@ -579,7 +577,7 @@ where
} }
let mut data_type: Option<String> = None; let mut data_type: Option<String> = None;
let mut items: Option<Items<Reference>> = None; let mut items: Option<json::Value> = None; // defer items deserialization to later
let mut keys = None; let mut keys = None;
let mut left = None; let mut left = None;
let mut right = None; let mut right = None;
@ -639,6 +637,18 @@ where
} }
} }
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 = || { let expect_no_items = || {
if items.is_some() { if items.is_some() {
return Err(de::Error::custom( return Err(de::Error::custom(
@ -695,26 +705,28 @@ where
Some(constructors) => Ok(Schema::Data(Data::AnyOf(constructors))), Some(constructors) => Ok(Schema::Data(Data::AnyOf(constructors))),
} }
} }
Some(data_type) if data_type == "list" || data_type == "#list" => { Some(data_type) if data_type == "list" => {
expect_no_keys()?; expect_no_keys()?;
expect_no_values()?; expect_no_values()?;
expect_no_any_of()?; expect_no_any_of()?;
expect_no_left_or_right()?; expect_no_left_or_right()?;
match items { let items = expect_data_items()?;
Some(items) if data_type == "list" => Ok(Schema::Data(Data::List(items))), Ok(Schema::Data(Data::List(items)))
Some(items) if data_type == "#list" => Ok(Schema::List(items)), }
Some(_) => unreachable!("condition checked in pattern guard"), Some(data_type) if data_type == "#list" => {
None => Err(de::Error::missing_field("items")), 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" => { Some(data_type) if data_type == "map" => {
expect_no_items()?; expect_no_items()?;
expect_no_any_of()?; expect_no_any_of()?;
expect_no_left_or_right()?; expect_no_left_or_right()?;
match (keys, values) { match (keys, values) {
(Some(keys), Some(values)) => { (Some(keys), Some(values)) => Ok(Schema::Data(Data::Map(keys, values))),
Ok(Schema::Data(Data::Map(Box::new(keys), Box::new(values))))
}
(None, _) => Err(de::Error::missing_field("keys")), (None, _) => Err(de::Error::missing_field("keys")),
(Some(..), None) => Err(de::Error::missing_field("values")), (Some(..), None) => Err(de::Error::missing_field("values")),
} }
@ -1069,7 +1081,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!({
@ -1084,7 +1096,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!({
@ -1098,9 +1112,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!({
@ -1139,12 +1153,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(),
])); ]));
@ -1236,7 +1250,7 @@ pub mod test {
#[test] #[test]
fn deserialize_data_list_one() { fn deserialize_data_list_one() {
assert_eq!( assert_eq!(
Data::List(Items::One(Box::new(Reference::new("foo")))), Data::List(Items::One(Declaration::Referenced(Reference::new("foo")))),
serde_json::from_value(json!({ serde_json::from_value(json!({
"dataType": "list", "dataType": "list",
"items": { "$ref": "foo" } "items": { "$ref": "foo" }
@ -1249,8 +1263,8 @@ pub mod test {
fn deserialize_data_list_many() { fn deserialize_data_list_many() {
assert_eq!( assert_eq!(
Data::List(Items::Many(vec![ Data::List(Items::Many(vec![
Reference::new("foo"), Declaration::Referenced(Reference::new("foo")),
Reference::new("bar") Declaration::Referenced(Reference::new("bar"))
])), ])),
serde_json::from_value(json!({ serde_json::from_value(json!({
"dataType": "list", "dataType": "list",
@ -1267,8 +1281,8 @@ pub mod test {
fn deserialize_data_map() { fn deserialize_data_map() {
assert_eq!( assert_eq!(
Data::Map( Data::Map(
Box::new(Reference::new("foo")), Declaration::Referenced(Reference::new("foo")),
Box::new(Reference::new("bar")) Declaration::Referenced(Reference::new("bar"))
), ),
serde_json::from_value(json!({ serde_json::from_value(json!({
"dataType": "map", "dataType": "map",
@ -1284,7 +1298,10 @@ pub mod test {
assert_eq!( assert_eq!(
Data::AnyOf(vec![Constructor { Data::AnyOf(vec![Constructor {
index: 0, index: 0,
fields: vec![Reference::new("foo").into(), Reference::new("bar").into()], fields: vec![
Declaration::Referenced(Reference::new("foo")).into(),
Declaration::Referenced(Reference::new("bar")).into()
],
} }
.into()]), .into()]),
serde_json::from_value(json!({ serde_json::from_value(json!({
@ -1309,7 +1326,10 @@ pub mod test {
assert_eq!( assert_eq!(
Data::AnyOf(vec![Constructor { Data::AnyOf(vec![Constructor {
index: 0, index: 0,
fields: vec![Reference::new("foo").into(), Reference::new("bar").into()], fields: vec![
Declaration::Referenced(Reference::new("foo")).into(),
Declaration::Referenced(Reference::new("bar")).into()
],
} }
.into()]), .into()]),
serde_json::from_value(json!({ serde_json::from_value(json!({
@ -1330,45 +1350,58 @@ pub mod test {
} }
fn arbitrary_data() -> impl Strategy<Value = Data> { fn arbitrary_data() -> impl Strategy<Value = Data> {
let r = prop_oneof![".*".prop_map(|s| Reference::new(&s))]; let leaf = prop_oneof![Just(Data::Opaque), Just(Data::Bytes), Just(Data::Integer)];
let constructor =
(0..3usize, prop::collection::vec(r.clone(), 0..3)).prop_map(|(index, fields)| { leaf.prop_recursive(3, 8, 3, |inner| {
Constructor { let r = prop_oneof![
index, ".*".prop_map(|s| Declaration::Referenced(Reference::new(&s))),
fields: fields.into_iter().map(|f| f.into()).collect(), inner.prop_map(|s| Declaration::Inline(Box::new(s)))
} ];
.into() let constructor =
}); (0..3usize, prop::collection::vec(r.clone(), 0..3)).prop_map(|(index, fields)| {
prop_oneof![ Constructor {
Just(Data::Opaque), index,
Just(Data::Bytes), fields: fields.into_iter().map(|f| f.into()).collect(),
Just(Data::Integer), }
(r.clone(), r.clone()).prop_map(|(k, v)| Data::Map(Box::new(k), Box::new(v))), .into()
r.clone().prop_map(|x| Data::List(Items::One(Box::new(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) 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> { fn arbitrary_schema() -> impl Strategy<Value = Schema> {
let r = prop_oneof![".*".prop_map(|s| Reference::new(&s))];
prop_compose! { prop_compose! {
fn data_strategy()(data in arbitrary_data()) -> Schema { fn data_strategy()(data in arbitrary_data()) -> Schema {
Schema::Data(data) Schema::Data(data)
} }
} }
prop_oneof![
let leaf = prop_oneof![
Just(Schema::Unit), Just(Schema::Unit),
Just(Schema::Boolean), Just(Schema::Boolean),
Just(Schema::Bytes), Just(Schema::Bytes),
Just(Schema::Integer), Just(Schema::Integer),
Just(Schema::String), Just(Schema::String),
(r.clone(), r.clone()).prop_map(|(l, r)| Schema::Pair(l, r)),
r.clone()
.prop_map(|x| Schema::List(Items::One(Box::new(x)))),
prop::collection::vec(r, 1..3).prop_map(|xs| Schema::List(Items::Many(xs))),
data_strategy(), 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! { proptest! {

View File

@ -226,7 +226,7 @@ mod test {
super::{ super::{
definitions::Definitions, definitions::Definitions,
error::Error, error::Error,
schema::{Annotated, Data, Schema}, schema::{Annotated, Data, Declaration, Items, Schema},
}, },
*, *,
}; };