Skip to content

Latest commit

 

History

History
1128 lines (811 loc) · 35.6 KB

tutorial.md

File metadata and controls

1128 lines (811 loc) · 35.6 KB

Introduction to API Definition Language (Cadl)

Cadl is a language for describing cloud service APIs and generating other API description languages, client and service code, documentation, and other assets. Cadl provides highly extensible core language primitives that can describe API shapes common among REST, GraphQL, gRPC, and other protocols.

Cadl is an object oriented dynamic language whose evaluation results in an object model describing service APIs. Unlike typical programming languages, Cadl consists primarily of declarations, however these declarations can be decorated to provide highly dynamic behavior.

Cadl's primary benefits include:

  • Protocol agnostic: it can describe and generate code for APIs across multiple protocols and serialization languages
  • Modular: developers can group common API shapes and conventions together and reuse them
  • Terse: the syntax is expressive, capable of describing complex APIs with minimal code
  • Extensible: developers can customize the language to describe just about any style of API

Language Tour

Cadl consists of the following language features:

  • Models: data shapes or schemas
  • Type Literals: strings and numbers with specific values
  • Type Operators: syntax for composing model types into other types
  • Operations: service endpoints with parameters and return values
  • Namespaces & Usings: groups models and operations together into hierarchical groups with friendly names
  • Interfaces: groups operations
  • Imports: links declarations across multiple files and libraries together into a single program
  • Decorators: bits of TypeScript code that add metadata or sometimes mutate declarations
  • Libraries: encapsulate Cadl definitions into reusable components

In addition, Cadl comes with a standard library for describing REST APIs and generating OpenAPI. Other protocol bindings are a work in progress!

Models

Cadl models are used to describe data shapes or schemas. Models have any number of members and can extend and be composed with other models. Members are required by default, but can made optional by appending a "?" to the member name. A default value can also be provided with adding = <value> on an optional property.

The following defines a data shape with three members:

model Dog {
  name: string;
  favoriteToy?: string;
  bestTreat?: string = "chicken";
}

Built-in Models

Type relations

Cadl comes with built-in models for common data types:

  • string: sequence of characters
  • bytes: a sequence of bytes
  • int8: 8-bit signed integer
  • int16: 16-bit signed integer
  • int32: 32-bit signed integer
  • int64: 64-bit signed integer
  • uint8: 8-bit unsigned integer
  • uint16: 16-bit unsigned integer
  • uint32: 32-bit unsigned integer
  • uint64: 64-bit unsigned integer
  • safeint: an integer that is safe to store in a IEEE754 double and safe to round trip through all JSON processors.
  • float32: IEEE 754 single-precision floating point number
  • float64: IEEE 754 double-precision floating point number
  • plainDate: A date on a calendar without a time zone, e.g. "April 10th"
  • plainTime: A time on a clock without a time zone, e.g. "3:00 am"
  • zonedDateTime: A date and time in a particular time zone, e.g. "April 10th at 3:00am in PST"
  • duration: A duration/time period. e.g 5s, 10h
  • boolean: true or false
  • null: the null value found in e.g. JSON.
  • Record<T>: a dictionary with string K and value T.
  • unknown: A top type in Cadl that all types can be assigned to.
  • void: A function return type indicating the function doesn't return a value.
  • never: The never type indicates the values that will never occur. Typically, you use the never type to represent the return type of a function that always throws an error.

Spread

The spread operator takes the members of a source model and copies them into a target model. Spread doesn't create any nominal relationship between source and target, and so it's useful when you want to reuse common properties without reasoning about or generating complex inheritance relationships.

model Animal {
  species: string;
}

model Pet {
  name: string;
}

model Dog {
  ...Animal;
  ...Pet;
}

// Dog is equivalent to the following declaration:
model Dog {
  species: string;
  name: string;
}

Extends

Sometimes you want to create an explicit relationship between two models, for example when you want to emit class definitions in languages which support inheritance. The extends keyword can be used to establish such a relationship. It is also used extensively with interface to compose from existing interface building blocks.

model Animal {
  species: string;
}

model Dog extends Animal {}

Is

