Examples
Implementation issue for: https://github.com/Azure/typespec-azure/issues/3540, #514. Design details are here
-
[ ] Update versioning decorators to accommodate 'never' for the version argument (meaning do not execute the decorator)
-
[ ] Add templates and decorators to OpenAPI library
- [ ] Add JsonExample templates, and, in private namespace,
@jsonExampleName,@jsonExampleLiteral,@jsonExampleParameters,@jsonExampleReturnValuedecorators and associated accessors:
@jsonExampleName(TName) @added(TVersionAdded) @removed(TVersionRemoved) model JsonExampleOptions< TName extends string = null, TVersionAdded extends EnumMember = never, TVersionRemoved extends EnumMember = never> {} @jsonExampleLiteral(TExample) model JsonExample< TExample extends unknown, TName extends string = null, TSummary extends string = null, TVersionAdded extends EnumMember = never, TVersionRemoved extends EnumMember = never > is ExampleOptions<TName, TVersionAdded, TVersionRemoved> {} // This is specifically for matching return values in OpenAPI, with a // model keyed by status code @jsonExampleParameters(TParams) @jsonExampleReturnValue(TReturnValue) model JsonOperationExample< TParams extends {}, TReturnValue extends Model | Model[], TName extends string = null, TSummary extends string = null, TVersionAdded extends EnumMember = never, TVersionRemoved extends EnumMember = never > is ExampleOptions<TName, TVersionAdded, TVersionRemoved> {}- [ ] Add
@jsonExampledecorator and associated accessors
extern dec jsonExample(target: Model | ModelProperty , example: unknown) extern dec jsonOperationExample(target: Operation , example: unknown)- [ ] Add JsonExternalExample templates and associated decorator (
@jsonExamplePathand accessor in private namespace)
@jsonExamplePath(TExternalValue) model JsonExternalExample< TExternalValue extends string, TName extends string, TSummary extends string = null, TVersionAdded extends EnumMember = never, TVersionRemoved extends EnumMember = never > is ExampleOptions<TName, TVersionAdded, TVersionRemoved> {}- [ ] Add
@jsonExternalExampledecorator and associated accessors
extern dec jsonExternalExample(target: Model | ModelProperty | Operation, externalExample: unknown) - [ ] Add JsonExample templates, and, in private namespace,
-
[ ] Implement OpenAPI3 example support based on decorators
- [ ] Model and ModelProperty examples: support inlined value examples and named examples:
TypeSpec
@jsonExample( JsonExample< {type: "bar", createdAt: "2023-03-01T19:00:00Z"}, TName = "bar-example", TSummary ="An example of a bar type foo">) model Foo { type: "bar" | "baz" @jsonExample("2023-03-01T19:00:00Z") createdAt: utcDateTime; }Corresponding OpenAPI3
Foo: type: object examples: bar-example: summary: "An example of a bar type foo" value: type: "bar" createdAt: "2023-03-01T19:00:00Z" properties: createdAt: type: string format: date-time examples: - "2023-03-01T19:00:00Z"- [ ] Example file support in OpenAPI3
TypeSpec
@jsonExternalExample( JsonExternalExample< "examples/simple-foo-example.json", TName = "simple-foo-example", TSummary ="A simple example of type foo">) model Foo { type: "bar" | "baz" createdAt: utcDateTime; }Corresponding OpenAPI:
Foo: type: object examples: simple-foo-example: summary: A simple example of type foo externalValue: 'examples/simple-foo-example.json' properties: createdAt: type: string format: date-time
est: 13
Examples with the new value world
With the introduction of the new values in TypeSpec, examples are a great fit for making use of those. One of the problem with the previous approach was it was limtied to Json examples as we didn't know how to represent non primitive scalars(a canonical datetime for example) but this is all things that are able to be represented with the new scalar constructors.
Types examples
First and easier part is to define examples for the types. This would give parity with OpenAPI3
Decorator
model ExampleOptions {
/** The title of the example */
title?: string;
/** Description of the example */
description?: string;
/** Scope this example can apply to */
scopes?: unknown[];
}
extern dec example(target, value: valueof unknown, options: valueof ExampleOptions);
Example scopes
We could say that the title and description are good enough to be able to distinguish across version and visibility as well as some automatic filtering of non applicable properties could work. However there is most likely times where you want to be more explicit.
Proposing each examples can have a list of applicable scopes. It is then the job of the emitter to filter out examples that are not applicable to their scope.
Potential scopes:
- ~Serialization format:
forMediaType.is("application/json")~ - Specific version:
forVersion.is(Versions.v3) - Specific visibility:
forVisibility.is("read") - ~Specific emitter:
Typespec.Http.Client.TypeScript.forEmitter()~
With this it means that the value parameter doesn't necessary needs to be a value of the target type. It could be the json serialized payload or some CSharp or TypeScript example code.
Separate decorators for protocol/different targets
dec TypeSpec.Json.example(target, value: valueof unknown, options: ExampleOptions );
dec Typespec.Http.Client.TypeScript.example(target, value: valueof string, options: ExampleOptions );
dec Typespec.Http.Client.Csharp.example(target, value: valueof string, options: ExampleOptions );
Examples
Simple model
@example(#{
name: "John",
age: 12,
birthDate: plainDate.fromISO("2000-01-01")
})
model Person {
name: string;
age: int32;
birthDate: plainDate;
}
Simple property
Same as above but the examples are on the properties
model Person {
@example("John")
name: string;
@example(24)
age: int32;
@example(plainDate.fromISO("2000-01-01"))
birthDate: plainDate;
}
Visibility example
@example(#{
id: "user-123",
name: "John",
birthDate: plainDate.fromISO("2000-01-01")
}, #{scopes: #[forVisibility.is("read")]})
@example(#{
name: "John",
password: "password123!",
birthDate: plainDate.fromISO("2000-01-01")
}, #{scopes: #[forVisibility.is("create")]})
model Person {
@visibility("read") id: string;
@visibility("create") password: string;
name: string;
birthDate: plainDate;
}
Visibility example
model Person {
@added(Version.v2) id?: string;
name: string;
birthDate: plainDate;
}
// Can be in a separate file
@@example(Person, #{
id: "user-123",
name: "John",
birthDate: plainDate.fromISO("2000-01-01")
}, #{scopes: #[fromVersioning.is(Versions.v2)]})
@@example(Person, #{
name: "John",
birthDate: plainDate.fromISO("2000-01-01")
}, #{scopes: #[fromVersioning.range(Versions.v1, Version.v2)]})
Json inline
@Json.example(#{
name: "John",
age: 12,
birthDate: "2000-01-01"
})
model Person {
name: string;
age: int32;
birthDate: plainDate;
}
or
@Json.example("""
{
"name": "John",
"age": 12,
"birthDate": "2000-01-01"
}
""")
model Person {
name: string;
age: int32;
birthDate: plainDate;
}
External file
Load an external file. In this case we need to specify that this example is for a specific protocol
scalar externalExample {
init path(path: string);
}
@Json.example(externalExample.path("person.json"))
model Person {
name: string;
age: int32;
birthDate: plainDate;
}
Client Examples
Load an external file. In this case we need to specify that this example is for a specific protocol
@Http.Client.TypeScript.example("""
const person: Person = {
name: "John",
age: 12,
birthDate: "2000-01-01"
};
""")
@Http.Client.CSharp.example("""
var person = new Person{
name = "John",
age = 12,
birthDate = new Date("2000-01-01")
};
""")
model Person {
name: string;
age: int32;
birthDate: plainDate;
}
Operation examples
Operation examples can be a little more tricky because depending on the protocol you might want to see things differently. But maybe its just the job of the emitter to move things in the same place that the model was moved(e.g. body)
Decorator
We have 2 options for decorators
- Use a dedicated decorator
@opExampleor@operationExample - Use the same decorator as for the models just expect a different structure
model ExampleOptions {
/** The title of the example */
title?: string;
/** Description of the example */
description?: string;
/** Scope this example can apply to */
scopes?: unknown[];
}
model OperationExample {
parameters?: unknown;
returns?: unknown;
}
extern dec opExample(target, value: valueof OperationExample, options: valueof ExampleOptions);
Example scopes
Scopes would apply as well however some might be irrelevant as the operation would already for example define the visibility.
Examples
Operation with spread parameters
@opExample(#{
parameters: #{
name: "John",
age: 12,
birthDate: plainDate.fromISO("2000-01-01")
},
returns: #{
name: "John",
age: 12,
birthDate: plainDate.fromISO("2000-01-01")
}
})
op createUser(...User): User;
Operation with explicit body
@opExample(#{
parameters: #{
user: #{
name: "John",
age: 12,
birthDate: plainDate.fromISO("2000-01-01")
}
},
returns: #{
name: "John",
age: 12,
birthDate: plainDate.fromISO("2000-01-01")
}
})
op createUser(@body user: User): User;
Operation with spread body and headers
@opExample(#{
parameters: #{
name: "John",
age: 12,
birthDate: plainDate.fromISO("2000-01-01"),
ifNotModified: eTag("abcdefghijklmnop")
},
})
op updateUser(...User, @header ifNotModified: eTag): void;
Operation with explicit body and headers
@opExample(#{
parameters: #{
user: #{
name: "John",
age: 12,
birthDate: plainDate.fromISO("2000-01-01")
},
ifNotModified: eTag("abcdefghijklmnop")
},
})
op updateUser(@body user: User, @header ifNotModified: eTag): void;
External canonical examples
Above we showed that we could have the example be loaded from an external file but that would be specific to a protocol(e.g. a json file, an xml file, some csharp code)
We could imaging having a special file extension that define canonical examples with TypeSpec. This is just a concept there is a lot more to think about this
examples/person.example.tsp
import "../scalars.tsp";
example "Example title" #{
name: "John",
age: 12,
birthDate: plainDate.fromISO("2000-01-01"),
data: customScalar.from("some data")
}
scalar.tsp
scalar customScalar {
from(value: string);
}
models.tsp
@example(import("./examples/person.example.tsp"))
model Person {
name: string;
age: int32;
birthDate: plainDate;
data: customScalar;
}
Stage 1:
- TypeSpec only examples
- Visibility and versioning are automatically applied to examples
- Provide view of the examples for certain protocol(Json, xml library can provide its own implementation)
- different library?
@typespec/experimental-examples- emit a single warning on import?
interface Example {
type: Type;
value: Value;
}
function getExample(): Example;
function toJsonExample(example: Example): unknown;
function toXmlExample(example: Example): unknown;
// http lib
function filterHttpVisibility(
example: Example,
visibility: Visibility
): Example;
// Versioning lib
function filterVersioning(example: Example, version: Version): Example;
I have a question, as for the opExample decorator, would we have validations/static checks for the types?
Such as this example:
@opExample(#{
parameters: #{
user: #{
name: "John",
age: 12,
birthDate: plainDate.fromISO("2000-01-01")
},
ifNotModified: eTag("abcdefghijklmnop")
},
})
op updateUser(@body user: User, @header ifNotModified: eTag): void;
what if the json we have in the example decorator has some missing required property? Would compile report error?
the opExample is on typespec operation and example value should be one one mapping to operation's parameters and return types, right? since typespec has template, it seems it is not easy to know all the parameters and responses structure, thus not easy to write an example.