From be2557f7dd250a945b9a9955a3793a3ce0f3b73d Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 9 Jul 2024 15:58:11 -0400 Subject: [PATCH 1/4] feat: initial work for contract invoke json schema --- cmd/crates/soroban-spec-tools/src/lib.rs | 1 + cmd/crates/soroban-spec-tools/src/schema.rs | 189 ++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 cmd/crates/soroban-spec-tools/src/schema.rs 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..6fdcc1af7 --- /dev/null +++ b/cmd/crates/soroban-spec-tools/src/schema.rs @@ -0,0 +1,189 @@ +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); + } + + Ok(json!({ + "type": "object", + "properties": properties, + "required": function.inputs.iter().map(|p| p.name.to_utf8_string_lossy()).collect::>() + })) + } + + fn struct_to_json_schema(&self, struct_: &xdr::ScSpecUdtStructV0) -> Result { + let mut properties = serde_json::Map::new(); + for field in struct_.fields.iter() { + let field_schema = self.type_to_json_schema(&field.type_)?; + properties.insert(field.name.to_utf8_string_lossy(), field_schema); + } + + Ok(json!({ + "type": "object", + "properties": properties, + "required": struct_.fields.iter().map(|f| f.name.to_utf8_string_lossy()).collect::>() + })) + } + + 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) => { + one_of.push(json!({ + "type": "string", + "enum": [void_case.name.to_utf8_string_lossy()] + })); + } + xdr::ScSpecUdtUnionCaseV0::TupleV0(tuple_case) => { + let mut properties = serde_json::Map::new(); + properties.insert(tuple_case.name.to_utf8_string_lossy(), json!({ + "type": "array", + "items": tuple_case.type_.iter().map(|t| self.type_to_json_schema(t).unwrap()).collect::>() + })); + one_of.push(json!({ + "type": "object", + "properties": properties, + "required": [tuple_case.name.to_utf8_string_lossy()] + })); + } + } + } + + Ok(json!({ "oneOf": one_of })) + } + + fn enum_to_json_schema(&self, enum_: &xdr::ScSpecUdtEnumV0) -> Result { + Ok(json!({ + "type": "integer", + "enum": enum_.cases.iter().map(|c| c.value).collect::>() + })) + } + + fn error_enum_to_json_schema( + &self, + error_enum: &xdr::ScSpecUdtErrorEnumV0, + ) -> Result { + Ok(json!({ + "type": "integer", + "enum": error_enum.cases.iter().map(|c| c.value).collect::>() + })) + } + + 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()); + } +} From 544ad16d74d669d5995dd5debf42b863b812bee1 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 16 Jul 2024 15:09:52 -0400 Subject: [PATCH 2/4] fix: remove unneeded dep --- Cargo.lock | 1 - cmd/crates/soroban-spec-tools/Cargo.toml | 2 +- cmd/crates/soroban-spec-tools/src/contract.rs | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) 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..48e710ffa 100644 --- a/cmd/crates/soroban-spec-tools/Cargo.toml +++ b/cmd/crates/soroban-spec-tools/Cargo.toml @@ -20,7 +20,7 @@ crate-type = ["rlib"] soroban-spec = { workspace = true } stellar-strkey = { workspace = true } stellar-xdr = { workspace = true, features = ["curr", "std", "serde"] } -soroban-env-host = { workspace = true } + 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 { From 19f335a6e77a46fec9fbf28e709d61af2e9dfae2 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 16 Jul 2024 16:06:50 -0400 Subject: [PATCH 3/4] feat: add docs --- cmd/crates/soroban-spec-tools/Cargo.toml | 2 +- cmd/crates/soroban-spec-tools/src/schema.rs | 71 ++++++++++++++++----- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/cmd/crates/soroban-spec-tools/Cargo.toml b/cmd/crates/soroban-spec-tools/Cargo.toml index 48e710ffa..3aaf06ed1 100644 --- a/cmd/crates/soroban-spec-tools/Cargo.toml +++ b/cmd/crates/soroban-spec-tools/Cargo.toml @@ -19,7 +19,7 @@ crate-type = ["rlib"] [dependencies] soroban-spec = { workspace = true } stellar-strkey = { workspace = true } -stellar-xdr = { workspace = true, features = ["curr", "std", "serde"] } +stellar-xdr = { workspace = true, features = ["curr", "std", "serde", "base64"] } serde_json = { workspace = true } diff --git a/cmd/crates/soroban-spec-tools/src/schema.rs b/cmd/crates/soroban-spec-tools/src/schema.rs index 6fdcc1af7..09a2c6bfc 100644 --- a/cmd/crates/soroban-spec-tools/src/schema.rs +++ b/cmd/crates/soroban-spec-tools/src/schema.rs @@ -51,25 +51,40 @@ impl Spec { properties.insert(param.name.to_utf8_string_lossy(), param_schema); } - Ok(json!({ + 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 field_schema = self.type_to_json_schema(&field.type_)?; + 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); } - Ok(json!({ + 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 { @@ -77,17 +92,25 @@ impl Spec { for case in union.cases.iter() { match case { xdr::ScSpecUdtUnionCaseV0::VoidV0(void_case) => { - one_of.push(json!({ + 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(); - properties.insert(tuple_case.name.to_utf8_string_lossy(), json!({ + 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, @@ -97,24 +120,42 @@ impl Spec { } } - Ok(json!({ "oneOf": one_of })) + 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 { - Ok(json!({ + 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 { - Ok(json!({ + 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 { @@ -186,4 +227,4 @@ mod tests { let json_schema = spec.to_json_schema().unwrap(); println!("{}", serde_json::to_string_pretty(&json_schema).unwrap()); } -} +} \ No newline at end of file From c5729737c7dfc122852a707d4e6d9311b9b7be74 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Tue, 16 Jul 2024 16:08:57 -0400 Subject: [PATCH 4/4] fix: fmt --- cmd/crates/soroban-spec-tools/src/schema.rs | 42 ++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/cmd/crates/soroban-spec-tools/src/schema.rs b/cmd/crates/soroban-spec-tools/src/schema.rs index 09a2c6bfc..6eb3c1b09 100644 --- a/cmd/crates/soroban-spec-tools/src/schema.rs +++ b/cmd/crates/soroban-spec-tools/src/schema.rs @@ -58,7 +58,10 @@ impl Spec { }); if !function.doc.is_empty() { - schema.as_object_mut().unwrap().insert("description".to_string(), json!(function.doc.to_utf8_string_lossy())); + schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(function.doc.to_utf8_string_lossy()), + ); } Ok(schema) @@ -69,7 +72,10 @@ impl Spec { 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())); + 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); } @@ -81,7 +87,10 @@ impl Spec { }); if !struct_.doc.is_empty() { - schema.as_object_mut().unwrap().insert("description".to_string(), json!(struct_.doc.to_utf8_string_lossy())); + schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(struct_.doc.to_utf8_string_lossy()), + ); } Ok(schema) @@ -97,7 +106,10 @@ impl Spec { "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())); + case_schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(void_case.doc.to_utf8_string_lossy()), + ); } one_of.push(case_schema); } @@ -108,7 +120,10 @@ impl Spec { "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())); + 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!({ @@ -123,7 +138,10 @@ impl Spec { 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())); + schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(union.doc.to_utf8_string_lossy()), + ); } Ok(schema) @@ -136,7 +154,10 @@ impl Spec { }); if !enum_.doc.is_empty() { - schema.as_object_mut().unwrap().insert("description".to_string(), json!(enum_.doc.to_utf8_string_lossy())); + schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(enum_.doc.to_utf8_string_lossy()), + ); } Ok(schema) @@ -152,7 +173,10 @@ impl Spec { }); if !error_enum.doc.is_empty() { - schema.as_object_mut().unwrap().insert("description".to_string(), json!(error_enum.doc.to_utf8_string_lossy())); + schema.as_object_mut().unwrap().insert( + "description".to_string(), + json!(error_enum.doc.to_utf8_string_lossy()), + ); } Ok(schema) @@ -227,4 +251,4 @@ mod tests { let json_schema = spec.to_json_schema().unwrap(); println!("{}", serde_json::to_string_pretty(&json_schema).unwrap()); } -} \ No newline at end of file +}