Introduction

The Atelier project is a suite of Rust crates that provides the ability to read, write, and process AWS Smithy interface definition models. AWS is using Smithy extensively to define their services and to generate client and server implementations.

$version: "1.0"

namespace example.motd

@documentation("Provides a Message of the day.")
service MessageOfTheDay {
   version: "2020-06-21"
   resources: [
      Message
   ]
}

resource Message {
   identifiers: {
      date: Date
   }
   read: GetMessage
}

@readonly
operation GetMessage {
   input: GetMessageInput
   output: GetMessageInput
}

@pattern("^\\d\\d\\d\\d\\-\\d\\d\\-\\d\\d$")
string Date

structure GetMessageInput {
   date: example.motd#Date
}

structure GetMessageOutput {
   @required
   message: String
}

The goal of the Atelier project is to provide both Rust-native crates that allow parsing and emitting of Smithy models but also a clean-slate implementation of the Smithy specifications. This aspect has been useful, addressing ambiguities in the published documentation.

After a more detailed description of Smithy itself, and a tour of the Atelier crates this book will cover the following topics:

  • How to programmatically create and manipulate in-memory models.
  • How to read and write Smithy model files, including assembling in-memory models from multiple source files.
  • How to use the lint and validate framework to check models.
  • How to run the cargo-atelier tool to perform many of these actions from the command-line.
  • How to extend the Atelier provided tools.

Smithy Overview

Smithy is effectively a framework consisting of a semantic model, a custom IDL language, a mapping to/from JSON, and a build process. The semantic model is therefore consistent across different representations allowing different representations to be used for human readability as well as machine/tool usage.

Smithy is an interface definition language and set of tools that allows developers to build clients and servers in multiple languages. Smithy models define a service as a collection of resources, operations, and shapes. A Smithy model enables API providers to generate clients and servers in various programming languages, API documentation, test automation, and example code.

Figures in this section use a combined ER notation to denote cardinality alongside UML inheritance relationships as needed.

Framework

The following figure demonstrates the framework elements and their relations.

Smithy Conceptual Model

1.1: Smithy Conceptual Model
  • Semantic Model; is the in-memory model used by tool. The semantic model has no file or format details associated with it, it may be serialized using a representation into a model file.
  • Representation; is a particular model file format such as the Smithy IDL or JSON AST.
  • Mapping; is a set of rules that allow for reading and writing a representation. Some representations may not provide a meaningful mapping for read or write.
  • Artifact; typically a file on the file system, in a particular representation. A single model is serialized into one, or more, artifacts potentially with different representations.

The build process takes multiple model files, validates them, and combines them into a single instance of the semantic model. The process may also perform model transformations to add or remove shapes and metadata before any final artifact generation.

Artifacts may represent different parts of the IDL for a given application but also their dependencies thus enabling the sharing of common shapes. Most tools will only ever deal with the semantic model interfaces with serialization and deserialization simply handled by representation mappings.

Transformations

The build process mentioned above takes advantage of a number of transformations and while the term process is useful in the build sense the following transformation terms are more specific.

  • model-to-model; the act of creating one output model from one or more input model, such as a projection to select only certain shapes from the input model(s).
  • model-to-artifact; the act of creating model files, or generating code or infrastructure artifacts.
  • artifact-to-model; the act of creating a model from one or more model files or other artifacts such as an OpenAPI file.

The following figure shows that a transform has to have one or more models and may have zero or more artifacts.

Transformations

1.2: Transformations

The Semantic Model

The semantic model is a specified in-memory model used by tools. It consists of a set of shapes that corresponds to the data and behavioral elements of a service.

The Semantic Model

1.3: The Semantic Model
  • Semantic Model; a container of shapes and optional metadata.
  • Shape; a defined thing, shapes are either simple, aggregate or service types as described below.
  • Applied Trait; traits are a meta feature that allows for values to be associated with shapes. Tools may use applied traits for additional validation and code generation.
  • ShapeID; the identifier for all shapes defined in a model, all members of defined shapes, and the names of all traits.

Shape IDs

The shape identifier is a key element of the semantic model and representations. From the Smithy spec:

A shape identifier is used to identify and connect shapes. Every shape in the model is assigned a shape ID which can be referenced by other parts of the model like member targets, resource bindings, operation bindings, and traits. All shape identifiers in the semantic model must be absolute identifiers (that is, they include a shape name and namespace).

Shape IDs

1.4: Shape IDs

Shape ID has three query methods, is_absolute is true if the id has a namespace; is_relative is true if the id does not have a namespace; and is_member returns true if the id has a member name. It also has four conversion methods, to_absolute returns a new id with the shape name and any member name intact but with the provided namespace; to_relative returns a new id with the shape name and any member name intact but any previous namespace is removed; to_member returns a new id with the namespace and any shape name intact but with the provided member name; and to_shape returns a new id with the namespace and any shape name intact but any previous member name is removed.

Shapes

Shapes come in three kinds; simple, aggregate, and service. A simple shape is the type for an atomic or primitive value such as integer or string. Aggregate shapes have members such as a list of strings or an address structure. Service shapes have specific semantics, unlike the very generic simple and aggregate shapes, as they represent either a service, a resource managed by a service, or operations on services and resources.

Shapes

1.5: Shapes

Note that the inheritance relationships in this model are not necessary to implement the semantic model semantics but do make it more understandable.

Members

The aggregate types list, set, map, structure, and union all reference a Member type. This type is shown below, but basically it allows for an identifier as well as a set of traits to be applied to the aggregate type’s components.

Members

1.6: Members

Traits

Traits in the Smithy IDL look very much like Java annotations, and fulfill a similar role; In the Java computer programming language, an annotation is a form of syntactic metadata that can be added to Java source code.Wikipedia. However, in Java and other programming languages that support annotations these must be added to the declaration of the source element. In contrast, Smithy allows traits to be applied to a shape in a different artifact or different model entirely.

The term applied trait refers to the usage of a trait either directly or indirectly applied to a shape or member. A trait declaration is simply a simple or aggregate shape declaration with the meta-trait trait applied to it.

Values

There are a few places in the semantic model where data values are used, and the following demonstrates the values supported by the model.

Data Values

1.7: Data Values
  • metadata; every Model has an optional metadata Object which is often used to store tool or process specific values.
  • node-value; a trait application has values for the structure that defines the trait itself.
  • members; the aggregate values, Array and Object, contain Value members.

Note that there is no value type for ShapeID, these are stored in the semantic model as simply strings, their validation happens before the creation of the model and are treated only as opaque values by the model itself.

Crate structure

The following figure shows the current set of crates and their dependencies. For most tools it is easiest to simply rely on the lib crate in the same way the cargo crate does in the figure below.

