Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Auto generate swagger docs #136

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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
24 changes: 24 additions & 0 deletions packages/pharaoh/lib/src/_next/_core/core_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,30 @@ class _PharaohNextImpl implements Application {
void useRoutes(RoutesResolver routeResolver) {
final routes = routeResolver.call();
routes.forEach((route) => route.commit(_spanner));

final openAPiRoutes = routes.fold(
<OpenApiRoute>[], (preV, curr) => preV..addAll(curr.openAPIRoutes));

final result = OpenApiGenerator.generateOpenApi(
openAPiRoutes,
apiName: _appConfig.name,
serverUrls: [_appConfig.url],
);

final openApiFile = File('openapi.json');
openApiFile.writeAsStringSync(JsonEncoder.withIndent(' ').convert(result));

Route.route(HTTPMethod.GET, '/swagger', (req, res) {
return res
.header(HttpHeaders.contentTypeHeader, ContentType.html.value)
.send(OpenApiGenerator.renderDocsPage('/swagger.json'));
}).commit(_spanner);

Route.route(HTTPMethod.GET, '/swagger.json', (_, res) {
return res
.header(HttpHeaders.contentTypeHeader, ContentType.json.value)
.send(openApiFile.openRead());
}).commit(_spanner);
}

@override
Expand Down
19 changes: 16 additions & 3 deletions packages/pharaoh/lib/src/_next/_core/reflector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,14 @@ ControllerMethod parseControllerMethod(ControllerMethodDefinition defn) {
methods.firstWhereOrNull((e) => e.simpleName == symbolToString(method));
if (actualMethod == null) {
throw ArgumentError(
'$type does not have method #${symbolToString(method)}');
'$type does not have method #${symbolToString(method)}',
);
}

final returnType = getActualType(actualMethod.reflectedReturnType);

final parameters = actualMethod.parameters;
if (parameters.isEmpty) return ControllerMethod(defn);
if (parameters.isEmpty) return ControllerMethod(defn, returnType: returnType);

if (parameters.any((e) => e.metadata.length > 1)) {
throw ArgumentError(
Expand Down Expand Up @@ -131,7 +134,17 @@ ControllerMethod parseControllerMethod(ControllerMethodDefinition defn) {
);
});

return ControllerMethod(defn, params);
return ControllerMethod(defn, params: params, returnType: returnType);
}

final _regex = RegExp(r"^(\w+)<(.+)>$");
Type? getActualType(Type type) {
final match = _regex.firstMatch(type.toString());
if (match != null) {
return q.data[inject]!.types
.firstWhereOrNull((type) => type.toString() == match.group(2));
}
return type;
}

BaseDTO? _tryResolveDtoInstance(Type type) {
Expand Down
57 changes: 53 additions & 4 deletions packages/pharaoh/lib/src/_next/_router/definition.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,26 @@ class RouteMapping {
}
}

typedef OpenApiRoute = ({
List<String> tags,
Type? returnType,
HTTPMethod method,
String route,
List<ControllerMethodParam> args,
});

abstract class RouteDefinition {
late RouteMapping route;
final RouteDefinitionType type;

String? group;

RouteDefinition(this.type);

void commit(Spanner spanner);

List<OpenApiRoute> get openAPIRoutes;

RouteDefinition _prefix(String prefix) => this..route = route.prefix(prefix);
}

Expand All @@ -53,7 +65,8 @@ class UseAliasedMiddleware {

RouteGroupDefinition routes(List<RouteDefinition> routes) {
return RouteGroupDefinition._(
BASE_PATH,
alias,
prefix: BASE_PATH,
definitions: routes,
)..middleware(mdw);
}
Expand All @@ -69,19 +82,27 @@ class _MiddlewareDefinition extends RouteDefinition {

@override
void commit(Spanner spanner) => spanner.addMiddleware(route.path, mdw);

@override
List<OpenApiRoute> get openAPIRoutes => const [];
}

typedef ControllerMethodDefinition = (Type controller, Symbol symbol);

class ControllerMethod {
final Type? returnType;
final ControllerMethodDefinition method;
final Iterable<ControllerMethodParam> params;

String get methodName => symbolToString(method.$2);

Type get controller => method.$1;

ControllerMethod(this.method, [this.params = const []]);
ControllerMethod(
this.method, {
this.params = const [],
this.returnType,
});
}

class ControllerMethodParam {
Expand Down Expand Up @@ -121,6 +142,17 @@ class ControllerRouteMethodDefinition extends RouteDefinition {
spanner.addRoute(routeMethod, route.path, useRequestHandler(handler));
}
}

@override
List<OpenApiRoute> get openAPIRoutes => route.methods
.map((e) => (
route: route.path,
method: e,
args: method.params.toList(),
returnType: method.returnType,
tags: <String>[if (group != null) group!]
))
.toList();
}

class RouteGroupDefinition extends RouteDefinition {
Expand All @@ -146,12 +178,12 @@ class RouteGroupDefinition extends RouteDefinition {
void _unwrapRoutes(Iterable<RouteDefinition> routes) {
for (final subRoute in routes) {
if (subRoute is! RouteGroupDefinition) {
defns.add(subRoute._prefix(route.path));
defns.add(subRoute._prefix(route.path)..group = name);
continue;
}

for (var e in subRoute.defns) {
defns.add(e._prefix(route.path));
defns.add(e._prefix(route.path)..group = subRoute.name);
}
}
}
Expand All @@ -169,6 +201,12 @@ class RouteGroupDefinition extends RouteDefinition {
mdw.commit(spanner);
}
}

@override
List<OpenApiRoute> get openAPIRoutes => defns.fold(
[],
(preV, c) => preV..addAll(c.openAPIRoutes),
);
}