Sometimes you want to copy all aspects of a type without creating a nominal inheritance relationship. The is keyword can be used for this purpose. It is like spread, but also copies decorators in addition to properties. One common use case is to give a better name to a template instantiation:

@decorator
model Thing<T> {
  property: T;
}

model StringThing is Thing<string>;

// StringThing declaration is equivalent to the following declaration:
@decorator
model StringThing {
  property: string;
}

Enums

Enums define a type which can hold one of a set of constant values.

enum Color {
  Red,
  Blue,
  Green,
}

In this case, we haven't specified how the constants will be represented, allowing for different choices in different scenarios. For example, the OpenAPI emitter will choose string values "Red", "Green", "Blue". Another protocol might prefer to assign incrementing numeric values 0, 1, 2.

We can also specify explicit string or numeric values:

enum Color {
  Red: "red",
  Blue: "blue",
  Green: "green",
}

enum Priority {
  High: 100,
  Low: 0,
}

Templates

It is often useful to let the users of a model fill in certain details. Model templates enable this pattern. Similar to generics found in other languages, model templates declare template parameters that users provide when referencing the model.

model Page<T> {
  size: number;
  item: T[];
}

model DogPage {
  ...Page<Dog>;
}

A template parameter can be given a default value with = <value>.

model Page<T = string> {
  size: number;
  item: T[];
}

Type Aliases

Sometimes it's convenient to alias a model template instantiation or type produced via type operators (covered later) as a convenient name. Aliases allow this:

alias DogPage = Page<Dog>;

Unlike model, alias does not create a new entity, and as such will not change generated code in any way. An alias merely describes a source code shorthand to avoid repeating the right-hand side in multiple places.

Because alias does not create a new entity, you cannot specify decorators on an alias.

Type Literals

API authors often need to describe API shapes in terms of specific literal values. For example, this operation returns this specific integer status code, or this model member can be one of a few specific string values. It is also often useful to pass specific literal values to decorators. Cadl supports string, number, and boolean literal values to support these cases:

model BestDog {
  name: "Suki";
  age: 14;
  best: true;
}

String literal types can also be created using the triple-quote syntax which enables multi-line strings:

model Dog {
  favoriteFoods: """
    McDonalds
    Chipotle
    And so on
    """;
}

Type Operators

Cadl supports a few type operators that make it easy to compose new models from other models.

Unions

Unions describe a type that must be exactly one of the union's constituents. Create a union with the | operator.

alias GoodBreed = Beagle | GermanShepherd | GoldenRetriever;
Named unions

There is also a declaration syntax for naming a union and its options:

union GoodBreed {
  beagle: Beagle,
  shepherd: GermanShepherd,
  retriever: GoldenRetriever,
}

The above example is equivalent to the GoodBreed alias above, except that emitters can actually see GoodBreed as a named entity and also see the beagle, shepherd, and retriever names for the options. It also becomes possible to apply decorators to each of the options when using this form.

Intersections

Intersections describe a type that must include all the intersection's constituents. Create an intersection with the & operator.

alias Dog = Animal & Pet;

Arrays

Arrays describe lists of things. Create an Array type with the [] operator.

alias Pack = Dog[];

Operations

Operations describe service endpoints and consist of an operation name, parameters, and return type. Operations are declared using the op keyword:

op getDog(name: string): Dog;

The operation's parameters describe a model, so anything you can do in a model you can do in a parameter list as well, including using the spread operator:

op getDog(...commonParams, name: string): Dog;

Often an endpoint returns one of any number of models. For example, there might be a return type for when an item is found, and a return type for when an item isn't found. Unions are used to describe this pattern:

model DogNotFound {
  error: "Not Found";
}

op getDog(name: string): Dog | DogNotFound;

Namespaces & Usings

Namespaces let you group related types together into namespaces. This helps organize your types, making them easier to find and prevents name conflicts. Namespaces are merged across files, so you can reference any type anywhere in your Cadl program via its namespace. You can create namespace blocks like the following:

namespace Models {
  model Dog {}
}

op getDog(): Models.Dog;

You can also put an entire Cadl file into a namespace by using the blockless namespace syntax:

// models.cadl
namespace Models;
model Dog {}
// main.cadl
import "./models.cadl";
op getDog(): Models.Dog;

