Skip to content

Getting Started

Marcel Joss edited this page Jun 15, 2023 · 17 revisions

Modeling Concepts

When getting started with SAMT, it's important to understand and embrace the core concepts which differentiate it from alternatives like OpenAPI or Protobuf.

At the core of SAMT is the separation of business requirements (using records and services) and technologies (using providers and consumers).

SAMT Language

SAMT uses a custom Domain-Specific-Language to describe it's model. We believe a new language is best learned by examples, which are added below every description text. Its basic syntax is inspired by Kotlin and other popular programming languages.

Every file must have exactly one package declaration, used to group records, services and the like together. You can then import other packages by using import statements.

Known Types

The following types can be used within SAMT:

  • Int: 32-bit whole number, signed
  • Long: 64-bit whole number, signed
  • Float: 32-bit floating point number, signed
  • Double: 64-bit floating point number, signed
  • Decimal: Arbitrary precision number, fixed amount of digits before and after decimal point
  • Boolean: Can be true or false
  • String: UTF-8 encoded text
  • Bytes: Arbitrary byte buffer
  • Date: Timezone-agnostic date value
  • DateTime: UTC timestamp, millisecond precision
  • Duration: Time duration, millisecond precision
  • List<T>: A list of elements
  • Map<K, V>: A map of key-value entries

Every type can be suffixed with a ? to denote that it's optional. For example, Int? can be a valid integer or null at runtime, meaning no value.

Constraints The following types can be used within SAMT:
  • range: limit number to min and max values
  • size: limit string, list or map to min and max size, where string size is defined as the number of unicode codepoints in a string
  • value: limit string, number or boolean to specific value
  • pattern: limit string to match regular expression

These constraints can be added to any type to denote business requirements. The following code snippet shows some examples on how such constraints would be used:

record ConstraintExample {
  // must not be empty and at most 100 characters long
  name: String (size(1..100))

  // must only contain lower-case letters 
  identifier: String (pattern("[a-z]+"))

  // If only one constraint is provided, the name can be omitted
  identifier2: String ("[a-z]+")

  // A 32-bit number which must be positive
  positiveNumber: Int (range(0..*))
}
Records

A record is a custom data type which consists of different fields:

record Email {
  from: String
  to: String
  cc: String?
  header: String (1..100)
  content: String (1..*)
  attachments: List<EmailAttachment>
}

record EmailAttachment {
  filename: String (size(1..256), pattern("[a-zA-Z]*"))
  content: Binary
}
Services & Operations

A service is a group of operations, where every operation is something callable:

record Email { /* ... */ }

service EmailService {
  // One-Way operation: fire and forget, don't wait
  oneway send(email: Email)

  // Regular operation: wait blocking for response
  sendBlocking(email: Email): Boolean

  // Async operation: wait for response asynchronously
  async sendMany(emails: List<Email>): Boolean
}
Providers

A provider is a transport-specific implementation of a service. Compared to the above mentioned concepts, this should be placed in a dedicated file to enforce strict separation of concern:

email.samt

record Email { /* ... */ }
service EmailService {
  oneway send(email: Email)
  sendBlocking(email: Email): Boolean
  async sendMany(emails: List<Email>): Boolean
}

email-provider.samt

provide EmailMonolithEndpoint {
  // We can implement multiple services in a single provider if we so desire (e.g. monolithic architecture)
  implements EmailService

  transport HTTP
}

provide EmailMicroEndpoint {
  // We can also only implement a subset of operations from a services if we so desire (e.g. microservice architecture)
  implements EmailService { sendBlocking }

  transport HTTP
}
Consumers

A consumer is a declaration of intent, for example "I intend on calling operation X from service Y". This should be placed in a dedicated file to enforce strict separation of concern:

email.samt

package email
record Email { /* ... */ }
service EmailService {
  oneway send(email: Email)
  sendBlocking(email: Email): Boolean
  async sendMany(emails: List<Email>): Boolean
}

email-provider.samt

package email
provide EmailMonolithEndpoint {
  implements EmailService

  transport HTTP
}

email-consumer.samt

package webshop // The consumer usually belongs to a different package
consume EmailMonolithEndpoint {
  // We can use multiple providers in a single consumer or just a specific operation
  uses EmailService { send }
}
Type Aliases

A type alias is an alternative name for a type. This can be used to avoid repeating the same complex type throughout a model.

typealias Filename = String (size(1..256), pattern("[a-zA-Z]*"))

record EmailAttachment {
  filename: Filename
  content: Binary
}
Annotations (API-Documentation)

Annotations can be added in most places to add additional metadata. Currently @Description and @Deprecated annotations are supported:

@Description("
  A very important business email with a great description 
")
record Email { /* ... */ }

@Description("
  A service to send important emails
")
service EmailService {
  @Deprecated("Use retrieveSmtp instead")
  retrievePop3(): List<Email>
  retrieveSmtp(): List<Email>
}

If you use markdown in your @Description annotations it will be rendered by the SAMT VS Code plugin.

Project Setup

We recommend using our official SAMT Template to get started. It's an easy way to bootstrap a new model, where the only prerequisite is a working internet connection and a local Java 17+ installation.

For a more advanced setup that invokes SAMT via Gradle check out the SAMT Ktor demo repository.

SAMT Wrapper

The SAMT Wrapper is a simple script which downloads a local copy of SAMT for every repository such that you don't have to install anything on your system. The local installation can then later be updated by running ./samtw wrapper, which updates your local installation to the latest version.

samt.yaml

The samt.yaml file contains the configuration of your project and defines where your SAMT source files are stored and where the code generators write their output to. A typical setup might look like this and is already included in the SAMT template:

source: ./src

generators:
  # Configure your desired generators here
  - name: kotlin-ktor-provider
    output: ./out/ktor-server
  - name: kotlin-ktor-consumer
    output: ./out/ktor-client
ℹ️ Behind a corporate proxy?

You can downloaded the latest release manually and upload it somewhere accessible within the company and adjust the distributionUrl parameter within the .samt/samt-wrapper.properties. The

samtVersion=...
distributionUrl=https\://my.corporate.proxy/path/to/cli-$samtVersion.tar
#        Variable for version, can be placed anywhere   ^^^^^^^^^^^^