diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..75bb803 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = [ "codegen", "rpc-rs" ] diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 9a2cb2b..8e6aa2c 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "weld-codegen" -version = "0.3.0" +version = "0.3.1" edition = "2021" authors = [ "wasmcloud Team" ] license = "Apache-2.0" diff --git a/codegen/src/codegen_rust.rs b/codegen/src/codegen_rust.rs index 65dcbf2..a509da1 100644 --- a/codegen/src/codegen_rust.rs +++ b/codegen/src/codegen_rust.rs @@ -93,6 +93,20 @@ enum MethodArgFlags { ToString, } +/// Returns true if the type is a rust primitive +pub fn is_rust_primitive(id: &ShapeID) -> bool { + (id.namespace() == prelude_namespace_id() + && matches!( + id.shape_name().to_string().as_str(), + "Boolean" | "Byte" | "Short" | "Integer" | "Long" | "Float" | "Double" + )) + || (id.namespace() == wasmcloud_model_namespace() + && matches!( + id.shape_name().to_string().as_str(), + "U64" | "U32" | "U16" | "U8" | "I64" | "I32" | "I16" | "I8" | "F64" | "F32" + )) +} + impl<'model> CodeGen for RustCodeGen<'model> { fn output_language(&self) -> OutputLanguage { OutputLanguage::Rust @@ -185,7 +199,6 @@ impl<'model> CodeGen for RustCodeGen<'model> { Some(ns) => Some(NamespaceID::from_str(ns)?), None => None, }; - //self.protocol = model.wasmbus_ if let Some(ref ns) = self.namespace { if self.packages.get(&ns.to_string()).is_none() { print_warning(&format!( @@ -302,10 +315,12 @@ impl<'model> CodeGen for RustCodeGen<'model> { ShapeKind::Structure(strukt) => { self.declare_structure_shape(w, id.shape_name(), traits, strukt)?; } + ShapeKind::Union(strukt) => { + self.declare_union_shape(w, id.shape_name(), traits, strukt)?; + } ShapeKind::Operation(_) | ShapeKind::Resource(_) | ShapeKind::Service(_) - | ShapeKind::Union(_) | ShapeKind::Unresolved => {} } @@ -473,6 +488,8 @@ impl<'model> RustCodeGen<'model> { s.push('i'); s.push_str(&name[1..]); } + "F64" => s.push_str("f64"), + "F32" => s.push_str("f32"), _ => { if self.namespace.is_none() || self.namespace.as_ref().unwrap() != id.namespace() @@ -721,6 +738,40 @@ impl<'model> RustCodeGen<'model> { Ok(()) } + fn declare_union_shape( + &mut self, + w: &mut Writer, + id: &Identifier, + traits: &AppliedTraits, + strukt: &StructureOrUnion, + ) -> Result<()> { + let (fields, is_numbered) = get_sorted_fields(id, strukt)?; + if !is_numbered { + return Err(Error::Model(format!( + "union {} must have numbered fields", + id + ))); + } + self.apply_documentation_traits(w, id, traits); + w.write(b"#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]\n"); + println!("Union: {}:\n:{:#?}", id, strukt); + + w.write(b"pub enum "); + self.write_ident(w, id); + w.write(b" {\n"); + for member in fields.iter() { + self.apply_documentation_traits(w, member.id(), member.traits()); + let variant_name = self.to_type_name(&member.id().to_string()); + w.write(&format!( + "{}({}),\n", + variant_name, + self.type_string(Ty::Shape(member.target()))? + )); // TODO: Ty::Ref ? + } + w.write(b"}\n\n"); + Ok(()) + } + /// Declares the service as a rust Trait whose methods are the smithy service operations fn write_service_interface( &mut self, @@ -850,6 +901,7 @@ impl<'model> RustCodeGen<'model> { w.write(b" : MessageDispatch + "); self.write_ident(w, service.id); let proto = crate::model::wasmbus_proto(service.traits)?; + let has_cbor = proto.map(|pv| pv.has_cbor()).unwrap_or(false); w.write( br#"{ async fn dispatch( @@ -875,7 +927,7 @@ impl<'model> RustCodeGen<'model> { if let Some(op_input) = op.input() { let symbol = op_input.shape_name().to_string(); // let value : InputType = deserialize(...)?; - if proto.has_cbor() { + if has_cbor { w.write(&format!( r#" let value : {} = {}::common::decode(&message.arg, &decode_{}) @@ -916,7 +968,7 @@ impl<'model> RustCodeGen<'model> { if let Some(_op_output) = op.output() { // serialize result - if proto.has_cbor() { + if has_cbor { w.write(&format!( "let mut e = {}::cbor::vec_encoder();\n", &self.import_core @@ -965,6 +1017,7 @@ impl<'model> RustCodeGen<'model> { self.write_comment(w, CommentKind::Documentation, &doc); self.apply_documentation_traits(w, service.id, service.traits); let proto = crate::model::wasmbus_proto(service.traits)?; + let has_cbor = proto.map(|pv| pv.has_cbor()).unwrap_or(false); w.write(&format!( r#"/// client for sending {} messages #[derive(Debug)] @@ -1010,7 +1063,7 @@ impl<'model> RustCodeGen<'model> { let _arg_is_string = matches!(arg_flags, MethodArgFlags::ToString); w.write(b" {\n"); if let Some(_op_input) = op.input() { - if proto.has_cbor() { + if has_cbor { if _arg_is_string { w.write(b"let arg = arg.to_string();\n"); } @@ -1053,7 +1106,7 @@ impl<'model> RustCodeGen<'model> { w.write(b"\", arg: Cow::Borrowed(&buf)}, None).await?;\n"); if let Some(op_output) = op.output() { let symbol = op_output.shape_name().to_string(); - if proto.has_cbor() { + if has_cbor { w.write(&format!( r#" let value : {} = {}::common::decode(&resp, &decode_{}) diff --git a/codegen/src/decode_rust.rs b/codegen/src/decode_rust.rs index 1364ede..5100ca0 100644 --- a/codegen/src/decode_rust.rs +++ b/codegen/src/decode_rust.rs @@ -1,7 +1,7 @@ //! CBOR Decode functions use crate::{ - codegen_rust::{is_optional_type, RustCodeGen}, + codegen_rust::{is_optional_type, is_rust_primitive, RustCodeGen}, error::{Error, Result}, gen::CodeGen, model::{wasmcloud_model_namespace, Ty}, @@ -110,6 +110,8 @@ impl<'model> RustCodeGen<'model> { b"I32" => decode_integer().to_string(), b"I16" => decode_short().to_string(), b"I8" => decode_byte().to_string(), + b"F64" => decode_double().to_string(), + b"F32" => decode_float().to_string(), _ => { let mut s = String::new(); if self.namespace.is_none() @@ -160,7 +162,6 @@ impl<'model> RustCodeGen<'model> { Ok(stmt) } - #[allow(dead_code)] fn decode_shape_kind(&self, id: &ShapeID, kind: &ShapeKind) -> Result { let s = match kind { ShapeKind::Simple(simple) => match simple { @@ -230,21 +231,50 @@ impl<'model> RustCodeGen<'model> { ) } ShapeKind::Structure(strukt) => self.decode_struct(id, strukt)?, + ShapeKind::Union(union_) => self.decode_union(id, union_)?, ShapeKind::Operation(_) | ShapeKind::Resource(_) | ShapeKind::Service(_) | ShapeKind::Unresolved => String::new(), - - ShapeKind::Union(_) => { - unimplemented!(); - } }; Ok(s) } + fn decode_union(&self, id: &ShapeID, strukt: &StructureOrUnion) -> Result { + let (fields, _) = crate::model::get_sorted_fields(id.shape_name(), strukt)?; + let enum_name = id.shape_name(); + let mut s = format!( + r#" + // decoding union {} + let len = d.array()?.ok_or_else(||RpcError::Deser("decoding union '{}': indefinite array not supported".to_string()))?; + if len != 2 {{ return Err(RpcError::Deser("decoding union '{}': expected 2-array".to_string())); }} + match d.u16()? {{ + "#, + enum_name, enum_name, enum_name + ); + for field in fields.iter() { + let field_num = field.field_num().unwrap(); + let field_name = self.to_type_name(&field.id().to_string()); + let field_decoder = self.decode_shape_id(field.target())?; + s.push_str(&format!( + r#" + {} => {{ + let val = {}; + {}::{}(val) + }}, + "#, + &field_num, field_decoder, enum_name, field_name + )); + } + s.push_str(&format!(r#" + n => {{ return Err(RpcError::Deser(format!("invalid field number for union '{}':{{}}", n))); }}, + }} + "#, id)); + Ok(s) + } + /// write decode statements for a structure /// This always occurs inside a dedicated function for the struct type - #[allow(dead_code)] fn decode_struct(&self, id: &ShapeID, strukt: &StructureOrUnion) -> Result { let (fields, _is_numbered) = crate::model::get_sorted_fields(id.shape_name(), strukt)?; let mut s = String::new(); @@ -419,7 +449,6 @@ impl<'model> RustCodeGen<'model> { /// name of the function is encode_ where is the camel_case type name /// It is generated in the module that declared type S, so it can always /// be found by prefixing the function name with the module path. - #[allow(dead_code)] pub(crate) fn declare_shape_decoder( &self, w: &mut Writer, @@ -429,12 +458,12 @@ impl<'model> RustCodeGen<'model> { match kind { ShapeKind::Simple(_) | ShapeKind::Structure(_) + | ShapeKind::Union(_) | ShapeKind::Map(_) | ShapeKind::List(_) | ShapeKind::Set(_) => { let name = id.shape_name(); - let is_rust_copy = vec!["U8", "I8", "U16", "I16", "U32", "I32", "U64", "I64"] - .contains(&name.to_string().as_str()); + let is_rust_copy = is_rust_primitive(id); let mut s = format!( r#" // Decode {} from cbor input stream @@ -452,31 +481,10 @@ impl<'model> RustCodeGen<'model> { s.push_str(&body); s.push_str("};\n Ok(__result)\n}\n"); w.write(s.as_bytes()); - - /*** - if !is_rust_copy { - let cbor_decode_impl = format!( - r#" - impl {}::common::Decode for {} {{ - fn decode(d: &mut {}::cbor::Decoder) -> Result<{}, RpcError> {{ - decode_{}(d) - }} - }} - "#, - self.import_core, - name, - self.import_core, - name, - crate::strings::to_snake_case(&name.to_string()), - ); - w.write(&cbor_decode_impl); - } - ***/ } ShapeKind::Operation(_) | ShapeKind::Resource(_) | ShapeKind::Service(_) - | ShapeKind::Union(_) | ShapeKind::Unresolved => { /* write nothing */ } } Ok(()) diff --git a/codegen/src/encode_rust.rs b/codegen/src/encode_rust.rs index 0f21578..5eac5fe 100644 --- a/codegen/src/encode_rust.rs +++ b/codegen/src/encode_rust.rs @@ -10,7 +10,7 @@ // The encoder is written as a plain function "encode_" where S is the type name // (camel cased for the fn name), and scoped to the module where S is defined. use crate::{ - codegen_rust::{is_optional_type, RustCodeGen}, + codegen_rust::{is_optional_type, is_rust_primitive, RustCodeGen}, error::{Error, Result}, gen::CodeGen, model::wasmcloud_model_namespace, @@ -100,10 +100,10 @@ fn encode_unsigned_long(val: ValExpr) -> String { format!("e.u64({})?;\n", val.as_copy()) } fn encode_float(val: ValExpr) -> String { - format!("e.f32({})?;\n", val.as_str()) + format!("e.f32({})?;\n", val.as_copy()) } fn encode_double(val: ValExpr) -> String { - format!("e.f64({})?;\n", val.as_str()) + format!("e.f64({})?;\n", val.as_copy()) } fn encode_document(val: ValExpr) -> String { format!("e.bytes({})?;\n", val.as_ref()) @@ -156,6 +156,8 @@ impl<'model> RustCodeGen<'model> { b"I32" => encode_integer(val), b"I16" => encode_short(val), b"I8" => encode_byte(val), + b"F64" => encode_double(val), + b"F32" => encode_float(val), _ => { let mut s = String::new(); if self.namespace.is_none() @@ -264,16 +266,13 @@ impl<'model> RustCodeGen<'model> { val.as_str(), val.as_str() ); + s.push_str(&self.encode_shape_id( list.member().target(), ValExpr::Ref("item"), false, )?); - s.push_str( - r#" - } - "#, - ); + s.push('}'); s } ShapeKind::Set(set) => { @@ -302,18 +301,41 @@ impl<'model> RustCodeGen<'model> { empty_struct = is_empty_struct; s } + ShapeKind::Union(union_) => { + let (s, _) = self.encode_union(id, union_, val)?; + s + } ShapeKind::Operation(_) | ShapeKind::Resource(_) | ShapeKind::Service(_) | ShapeKind::Unresolved => String::new(), - - ShapeKind::Union(_) => { - unimplemented!(); - } }; Ok((s, empty_struct)) } + /// Generate string to encode union. + fn encode_union( + &self, + id: &ShapeID, + strukt: &StructureOrUnion, + val: ValExpr, + ) -> Result<(String, IsEmptyStruct)> { + let (fields, _) = crate::model::get_sorted_fields(id.shape_name(), strukt)?; + let mut s = String::new(); + s.push_str(&format!("// encoding union {}\n", id.shape_name())); + s.push_str("e.array(2)?;\n"); + s.push_str(&format!("match {} {{\n", val.as_str())); + for field in fields.iter() { + let field_name = self.to_type_name(&field.id().to_string()); + s.push_str(&format!("{}::{}(v) => {{", id.shape_name(), &field_name)); + s.push_str(&format!("e.u16({})?;\n", &field.field_num().unwrap())); + s.push_str(&self.encode_shape_id(field.target(), ValExpr::Ref("v"), false)?); + s.push_str("},\n"); + } + s.push_str("}\n"); + Ok((s, fields.is_empty())) + } + /// Generate string to encode structure. /// Second Result field is true if structure has no fields, e.g., "MyStruct {}" fn encode_struct( @@ -386,14 +408,14 @@ impl<'model> RustCodeGen<'model> { match kind { ShapeKind::Simple(_) | ShapeKind::Structure(_) + | ShapeKind::Union(_) | ShapeKind::Map(_) | ShapeKind::List(_) | ShapeKind::Set(_) => { let name = id.shape_name(); // use val-by-copy as param to encode if type is rust primitive "copy" type // This is only relevant for aliases of primitive types in wasmbus-model namespace - let is_rust_copy = vec!["U8", "I8", "U16", "I16", "U32", "I32", "U64", "I64"] - .contains(&name.to_string().as_str()); + let is_rust_copy = is_rust_primitive(id); //let val_or_copy = if is_rust_copy { "*val" } else { "val" }; // The purpose of is_empty_struct is to determine when the parameter is unused // in the function body, and append '_' to the name to avoid a compiler warning. @@ -421,7 +443,6 @@ impl<'model> RustCodeGen<'model> { ShapeKind::Operation(_) | ShapeKind::Resource(_) | ShapeKind::Service(_) - | ShapeKind::Union(_) | ShapeKind::Unresolved => { /* write nothing */ } } Ok(()) diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index edc2de7..6b465d9 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -14,6 +14,7 @@ pub(crate) mod gen; mod loader; pub(crate) mod model; pub mod render; +pub(crate) mod validate; pub mod writer; pub use gen::{templates_from_dir, Generator}; diff --git a/codegen/src/model.rs b/codegen/src/model.rs index 4fad24d..7ccc190 100644 --- a/codegen/src/model.rs +++ b/codegen/src/model.rs @@ -22,16 +22,12 @@ use std::{fmt, ops::Deref, str::FromStr}; const WASMCLOUD_MODEL_NAMESPACE: &str = "org.wasmcloud.model"; const WASMCLOUD_CORE_NAMESPACE: &str = "org.wasmcloud.core"; -//const TRAIT_ACTOR_RECEIVER: &str = "actorReceiver"; -//const TRAIT_CAPABILITY: &str = "capability"; -//const TRAIT_PROVIDER_RECEIVER: &str = "providerReceiver"; const TRAIT_CODEGEN_RUST: &str = "codegenRust"; const TRAIT_SERIALIZATION: &str = "serialization"; const TRAIT_WASMBUS: &str = "wasmbus"; const TRAIT_WASMBUS_DATA: &str = "wasmbusData"; const TRAIT_FIELD_NUM: &str = "n"; const TRAIT_RENAME: &str = "rename"; -//const TRAIT_SERIALIZE_RENAME: &str = "rename"; lazy_static! { static ref WASMCLOUD_MODEL_NAMESPACE_ID: NamespaceID = @@ -135,6 +131,7 @@ impl TryFrom<&str> for WasmbusProtoVersion { } } } + impl fmt::Debug for WasmbusProtoVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(&self.to_string()) @@ -165,6 +162,7 @@ pub(crate) enum Ty<'typ> { /// write a reference type: preceeded by & Ref(&'typ ShapeID), } + // verify that the model doesn't contain unsupported types #[macro_export] macro_rules! expect_empty { @@ -248,13 +246,17 @@ pub fn get_trait(traits: &AppliedTraits, id: &ShapeID) -> R } } -pub fn wasmbus_proto(traits: &AppliedTraits) -> Result { +/// Returns Ok(Some( version )) if this is a service with the wasmbus protocol trait +/// Returns Ok(None) if this is not a wasmbus service +/// Returns Err() if there was an error parsing the declarataion +pub fn wasmbus_proto(traits: &AppliedTraits) -> Result> { match get_trait(traits, wasmbus_trait()) { Ok(Some(Wasmbus { protocol: Some(version), .. - })) => WasmbusProtoVersion::try_from(version.as_str()), - _ => Ok(WasmbusProtoVersion::default()), + })) => Ok(Some(WasmbusProtoVersion::try_from(version.as_str())?)), + Ok(_) => Ok(Some(WasmbusProtoVersion::default())), + _ => Ok(None), } } @@ -339,7 +341,7 @@ pub fn has_default(model: &'_ Model, member: &MemberShape) -> bool { } else if id.namespace() == wasmcloud_model_namespace() { matches!( name.as_str(), - "U64" | "U32" | "U16" | "U8" | "I64" | "I32" | "I16" | "I8" + "U64" | "U32" | "U16" | "U8" | "I64" | "I32" | "I16" | "I8" | "F64" | "F32" ) } else { false diff --git a/codegen/src/rust_build.rs b/codegen/src/rust_build.rs index 39059aa..6399480 100644 --- a/codegen/src/rust_build.rs +++ b/codegen/src/rust_build.rs @@ -49,6 +49,10 @@ pub fn rust_build_into, OUT: Into>( let model = crate::sources_to_model(&config.models, &config.base_dir, 0)?; + if let Err(msgs) = crate::validate::validate(&model) { + return Err(Box::new(Error::Build(msgs.join("\n")))); + } + // the second time we do this it should be faster since no downloading is required, // and we also don't invoke assembler to traverse directories for path in crate::sources_to_paths(&config.models, &config.base_dir, 0)?.into_iter() { diff --git a/codegen/src/validate.rs b/codegen/src/validate.rs new file mode 100644 index 0000000..b91766b --- /dev/null +++ b/codegen/src/validate.rs @@ -0,0 +1,270 @@ +//! utilities for model validation +//! +//! +use atelier_core::model::shapes::{ + AppliedTraits, ListOrSet, Operation, Service, Simple, StructureOrUnion, +}; +use atelier_core::model::visitor::{walk_model, ModelVisitor}; +use atelier_core::model::{Model, ShapeID}; +use std::cell::RefCell; +use std::collections::{BTreeMap, BTreeSet}; + +const MAX_DEPTH: usize = 8; + +#[derive(Debug)] +enum NodeType { + List, + Map, + Operation, + Service, + Structure, + Simple, + Union, + Unknown, +} + +/// Use Visitor pattern to build a tree of shapes and their dependencies. +/// Used for validation checks +struct ShapeTree { + tree: RefCell, + services: RefCell>, +} + +impl Default for ShapeTree { + fn default() -> Self { + ShapeTree { + tree: RefCell::new(Tree::default()), + services: RefCell::new(BTreeSet::default()), + } + } +} + +/// Data structure containing dependency tree of all shapes in model +#[derive(Default)] +struct Tree { + nodes: BTreeMap, +} + +impl Tree { + /// adds a parent-child relationship, ensuring nodes are created for both + fn insert_parent_child(&mut self, parent_id: &ShapeID, child_id: &ShapeID) { + let parent = self.get_or_insert(parent_id); + parent.add_child(child_id.clone()); + + let child = self.get_or_insert(child_id); + child.add_parent(parent_id.clone()); + } + + /// Returns node if it is in the model + fn get(&self, id: &ShapeID) -> Option<&Node> { + self.nodes.get(id) + } + + /// Returns the model node, creating a default instance if this is the first lookup + fn get_or_insert(&mut self, id: &ShapeID) -> &mut Node { + if !self.nodes.contains_key(id) { + self.nodes.insert(id.clone(), Node::new(id.clone())); + } + self.nodes.get_mut(id).unwrap() + } + + /// Returns true if the type is supported for cbor-only services + fn is_cbor_only(&self, node: &Node) -> Option { + if let NodeType::Union = node.typ { + Some(format!( + "union '{}' in namespace: {}", + node.id.shape_name(), + node.id.namespace() + )) + } else { + None + } + } + + /// Recursive node dump. Prints current value, and recurses up to max depts + fn has_cbor_only(&self, node: &Node, depth: usize) -> Option { + if let Some(reason) = self.is_cbor_only(node) { + Some(reason) + } else if depth < MAX_DEPTH { + node.children + .iter() + .filter_map(|child_id| { + self.nodes + .get(child_id) + .and_then(|c| self.has_cbor_only(c, depth + 1)) + }) + .next() + } else { + None + } + } +} + +impl ModelVisitor for ShapeTree { + type Error = String; + + fn simple_shape( + &self, + id: &ShapeID, + _traits: &AppliedTraits, + _shape: &Simple, + ) -> Result<(), Self::Error> { + let mut tree = self.tree.borrow_mut(); + let mut node = tree.get_or_insert(id); + node.typ = NodeType::Simple; + Ok(()) + } + + fn list( + &self, + id: &ShapeID, + _: &AppliedTraits, + list: &ListOrSet, + ) -> std::result::Result<(), Self::Error> { + let mut tree = self.tree.borrow_mut(); + let mut node = tree.get_or_insert(id); + node.typ = NodeType::List; + tree.insert_parent_child(id, list.member().target()); + Ok(()) + } + + fn map( + &self, + id: &ShapeID, + _: &AppliedTraits, + map: &atelier_core::model::shapes::Map, + ) -> std::result::Result<(), Self::Error> { + let mut tree = self.tree.borrow_mut(); + let mut node = tree.get_or_insert(id); + node.typ = NodeType::Map; + tree.insert_parent_child(id, map.key().target()); + tree.insert_parent_child(id, map.value().target()); + Ok(()) + } + + fn structure( + &self, + id: &ShapeID, + _: &AppliedTraits, + strukt: &StructureOrUnion, + ) -> std::result::Result<(), Self::Error> { + let mut tree = self.tree.borrow_mut(); + let mut node = tree.get_or_insert(id); + node.typ = NodeType::Structure; + for field in strukt.members() { + tree.insert_parent_child(id, field.target()); + } + Ok(()) + } + + fn union( + &self, + id: &ShapeID, + _: &AppliedTraits, + strukt: &StructureOrUnion, + ) -> std::result::Result<(), Self::Error> { + let mut tree = self.tree.borrow_mut(); + let mut node = tree.get_or_insert(id); + node.typ = NodeType::Union; + for field in strukt.members() { + tree.insert_parent_child(id, field.target()); + } + Ok(()) + } + + fn service( + &self, + id: &ShapeID, + _: &AppliedTraits, + service: &Service, + ) -> std::result::Result<(), Self::Error> { + let mut tree = self.tree.borrow_mut(); + let mut node = tree.get_or_insert(id); + node.typ = NodeType::Service; + for operation in service.operations() { + tree.insert_parent_child(id, operation); + } + let mut services = self.services.borrow_mut(); + services.insert(id.clone()); + Ok(()) + } + + fn operation( + &self, + id: &ShapeID, + _: &AppliedTraits, + operation: &Operation, + ) -> std::result::Result<(), Self::Error> { + let mut tree = self.tree.borrow_mut(); + let mut node = tree.get_or_insert(id); + node.typ = NodeType::Operation; + if let Some(input) = operation.input() { + tree.insert_parent_child(id, input); + } + if let Some(output) = operation.output() { + tree.insert_parent_child(id, output); + } + Ok(()) + } +} + +struct Node { + id: ShapeID, + parents: BTreeSet, + children: BTreeSet, + typ: NodeType, +} + +impl Node { + fn new(id: ShapeID) -> Self { + Self { + id, + parents: BTreeSet::new(), + children: BTreeSet::new(), + typ: NodeType::Unknown, + } + } + fn add_parent(&mut self, parent: ShapeID) { + self.parents.insert(parent); + } + fn add_child(&mut self, child: ShapeID) { + self.children.insert(child); + } +} + +/// Check the model for structures that require cbor serialization that are +/// used by a service operation (directly or indirectly), if the service uses non-cbor serialization. +/// This validation rule returns the first incompatibility found, not a list of all incompatibilities +pub(crate) fn check_cbor_dependencies(model: &Model) -> Result<(), String> { + use atelier_core::model::shapes::HasTraits as _; + let visitor = ShapeTree::default(); + let _ = walk_model(model, &visitor).expect("walk model"); + for service_id in visitor.services.borrow().iter() { + let service = model.shape(service_id).unwrap(); + let traits = service.traits(); + let proto = crate::model::wasmbus_proto(traits).map_err(|e| e.to_string())?; + let service_has_cbor = proto.map(|pv| pv.has_cbor()).unwrap_or(false); + // we only need to check for cbor conflicts on services that haven't declared cbor proto + if !service_has_cbor { + let tree = visitor.tree.borrow(); + let node = tree.get(service_id).unwrap(); + if let Some(reason) = tree.has_cbor_only(node, 0) { + return Err(format!( + "Service {}.{} must be declared @wasmbus{{protocol: \"2\"}} due to a dependency on {}", + service_id.namespace(), service_id.shape_name(), reason)); + } + } + } + Ok(()) +} + +/// Perform model validation - language-independent validation +/// Returns Ok(), or a list of one or more errors +/// TODO: these should also be called from `wash validate` +pub(crate) fn validate(model: &Model) -> Result<(), Vec> { + if let Err(msg) = check_cbor_dependencies(model) { + Err(vec![msg]) + } else { + Ok(()) + } +} diff --git a/rpc-rs/Cargo.toml b/rpc-rs/Cargo.toml index 10e73f0..2c5b6d0 100644 --- a/rpc-rs/Cargo.toml +++ b/rpc-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wasmbus-rpc" -version = "0.7.1" +version = "0.7.2" authors = [ "wasmcloud Team" ] license = "Apache-2.0" description = "Runtime library for actors and capability providers" @@ -44,7 +44,7 @@ bigdecimal = { version = "0.3", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1", features = ["full"]} futures = "0.3" -nats-aflowt = "0.16.103" +nats-aflowt = "0.16.104" once_cell = "1.8" crossbeam = "0.8" uuid = { version = "0.8", features=["v4", "serde"] } diff --git a/rpc-rs/src/lib.rs b/rpc-rs/src/lib.rs index 47c568a..050eae4 100644 --- a/rpc-rs/src/lib.rs +++ b/rpc-rs/src/lib.rs @@ -22,42 +22,42 @@ pub mod cbor; pub mod error; #[deprecated( - since = "0.7.0-alpha.2", + since = "0.7.0", note = "use wasmbus_rpc::common::deserialize instead of wasmbus_rpc::deseerialize" )] pub use common::deserialize; #[deprecated( - since = "0.7.0-alpha.2", + since = "0.7.0", note = "use wasmbus_rpc::common::serialize instead of wasmbus_rpc::serialize" )] pub use common::serialize; #[deprecated( - since = "0.7.0-alpha.2", + since = "0.7.0", note = "use wasmbus_rpc::common::Context instead of wasmbus_rpc::Context" )] pub use common::Context; #[deprecated( - since = "0.7.0-alpha.2", + since = "0.7.0", note = "use wasmbus_rpc::common::Message instead of wasmbus_rpc::Message" )] pub use common::Message; #[deprecated( - since = "0.7.0-alpha.2", + since = "0.7.0", note = "use wasmbus_rpc::common::SendOpts instead of wasmbus_rpc::SendOpts" )] pub use common::SendOpts; #[deprecated( - since = "0.7.0-alpha.2", + since = "0.7.0", note = "use wasmbus_rpc::common::Transport instead of wasmbus_rpc::Transport" )] pub use common::Transport; #[deprecated( - since = "0.7.0-alpha.2", + since = "0.7.0", note = "use wasmbus_rpc::error::RpcError instead of wasmbus_rpc::RpcError" )] pub use error::RpcError; #[deprecated( - since = "0.7.0-alpha.2", + since = "0.7.0", note = "use wasmbus_rpc::error::RpcResult instead of wasmbus_rpc::RpcResult" )] pub use error::RpcResult;