How generation works
What it is
Section titled “What it is”gisila_doc ships a source_gen builder. For every class annotated with
@Controller it generates an extension
named <ClassName>GisilaDoc with one method:
void attachToApp(GisilaApp app, Router router, OpenApiSpec spec, {String prefix = ''});The method registers each annotated route on router and records the matching OpenAPI
operation in spec. Nothing is computed by reflection at runtime; parameter binding code and
schema maps are written out as plain Dart at build time.
When to use it
Section titled “When to use it”Read this page when you need to predict what the generator will do with a signature: which parameters become path, query, header or body parameters, which responses are documented, and when a build fails.
When not to use it
Section titled “When not to use it”For the shape of the JSON schemas themselves, see How schemas are derived.
How it works
Section titled “How it works”Build time
Section titled “Build time”dart run build_runner buildruns thegisila_docbuilder on every Dart file in a package that depends ongisila_doc. It writes a.gisila_doc.g.part, whichsource_gen’s combining builder merges into<file>.g.dart. Your controller file needspart '<file>.g.dart';.- For each method carrying a verb annotation (
@Get,@Post, …, or@Routeitself), the generator joins the controller path and the method path, converts{name}placeholders to shelf_router’s<name>, and classifies each parameter. - A controller with
@Controllerbut no annotated methods logs a warning and generates nothing.
Parameter classification
Section titled “Parameter classification”Each method parameter is classified in this order. The first rule that matches wins.
| # | Rule | Becomes |
|---|---|---|
| 1 | Annotated @Path, @Query, @Header or @Body |
That location, using the annotation’s name if given |
| 2 | Type is shelf Request |
Passed ctx.request; not documented |
| 3 | Type is gisila RequestContext |
Passed the context; not documented |
| 4 | Type extends gisila Form |
await bindForm(request, T.new); documented as the request body |
| 5 | Type extends gisila Service |
ctx.service<T>(); not documented |
| 6 | Name matches a {placeholder} in the full path |
Required path parameter |
| 7 | Type is String, int, double, num, bool or DateTime (nullable or not) |
Query parameter |
| 8 | Anything else, if no body has been bound yet | JSON request body |
| 9 | Anything else | Build error: Could not classify parameter … |
Details that follow from the generated code:
- Path and query values are parsed with gisila’s
coerce<T>(). A query parameter is required in the spec (and at runtime) when it is non-nullable and not optional. - Dart default values are not applied. A missing optional
{int page = 1}is passed tocoerce<int>asnull, which fails binding with a400. Declare optional query parameters nullable (int? page) and apply the default in the method body. - A header parameter is read as the raw header string, so declare it
String?. - A JSON body is read with
readJsonBody. If the type has afromJson(Map<String, dynamic>)constructor it is called;List<T>withT.fromJsonmaps each element;Map<String, ...>is cast. The body is required unless the parameter is optional or nullable. - Positional and named parameters are both supported; the generated call passes each the same way it was declared.
Response handling
Section titled “Response handling”| Method return type | Runtime behaviour | Documented success response |
|---|---|---|
Response / Future<Response> |
Returned unchanged | Status only, unless @Returns(bodyType: ...) supplies a schema |
void / Future<void> |
emptyResponse(statusCode: <success>) |
Status only |
| Anything else | jsonResponse(body: result, statusCode: <success>) |
application/json with the return type’s schema |
The success status is the verb annotation’s statusCode (200, or 201 for @Post, 204
for @Delete), overridden by the first Returns
entry when that has a statusCode.
Responses added automatically
Section titled “Responses added automatically”- 400 with the error envelope when the route binds any input (a form, a body, or a path, query or header parameter).
- 401 when the route is guarded, and 403 as well when the guards include permissions or roles. See Document authentication.
A response you declare yourself for the same status code replaces the automatic one.
Runtime
Section titled “Runtime”Each route block in attachToApp:
- builds the route’s
RouteConfigby merging the class@UseRouteConfig, the guards, the annotation’sconfig:and any@UseMiddlewarenames; - calls
gisilaRoute(app: app, config: ..., routeId: 'METHOD <path>', handler: ...); - registers it with
router.<verb>(prefix + path, ...). A route whose path ends in/(such as@Get('/')under/users) is also registered without the slash; - calls
spec.putOperation(prefix + openApiPath, verb, Operation(...)).
Argument binding runs inside a try that turns TypeError and FormatException into
BadRequestException (a 400). Exceptions thrown by your controller method are not caught
there; they reach the app’s error handler.
Schemas and controller tags are registered with spec.putSchema and spec.putTag at the top of
attachToApp.
Example
Section titled “Example”@Controller('/orders', ['Orders'])class OrdersController { @Get('/{id}') Future<Order> getOrder( String id, // path (matches {id}) OrderService orders, // service, injected {bool? expand, // query, optional @Header(name: 'X-Request-Id') String? requestId}, // header ) => orders.get(id, expand: expand ?? false);
@Post('/') Future<Order> create(CreateOrder body, OrderService orders) => orders.create(body); // body: CreateOrder.fromJson(...)}