typedef RequestHandlerWithApp = Function(
Expand Down Expand Up @@ -208,4 +246,15 @@ class FunctionalRouteDefinition extends RouteDefinition {
spanner.addRoute<Middleware>(method, path, _requestHandler!);
}
}

@override
List<OpenApiRoute> get openAPIRoutes => [
(
args: [],
returnType: Response,
method: method,
route: route.path,
tags: <String>[if (group != null) group!]
)
];
}
4 changes: 2 additions & 2 deletions packages/pharaoh/lib/src/_next/_router/meta.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
part of '../router.dart';

abstract class RequestAnnotation<T> {
sealed class RequestAnnotation<T> {
final String? name;

const RequestAnnotation([this.name]);
Expand Down Expand Up @@ -61,7 +61,7 @@ class Body extends RequestAnnotation {
}

final dtoInstance = methodParam.dto;
if (dtoInstance != null) return dtoInstance..make(request);
if (dtoInstance != null) return dtoInstance..validate(request);

final type = methodParam.type;
if (type != dynamic && body.runtimeType != type) {
Expand Down
35 changes: 22 additions & 13 deletions packages/pharaoh/lib/src/_next/_validation/dto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const dtoReflector = DtoReflector();
abstract interface class _BaseDTOImpl {
late Map<String, dynamic> data;

void make(Request request) {
void validate(Request request) {
data = const {};
final (result, errors) = schema.validateSync(request.body ?? {});
if (errors.isNotEmpty) {
Expand All @@ -29,15 +29,12 @@ abstract interface class _BaseDTOImpl {
data = Map<String, dynamic>.from(result);
}

EzSchema? _schemaCache;

EzSchema get schema {
if (_schemaCache != null) return _schemaCache!;

final mirror = dtoReflector.reflectType(runtimeType) as r.ClassMirror;
final properties = mirror.getters.where((e) => e.isAbstract);

final entries = properties.map((prop) {
r.ClassMirror? _classMirrorCache;
Iterable<({String name, Type type, ClassPropertyValidator meta})>
get properties {
_classMirrorCache ??=
dtoReflector.reflectType(runtimeType) as r.ClassMirror;
return _classMirrorCache!.getters.where((e) => e.isAbstract).map((prop) {
final returnType = prop.reflectedReturnType;
final meta =
prop.metadata.whereType<ClassPropertyValidator>().firstOrNull ??
Expand All @@ -48,11 +45,23 @@ abstract interface class _BaseDTOImpl {
'Type Mismatch between ${meta.runtimeType}(${meta.propertyType}) & $runtimeType class property ${prop.simpleName}->($returnType)');
}

return MapEntry(meta.name ?? prop.simpleName, meta.validator);
return (
name: (meta.name ?? prop.simpleName),
meta: meta,
type: returnType,
);
});
}

EzSchema? _schemaCache;
EzSchema get schema {
if (_schemaCache != null) return _schemaCache!;

final entriesToMap = properties.fold<Map<String, EzValidator<dynamic>>>(
{},
(prev, curr) => prev..[curr.name] = curr.meta.validator,
);

final entriesToMap = entries.fold<Map<String, EzValidator<dynamic>>>(
{}, (prev, curr) => prev..[curr.key] = curr.value);
return _schemaCache = EzSchema.shape(entriesToMap);
}
}
Expand Down
29 changes: 18 additions & 11 deletions packages/pharaoh/lib/src/_next/core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:io';

import 'package:pharaoh/pharaoh.dart';
import 'package:reflectable/reflectable.dart' as r;
import 'package:reflectable/src/reflectable_builder_based.dart' as q;
import 'package:spanner/spanner.dart';
import 'package:spookie/spookie.dart' as spookie;
import 'package:collection/collection.dart';
Expand All @@ -14,6 +15,7 @@ import 'package:get_it/get_it.dart';
import 'package:meta/meta.dart';

import 'http.dart';
import 'openapi.dart';
import 'router.dart';
import 'validation.dart';

Expand Down Expand Up @@ -112,36 +114,36 @@ abstract class ApplicationFactory {
final spanner = Spanner()..addMiddleware('/', bodyParser);
Application._instance = _PharaohNextImpl(config, spanner);

final providerInstances = providers.map(createNewInstance<ServiceProvider>);
final providerInstances = providers
.map(createNewInstance<ServiceProvider>)
.toList(growable: false);

/// register dependencies
for (final instance in providerInstances) {
await Future.sync(instance.register);
}
await providerInstances
.map((provider) => Future.sync(provider.register))
.wait;

if (globalMiddleware != null) {
spanner.addMiddleware<Middleware>('/', globalMiddleware!);
}

/// boot providers
for (final provider in providerInstances) {
await Future.sync(provider.boot);
}
await providerInstances.map((provider) => Future.sync(provider.boot)).wait;
}

static RequestHandler buildControllerMethod(ControllerMethod method) {
final params = method.params;
final methodName = method.methodName;

return (req, res) {
final methodName = method.methodName;
return (req, res) async {
final instance = createNewInstance<HTTPController>(method.controller);
final mirror = inject.reflect(instance);

mirror
..invokeSetter('request', req)
..invokeSetter('response', res);

late Function() methodCall;
Function methodCall;

if (params.isNotEmpty) {
final args = _resolveControllerMethodArgs(req, method);
Expand All @@ -150,7 +152,12 @@ abstract class ApplicationFactory {
methodCall = () => mirror.invoke(methodName, []);
}

return Future.sync(methodCall);
try {
final result = await methodCall.call();
return result is Response ? result : res.json(result);
} on Response catch (response) {
return response;
}
};
}

Expand Down
Loading