Namespace declarations can declare multiple namespaces at once by using a dotted member expression. There's no need to declare nested namespace blocks if you don't want to.

namespace A.B;
namespace C.D {

}
namespace C.D.E {
  model M {}
}

alias M = A.B.C.D.E.M;

It can be convenient to add references to a namespace's declarations to your local namespace, especially when namespaces can become deeply nested. The using statement lets us do this:

// models.cadl
namespace Service.Models;
model Dog {}
// main.cadl
import "./models.cadl";
using ServiceModels;
op getDog(): Dog; // here we can use Dog directly.

The bindings introduced by a using statement are local to the namespace they are declared in. They do not become part of the namespace themselves.

namespace Test {
  model A {}
}

namespace Test2 {
  using Test;
  alias B = A; // ok
}

alias C = Test2.A; // not ok
alias C = Test2.B; // ok

Interfaces

Interfaces can be used to group operations.

interface A {
  a(): string;
}

interface B {
  b(): string;
}

And the keyword extends can be used to compose operations from other interfaces into a new interface:

interface C extends A, B {
  c(): string;
}

// C is equivalent to the following declaration
interface C {
  a(): string;
  b(): string;
  c(): string;
}

Imports

Imports add files or libraries to your Cadl program. When you compile a Cadl file, you provide a path to your root Cadl file, by convention called "main.cadl". From there, any files you import are added to your program. If you import a directory, Cadl will look for a main.cadl file inside that directory.

The path you import must either begin with "./" or "../" or otherwise be an absolute path. The path must either refer to a directory, or else have an extension of either ".cadl" or ".js". The following demonstrates how to use imports to assemble a Cadl program from multiple files:

// main.cadl
import "./models";
op getDog(): Dog;
// models/main.cadl
import "./dog.cadl";
// models/dog.cadl
namespace Models;
model Dog {}

Decorators

Decorators enable a developer to attach metadata to types in a Cadl program. They can also be used to calculate types based on their inputs. Decorators are the backbone of Cadl's extensibility and give it the flexibility to describe many different kinds of APIs and associated metadata like documentation, constraints, samples, and the like.

Many Cadl constructs can be decorated, including namespaces, operations and their parameters, and models and their members.

Decorators are defined using JavaScript functions that are exported from a standard ECMAScript module. When you import a JavaScript file, Cadl will look for any exported functions, and make them available as decorators inside the Cadl syntax. When a decorated declaration is evaluated by Cadl, it will invoke the decorator function, passing along a reference to the current compilation, an object representing the type it is attached to, and any arguments the user provided to the decorator.

Decorators are attached by adding the decorator before the element you want to decorate, prefixing the name of the decorator with @. Arguments can be provided by using parentheses in a manner similar to many programming languages, e.g. @dec(1, "hi", { a: string }). The parentheses can be omitted when no arguments are provided.

The following shows an example of declaring and then using a decorator:

// model.js
export function logType(compilation, targetType, name) {
  console.log(name + ": " + targetType.kind);
}
// main.cadl
import "./model.js";

@logType("Dog type")
model Dog {
  @logType("Name type")
  name: string;
}

After running this Cadl program, the following will be printed to the console:

Name type: ModelProperty
Dog type: Model

Built-in decorators

Cadl comes built-in with a number of decorators that are useful for defining service APIs regardless of what protocol or language you're targeting.

  • @deprecated - indicates that the decorator target has been deprecated.
  • @doc - attach a documentation string. Works great with multi-line string literals.
  • @error - specify a model is representing an error
  • @format - specify the data format hint for a string type
  • @friendlyName - specify a friendly name to be used instead of declared model name
  • @indexer
  • @inspectType/@inspectTypeName - displays information about a type during compilation
  • @key - mark a model property as the key to identify instances of that type
  • @knownValues - mark a string type with an enum that contains all known values
  • @list -
  • @minLength/@maxLength - set the min and max lengths for strings
  • @minValue/@maxValue - set the min and max values of number types
  • @pattern - set the pattern for a string using regular expression syntax
  • @secret - mark a string as a secret value that should be treated carefully to avoid exposure
  • @summary - attach a documentation string, typically a short, single-line description.
  • @tag - attach a simple tag to a declaration
  • @visibility/@withVisibility
  • @withDefaultKeyVisibility - set the visibility of key properties in a model if not already set.
  • @withOptionalProperties - makes all properties of the target type optional.
  • @withoutDefaultValues - removes all read-only properties from the target type.
  • @withoutOmittedProperties - removes all model properties that match a type.
  • @withUpdateableProperties - remove all read-only properties from the target type
@inspectType

Syntax:

@inspectType(message)
@inspectTypeName(message)

@inspectType displays information about a type during compilation. @inspectTypeName displays information and name of type during compilation. They can be specified on any language element -- a model, an operation, a namespace, etc.

@deprecated

Syntax:

@deprecated(message)

@deprecated marks a type as deprecated. It can be specified on any language element -- a model, an operation, a namespace, etc.

@friendlyName

Syntax:

@friendlyName(string)

@friendlyName specifies how a templated type should name their instances. It takes a string literal coresponding the the name. {name} can be used to interpolate the value of the template parameter which can be passed as a 2nd parameter.

Example:

@friendlyName("{name}List", T)
model List<T> {}

alias A = List<FooBar>; // Instance friendly name would be `FooBarList`
alias B = List<Person>; // Instance friendly name would be `PersonList`
@pattern

Syntax:

@pattern(regularExpressionText)

@pattern specifies a regular expression on a string property.

@summary

Syntax:

@summary(text [, object])

@summary attaches a documentation string. It is typically used to give a short, single-line description, and can be used in combination with or instead of @doc.

The first argument to @summary is a string, which may contain template parameters, enclosed in braces, which are replaced with an attribute for the type (commonly "name") passed as the second (optional) argument.

@summary can be specified on any language element -- a model, an operation, a namespace, etc.

@doc

Syntax:

@doc(text [, object])

@doc attaches a documentation string. Works great with multi-line string literals.

The first argument to @doc is a string, which may contain template parameters, enclosed in braces, which are replaced with an attribute for the type (commonly "name") passed as the second (optional) argument.

@doc can be specified on any language element -- a model, an operation, a namespace, etc.

@knownValues

Syntax:

@knownValues(enumTypeReference)

@knownValues marks a string type with an enum that contains all known values

The first parameter is a reference to an enum type that enumerates all possible values that the type accepts.

@knownValues can only be applied to model types that extend string.

Example:

enum OperationStateValues {
  Running,
  Completed,
  Failed
}

@knownValues(OperationStateValues)
model OperationState extends string {
}
@key

Syntax:

@key([keyName])

@key - mark a model property as the key to identify instances of that type

The optional first argument accepts an alternate key name which may be used by emitters. Otherwise, the name of the target property will be used.

@key can only be applied to model properties.

@secret

Syntax:

@secret

@secret mark a string as a secret value that should be treated carefully to avoid exposure

@secret
model Password is string;

@secret can only be applied to string model;

@format

Syntax:

@format(formatName)

@format - specify the data format hint for a string type

The first argument is a string that identifies the format that the string type expects. Any string can be entered here, but a Cadl emitter must know how to interpret

For Cadl specs that will be used with an OpenAPI emitter, the OpenAPI specification describes possible valid values for a string type's format:

https://swagger.io/specification/#data-types

@format can be applied to a type that extends from string or a string-typed model property.

@error

Syntax:

@error

@format - specify that this model is an error type

For HTTP API this can be used to represent a failure.

Visibility decorators

Additionally, the decorators @withVisibility and @visibility provide an extensible visibility framework that allows for defining a canonical model with fine-grained visibility flags and derived models that apply those flags. Flags can be any string value and so can be customized to your application. Also, @visibility can take multiple string flags to set multiple flags at once, and @withVisibility can take multiple string flags to filter on at once.

Consider the following example:

model Dog {
  // the service will generate an ID, so you dont need to send it.
  @visibility("read") id: int32;
  // the service will store this secret name, but won't ever return it
  @visibility("write") secretName: string;
  // no flags are like specifying all flags at once, so in this case
  // equivalent to @visibility("read", "write")
  name: string;
}

// The spread operator will copy all the properties of Dog into ReadDog,
// and withVisibility will remove any that don't match the current
// visibility setting
@withVisibility("read")
model ReadDog {
  ...Dog;
}

@withVisibility("write")
model WriteDog {
  ...Dog;
}

@withDefaultKeyVisibility

Syntax:

@withDefaultKeyVisibility(string)

@withDefaultKeyVisibility - set the visibility of key properties in a model if not already set. The first argument accepts a string representing the desired default visibility value. If a key property already has a visibility decorator then the default visibility is not applied.

@withDefaultKeyVisibility can only be applied to model types.

@withOptionalProperties

Syntax:

@withOptionalProperties()

@withOptionalProperties makes all properties of the target type optional.

@withOptionalProperties can only be applied to model types.

@withoutDefaultValues

Syntax:

@withoutDefaultValues()

@withoutDefaultValues removes all read-only properties from the target type.

@withoutDefaultValues can only be applied to model types.

@withoutOmittedProperties

Syntax:

@withoutOmittedProperties(type)

@withoutOmittedProperties removes all model properties that match a type.

@withoutOmittedProperties can only be applied to model types.

@withUpdateableProperties

Syntax:

@withUpdateableProperties()

@withUpdateableProperties remove all read-only properties from the target type.

@withUpdateableProperties can only be applied to model types.

Libraries

Cadl libraries are bundles of useful Cadl declarations and decorators into reusable packages. Cadl libraries are actually npm packages under the covers. Official Cadl libraries can be found with the @cadl-lang/ or @azure-tools/cadl- npm package name prefix. Libraries can be either a language library, an emitter library or both.

Setting up Cadl library

The first step in using a library is to install it via npm. You can get npm and node from the Node.js website.

If you haven't already initialized your Cadl project's package.json file, now would be a good time to do so. The package.json file lets you track the dependencies your project depends on, and is a best practice to check in along with any Cadl files you create. Run npm init create your package.json file.

Then, in your Cadl project directory, type npm install libraryName to install a library. For example, to install the official Cadl REST API bindings and OpenAPI generator, you would type npm install @cadl-lang/rest @cadl-lang/openapi3.

Using language libraries

Lastly, you need to import the libraries into your Cadl program. By convention, all external dependencies are imported in your main.cadl file, but can be in any Cadl file imported into your program. Importing the two libraries we installed above would look like this:

// in main.cadl
import "@cadl-lang/rest";
import "@cadl-lang/openapi3";

Using emitter libraries

The emitter needs to be referenced either via the cli --emit option or configured in the CADL config file.

# Run openapi3 emitter on the spec
cadl compile . --emit=@cadl-lang/openapi3

or in the config file cadl-project.yaml

emitters:
  "@cadl-lang/openapi3": true
Configuring emitter libraries

Emitters might provide some options to configure the generated output. Those options can be set either via the config cadl-project.yaml or the cli

  • Via config
emitters:
  <emitterName>:
    <optionName>: <value>

# For example
emitters:
 "@cadl-lang/openapi3":
    output-file: my-custom-file.json
  • Via cli
--option "<emitterName>.<optionName>=<value>"

# For example
--option "@cadl-lang/openapi3.output-file=my-custom-file.json"

Standard emitter libraries

Cadl has following standard libraries:

Library Package name Documentation Source
OpenAPI binding library @cadl-lang/openapi Readme.md Link
OpenAPI 3 @cadl-lang/openapi3 Readme.md Link
HTTP, REST @cadl-lang/rest Readme.md Link

Creating libraries

Creating a Cadl library is essentially the same as creating any NPM library. Consult the official documentation for more info. main should refer to a JS file that exports all your library's decorators and helper utilities.

The package.json file for a Cadl library requires one additional field: cadlMain, which refers to the root file of your Cadl program similar to how main refers to the root of a JS program. If you don't have any Cadl declarations, cadlMain can be identical to main.

REST APIs

With the language building blocks we've covered so far we're ready to author our first REST API. Cadl has an official REST API "binding" called @cadl-lang/rest. It's a set of Cadl declarations and decorators that describe REST APIs and can be used by code generators to generate OpenAPI descriptions, implementation code, and the like.

