Taxi Language

Last updated 4 months ago

Types

Types are the basic building block of an API.

/** Comments can be defined in either
* block style, or // inline style
*/
namespace taxi.demo { // Namespaces are optional, but reccommended.
@SomeAnnotation
type Person {
@Id // Arbitary annotations are supported at both class and field level
id : PersonId as Int // Inline type alises allow succinct Microtypes
firstName : FirstName as String
lastName : String // You don't have to use microtypes if they don't add value
friends : Person[] // Lists are supported
spouse : Person? // '?' indicates nullable types
}
}

Primitive types

Taxi currently supports the following primitive types. (See the Language Spec)

Taxi Type

Comments

Boolean

true or false

String

Strings are quoted in "double quotes"

Int

A numeric type without decimal places

Double

A numeric type with decimal places

Decimal

A numeric type with decimal places

Date

Full date notations, in the format of yyyy-mm-dd. Does not support time, or time-zones

Time

A time, in the form of hh:mm:ss. Does not support dates, or time-zones.

DateTime

A combination of Date and Time types, in the format of yyyy-mm-ddThh:mm:ss. Does not support time-zones.

Instant

A timestamp indicating an absolute point in time. Includes time-zone. Should be rfc3339 format - (eg: 2016-02-28T16:41:41.090Z)

Nullability

All types are considered mandatory (non-nullable), unless explicitly represented as nullable with the ? operator.

type Person {
id : Id as Int // Mandatory
spouse : Person? // Optional - may be null.
}

Inheritance

Taxi currently has basic support for inheritance:

type TypeA {
fieldA : String
}
type TypeB inherits TypeA {
fieldB : String
}
type TypeC inherits TypeB {
fieldC : String
}
// TypeD inherits TypeA and TypeB. TypeB also inherits TypeA.
// TypeD has fields fieldA, fieldB and fieldC
type TypeD inherits TypeA, TypeB {}

Enum types

Enum types are defined as follows:

enum BookClassification {
@Annotation // Enums may have annotations
FICTION,
NON_FICTION
}

Type Aliases

Taxi has strong support for type aliases, and judicious usage of micro-types are encouraged.

Type aliases may be defined in one of two ways, either explicitly using the type alias declaration, or inline using Microtype as Primitive syntax

@Annotation // Type-aliases may have annotations
type alias FirstName as String
type Person {
firstName : FirstName
lastName : LastName as String // defining a type alias in-line.
}

If using the inline declaration syntax, then a type alias may only be defined once, or an exception is thrown by the compiler.

Parameter types

Parameter types are special types that indicate to tooling that it's safe to construct these at runtime. Declaring something as a Parameter type has no other impact within Taxi on the type's definition, but is used in other tooling. (Eg., Vyne).

service PeopleService {
operation createPerson(CreatePersonRequest):Person
}
type Person {
id : Id as Int
firstName : FirstName as String
lastName : LastName as String
}
parameter type CreatePersonRequest {
firstName : FirstName
lastName : LastName
}

Type constraints

A type may express a constraint that indicates what is a permissible value:

enum Currency {
GBP, USD
}
type Money {
currency : Currency
amount : MoneyAmount as Decimal
}
type DepositCashRequest {
// We only accept deposits in GBP
amount : Money(currency = 'GBP')
clientId : ClientId as String
}

Taxi doesn't perform any validation of this itself - it's up to the services that consume and product the types. However, by making the contract part of the API definition, it allows for tooling to automate the validation process, and to automate interaction between services.

The types of constraints supported are discussed in Operation Contracts

Annotations

Annotations can be defined against any of the following:

  • Types

  • Fields

  • Enums

  • Services

  • Operations

Annotations may have parameters, or be left 'naked':

@ValidAnnotation
@AnotherAnnotation(stringParam = 'hello', boolValue = true, intValue = 123)
type Foo {}

Annotation parameters may be values of type string,int or boolean

Annotations do not have any direct impact on the types. However, they can be used to power tooling - such as generators, or to provide hints to consumers

Currently, there's no contracts or type checking around annotations, beyond basic syntax checking. However, this is expected to be improved in a future release.

