Describing services
Using our taxonomy to describe rich semantic services
Services
A service is simply a group of operations.
service PeopleService {
operation listAllPeople():Person[]
}
Services that contain HTTP operations may optionally declare an @HttpService
annotation, which allows it to set a baseUrl for all contained operations.
@HttpService(baseUrl="https://foo.com")
service PeopleService {
// resolves to https://foo.com/people
@HttpOperation(method = 'GET', url = '/people')
operation listAllPeople():Person[]
}
Http Operations
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 |
@PathVariable(name) | Indicates a property from the path that should be populated with a variable passed to the operation |
Examples
service MovieService {
@HttpOperation(method = "GET" , url = "https://myMovies/movies")
operation findAllMovies() : Movie[]
@HttpOperation(method = "GET" , url = "https://myMovies/movies/{id}")
operationFindMovie( @PathVariable(name = "id") id : MovieId ) : Movie
}
@HttpService(baseUrl = "http://actorService")
service ActorService {
// resolves to http://actorService/actors
@HttpOperation(method = "GET" , url = "/actors")
operation findAllActors() : Actor[]
}
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 describe the same operation
operation convertUnits(source:Weight, targetUnit:Unit):Weight
operation convertUnits(Weight,Unit):Weight
Http Parameters
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
Database tables
Taxi supports declaring tables within a service using the table
keyword:
service MyDatabase {
table actors : Actor[]
table movies : Movie[]
}
The return type of a table
must be an array (either T[]
or Array<T>
) - a compilation error is thrown if a table declares a non-array type.
Taxi doesn't directly support specifying database connectivity details, or specifying the type of database. Use annotations to specify this level of information.
Streaming data sources
Streaming services (such as message brokers) can be declared using the stream
keyword:
service MyMessageBroker {
stream loggedOnEvents(): Stream<UserLoggedOnEvent>
stream stockPrices(): Stream<StockPrice>
}
The return type of a stream
must be an Stream<T>
- a compilation error is thrown if a table declares a non-stream type.
Taxi doesn't directly support specifying message broker details, or specifying the message stream (eg., queue vs topic). Use annotations to specify this level of information.
Query operations
RESTful style APIs are great for discovering resources that have fairly predictable resource paths - such as /Author/jkrowling/Books
, returns all the books that jkrowling
has authored.
However, once you get beyond simple resource lookup, and into more bespoke query-style operations, services tend to expose their own query APIs. Taxi supports this with a rich, extensible syntax for exposing query contracts.
Here's an example:
taxiQl query personQuery(@RequestBody body:TaxiQlQuery):Person[] with capabilities {
filter(=,in,like),
sum,
count
}
This describes:
- A
query
endpoint that understands a query language calledtaxiQl
- An operation named
personQuery
, which accepts aTaxiQlQuery
- The operation understands how to perform filtering using
=
,in
andlike
semantics - And supports custom aggregation functions called
sum
andcount
.
Grammar names
In the above example, we saw a query endpoint that understands a query language called taxiQl
.
From taxi's perspective, the name of the grammar being used is entirely arbitrary. The query server that is invoking endpoints must understand how to construct queries in the described language.
The Orbital query server understands how to construct
SQL
, and GraphQL
) planned.
However, query servers may implement their own grammar support.Streaming data
Data sources that expose a stream of data (such as a websocket, or a message queue) can expose this using the Stream<>
type:
service PriceService {
operation getPrices(): Stream<Price>
}
Documenting service lineage (experimental)
Services can publish lineage information describing indicating other operations they depend on, and the types of data they persist.
This can be useful input into data catalogs for building a graph of dependencies across an organisation.
Lineage can be declared in an optional lineage
block within the service declaration:
service PersonService {
lineage {
consumes operation people.AddressService.resolvePostCodeToAddress
stores Person
}
operation findPerson(PersonId):Person
}
This indicates that our PersonService
has an upstream dependency on another operation - resolvePostCodeToAddress
, and
additionally holds a persistent store of the Person
model.
This data is exposed in the taxi schema and can be interrogated by tooling for integration with data catalogs.