Crates

1.8: Crates
  • core; contains the core model and framework elements such as the ModelReader, ModelWriter traits, and Action, Linter, and Validator traits.
  • assembler; provides the ability to load multiple files, in supported representations, and merge into a single semantic model. The ModelAssembler, along with the FileTypeRegistry and SearchPath are intended to support tools that process models, not just files.
  • smithy; contains implementations of the ModelReader and ModelWriter traits for the Smithy IDL representation.
  • json; contains implementations of the ModelReader and ModelWriter traits for the JSON AST representation.
  • query; will contain the implementation of selector expressions as queries.
  • describe; contains an implementation of the ModelWriter traits that generates formatted documentation.
  • rdf; contains an implementation of the ModelWriter traits for an RDF representation.
  • openapi; will contain the transformation to open API.
  • lib; a combined, single dependency, crate for clients that want to use a lot of the Atelier functionality.
  • cargo; the cargo command for Smithy file processing.

Creating Models

Semantic models can be created using a few different techniques:

  1. The Model API directly
  2. The Model Builder API to
  3. Model file operations, described later.

Using the Model API

The following example demonstrates the core model API to create a model for a simple service. The service, MessageOfTheDay has a single resource Message. The resource has an identifier for the date, but the read operation does not make the date member required and so will return the message for the current date.

This API acts as a set of generic data objects and as such has a tendency to be verbose in the construction of models. The need to create a lot of Identifier and ShapeID instances, for example, does impact the readability. It is important to note, that while the Smithy specification describes both absolute and relative shape identifiers, relative identifiers are not supported in the semantic model. All names in the semantic model must be resolved to an absolute name.


#![allow(unused_variables)]
fn main() {
use atelier_core::model::identity::{HasIdentity, Identifier};
use atelier_core::model::shapes::{
    HasTraits, MemberShape, Operation, Resource, Service, Shape,
    ShapeKind, Simple, StructureOrUnion, TopLevelShape,
};
use atelier_core::model::values::Value;
use atelier_core::model::{Model, NamespaceID};
use atelier_core::prelude::PRELUDE_NAMESPACE;
use atelier_core::Version;

let prelude: NamespaceID = PRELUDE_NAMESPACE.parse().unwrap();
let namespace: NamespaceID = "example.motd".parse().unwrap();

// ----------------------------------------------------------------------------------------
let mut date = TopLevelShape::new(
    namespace.make_shape("Date".parse().unwrap()),
    ShapeKind::Simple(Simple::String),
);
date
    .apply_with_value(
        prelude.make_shape("pattern".parse().unwrap()),
        Value::String(r"^\d\d\d\d\-\d\d-\d\d$".to_string()).into()
    )
    .unwrap();

// ----------------------------------------------------------------------------------------
let shape_name = namespace.make_shape("BadDateValue".parse().unwrap());
let mut body = StructureOrUnion::new();
body.add_member(
    "errorMessage".parse().unwrap(),
    prelude.make_shape("String".parse().unwrap()),
);
let mut error = TopLevelShape::new(shape_name, ShapeKind::Structure(body));
error
    .apply_with_value(
        prelude.make_shape("error".parse().unwrap()),
        Some("client".to_string().into()),
    )
    .unwrap();

// ----------------------------------------------------------------------------------------
let shape_name = namespace.make_shape("GetMessageOutput".parse().unwrap());
let mut output = StructureOrUnion::new();
let mut message = MemberShape::new(
    "message".parse().unwrap(),
    prelude.make_shape("String".parse().unwrap()),
);
message
    .apply(prelude.make_shape("required".parse().unwrap()))
    .unwrap();
let _ = output.add_a_member(message);
let output = TopLevelShape::new(
    namespace.make_shape("GetMessageOutput".parse().unwrap()),
    ShapeKind::Structure(output),
);

// ----------------------------------------------------------------------------------------
let shape_name = namespace.make_shape("GetMessageInput".parse().unwrap());
let mut input = StructureOrUnion::new();
input.add_member(
    "date".parse().unwrap(),
    date.id().clone(),
);
let input = TopLevelShape::new(
    namespace.make_shape("GetMessageInput".parse().unwrap()),
    ShapeKind::Structure(input),
);

// ----------------------------------------------------------------------------------------
let mut get_message = Operation::default();
get_message.set_input_shape(&input);
get_message.set_output_shape(&output);
get_message.add_error_shape(&error);
let mut get_message = TopLevelShape::new(
    namespace.make_shape("GetMessage".parse().unwrap()),
    ShapeKind::Operation(get_message),
);
get_message
    .apply(prelude.make_shape("readonly".parse().unwrap()))
    .unwrap();

// ----------------------------------------------------------------------------------------
let mut message = Resource::default();
message.add_identifier(Identifier::new_unchecked("date"), date.id().clone());
message.set_read_operation_shape(&get_message);
let message = TopLevelShape::new(
    namespace.make_shape("Message".parse().unwrap()),
    ShapeKind::Resource(message),
);

// ----------------------------------------------------------------------------------------
let mut service = Service::new("2020-06-21");
service.add_resource_shape(&message);
let mut service = TopLevelShape::new(
    namespace.make_shape("MessageOfTheDay".parse().unwrap()),
    ShapeKind::Service(service),
);
service
    .apply_with_value(
        prelude.make_shape("documentation".parse().unwrap()),
        Value::String("Provides a Message of the day.".to_string()).into(),
    )
    .unwrap();

// ----------------------------------------------------------------------------------------
let mut model = Model::new(Version::V10);
model.add_shape(message);
model.add_shape(date);
model.add_shape(get_message);
model.add_shape(input);
model.add_shape(output);
model.add_shape(error);

println!("{:#?}", model);
}

Using the Builder API

The following example demonstrates the builder interface to create the same service as the example above. Hopefully this is more readable as it tends to be less repetative, uses &str for identifiers, and includes helper functions for common traits for example. It provides this better construction experience (there are no read methods on builder objects) by compromising two aspects:

  1. The API itself is very repetative; this means the same method may be on multiple objects, but makes it easier to use. For example, you want to add the documentation trait to a shape, so you can:
    1. construct a Trait entity using the core model and the Builder::add_trait method,
    2. use the TraitBuilder::documentation method which also takes the string to use as the trait value and returns a new TraitBuilder, or
    3. use the Builder::documentation method that hides all the details of a trait and just takes a string.
  2. It hides a lot of the Identifier and ShapeID construction and so any of those calls to from_str may fail when the code unwraps the result. This means the builder can panic in ways the core model does not.

#![allow(unused_variables)]
fn main() {
use atelier_core::builder::traits::ErrorSource;
use atelier_core::builder::values::{ArrayBuilder, ObjectBuilder};
use atelier_core::builder::{
    traits, ListBuilder, MemberBuilder, ModelBuilder, OperationBuilder, ResourceBuilder,
    ServiceBuilder, ShapeTraits, SimpleShapeBuilder, StructureBuilder, TraitBuilder,
};
use atelier_core::model::{Identifier, Model, ShapeID};
use atelier_core::Version;
use std::convert::TryInto;

let model: Model = ModelBuilder::new(Version::V10, "example.motd")
    .service(
        ServiceBuilder::new("MessageOfTheDay", "2020-06-21")
            .documentation("Provides a Message of the day.")
            .resource("Message")
            .into(),
    )
    .resource(
        ResourceBuilder::new("Message")
            .identifier("date", "Date")
            .read("GetMessage")
            .into(),
    )
    .simple_shape(
        SimpleShapeBuilder::string("Date")
            .apply_trait(traits::pattern(r"^\d\d\d\d\-\d\d-\d\d$"))
            .into(),
    )
    .operation(
        OperationBuilder::new("GetMessage")
            .readonly()
            .input("GetMessageInput")
            .output("GetMessageOutput")
            .error("BadDateValue")
            .into(),
    )
    .structure(
        StructureBuilder::new("GetMessageInput")
            .member("date", "Date")
            .into(),
    )
    .structure(
        StructureBuilder::new("GetMessageOutput")
            .add_member(MemberBuilder::string("message").required().into())
            .into(),
    )
    .structure(
        StructureBuilder::new("BadDateValue")
            .error_source(ErrorSource::Client)
            .add_member(MemberBuilder::string("errorMessage").required().into())
            .into(),
    )
    .try_into().unwrap();
}

Model Visitor

Reading and Writing Models

Model Files

Smithy Representation

JSON Representation

RDF Representation

Model Reader


#![allow(unused_variables)]
fn main() {
const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR");

fn test_file_parses(file_name: &str) {
    let mut path = PathBuf::from_str(MANIFEST_DIR).unwrap();
    path.push(format!("tests/good/{}.smithy", file_name));
    println!("{:?}", path);
    let mut file = File::open(path).unwrap();
    let mut content: Vec<u8> = Vec::default();
    let _ = file.read_to_end(&mut content).unwrap();

    let mut reader = SmithyReader::default();
    let result = read_model_from_string(&mut reader, content);
    let trait_trait = ShapeID::from_str("smithy.api#trait").unwrap();
    match result {
        Ok(parsed) => {
            let mut names = parsed
                .shapes()
                .map(|shape| {
                    format!(
                        "{:<32} -> {}{}",
                        shape.id(),
                        if shape.has_trait(&trait_trait) {
                            "trait "
                        } else {
                            ""
                        },
                        match shape.body() {
                            ShapeKind::Simple(v) => v.to_string(),
                            ShapeKind::List(_) => SHAPE_LIST.to_string(),
                            ShapeKind::Set(_) => SHAPE_SET.to_string(),
                            ShapeKind::Map(_) => SHAPE_MAP.to_string(),
                            ShapeKind::Structure(_) => SHAPE_STRUCTURE.to_string(),
                            ShapeKind::Union(_) => SHAPE_UNION.to_string(),
                            ShapeKind::Service(_) => SHAPE_SERVICE.to_string(),
                            ShapeKind::Operation(_) => SHAPE_OPERATION.to_string(),
                            ShapeKind::Resource(_) => SHAPE_RESOURCE.to_string(),
                            ShapeKind::Unresolved => SHAPE_APPLY.to_string(),
                        }
                    )
                })
                .collect::<Vec<String>>();
            names.sort();
            print!("{:#?}", names)
        }
        Err(err) => panic!(err.to_string()),
    }
}
}

Model Writer

Model Assembly

The model assembler is used to create a single semantic model from a set of model artifacts. The assembler uses a file type registry to identify different representations and how to load them. Additionally, the assembler will process paths specified in a typical search path environment variable.

Examples

The following is the simple, and most common, method of using the assembler. This uses the default FileTypeRegistry and will search for all models in the set of paths specified in the environment variable “SMITHY_PATH“.


#![allow(unused_variables)]
fn main() {
use atelier_assembler::ModelAssembler;
use atelier_core::error::Result;
use atelier_core::model::Model;
use std::convert::TryFrom;

let env_assembler = ModelAssembler::default();

let model: Result<Model> = Model::try_from(env_assembler);
}

The next example turns off the search path handling entirely (the None passed to the new function) and then adds a single directory to the assembler.


#![allow(unused_variables)]
fn main() {
use atelier_assembler::{FileTypeRegistry, ModelAssembler};
use atelier_core::error::Result;
use atelier_core::model::Model;
use std::convert::TryFrom;

let mut assembler = ModelAssembler::new(FileTypeRegistry::default(), None);

assembler.push_str("tests/good");

let model: Result<Model> = Model::try_from(assembler);
}

Checking Models

The following example is taken from the Smithy specification discussing relative name resolution. The run_validation_actions function is commonly used to take a list of actions to be performed on the model in sequence.


#![allow(unused_variables)]
fn main() {
use atelier_core::action::validate::{
    run_validation_actions, CorrectTypeReferences
};
use atelier_core::action::Validator;
use atelier_core::builder::{
    ModelBuilder, ShapeTraits, SimpleShapeBuilder, StructureBuilder, TraitBuilder
};
use atelier_core::model::Model;
use atelier_core::Version;

let model: Model = ModelBuilder::new(Version::V10, "smithy.example")
    .uses("foo.baz#Bar")
    .structure(
        StructureBuilder::new("MyStructure")
            .member("a", "MyString")
            .member("b", "smithy.example#MyString")
            .member("d", "foo.baz#Bar")
            .member("e", "foo.baz#MyString")
            .member("f", "String")
            .member("g", "MyBoolean")
            .apply_trait(TraitBuilder::new("documentation"))
            .into(),
    )
    .simple_shape(SimpleShapeBuilder::string("MyString"))
    .simple_shape(SimpleShapeBuilder::boolean("MyBoolean"))
    .into();
let result = run_validation_actions(&mut [
        Box::new(CorrectTypeReferences::default()),
    ], &model, false);
}

This will result in the following list of validation errors. Note that the error is denoted against shape or member identifier accordingly.