Type Extensions

Type extensions allow mixing in additional data to a type after it's been defined.

It's possible to extend a type by adding either annotations, or type refinements. Structural changes to types are not permitted (and the compiler will prevent them).

Type extensions can be defined either in the same file, or (more typically), in a different file from the original defining type.

// As defined in one file.
type Person {
personId : Int
firstName : String
lastName : String
}
type alias FirstName as String
// Extending the type to provide additional context:
type extension Person {
// Adding an annotation. No additional type is defined, so the
// underlying type remains the same -- an Int
@Id
personId
// Refining the type. Note that FirstName represents the same
// type as the original String, so this is permitted.
// If the types were incompatible, the compiler would throw an exception
firstName : FirstName
}

There are a few scenarios where this may be useful:

Refining types with type aliases

Not all languages and spec tools are created equal - and some don't have great support for type aliases. Extensions allow for the narrowing (but not redefinition) of the type, through type aliases.

Mixing-in metadata

A consumer of an API may wish to leverage tooling that uses annotations - eg., persisting an entity into a database. Type extensions allow providing tooling specific metadata that's only useful to the consumer - not the producer.

Namespaces

Namespaces provide a way to qualify type names. They are analogous to namespaces in C#, or package in Java / Kotlin.

namespace demo.people {
// Defines "demo.people.Person"
type Person {}
type MovieCharacter {
name : String
// When referencing types from within the same namespace,
// they don't need to be qualified
actor : Person
}
}
namespace books {
type Book {
// Fully qualified names are needed when referencing namespaced types
// from another namespace
author : demo.people.Person
}
}

When namespaces are in use, the following rules apply:

  • Type references within the same namespace need not qualify their references

  • Type references within a different namespace must use a fully qualified reference

It's not mandatory to use namespaces, but it's recommended. It's useful to avoid name collisions and improves the output from generators, which typically are targeting languages that do use packages / namespaces

Services & Operations

Services

A services is simply a group of operations.

service PeopleService {
operation listAllPeople():Person[]
}

Operation

An operation defines a function on the API.

@HttpOperation(method = 'GET', url = 'https://myservice/people')
operation listAllPeople():Person[]

Operations often have annotations that provide hints to tooling as to how to invoke them.

Taxi ships with some standard annotations, as part of it's std-lib. Although it's up to individual tooling to determine how to interpret these, the suggested usage is as follows:

Annotation

Usage

@HttpOperation(method,url)

Indicates that the operation should be invoked over HTTP, using the provided method and url

@HttpRequestBody

Indicates that a parameter will be found on the request body

@ServiceDiscoveryClient(serviceName)

Indicates that the service's absolute url should be discovered from Service Discovery, using the provided name for lookup

Operation parameters

Names of operation parameters are optional. This is to encourage developers to leverage a richer type system where possible:

// These two declarations are both valid, and desribe the same operation
operation convertUnits(source:Weight, targetUnit:Unit):Weight
operation convertUnits(Weight,Unit):Weight

Operation Contracts & Constraints

Contracts and constraints are useful for telling tooling about what functionality an operation can provide, and what conditions must be met before invoking.

Both contracts and constraints use the same syntax.

type Money {
currency : String
amount : Decimal
}
operation convertCurrency(input: Money,
targetCurrency: String) : Money(from input, currency = targetCurrency)

`from input`

A contract may indicate that a return type is derived from one of the inputs, by using the from {input} syntax:

operation convertUnits(input: Weight, target: Unit):Weight( from input )

Attribute constraints

Attribute constraints describe either a pre-condition (if on an input) or a post-condition (if on a return type) for an operation

operation convertFromPounds(input : Money(currency = 'GBP'), target: Currency)
: Money( from input, currency = target)

As shown above, attribute constraints may either be:

  • A constant value (ie., "GBP")

  • A reference to an attribute of another parameter.

    • Nested syntax is supported (ie., foo.bar.baz)

These constraints are applicable on types too - see type constraints for an example.

This is an area of taxi that we expect to see significant development in, to improve the richness of the possible expressions