weld_codegen/
model.rs

1//! Smithy model helpers
2//! - some constants used for model validation
3//! - ModelIndex is a "cache" of a smithy model grouped by shape kind and sorted by identifier name
4//! - various macros used in the codegen crate
5//!
6use std::{fmt, ops::Deref, str::FromStr};
7
8use atelier_core::{
9    model::{
10        shapes::{AppliedTraits, HasTraits, MemberShape, Operation, ShapeKind, StructureOrUnion},
11        values::{Number, Value as NodeValue},
12        HasIdentity, Identifier, Model, NamespaceID, ShapeID,
13    },
14    prelude::prelude_namespace_id,
15};
16use lazy_static::lazy_static;
17use serde::{de::DeserializeOwned, Deserialize};
18
19use crate::{
20    error::{Error, Result},
21    JsonValue,
22};
23
24const WASMCLOUD_MODEL_NAMESPACE: &str = "org.wasmcloud.model";
25const WASMCLOUD_CORE_NAMESPACE: &str = "org.wasmcloud.core";
26const WASMCLOUD_ACTOR_NAMESPACE: &str = "org.wasmcloud.actor";
27
28const TRAIT_CODEGEN_RUST: &str = "codegenRust";
29// If any of these are needed, they would have to be defined in core namespace
30//const TRAIT_CODEGEN_ASM: &str = "codegenAsm";
31//const TRAIT_CODEGEN_GO: &str = "codegenGo";
32//const TRAIT_CODEGEN_TINYGO: &str = "codegenTinyGo";
33
34const TRAIT_SERIALIZATION: &str = "serialization";
35const TRAIT_WASMBUS: &str = "wasmbus";
36const TRAIT_WASMBUS_DATA: &str = "wasmbusData";
37const TRAIT_FIELD_NUM: &str = "n";
38const TRAIT_RENAME: &str = "rename";
39
40lazy_static! {
41    static ref WASMCLOUD_MODEL_NAMESPACE_ID: NamespaceID =
42        NamespaceID::new_unchecked(WASMCLOUD_MODEL_NAMESPACE);
43    static ref WASMCLOUD_CORE_NAMESPACE_ID: NamespaceID =
44        NamespaceID::new_unchecked(WASMCLOUD_CORE_NAMESPACE);
45    static ref WASMCLOUD_ACTOR_NAMESPACE_ID: NamespaceID =
46        NamespaceID::new_unchecked(WASMCLOUD_ACTOR_NAMESPACE);
47    static ref SERIALIZATION_TRAIT_ID: ShapeID = ShapeID::new(
48        NamespaceID::new_unchecked(WASMCLOUD_MODEL_NAMESPACE),
49        Identifier::from_str(TRAIT_SERIALIZATION).unwrap(),
50        None
51    );
52    static ref CODEGEN_RUST_TRAIT_ID: ShapeID = ShapeID::new(
53        NamespaceID::new_unchecked(WASMCLOUD_MODEL_NAMESPACE),
54        Identifier::from_str(TRAIT_CODEGEN_RUST).unwrap(),
55        None
56    );
57    static ref WASMBUS_TRAIT_ID: ShapeID = ShapeID::new(
58        NamespaceID::new_unchecked(WASMCLOUD_MODEL_NAMESPACE),
59        Identifier::from_str(TRAIT_WASMBUS).unwrap(),
60        None
61    );
62    static ref WASMBUS_DATA_TRAIT_ID: ShapeID = ShapeID::new(
63        NamespaceID::new_unchecked(WASMCLOUD_MODEL_NAMESPACE),
64        Identifier::from_str(TRAIT_WASMBUS_DATA).unwrap(),
65        None
66    );
67    static ref FIELD_NUM_TRAIT_ID: ShapeID = ShapeID::new(
68        NamespaceID::new_unchecked(WASMCLOUD_MODEL_NAMESPACE),
69        Identifier::from_str(TRAIT_FIELD_NUM).unwrap(),
70        None
71    );
72    static ref RENAME_TRAIT_ID: ShapeID = ShapeID::new(
73        NamespaceID::new_unchecked(WASMCLOUD_MODEL_NAMESPACE),
74        Identifier::from_str(TRAIT_RENAME).unwrap(),
75        None
76    );
77    static ref UNIT_ID: ShapeID = ShapeID::new_unchecked(WASMCLOUD_MODEL_NAMESPACE, "Unit", None);
78}
79
80/// namespace for org.wasmcloud.model
81pub fn wasmcloud_model_namespace() -> &'static NamespaceID {
82    &WASMCLOUD_MODEL_NAMESPACE_ID
83}
84pub fn wasmcloud_core_namespace() -> &'static NamespaceID {
85    &WASMCLOUD_CORE_NAMESPACE_ID
86}
87pub fn wasmcloud_actor_namespace() -> &'static NamespaceID {
88    &WASMCLOUD_ACTOR_NAMESPACE_ID
89}
90
91#[cfg(feature = "wasmbus")]
92/// shape id of trait @wasmbus
93pub fn wasmbus_trait() -> &'static ShapeID {
94    &WASMBUS_TRAIT_ID
95}
96
97#[allow(dead_code)]
98#[cfg(feature = "wasmbus")]
99/// shape id of trait @wasmbusData
100pub fn wasmbus_data_trait() -> &'static ShapeID {
101    &WASMBUS_DATA_TRAIT_ID
102}
103
104/// shape id of trait @serialization
105pub fn serialization_trait() -> &'static ShapeID {
106    &SERIALIZATION_TRAIT_ID
107}
108
109/// shape id of trait @codegenRust
110pub fn codegen_rust_trait() -> &'static ShapeID {
111    &CODEGEN_RUST_TRAIT_ID
112}
113
114/// shape id of trait @n
115pub fn field_num_trait() -> &'static ShapeID {
116    &FIELD_NUM_TRAIT_ID
117}
118
119/// shape id of trait @rename
120pub fn rename_trait() -> &'static ShapeID {
121    &RENAME_TRAIT_ID
122}
123
124pub fn unit_shape() -> &'static ShapeID {
125    &UNIT_ID
126}
127
128#[allow(dead_code)]
129pub enum CommentKind {
130    Inner,
131    Documentation,
132    /// inside a multi-line quote, as in python
133    InQuote,
134}
135
136#[derive(Default, Clone, PartialEq, Eq)]
137pub struct WasmbusProtoVersion {
138    base: u8, // base number
139}
140
141impl TryFrom<&str> for WasmbusProtoVersion {
142    type Error = crate::error::Error;
143
144    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
145        match value {
146            "0" => Ok(WasmbusProtoVersion { base: 0 }),
147            "2" => Ok(WasmbusProtoVersion { base: 2 }),
148            _ => Err(Error::Model(format!(
149                "Invalid wasmbus.protocol: '{value}'. The default value is \"0\"."
150            ))),
151        }
152    }
153}
154
155impl fmt::Debug for WasmbusProtoVersion {
156    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
157        f.write_str(&self.to_string())
158    }
159}
160
161impl ToString for WasmbusProtoVersion {
162    fn to_string(&self) -> String {
163        format!("{}", self.base)
164    }
165}
166
167impl WasmbusProtoVersion {
168    pub fn has_cbor(&self) -> bool {
169        self.base >= 2
170    }
171}
172
173// Modifiers on data type
174// This enum may be extended in the future if other variations are required.
175// It's recursively composable, so you could represent &Option<&Value>
176// with `Ty::Ref(Ty::Opt(Ty::Ref(id)))`
177pub(crate) enum Ty<'typ> {
178    /// write a plain shape declaration
179    Shape(&'typ ShapeID),
180    /// write a type wrapped in Option<>
181    Opt(&'typ ShapeID),
182    /// write a reference type: preceded by &
183    Ref(&'typ ShapeID),
184
185    /// write a ptr type: preceded by *
186    Ptr(&'typ ShapeID),
187}
188
189// verify that the model doesn't contain unsupported types
190#[macro_export]
191macro_rules! expect_empty {
192    ($list:expr, $msg:expr) => {
193        if !$list.is_empty() {
194            return Err(Error::InvalidModel(format!(
195                "{}: {}",
196                $msg,
197                $list.keys().map(|k| k.to_string()).collect::<Vec<String>>().join(",")
198            )));
199        }
200    };
201}
202
203#[macro_export]
204macro_rules! unsupported_shape {
205    ($fn_name:ident, $shape_type:ty, $doc:expr) => {
206        #[allow(unused_variables)]
207        fn $fn_name(
208            &mut self,
209            id: &ShapeID,
210            traits: &AppliedTraits,
211            shape: &$shape_type,
212        ) -> Result<()> {
213            return Err(weld_codegen::error::Error::UnsupportedShape(
214                id.to_string(),
215                $doc.to_string(),
216            ));
217        }
218    };
219}
220
221/// true if namespace matches, or if there is no namespace constraint
222pub fn is_opt_namespace(id: &ShapeID, ns: &Option<NamespaceID>) -> bool {
223    match ns {
224        Some(ns) => id.namespace() == ns,
225        None => true,
226    }
227}
228
229/// Finds the operation in the model or returns error
230pub fn get_operation<'model>(
231    model: &'model Model,
232    operation_id: &'_ ShapeID,
233    service_id: &'_ Identifier,
234) -> Result<(&'model Operation, &'model AppliedTraits)> {
235    let op = model
236        .shapes()
237        .filter(|t| t.id() == operation_id)
238        .find_map(|t| {
239            if let ShapeKind::Operation(op) = t.body() {
240                Some((op, t.traits()))
241            } else {
242                None
243            }
244        })
245        .ok_or_else(|| {
246            Error::Model(format!(
247                "missing operation {} for service {}",
248                &operation_id.to_string(),
249                &service_id.to_string()
250            ))
251        })?;
252    Ok(op)
253}
254
255/// Returns trait as deserialized object, or None if the trait is not defined.
256/// Returns error if the deserialization failed.
257pub fn get_trait<T: DeserializeOwned>(traits: &AppliedTraits, id: &ShapeID) -> Result<Option<T>> {
258    match traits.get(id) {
259        Some(Some(val)) => match trait_value(val) {
260            Ok(obj) => Ok(Some(obj)),
261            Err(e) => Err(e),
262        },
263        Some(None) => Ok(None),
264        None => Ok(None),
265    }
266}
267
268/// Returns Ok(Some( version )) if this is a service with the wasmbus protocol trait
269/// Returns Ok(None) if this is not a wasmbus service
270/// Returns Err() if there was an error parsing the declarataion
271pub fn wasmbus_proto(traits: &AppliedTraits) -> Result<Option<WasmbusProtoVersion>> {
272    match get_trait(traits, wasmbus_trait()) {
273        Ok(Some(Wasmbus { protocol: Some(version), .. })) => {
274            Ok(Some(WasmbusProtoVersion::try_from(version.as_str())?))
275        }
276        Ok(_) => Ok(Some(WasmbusProtoVersion::default())),
277        _ => Ok(None),
278    }
279}
280
281/// Convert trait object to its native type
282pub fn trait_value<T: DeserializeOwned>(value: &NodeValue) -> Result<T> {
283    let json = value_to_json(value);
284    let obj = serde_json::from_value(json)?;
285    Ok(obj)
286}
287
288/// Convert smithy model 'Value' to a json object
289pub fn value_to_json(value: &NodeValue) -> JsonValue {
290    match value {
291        NodeValue::None => JsonValue::Null,
292        NodeValue::Array(v) => JsonValue::Array(v.iter().map(value_to_json).collect()),
293        NodeValue::Object(v) => {
294            let mut object = crate::JsonMap::default();
295            for (k, v) in v {
296                let _ = object.insert(k.clone(), value_to_json(v));
297            }
298            JsonValue::Object(object)
299        }
300        NodeValue::Number(v) => match v {
301            Number::Integer(v) => JsonValue::Number((*v).into()),
302            Number::Float(v) => JsonValue::Number(serde_json::Number::from_f64(*v).unwrap()),
303        },
304        NodeValue::Boolean(v) => JsonValue::Bool(*v),
305        NodeValue::String(v) => JsonValue::String(v.clone()),
306    }
307}
308
309/// resolve shape to its underlying shape
310/// e.g., if you have a declaration "string Foo",
311/// it will resolve Foo into smithy.api#String
312pub fn resolve<'model>(model: &'model Model, shape: &'model ShapeID) -> &'model ShapeID {
313    if let Some(resolved) = model.shape(shape) {
314        resolved.id()
315    } else {
316        shape
317    }
318}
319
320/// Returns true if the type has a natural default (zero, empty set/list/map, etc.).
321/// Doesn't work for user-defined structs, only (most) simple types,
322/// and set, list, and map.
323///
324/// This can be used for deserialization,
325/// to allow missing fields to be filled in with the default.
326///
327/// The smithy developers considered and rejected the idea of being able to declare
328/// a default value that is not zero (such as http status with a default 200),
329/// which would be in the realm of business logic and outside the scope of smithy.
330/// This default only applies to simple types that have a zero value,
331/// and empty sets, list, and maps.
332pub fn has_default(model: &'_ Model, member: &MemberShape) -> bool {
333    let id = resolve(model, member.target());
334    #[allow(unused_mut)]
335    let mut has = false;
336    let name = id.shape_name().to_string();
337
338    if id.namespace().eq(prelude_namespace_id()) {
339        cfg_if::cfg_if! {
340            if #[cfg(feature = "BigInteger")] {
341                has = has || &name == "bigInteger";
342            }
343        }
344        cfg_if::cfg_if! {
345            if #[cfg(feature = "BigDecimal")] {
346                has = has || &name == "bigDecimal";
347            }
348        }
349        has || matches!(
350            name.as_str(),
351            // some aggregate types
352            "List" | "Set" | "Map"
353            // most simple types
354            | "Blob" | "Boolean" | "String" | "Byte" | "Short"
355            | "Integer" | "Long" | "Float" | "Double"
356            | "Timestamp"
357        )
358        // excluded: Resource, Operation, Service, Document, Union
359    } else if id.namespace() == wasmcloud_model_namespace() {
360        matches!(
361            name.as_str(),
362            "U64" | "U32" | "U16" | "U8" | "I64" | "I32" | "I16" | "I8" | "F64" | "F32"
363        )
364    } else {
365        false
366        // for any other type, return false.
367        // if there was a need to override this,
368        // a trait could be added
369    }
370}
371
372pub struct NumberedMember {
373    field_num: Option<u16>,
374    shape: MemberShape,
375}
376
377impl NumberedMember {
378    pub(crate) fn new(member: &MemberShape) -> Result<Self> {
379        Ok(NumberedMember {
380            shape: member.to_owned(),
381            field_num: get_trait::<u16>(member.traits(), field_num_trait()).map_err(|e| {
382                Error::Model(format!(
383                    "invalid field number @n() for field '{}': {}",
384                    member.id(),
385                    e
386                ))
387            })?,
388        })
389    }
390
391    pub(crate) fn field_num(&self) -> &Option<u16> {
392        &self.field_num
393    }
394}
395
396impl Deref for NumberedMember {
397    type Target = MemberShape;
398
399    fn deref(&self) -> &Self::Target {
400        &self.shape
401    }
402}
403
404use std::iter::Iterator;
405
406use crate::wasmbus_model::Wasmbus;
407
408/// Returns sorted list of fields for the structure, and whether it is numbered.
409/// If there are any errors in numbering, returns Error::Model
410pub(crate) fn get_sorted_fields(
411    id: &Identifier,
412    strukt: &StructureOrUnion,
413) -> Result<(Vec<NumberedMember>, bool)> {
414    let mut fields = strukt
415        .members()
416        .map(NumberedMember::new)
417        .collect::<Result<Vec<NumberedMember>>>()?;
418    let has_numbers = crate::model::has_field_numbers(&fields, &id.to_string())?;
419    // Sort fields for deterministic output
420    // by number, if declared with numbers, otherwise by name
421    if has_numbers {
422        fields.sort_by_key(|f| f.field_num().unwrap());
423    } else {
424        fields.sort_by_key(|f| f.id().to_owned());
425    }
426    Ok((fields, has_numbers))
427}
428
429/// Checks whether a struct has complete and valid field numbers.
430/// Returns true if all fields have unique numbers.
431/// Returns false if no fields are numbered.
432/// Returns Err if fields are incompletely numbered, or there are duplicate numbers.
433pub(crate) fn has_field_numbers(fields: &[NumberedMember], name: &str) -> Result<bool> {
434    let mut numbered = std::collections::BTreeSet::default();
435    for f in fields.iter() {
436        if let Some(n) = f.field_num() {
437            numbered.insert(*n);
438        }
439    }
440    if numbered.is_empty() {
441        Ok(false)
442    } else if numbered.len() == fields.len() {
443        // all fields are numbered uniquely
444        Ok(true)
445    } else {
446        Err(crate::Error::Model(format!(
447            "structure {name} has incomplete or invalid field numbers: either some fields are missing \
448             the '@n()' trait, or some fields have duplicate numbers."
449        )))
450    }
451}
452
453/*
454pub fn get_metadata(model: &Model) -> JsonMap {
455    let mut metadata_map = JsonMap::default();
456    for (key, value) in model.metadata() {
457        let _ = metadata_map.insert(key.to_string(), value_to_json(value));
458    }
459    metadata_map
460}
461 */
462
463/// Map namespace to package
464///   rust: crate_name
465///   other-languages: TBD
466#[derive(Clone, Deserialize)]
467pub struct PackageName {
468    pub namespace: String,
469    #[serde(rename = "crate")]
470    pub crate_name: Option<String>,
471    #[serde(rename = "py_module")]
472    pub py_module: Option<String>,
473    pub go_package: Option<String>,
474    pub doc: Option<String>,
475}