[
    ActionIssue {
        reporter: "CorrectTypeReferences",
        level: Info,
        message: "The simple shape (smithy.example#MyBoolean) is simply a synonym, did you mean to add any constraint traits?",
        locus: Some(
            ShapeID {
                namespace: NamespaceID(
                    "smithy.example",
                ),
                shape_name: Identifier(
                    "MyBoolean",
                ),
                member_name: None,
            },
        ),
    },
    ActionIssue {
        reporter: "CorrectTypeReferences",
        level: Info,
        message: "The simple shape (smithy.example#MyString) is simply a synonym, did you mean to add any constraint traits?",
        locus: Some(
            ShapeID {
                namespace: NamespaceID(
                    "smithy.example",
                ),
                shape_name: Identifier(
                    "MyString",
                ),
                member_name: None,
            },
        ),
    },
    ActionIssue {
        reporter: "CorrectTypeReferences",
        level: Warning,
        message: "Structure member's type (foo.baz#MyString) cannot be resolved to a shape in this model.",
        locus: Some(
            ShapeID {
                namespace: NamespaceID(
                    "smithy.example",
                ),
                shape_name: Identifier(
                    "MyStructure",
                ),
                member_name: Some(
                    Identifier(
                        "e",
                    ),
                ),
            },
        ),
    },
]

Cargo Integration

The cargo_atelier crate provides a cargo sub-command for processing Smithy files, and is installed in the usual manner.

> cargo install cargo_atelier

To ensure this installed correctly, you can check the help.

> cargo atelier --help
cargo-atelier 0.2.7
Tools for the Smithy IDL.

USAGE:
    cargo-atelier [FLAGS] <SUBCOMMAND>

FLAGS:
    -h, --help        Prints help information
    -n, --no-color    Turn off color in the output
    -V, --version     Prints version information
    -v, --verbose     The level of logging to perform; from off to trace

SUBCOMMANDS:
    convert     Convert model from one representation to another
    help        Prints this message or the help of the given subcommand(s)
    lint        Run standard linter rules on a model file
    validate    Run standard validators on a model file

Both the lint and validate commands use a common mechanism for printing results and will by default print using a colorized output. As different linter and validation rules can be used the reported by row informs you which rule-set has determined the error.

Linter example

For the following badly formatted Smithy file, in test-models/lint-test.smithy.

namespace org.example.smithy

@ThisIsNotAGoodName
structure thisIsMyStructure {
    lower: String,
    Upper: String,
    someJSONThing: someUnknownShape,
    OK: Boolean
}

string someUnknownShape

@trait
structure ThisIsNotAGoodName {}

The following issues will be output when the linter runs.

> cargo atelier lint -i test-models/lint-test.smithy

[info] Shape names should conform to UpperCamelCase, i.e. ThisIsMyStructure
	Reported by NamingConventions on/for element `thisIsMyStructure`.

[info] Trait names should conform to lowerCamelCase, i.e. thisIsNotAGoodName
	Reported by NamingConventions on/for element `ThisIsNotAGoodName`.

[info] Member names should conform to lowerCamelCase, i.e. ok
	Reported by NamingConventions on/for element `thisIsMyStructure$OK`.

[info] Member name 'OK' appears to contain a known acronym, consider renaming i.e. ok
	Reported by NamingConventions on/for element `thisIsMyStructure`.

[info] Member names should conform to lowerCamelCase, i.e. someJsonThing
	Reported by NamingConventions on/for element `thisIsMyStructure$someJSONThing`.

[info] Member name 'someJSONThing' appears to contain a known acronym, consider renaming i.e. Json
	Reported by NamingConventions on/for element `thisIsMyStructure`.

[info] Shape names should conform to UpperCamelCase, i.e. SomeUnknownShape
	Reported by NamingConventions on/for element `someUnknownShape`.

[info] Member names should conform to lowerCamelCase, i.e. upper
	Reported by NamingConventions on/for element `thisIsMyStructure$Upper`.

Validation example

For the following erroneous Smithy file, in test-models/validation-test.smithy.

namespace org.example.smithy

structure MyStructure {
    known: String,
    wrongType: SomeOperation,
}

operation SomeOperation {
    input: SomeService
}

service SomeService {
    version: "1.0",
    operations: [MyStructure]
}

The following issues will be output when the validation runs.

> cargo atelier validate -i test-models/validation-test.smithy

[error] Structure member may not refer to a service, operation, resource or apply.
	Reported by CorrectTypeReferences on/for element `MyStructure$wrongType`.

[warning] Structure member's type (smithy.api#NotString) cannot be resolved to a shape in this model.
	Reported by CorrectTypeReferences on/for element `MyStructure$unknown`.

[error] Service operation must be an operation.
	Reported by CorrectTypeReferences on/for element `SomeService`.

[error] Operation input may not refer to a service, operation, resource or apply.
	Reported by CorrectTypeReferences on/for element `SomeOperation`.

Parameters

Common parameters that may be included with any command.

  • -V, --version; prints version information (and exits).
  • -h, --help; prints help information (and exits).
  • -v, --verbose; turn on more logging, the more times you add the parameter the more logging you get.
  • --no-color; turn off color support.

The following parameters are supported for all file input. File input uses the atelier_assembler crate to read multiple files and support multiple file representations. By default, the model assembler does not use a search path to load files. However, this can be changed with either the -d flag which will load any files found in the search path in the environment variable $SMITHY_PATH. Alternatively the -s parameter provides the name of an environment variable to use instead of $SMITHY_PATH.

  • -d, --default-search-env; if set, the standard SMITHY_PATH environment variable will be used as a search path.
  • -i, --in-file <in-file>;the name of a file to read, multiple files can be specified.
  • -s, --search-env <search-env>; the name of an environment variable to use as a search path.

The following will process all files in the default environment variable, with local files prepended to the search path.

> export SMITHY_PATH=./src/models:$SMITHY_PATH
> cargo atelier validate -d

The above can also be accomplished using -d and -i together.

> cargo atelier validate -d -i ./src/models

The following parameters are supported for all file output.

  • -n, --namespace <namespace>;a namespace to write, if the output format requires one.
  • -o, --out-file <out-file>; the name of a file to write to or stdout.
  • -w, --write-format <write-format>; the representation of the output file, the default is dependent on the command.

Extending Atelier

The core Atelier functionality contains a number of extension points, usually in the way of traits that can be implemented by other clients.

  1. Adding an Artifact Representation allows the reading and/or writing of different file formats.
  2. Adding a Linter allows for the creation of custom lint rules.
  3. Adding a Validator allows for the creation of custom validation rules.
  4. Adding a Model Transformation allows for the creation of model-to-model transformations.

Unlike the Java implementation Rust does not have a dynamic discovery mechanism, so additional linters and validators are not automatically added at runtime. This means that custom implementations cannot be used without additional work in existing tools such as cargo-atelier.

Adding an Artifact Representation

Model Writer

The example below is pretty much the implementation of the atelier_core::io::debug module, it writes the model using the Debug implementation associated with those objects.


