Bootstrap schema validation for simple constants.
This commit is contained in:
parent
9033b44044
commit
d620f6367c
|
@ -1,13 +1,10 @@
|
|||
use std::{cell::RefCell, collections::HashMap, ops::Deref, sync::Arc};
|
||||
|
||||
use uplc::{ast::Type as UplcType, builtins::DefaultFunction};
|
||||
|
||||
use self::{environment::Environment, pretty::Printer};
|
||||
use crate::{
|
||||
ast::{Constant, DefinitionLocation, ModuleKind, Span},
|
||||
tipo::fields::FieldMap,
|
||||
};
|
||||
|
||||
use self::{environment::Environment, pretty::Printer};
|
||||
use std::{cell::RefCell, collections::HashMap, ops::Deref, sync::Arc};
|
||||
use uplc::{ast::Type as UplcType, builtins::DefaultFunction};
|
||||
|
||||
mod environment;
|
||||
pub mod error;
|
||||
|
|
|
@ -95,14 +95,14 @@ impl Reference {
|
|||
}
|
||||
|
||||
/// Turn a reference into a key suitable for lookup.
|
||||
fn as_key(&self) -> &str {
|
||||
pub(crate) fn as_key(&self) -> &str {
|
||||
self.inner.as_str()
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
pub(crate) fn as_json_pointer(&self) -> String {
|
||||
format!("#/definitions/{}", self.as_key().replace('/', "~1"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use super::schema;
|
||||
use super::{
|
||||
definitions::Reference,
|
||||
schema::{self, Schema},
|
||||
};
|
||||
use aiken_lang::ast::Span;
|
||||
use miette::{Diagnostic, NamedSource};
|
||||
use owo_colors::{OwoColorize, Stream::Stdout};
|
||||
|
@ -43,6 +46,30 @@ pub enum Error {
|
|||
blueprint_apply_command = "blueprint apply".if_supports_color(Stdout, |s| s.purple()),
|
||||
))]
|
||||
ParameterizedValidator { n: usize },
|
||||
|
||||
#[error("I failed to infer what should be the schema of a given parameter to apply.")]
|
||||
#[diagnostic(code("aiken:blueprint::apply::malformed::argument"))]
|
||||
#[diagnostic(help(
|
||||
"I couldn't figure out the schema corresponding to a term you've given. Here's a possible hint about why I failed: {hint}"
|
||||
))]
|
||||
UnableToInferArgumentSchema { hint: String },
|
||||
|
||||
#[error("I couldn't find a definition corresponding to a reference.")]
|
||||
#[diagnostic(code("aiken::blueprint::apply::unknown::reference"))]
|
||||
#[diagnostic(help(
|
||||
"While resolving a schema definition, I stumble upon an unknown reference:\n\n {reference}\n\nThis is unfortunate, but signals that either the reference is invalid or that the correspond schema definition is missing.",
|
||||
reference = reference.as_json_pointer()
|
||||
))]
|
||||
UnresolvedSchemaReference { reference: Reference },
|
||||
|
||||
#[error("I caught a parameter application that seems off.")]
|
||||
#[diagnostic(code("aiken::blueprint::apply::mismatch"))]
|
||||
#[diagnostic(help(
|
||||
"When applying parameters to a validator, I control that the shape of the parameter you give me matches what is specified in the blueprint. Unfortunately, schemas didn't match in this case.\n\nI am expecting the following:\n\n{}But I've inferred the following schema from your input:\n\n{}",
|
||||
serde_json::to_string_pretty(&expected).unwrap().if_supports_color(Stdout, |s| s.green()),
|
||||
serde_json::to_string_pretty(&inferred).unwrap().if_supports_color(Stdout, |s| s.red()),
|
||||
))]
|
||||
SchemaMismatch { expected: Schema, inferred: Schema },
|
||||
}
|
||||
|
||||
unsafe impl Send for Error {}
|
||||
|
|
|
@ -12,6 +12,7 @@ use serde::{
|
|||
ser::{Serialize, SerializeStruct, Serializer},
|
||||
};
|
||||
use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
|
||||
use uplc::ast::Term;
|
||||
|
||||
// NOTE: Can be anything BUT 0
|
||||
pub const REDEEMER_DISCRIMINANT: usize = 1;
|
||||
|
@ -80,6 +81,50 @@ impl<T> From<T> for Annotated<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T> TryFrom<&'a Term<T>> for Schema {
|
||||
type Error = &'a str;
|
||||
|
||||
fn try_from(term: &'a Term<T>) -> Result<Schema, Self::Error> {
|
||||
use uplc::{ast::Constant, Constr, PlutusData};
|
||||
|
||||
match term {
|
||||
Term::Constant(constant) => match constant.deref() {
|
||||
Constant::Integer(..) => Ok(Schema::Integer),
|
||||
Constant::Bool(..) => Ok(Schema::Boolean),
|
||||
Constant::ByteString(..) => Ok(Schema::Bytes),
|
||||
Constant::String(..) => Ok(Schema::String),
|
||||
Constant::Unit => Ok(Schema::Unit),
|
||||
Constant::ProtoList{..} => todo!("can't convert from ProtoList to Schema; note that you probably want to use a Data's list instead anyway."),
|
||||
Constant::ProtoPair{..} => todo!("can't convert from ProtoPair to Schema; note that you probably want to use a Data's list instead anyway."),
|
||||
Constant::Data(data) => Ok(Schema::Data(match data {
|
||||
PlutusData::BigInt(..) => {
|
||||
Data::Integer
|
||||
}
|
||||
PlutusData::BoundedBytes(..) => {
|
||||
Data::Bytes
|
||||
}
|
||||
PlutusData::Map(keyValuePair) => {
|
||||
todo!()
|
||||
}
|
||||
PlutusData::Array(elems) => {
|
||||
todo!()
|
||||
}
|
||||
PlutusData::Constr(Constr{ tag, fields, any_constructor }) => {
|
||||
todo!()
|
||||
}
|
||||
}))
|
||||
},
|
||||
Term::Delay(..)
|
||||
| Term::Lambda { .. }
|
||||
| Term::Var(..)
|
||||
| Term::Apply { .. }
|
||||
| Term::Force(..)
|
||||
| Term::Error
|
||||
| Term::Builtin(..) => Err("not a UPLC constant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Annotated<Schema> {
|
||||
pub fn as_wrapped_redeemer(
|
||||
definitions: &mut Definitions<Annotated<Schema>>,
|
||||
|
|
|
@ -6,11 +6,13 @@ use super::{
|
|||
use crate::module::{CheckedModule, CheckedModules};
|
||||
use aiken_lang::{
|
||||
ast::{TypedArg, TypedFunction, TypedValidator},
|
||||
builtins,
|
||||
gen_uplc::CodeGenerator,
|
||||
};
|
||||
use miette::NamedSource;
|
||||
use serde;
|
||||
use uplc::ast::{DeBruijn, Program, Term};
|
||||
use std::{borrow::Borrow, collections::HashMap};
|
||||
use uplc::ast::{Constant, DeBruijn, Program, Term};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Validator {
|
||||
|
@ -44,6 +46,48 @@ pub struct Argument {
|
|||
pub schema: Reference,
|
||||
}
|
||||
|
||||
impl From<Reference> for Argument {
|
||||
fn from(schema: Reference) -> Argument {
|
||||
Argument {
|
||||
title: None,
|
||||
schema,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Argument {
|
||||
pub fn validate(
|
||||
&self,
|
||||
definitions: &Definitions<Annotated<Schema>>,
|
||||
term: &Term<DeBruijn>,
|
||||
) -> Result<(), Error> {
|
||||
let expected_schema = &definitions
|
||||
.lookup(&self.schema)
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| {
|
||||
Err(Error::UnresolvedSchemaReference {
|
||||
reference: self.schema.clone(),
|
||||
})
|
||||
})?
|
||||
.annotated;
|
||||
|
||||
let inferred_schema: Schema =
|
||||
term.try_into()
|
||||
.map_err(|hint: &str| Error::UnableToInferArgumentSchema {
|
||||
hint: hint.to_owned(),
|
||||
})?;
|
||||
|
||||
if expected_schema != &inferred_schema {
|
||||
Err(Error::SchemaMismatch {
|
||||
expected: expected_schema.to_owned(),
|
||||
inferred: inferred_schema,
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Validator {
|
||||
pub fn from_checked_module(
|
||||
modules: &CheckedModules,
|
||||
|
@ -164,8 +208,8 @@ impl Validator {
|
|||
pub fn apply(self, arg: &Term<DeBruijn>) -> Result<Self, Error> {
|
||||
match self.parameters.split_first() {
|
||||
None => Err(Error::NoParametersToApply),
|
||||
Some((_, tail)) => {
|
||||
// TODO: Ideally, we should control that the applied term matches its schema.
|
||||
Some((head, tail)) => {
|
||||
head.validate(&self.definitions, arg)?;
|
||||
Ok(Self {
|
||||
program: self.program.apply_term(arg),
|
||||
parameters: tail.to_vec(),
|
||||
|
@ -178,7 +222,14 @@ impl Validator {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use super::{
|
||||
super::{
|
||||
definitions::Definitions,
|
||||
error::Error,
|
||||
schema::{Annotated, Data, Schema},
|
||||
},
|
||||
*,
|
||||
};
|
||||
use crate::{module::ParsedModule, PackageName};
|
||||
use aiken_lang::{
|
||||
self,
|
||||
|
@ -193,6 +244,7 @@ mod test {
|
|||
use indexmap::IndexMap;
|
||||
use serde_json::{self, json};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use uplc::ast as uplc;
|
||||
|
||||
// TODO: Possible refactor this out of the module and have it used by `Project`. The idea would
|
||||
// be to make this struct below the actual project, and wrap it in another metadata struct
|
||||
|
@ -314,6 +366,24 @@ mod test {
|
|||
assert_json_eq!(serde_json::to_value(validator).unwrap(), expected);
|
||||
}
|
||||
|
||||
fn fixture_definitions() -> Definitions<Annotated<Schema>> {
|
||||
let mut definitions = Definitions::new();
|
||||
|
||||
definitions
|
||||
.register::<_, Error>(&builtins::int(), &HashMap::new(), |_| {
|
||||
Ok(Schema::Data(Data::Integer).into())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
definitions
|
||||
.register::<_, Error>(&builtins::byte_array(), &HashMap::new(), |_| {
|
||||
Ok(Schema::Data(Data::Bytes).into())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
definitions
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mint_basic() {
|
||||
assert_validator(
|
||||
|
@ -1092,4 +1162,27 @@ mod test {
|
|||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_arguments_integer() {
|
||||
let term = Term::data(uplc::Data::integer(42.into()));
|
||||
let definitions = fixture_definitions();
|
||||
let arg = Argument {
|
||||
title: None,
|
||||
schema: Reference::new("Int"),
|
||||
};
|
||||
|
||||
assert!(matches!(arg.validate(&definitions, &term), Ok { .. }))
|
||||
}
|
||||
#[test]
|
||||
fn validate_arguments_bytestring() {
|
||||
let term = Term::data(uplc::Data::bytestring(vec![102, 111, 111]));
|
||||
let definitions = fixture_definitions();
|
||||
let arg = Argument {
|
||||
title: None,
|
||||
schema: Reference::new("ByteArray"),
|
||||
};
|
||||
|
||||
assert!(matches!(arg.validate(&definitions, &term), Ok { .. }))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue