Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions example/example_open_api.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'package:alfred/alfred.dart';

void main() async {
final app = Alfred();

app.get(
'/path/:param1',
(req, res) {
res.json({'key': 'value'});
},
middleware: [],
openAPIDoc: OpenAPIDoc(
title: 'Title of the endpoint',
description: 'Description for the endpoint',
responses: [
OpenAPIResponse(
statusCode: 200,
description: 'Success',
schema: [
OpenAPIResponseContent(
key: 'key',
type: OpenAPIType.string,
example: 'value ABC',
),
],
),
],
),
);

app.patch(
'/path/:param2',
(req, res) {
res.json({'key': 'value'});
},
middleware: [],
openAPIDoc: OpenAPIDoc(
title: 'Title of the endpoint',
description: 'Description for the endpoint',
responses: [],
request: OpenAPIRequest(
schema: [
OpenAPIResponseContent(
key: 'key',
type: OpenAPIType.string,
example: 'value ABC',
),
],
),
),
);

app.get('/openapi', (req, res) {
res.setContentTypeFromExtension('yaml');
return app.getRoutesSpecifications(
title: "Test API",
description: "Test API Description",
version: "1.2.3",
);
});

await app.listen();
}
1 change: 1 addition & 0 deletions lib/alfred.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export 'dart:io' show HttpRequest, HttpResponse;

export 'src/alfred.dart';
export 'src/alfred_exception.dart';
export 'src/alfred_openapi.dart';
export 'src/router.dart';
export 'src/body_parser/http_body.dart';
export 'src/extensions/file_helpers.dart';
Expand Down
204 changes: 204 additions & 0 deletions lib/src/alfred_openapi.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import 'package:alfred/alfred.dart';
import 'package:yaml_writer/yaml_writer.dart';

/// Enum to define the OpenAPI types
enum OpenAPIType {
string,
number,
integer,
boolean,
array,
object;

String toJson() {
return toString().split('.').last;
}
}

/// Enum to define the OpenAPI content types
enum OpenAPIContentType {
object,
array;

String toJson() {
return toString().split('.').last;
}
}

/// Extension to generate OpenAPI specifications for the routes in the Alfred app
extension AlfredOpenAPI on Alfred {
/// Returns the OpenAPI specifications for the routes in the Alfred app
/// - [title] is the title of the API
/// - [description] is the description of the API
/// - [version] is the version of the API
String getRoutesSpecifications(
{String? title, String? description, String? version}) {
var specsYaml = YamlWriter();
Map<String, dynamic> specs = {
'openapi': '3.0.0',
'info': {
'title': title ?? 'API',
'description': description ?? 'API Description',
'version': version ?? '1.0.0'
},
'paths': [],
};
for (var route in routes) {
String methodString = route.method.name;
var routeParameters = <Map<String, dynamic>>[];

for (var parameter in route.params) {
routeParameters.add({
'name': parameter.name,
'in': 'query',
'required': true,
'schema': {'type': parameter.type?.name ?? 'string'}
});
}

(specs['paths'] as List).add({
route.route: {
methodString: {
'summary': route.openAPIDoc?.title ?? 'Summary',
if (route.openAPIDoc?.description != null)
'description': route.openAPIDoc!.description,
'parameters': routeParameters,
'responses': {
if (route.openAPIDoc?.responses != null)
for (var response in route.openAPIDoc!.responses)
...response.toJson(),
if (route.openAPIDoc?.responses == null ||
route.openAPIDoc!.responses.isEmpty)
'200': {
'description': 'Success',
}
},
if (route.openAPIDoc?.request != null)
'requestBody': {
'content': {
route.openAPIDoc!.request!.contentType: {
'schema': {
'type': route.openAPIDoc!.request!.content.toJson(),
if (route.openAPIDoc!.request!.content ==
OpenAPIContentType.object)
'properties': {
for (var item in route.openAPIDoc!.request!.schema)
...item.toJson(),
},
if (route.openAPIDoc!.request!.content ==
OpenAPIContentType.array)
'items': {
'type': route.openAPIDoc!.request!.schema.first.type
.toJson(),
if (route.openAPIDoc!.request!.schema.first.example !=
null)
'example':
route.openAPIDoc!.request!.schema.first.example,
},
},
},
},
},
}
}
});
}

return specsYaml.write(specs).replaceAll('- /', '/');
}
}

/// Class to define the OpenAPI documentation
class OpenAPIDoc {
final String title;
final String? description;
final List<OpenAPIResponse> responses;
final OpenAPIRequest? request;

OpenAPIDoc({
required this.title,
this.description,
required this.responses,
this.request,
});
}

/// Class to define the OpenAPI request
class OpenAPIRequest {
final String contentType;
final OpenAPIContentType content;
final List<OpenAPIResponseContent> schema;

OpenAPIRequest({
this.contentType = 'application/json',
this.content = OpenAPIContentType.object,
required this.schema,
});
}

/// Class to define the OpenAPI response
class OpenAPIResponse {
final int statusCode;
final String? description;
final String contentType;
final OpenAPIContentType content;
final List<OpenAPIResponseContent> schema;

OpenAPIResponse({
required this.statusCode,
this.description,
this.contentType = 'application/json',
this.content = OpenAPIContentType.object,
required this.schema,
});

Map<String, dynamic> toJson() {
return {
'$statusCode': {
'description': description,
'content': {
contentType: {
'schema': {
'type': content.toJson(),
if (content == OpenAPIContentType.object)
'properties': {
for (var item in schema) ...item.toJson(),
},
if (content == OpenAPIContentType.array)
'items': {
'type': schema.first.type.toJson(),
if (schema.first.example != null)
'example': schema.first.example,
},
},
},
},
}
};
}
}

/// Class to define the OpenAPI response content
class OpenAPIResponseContent {
final String key;
final OpenAPIType type;
final dynamic example;
final bool required;

OpenAPIResponseContent({
required this.key,
required this.type,
this.example,
this.required = true,
});

Map<String, dynamic> toJson() {
return {
key: {
'type': type.toJson(),
// 'required': required,
if (example != null) 'example': example,
}
};
}
}
14 changes: 11 additions & 3 deletions lib/src/http_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import '../alfred.dart';
class HttpRoute {
final Method method;
final String route;

/// The OpenAPI documentation for this route
final OpenAPIDoc? openAPIDoc;

final FutureOr Function(HttpRequest req, HttpResponse res) callback;
final List<FutureOr Function(HttpRequest req, HttpResponse res)> middleware;

Expand All @@ -20,9 +24,13 @@ class HttpRoute {

Iterable<HttpRouteParam> get params => _params.values;

HttpRoute(this.route, this.callback, this.method,
{this.middleware = const []})
: usesWildcardMatcher = route.contains('*') {
HttpRoute(
this.route,
this.callback,
this.method, {
this.middleware = const [],
this.openAPIDoc,
}) : usesWildcardMatcher = route.contains('*') {
// Split route path into segments

/// Because in dart 2.18 uri parsing is more permissive, using a \ in regex
Expand Down
Loading