#![allow(unused_variables)]
fn main() {
use atelier_core::io::ModelWriter;
use atelier_core::model::Model;
use atelier_core::error::Result as ModelResult;
use std::io::Write;

#[derive(Debug)]
pub struct FooWriter {}

impl Default for FooWriter {
    fn default() -> Self {
        Self {}
    }
}

impl ModelWriter for FooWriter {
    fn write(&mut self, w: &mut impl Write, model: &Model) -> ModelResult<()> {
        todo!()
    }
}
}

Add transform function


#![allow(unused_variables)]
fn main() {
pub fn model_to_foo(source: &Model) -> Result<Foo> {
    todo!()
}
}

#![allow(unused_variables)]
fn main() {
impl ModelWriter for FooWriter {
    fn write(&mut self, w: &mut impl Write, model: &Model) -> ModelResult<()> {
        let foo = model_to_foo(model)?;
        write!(w, "{}", foo)?;
        Ok(())
    }
}
}

Model Reader


#![allow(unused_variables)]
fn main() {
use atelier_core::io::ModelReader;
use atelier_core::model::Model;
use atelier_core::error::Result as ModelResult;
use std::io::Write;

#[derive(Debug)]
pub struct FooReader {}

impl Default for FooReader {
    fn default() -> Self {
        Self {}
    }
}

impl ModelReader for FooReader {
    fn read(&mut self, r: &mut impl Read) -> ModelResult<Model> {
        todo!()
    }
}
}

Add transform function


#![allow(unused_variables)]
fn main() {
pub fn pub fn parse_model(r: &mut impl Read) -> ModelResult<Model> {
    todo!()
}
}

#![allow(unused_variables)]
fn main() {
impl ModelReader for FooReader {
    fn read(&mut self, r: &mut impl Read) -> ModelResult<Model> {
        parse_model(r)
    }
}
}

Adding a Linter

Adding a Validator

Adding a Model Transformation

Java & Rust Implementation Differences

The following, for reference, is a combined view of the semantic model from the Smithy Overview. Clearly this view is language-neutral and is unlikely to produce an idiomatic interface if implemented as-is.

Semantic Model

A.1: The Combined Semantic Model

The Java Model

The Java model includes a number of additional abstract classes to take advantage of implementation inheritance provided by the Java object model.

Java Implementation

A.2: Java Implementation

Points of interest:

  1. MemberShape and all the model shapes share a common Shape base class.
  2. The ShapeType and NodeType enumerations allow for type determination between shapes and nodes at runtime.
  3. The use of Java’s Number class allows for a wide range of concrete numeric types.

The Rust model

The corresponding Rust model on the other hand makes little use of inheritance, except for a few traits, but makes use of enumerations instead.

Rust Implementation

A.3: Rust Implementation

Points of interest:

  1. Node has been renamed as Value, a more approachable and less generic term.
  2. TopLevelShape has been introduced as the container type for all non-member shape types.
  3. Neither TopLevelShape, or MemberShape, share a common parent type but do implement the common traits HasIdentity, HasTraits, and NonTraitEq.
  4. The use of Rust enumerations for ShapeKind, Simple, and Value allow for run-time type determination without explicit “Type” values.
  5. The Service::rename and Resource::identifiers values map between ShapeID and Identifier rather than the ShapeID and String used in Java.
  6. Nullable values translate to Rust Option type.
  7. Java List containers translate to Rust Vec containers.
  8. Java Set containers translate to Rust HashSet containers.
  9. Java Map containers translate to Rust HashMap containers.

Traits

The Java implementation uses Java service discovery to describe traits as Java classes and apply them to models. There isn’t a direct comparison in Rust to the Java service framework and so traits have to be dealt with in a more static manner (details TBD).

Model Operations

The Java implementation incorporates a number of operations, such as builders, serialization and validation, into the core model which is common for Java. Rust on the other hand tends toward smaller units of packaging and therefore more decoupling of these operations from the model itself.

The core crate provides the following modules which are further extended in separate crates.

  • action; the Action, Linter, Validator, and Transformer traits used to perform the corresponding operations on models. The module also provides some basic lint, transform, and validate implementations.
  • builder; a set of builder types providing a more fluent style for model construction.
  • io; the ModelReader and ModelWriter traits for implementing serialization of different representations.
  • model; just the types necessary to hold the in-memory semantic model.

Additionally, the following crates externalize operations.

  • assembler provides model discovery and merge.
  • json provides support for the JSON AST representation.
  • smithy provides support for the Smithy IDL representation.

Appendix: RDF Mapping

This appendix describes the mapping from Smithy to RDF in detail.

RDF Representation

The examples below are shown in the RDF Turtle syntax. The following namespace prefixes are used:

@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix smithy: <https://awslabs.github.io/smithy/rdf-1.0#> .
@prefix api: <urn:smithy:smithy.api:> .
  • rdf - the RDF namespace, used for certain type assertions.
  • xsd - XML Schema data types.
  • smithy - the namespace for the Smithy IDL mapping itself.
  • api - the namespace for the Smithy prelude shapes; this follows the rules in the following section to generate a URN for the Smithy namespace smithy.api.

Shape IDs

To allow for the linking of models in RDF the key identifier in and between models need to be represented as IRIs. This section introduces a Simple URN naming scheme for absolute Smithy shape identifiers.

While it is clear that a stable, unique identifier should be used in the same way as the Smithy Shape ID, it is not at all clear that this needs to carry any location information with it. It would be preferrable to use the Smithy trait system to associate locations with models rather than forcing location onto all models and model elements. The choice of a URN over URL scheme was therefore easier, and provides a clear, human-readable and easily parsed identifier format.

The following rules describe the mapping from Smithy Shape ID to a URN form required by the model and shape mapping.

  1. The URI scheme MUST be exactly urn.
  2. The URN scheme MUST be exactly smithy.
  3. The namespace-specific string (NSS) MUST be formatted as follows.
    1. The identifier’s namespace component.
    2. The colon character, ':'.
    3. The identifier’s shape name component.
    4. If the Shape ID represents a member shape:
      1. The forward slash character, '/'.
      2. The identifier’s member name component.

The following demonstrates this mapping visually.

           example.namespace#shape$member
           |---------------| |---| |----|
urn:smithy:example.namespace:shape/member

The following is a simplified form of the mapping described above.


#![allow(unused_variables)]
fn main() {
use atelier_core::model::ShapeID;

fn simple_shapeid_to_urn(shape_id: &ShapeID) -> String {
   format!(
      "urn:smithy:{}:{}{}",
      shape_id.namespace(),
      shape_id.shape_name(),
      if let Some(member_name) = shape_id.member_name() {
         format!("/{}", member_name)
      } else {
         String::new()
      }
   )
}
}

