diff --git a/Cargo.lock b/Cargo.lock index 0b1841f80..ba877e97c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4923,7 +4923,6 @@ dependencies = [ "hex", "itertools 0.10.5", "serde_json", - "soroban-env-host", "soroban-spec", "stellar-strkey", "stellar-xdr", diff --git a/cmd/crates/soroban-spec-tools/Cargo.toml b/cmd/crates/soroban-spec-tools/Cargo.toml index 514b82924..3aaf06ed1 100644 --- a/cmd/crates/soroban-spec-tools/Cargo.toml +++ b/cmd/crates/soroban-spec-tools/Cargo.toml @@ -19,8 +19,8 @@ crate-type = ["rlib"] [dependencies] soroban-spec = { workspace = true } stellar-strkey = { workspace = true } -stellar-xdr = { workspace = true, features = ["curr", "std", "serde"] } -soroban-env-host = { workspace = true } +stellar-xdr = { workspace = true, features = ["curr", "std", "serde", "base64"] } + serde_json = { workspace = true } itertools = { workspace = true } diff --git a/cmd/crates/soroban-spec-tools/src/contract.rs b/cmd/crates/soroban-spec-tools/src/contract.rs index bca72f533..37f843bd3 100644 --- a/cmd/crates/soroban-spec-tools/src/contract.rs +++ b/cmd/crates/soroban-spec-tools/src/contract.rs @@ -4,10 +4,10 @@ use std::{ io::{self, Cursor}, }; -use soroban_env_host::xdr::{ - self, Limited, Limits, ReadXdr, ScEnvMetaEntry, ScMetaEntry, ScMetaV0, ScSpecEntry, - ScSpecFunctionV0, ScSpecUdtEnumV0, ScSpecUdtErrorEnumV0, ScSpecUdtStructV0, ScSpecUdtUnionV0, - StringM, WriteXdr, +use stellar_xdr::curr as xdr; +use xdr::{ + Limited, Limits, ReadXdr, ScEnvMetaEntry, ScMetaEntry, ScMetaV0, ScSpecEntry, ScSpecFunctionV0, + ScSpecUdtEnumV0, ScSpecUdtErrorEnumV0, ScSpecUdtStructV0, ScSpecUdtUnionV0, StringM, WriteXdr, }; pub struct Spec { diff --git a/cmd/crates/soroban-spec-tools/src/lib.rs b/cmd/crates/soroban-spec-tools/src/lib.rs index c227c3478..44d86762f 100644 --- a/cmd/crates/soroban-spec-tools/src/lib.rs +++ b/cmd/crates/soroban-spec-tools/src/lib.rs @@ -14,6 +14,7 @@ use stellar_xdr::curr::{ }; pub mod contract; +pub mod schema; pub mod utils; #[derive(thiserror::Error, Debug)] diff --git a/cmd/crates/soroban-spec-tools/src/schema.rs b/cmd/crates/soroban-spec-tools/src/schema.rs new file mode 100644 index 000000000..6eb3c1b09 --- /dev/null +++ b/cmd/crates/soroban-spec-tools/src/schema.rs @@ -0,0 +1,254 @@ +use serde_json::{json, Value}; +use stellar_xdr::curr::{self as xdr, ScSpecTypeDef as ScType}; + +use crate::{Error, Spec}; + +impl Spec { + pub fn to_json_schema(&self) -> Result { + let mut definitions = serde_json::Map::new(); + let mut properties = serde_json::Map::new(); + + if let Some(entries) = &self.0 { + for entry in entries { + match entry { + xdr::ScSpecEntry::FunctionV0(function) => { + let function_schema = self.function_to_json_schema(function)?; + properties.insert(function.name.to_utf8_string_lossy(), function_schema); + } + xdr::ScSpecEntry::UdtStructV0(struct_) => { + let struct_schema = self.struct_to_json_schema(struct_)?; + definitions.insert(struct_.name.to_utf8_string_lossy(), struct_schema); + } + xdr::ScSpecEntry::UdtUnionV0(union) => { + let union_schema = self.union_to_json_schema(union)?; + definitions.insert(union.name.to_utf8_string_lossy(), union_schema); + } + xdr::ScSpecEntry::UdtEnumV0(enum_) => { + let enum_schema = self.enum_to_json_schema(enum_)?; + definitions.insert(enum_.name.to_utf8_string_lossy(), enum_schema); + } + xdr::ScSpecEntry::UdtErrorEnumV0(error_enum) => { + let error_enum_schema = self.error_enum_to_json_schema(error_enum)?; + definitions + .insert(error_enum.name.to_utf8_string_lossy(), error_enum_schema); + } + } + } + } + + Ok(json!({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": properties, + "definitions": definitions + })) + } + + fn function_to_json_schema(&self, function: &xdr::ScSpecFunctionV0) -> Result { + let mut properties = serde_json::Map::new(); + for param in function.inputs.iter() { + let param_schema = self.type_to_json_schema(¶m.type_)?; + properties.insert(param.name.to_utf8_string_lossy(), param_schema); + } + + let mut schema = json!({ + "type": "object", + "properties": properties, + "required": function.inputs.iter().map(|p| p.name.to_utf8_string_lossy()).collect::>() + }); + + if !function.doc.is_empty() { + schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(function.doc.to_utf8_string_lossy()), + ); + } + + Ok(schema) + } + + fn struct_to_json_schema(&self, struct_: &xdr::ScSpecUdtStructV0) -> Result { + let mut properties = serde_json::Map::new(); + for field in struct_.fields.iter() { + let mut field_schema = self.type_to_json_schema(&field.type_)?; + if !field.doc.is_empty() { + field_schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(field.doc.to_utf8_string_lossy()), + ); + } + properties.insert(field.name.to_utf8_string_lossy(), field_schema); + } + + let mut schema = json!({ + "type": "object", + "properties": properties, + "required": struct_.fields.iter().map(|f| f.name.to_utf8_string_lossy()).collect::>() + }); + + if !struct_.doc.is_empty() { + schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(struct_.doc.to_utf8_string_lossy()), + ); + } + + Ok(schema) + } + + fn union_to_json_schema(&self, union: &xdr::ScSpecUdtUnionV0) -> Result { + let mut one_of = Vec::new(); + for case in union.cases.iter() { + match case { + xdr::ScSpecUdtUnionCaseV0::VoidV0(void_case) => { + let mut case_schema = json!({ + "type": "string", + "enum": [void_case.name.to_utf8_string_lossy()] + }); + if !void_case.doc.is_empty() { + case_schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(void_case.doc.to_utf8_string_lossy()), + ); + } + one_of.push(case_schema); + } + xdr::ScSpecUdtUnionCaseV0::TupleV0(tuple_case) => { + let mut properties = serde_json::Map::new(); + let mut case_schema = json!({ + "type": "array", + "items": tuple_case.type_.iter().map(|t| self.type_to_json_schema(t).unwrap()).collect::>() + }); + if !tuple_case.doc.is_empty() { + case_schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(tuple_case.doc.to_utf8_string_lossy()), + ); + } + properties.insert(tuple_case.name.to_utf8_string_lossy(), case_schema); + one_of.push(json!({ + "type": "object", + "properties": properties, + "required": [tuple_case.name.to_utf8_string_lossy()] + })); + } + } + } + + let mut schema = json!({ "oneOf": one_of }); + + if !union.doc.is_empty() { + schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(union.doc.to_utf8_string_lossy()), + ); + } + + Ok(schema) + } + + fn enum_to_json_schema(&self, enum_: &xdr::ScSpecUdtEnumV0) -> Result { + let mut schema = json!({ + "type": "integer", + "enum": enum_.cases.iter().map(|c| c.value).collect::>() + }); + + if !enum_.doc.is_empty() { + schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(enum_.doc.to_utf8_string_lossy()), + ); + } + + Ok(schema) + } + + fn error_enum_to_json_schema( + &self, + error_enum: &xdr::ScSpecUdtErrorEnumV0, + ) -> Result { + let mut schema = json!({ + "type": "integer", + "enum": error_enum.cases.iter().map(|c| c.value).collect::>() + }); + + if !error_enum.doc.is_empty() { + schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(error_enum.doc.to_utf8_string_lossy()), + ); + } + + Ok(schema) + } + + fn type_to_json_schema(&self, type_: &ScType) -> Result { + Ok(match type_ { + ScType::Bool => json!({"type": "boolean"}), + ScType::Void => json!({"type": "null"}), + ScType::Error => { + json!({"type": "object", "properties": {"Error": {"type": "integer"}}}) + } + ScType::U32 | ScType::I32 | ScType::U64 | ScType::I64 => { + json!({"type": "integer"}) + } + ScType::U128 | ScType::I128 | ScType::U256 | ScType::I256 => { + json!({"type": "string"}) + } + ScType::Bytes | ScType::String | ScType::Symbol => { + json!({"type": "string"}) + } + ScType::Vec(vec_type) => json!({ + "type": "array", + "items": self.type_to_json_schema(&vec_type.element_type)? + }), + ScType::Map(map_type) => json!({ + "type": "object", + "additionalProperties": self.type_to_json_schema(&map_type.value_type)? + }), + ScType::Option(option_type) => json!({ + "oneOf": [ + {"type": "null"}, + self.type_to_json_schema(&option_type.value_type)? + ] + }), + ScType::Result(result_type) => json!({ + "oneOf": [ + {"type": "object", "properties": {"Ok": self.type_to_json_schema(&result_type.ok_type)?}}, + {"type": "object", "properties": {"Error": self.type_to_json_schema(&result_type.error_type)?}} + ] + }), + ScType::Tuple(tuple_type) => json!({ + "type": "array", + "items": tuple_type.value_types.iter().map(|t| self.type_to_json_schema(t).unwrap()).collect::>(), + "minItems": tuple_type.value_types.len(), + "maxItems": tuple_type.value_types.len() + }), + ScType::BytesN(bytes_n) => json!({ + "type": "string", + "pattern": format!("^[0-9a-fA-F]{{{}}}$", bytes_n.n * 2) + }), + ScType::Address => json!({"type": "string", "pattern": "^[GC][A-Z2-7]{55}$"}), + ScType::Timepoint | ScType::Duration => json!({"type": "integer"}), + ScType::Udt(udt_type) => { + json!({"$ref": format!("#/definitions/{}", udt_type.name.to_utf8_string_lossy())}) + } + ScType::Val => json!({}), // Allow any type for Val + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate() { + let wasm_bytes = include_bytes!( + "../../../../target/wasm32-unknown-unknown/test-wasms/test_hello_world.wasm" + ); + let spec = Spec::from_wasm(wasm_bytes).unwrap(); + let json_schema = spec.to_json_schema().unwrap(); + println!("{}", serde_json::to_string_pretty(&json_schema).unwrap()); + } +}