Cadl also has an official OpenAPI emitter called @cadl-lang/openapi3 that consumes the REST API bindings and emits standard OpenAPI descriptions. This can then be fed in to any OpenAPI code generation pipeline.

The following examples assume you have imported both @cadl-lang/openapi3 and @cadl-lang/rest somewhere in your Cadl program (though importing them in main.cadl is the standard convention). For detailed library reference, please see rest library's Readme.md.

Service definition and metadata

A definition for a service is the namespace that contains all the operations for the service and carries top-level metadata like service name and version. Cadl offers the following decorators for providing this metadata, and all are optional.

  • @serviceTitle - the title of the service
  • @serviceVersion - the version of the service. Can be any string, but later version should lexicographically sort after earlier versions
  • @server - the host of the service. Can accept parameters.

Here's an example that uses these to define a Pet Store service:

@serviceTitle("Pet Store Service")
@serviceVersion("2021-03-25")
@server("https://example.com", "Single server endpoint")
@doc("This is a sample server Petstore server.")
namespace PetStore;

The server keyword can take a third parameter with parameters as necessary:

@server("https://{region}.foo.com", "Regional endpoint", {
  @doc("Region name")
  region?: string = "westus",
})

Resources & routes

Resources are operations that are grouped in a namespace. You declare such a namespace by adding the @route decorator to provide the path to that resource:

using Cadl.Http;

@route("/pets")
namespace Pets {

}

To define an operation on this resource, you need to provide the HTTP verb for the route using the @get, @head @post, @put, @patch, or @delete decorators. Alternatively, you can name your operation list, create, read, update, delete, or deleteAll and the appropriate verb will be used automatically. Lets add an operation to our Pets resource:

@route("/pets")
namespace Pets {
  op list(): Pet[];

  // or you could also use
  @get op listPets(): Pet[];
}
Automatic route generation

Instead of manually specifying routes using the @route decorator, you automatically generate routes from operation parameters by applying the @autoRoute decorator to an operation, namespace, or interface containing operations.

For this to work, an operation's path parameters (those marked with @path) must also be marked with the @segment decorator to define the preceding path segment.

This is especially useful when reusing common parameter sets defined as model types.

For example:

model CommonParameters {
  @path
  @segment("tenants")
  tenantId: string;

  @path
  @segment("users")
  userName: string;
}

@autoRoute
interface UserOperations {
  @get
  getUser(...CommonParameters): User | Error;

  @put
  updateUser(...CommonParameters, user: User): User | Error;
}

This will result in the following route for both operations

/tenants/{tenantId}/users/{userName}

Path and query parameters

Model properties and parameters which should be passed as path and query parameters use the @path and @query parameters respectively. Let's modify our list operation to support pagination, and add a read operation to our Pets resource:

@route("/pets")
namespace Pets {
  op list(@query skip: int32, @query top: int32): Pet[];
  op read(@path petId: int32): Pet;
}

Path parameters are appended to the URL unless a substitution with that parameter name exists on the resource path. For example, we might define a sub-resource using the following Cadl. Note how the path parameter for our sub-resource's list operation corresponds to the substitution in the URL.

@route("/pets/{petId}/toys")
namespace PetToys {
  op list(@path petId: int32): Toy[];
}

Request & response bodies

Request and response bodies can be declared explictly using the @body decorator. Let's add an endpoint to create a pet. Let's also use this decorator for the responses, although this doesn't change anything about the API.

@route("/pets")
namespace Pets {
  op list(@query skip: int32, @query top: int32): {
    @body pets: Pet[];
  };
  op read(@path petId: int32): {
    @body pet: Pet;
  };
  @post
  op create(@body pet: Pet): {};
}

Note that in the absence of explicit @body:

  1. The set of parameters that are not marked @header, @query, or @path form the request body.
  2. The set of properties of the return model that are not marked @header, @query, or @path form the response body.
  3. If the return type is not a model, then it defines the response body.

This is how we were able to return Pet and Pet[] bodies without using @body for list and read. We can actually write create in the same terse style by spreading the Pet object into the parameter list like this:

@route("/pets")
namespace Pets {
  @post
  op create(...Pet): {};
}

Polymorphism with discriminators