Models

  1. Each model is an RDF resource, it’s identifier may be an IRI or blank node.

  2. The model resource MUST have a property rdf:type with the IRI value smithy:Model.

  3. The model resource MUST have a property smithy:smithy_version with a literal string value representing the Smithy version used to define the model.

    [] 
     a smithy:Model ;
     smithy:smithy_version "1.0" .
    
  4. ForEach shape in the model the model resource MUST have a property, named smithy:metadata with a value which is the identifier of a top-level shape resource.

    [] 
      a smithy:Model ;
      smithy:smithy_version "1.0" ;
      smithy:shape <urn:smithy:example.motd:Date> .
    
  5. The model resource MAY have a property, named smithy:metadata that is treated as an Object value and generated according to the Value rules.

    [] 
      a smithy:Model ;
      smithy:smithy_version "1.0" ;
      smithy:shape <urn:smithy:example.motd:Date> ;
      smithy:metadata [
        a rdf:Bag
        rdf:_1 [
          smithy:key "domain" ;
          smithy:value "identity" ;
        ]
      ] .
    

Shapes

  1. Each top-level shape is an RDF resource, it’s identifier is the URN form of the shape’s Shape ID.
  2. The shape resource MUST have a property rdf:type that denotes it’s Smithy type.
  3. The shape resource MAY have applied traits, see later for details.
  4. Aggregate and Service shapes have members, these member shapes have a common set of rules.
  5. All top-level and member shape resource MAY have applied traits.

Member Shapes

  1. Each member shape is an RDF resource, it’s identifier is the URN form of the shape’s Shape ID.
  2. The shape resource MUST have a property rdf:type with a value which is the identifier of a top-level shape resource.
  3. The shape resource MUST have a property smithy:name with a value which is a literal string for the name of this member, this value must be a valid identifier.
motd:GetMessageInput
  a smithy:Structure ;
  smithy:member [
    a motd:Date ;
    smithy:name "date"
  ] .

Simple shapes

No additional rules.

<urn:smithy:example.motd:Date> a smithy:String .

List shapes

  1. The shape resource MUST have a property rdf:type with the IRI value smithy:List.
  2. The shape resource MUST have a member shape with the name “member”.
<urn:smithy:example.motd:Messages> 
  a smithy:List ;
  smithy:member [
    a smithy:String ;
    smithy:name "member"
  ] .

Set shapes

  1. The shape resource MUST have a property rdf:type with the IRI value smithy:Set.
  2. The shape resource MUST have a member shape with the name “member”.
<urn:smithy:example.motd:Messages> 
  a smithy:Set ;
  smithy:member [
    a smithy:String ;
    smithy:name "member"
  ] .

Map shapes

  1. The shape resource MUST have a property rdf:type with the IRI value smithy:Map.
  2. The shape resource MUST have a member shape with the name “key”.
  3. The shape resource MUST have a member shape with the name “value”.
<urn:smithy:example.motd:Messages> 
  a smithy:Map ;
  smithy:member [
    a <urn:smithy:example.motd:Language> ;
    smithy:name "key"
  ] ;
  smithy:member [
    a smithy:String ;
    smithy:name "value"
  ] .

Structure shapes

  1. The shape resource MUST have a property rdf:type with the IRI value smithy:Structure.
  2. The shape resource MAY have any number of member shapes.
<urn:smithy:example.motd:MessageResponse> 
  a smithy:Structure ;
  smithy:member [
    a smithy:String ;
    smithy:name "language"
  ] ;
  smithy:member [
    a smithy:String ;
    smithy:name "message"
  ] .

Union shapes

  1. The shape resource MUST have a property rdf:type with the IRI value smithy:Structure.
  2. The shape resource MAY have any number of member shapes.
<urn:smithy:example.motd:MessageResponse> 
  a smithy:Union ;
  smithy:member [
    a smithy:Integer ;
    smithy:name "messageCode"
  ] ;
  smithy:member [
    a smithy:String ;
    smithy:name "message"
] .

Operation shapes

  1. The shape resource MUST have a property rdf:type with the IRI value smithy:Operation.
  2. The shape resource MAY have a property, named smithy:input with a value which is the identifier of a top-level shape resource.
  3. The shape resource MAY have a property, named smithy:output with a value which is the identifier of a top-level shape resource.
  4. The shape resource MAY have any number of properties, named smithy:error with a value which is the identifier of a top-level shape resource.
<urn:smithy:example.motd:GetMessage>
  a smithy:Operation ;
  smithy:input <urn:smithy:example.motd:GetMessageRequest> ;
  smithy:output <urn:smithy:example.motd:GetMessageResponse> ;
  smithy:error <urn:smithy:example.motd:BadDateValue> .

Resource shapes

  1. The shape resource MUST have a property rdf:type with the IRI value smithy:Resource.
  2. The shape resource MAY have a property, named smithy:identifiers with a blank node value.
    1. This blank node MUST have a property rdf:type with the IRI value rdf:Bag.
    2. Each property of this blank node follows the standard method to generate predicate names of the form rdf:_{n} with a blank node value.
      1. This blank node MUST have a property smithy:key with a literal string value representing the identifier item’s key.
      2. This blank node MUST have a property smithy:target with a value which is the identifier of a top-level shape resource.
  3. The shape resource MAY have a property, named smithy:create with a value which is the identifier of a top-level shape resource.
  4. The shape resource MAY have a property, named smithy:put with a value which is the identifier of a top-level shape resource.
  5. The shape resource MAY have a property, named smithy:read with a value which is the identifier of a top-level shape resource.
  6. The shape resource MAY have a property, named smithy:update with a value which is the identifier of a top-level shape resource.
  7. The shape resource MAY have a property, named smithy:delete with a value which is the identifier of a top-level shape resource.
  8. The shape resource MAY have a property, named smithy:list with a value which is the identifier of a shape resource.
  9. The shape resource MAY have any number of properties, named smithy:operation with a value which is the identifier of a top-level shape resource.
  10. The shape resource MAY have any number of properties, named smithy:collectionOperation with a value which is the identifier of a top-level shape resource.
  11. The shape resource MAY have any number of properties, named smithy:resource with a value which is the identifier of a top-level shape resource.
weather:Forecast
  a smithy:Resource ;
  smithy:identifiers [
    a rdf:Bag ;
    rdf:_1 [
      smithy:key "forecastId" ;
      smithy:target weather:ForecastId
    ]
  ] ;
  smithy:read weather:GetForecast .

