Sign in

    smithy

    Smithy model library for MoonBit

    smithy
    idl
    api
    moonbit
    Download zip
    Author
    Version
    0.3.0
    License
    Apache-2.0
    Last updated
    5 hours ago
    Downloads
    17

    #moonrockz/smithy

    An in-memory Smithy semantic model and multi-file loader for MoonBit. IDL and JSON AST inputs assemble together with the official Smithy prelude and contract trait definitions.

    #Load a model

    ///|
    test {
    let result = @smithy.Loader::new()
    .add_idl(
    "city.smithy",
    (
    #|$version: "2"
    #|namespace example.weather
    #|structure City {
    #| @required
    #| name: String
    #|}
    ),
    )
    .assemble()
    assert_true(!result.has_errors())
    let model = result.model.unwrap()
    let name = model.get("example.weather#City$name").unwrap()
    guard name.body is Member(target~) else { fail("expected a member") }
    assert_eq(target.to_string(), "smithy.api#String")
    assert_true(name.trait_value("smithy.api#required") is Some(_))
    }

    Use add_json(name, text) for JSON text, add_ast(name, document) for a typed @syntax.JsonAst, or add(ModelInput) for an input chosen at runtime. Names identify sources in diagnostics; <prelude> and <contracts> are reserved. The loader does no file-system or network I/O. Repeated calls to assemble build fresh state without changing caller-owned AST documents.

    #Semantic records

    • ShapeId is an absolute ID with validated namespace, shape name and optional member name. ShapeId::parse returns None for invalid input.
    • Model contains metadata and a map of shape IDs to Shape records, including first-class member shapes. Model::get accepts a string ID.
    • ShapeBody distinguishes simple shapes, lists, maps, structures, unions, enums, operations, services, resources and members. Every reference is a ShapeId. A member's body holds its target; its traits belong to that member.
    • NodeValue holds null, Boolean, number, string, array and object values. NodeNumber retains the original number token and compares exact numeric values. 1 and 1.0 compare equal; adjacent integers above Double's exact range remain distinct. Large exponents are stored without expanding zeros.
    • A Shape retains its source, applied traits and direct mixin IDs. Inherited members have IDs belonging to their containing shape. Shape::introduced_traits() reports explicitly declared traits; Shape::is_legacy_set() retains the origin of Smithy 1 set shapes. Its optional provenance record lets transforms remove mixins while preserving declared overrides.

    These records can also be constructed directly. Assembly validates a model; mutating its maps afterward requires the caller to maintain those invariants.

    Model::to_json_ast() emits flattened contents. Set preserve_mixins=true to emit mixin declarations, introduced fields and member apply entries in the reference representation. The build model plugin uses this form.

    #Assembly rules

    The loader collects names across all inputs before resolving IDL references. Resolution checks imports, definitions in the current namespace, then public prelude shapes. Absolute references remain absolute. Only public prelude names participate in implicit resolution. Unquoted IDL node values resolve as shape IDs; quoted values remain literal strings.

    The loader expands mixins using an explicit dependency stack. It checks cycles, compatible shape kinds, member target conflicts and mixin restrictions. Members follow mixin order, followed by local members. Local traits override inherited traits, and mixin-local traits stay on their defining mixin. Service properties merge across mixins. Resource-bound $member elision resolves against identifiers/properties and inherited members.

    External apply entries merge onto shapes and members after their targets are known. List-valued traits concatenate; equal values are accepted; other collisions fail. Metadata arrays concatenate, equal metadata values are accepted, and other metadata collisions fail. Conflicting duplicate shape definitions are errors. Built-in prelude and contract definitions cannot be redefined or modified.

    Inline operation structures receive the input or output trait, with names controlled by $operationInputSuffix and $operationOutputSuffix. Missing operation inputs and outputs target smithy.api#Unit. Enum members receive their inferred string values after mixin inheritance. Legacy sets load as lists with uniqueItems; set syntax in a Smithy 2.0 input is an error.

    Assembly checks reference existence and target kinds, member-name collisions, required collection members, enum values, resource cycles and the structure of applied trait values. Configurable validation plugins run after assembly.

    LoadResult::validation_events() reports assembly findings and model validation events with their reference IDs, severities and shape IDs. It also works when assembly cannot return a model. Model::validation_events() validates a model constructed or transformed in memory. Both apply metadata suppressions, severity overrides and @suppress, and run critical checks before later validators. The test suite compares these event identities with Java Smithy for all 277 paired validation corpus models.

    #Select shapes

    Selector::parse(expression) parses Smithy selectors. select_ids(model) returns matching absolute shape IDs in lexical order. The selector engine supports shape types, attributes, directed and recursive neighbors, functions, scoped comparisons and variables. Unknown function names are valid syntax and act as identity steps, as in the reference implementation. Internal contract definitions are omitted from selection; public prelude shapes remain available.

    ///|
    test {
    let result = @smithy.Loader::new()
    .add_idl(
    "one.smithy", "$version: \"2\"\nnamespace example\nstructure One {}",
    )
    .assemble()
    let model = result.model.unwrap()
    let ids = @smithy.Selector::parse("structure[id|namespace=example]").select_ids(
    model,
    )
    assert_eq(ids, ["example#One"])
    }

    #Contract traits

    The loader includes smithy.contracts#conditions. Import it with use smithy.contracts#conditions, or use its absolute ID. It is not an implicitly imported prelude name.

    $version: "2" namespace example.logs use smithy.contracts#conditions @conditions({ StartBeforeEnd: { expression: "start < end" documentation: "The start time must be strictly less than the end time." } }) structure FetchLogsInput { @required start: Timestamp @required end: Timestamp }

    Assembly checks that each condition has string expression and documentation fields, that condition names match ^[A-Z]+[A-Za-z0-9]*$, and that the trait is applied to a data shape or member. Services, operations and resources are invalid targets. Empty condition maps are valid. Expressions and CommonMark documentation survive mixin inheritance, external apply and IDL/JSON conversion unchanged.

    Assembly parses expressions with the public moonrockz/jmespath package. Static analysis checks member references, collection operations, function names, arity and argument types, including expression references such as sort_by(items, &name). A condition must have a boolean result type. Both logical branches are checked. ends_with(name, 'suffix') has a boolean type even when it would return false.

    Types follow the Smithy JMESPath data model. Timestamps are numbers, blobs and enums are strings, and members use their Smithy names rather than jsonName wire names. Map keys and document contents are dynamic. Recursive shapes expand only when a field or collection operation needs their type; analysis uses an explicit stack. Mixed element or branch types can become unknown. Unknown data permits operations that cannot be disproved, but an unknown final result does not establish a boolean contract. This analysis does not prove that a condition will hold for runtime values.

    Syntax errors use SM-MODEL-015, invalid operations and function calls use SM-MODEL-016, and non-boolean results use SM-MODEL-017. Invalid ordering comparisons produce a warning because JMESPath evaluates them to null; a null final result also produces SM-MODEL-017. Other static failures prevent assembly. These errors cover upstream ConditionsTraitValidator errors and danger events using this library's existing Error/Warning severities.

    Diagnostics retain the owning shape's source location and add a JSON Pointer to the condition's expression. The message contains the condition name, expression-relative line and column, and UTF-16 offsets. Expression offsets are never treated as offsets into the enclosing IDL or JSON file. Serialization and default-value validation do not evaluate contracts.

    #Diagnostics

    LoadResult contains diagnostics and an optional model. Errors prevent a partial model from being returned; warnings permit a model. Each diagnostic has a stable code, severity, message, source and optional shape ID. IDL parser diagnostics keep their original codes and UTF-16 spans. JSON codec errors keep their JSON Pointer paths. diagnostic_codes() lists the assembly codes.

    #Export a model

    ///|
    test {
    let result = @smithy.Loader::new()
    .add_json(
    "name.json", "{\"smithy\":\"2\",\"shapes\":{\"example.weather#Name\":{\"type\":\"string\"}}}",
    )
    .assemble()
    let document = result.model.unwrap().to_json_ast()
    assert_eq(document.smithy, "2.0")
    assert_eq(document.shapes.unwrap().length(), 1)
    }

    Model::to_json_ast exports Smithy 2.0 with inherited contents flattened into each shape. Built-in prelude and contract definitions are omitted unless include_prelude=true. Exact node number tokens survive export. Source locations and mixin provenance belong to the in-memory model and are not serialized.

    Model::to_idl exports a map from namespace to Smithy 2 IDL text. It omits built-in definitions, flattens inherited contents and uses absolute references. Metadata appears once, in the first namespace. An empty or metadata-only model uses the empty namespace key. Generated text is deterministic and retains exact node numbers; conversion does not retain comments or original source layout.

    ///|
    test {
    let model = @smithy.Loader::new()
    .add_idl("name.smithy", "$version: \"2\"\nnamespace example\nstring Name")
    .assemble().model.unwrap()
    let files = model.to_idl()
    let again = @smithy.Loader::new()
    for ns, text in files {
    again.add_idl(ns + ".smithy", text) |> ignore
    }
    assert_true(!again.assemble().has_errors())
    }

    The prelude and contract definitions are vendored from smithy-lang/smithy at PRELUDE_SHA, embedded in prelude_data.mbt and contracts_data.mbt, and distributed under Apache-2.0. Refresh both with mise run prelude:vendor -- <git-ref>.

    The acceptance contract is tests/features/model.feature. Compatibility tests compare every paired vendored IDL/JSON model, check reference flattened mixins, load the unflattened JSON mixin corpus and reject every invalid IDL case.

    SimpleType

    The AST.

    NodeError

    pub(all) suberror NodeError {
    InvalidNode(String)
    } derive(Eq,
    Debug
    )

    SelectorError

    pub(all) suberror SelectorError {
    InvalidSelector(String)
    } derive(Eq,
    Debug
    )

    LoadResult

    pub(all) struct LoadResult {
    model : Model?
    diagnostics : Array[ModelDiagnostic]
    validation_findings : Array[ValidationEvent]
    } derive(
    Debug
    )

    Failed assembly never returns a partially validated model.

    LoadResult::has_errors

    fn LoadResult::has_errors(self : LoadResult) -> Bool

    LoadResult::validation_events

    fn LoadResult::validation_events(self : LoadResult) -> Array[ValidationEvent]

    Include assembly findings even when no complete model could be returned.

    Loader

    pub struct Loader {
    inputs : Array[ModelInput]
    }

    An in-memory multi-file loader. It never reads files or the network. Repeated assembly creates fresh state and does not mutate its inputs.

    Loader::add

    fn Loader::add(self : Loader, input : ModelInput) -> Loader

    Loader::add_ast

    fn Loader::add_ast(self : Loader, name : String, document :
    JsonAst
    ) -> Loader

    Loader::add_idl

    fn Loader::add_idl(self : Loader, name : String, text : String) -> Loader

    Loader::add_json

    fn Loader::add_json(self : Loader, name : String, text : String) -> Loader

    Loader::assemble

    fn Loader::assemble(self : Loader) -> LoadResult

    Loader::new

    fn Loader::new() -> Loader

    Model

    pub(all) struct Model {
    shapes : Map[ShapeId, Shape]
    metadata : Map[String, NodeValue]
    } derive(Eq,
    Debug
    )

    A graph of semantic shapes and metadata. Assembly includes built-in prelude and contract definitions; serialization omits them unless explicitly requested.

    Model::get

    fn Model::get(self : Model, id : String) -> Shape?

    Model::remove_mixin_relationships

    fn Model::remove_mixin_relationships(self : Model, removed : Array[ShapeId]) -> Model raise

    Rebuild inherited content after removing mixin relationships, without running validation or reintroducing prelude shapes into projected models.

    Model::to_idl

    fn Model::to_idl(self : Model) -> Map[String, String] raise

    Serialize an assembled model to Smithy 2 IDL, keyed by namespace. Built-in definitions are omitted and inherited contents are flattened, as in to_json_ast. Metadata appears exactly once, in the first namespace. A metadata-only or empty model uses the empty namespace key. References are absolute, so names cannot collide with local definitions or implicit prelude names.

    Model::to_json_ast

    fn Model::to_json_ast(self : Model, include_prelude? : Bool, preserve_mixins? : Bool) ->
    JsonAst
    raise

    Serialize the assembled graph to a flattened JSON AST. Mixins remain available as shape provenance in memory; their inherited contents are explicit in this representation. Built-in prelude and contract definitions are omitted unless include_prelude is true.

    Model::validation_events

    fn Model::validation_events(self : Model) -> Array[ValidationEvent]

    Run validators configured in model metadata and apply event decoration.

    ModelDiagnostic

    pub(all) struct ModelDiagnostic {
    code : String
    event_id : String
    severity :
    Severity

    message : String
    source : SourceLocation
    shape : ShapeId?
    } derive(Eq,
    Debug
    )

    An assembly error. Syntax diagnostics retain their original code and span.

    ModelInput

    pub(all) enum ModelInput {
    Idl(name~ : String, text~ : String)
    Json(name~ : String, text~ : String)
    Ast(name~ : String, document~ :
    JsonAst
    )
    } derive(
    Debug
    )

    NodeNumber

    pub struct NodeNumber {
    text : String
    coefficient :
    BigInt

    exponent :
    BigInt

    approximate : Double
    } derive(
    Debug
    )

    An exact JSON number. Equality compares numeric values, not Double approximations or token spellings. No exponent is expanded into zeros.
    impl Eq for NodeNumber

    NodeNumber::parse

    fn NodeNumber::parse(text : String) -> NodeNumber?

    NodeValue

    pub(all) enum NodeValue {
    Null
    Boolean(Bool)
    Number(NodeNumber)
    String(String)
    Array(Array[NodeValue])
    Object(Map[String, NodeValue])
    } derive(Eq,
    Debug
    )

    Source-independent values used for metadata and applied traits.
    impl ToJson for NodeValue

    NodeValue::from_json

    fn NodeValue::from_json(value : Json) -> NodeValue raise NodeError

    ScopedAssertion

    pub(all) struct ScopedAssertion {
    left : ScopedValue
    operator : String
    right : Array[ScopedValue]
    insensitive : Bool
    } derive(Eq,
    Debug
    )

    ScopedValue

    pub(all) enum ScopedValue {
    Literal(String)
    Path(Array[String])
    } derive(Eq,
    Debug
    )

    Selector

    pub(all) struct Selector {
    source : String
    steps : Array[SelectorStep]
    } derive(Eq,
    Debug
    )

    Selector::parse

    fn Selector::parse(source : String) -> Selector raise SelectorError

    Selector::select_ids

    fn Selector::select_ids(self : Selector, model : Model) -> Array[String]

    Return matching absolute shape IDs in lexical order.

    Selector::select_ids_from

    fn Selector::select_ids_from(self : Selector, model : Model, start_id : String) -> Array[String]

    Evaluate a selector with one shape as its starting context.

    Selector::select_matches

    fn Selector::select_matches(self : Selector, model : Model) -> Array[SelectorMatch]

    Retain variable captures for each selector match.

    SelectorMatch

    pub(all) struct SelectorMatch {
    id : String
    variables : Map[String, Array[String]]
    } derive(
    Debug
    )

    SelectorStep

    pub(all) enum SelectorStep {
    ShapeType(String)
    AnyShape
    Attribute(Array[String], String?, Array[String], Bool)
    ScopedAttribute(Array[String], Array[ScopedAssertion])
    Neighbor(String, Array[String])
    Function(String, Array[Array[SelectorStep]])
    VariableStore(String, Array[SelectorStep])
    VariableGet(String)
    } derive(Eq,
    Debug
    )

    Shape

    pub(all) struct Shape {
    id : ShapeId
    body : ShapeBody
    traits : Map[ShapeId, NodeValue]
    mixins : Array[ShapeId]
    provenance : ShapeProvenance?
    source : SourceLocation
    } derive(Eq,
    Debug
    )

    A shape, including first-class member shapes.

    Shape::introduced_traits

    fn Shape::introduced_traits(self : Shape) -> Map[ShapeId, NodeValue]

    Traits introduced by this shape, including changes made after assembly.

    Shape::is_legacy_set

    fn Shape::is_legacy_set(self : Shape) -> Bool

    Whether this collection retains the identity of a Smithy 1.0 set.

    Shape::trait_value

    fn Shape::trait_value(self : Shape, id : String) -> NodeValue?

    ShapeBody

    pub(all) enum ShapeBody {
    Simple(
    SimpleType
    )
    List(ShapeId)
    Map(key~ : ShapeId, value~ : ShapeId)
    Structure(Array[ShapeId])
    Union(Array[ShapeId])
    Enum(Array[ShapeId])
    IntEnum(Array[ShapeId])
    Member(target~ : ShapeId)
    Operation(input~ : ShapeId, output~ : ShapeId, errors~ : Array[ShapeId])
    Service(version~ : String, operations~ : Array[ShapeId], resources~ : Array[ShapeId], errors~ : Array[ShapeId], rename~ : Map[ShapeId, String])
    Resource(identifiers~ : Map[String, ShapeId], properties~ : Map[String, ShapeId], create~ : ShapeId?, put~ : ShapeId?, read~ : ShapeId?, update~ : ShapeId?, delete~ : ShapeId?, list~ : ShapeId?, operations~ : Array[ShapeId], collectionOperations~ : Array[ShapeId], resources~ : Array[ShapeId])
    } derive(Eq,
    Debug
    )

    ShapeBody::keyword

    fn ShapeBody::keyword(self : ShapeBody) -> String

    ShapeId

    pub struct ShapeId {
    namespace : String
    name : String
    member : String?
    } derive(Eq, Hash,
    Debug
    )

    A validated absolute semantic shape ID. Relative IDs belong to the IDL representation and are resolved before they enter the model.
    impl Show for ShapeId

    ShapeId::parse

    fn ShapeId::parse(text : String) -> ShapeId?

    ShapeId::root

    fn ShapeId::root(self : ShapeId) -> ShapeId

    ShapeId::to_string

    fn ShapeId::to_string(self : ShapeId) -> String

    ShapeId::with_member

    fn ShapeId::with_member(self : ShapeId, name : String) -> ShapeId?

    ShapeProvenance

    pub(all) struct ShapeProvenance {
    introduced_traits : Map[ShapeId, NodeValue]
    inherited_traits : Map[ShapeId, NodeValue]
    introduced_fields : Map[String, NodeValue]
    inherited_fields : Map[String, NodeValue]
    legacy_set : Bool
    } derive(Eq,
    Debug
    )

    Source declarations and the inherited baseline of an assembled shape. Effective traits and members stay on Shape; provenance distinguishes local overrides, including overrides equal to the inherited value.

    SourceLocation

    pub(all) struct SourceLocation {
    file : String
    path : String
    span :
    Span
    ?
    } derive(Eq,
    Debug
    )

    A shape's source in the assembled input. JSON paths use JSON Pointer; IDL locations retain the parser's UTF-16 span.

    ValidationEvent

    pub(all) struct ValidationEvent {
    id : String
    severity : String
    shape : ShapeId?
    message : String
    source : SourceLocation?
    suppression_reason : String?
    } derive(Eq,
    Debug
    )

    A Smithy validation finding after configured severity and suppression rules.

    SMITHY_VERSION

    let SMITHY_VERSION : String

    The version of the Smithy specification that this library targets.

    diagnostic_codes

    fn diagnostic_codes() -> Array[(String, String)]

    The assembly diagnostic codes. Parser diagnostics keep their syntax codes.

    suppression_matches

    fn suppression_matches(rule_id : String, event_id : String) -> Bool

    Event IDs form a dot-delimited hierarchy.

    version

    fn version() -> String

    The version of this library.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io