A pattern often used in REST APIs is to define a request or response body as having one of several different shapes, with a property called the "discriminator" indicating which actual shape is used for a particular instance. Cadl supports this pattern with the @discriminator decorator of the Rest library.

The @discrminator decorator takes one argument, the name of the discriminator property, and should be placed on the model for the request or response body. The different shapes are then defined by separate models that extend this request or response model. The discriminator property is defined in the "child" models with the value or values that indicate an instance that conforms to its shape.

As an example, a Pet model that allows instances that are either a Cat or a Dog can be defined with

@discriminator("kind")
model Pet {
  name: string;
  weight?: float32;
}
model Cat extends Pet {
  kind: "cat";
  meow: int32;
}
model Dog extends Pet {
  kind: "dog";
  bark: string;
}

Headers

Model properties and parameters that should be passed in a header use the @header decorator. The decorator takes the header name as a parameter. If a header name is not provided, it is inferred from the property or parameter name. Let's add etag support to our pet store's read operation.

@route("/pets")
namespace Pets {
  op list(@query skip: int32, @query top: int32): {
    @body pets: Pet[];
  };
  op read(@path petId: int32, @header ifMatch?: string): {
    @header eTag: string;
    @body pet: Pet;
  };
  @post
  op create(@body pet: Pet): {};
}

Status codes

Use the @header decorator on a property named statusCode to declare a status code for a response. Generally, setting this to just int32 isn't particularly useful. Instead, use number literal types to create a discriminated union of response types. Let's add status codes to our responses, and add a 404 response to our read endpoint.

@route("/pets")
namespace Pets {
  op list(@query skip: int32, @query top: int32): {
    @statusCode statusCode: 200;
    @body pets: Pet[];
  };
  op read(@path petId: int32, @header ifMatch?: string): {
    @statusCode statusCode: 200;
    @header eTag: string;
    @body pet: Pet;
  } | {
    @statusCode statusCode: 404;
  };
  op create(@body pet: Pet): {
    @statusCode statusCode: 204;
  };
}

Built-in response shapes

Since status codes are so common for REST APIs, Cadl comes with some built-in types for common status codes so you don't need to declare status codes so frequently.

There is also a Body type, which can be used as a shorthand for { @body body: T } when an explicit body is required.

Lets update our sample one last time to use these built-in types:

model ETag {
  @header eTag: string;
}
@route("/pets")
namespace Pets {
  op list(@query skip: int32, @query top: int32): OkResponse & Body<Pet[]>;
  op read(@path petId: int32, @header ifMatch?: string): (OkResponse &
    Body<Pet> &
    ETag) | NotFoundResponse;
  @post
  op create(...Pet): NoContentResponse;
}

Note that the default status code is 200 for non-empty bodies and 204 for empty bodies. Similarly, explicit Body<T> is not required when T is known to be a model. So the following terser form is equivalent:

@route("/pets")
namespace Pets {
  op list(@query skip: int32, @query top: int32): Pet[];
  op read(@path petId: int32, @header ifMatch?: string): (Pet & ETag) | NotFoundResponse;
  @post
  op create(...Pet): {};
}

Finally, another common style is to make helper response types that are shared across a larger service definition. In this style, you can be entirely explicit while also keeping operation definitions concise.

For example, we could write :

model ListResponse<T> {
  ...OkResponse;
  ...Body<T[]>;
}

model ReadSuccessResponse<T> {
  ...OkResponse;
  ...ETag;
  ...Body<T>;
}

alias ReadResponse<T> = ReadSuccessResponse<T> | NotFoundResponse;

model CreateResponse {
  ...NoContentResponse;
}

@route("/pets")
namespace Pets {
  op list(@query skip: int32, @query top: int32): ListResponse<Pet>;
  op read(@path petId: int32, @header ifMatch?: string): ReadResponse<Pet>;
  @post
  op create(...Pet): CreateResponse;
}

CADL Config

Cadl has a configuration file cadl-project.yaml that right now is only used to configure the default emitter to use. The config file needs to be a sibling of the package.json. Cadl will look for the following files in that order and pick the 1st one found:

Configuration schema:

# Map of the default emitters to use when not using `--emit`
emitters:
  <emitterName>: true