Service shapes

  1. The shape resource MUST have a property rdf:type with the IRI value smithy:Service.
  2. The shape resource MUST have a property smithy:version with a literal, non-empty, string value.
  3. The shape resource MAY have any number of properties, named smithy:operation with a value which is the identifier of a top-level shape resource.
  4. The shape resource MAY have any number of properties, named smithy:resource with a value which is the identifier of a top-level shape resource.
  5. The shape resource MAY have a property, named smithy:rename with a blank node value.
    1. This blank node MUST have a property rdf:type with the IRI value rdf:Bag.
    2. Each property of this blank node follows the standard method to generate predicate names of the form rdf:_{n} with a blank node value.
      1. This blank node MUST have a property smithy:shape with a value which is the identifier of a top-level shape resource.
      2. This blank node MUST have a property smithy:name with a literal string value.
example:MyService
  a smithy:Service ;
  smithy:version "2017-02-11" ;
  smithy:operations [
    a rdf:Bag ;
    rdf:_1 example:GetSomething
  ] ;
  smithy:rename [
    a rdf:Bag ;
    rdf:_1 [
      smithy:shape <urn:smithy:foo.example:Widget> ;
      smithy:name "FooWidget"
    ]
  ] .

Traits and Values

Traits

Any shape, either a top-level, or a member, may have traits applied and these are represented as follows.

  1. The shape resource MAY have any number of properties, named smithy:apply with a blank node value.
    1. This blank node MUST have a property smithy:trait with a value which is the identifier of a top-level shape resource.
    2. This blank node MAY have a property smithy:value representing the trait parameter value - see Value production rules later.

The following example shows traits applied to a top-level shape.

motd:BadDateValue
  a smithy:Structure ;
  smithy:apply [
    smithy:trait api:error ;
    smithy:value "client"
  ] .

The following example shows traits applied to a member shape.

<urn:smithy:example.motd:BadDateValue/errorMessage> 
  a smithy:Member ;
  smithy:target smithy:String ;
  smithy:apply [
    smithy:trait api:required
  ] .

Values

Values are are used in both the model metadata section,

smithy:metadata [
  a rdf:Bag
  rdf:_1 [
    smithy:key "domain" ;
    smithy:value "identity" ;
  ]
] .

as well as in passing parameters to applied traits.

smithy:apply [
  smithy:trait api:title ;
  smithy:value "My new thing"
] .

The following define the production rules for values in either of these cases.

Strings

String values MAY be represented as unqualified string literals OR as qualified strings with the data type xsd:string.

[] smithy:value "My new thing" .

[] smithy:value "My new thing"^^xsd:string .

Booleans

Boolean values MUST be represented as string literals with the type xsd:boolean.

[] smithy:value "true"^^xsd:boolean .

# alternatively, in Turtle:

[] smithy:value true .

Numbers

Number values MUST be represented as string literals with either the type xsd:signedLong or xsd:double.

[] smithy:value "1"^^xsd:signedLong .

[] smithy:value "3.14"^^xsd:double" .

Arrays

Array values MUST be represented as a new blank node.

  1. This node MUST have a property rdf:type with the IRI value rdf:Seq.
  2. Each property of this blank node follows the standard method to generate predicate names of the form rdf:_{n} with a value using these same production rules.
smithy:value [
  a rdf:Seq ;
  rdf:_1 "experimental" ;
  rdf:_2 "public"
]

Objects

Object values MUST be represented as a new blank node.

  1. This node MUST have a property rdf:type with the IRI value rdf:Bag.
  2. Each property of this blank node follows the standard method to generate predicate names of the formrdf:_{n} with a blank node value.
    1. This node MUST have a property smithy:key with a string literal for the identifier name.
    2. This node MUST have a property smithy:value with a value using these same production rules.
smithy:value [
 a rdf:Bag ;
 rdf:_1 [
   smithy:key "Homepage" ;
   smithy:value "https://www.example.com/" ;
 ] ;
 rdf:_1 [
   smithy:key "API Reference" ;
   smithy:value "https://www.example.com/api-ref" ;
 ] ;
]

Null

Smithy supports the notion of a null type, this is represented by the specific IRI smithy:null.

[] smithy:value smithy:null .

Example

Presented below is the Message of the Day Example with the corresponding Smithy and RDF sections side by side for comparison. You can download the Turtle source file as well.

Smithy IDL RDF Representation
1
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix smithy: <https://awslabs.github.io/smithy/rdf-1.0#> .
@prefix api: <urn:smithy:smithy.api:> .
@prefix : <urn:smithy:example.motd:> .
2
$version: "1.0"

namespace example.motd
[]
  a smithy:Model ;
  smithy:smithy_version "1.0" ;
  smithy:shape
    :GetMessageOutput ,
    :Message ,
    :GetMessageInput ,
    :BadDateValue ,
    :GetMessage ,
    :MessageOfTheDay ,
    :Date .
3
@pattern(
  "^\\d\\d\\d\\d\\-\\d\\d\\-\\d\\d$"
)
string Date
:Date
  a smithy:String ;
  smithy:apply [
    smithy:trait api:pattern ;
    smithy:value "^\\d\\d\\d\\d\\-\\d\\d-\\d\\d$"
  ] .
4
resource Message {
   identifiers: {
      date: Date
   }
   read: GetMessage
}
:Message
  a smithy:Resource ;
  smithy:identifiers [
    a rdf:Bag ;
    rdf:_1 [
      smithy:key "date" ;
      smithy:target :Date
    ]
  ] ;
  smithy:read :GetMessage .
5
structure GetMessageInput {
   date: Date
}
:GetMessageInput
  a smithy:Structure ;
  smithy:member [
    a :Date ;
    smithy:name "date"^^xsd:string
  ] .
6
structure GetMessageOutput {
   @required
   message: String
}
:GetMessageOutput
  a smithy:Structure ;
  smithy:member [
    a api:String ;
    smithy:name "message"^^xsd:string ;
    smithy:apply [ smithy:trait api:required ] ;
  ] .
7
@error("client")
structure BadDateValue {
   @required
   errorMessage: String
}
:BadDateValue
  a smithy:Structure ;
  smithy:apply [
    smithy:trait api:error ;
    smithy:value "client"
  ] ;
  smithy:member [
    a api:String ;
    smithy:name "errorMessage"^^xsd:string ;
    smithy:apply [ smithy:trait <urn:smithy:smithy.api:required> ] ;
  ] .
8
@readonly
operation GetMessage {
   input: GetMessageInput
   output: GetMessageInput
   errors: [ BadDateValue ]
}
:GetMessage
  a smithy:Operation ;
  smithy:input :GetMessageInput ;
  smithy:output :GetMessageOutput ;
  smithy:error :BadDateValue .
9
@documentation(
  "Provides a Message of the day."
)
service MessageOfTheDay {
   version: "2020-06-21"
   resources: [ Message ]
}
:MessageOfTheDay
  a smithy:Service ;
  smithy:apply [
    smithy:trait api:documentation ;
    smithy:value "Provides a Message of the day."
  ] ;
  smithy:version "2020-06-21" ;
  smithy:resource :Message .

