Fix blueprint generation for recursive types.
This was a bit tricky and I ended up breaking things down a lot and trying different path. This commit is the result of the most satisfying one. It introduces a new 'concept' and types: Definitions and Reference. These elements are meant to reflect JSON pointers and JSON-schema definitions which we now use for pretty much all user-defined data-types. In fact, Schemas are no longer inlined, but are always referencing some schema under "definitions". This indirection is necessary in order to cope with recursive types. And while it's only truly necessary for recursive types, using it consistently makes it both easier to produce and easier to consume. --- The blueprint generation for recursive types here also works thanks to the 'Definitions' data-structure wrapper around a BTreeMap. This uses a strategy where: (1) schemas are only generated if they haven't been seen before (2) schemas are marked as seen BEFORE actually being generated (to effectively stop a recursive generation). This relies on one important aspect: the key must be uniquely identifying a given schema. Which means that we have to monomorphize data-types with generic parameters also here, and use keys that are specialized in one data-type. --- In this large overhaul we've also lost one thing which I didn't bother re-introducing yet to keep the work manageable: title for record fields. Before, we use to pull those from record constructor when available, yet now, every record constructor has been replaced by a `$ref`. We could theoritically attach a title to the reference. I'll try to quickly add that in a later commit.
This commit is contained in:
parent
f67e049dc2
commit
451737237e
|
@ -0,0 +1,183 @@
|
|||
use aiken_lang::tipo::{Type, TypeVar};
|
||||
use serde::{
|
||||
self,
|
||||
ser::{Serialize, SerializeStruct, Serializer},
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
// ---------- Definitions
|
||||
|
||||
/// A map of definitions meant to be optionally registered and looked up.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Definitions<T> {
|
||||
#[serde(flatten, default)]
|
||||
inner: BTreeMap<String, Option<T>>,
|
||||
}
|
||||
|
||||
impl<T> Definitions<T> {
|
||||
/// Constructs a new empty definitions set.
|
||||
pub fn new() -> Self {
|
||||
Definitions {
|
||||
inner: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// True when there's no known definitions.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
/// Retrieve a definition, if it exists.
|
||||
pub fn lookup(&self, reference: &Reference) -> Option<&T> {
|
||||
self.inner
|
||||
.get(reference.as_key())
|
||||
.map(|v| v
|
||||
.as_ref()
|
||||
.expect("All registered definitions are 'Some'. 'None' state is only transient during registration")
|
||||
)
|
||||
}
|
||||
|
||||
/// Merge two set of definitions together. Prioritize callee.
|
||||
pub fn merge(&mut self, other: &mut Definitions<T>) {
|
||||
self.inner.append(&mut other.inner);
|
||||
}
|
||||
|
||||
/// Erase a known definition. Does nothing if the reference is unknown.
|
||||
pub fn remove(&mut self, reference: &Reference) {
|
||||
self.inner.remove(reference.as_key());
|
||||
}
|
||||
|
||||
/// Register a new definition only if it doesn't exist. This uses a strategy of
|
||||
/// mark-and-insert such that recursive definitions are only built once.
|
||||
pub fn register<F, E>(
|
||||
&mut self,
|
||||
type_info: &Type,
|
||||
type_parameters: &HashMap<u64, Arc<Type>>,
|
||||
build_schema: F,
|
||||
) -> Result<Reference, E>
|
||||
where
|
||||
F: FnOnce(&mut Self) -> Result<T, E>,
|
||||
{
|
||||
let reference = Reference::from_type(type_info, type_parameters);
|
||||
let key = reference.as_key();
|
||||
|
||||
if !self.inner.contains_key(key) {
|
||||
self.inner.insert(key.to_string(), None);
|
||||
let schema = build_schema(self)?;
|
||||
self.inner.insert(key.to_string(), Some(schema));
|
||||
}
|
||||
|
||||
Ok(reference)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Reference
|
||||
|
||||
/// A URI pointer to an underlying data-type.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Default)]
|
||||
pub struct Reference {
|
||||
inner: String,
|
||||
}
|
||||
|
||||
impl Reference {
|
||||
/// Create a (possibly unsound) Reference from a string. This isn't the preferred way to create
|
||||
/// a reference. One should use: `into()` on a 'Type' instead.
|
||||
pub fn new(path: &str) -> Reference {
|
||||
Reference {
|
||||
inner: path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn a reference into a key suitable for lookup.
|
||||
fn as_key(&self) -> &str {
|
||||
self.inner.as_str()
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// treated as path delimiter in pointers paths).
|
||||
fn as_json_pointer(&self) -> String {
|
||||
format!("#/definitions/{}", self.as_key().replace('/', "~1"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Reference {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(&self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl Reference {
|
||||
pub fn from_type(type_info: &Type, type_parameters: &HashMap<u64, Arc<Type>>) -> Self {
|
||||
match type_info {
|
||||
Type::App {
|
||||
module, name, args, ..
|
||||
} => {
|
||||
let args: Self = Self::from_types(args, type_parameters);
|
||||
Self {
|
||||
inner: if module.is_empty() {
|
||||
format!("{name}{args}")
|
||||
} else {
|
||||
format!("{module}/{name}{args}")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Type::Tuple { elems } => Self {
|
||||
inner: format!(
|
||||
"Tuple{elems}",
|
||||
elems = Self::from_types(elems, type_parameters)
|
||||
),
|
||||
},
|
||||
|
||||
// NOTE:
|
||||
//
|
||||
// Implementations below are only there for completeness. In practice, we should never
|
||||
// end up creating references for 'Var' or 'Fn' in the context of blueprints.
|
||||
Type::Var { tipo } => match tipo.borrow().deref() {
|
||||
TypeVar::Link { tipo } => Self::from_type(tipo.as_ref(), type_parameters),
|
||||
TypeVar::Generic { id } | TypeVar::Unbound { id } => {
|
||||
let tipo = type_parameters.get(id).unwrap();
|
||||
Self::from_type(tipo, type_parameters)
|
||||
}
|
||||
},
|
||||
|
||||
Type::Fn { args, ret } => Self {
|
||||
inner: format!(
|
||||
"Fn{args}_{ret}",
|
||||
args = Self::from_types(args, type_parameters),
|
||||
ret = Self::from_type(ret, type_parameters)
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn from_types(args: &Vec<Arc<Type>>, type_parameters: &HashMap<u64, Arc<Type>>) -> Self {
|
||||
if args.is_empty() {
|
||||
Reference::new("")
|
||||
} else {
|
||||
Reference {
|
||||
inner: format!(
|
||||
"${}",
|
||||
args.iter()
|
||||
.map(|s| Self::from_type(s.as_ref(), type_parameters).inner)
|
||||
.collect::<Vec<_>>()
|
||||
.join("_")
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Reference {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
let mut s = serializer.serialize_struct("$ref", 1)?;
|
||||
s.serialize_field("$ref", &self.as_json_pointer())?;
|
||||
s.end()
|
||||
}
|
||||
}
|
|
@ -1,18 +1,22 @@
|
|||
pub mod definitions;
|
||||
pub mod error;
|
||||
pub mod schema;
|
||||
pub mod validator;
|
||||
|
||||
use crate::{config::Config, module::CheckedModules};
|
||||
use aiken_lang::uplc::CodeGenerator;
|
||||
use definitions::{Definitions, Reference};
|
||||
use error::Error;
|
||||
use schema::Schema;
|
||||
use std::fmt::{self, Debug, Display};
|
||||
use schema::{Annotated, Schema};
|
||||
use std::fmt::Debug;
|
||||
use validator::Validator;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Blueprint<T: Default> {
|
||||
pub struct Blueprint<R: Default, S: Default> {
|
||||
pub preamble: Preamble,
|
||||
pub validators: Vec<Validator<T>>,
|
||||
pub validators: Vec<Validator<R, S>>,
|
||||
#[serde(skip_serializing_if = "Definitions::is_empty", default)]
|
||||
pub definitions: Definitions<S>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||
|
@ -44,7 +48,7 @@ pub enum LookupResult<'a, T> {
|
|||
Many,
|
||||
}
|
||||
|
||||
impl Blueprint<Schema> {
|
||||
impl Blueprint<Reference, Annotated<Schema>> {
|
||||
pub fn new(
|
||||
config: &Config,
|
||||
modules: &CheckedModules,
|
||||
|
@ -52,25 +56,35 @@ impl Blueprint<Schema> {
|
|||
) -> Result<Self, Error> {
|
||||
let preamble = config.into();
|
||||
|
||||
let mut definitions = Definitions::new();
|
||||
|
||||
let validators: Result<Vec<_>, Error> = modules
|
||||
.validators()
|
||||
.map(|(validator, def)| {
|
||||
Validator::from_checked_module(modules, generator, validator, def)
|
||||
Validator::from_checked_module(modules, generator, validator, def).map(
|
||||
|mut schema| {
|
||||
definitions.merge(&mut schema.definitions);
|
||||
schema.definitions = Definitions::new();
|
||||
schema
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Blueprint {
|
||||
preamble,
|
||||
validators: validators?,
|
||||
definitions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Blueprint<T>
|
||||
impl<R, S> Blueprint<R, S>
|
||||
where
|
||||
T: Clone + Default,
|
||||
R: Clone + Default,
|
||||
S: Clone + Default,
|
||||
{
|
||||
pub fn lookup(&self, title: Option<&String>) -> Option<LookupResult<Validator<T>>> {
|
||||
pub fn lookup(&self, title: Option<&String>) -> Option<LookupResult<Validator<R, S>>> {
|
||||
let mut validator = None;
|
||||
|
||||
for v in self.validators.iter() {
|
||||
|
@ -95,7 +109,7 @@ where
|
|||
action: F,
|
||||
) -> Result<A, E>
|
||||
where
|
||||
F: Fn(Validator<T>) -> Result<A, E>,
|
||||
F: Fn(Validator<R, S>) -> Result<A, E>,
|
||||
{
|
||||
match self.lookup(title) {
|
||||
Some(LookupResult::One(validator)) => action(validator.to_owned()),
|
||||
|
@ -109,13 +123,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: serde::Serialize + Default> Display for Blueprint<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?;
|
||||
f.write_str(&s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Config> for Preamble {
|
||||
fn from(config: &Config) -> Self {
|
||||
Preamble {
|
||||
|
@ -135,11 +142,14 @@ impl From<&Config> for Preamble {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use aiken_lang::builtins;
|
||||
use schema::{Data, Items, Schema};
|
||||
use serde_json::{self, json};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn serialize_no_description() {
|
||||
let blueprint: Blueprint<Schema> = Blueprint {
|
||||
let blueprint: Blueprint<Reference, Annotated<Schema>> = Blueprint {
|
||||
preamble: Preamble {
|
||||
title: "Foo".to_string(),
|
||||
description: None,
|
||||
|
@ -148,6 +158,7 @@ mod test {
|
|||
license: Some("Apache-2.0".to_string()),
|
||||
},
|
||||
validators: vec![],
|
||||
definitions: Definitions::new(),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&blueprint).unwrap(),
|
||||
|
@ -165,7 +176,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn serialize_with_description() {
|
||||
let blueprint: Blueprint<Schema> = Blueprint {
|
||||
let blueprint: Blueprint<Reference, Annotated<Schema>> = Blueprint {
|
||||
preamble: Preamble {
|
||||
title: "Foo".to_string(),
|
||||
description: Some("Lorem ipsum".to_string()),
|
||||
|
@ -174,6 +185,7 @@ mod test {
|
|||
license: None,
|
||||
},
|
||||
validators: vec![],
|
||||
definitions: Definitions::new(),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&blueprint).unwrap(),
|
||||
|
@ -188,4 +200,65 @@ mod test {
|
|||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_with_definitions() {
|
||||
let mut definitions = Definitions::new();
|
||||
definitions
|
||||
.register::<_, Error>(&builtins::int(), &HashMap::new(), |_| {
|
||||
Ok(Schema::Data(Data::Integer).into())
|
||||
})
|
||||
.unwrap();
|
||||
definitions
|
||||
.register::<_, Error>(
|
||||
&builtins::list(builtins::byte_array()),
|
||||
&HashMap::new(),
|
||||
|definitions| {
|
||||
let ref_bytes = definitions.register::<_, Error>(
|
||||
&builtins::byte_array(),
|
||||
&HashMap::new(),
|
||||
|_| Ok(Schema::Data(Data::Bytes).into()),
|
||||
)?;
|
||||
Ok(Schema::Data(Data::List(Items::One(Box::new(ref_bytes)))).into())
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let blueprint: Blueprint<Reference, Annotated<Schema>> = Blueprint {
|
||||
preamble: Preamble {
|
||||
title: "Foo".to_string(),
|
||||
description: None,
|
||||
version: "1.0.0".to_string(),
|
||||
plutus_version: PlutusVersion::V2,
|
||||
license: None,
|
||||
},
|
||||
validators: vec![],
|
||||
definitions,
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&blueprint).unwrap(),
|
||||
json!({
|
||||
"preamble": {
|
||||
"title": "Foo",
|
||||
"version": "1.0.0",
|
||||
"plutusVersion": "v2"
|
||||
},
|
||||
"validators": [],
|
||||
"definitions": {
|
||||
"ByteArray": {
|
||||
"dataType": "bytes"
|
||||
},
|
||||
"Int": {
|
||||
"dataType": "integer"
|
||||
},
|
||||
"List$ByteArray": {
|
||||
"dataType": "list",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ByteArray"
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::blueprint::definitions::{Definitions, Reference};
|
||||
use crate::CheckedModule;
|
||||
use aiken_lang::{
|
||||
ast::{DataType, Definition, TypedDefinition},
|
||||
|
@ -8,13 +9,8 @@ use serde::{
|
|||
self,
|
||||
ser::{Serialize, SerializeStruct, Serializer},
|
||||
};
|
||||
use serde_json;
|
||||
use std::ops::Deref;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Display},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Annotated<T> {
|
||||
|
@ -44,8 +40,8 @@ pub enum Schema {
|
|||
pub enum Data {
|
||||
Integer,
|
||||
Bytes,
|
||||
List(Items<Data>),
|
||||
Map(Box<Data>, Box<Data>),
|
||||
List(Items<Reference>),
|
||||
Map(Box<Reference>, Box<Reference>),
|
||||
AnyOf(Vec<Annotated<Constructor>>),
|
||||
Opaque,
|
||||
}
|
||||
|
@ -61,15 +57,7 @@ pub enum Items<T> {
|
|||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Constructor {
|
||||
pub index: usize,
|
||||
pub fields: Vec<Field>,
|
||||
}
|
||||
|
||||
/// A field of a constructor. Can be either an inlined Data schema or a reference to another type.
|
||||
/// References are mostly only used for recursive types.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Field {
|
||||
Inline(Annotated<Data>),
|
||||
Reference { path: String },
|
||||
pub fields: Vec<Reference>,
|
||||
}
|
||||
|
||||
impl<T> From<T> for Annotated<T> {
|
||||
|
@ -86,195 +74,251 @@ impl Annotated<Schema> {
|
|||
pub fn from_type(
|
||||
modules: &HashMap<String, CheckedModule>,
|
||||
type_info: &Type,
|
||||
type_parameters: &HashMap<u64, &Arc<Type>>,
|
||||
) -> Result<Self, Error> {
|
||||
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, Arc<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() => match &type_name[..] {
|
||||
"Data" => Ok(Annotated {
|
||||
title: Some("Data".to_string()),
|
||||
description: Some("Any Plutus data.".to_string()),
|
||||
annotated: Schema::Data(Data::Opaque),
|
||||
}),
|
||||
} 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()),
|
||||
"ByteArray" => Ok(Schema::Data(Data::Bytes).into()),
|
||||
|
||||
"Int" => Ok(Schema::Data(Data::Integer).into()),
|
||||
"Int" => Ok(Schema::Data(Data::Integer).into()),
|
||||
|
||||
"String" => Ok(Schema::String.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::from_type(modules, args.get(0).unwrap(), type_parameters)
|
||||
.and_then(|s| s.into_data(type_info))?;
|
||||
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()),
|
||||
"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![Field::Inline(generic)],
|
||||
},
|
||||
},
|
||||
Annotated {
|
||||
title: Some("None".to_string()),
|
||||
description: Some("Nothing.".to_string()),
|
||||
annotated: Constructor {
|
||||
index: 1,
|
||||
fields: vec![],
|
||||
},
|
||||
},
|
||||
])),
|
||||
})
|
||||
}
|
||||
}])),
|
||||
}),
|
||||
|
||||
"List" => {
|
||||
let generic =
|
||||
Annotated::from_type(modules, args.get(0).unwrap(), type_parameters)?;
|
||||
"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![],
|
||||
},
|
||||
},
|
||||
])),
|
||||
}),
|
||||
|
||||
// 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 generic.annotated {
|
||||
Schema::Data(Data::List(Items::Many(xs))) if xs.len() == 2 => Data::Map(
|
||||
Box::new(xs.first().unwrap().to_owned()),
|
||||
Box::new(xs.last().unwrap().to_owned()),
|
||||
),
|
||||
_ => {
|
||||
let inner = generic.into_data(type_info)?.annotated;
|
||||
Data::List(Items::One(Box::new(inner)))
|
||||
"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.get(0)
|
||||
.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![generic],
|
||||
},
|
||||
},
|
||||
Annotated {
|
||||
title: Some("None".to_string()),
|
||||
description: Some("Nothing.".to_string()),
|
||||
annotated: Constructor {
|
||||
index: 1,
|
||||
fields: vec![],
|
||||
},
|
||||
},
|
||||
])),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Schema::Data(data).into())
|
||||
}
|
||||
"List" => {
|
||||
let generic = Annotated::do_from_type(
|
||||
args.get(0)
|
||||
.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
|
||||
.lookup(&generic)
|
||||
.expect(
|
||||
"Generic type argument definition was registered just above.",
|
||||
)
|
||||
.clone()
|
||||
{
|
||||
Annotated {
|
||||
annotated: Schema::Data(Data::List(Items::Many(xs))),
|
||||
..
|
||||
} if xs.len() == 2 => {
|
||||
definitions.remove(&generic);
|
||||
Data::Map(
|
||||
Box::new(
|
||||
xs.first()
|
||||
.expect("length (== 2) checked in pattern clause")
|
||||
.to_owned(),
|
||||
),
|
||||
Box::new(
|
||||
xs.last()
|
||||
.expect("length (== 2) checked in pattern clause")
|
||||
.to_owned(),
|
||||
),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
// let inner = schema.clone().into_data(type_info)?.annotated;
|
||||
Data::List(Items::One(Box::new(generic)))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Schema::Data(data).into())
|
||||
}
|
||||
|
||||
_ => Err(Error::new(ErrorContext::UnsupportedType, type_info)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_ => Err(Error::new(ErrorContext::UnsupportedType, type_info)),
|
||||
},
|
||||
Type::App {
|
||||
module: module_name,
|
||||
name: type_name,
|
||||
args,
|
||||
..
|
||||
} => {
|
||||
let module = modules.get(module_name).unwrap();
|
||||
let constructor = find_definition(type_name, &module.ast.definitions).unwrap();
|
||||
let type_parameters = collect_type_parameters(&constructor.typed_parameters, args);
|
||||
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(modules, constructor, &type_parameters)
|
||||
Data::from_data_type(&data_type, modules, type_parameters, definitions)
|
||||
.map_err(|e| e.backtrack(type_info))?,
|
||||
);
|
||||
|
||||
Ok(Annotated {
|
||||
title: Some(constructor.name.clone()),
|
||||
description: constructor.doc.clone().map(|s| s.trim().to_string()),
|
||||
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)
|
||||
})
|
||||
.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::from_type(modules, tipo, type_parameters),
|
||||
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))?;
|
||||
Annotated::from_type(modules, tipo, type_parameters)
|
||||
.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::Tuple { elems } => {
|
||||
let elems = elems
|
||||
.iter()
|
||||
.map(|e| {
|
||||
Annotated::from_type(modules, e, type_parameters)
|
||||
.and_then(|s| s.into_data(e).map(|s| s.annotated))
|
||||
})
|
||||
.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::Fn { .. } => Err(Error::new(ErrorContext::UnexpectedFunction, type_info)),
|
||||
Type::Fn { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -295,28 +339,35 @@ impl Annotated<Schema> {
|
|||
}
|
||||
|
||||
impl Data {
|
||||
pub fn from_data_type(
|
||||
modules: &HashMap<String, CheckedModule>,
|
||||
fn from_data_type(
|
||||
data_type: &DataType<Arc<Type>>,
|
||||
type_parameters: &HashMap<u64, &Arc<Type>>,
|
||||
modules: &HashMap<String, CheckedModule>,
|
||||
type_parameters: &mut HashMap<u64, Arc<Type>>,
|
||||
definitions: &mut Definitions<Annotated<Schema>>,
|
||||
) -> Result<Self, Error> {
|
||||
let mut variants = vec![];
|
||||
|
||||
let len_constructors = data_type.constructors.len();
|
||||
for (index, constructor) in data_type.constructors.iter().enumerate() {
|
||||
let mut fields = vec![];
|
||||
|
||||
let len_arguments = data_type.constructors.len();
|
||||
for field in constructor.arguments.iter() {
|
||||
let mut schema = Annotated::from_type(modules, &field.tipo, type_parameters)
|
||||
.and_then(|t| t.into_data(&field.tipo))?;
|
||||
let reference =
|
||||
Annotated::do_from_type(&field.tipo, modules, type_parameters, definitions)?;
|
||||
|
||||
if field.label.is_some() {
|
||||
schema.title = field.label.clone();
|
||||
// NOTE: Opaque data-types with a single variant and a single field are transparent, they
|
||||
// are erased completely at compilation time.
|
||||
if data_type.opaque && len_constructors == 1 && len_arguments == 1 {
|
||||
let schema = definitions
|
||||
.lookup(&reference)
|
||||
.expect("Schema definition registered just above")
|
||||
.clone();
|
||||
definitions.remove(&reference);
|
||||
return Ok(schema.into_data(&field.tipo)?.annotated);
|
||||
}
|
||||
|
||||
if field.doc.is_some() {
|
||||
schema.description = field.doc.clone().map(|s| s.trim().to_string());
|
||||
}
|
||||
|
||||
fields.push(Field::Inline(schema));
|
||||
fields.push(reference);
|
||||
}
|
||||
|
||||
let variant = Annotated {
|
||||
|
@ -328,31 +379,56 @@ impl Data {
|
|||
variants.push(variant);
|
||||
}
|
||||
|
||||
// NOTE: Opaque data-types with a single variant and a single field are transparent, they
|
||||
// are erased completely at compilation time.
|
||||
if data_type.opaque {
|
||||
if let [variant] = &variants[..] {
|
||||
if let [Field::Inline(field)] = &variant.annotated.fields[..] {
|
||||
return Ok(field.annotated.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Data::AnyOf(variants))
|
||||
}
|
||||
}
|
||||
|
||||
// Needed because of Blueprint's default, but actually never used.
|
||||
impl Default for Schema {
|
||||
fn default() -> Self {
|
||||
Schema::Data(Data::Opaque)
|
||||
fn collect_type_parameters<'a>(
|
||||
type_parameters: &'a mut HashMap<u64, Arc<Type>>,
|
||||
generics: &'a [Arc<Type>],
|
||||
applications: &'a [Arc<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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Schema {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?;
|
||||
f.write_str(&s)
|
||||
fn find_data_type(name: &str, definitions: &[TypedDefinition]) -> Option<DataType<Arc<Type>>> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -439,11 +515,6 @@ impl Serialize for Data {
|
|||
s.end()
|
||||
}
|
||||
Data::AnyOf(constructors) => {
|
||||
// TODO: Avoid 'anyOf' applicator when there's only one constructor
|
||||
//
|
||||
// match &constructors[..] {
|
||||
// [constructor] => constructor.serialize(serializer),
|
||||
// _ => {
|
||||
let mut s = serializer.serialize_struct("AnyOf", 1)?;
|
||||
s.serialize_field("anyOf", &constructors)?;
|
||||
s.end()
|
||||
|
@ -461,19 +532,6 @@ impl Serialize for Constructor {
|
|||
}
|
||||
}
|
||||
|
||||
impl Serialize for Field {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match self {
|
||||
Field::Inline(schema) => schema.serialize(serializer),
|
||||
Field::Reference { path } => {
|
||||
let mut s = serializer.serialize_struct("$ref", 1)?;
|
||||
s.serialize_field("$ref", path)?;
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, thiserror::Error)]
|
||||
#[error("{}", context)]
|
||||
pub struct Error {
|
||||
|
@ -524,7 +582,11 @@ impl Error {
|
|||
╰─▶ {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().unwrap().to_owned()]),
|
||||
signature = Error::fmt_breadcrumbs(&[self
|
||||
.breadcrumbs
|
||||
.last()
|
||||
.expect("always at least one breadcrumb")
|
||||
.to_owned()]),
|
||||
),
|
||||
|
||||
ErrorContext::FreeTypeVariable => format!(
|
||||
|
@ -584,46 +646,6 @@ Here's the types I followed and that led me to this problem:
|
|||
}
|
||||
}
|
||||
|
||||
fn collect_type_parameters<'a>(
|
||||
generics: &'a [Arc<Type>],
|
||||
applications: &'a [Arc<Type>],
|
||||
) -> HashMap<u64, &'a Arc<Type>> {
|
||||
let mut type_parameters = HashMap::new();
|
||||
|
||||
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());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
type_parameters
|
||||
}
|
||||
|
||||
fn find_definition<'a>(
|
||||
name: &str,
|
||||
definitions: &'a Vec<TypedDefinition>,
|
||||
) -> Option<&'a DataType<Arc<Type>>> {
|
||||
for def in definitions {
|
||||
match def {
|
||||
Definition::DataType(data_type) if name == data_type.name => return Some(data_type),
|
||||
Definition::Fn { .. }
|
||||
| Definition::Validator { .. }
|
||||
| Definition::DataType { .. }
|
||||
| Definition::TypeAlias { .. }
|
||||
| Definition::Use { .. }
|
||||
| Definition::ModuleConstant { .. }
|
||||
| Definition::Test { .. } => continue,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use super::*;
|
||||
|
@ -657,13 +679,14 @@ pub mod test {
|
|||
|
||||
#[test]
|
||||
fn serialize_data_list_1() {
|
||||
let schema = Schema::Data(Data::List(Items::One(Box::new(Data::Integer))));
|
||||
let ref_integer = Reference::new("Int");
|
||||
let schema = Schema::Data(Data::List(Items::One(Box::new(ref_integer))));
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "list",
|
||||
"items": {
|
||||
"dataType": "integer"
|
||||
"$ref": "#/definitions/Int"
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
@ -671,70 +694,33 @@ pub mod test {
|
|||
|
||||
#[test]
|
||||
fn serialize_data_list_2() {
|
||||
let schema = Schema::Data(Data::List(Items::One(Box::new(Data::List(Items::One(
|
||||
Box::new(Data::Integer),
|
||||
))))));
|
||||
let ref_list_integer = Reference::new("List$Int");
|
||||
let schema = Schema::Data(Data::List(Items::One(Box::new(ref_list_integer))));
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "list",
|
||||
"items":
|
||||
{
|
||||
"dataType": "list",
|
||||
"items": { "dataType": "integer" }
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_data_list_3() {
|
||||
let schema = Schema::Data(Data::List(Items::Many(vec![Data::Integer, Data::Bytes])));
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "list",
|
||||
"items": [
|
||||
{ "dataType": "integer" },
|
||||
{ "dataType": "bytes" },
|
||||
]
|
||||
"items": {
|
||||
"$ref": "#/definitions/List$Int"
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_data_map_1() {
|
||||
let schema = Schema::Data(Data::Map(Box::new(Data::Integer), Box::new(Data::Bytes)));
|
||||
let ref_integer = Reference::new("Int");
|
||||
let ref_bytes = Reference::new("ByteArray");
|
||||
let schema = Schema::Data(Data::Map(Box::new(ref_integer), Box::new(ref_bytes)));
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "map",
|
||||
"keys": {
|
||||
"dataType": "integer"
|
||||
"$ref": "#/definitions/Int"
|
||||
},
|
||||
"values": {
|
||||
"dataType": "bytes"
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_data_map_2() {
|
||||
let schema = Schema::Data(Data::Map(
|
||||
Box::new(Data::Bytes),
|
||||
Box::new(Data::List(Items::One(Box::new(Data::Integer)))),
|
||||
));
|
||||
assert_json(
|
||||
&schema,
|
||||
json!({
|
||||
"dataType": "map",
|
||||
"keys": {
|
||||
"dataType": "bytes"
|
||||
},
|
||||
"values": {
|
||||
"dataType": "list",
|
||||
"items": { "dataType": "integer" }
|
||||
"$ref": "#/definitions/ByteArray"
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
@ -764,12 +750,12 @@ pub mod test {
|
|||
let schema = Schema::Data(Data::AnyOf(vec![
|
||||
Constructor {
|
||||
index: 0,
|
||||
fields: vec![Field::Inline(Data::Integer.into())],
|
||||
fields: vec![Reference::new("Int")],
|
||||
}
|
||||
.into(),
|
||||
Constructor {
|
||||
index: 1,
|
||||
fields: vec![Field::Inline(Data::Bytes.into())],
|
||||
fields: vec![Reference::new("Bytes")],
|
||||
}
|
||||
.into(),
|
||||
]));
|
||||
|
@ -780,12 +766,12 @@ pub mod test {
|
|||
{
|
||||
"dataType": "constructor",
|
||||
"index": 0,
|
||||
"fields": [{ "dataType": "integer" }]
|
||||
"fields": [{ "$ref": "#/definitions/Int" }]
|
||||
},
|
||||
{
|
||||
"dataType": "constructor",
|
||||
"index": 1,
|
||||
"fields": [{ "dataType": "bytes" }]
|
||||
"fields": [{ "$ref": "#/definitions/Bytes" }]
|
||||
}
|
||||
]
|
||||
}),
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,7 +12,11 @@ pub mod pretty;
|
|||
pub mod script;
|
||||
pub mod telemetry;
|
||||
|
||||
use crate::blueprint::{schema::Schema, Blueprint};
|
||||
use crate::blueprint::{
|
||||
definitions::Reference,
|
||||
schema::{Annotated, Schema},
|
||||
Blueprint,
|
||||
};
|
||||
use aiken_lang::{
|
||||
ast::{Definition, Function, ModuleKind, Tracing, TypedDataType, TypedFunction},
|
||||
builder::{DataTypeKey, FunctionAccessKey},
|
||||
|
@ -214,7 +218,10 @@ where
|
|||
self.compile(options)
|
||||
}
|
||||
|
||||
pub fn dump_uplc(&self, blueprint: &Blueprint<Schema>) -> Result<(), Error> {
|
||||
pub fn dump_uplc(
|
||||
&self,
|
||||
blueprint: &Blueprint<Reference, Annotated<Schema>>,
|
||||
) -> Result<(), Error> {
|
||||
let dir = self.root.join("artifacts");
|
||||
|
||||
self.event_listener
|
||||
|
@ -355,7 +362,7 @@ where
|
|||
// Read blueprint
|
||||
let blueprint = File::open(self.blueprint_path())
|
||||
.map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?;
|
||||
let blueprint: Blueprint<serde_json::Value> =
|
||||
let blueprint: Blueprint<serde_json::Value, serde_json::Value> =
|
||||
serde_json::from_reader(BufReader::new(blueprint))?;
|
||||
|
||||
// Calculate the address
|
||||
|
@ -379,11 +386,11 @@ where
|
|||
&self,
|
||||
title: Option<&String>,
|
||||
param: &Term<DeBruijn>,
|
||||
) -> Result<Blueprint<serde_json::Value>, Error> {
|
||||
) -> Result<Blueprint<serde_json::Value, serde_json::Value>, Error> {
|
||||
// Read blueprint
|
||||
let blueprint = File::open(self.blueprint_path())
|
||||
.map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?;
|
||||
let mut blueprint: Blueprint<serde_json::Value> =
|
||||
let mut blueprint: Blueprint<serde_json::Value, serde_json::Value> =
|
||||
serde_json::from_reader(BufReader::new(blueprint))?;
|
||||
|
||||
// Apply parameters
|
||||
|
|
|
@ -65,7 +65,7 @@ pub fn exec(
|
|||
.map_err(|_| BlueprintError::InvalidOrMissingFile)
|
||||
.into_diagnostic()?;
|
||||
|
||||
let blueprint: Blueprint<serde_json::Value> =
|
||||
let blueprint: Blueprint<serde_json::Value, serde_json::Value> =
|
||||
serde_json::from_reader(BufReader::new(blueprint)).into_diagnostic()?;
|
||||
|
||||
// Perform the conversion
|
||||
|
|
Loading…
Reference in New Issue