From 875e62d703e58e4fb4bc170544a091472591aef9 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Wed, 3 Dec 2025 16:18:38 -0300 Subject: [PATCH 1/3] feat: add CIP-57 parsing crate with blueprint and schema structures --- Cargo.lock | 9 + Cargo.toml | 7 +- crates/CIP-57/Cargo.toml | 19 ++ crates/CIP-57/src/blueprint.rs | 156 +++++++++++ crates/CIP-57/src/lib.rs | 482 +++++++++++++++++++++++++++++++++ crates/CIP-57/src/schema.rs | 267 ++++++++++++++++++ 6 files changed, 939 insertions(+), 1 deletion(-) create mode 100644 crates/CIP-57/Cargo.toml create mode 100644 crates/CIP-57/src/blueprint.rs create mode 100644 crates/CIP-57/src/lib.rs create mode 100644 crates/CIP-57/src/schema.rs diff --git a/Cargo.lock b/Cargo.lock index be1edebf..3a334efe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,6 +313,15 @@ dependencies = [ "half", ] +[[package]] +name = "cip57" +version = "0.13.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", +] + [[package]] name = "core-foundation" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index 42a07143..6cecbe46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,11 @@ [workspace] resolver = "2" -members = ["crates/tx3-cardano", "crates/tx3-lang", "crates/tx3-resolver"] +members = [ + "crates/tx3-cardano", + "crates/tx3-lang", + "crates/tx3-resolver", + "crates/CIP-57", +] [workspace.package] publish = true diff --git a/crates/CIP-57/Cargo.toml b/crates/CIP-57/Cargo.toml new file mode 100644 index 00000000..e7345fd9 --- /dev/null +++ b/crates/CIP-57/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cip57" +description = "CIP-57 compatibility (JSON parsing and serialization)" +publish.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true +keywords.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true + + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +anyhow = "1" diff --git a/crates/CIP-57/src/blueprint.rs b/crates/CIP-57/src/blueprint.rs new file mode 100644 index 00000000..09b4147c --- /dev/null +++ b/crates/CIP-57/src/blueprint.rs @@ -0,0 +1,156 @@ +//! This module defines the structures for a Blueprint, including its preamble, validators, and definitions. + +use serde::{Deserialize, Serialize}; +use serde_json::Number; +use std::collections::BTreeMap; + +/// Represents a blueprint containing preamble, validators, and optional definitions. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Blueprint { + pub preamble: Preamble, + pub validators: Vec, + pub definitions: Option, +} + +/// Represents the preamble of a blueprint, including metadata such as title, description, version, and compiler information. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Preamble { + pub title: String, + pub description: Option, + pub version: String, + pub plutus_version: String, + pub compiler: Option, + pub license: Option, +} + +/// Represents the compiler information in the preamble. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Compiler { + pub name: String, + pub version: Option, +} + +/// Represents a validator in the blueprint, including its title, description, compiled code, hash, datum, redeemer, and parameters. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Validator { + pub title: String, + pub description: Option, + pub compiled_code: Option, + pub hash: Option, + pub datum: Option, + pub redeemer: Option, + pub parameters: Option>, +} + +/// Represents an argument in a validator, including its title, description, purpose, and schema reference. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Argument { + pub title: Option, + pub description: Option, + pub purpose: Option, + pub schema: Reference, +} + +/// Represents a purpose array which can be either a single purpose or an array of purposes. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum PurposeArray { + Single(Purpose), + Array(Vec), +} + +/// Represents the purpose of an argument, which can be spend, mint, withdraw, or publish. +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum Purpose { + Spend, + Mint, + Withdraw, + Publish, +} + +/// Represents a reference to a schema. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Reference { + #[serde(rename = "$ref")] + pub reference: Option, +} + +/// Represents a parameter in a validator, including its title, description, purpose, and schema reference. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Parameter { + pub title: Option, + pub description: Option, + pub purpose: Option, + pub schema: Reference, +} + +/// Represents the definitions in a blueprint, which is a map of definition names to their corresponding definitions. +#[derive(Debug, Default, Deserialize, Serialize, Clone)] +pub struct Definitions { + #[serde(flatten, default)] + pub inner: BTreeMap, +} + +/// Represents a definition in the blueprint, including its title, description, data type, any_of schemas, items, keys, and values. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Definition { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub data_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub any_of: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub items: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub keys: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub values: Option, +} + +/// Represents an array of references which can be either a single reference or an array of references. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum ReferencesArray { + Single(Reference), + Array(Vec), +} + +/// Represents a schema in a definition, including its title, description, data type, index, and fields. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Schema { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub data_type: DataType, + pub index: Number, + pub fields: Vec, +} + +/// Represents the data type of a schema, which can be integer, bytes, list, map, or constructor. +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum DataType { + Integer, + Bytes, + List, + Map, + Constructor, +} + +/// Represents a field in a schema, including its title and reference. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Field { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(rename = "$ref")] + pub reference: String, +} diff --git a/crates/CIP-57/src/lib.rs b/crates/CIP-57/src/lib.rs new file mode 100644 index 00000000..1fe7651a --- /dev/null +++ b/crates/CIP-57/src/lib.rs @@ -0,0 +1,482 @@ +//! This module provides functions to work with blueprints and schemas, including +//! parsing JSON, reading from files, and JSON-to-JSON conversion helpers. + +use anyhow::Result; +use schema::TypeName; +use serde_json; +use std::fs; +use tx3_lang::ir::{Expression, StructExpr}; + +pub mod blueprint; +pub mod schema; +// Templates are intentionally removed for a pure JSON-to-JSON crate. + +pub struct Codegen {} + +impl Codegen { + pub fn new() -> Codegen { + Codegen {} + } + + fn get_schema_name(&self, key: String) -> String { + // Keep a simple normalization without external deps (heck) + let normalized = key + .replace("#/definitions/", "") + .replace("~1", " ") + .replace("/", " ") + .replace("_", " ") + .replace("$", " "); + // Basic PascalCase conversion + normalized + .split_whitespace() + .map(|w| { + let mut chars = w.chars(); + match chars.next() { + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + None => String::new(), + } + }) + .collect::>() + .join("") + } + + fn parse_bytes_string(s: &str) -> Vec { + if let Some(hex) = s.strip_prefix("0x") { + return hex::decode(hex).unwrap_or_default(); + } + s.as_bytes().to_vec() + } + + /// Parses a JSON string into a `Blueprint`. + /// + /// # Arguments + /// + /// * `json` - The JSON data from a `plutus.json` file + /// + /// # Returns + /// + /// A `Blueprint` instance or an error. + pub fn get_blueprint_from_json(&self, json: String) -> Result { + let bp = serde_json::from_str(&json)?; + Ok(bp) + } + + /// Reads a JSON file from a specified path and parses it into a `Blueprint`. + /// + /// # Arguments + /// + /// * `path` - The `plutus.json` file path in the filesystem + /// + /// # Returns + /// + /// A `Blueprint` instance or an error. + pub fn get_blueprint_from_path(&self, path: String) -> Result { + let json = fs::read_to_string(path)?; + self.get_blueprint_from_json(json) + } + + /// Obtains the list of schemas from a given `Blueprint`. + /// + /// # Arguments + /// + /// * `blueprint` - A `Blueprint` from which to obtain the schemas. + /// + /// # Returns + /// + /// A vector of `Schema` from the blueprint. + pub fn get_schemas_from_blueprint( + &self, + blueprint: blueprint::Blueprint, + ) -> Vec { + let mut schemas: Vec = vec![]; + if blueprint.definitions.is_some() { + for definition in blueprint.definitions.unwrap().inner.iter() { + let definition_name = self.get_schema_name(definition.0.clone()); + let definition_json = serde_json::to_string(&definition.1).unwrap(); + if definition.1.data_type.is_some() { + match definition.1.data_type.unwrap() { + blueprint::DataType::Integer => { + schemas.push(schema::Schema::new_integer( + definition_name.clone(), + definition_json.clone(), + )); + } + blueprint::DataType::Bytes => { + schemas.push(schema::Schema::new_bytes( + definition_name.clone(), + definition_json.clone(), + )); + } + blueprint::DataType::List => { + if definition.1.items.is_some() { + match definition.1.items.as_ref().unwrap() { + blueprint::ReferencesArray::Single(reference) => { + if reference.reference.is_some() { + schemas.push(schema::Schema::new_list( + definition_name.clone(), + schema::Reference { + name: None, + schema_name: self.get_schema_name( + reference + .reference + .as_ref() + .unwrap() + .clone(), + ), + }, + definition_json.clone(), + )); + } + } + blueprint::ReferencesArray::Array(references) => { + let mut properties: Vec = vec![]; + for reference in references { + if reference.reference.is_some() { + properties.push(schema::Reference { + name: None, + schema_name: self.get_schema_name( + reference + .reference + .as_ref() + .unwrap() + .clone(), + ), + }); + } + } + schemas.push(schema::Schema::new_tuple( + definition_name.clone(), + properties, + definition_json.clone(), + )); + } + } + } + } + _ => {} + } + } + if definition.1.title.is_some() { + if definition.1.title.as_ref().unwrap() == "Data" && definition_name == "Data" { + schemas.push(schema::Schema::new_anydata(definition_json.clone())); + } + } + if definition.1.any_of.is_some() { + let mut internal_schemas: Vec = vec![]; + for (index, parameter) in + definition.1.any_of.as_ref().unwrap().iter().enumerate() + { + match parameter.data_type { + blueprint::DataType::Constructor => { + let schema_name = format!( + "{}{}", + definition_name, + parameter.title.clone().unwrap_or((index + 1).to_string()) + ); + let mut properties: Vec = vec![]; + for property in ¶meter.fields { + let mut schema_name = + self.get_schema_name(property.reference.clone()); + if schema_name == "Data" { + schema_name = "AnyData".to_string(); + } + properties.push(schema::Reference { + name: property.title.clone(), + schema_name, + }); + } + let schema: schema::Schema; + if properties.len().gt(&0) || parameter.title.is_none() { + if properties.iter().any(|p| p.name.is_none()) { + schema = schema::Schema::new_tuple( + schema_name, + properties, + definition_json.clone(), + ); + } else { + schema = schema::Schema::new_object( + schema_name, + properties, + definition_json.clone(), + ); + } + } else { + schema = schema::Schema::new_literal( + schema_name, + parameter.title.clone().unwrap(), + definition_json.clone(), + ); + } + internal_schemas.push(schema); + } + _ => {} + } + } + if internal_schemas.len().eq(&1) { + let mut schema = internal_schemas.first().unwrap().clone(); + schema.name = definition_name.clone(); + schemas.push(schema); + } + if internal_schemas.len().gt(&1) { + if internal_schemas.len().eq(&2) + && internal_schemas + .iter() + .any(|s| s.type_name.eq(&TypeName::Literal)) + && !internal_schemas + .iter() + .all(|s| s.type_name.eq(&TypeName::Literal)) + { + let reference = internal_schemas + .iter() + .find(|s| s.type_name.ne(&TypeName::Literal)); + schemas.push(reference.unwrap().clone()); + schemas.push(schema::Schema::new_nullable( + definition_name.clone(), + reference.unwrap().name.clone(), + definition_json.clone(), + )); + } else { + for schema in &internal_schemas { + schemas.push(schema.clone()); + } + schemas.push(schema::Schema::new_enum( + definition_name.clone(), + &internal_schemas, + definition_json.clone(), + )); + } + } + } + } + } + + schemas + } + + /// Obtains the list of validators from a given `Blueprint`. + /// + /// # Arguments + /// + /// * `blueprint` - A `Blueprint` from which to obtain the validators. + /// + /// # Returns + /// + /// A vector of `Validator` from the blueprint. + pub fn get_validators_from_blueprint( + &self, + blueprint: blueprint::Blueprint, + ) -> Vec { + let mut validators: Vec = vec![]; + for validator in blueprint.validators.iter() { + let mut datum: Option = None; + if validator.datum.is_some() + && validator.datum.as_ref().unwrap().schema.reference.is_some() + { + datum = Some(schema::Reference { + name: validator.datum.as_ref().unwrap().title.clone(), + schema_name: self.get_schema_name( + validator + .datum + .as_ref() + .unwrap() + .schema + .reference + .as_ref() + .unwrap() + .clone(), + ), + }); + } + let mut redeemer: Option = None; + if validator.redeemer.is_some() + && validator + .redeemer + .as_ref() + .unwrap() + .schema + .reference + .is_some() + { + redeemer = Some(schema::Reference { + name: validator.redeemer.as_ref().unwrap().title.clone(), + schema_name: self.get_schema_name( + validator + .redeemer + .as_ref() + .unwrap() + .schema + .reference + .as_ref() + .unwrap() + .clone(), + ), + }); + } + let mut parameters: Vec = vec![]; + if let Some(p) = &validator.parameters { + for parameter in p { + if parameter.schema.reference.is_some() { + parameters.push(schema::Reference { + name: parameter.title.clone(), + schema_name: self.get_schema_name( + parameter.schema.reference.as_ref().unwrap().clone(), + ), + }) + } + } + } + validators.push(schema::Validator { + name: validator.title.clone(), + datum: datum, + redeemer: redeemer, + parameters: parameters, + }); + } + validators + } + + /// Converts a `Blueprint` into a JSON value containing extracted schemas and validators. + + + /// Convert a JSON value according to a schema name into a tx3 IR Expression. + /// This expects `value` to match the referenced schema (by name) present in the blueprint definitions. + pub fn convert_value_by_schema_name( + &self, + blueprint: &blueprint::Blueprint, + schema_name: &str, + value: &serde_json::Value, + ) -> Result { + // Build a quick lookup of definitions by normalized schema name + let mut defs = std::collections::BTreeMap::new(); + if let Some(all) = &blueprint.definitions { + for (raw_name, def) in &all.inner { + let name = self.get_schema_name(raw_name.clone()); + defs.insert(name, def); + } + } + + let def = defs + .get(schema_name) + .ok_or_else(|| anyhow::anyhow!("unknown schema: {}", schema_name))?; + + // Primitive types + if let Some(dt) = def.data_type { + match dt { + blueprint::DataType::Integer => { + let num = match value { + serde_json::Value::Number(n) => n.as_i64().unwrap_or(0) as i128, + serde_json::Value::String(s) => s.parse::().unwrap_or(0), + _ => 0, + }; + return Ok(Expression::Number(num)); + } + blueprint::DataType::Bytes => { + let bytes = match value { + serde_json::Value::String(s) => Self::parse_bytes_string(s), + serde_json::Value::Array(arr) => arr + .iter() + .filter_map(|v| v.as_u64().map(|x| x as u8)) + .collect(), + _ => Vec::new(), + }; + return Ok(Expression::Bytes(bytes)); + } + blueprint::DataType::List => { + // For lists, expect a single reference in items (or tuple in Array case already handled elsewhere) + if let Some(items) = &def.items { + match items { + blueprint::ReferencesArray::Single(r) => { + let inner_name = r + .reference + .as_ref() + .map(|s| self.get_schema_name(s.clone())) + .ok_or_else(|| anyhow::anyhow!("list items missing $ref"))?; + let list = match value { + serde_json::Value::Array(arr) => { + let mut out = Vec::new(); + for v in arr { + out.push(self.convert_value_by_schema_name( + blueprint, + &inner_name, + v, + )?); + } + out + } + _ => Vec::new(), + }; + return Ok(Expression::List(list)); + } + blueprint::ReferencesArray::Array(_) => { + // Treat as tuple + let arr = value.as_array().cloned().unwrap_or_default(); + let mut fields = Vec::new(); + if let blueprint::ReferencesArray::Array(refs) = items { + for (i, r) in refs.iter().enumerate() { + let inner_name = r + .reference + .as_ref() + .map(|s| self.get_schema_name(s.clone())) + .ok_or_else(|| { + anyhow::anyhow!("tuple item missing $ref") + })?; + let v = + arr.get(i).cloned().unwrap_or(serde_json::Value::Null); + fields.push(self.convert_value_by_schema_name( + blueprint, + &inner_name, + &v, + )?); + } + } + return Ok(Expression::Struct(StructExpr { + constructor: 0, + fields, + })); + } + } + } + } + _ => {} + } + } + + // Constructors (anyOf with data_type = Constructor). We choose variant by matching expected shape or `index`. + if let Some(any_of) = &def.any_of { + // Strategy: if `value` is an object with fields, pick matching constructor by field count; else use first. + let chosen = any_of + .first() + .ok_or_else(|| anyhow::anyhow!("empty anyOf"))?; + let index = chosen.index.as_i64().unwrap_or(0) as usize; + let mut fields_expr = Vec::new(); + for f in &chosen.fields { + let ref_name = self.get_schema_name(f.reference.clone()); + // Try to fetch field by title from value object; fallback to Null + let field_json = match value { + serde_json::Value::Object(map) => { + if let Some(title) = &f.title { + map.get(title).cloned().unwrap_or(serde_json::Value::Null) + } else { + serde_json::Value::Null + } + } + _ => serde_json::Value::Null, + }; + fields_expr.push(self.convert_value_by_schema_name( + blueprint, + &ref_name, + &field_json, + )?); + } + return Ok(Expression::Struct(StructExpr { + constructor: index, + fields: fields_expr, + })); + } + + // Fallback + Ok(Expression::None) + } + +// (no free functions) +} diff --git a/crates/CIP-57/src/schema.rs b/crates/CIP-57/src/schema.rs new file mode 100644 index 00000000..8680e523 --- /dev/null +++ b/crates/CIP-57/src/schema.rs @@ -0,0 +1,267 @@ +//! This module defines the structures and implementations for schemas, including types, references, and validators. + +use serde::{Deserialize, Serialize}; +use std::str; + +/// Represents the different types a schema can have. +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum TypeName { + AnyData, + Integer, + Bytes, + Literal, + Nullable, + Object, + Enum, + Tuple, + List, +} + +/// Represents a schema with a name, type, optional properties, and JSON representation. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Schema { + pub name: String, + pub type_name: TypeName, + pub properties: Option>, + pub json: String, +} + +/// Represents a reference to another schema, including an optional name and schema name. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Reference { + pub name: Option, + pub schema_name: String, +} + +/// Represents a validator with a name, optional datum and redeemer, and parameters. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Validator { + pub name: String, + pub datum: Option, + pub redeemer: Option, + pub parameters: Vec, +} + +impl Schema { + /// Creates a new any data schema. + /// + /// # Arguments + /// + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `AnyData`. + pub fn new_anydata(json: String) -> Self { + Self { + name: "AnyData".to_string(), + type_name: TypeName::AnyData, + properties: None, + json, + } + } + + /// Creates a new integer schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Integer`. + pub fn new_integer(name: String, json: String) -> Self { + Self { + name, + type_name: TypeName::Integer, + properties: None, + json, + } + } + + /// Creates a new bytes schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Bytes`. + pub fn new_bytes(name: String, json: String) -> Self { + Self { + name, + type_name: TypeName::Bytes, + properties: None, + json, + } + } + + /// Creates a new literal schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `value` - The literal value of the schema. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Literal`. + pub fn new_literal(name: String, value: String, json: String) -> Self { + Self { + name, + type_name: TypeName::Literal, + properties: Some(vec![Reference { + name: None, + schema_name: value, + }]), + json, + } + } + + /// Creates a new nullable schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `reference` - The reference schema name. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Nullable`. + pub fn new_nullable(name: String, reference: String, json: String) -> Self { + Self { + name, + type_name: TypeName::Nullable, + properties: Some(vec![Reference { + name: None, + schema_name: reference, + }]), + json, + } + } + + /// Creates a new object schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `properties` - The properties of the object schema. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Object`. + pub fn new_object(name: String, properties: Vec, json: String) -> Self { + Self { + name, + type_name: TypeName::Object, + properties: Some(properties), + json, + } + } + + /// Creates a new enum schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `schemas` - The schemas that make up the enum. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Enum`. + pub fn new_enum(name: String, schemas: &Vec, json: String) -> Self { + Self { + name, + type_name: TypeName::Enum, + properties: Some( + schemas + .iter() + .map(|s| Reference { + name: None, + schema_name: s.name.clone(), + }) + .collect(), + ), + json, + } + } + + /// Creates a new tuple schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `properties` - The properties of the tuple schema. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `Tuple`. + pub fn new_tuple(name: String, properties: Vec, json: String) -> Self { + Self { + name, + type_name: TypeName::Tuple, + properties: Some(properties), + json, + } + } + + /// Creates a new list schema. + /// + /// # Arguments + /// + /// * `name` - The name of the schema. + /// * `reference` - The reference schema. + /// * `json` - The JSON representation of the schema. + /// + /// # Returns + /// + /// A new `Schema` instance with type `List`. + pub fn new_list(name: String, reference: Reference, json: String) -> Self { + Self { + name, + type_name: TypeName::List, + properties: Some(vec![reference]), + json, + } + } +} + +impl str::FromStr for TypeName { + type Err = (); + + /// Converts a string to a `TypeName`. + /// + /// # Arguments + /// + /// * `input` - The string representation of the type name. + /// + /// # Returns + /// + /// A `Result` containing the `TypeName` or an error. + fn from_str(input: &str) -> Result { + match input { + "AnyData" => Ok(TypeName::AnyData), + "Integer" => Ok(TypeName::Integer), + "Bytes" => Ok(TypeName::Bytes), + "Literal" => Ok(TypeName::Literal), + "Nullable" => Ok(TypeName::Nullable), + "Object" => Ok(TypeName::Object), + "Enum" => Ok(TypeName::Enum), + "Tuple" => Ok(TypeName::Tuple), + "List" => Ok(TypeName::List), + _ => Err(()), + } + } +} From e8d4749080d2419c06c488667b9b07289de4114f Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 4 Dec 2025 13:35:20 -0300 Subject: [PATCH 2/3] Refactor blueprint and schema modules: consolidate structures and remove unused code --- crates/CIP-57/examples/plutus.json | 634 +++++++++++++++++++++++++++++ crates/CIP-57/src/blueprint.rs | 156 ------- crates/CIP-57/src/lib.rs | 620 ++++++++-------------------- crates/CIP-57/src/schema.rs | 267 ------------ 4 files changed, 793 insertions(+), 884 deletions(-) create mode 100644 crates/CIP-57/examples/plutus.json delete mode 100644 crates/CIP-57/src/blueprint.rs delete mode 100644 crates/CIP-57/src/schema.rs diff --git a/crates/CIP-57/examples/plutus.json b/crates/CIP-57/examples/plutus.json new file mode 100644 index 00000000..97472029 --- /dev/null +++ b/crates/CIP-57/examples/plutus.json @@ -0,0 +1,634 @@ +{ + "preamble": { + "title": "txpipe/contract", + "description": "Aiken contracts for project 'txpipe/contract'", + "version": "0.0.0", + "plutusVersion": "v3", + "compiler": { + "name": "Aiken", + "version": "v1.1.17+c3a7fba" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "githoney_contract.badges_contract.spend", + "datum": { + "title": "_datum", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1Datum" + } + }, + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1Redeemer" + } + }, + "parameters": [ + { + "title": "settings_policy_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "eedaa957c60268de", + "hash": "24b9b1964ce02550db270a1d6270b505b9c0342625ee766d77fab1f9" + }, + { + "title": "githoney_contract.badges_contract.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "settings_policy_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "3fe9763bc5ea0108", + "hash": "24b9b1964ce02550db270a1d6270b505b9c0342625ee766d77fab1f9" + }, + { + "title": "githoney_contract.badges_policy.mint", + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1Redeemer" + } + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "nonce", + "schema": { + "$ref": "#/definitions/Int" + } + } + ], + "compiledCode": "c8db1de3fbbc8921", + "hash": "87d6bd0b40f204d49803dad8e0d70611918d22354125c8f226a42670" + }, + { + "title": "githoney_contract.badges_policy.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "nonce", + "schema": { + "$ref": "#/definitions/Int" + } + } + ], + "compiledCode": "f143b98f13d5b12b", + "hash": "87d6bd0b40f204d49803dad8e0d70611918d22354125c8f226a42670" + }, + { + "title": "githoney_contract.githoney.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/types~1GithoneyDatum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1GithoneyContractRedeemers" + } + }, + "parameters": [ + { + "title": "settings_policy_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "f1a9d1de401b5004", + "hash": "7577273f99e7f3c9211d6342338d6316afc91689333c4d5099e7b2d1" + }, + { + "title": "githoney_contract.githoney.mint", + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1Redeemer" + } + }, + "parameters": [ + { + "title": "settings_policy_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "f1f665b7b5ec44d6", + "hash": "7577273f99e7f3c9211d6342338d6316afc91689333c4d5099e7b2d1" + }, + { + "title": "githoney_contract.githoney.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "settings_policy_id", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + } + ], + "compiledCode": "8dc98413cbd1b43f", + "hash": "7577273f99e7f3c9211d6342338d6316afc91689333c4d5099e7b2d1" + }, + { + "title": "githoney_contract.settings.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/types~1SettingsDatum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/types~1SettingsRedeemers" + } + }, + "compiledCode": "5eb78784a02ee1a0", + "hash": "049f1a09fb535089fdd9df98b4b0975d1081f9afc2d6f59ad2f9c208" + }, + { + "title": "githoney_contract.settings.else", + "redeemer": { + "schema": {} + }, + "compiledCode": "9813b3c286674b27", + "hash": "049f1a09fb535089fdd9df98b4b0975d1081f9afc2d6f59ad2f9c208" + }, + { + "title": "githoney_contract.settings_minting.mint", + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1Redeemer" + } + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "settings_script_addr", + "schema": { + "$ref": "#/definitions/cardano~1address~1Address" + } + } + ], + "compiledCode": "6396e66bd8b6ac88", + "hash": "15721f473d73adf918aa7541871739c2e8df0a60abe023411cf9f1b0" + }, + { + "title": "githoney_contract.settings_minting.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/cardano~1transaction~1OutputReference" + } + }, + { + "title": "settings_script_addr", + "schema": { + "$ref": "#/definitions/cardano~1address~1Address" + } + } + ], + "compiledCode": "fa2dba3b69a8d00c", + "hash": "15721f473d73adf918aa7541871739c2e8df0a60abe023411cf9f1b0" + } + ], + "definitions": { + "Bool": { + "title": "Bool", + "anyOf": [ + { + "title": "False", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "True", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "ByteArray": { + "title": "ByteArray", + "dataType": "bytes" + }, + "Data": { + "title": "Data", + "description": "Any Plutus data." + }, + "Int": { + "dataType": "integer" + }, + "Option$cardano/address/Address": { + "title": "Option", + "anyOf": [ + { + "title": "Some", + "description": "An optional value.", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/cardano~1address~1Address" + } + ] + }, + { + "title": "None", + "description": "Nothing.", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "Option$cardano/address/StakeCredential": { + "title": "Option", + "anyOf": [ + { + "title": "Some", + "description": "An optional value.", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/cardano~1address~1StakeCredential" + } + ] + }, + { + "title": "None", + "description": "Nothing.", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "Pairs$cardano/assets/AssetName_Int": { + "title": "Pairs", + "dataType": "map", + "keys": { + "$ref": "#/definitions/cardano~1assets~1AssetName" + }, + "values": { + "$ref": "#/definitions/Int" + } + }, + "Pairs$cardano/assets/PolicyId_Pairs$cardano/assets/AssetName_Int": { + "title": "Pairs>", + "dataType": "map", + "keys": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + }, + "values": { + "$ref": "#/definitions/Pairs$cardano~1assets~1AssetName_Int" + } + }, + "aiken/crypto/DataHash": { + "title": "DataHash", + "dataType": "bytes" + }, + "aiken/crypto/ScriptHash": { + "title": "ScriptHash", + "dataType": "bytes" + }, + "aiken/crypto/VerificationKeyHash": { + "title": "VerificationKeyHash", + "dataType": "bytes" + }, + "cardano/address/Address": { + "title": "Address", + "description": "A Cardano `Address` typically holding one or two credential references.\n\n Note that legacy bootstrap addresses (a.k.a. 'Byron addresses') are\n completely excluded from Plutus contexts. Thus, from an on-chain\n perspective only exists addresses of type 00, 01, ..., 07 as detailed\n in [CIP-0019 :: Shelley Addresses](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0019/#shelley-addresses).", + "anyOf": [ + { + "title": "Address", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "payment_credential", + "$ref": "#/definitions/cardano~1address~1PaymentCredential" + }, + { + "title": "stake_credential", + "$ref": "#/definitions/Option$cardano~1address~1StakeCredential" + } + ] + } + ] + }, + "cardano/address/Credential": { + "title": "Credential", + "description": "A general structure for representing an on-chain `Credential`.\n\n Credentials are always one of two kinds: a direct public/private key\n pair, or a script (native or Plutus).", + "anyOf": [ + { + "title": "VerificationKey", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + } + ] + }, + { + "title": "Script", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1ScriptHash" + } + ] + } + ] + }, + "cardano/address/PaymentCredential": { + "title": "PaymentCredential", + "description": "A general structure for representing an on-chain `Credential`.\n\n Credentials are always one of two kinds: a direct public/private key\n pair, or a script (native or Plutus).", + "anyOf": [ + { + "title": "VerificationKey", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + } + ] + }, + { + "title": "Script", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1ScriptHash" + } + ] + } + ] + }, + "cardano/address/StakeCredential": { + "title": "StakeCredential", + "description": "Represent a type of object that can be represented either inline (by hash)\n or via a reference (i.e. a pointer to an on-chain location).\n\n This is mainly use for capturing pointers to a stake credential\n registration certificate in the case of so-called pointer addresses.", + "anyOf": [ + { + "title": "Inline", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/cardano~1address~1Credential" + } + ] + }, + { + "title": "Pointer", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "title": "slot_number", + "$ref": "#/definitions/Int" + }, + { + "title": "transaction_index", + "$ref": "#/definitions/Int" + }, + { + "title": "certificate_index", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "cardano/assets/AssetName": { + "title": "AssetName", + "dataType": "bytes" + }, + "cardano/assets/PolicyId": { + "title": "PolicyId", + "dataType": "bytes" + }, + "cardano/transaction/Datum": { + "title": "Datum", + "description": "An output `Datum`.", + "anyOf": [ + { + "title": "NoDatum", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "DatumHash", + "description": "A datum referenced by its hash digest.", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1DataHash" + } + ] + }, + { + "title": "InlineDatum", + "description": "A datum completely inlined in the output.", + "dataType": "constructor", + "index": 2, + "fields": [ + { + "$ref": "#/definitions/Data" + } + ] + } + ] + }, + "cardano/transaction/OutputReference": { + "title": "OutputReference", + "description": "An `OutputReference` is a unique reference to an output on-chain. The `output_index`\n corresponds to the position in the output list of the transaction (identified by its id)\n that produced that output", + "anyOf": [ + { + "title": "OutputReference", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "transaction_id", + "$ref": "#/definitions/ByteArray" + }, + { + "title": "output_index", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "cardano/transaction/Redeemer": { + "title": "Redeemer", + "description": "Any Plutus data." + }, + "types/GithoneyContractRedeemers": { + "title": "GithoneyContractRedeemers", + "anyOf": [ + { + "title": "AddRewards", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "Assign", + "dataType": "constructor", + "index": 1, + "fields": [] + }, + { + "title": "Merge", + "dataType": "constructor", + "index": 2, + "fields": [] + }, + { + "title": "Close", + "dataType": "constructor", + "index": 3, + "fields": [] + }, + { + "title": "Claim", + "dataType": "constructor", + "index": 4, + "fields": [] + } + ] + }, + "types/GithoneyDatum": { + "title": "GithoneyDatum", + "anyOf": [ + { + "title": "GithoneyDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "admin_payment_credential", + "$ref": "#/definitions/cardano~1address~1Credential" + }, + { + "title": "maintainer_address", + "$ref": "#/definitions/cardano~1address~1Address" + }, + { + "title": "contributor_address", + "$ref": "#/definitions/Option$cardano~1address~1Address" + }, + { + "title": "bounty_reward_fee", + "$ref": "#/definitions/Int" + }, + { + "title": "deadline", + "$ref": "#/definitions/Int" + }, + { + "title": "merged", + "$ref": "#/definitions/Bool" + }, + { + "title": "initial_value", + "$ref": "#/definitions/Pairs$cardano~1assets~1PolicyId_Pairs$cardano~1assets~1AssetName_Int" + } + ] + } + ] + }, + "types/SettingsDatum": { + "title": "SettingsDatum", + "anyOf": [ + { + "title": "SettingsDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "githoney_address", + "$ref": "#/definitions/cardano~1address~1Address" + }, + { + "title": "bounty_creation_fee", + "$ref": "#/definitions/Int" + }, + { + "title": "bounty_reward_fee", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "types/SettingsRedeemers": { + "title": "SettingsRedeemers", + "anyOf": [ + { + "title": "UpdateSettings", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "CloseSettings", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + } + } +} diff --git a/crates/CIP-57/src/blueprint.rs b/crates/CIP-57/src/blueprint.rs deleted file mode 100644 index 09b4147c..00000000 --- a/crates/CIP-57/src/blueprint.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! This module defines the structures for a Blueprint, including its preamble, validators, and definitions. - -use serde::{Deserialize, Serialize}; -use serde_json::Number; -use std::collections::BTreeMap; - -/// Represents a blueprint containing preamble, validators, and optional definitions. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Blueprint { - pub preamble: Preamble, - pub validators: Vec, - pub definitions: Option, -} - -/// Represents the preamble of a blueprint, including metadata such as title, description, version, and compiler information. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Preamble { - pub title: String, - pub description: Option, - pub version: String, - pub plutus_version: String, - pub compiler: Option, - pub license: Option, -} - -/// Represents the compiler information in the preamble. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Compiler { - pub name: String, - pub version: Option, -} - -/// Represents a validator in the blueprint, including its title, description, compiled code, hash, datum, redeemer, and parameters. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Validator { - pub title: String, - pub description: Option, - pub compiled_code: Option, - pub hash: Option, - pub datum: Option, - pub redeemer: Option, - pub parameters: Option>, -} - -/// Represents an argument in a validator, including its title, description, purpose, and schema reference. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Argument { - pub title: Option, - pub description: Option, - pub purpose: Option, - pub schema: Reference, -} - -/// Represents a purpose array which can be either a single purpose or an array of purposes. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(untagged)] -pub enum PurposeArray { - Single(Purpose), - Array(Vec), -} - -/// Represents the purpose of an argument, which can be spend, mint, withdraw, or publish. -#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum Purpose { - Spend, - Mint, - Withdraw, - Publish, -} - -/// Represents a reference to a schema. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Reference { - #[serde(rename = "$ref")] - pub reference: Option, -} - -/// Represents a parameter in a validator, including its title, description, purpose, and schema reference. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Parameter { - pub title: Option, - pub description: Option, - pub purpose: Option, - pub schema: Reference, -} - -/// Represents the definitions in a blueprint, which is a map of definition names to their corresponding definitions. -#[derive(Debug, Default, Deserialize, Serialize, Clone)] -pub struct Definitions { - #[serde(flatten, default)] - pub inner: BTreeMap, -} - -/// Represents a definition in the blueprint, including its title, description, data type, any_of schemas, items, keys, and values. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Definition { - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub data_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub any_of: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub items: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub keys: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub values: Option, -} - -/// Represents an array of references which can be either a single reference or an array of references. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(untagged)] -pub enum ReferencesArray { - Single(Reference), - Array(Vec), -} - -/// Represents a schema in a definition, including its title, description, data type, index, and fields. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Schema { - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - pub data_type: DataType, - pub index: Number, - pub fields: Vec, -} - -/// Represents the data type of a schema, which can be integer, bytes, list, map, or constructor. -#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum DataType { - Integer, - Bytes, - List, - Map, - Constructor, -} - -/// Represents a field in a schema, including its title and reference. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Field { - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(rename = "$ref")] - pub reference: String, -} diff --git a/crates/CIP-57/src/lib.rs b/crates/CIP-57/src/lib.rs index 1fe7651a..0e368fab 100644 --- a/crates/CIP-57/src/lib.rs +++ b/crates/CIP-57/src/lib.rs @@ -1,482 +1,180 @@ -//! This module provides functions to work with blueprints and schemas, including -//! parsing JSON, reading from files, and JSON-to-JSON conversion helpers. +//! This module defines the structures for a Blueprint, including its preamble, validators, and definitions. -use anyhow::Result; -use schema::TypeName; -use serde_json; +use serde::{Deserialize, Serialize}; +use serde_json::Number; +use std::collections::BTreeMap; use std::fs; -use tx3_lang::ir::{Expression, StructExpr}; +use std::path::PathBuf; + +/// Represents a blueprint containing preamble, validators, and optional definitions. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Blueprint { + pub preamble: Preamble, + pub validators: Vec, + pub definitions: Option, +} -pub mod blueprint; -pub mod schema; -// Templates are intentionally removed for a pure JSON-to-JSON crate. +/// Represents the preamble of a blueprint, including metadata such as title, description, version, and compiler information. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Preamble { + pub title: String, + pub description: Option, + pub version: String, + pub plutus_version: String, + pub compiler: Option, + pub license: Option, +} -pub struct Codegen {} +/// Represents the compiler information in the preamble. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Compiler { + pub name: String, + pub version: Option, +} -impl Codegen { - pub fn new() -> Codegen { - Codegen {} - } +/// Represents a validator in the blueprint, including its title, description, compiled code, hash, datum, redeemer, and parameters. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Validator { + pub title: String, + pub description: Option, + pub compiled_code: Option, + pub hash: Option, + pub datum: Option, + pub redeemer: Option, + pub parameters: Option>, +} - fn get_schema_name(&self, key: String) -> String { - // Keep a simple normalization without external deps (heck) - let normalized = key - .replace("#/definitions/", "") - .replace("~1", " ") - .replace("/", " ") - .replace("_", " ") - .replace("$", " "); - // Basic PascalCase conversion - normalized - .split_whitespace() - .map(|w| { - let mut chars = w.chars(); - match chars.next() { - Some(first) => first.to_uppercase().collect::() + chars.as_str(), - None => String::new(), - } - }) - .collect::>() - .join("") - } +/// Represents an argument in a validator, including its title, description, purpose, and schema reference. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Argument { + pub title: Option, + pub description: Option, + pub purpose: Option, + pub schema: Reference, +} - fn parse_bytes_string(s: &str) -> Vec { - if let Some(hex) = s.strip_prefix("0x") { - return hex::decode(hex).unwrap_or_default(); - } - s.as_bytes().to_vec() - } +/// Represents a purpose array which can be either a single purpose or an array of purposes. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum PurposeArray { + Single(Purpose), + Array(Vec), +} - /// Parses a JSON string into a `Blueprint`. - /// - /// # Arguments - /// - /// * `json` - The JSON data from a `plutus.json` file - /// - /// # Returns - /// - /// A `Blueprint` instance or an error. - pub fn get_blueprint_from_json(&self, json: String) -> Result { - let bp = serde_json::from_str(&json)?; - Ok(bp) - } +/// Represents the purpose of an argument, which can be spend, mint, withdraw, or publish. +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum Purpose { + Spend, + Mint, + Withdraw, + Publish, +} - /// Reads a JSON file from a specified path and parses it into a `Blueprint`. - /// - /// # Arguments - /// - /// * `path` - The `plutus.json` file path in the filesystem - /// - /// # Returns - /// - /// A `Blueprint` instance or an error. - pub fn get_blueprint_from_path(&self, path: String) -> Result { - let json = fs::read_to_string(path)?; - self.get_blueprint_from_json(json) - } +/// Represents a reference to a schema. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Reference { + #[serde(rename = "$ref")] + pub reference: Option, +} - /// Obtains the list of schemas from a given `Blueprint`. - /// - /// # Arguments - /// - /// * `blueprint` - A `Blueprint` from which to obtain the schemas. - /// - /// # Returns - /// - /// A vector of `Schema` from the blueprint. - pub fn get_schemas_from_blueprint( - &self, - blueprint: blueprint::Blueprint, - ) -> Vec { - let mut schemas: Vec = vec![]; - if blueprint.definitions.is_some() { - for definition in blueprint.definitions.unwrap().inner.iter() { - let definition_name = self.get_schema_name(definition.0.clone()); - let definition_json = serde_json::to_string(&definition.1).unwrap(); - if definition.1.data_type.is_some() { - match definition.1.data_type.unwrap() { - blueprint::DataType::Integer => { - schemas.push(schema::Schema::new_integer( - definition_name.clone(), - definition_json.clone(), - )); - } - blueprint::DataType::Bytes => { - schemas.push(schema::Schema::new_bytes( - definition_name.clone(), - definition_json.clone(), - )); - } - blueprint::DataType::List => { - if definition.1.items.is_some() { - match definition.1.items.as_ref().unwrap() { - blueprint::ReferencesArray::Single(reference) => { - if reference.reference.is_some() { - schemas.push(schema::Schema::new_list( - definition_name.clone(), - schema::Reference { - name: None, - schema_name: self.get_schema_name( - reference - .reference - .as_ref() - .unwrap() - .clone(), - ), - }, - definition_json.clone(), - )); - } - } - blueprint::ReferencesArray::Array(references) => { - let mut properties: Vec = vec![]; - for reference in references { - if reference.reference.is_some() { - properties.push(schema::Reference { - name: None, - schema_name: self.get_schema_name( - reference - .reference - .as_ref() - .unwrap() - .clone(), - ), - }); - } - } - schemas.push(schema::Schema::new_tuple( - definition_name.clone(), - properties, - definition_json.clone(), - )); - } - } - } - } - _ => {} - } - } - if definition.1.title.is_some() { - if definition.1.title.as_ref().unwrap() == "Data" && definition_name == "Data" { - schemas.push(schema::Schema::new_anydata(definition_json.clone())); - } - } - if definition.1.any_of.is_some() { - let mut internal_schemas: Vec = vec![]; - for (index, parameter) in - definition.1.any_of.as_ref().unwrap().iter().enumerate() - { - match parameter.data_type { - blueprint::DataType::Constructor => { - let schema_name = format!( - "{}{}", - definition_name, - parameter.title.clone().unwrap_or((index + 1).to_string()) - ); - let mut properties: Vec = vec![]; - for property in ¶meter.fields { - let mut schema_name = - self.get_schema_name(property.reference.clone()); - if schema_name == "Data" { - schema_name = "AnyData".to_string(); - } - properties.push(schema::Reference { - name: property.title.clone(), - schema_name, - }); - } - let schema: schema::Schema; - if properties.len().gt(&0) || parameter.title.is_none() { - if properties.iter().any(|p| p.name.is_none()) { - schema = schema::Schema::new_tuple( - schema_name, - properties, - definition_json.clone(), - ); - } else { - schema = schema::Schema::new_object( - schema_name, - properties, - definition_json.clone(), - ); - } - } else { - schema = schema::Schema::new_literal( - schema_name, - parameter.title.clone().unwrap(), - definition_json.clone(), - ); - } - internal_schemas.push(schema); - } - _ => {} - } - } - if internal_schemas.len().eq(&1) { - let mut schema = internal_schemas.first().unwrap().clone(); - schema.name = definition_name.clone(); - schemas.push(schema); - } - if internal_schemas.len().gt(&1) { - if internal_schemas.len().eq(&2) - && internal_schemas - .iter() - .any(|s| s.type_name.eq(&TypeName::Literal)) - && !internal_schemas - .iter() - .all(|s| s.type_name.eq(&TypeName::Literal)) - { - let reference = internal_schemas - .iter() - .find(|s| s.type_name.ne(&TypeName::Literal)); - schemas.push(reference.unwrap().clone()); - schemas.push(schema::Schema::new_nullable( - definition_name.clone(), - reference.unwrap().name.clone(), - definition_json.clone(), - )); - } else { - for schema in &internal_schemas { - schemas.push(schema.clone()); - } - schemas.push(schema::Schema::new_enum( - definition_name.clone(), - &internal_schemas, - definition_json.clone(), - )); - } - } - } - } - } +/// Represents a parameter in a validator, including its title, description, purpose, and schema reference. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Parameter { + pub title: Option, + pub description: Option, + pub purpose: Option, + pub schema: Reference, +} - schemas - } +/// Represents the definitions in a blueprint, which is a map of definition names to their corresponding definitions. +#[derive(Debug, Default, Deserialize, Serialize, Clone)] +pub struct Definitions { + #[serde(flatten, default)] + pub inner: BTreeMap, +} - /// Obtains the list of validators from a given `Blueprint`. - /// - /// # Arguments - /// - /// * `blueprint` - A `Blueprint` from which to obtain the validators. - /// - /// # Returns - /// - /// A vector of `Validator` from the blueprint. - pub fn get_validators_from_blueprint( - &self, - blueprint: blueprint::Blueprint, - ) -> Vec { - let mut validators: Vec = vec![]; - for validator in blueprint.validators.iter() { - let mut datum: Option = None; - if validator.datum.is_some() - && validator.datum.as_ref().unwrap().schema.reference.is_some() - { - datum = Some(schema::Reference { - name: validator.datum.as_ref().unwrap().title.clone(), - schema_name: self.get_schema_name( - validator - .datum - .as_ref() - .unwrap() - .schema - .reference - .as_ref() - .unwrap() - .clone(), - ), - }); - } - let mut redeemer: Option = None; - if validator.redeemer.is_some() - && validator - .redeemer - .as_ref() - .unwrap() - .schema - .reference - .is_some() - { - redeemer = Some(schema::Reference { - name: validator.redeemer.as_ref().unwrap().title.clone(), - schema_name: self.get_schema_name( - validator - .redeemer - .as_ref() - .unwrap() - .schema - .reference - .as_ref() - .unwrap() - .clone(), - ), - }); - } - let mut parameters: Vec = vec![]; - if let Some(p) = &validator.parameters { - for parameter in p { - if parameter.schema.reference.is_some() { - parameters.push(schema::Reference { - name: parameter.title.clone(), - schema_name: self.get_schema_name( - parameter.schema.reference.as_ref().unwrap().clone(), - ), - }) - } - } - } - validators.push(schema::Validator { - name: validator.title.clone(), - datum: datum, - redeemer: redeemer, - parameters: parameters, - }); - } - validators - } +/// Represents a definition in the blueprint, including its title, description, data type, any_of schemas, items, keys, and values. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Definition { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub data_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub any_of: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub items: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub keys: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub values: Option, +} + +/// Represents an array of references which can be either a single reference or an array of references. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(untagged)] +pub enum ReferencesArray { + Single(Reference), + Array(Vec), +} + +/// Represents a schema in a definition, including its title, description, data type, index, and fields. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Schema { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub data_type: DataType, + pub index: Number, + pub fields: Vec, +} - /// Converts a `Blueprint` into a JSON value containing extracted schemas and validators. +/// Represents the data type of a schema, which can be integer, bytes, list, map, or constructor. +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum DataType { + Integer, + Bytes, + List, + Map, + Constructor, +} +/// Represents a field in a schema, including its title and reference. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Field { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(rename = "$ref")] + pub reference: String, +} - /// Convert a JSON value according to a schema name into a tx3 IR Expression. - /// This expects `value` to match the referenced schema (by name) present in the blueprint definitions. - pub fn convert_value_by_schema_name( - &self, - blueprint: &blueprint::Blueprint, - schema_name: &str, - value: &serde_json::Value, - ) -> Result { - // Build a quick lookup of definitions by normalized schema name - let mut defs = std::collections::BTreeMap::new(); - if let Some(all) = &blueprint.definitions { - for (raw_name, def) in &all.inner { - let name = self.get_schema_name(raw_name.clone()); - defs.insert(name, def); - } - } +#[cfg(test)] +mod tests { + use super::*; - let def = defs - .get(schema_name) - .ok_or_else(|| anyhow::anyhow!("unknown schema: {}", schema_name))?; + #[test] + fn deserialize_plutus_json_into_blueprint() { + let manifest = env!("CARGO_MANIFEST_DIR"); + let path = PathBuf::from(manifest).join("examples").join("plutus.json"); - // Primitive types - if let Some(dt) = def.data_type { - match dt { - blueprint::DataType::Integer => { - let num = match value { - serde_json::Value::Number(n) => n.as_i64().unwrap_or(0) as i128, - serde_json::Value::String(s) => s.parse::().unwrap_or(0), - _ => 0, - }; - return Ok(Expression::Number(num)); - } - blueprint::DataType::Bytes => { - let bytes = match value { - serde_json::Value::String(s) => Self::parse_bytes_string(s), - serde_json::Value::Array(arr) => arr - .iter() - .filter_map(|v| v.as_u64().map(|x| x as u8)) - .collect(), - _ => Vec::new(), - }; - return Ok(Expression::Bytes(bytes)); - } - blueprint::DataType::List => { - // For lists, expect a single reference in items (or tuple in Array case already handled elsewhere) - if let Some(items) = &def.items { - match items { - blueprint::ReferencesArray::Single(r) => { - let inner_name = r - .reference - .as_ref() - .map(|s| self.get_schema_name(s.clone())) - .ok_or_else(|| anyhow::anyhow!("list items missing $ref"))?; - let list = match value { - serde_json::Value::Array(arr) => { - let mut out = Vec::new(); - for v in arr { - out.push(self.convert_value_by_schema_name( - blueprint, - &inner_name, - v, - )?); - } - out - } - _ => Vec::new(), - }; - return Ok(Expression::List(list)); - } - blueprint::ReferencesArray::Array(_) => { - // Treat as tuple - let arr = value.as_array().cloned().unwrap_or_default(); - let mut fields = Vec::new(); - if let blueprint::ReferencesArray::Array(refs) = items { - for (i, r) in refs.iter().enumerate() { - let inner_name = r - .reference - .as_ref() - .map(|s| self.get_schema_name(s.clone())) - .ok_or_else(|| { - anyhow::anyhow!("tuple item missing $ref") - })?; - let v = - arr.get(i).cloned().unwrap_or(serde_json::Value::Null); - fields.push(self.convert_value_by_schema_name( - blueprint, - &inner_name, - &v, - )?); - } - } - return Ok(Expression::Struct(StructExpr { - constructor: 0, - fields, - })); - } - } - } - } - _ => {} - } - } + let json = fs::read_to_string(&path) + .expect(&format!("failed to read example file: {}", path.display())); - // Constructors (anyOf with data_type = Constructor). We choose variant by matching expected shape or `index`. - if let Some(any_of) = &def.any_of { - // Strategy: if `value` is an object with fields, pick matching constructor by field count; else use first. - let chosen = any_of - .first() - .ok_or_else(|| anyhow::anyhow!("empty anyOf"))?; - let index = chosen.index.as_i64().unwrap_or(0) as usize; - let mut fields_expr = Vec::new(); - for f in &chosen.fields { - let ref_name = self.get_schema_name(f.reference.clone()); - // Try to fetch field by title from value object; fallback to Null - let field_json = match value { - serde_json::Value::Object(map) => { - if let Some(title) = &f.title { - map.get(title).cloned().unwrap_or(serde_json::Value::Null) - } else { - serde_json::Value::Null - } - } - _ => serde_json::Value::Null, - }; - fields_expr.push(self.convert_value_by_schema_name( - blueprint, - &ref_name, - &field_json, - )?); - } - return Ok(Expression::Struct(StructExpr { - constructor: index, - fields: fields_expr, - })); - } + let bp: Blueprint = serde_json::from_str(&json).expect("failed to deserialize blueprint"); - // Fallback - Ok(Expression::None) + assert!( + !bp.preamble.title.is_empty(), + "preamble.title should not be empty" + ); + assert!(!bp.validators.is_empty(), "expected at least one validator"); } - -// (no free functions) } diff --git a/crates/CIP-57/src/schema.rs b/crates/CIP-57/src/schema.rs deleted file mode 100644 index 8680e523..00000000 --- a/crates/CIP-57/src/schema.rs +++ /dev/null @@ -1,267 +0,0 @@ -//! This module defines the structures and implementations for schemas, including types, references, and validators. - -use serde::{Deserialize, Serialize}; -use std::str; - -/// Represents the different types a schema can have. -#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum TypeName { - AnyData, - Integer, - Bytes, - Literal, - Nullable, - Object, - Enum, - Tuple, - List, -} - -/// Represents a schema with a name, type, optional properties, and JSON representation. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Schema { - pub name: String, - pub type_name: TypeName, - pub properties: Option>, - pub json: String, -} - -/// Represents a reference to another schema, including an optional name and schema name. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Reference { - pub name: Option, - pub schema_name: String, -} - -/// Represents a validator with a name, optional datum and redeemer, and parameters. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Validator { - pub name: String, - pub datum: Option, - pub redeemer: Option, - pub parameters: Vec, -} - -impl Schema { - /// Creates a new any data schema. - /// - /// # Arguments - /// - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `AnyData`. - pub fn new_anydata(json: String) -> Self { - Self { - name: "AnyData".to_string(), - type_name: TypeName::AnyData, - properties: None, - json, - } - } - - /// Creates a new integer schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Integer`. - pub fn new_integer(name: String, json: String) -> Self { - Self { - name, - type_name: TypeName::Integer, - properties: None, - json, - } - } - - /// Creates a new bytes schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Bytes`. - pub fn new_bytes(name: String, json: String) -> Self { - Self { - name, - type_name: TypeName::Bytes, - properties: None, - json, - } - } - - /// Creates a new literal schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `value` - The literal value of the schema. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Literal`. - pub fn new_literal(name: String, value: String, json: String) -> Self { - Self { - name, - type_name: TypeName::Literal, - properties: Some(vec![Reference { - name: None, - schema_name: value, - }]), - json, - } - } - - /// Creates a new nullable schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `reference` - The reference schema name. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Nullable`. - pub fn new_nullable(name: String, reference: String, json: String) -> Self { - Self { - name, - type_name: TypeName::Nullable, - properties: Some(vec![Reference { - name: None, - schema_name: reference, - }]), - json, - } - } - - /// Creates a new object schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `properties` - The properties of the object schema. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Object`. - pub fn new_object(name: String, properties: Vec, json: String) -> Self { - Self { - name, - type_name: TypeName::Object, - properties: Some(properties), - json, - } - } - - /// Creates a new enum schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `schemas` - The schemas that make up the enum. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Enum`. - pub fn new_enum(name: String, schemas: &Vec, json: String) -> Self { - Self { - name, - type_name: TypeName::Enum, - properties: Some( - schemas - .iter() - .map(|s| Reference { - name: None, - schema_name: s.name.clone(), - }) - .collect(), - ), - json, - } - } - - /// Creates a new tuple schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `properties` - The properties of the tuple schema. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `Tuple`. - pub fn new_tuple(name: String, properties: Vec, json: String) -> Self { - Self { - name, - type_name: TypeName::Tuple, - properties: Some(properties), - json, - } - } - - /// Creates a new list schema. - /// - /// # Arguments - /// - /// * `name` - The name of the schema. - /// * `reference` - The reference schema. - /// * `json` - The JSON representation of the schema. - /// - /// # Returns - /// - /// A new `Schema` instance with type `List`. - pub fn new_list(name: String, reference: Reference, json: String) -> Self { - Self { - name, - type_name: TypeName::List, - properties: Some(vec![reference]), - json, - } - } -} - -impl str::FromStr for TypeName { - type Err = (); - - /// Converts a string to a `TypeName`. - /// - /// # Arguments - /// - /// * `input` - The string representation of the type name. - /// - /// # Returns - /// - /// A `Result` containing the `TypeName` or an error. - fn from_str(input: &str) -> Result { - match input { - "AnyData" => Ok(TypeName::AnyData), - "Integer" => Ok(TypeName::Integer), - "Bytes" => Ok(TypeName::Bytes), - "Literal" => Ok(TypeName::Literal), - "Nullable" => Ok(TypeName::Nullable), - "Object" => Ok(TypeName::Object), - "Enum" => Ok(TypeName::Enum), - "Tuple" => Ok(TypeName::Tuple), - "List" => Ok(TypeName::List), - _ => Err(()), - } - } -} From cec9b3149bcb4d1feddda46f959d0b3fc7ce6734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Thu, 4 Dec 2025 14:10:34 -0300 Subject: [PATCH 3/3] chore: renaming, removing unnecessary deps & fixing warnings --- Cargo.lock | 3 +-- Cargo.toml | 8 ++++---- crates/{CIP-57 => cip-57}/Cargo.toml | 3 +-- crates/{CIP-57 => cip-57}/examples/plutus.json | 0 crates/{CIP-57 => cip-57}/src/lib.rs | 9 ++++----- 5 files changed, 10 insertions(+), 13 deletions(-) rename crates/{CIP-57 => cip-57}/Cargo.toml (93%) rename crates/{CIP-57 => cip-57}/examples/plutus.json (100%) rename crates/{CIP-57 => cip-57}/src/lib.rs (96%) diff --git a/Cargo.lock b/Cargo.lock index 3a334efe..d7c9129a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,10 +314,9 @@ dependencies = [ ] [[package]] -name = "cip57" +name = "cip-57" version = "0.13.0" dependencies = [ - "anyhow", "serde", "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 6cecbe46..2e313c3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [workspace] resolver = "2" members = [ - "crates/tx3-cardano", - "crates/tx3-lang", - "crates/tx3-resolver", - "crates/CIP-57", + "crates/tx3-cardano", + "crates/tx3-lang", + "crates/tx3-resolver", + "crates/cip-57", ] [workspace.package] diff --git a/crates/CIP-57/Cargo.toml b/crates/cip-57/Cargo.toml similarity index 93% rename from crates/CIP-57/Cargo.toml rename to crates/cip-57/Cargo.toml index e7345fd9..d70aa572 100644 --- a/crates/CIP-57/Cargo.toml +++ b/crates/cip-57/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "cip57" +name = "cip-57" description = "CIP-57 compatibility (JSON parsing and serialization)" publish.workspace = true authors.workspace = true @@ -16,4 +16,3 @@ readme.workspace = true [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" -anyhow = "1" diff --git a/crates/CIP-57/examples/plutus.json b/crates/cip-57/examples/plutus.json similarity index 100% rename from crates/CIP-57/examples/plutus.json rename to crates/cip-57/examples/plutus.json diff --git a/crates/CIP-57/src/lib.rs b/crates/cip-57/src/lib.rs similarity index 96% rename from crates/CIP-57/src/lib.rs rename to crates/cip-57/src/lib.rs index 0e368fab..b1a4d97d 100644 --- a/crates/CIP-57/src/lib.rs +++ b/crates/cip-57/src/lib.rs @@ -3,9 +3,6 @@ use serde::{Deserialize, Serialize}; use serde_json::Number; use std::collections::BTreeMap; -use std::fs; -use std::path::PathBuf; - /// Represents a blueprint containing preamble, validators, and optional definitions. #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Blueprint { @@ -160,14 +157,16 @@ pub struct Field { #[cfg(test)] mod tests { use super::*; + use std::fs; + use std::path::PathBuf; #[test] fn deserialize_plutus_json_into_blueprint() { let manifest = env!("CARGO_MANIFEST_DIR"); let path = PathBuf::from(manifest).join("examples").join("plutus.json"); - let json = fs::read_to_string(&path) - .expect(&format!("failed to read example file: {}", path.display())); + let failure_msg = format!("failed to read example file: {}", path.display()); + let json = fs::read_to_string(&path).expect(&failure_msg); let bp: Blueprint = serde_json::from_str(&json).expect("failed to deserialize blueprint");