Appendix: Testing

Rust has excellent test tools, patterns, and idioms, but where different crates are implementing common traits the tendency is to duplicate tests. To this end the test contains common examples with expected results that can be used by different crate implementations. To achieve this the crate leverages a specific write-only representation that is stable in it’s ordering and can be directly diffed between test runs.

The LineOrientedWriter

This representation is such that it is always ordered and has no whitespace or other ambiguities and so can be directly compared as a whole. This is valuable for testing but can also be extremely fast for parsing tools.

Example


#![allow(unused_variables)]
fn main() {
use atelier_core::io::ModelWriter;
use atelier_core::io::lines::LineOrientedWriter;
use atelier_core::model::Model;
fn make_model() -> Model { Model::default() }
let model = make_model();

let mut writer = LineOrientedWriter::default();
let result = writer.write(&mut std::io::stdout(), &model);
assert!(result.is_ok())
}

Representation Format

The following is a description of the production of the format.

segment-separator = "::" ;
target-operator = "=>" ;
value-assignment-operator = "<=" ;

The numbers on the left of the lines below are for reference within in the production rule text.

 1. {shape_type}::{shape_id}
 2. {shape_type}::{shape_id}::trait::{shape_id}
 3. {shape_type}::{shape_id}::trait::{shape_id}<={value...}
 4. {shape_type}::{shape_id}::{member_name}=>{target_shape_id}
 5. {shape_type}::{shape_id}::{member_name}::trait::{shape_id}
 6. {shape_type}::{shape_id}::{member_name}::trait::{shape_id}<={value...}
 7. resource::{shape_id}::identifier::{identifier}=>{shape_id}
 8. service::{shape_id}::rename::{shape_id}<={identifier}
 9. meta::{identifier}<={value...}
10. ()
11. {simple_value}
12. [{integer}]<={value...}
13. {{identifier}}<={value...}

Shape Production Rules

For each top-level shape:

  • emit the name of the shape’s type, a segment-separator, and the shape’s fully qualified name, and a newline (1).
  • For each trait applied to this shape:
    • append to the above string the value “::trait“, and the trait’s fully qualified name (2),
    • if the trait has a value, append the value-assignment-operator and follow the value production rules below (3).
    • finish with a newline.
  • For each member of the shape:
    • emit a line with the member identifier, the target-operator, and the target’s fully qualified name, and a newline (4),
      • for array-valued members the member name emitted is the singular form with a line per value (error for errors, etc.).
      • If the shape is a resource; emit the “identifiers“ map-valued member:
        • append an additional “::identifier::“ string,
        • emit each key followed by the target-operator, and the target’s fully qualified name, and a newline (7),
      • If the shape is a service; emit the “rename“ map-valued member:
        • append an additional “::rename::“ string,
        • emit each key (fully qualified shape ID), followed by the value-assignment-operator, and the value (identifier), and a newline (8),
    • For each trait applied to the member:
      • append to the above string the value “::trait“, and the trait’s fully qualified name (5),
      • if the trait has a value, append the value-assignment-operator and follow the value production rules below (6).
      • finish with a newline.

Metadata Production Rules

For each value in the model’s metadata map:

  • use the string “meta“ as if it where a shape name followed by the segment-separator.
  • append the key name, the value-assignment-operator and follow the value production rules below (9).

Value Production Rules

  • For null values simply emit the string "()" (10).
  • For boolean, numeric, and string values emit their natural form (11).
    • Ensure string values quote the characters '\n', '\r', and '"'.
  • For arrays:
    • emit a line per index, with '[', the index as a zero-based integer, ']', the value-assignment-operator and follow these same value production rules (12),
    • an empty array will be denoted by the string "[]".
  • For objects:
    • emit a line per key, with "{", the key name, "}", the value-assignment-operator and follow these same value production rules (13),
    • an empty object MUST be denoted by the string "{}".

Finally, all lines MUST be sorted to ensure the overall output can be compared.

Examples

A simple string shape with a trait applied.

// "pattern" is a trait.
@pattern("^[A-Za-z0-9 ]+$")
string CityId
string::example.weather#CityId
string::example.weather#CityId::trait::smithy.api#pattern<="^[A-Za-z0-9 ]+$"

An operation, note the rename of “errors” to “error as the member identifier.

@readonly
operation GetCity {
    input: GetCityInput
    output: GetCityOutput
    errors: [NoSuchResource]
}
operation::example.weather#GetCity
operation::example.weather#GetCity::error=>example.weather#NoSuchResource
operation::example.weather#GetCity::input=>example.weather#GetCityInput
operation::example.weather#GetCity::output=>example.weather#GetCityInput
operation::example.weather#GetCity::trait::smithy.api#readonly

A service, note the object-based trait “paginated”, and the comment that has been turned into a documentation trait.

/// Provides weather forecasts.
@paginated(inputToken: "nextToken", outputToken: "nextToken",
           pageSize: "pageSize")
service Weather {
    version: "2006-03-01"
    resources: [City]
    operations: [GetCurrentTime]
    rename: {
        "foo.example#Widget": "FooWidget"
    }
}
service::example.weather#Weather
service::example.weather#Weather::operation=>example.weather#GetCurrentTime
service::example.weather#Weather::resource=>example.weather#City
service::example.weather#Weather::rename::foo.example#Widget<=FooWidget
service::example.weather#Weather::trait::smithy.api#documentation<="Provides weather forecasts."
service::example.weather#Weather::trait::smithy.api#paginated<={inputToken}="nextToken"
service::example.weather#Weather::trait::smithy.api#paginated<={outputToken}="nextToken"
service::example.weather#Weather::trait::smithy.api#paginated<={pageSize}="pageSize"
service::example.weather#Weather::version<="2006-03-01"

The test Crate

The following example (from the smithy crate) shows how the common test crate is used to read a .smithy file, then serialize in the line-oriented form and compare to a pre-stored expected result.


#![allow(unused_variables)]
fn main() {
use atelier_smithy::SmithyReader;
use atelier_test::parse_and_compare_to_files;
use std::path::PathBuf;

const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR");

fn test_file_parses(file_name: &str) {
    let source_file = PathBuf::from(format!("{}/tests/good/{}.smithy", MANIFEST_DIR, file_name));
    let expected_file = PathBuf::from(format!("{}/tests/good/{}.lines", MANIFEST_DIR, file_name));
    let mut reader = SmithyReader::default();
    parse_and_compare_to_files(&mut reader, &source_file, &expected_file);
}

#[test]
fn test_weather_example() {
    test_file_parses("weather");
}
}

For more information, see the crate documentation.