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.Blob
, wrapper toArray[Byte]
orByteBuffer
) - 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: String
}
// 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 specialized services. Smithy4s only transitively transfers the operations defined in a resource
to the service that lists that resource
.