Support interactive blueprint parameter application.
This commit is contained in:
parent
c1b8040ae2
commit
139226cdab
|
@ -32,6 +32,9 @@ aiken-lsp = { path = "../aiken-lsp", version = "1.0.14-alpha" }
|
|||
aiken-project = { path = '../aiken-project', version = "1.0.14-alpha" }
|
||||
uplc = { path = '../uplc', version = "1.0.14-alpha" }
|
||||
clap_complete = "4.3.2"
|
||||
inquire = "0.6.2"
|
||||
num-bigint = "0.4.3"
|
||||
ordinal = "0.3.2"
|
||||
|
||||
[build-dependencies]
|
||||
built = { version = "0.6.0", features = ["git2"] }
|
||||
|
|
|
@ -1,8 +1,21 @@
|
|||
use crate::with_project;
|
||||
use aiken_project::{blueprint, error::Error};
|
||||
use aiken_project::{
|
||||
blueprint::{
|
||||
self,
|
||||
definitions::Definitions,
|
||||
schema::{Annotated, Constructor, Data, Declaration, Items, Schema},
|
||||
},
|
||||
error::Error,
|
||||
pretty::multiline,
|
||||
};
|
||||
use inquire;
|
||||
use num_bigint::BigInt;
|
||||
use ordinal::Ordinal;
|
||||
use owo_colors::{OwoColorize, Stream::Stderr};
|
||||
use pallas_primitives::alonzo::PlutusData;
|
||||
use std::str::FromStr;
|
||||
use std::{fs, path::PathBuf, process, rc::Rc};
|
||||
use uplc::ast::{Constant, DeBruijn, Term};
|
||||
use uplc::ast::{Constant, Data as UplcData, DeBruijn, Term};
|
||||
|
||||
/// Apply a parameter to a parameterized validator.
|
||||
#[derive(clap::Args)]
|
||||
|
@ -12,10 +25,7 @@ pub struct Args {
|
|||
/// For example, `182A` designates an integer of value 42. If you're unsure about the shape of
|
||||
/// the parameter, look at the schema specified in the project's blueprint (i.e.
|
||||
/// `plutus.json`), or use the `cbor.serialise` function from the Aiken standard library.
|
||||
parameter: String,
|
||||
|
||||
/// Path to project
|
||||
directory: Option<PathBuf>,
|
||||
parameter: Option<String>,
|
||||
|
||||
/// Output file. Optional, print on stdout when omitted.
|
||||
#[clap(short, long)]
|
||||
|
@ -33,55 +43,12 @@ pub struct Args {
|
|||
pub fn exec(
|
||||
Args {
|
||||
parameter,
|
||||
directory,
|
||||
out,
|
||||
module,
|
||||
validator,
|
||||
}: Args,
|
||||
) -> miette::Result<()> {
|
||||
eprintln!(
|
||||
"{} inputs",
|
||||
" Parsing"
|
||||
.if_supports_color(Stderr, |s| s.purple())
|
||||
.if_supports_color(Stderr, |s| s.bold()),
|
||||
);
|
||||
|
||||
let bytes = hex::decode(parameter)
|
||||
.map_err::<Error, _>(|e| {
|
||||
blueprint::error::Error::MalformedParameter {
|
||||
hint: format!("Invalid hex-encoded string: {e}"),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
println!();
|
||||
e.report();
|
||||
process::exit(1)
|
||||
});
|
||||
|
||||
let data = uplc::plutus_data(&bytes)
|
||||
.map_err::<Error, _>(|e| {
|
||||
blueprint::error::Error::MalformedParameter {
|
||||
hint: format!("Invalid Plutus data; malformed CBOR encoding: {e}"),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
println!();
|
||||
e.report();
|
||||
process::exit(1)
|
||||
});
|
||||
|
||||
let term: Term<DeBruijn> = Term::Constant(Rc::new(Constant::Data(data)));
|
||||
|
||||
eprintln!(
|
||||
"{} blueprint",
|
||||
" Analyzing"
|
||||
.if_supports_color(Stderr, |s| s.purple())
|
||||
.if_supports_color(Stderr, |s| s.bold()),
|
||||
);
|
||||
|
||||
with_project(directory, |p| {
|
||||
with_project(None, |p| {
|
||||
let title = module.as_ref().map(|m| {
|
||||
format!(
|
||||
"{m}{}",
|
||||
|
@ -95,10 +62,65 @@ pub fn exec(
|
|||
let title = title.as_ref().or(validator.as_ref());
|
||||
|
||||
eprintln!(
|
||||
"{} parameter",
|
||||
"{} blueprint",
|
||||
" Analyzing"
|
||||
.if_supports_color(Stderr, |s| s.purple())
|
||||
.if_supports_color(Stderr, |s| s.bold()),
|
||||
);
|
||||
|
||||
let term: Term<DeBruijn> = match ¶meter {
|
||||
Some(param) => {
|
||||
eprintln!(
|
||||
"{} inputs",
|
||||
" Parsing"
|
||||
.if_supports_color(Stderr, |s| s.purple())
|
||||
.if_supports_color(Stderr, |s| s.bold()),
|
||||
);
|
||||
|
||||
let bytes = hex::decode(param)
|
||||
.map_err::<Error, _>(|e| {
|
||||
blueprint::error::Error::MalformedParameter {
|
||||
hint: format!("Invalid hex-encoded string: {e}"),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
println!();
|
||||
e.report();
|
||||
process::exit(1)
|
||||
});
|
||||
|
||||
let data = uplc::plutus_data(&bytes)
|
||||
.map_err::<Error, _>(|e| {
|
||||
blueprint::error::Error::MalformedParameter {
|
||||
hint: format!("Invalid Plutus data; malformed CBOR encoding: {e}"),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
println!();
|
||||
e.report();
|
||||
process::exit(1)
|
||||
});
|
||||
|
||||
Term::Constant(Rc::new(Constant::Data(data)))
|
||||
}
|
||||
|
||||
None => p.construct_parameter_incrementally(title, ask_schema)?,
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"{} {}",
|
||||
" Applying"
|
||||
.if_supports_color(Stderr, |s| s.purple())
|
||||
.if_supports_color(Stderr, |s| s.bold()),
|
||||
match TryInto::<PlutusData>::try_into(term.clone()) {
|
||||
Ok(data) => {
|
||||
let padding = "\n ";
|
||||
multiline(48, UplcData::to_hex(data)).join(padding)
|
||||
}
|
||||
Err(_) => term.to_pretty(),
|
||||
}
|
||||
);
|
||||
|
||||
let blueprint = p.apply_parameter(title, &term)?;
|
||||
|
@ -126,3 +148,262 @@ pub fn exec(
|
|||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn ask_schema(
|
||||
schema: &Annotated<Schema>,
|
||||
definitions: &Definitions<Annotated<Schema>>,
|
||||
) -> Result<PlutusData, blueprint::error::Error> {
|
||||
match schema.annotated {
|
||||
Schema::Data(Data::Integer) => {
|
||||
let input = prompt_primitive("an integer", schema)?;
|
||||
|
||||
let n = BigInt::from_str(input.as_str()).map_err(|e| {
|
||||
blueprint::error::Error::MalformedParameter {
|
||||
hint: format!("Unable to convert input to integer: {e}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(UplcData::integer(n))
|
||||
}
|
||||
|
||||
Schema::Data(Data::Bytes) => {
|
||||
let input = prompt_primitive("a byte-array", schema)?;
|
||||
|
||||
let bytes =
|
||||
hex::decode(input).map_err(|e| blueprint::error::Error::MalformedParameter {
|
||||
hint: format!("Invalid hex-encoded string: {e}"),
|
||||
})?;
|
||||
|
||||
Ok(UplcData::bytestring(bytes))
|
||||
}
|
||||
|
||||
Schema::Data(Data::List(Items::Many(ref decls))) => {
|
||||
eprintln!(" {}", asking(schema, "Found", &format!("a {}-tuple", decls.len())));
|
||||
|
||||
let mut elems = vec![];
|
||||
|
||||
for (ix, decl) in decls.iter().enumerate() {
|
||||
eprintln!(
|
||||
" {} Tuple's {}{} element",
|
||||
"Asking".if_supports_color(Stderr, |s| s.purple()).if_supports_color(Stderr, |s| s.bold()),
|
||||
ix+1,
|
||||
Ordinal::<usize>(ix+1).suffix()
|
||||
);
|
||||
let inner_schema = lookup_declaration(&decl.clone().into(), definitions);
|
||||
elems.push(ask_schema(&inner_schema, definitions)?);
|
||||
}
|
||||
|
||||
Ok(UplcData::list(elems))
|
||||
}
|
||||
|
||||
Schema::Data(Data::List(Items::One(ref decl))) => {
|
||||
eprintln!(" {}", asking(schema, "Found", "a list"));
|
||||
|
||||
let inner_schema = lookup_declaration(&decl.clone().into(), definitions);
|
||||
|
||||
let mut elems = vec![];
|
||||
while prompt_iterable(schema, "item")? {
|
||||
elems.push(ask_schema(&inner_schema, definitions)?);
|
||||
}
|
||||
|
||||
Ok(UplcData::list(elems))
|
||||
}
|
||||
|
||||
Schema::Data(Data::Map(ref key_decl, ref value_decl)) => {
|
||||
eprintln!(" {}", asking(schema, "Found", "an associative map"));
|
||||
|
||||
let key_schema = lookup_declaration(&key_decl.clone().into(), definitions);
|
||||
let value_schema = lookup_declaration(&value_decl.clone().into(), definitions);
|
||||
|
||||
let mut elems = vec![];
|
||||
while prompt_iterable(schema, "key/value entry")? {
|
||||
elems.push((
|
||||
ask_schema(&key_schema, definitions)?,
|
||||
ask_schema(&value_schema, definitions)?,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(UplcData::map(elems))
|
||||
}
|
||||
|
||||
Schema::Data(Data::AnyOf(ref constructors)) => {
|
||||
eprintln!(
|
||||
" {}",
|
||||
asking(
|
||||
schema,
|
||||
"Found",
|
||||
if constructors.len() == 1 {
|
||||
"a record"
|
||||
} else {
|
||||
"a data-type"
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
let ix = prompt_constructor(constructors, schema)?;
|
||||
|
||||
let mut fields = Vec::new();
|
||||
for field in &constructors[ix].annotated.fields {
|
||||
let inner_schema = lookup_declaration(field, definitions);
|
||||
fields.push(ask_schema(&inner_schema, definitions)?);
|
||||
}
|
||||
|
||||
Ok(UplcData::constr(ix.try_into().unwrap(), fields))
|
||||
}
|
||||
|
||||
_ => unimplemented!("Hey! You've found a case that we haven't implemented yet. Yes, we've been a bit lazy on that one... If that use-case is important to you, please let us know on Discord or on Github."),
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_declaration(
|
||||
decl: &Annotated<Declaration<Data>>,
|
||||
definitions: &Definitions<Annotated<Schema>>,
|
||||
) -> Annotated<Schema> {
|
||||
match decl.annotated {
|
||||
Declaration::Inline(ref data) => Annotated {
|
||||
title: decl.title.clone(),
|
||||
description: decl.description.clone(),
|
||||
annotated: Schema::Data(*(*data).clone()),
|
||||
},
|
||||
Declaration::Referenced(ref reference) => {
|
||||
let schema = definitions
|
||||
.lookup(reference)
|
||||
.expect("reference to unknown type in blueprint?");
|
||||
Annotated {
|
||||
title: decl.title.clone().or_else(|| schema.title.clone()),
|
||||
description: decl
|
||||
.description
|
||||
.clone()
|
||||
.or_else(|| schema.description.clone()),
|
||||
annotated: schema.annotated.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn asking(schema: &Annotated<Schema>, verb: &str, type_name: &str) -> String {
|
||||
let subject = get_subject(schema, type_name);
|
||||
format!(
|
||||
"{} {subject}",
|
||||
verb.if_supports_color(Stderr, |s| s.purple())
|
||||
.if_supports_color(Stderr, |s| s.bold()),
|
||||
subject = subject,
|
||||
)
|
||||
}
|
||||
|
||||
fn prompt_primitive(
|
||||
type_name: &str,
|
||||
schema: &Annotated<Schema>,
|
||||
) -> Result<String, blueprint::error::Error> {
|
||||
inquire::Text::new(&format!(" {}:", asking(schema, "Asking", type_name)))
|
||||
.with_description(schema.description.as_ref())
|
||||
.prompt()
|
||||
.map_err(|e| blueprint::error::Error::MalformedParameter {
|
||||
hint: format!("Invalid input received from prompt: {e}"),
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_iterable(
|
||||
schema: &Annotated<Schema>,
|
||||
elem_name: &str,
|
||||
) -> Result<bool, blueprint::error::Error> {
|
||||
inquire::Confirm::new(&format!(
|
||||
" {} one more {elem_name}?",
|
||||
"Adding"
|
||||
.if_supports_color(Stderr, |s| s.purple())
|
||||
.if_supports_color(Stderr, |s| s.bold())
|
||||
))
|
||||
.with_description(schema.description.as_ref())
|
||||
.with_default(true)
|
||||
.prompt()
|
||||
.map_err(|e| blueprint::error::Error::MalformedParameter {
|
||||
hint: format!("Invalid input received from prompt: {e}"),
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_constructor(
|
||||
constructors: &[Annotated<Constructor>],
|
||||
schema: &Annotated<Schema>,
|
||||
) -> Result<usize, blueprint::error::Error> {
|
||||
let mut choices = Vec::new();
|
||||
for c in constructors {
|
||||
let name = c
|
||||
.title
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("{}", c.annotated.index));
|
||||
choices.push(name);
|
||||
}
|
||||
|
||||
let mut choice = choices
|
||||
.first()
|
||||
.expect("Data-type with no constructor?")
|
||||
.to_string();
|
||||
|
||||
if choices.len() > 1 {
|
||||
choice = inquire::Select::new(
|
||||
&format!(
|
||||
" {} constructor",
|
||||
"Selecting"
|
||||
.if_supports_color(Stderr, |s| s.purple())
|
||||
.if_supports_color(Stderr, |s| s.bold())
|
||||
),
|
||||
choices.clone(),
|
||||
)
|
||||
.with_description(schema.description.as_ref())
|
||||
.prompt()
|
||||
.map_err(|e| blueprint::error::Error::MalformedParameter {
|
||||
hint: format!("Invalid input received from prompt: {e}"),
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(choices.into_iter().position(|c| c == choice).unwrap())
|
||||
}
|
||||
|
||||
fn get_subject<T>(schema: &Annotated<T>, type_name: &str) -> String {
|
||||
schema
|
||||
.title
|
||||
.as_ref()
|
||||
.map(|title| format!("{title} ({type_name})"))
|
||||
.unwrap_or_else(|| type_name.to_string())
|
||||
}
|
||||
|
||||
trait WithDescription<'a> {
|
||||
fn with_description(self, opt: Option<&'a String>) -> Self;
|
||||
}
|
||||
|
||||
impl<'a> WithDescription<'a> for inquire::Confirm<'a> {
|
||||
fn with_description(
|
||||
self: inquire::Confirm<'a>,
|
||||
opt: Option<&'a String>,
|
||||
) -> inquire::Confirm<'a> {
|
||||
match opt {
|
||||
Some(description) => self.with_help_message(description),
|
||||
None => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WithDescription<'a> for inquire::Text<'a> {
|
||||
fn with_description(self: inquire::Text<'a>, opt: Option<&'a String>) -> inquire::Text<'a> {
|
||||
match opt {
|
||||
Some(description) => self.with_help_message(description),
|
||||
None => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> WithDescription<'a> for inquire::Select<'a, T>
|
||||
where
|
||||
T: std::fmt::Display,
|
||||
{
|
||||
fn with_description(
|
||||
self: inquire::Select<'a, T>,
|
||||
opt: Option<&'a String>,
|
||||
) -> inquire::Select<'a, T> {
|
||||
match opt {
|
||||
Some(description) => self.with_help_message(description),
|
||||
None => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue