Skip to main content

The Smithy IDL

Smithy is a protocol agnostic definition language. It means that it is not tied to any transport or application protocol or serialisation mechanism, be that http, websockets, json, protobuf, etc.

In order to achieve this, whilst still being useful, smithy separates its core semantics from protocol-level concerns.

The core semantics contain means to concisely and simply express data models, as well as operations and services.

The protocol level semantics are provided by means of an annotation mechanism. Some annotations are provided with the smithy language out of the box, in a "standard library" called prelude, but users are free to define their own.

Annotations are called traits in smithy.

The smithy metamodel

In this section, we'll list various available shapes that let you define data and operations in smithy, and how they translate in the Scala code generated by Smithy4s.

Primitive types

Smithy provides the following "primitive" types out of the box.

  • Boolean
  • String
  • Integer
  • Long
  • Float
  • Short
  • Double
  • Byte
  • BigInteger
  • BigDecimal
  • Blob (smithy4s.ByteArray, wrapper to Array[Byte])
  • Timestamp (smithy4s.Timestamp, translated from/to java or javascript time types)
  • Document (smithy4s.Document, a bespoke Json ADT)

Named primitives

Smithy lets you define custom names for primitive types:

namespace foo

integer Age
long Identifier

These get translated as unboxed type wrappers, or newtypes, that look like a case class but do not induce any boxing at runtime.

Collection types

Smithy provides 3 different shapes of collections: lists, sets, and maps. They translate to the corresponding scala.collection types in the generated Scala code.

namespace foo

list IntList {
member: Integer
}

set StringSet {
member: Set
}

// At this time, only string shapes can be used as keys to map.
map AgeMap {
key: String
value: Integer
}

Enums

Smithy supports two types of enums, for string and integers :

enum FooBar {
FOO = "foo"
BAR = "bar"
}

intEnum FaceCard {
JACK = 1
QUEEN = 2
KING = 3
ACE = 4
JOKER = 5
}

Structures

Structures are product types. In Scala, they naturally translate to case classes.

namespace foo

structure Person {
@required
firstName: String
@required
lastName: String
dateOfBirth: Timestamp
}

Unions

Unions are coproduct types. In Scala, they quite naturally translate to sealed traits.

Union members can target any data shape, be it a structure or a primitive type.

namespace foo

structure Cat {
name: String
}

structure Dog {
name: String
}

union Animal {
cat: Cat
dog: Dog
}

Operations and services

Operations

Operations are essentially an optional Input, an optional Output, and an optional list of errors. Inputs, outputs and errors all have to be structure shapes.

namespace foo

operation Greet {
input: GreetInput
output: GreetOutput
errors: [BadInput]
}

structure GreetInput {
name: String
}

structure GreetOutput {
message: String
}

@error("client")
structure BadInput {
message: String
}

Errors

Regarding errors, smithy4s translates them as case classes extending Throwable.

The getMessage method of the throwable is implemented in terms of the following (based on the first match):

  • a field annotated with the @errorMessage trait
  • a field named message

Services

Services are basically a list of operations, and an optional list of errors.

namespace foo

service HelloService {
operations: [Greet]
errors: [ServerError]
}

@error("server")
structure ServerError {
message: String
}

Smithy4s translates them in the following fashion:

package object foo {

type HelloService[F[_]] = HelloServiceGen[???]

}

HelloService is type alias that exposes a normal "functor-shaped" type parameter: we are aware that the most common usecase of Smithy4s abides by the "capatibility trait" pattern (or tagless-final), against effect types that probably abide by the cats-effect semantics.

However, the actual interface is HelloServiceGen, which has a higher degree of polymorphism. It looks like this:

package foo

trait HelloServiceGen[P[_, _, _, _, _]]{

def greet(name: String) : P[GreetInput, Greet.Error, GreetOutput, Nothing, Nothing]

}

P represents an abstract context against which operations are going to run. The abstract context has 5 type parameters:

  • input
  • error
  • output
  • streamed input (Nothing, most of the time)
  • streamed output (Nothing, most of the time)

Keeping track of these parameters is really important for the implementation intepreters. It also opens the door for providing interpreters that work against bi-functors (EitherT[IO, *, *]) without changing the generated code.

Currently not supported (in particular)

Smithy has a resource type of shape, that represents CRUD specialised services. It is currently not supported in Smithy4s.