From 3692b9313db0a1411b47324a24ffec592255a0ac Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Wed, 13 Nov 2024 10:21:38 +0300 Subject: [PATCH 1/7] wip --- .../lib/src/_next/_core/core_impl.dart | 12 ++ .../lib/src/_next/_core/reflector.dart | 3 +- .../lib/src/_next/_router/definition.dart | 26 ++++ .../pharaoh/lib/src/_next/_router/meta.dart | 4 +- .../lib/src/_next/_validation/dto.dart | 35 +++-- packages/pharaoh/lib/src/_next/openapi.dart | 137 ++++++++++++++++++ .../validation/validation_test.dart | 6 +- 7 files changed, 204 insertions(+), 19 deletions(-) create mode 100644 packages/pharaoh/lib/src/_next/openapi.dart diff --git a/packages/pharaoh/lib/src/_next/_core/core_impl.dart b/packages/pharaoh/lib/src/_next/_core/core_impl.dart index 3b097969..3dfa0335 100644 --- a/packages/pharaoh/lib/src/_next/_core/core_impl.dart +++ b/packages/pharaoh/lib/src/_next/_core/core_impl.dart @@ -20,6 +20,18 @@ class _PharaohNextImpl implements Application { void useRoutes(RoutesResolver routeResolver) { final routes = routeResolver.call(); routes.forEach((route) => route.commit(_spanner)); + + final openAPiRoutes = routes.fold( + [], (preV, curr) => preV..addAll(curr.openAPIRoutes)); + + final result = OpenApiGenerator.generateOpenApi( + openAPiRoutes, + apiName: _appConfig.name, + serverUrls: [_appConfig.url], + ); + + File('openapi.json') + .writeAsStringSync(JsonEncoder.withIndent(' ').convert(result)); } @override diff --git a/packages/pharaoh/lib/src/_next/_core/reflector.dart b/packages/pharaoh/lib/src/_next/_core/reflector.dart index 3148d0e0..3015ba5a 100644 --- a/packages/pharaoh/lib/src/_next/_core/reflector.dart +++ b/packages/pharaoh/lib/src/_next/_core/reflector.dart @@ -99,7 +99,8 @@ 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 parameters = actualMethod.parameters; diff --git a/packages/pharaoh/lib/src/_next/_router/definition.dart b/packages/pharaoh/lib/src/_next/_router/definition.dart index 01a4e7f5..c1efb90f 100644 --- a/packages/pharaoh/lib/src/_next/_router/definition.dart +++ b/packages/pharaoh/lib/src/_next/_router/definition.dart @@ -23,6 +23,12 @@ class RouteMapping { } } +typedef OpenApiRoute = ({ + HTTPMethod method, + String route, + List args, +}); + abstract class RouteDefinition { late RouteMapping route; final RouteDefinitionType type; @@ -31,6 +37,8 @@ abstract class RouteDefinition { void commit(Spanner spanner); + List get openAPIRoutes; + RouteDefinition _prefix(String prefix) => this..route = route.prefix(prefix); } @@ -69,6 +77,9 @@ class _MiddlewareDefinition extends RouteDefinition { @override void commit(Spanner spanner) => spanner.addMiddleware(route.path, mdw); + + @override + List get openAPIRoutes => const []; } typedef ControllerMethodDefinition = (Type controller, Symbol symbol); @@ -121,6 +132,11 @@ class ControllerRouteMethodDefinition extends RouteDefinition { spanner.addRoute(routeMethod, route.path, useRequestHandler(handler)); } } + + @override + List get openAPIRoutes => route.methods + .map((e) => (route: route.path, method: e, args: method.params.toList())) + .toList(); } class RouteGroupDefinition extends RouteDefinition { @@ -169,6 +185,12 @@ class RouteGroupDefinition extends RouteDefinition { mdw.commit(spanner); } } + + @override + List get openAPIRoutes => defns.fold( + [], + (preV, c) => preV..addAll(c.openAPIRoutes), + ); } typedef RequestHandlerWithApp = Function( @@ -208,4 +230,8 @@ class FunctionalRouteDefinition extends RouteDefinition { spanner.addRoute(method, path, _requestHandler!); } } + + @override + List get openAPIRoutes => + [(args: [], method: method, route: route.path)]; } diff --git a/packages/pharaoh/lib/src/_next/_router/meta.dart b/packages/pharaoh/lib/src/_next/_router/meta.dart index 43c334ea..4670aa87 100644 --- a/packages/pharaoh/lib/src/_next/_router/meta.dart +++ b/packages/pharaoh/lib/src/_next/_router/meta.dart @@ -1,6 +1,6 @@ part of '../router.dart'; -abstract class RequestAnnotation { +sealed class RequestAnnotation { final String? name; const RequestAnnotation([this.name]); @@ -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) { diff --git a/packages/pharaoh/lib/src/_next/_validation/dto.dart b/packages/pharaoh/lib/src/_next/_validation/dto.dart index 5b3f1b16..300af9a2 100644 --- a/packages/pharaoh/lib/src/_next/_validation/dto.dart +++ b/packages/pharaoh/lib/src/_next/_validation/dto.dart @@ -20,7 +20,7 @@ const dtoReflector = DtoReflector(); abstract interface class _BaseDTOImpl { late Map data; - void make(Request request) { + void validate(Request request) { data = const {}; final (result, errors) = schema.validateSync(request.body ?? {}); if (errors.isNotEmpty) { @@ -29,15 +29,12 @@ abstract interface class _BaseDTOImpl { data = Map.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().firstOrNull ?? @@ -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>>( + {}, + (prev, curr) => prev..[curr.name] = curr.meta.validator, + ); - final entriesToMap = entries.fold>>( - {}, (prev, curr) => prev..[curr.key] = curr.value); return _schemaCache = EzSchema.shape(entriesToMap); } } diff --git a/packages/pharaoh/lib/src/_next/openapi.dart b/packages/pharaoh/lib/src/_next/openapi.dart new file mode 100644 index 00000000..91a81300 --- /dev/null +++ b/packages/pharaoh/lib/src/_next/openapi.dart @@ -0,0 +1,137 @@ +import 'package:collection/collection.dart'; +import 'package:pharaoh/src/_next/router.dart'; + +class OpenApiGenerator { + static Map generateOpenApi( + List routes, { + required String apiName, + required List serverUrls, + }) { + return { + "openapi": "3.0.0", + "info": {"title": apiName, "version": "1.0.0"}, + "servers": serverUrls.map((e) => {'url': e}).toList(), + "paths": _generatePaths(routes), + "components": {"schemas": _generateSchemas(routes)} + }; + } + + static Map _generatePaths(List routes) { + final paths = >{}; + + for (final route in routes) { + final pathParams = route.args.where((e) => e.meta is Param).toList(); + final bodyParam = route.args.firstWhereOrNull((e) => e is Body); + + var path = route.route; + // Convert Express-style path params (:id) to OpenAPI style ({id}) + for (final param in pathParams) { + path = path.replaceAll('<${param.name}>', '{${param.name}}'); + } + + paths[path] = paths[path] ?? {}; + paths[path]![route.method.name.toLowerCase()] = { + "summary": "", // Could be added as a parameter + "parameters": _generateParameters(route.args), + "responses": { + "200": {"description": "Successful response"} + } + }; + + if (bodyParam != null) { + paths[path]![route.method.name.toLowerCase()]["requestBody"] = { + "required": !bodyParam.optional, + "content": { + "application/json": {"schema": _generateSchema(bodyParam)} + } + }; + } + } + + return paths; + } + + static List> _generateParameters( + List args, + ) { + final parameters = >[]; + + for (final arg in args) { + final parameterLocation = _getParameterLocation(arg.meta); + if (parameterLocation == null) continue; + + final parameterSchema = _generateSchema(arg); + + // Add default value if available and not a path parameter + if (arg.defaultValue != null && parameterLocation != "path") { + parameterSchema["default"] = arg.defaultValue; + } + + final param = { + "name": arg.name, + "in": parameterLocation, + "required": parameterLocation == "path" ? true : !arg.optional, + "schema": parameterSchema, + }; + + parameters.add(param); + } + + return parameters; + } + + static String? _getParameterLocation(RequestAnnotation? annotation) { + return switch (annotation) { + const Header() => "header", + const Query() => "query", + const Param() => "path", + _ => null, + }; + } + + static Map _generateSchema(ControllerMethodParam param) { + if (param.dto != null) { + return { + "\$ref": "#/components/schemas/${param.dto.runtimeType.toString()}" + }; + } + + return _typeToOpenApiType(param.type); + } + + static Map _typeToOpenApiType(Type type) { + switch (type.toString()) { + case "String": + return {"type": "string"}; + case "int": + return {"type": "integer", "format": "int32"}; + case "double": + return {"type": "number", "format": "double"}; + case "bool": + return {"type": "boolean"}; + case "DateTime": + return {"type": "string", "format": "date-time"}; + default: + return {"type": "object"}; + } + } + + static Map _generateSchemas(List routes) { + final schemas = {}; + + for (final route in routes) { + for (final arg in route.args) { + final dto = arg.dto; + if (dto == null) continue; + + schemas[dto.runtimeType.toString()] = { + "type": "object", + "properties": dto.properties.fold({}, + (preV, curr) => preV..[curr.name] = _typeToOpenApiType(curr.type)) + }; + } + } + + return schemas; + } +} diff --git a/packages/pharaoh/test/pharaoh_next/validation/validation_test.dart b/packages/pharaoh/test/pharaoh_next/validation/validation_test.dart index 9046713e..3d632794 100644 --- a/packages/pharaoh/test/pharaoh_next/validation/validation_test.dart +++ b/packages/pharaoh/test/pharaoh_next/validation/validation_test.dart @@ -114,7 +114,7 @@ void main() { final app = pharaoh ..post('/', (req, res) { - dto.make(req); + dto.validate(req); return res.json({ 'firstname': dto.username, 'lastname': dto.lastname, @@ -147,7 +147,7 @@ void main() { final app = pharaoh ..post('/optional', (req, res) { - dto.make(req); + dto.validate(req); return res.json({ 'nationality': dto.nationality, @@ -197,7 +197,7 @@ void main() { final dto = DTOTypeMismatch(); pharaoh.post('/type-mismatch', (req, res) { - dto.make(req); + dto.validate(req); return res.ok('Foo Bar'); }); From 9e77df1ec7e522bc164749bb54656aada8b923c5 Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Wed, 13 Nov 2024 10:21:52 +0300 Subject: [PATCH 2/7] _ --- packages/pharaoh/lib/src/_next/core.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pharaoh/lib/src/_next/core.dart b/packages/pharaoh/lib/src/_next/core.dart index c857ae3a..b82243dd 100644 --- a/packages/pharaoh/lib/src/_next/core.dart +++ b/packages/pharaoh/lib/src/_next/core.dart @@ -14,6 +14,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'; From 3a6caede3fd03273d4a66d4d2c6874f2b1ab16ac Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Wed, 13 Nov 2024 10:55:37 +0300 Subject: [PATCH 3/7] _ --- .../lib/src/_next/_router/definition.dart | 27 ++++++++++++++----- packages/pharaoh/lib/src/_next/openapi.dart | 14 ++++++---- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/pharaoh/lib/src/_next/_router/definition.dart b/packages/pharaoh/lib/src/_next/_router/definition.dart index c1efb90f..d0494ad6 100644 --- a/packages/pharaoh/lib/src/_next/_router/definition.dart +++ b/packages/pharaoh/lib/src/_next/_router/definition.dart @@ -24,6 +24,7 @@ class RouteMapping { } typedef OpenApiRoute = ({ + List tags, HTTPMethod method, String route, List args, @@ -33,6 +34,8 @@ abstract class RouteDefinition { late RouteMapping route; final RouteDefinitionType type; + String? group; + RouteDefinition(this.type); void commit(Spanner spanner); @@ -61,7 +64,8 @@ class UseAliasedMiddleware { RouteGroupDefinition routes(List routes) { return RouteGroupDefinition._( - BASE_PATH, + alias, + prefix: BASE_PATH, definitions: routes, )..middleware(mdw); } @@ -135,7 +139,12 @@ class ControllerRouteMethodDefinition extends RouteDefinition { @override List get openAPIRoutes => route.methods - .map((e) => (route: route.path, method: e, args: method.params.toList())) + .map((e) => ( + route: route.path, + method: e, + args: method.params.toList(), + tags: [if (group != null) group!] + )) .toList(); } @@ -162,12 +171,12 @@ class RouteGroupDefinition extends RouteDefinition { void _unwrapRoutes(Iterable 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); } } } @@ -232,6 +241,12 @@ class FunctionalRouteDefinition extends RouteDefinition { } @override - List get openAPIRoutes => - [(args: [], method: method, route: route.path)]; + List get openAPIRoutes => [ + ( + args: [], + method: method, + route: route.path, + tags: [if (group != null) group!] + ) + ]; } diff --git a/packages/pharaoh/lib/src/_next/openapi.dart b/packages/pharaoh/lib/src/_next/openapi.dart index 91a81300..ff5e5fbf 100644 --- a/packages/pharaoh/lib/src/_next/openapi.dart +++ b/packages/pharaoh/lib/src/_next/openapi.dart @@ -21,25 +21,29 @@ class OpenApiGenerator { for (final route in routes) { final pathParams = route.args.where((e) => e.meta is Param).toList(); - final bodyParam = route.args.firstWhereOrNull((e) => e is Body); + final bodyParam = route.args.firstWhereOrNull((e) => e.meta is Body); + final parameters = _generateParameters(route.args); + final routeMethod = route.method.name.toLowerCase(); var path = route.route; + // Convert Express-style path params (:id) to OpenAPI style ({id}) for (final param in pathParams) { path = path.replaceAll('<${param.name}>', '{${param.name}}'); } paths[path] = paths[path] ?? {}; - paths[path]![route.method.name.toLowerCase()] = { - "summary": "", // Could be added as a parameter - "parameters": _generateParameters(route.args), + paths[path]![routeMethod] = { + "summary": "", + if (parameters.isNotEmpty) "parameters": parameters, + if (route.tags.isNotEmpty) "tags": route.tags, "responses": { "200": {"description": "Successful response"} } }; if (bodyParam != null) { - paths[path]![route.method.name.toLowerCase()]["requestBody"] = { + paths[path]![routeMethod]["requestBody"] = { "required": !bodyParam.optional, "content": { "application/json": {"schema": _generateSchema(bodyParam)} From 56d8c588d6ca08221bf96ee23387c438bfda6503 Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Wed, 13 Nov 2024 16:12:42 +0300 Subject: [PATCH 4/7] chore: allow returning objects not response --- packages/pharaoh/lib/src/_next/core.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/pharaoh/lib/src/_next/core.dart b/packages/pharaoh/lib/src/_next/core.dart index b82243dd..e27428d6 100644 --- a/packages/pharaoh/lib/src/_next/core.dart +++ b/packages/pharaoh/lib/src/_next/core.dart @@ -132,9 +132,9 @@ abstract class ApplicationFactory { 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(method.controller); final mirror = inject.reflect(instance); @@ -142,7 +142,7 @@ abstract class ApplicationFactory { ..invokeSetter('request', req) ..invokeSetter('response', res); - late Function() methodCall; + Function methodCall; if (params.isNotEmpty) { final args = _resolveControllerMethodArgs(req, method); @@ -151,7 +151,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; + } }; } From abc0301a748b498ce1968233741cbda1ae7f1f08 Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Wed, 13 Nov 2024 16:59:17 +0300 Subject: [PATCH 5/7] chore: mount docs.json & swagger --- .../lib/src/_next/_core/core_impl.dart | 16 +++++++-- packages/pharaoh/lib/src/_next/openapi.dart | 33 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/pharaoh/lib/src/_next/_core/core_impl.dart b/packages/pharaoh/lib/src/_next/_core/core_impl.dart index 3dfa0335..99bdb541 100644 --- a/packages/pharaoh/lib/src/_next/_core/core_impl.dart +++ b/packages/pharaoh/lib/src/_next/_core/core_impl.dart @@ -30,8 +30,20 @@ class _PharaohNextImpl implements Application { serverUrls: [_appConfig.url], ); - File('openapi.json') - .writeAsStringSync(JsonEncoder.withIndent(' ').convert(result)); + 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 diff --git a/packages/pharaoh/lib/src/_next/openapi.dart b/packages/pharaoh/lib/src/_next/openapi.dart index ff5e5fbf..6b943c97 100644 --- a/packages/pharaoh/lib/src/_next/openapi.dart +++ b/packages/pharaoh/lib/src/_next/openapi.dart @@ -138,4 +138,37 @@ class OpenApiGenerator { return schemas; } + + static String renderDocsPage(String openApiRoute) { + return ''' + + + + + + + SwaggerUI + + + +
+ + + + + +'''; + } } From 8b2ed5edb29b6b4f630105387f57559ce79064d9 Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Sat, 23 Nov 2024 10:32:02 +0300 Subject: [PATCH 6/7] _ --- .../lib/src/_next/_core/reflector.dart | 16 +++++- .../lib/src/_next/_router/definition.dart | 10 +++- packages/pharaoh/lib/src/_next/core.dart | 1 + packages/pharaoh/lib/src/_next/openapi.dart | 52 ++++++++++++++++--- packages/pharaoh/lib/src/_next/router.dart | 19 ++++--- .../core/application_factory_test.dart | 2 +- 6 files changed, 82 insertions(+), 18 deletions(-) diff --git a/packages/pharaoh/lib/src/_next/_core/reflector.dart b/packages/pharaoh/lib/src/_next/_core/reflector.dart index 3015ba5a..51f817d5 100644 --- a/packages/pharaoh/lib/src/_next/_core/reflector.dart +++ b/packages/pharaoh/lib/src/_next/_core/reflector.dart @@ -103,8 +103,10 @@ ControllerMethod parseControllerMethod(ControllerMethodDefinition defn) { ); } + 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( @@ -132,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) { diff --git a/packages/pharaoh/lib/src/_next/_router/definition.dart b/packages/pharaoh/lib/src/_next/_router/definition.dart index d0494ad6..29297a6f 100644 --- a/packages/pharaoh/lib/src/_next/_router/definition.dart +++ b/packages/pharaoh/lib/src/_next/_router/definition.dart @@ -25,6 +25,7 @@ class RouteMapping { typedef OpenApiRoute = ({ List tags, + Type? returnType, HTTPMethod method, String route, List args, @@ -89,6 +90,7 @@ class _MiddlewareDefinition extends RouteDefinition { typedef ControllerMethodDefinition = (Type controller, Symbol symbol); class ControllerMethod { + final Type? returnType; final ControllerMethodDefinition method; final Iterable params; @@ -96,7 +98,11 @@ class ControllerMethod { Type get controller => method.$1; - ControllerMethod(this.method, [this.params = const []]); + ControllerMethod( + this.method, { + this.params = const [], + this.returnType, + }); } class ControllerMethodParam { @@ -143,6 +149,7 @@ class ControllerRouteMethodDefinition extends RouteDefinition { route: route.path, method: e, args: method.params.toList(), + returnType: method.returnType, tags: [if (group != null) group!] )) .toList(); @@ -244,6 +251,7 @@ class FunctionalRouteDefinition extends RouteDefinition { List get openAPIRoutes => [ ( args: [], + returnType: Response, method: method, route: route.path, tags: [if (group != null) group!] diff --git a/packages/pharaoh/lib/src/_next/core.dart b/packages/pharaoh/lib/src/_next/core.dart index e27428d6..37e5be83 100644 --- a/packages/pharaoh/lib/src/_next/core.dart +++ b/packages/pharaoh/lib/src/_next/core.dart @@ -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'; diff --git a/packages/pharaoh/lib/src/_next/openapi.dart b/packages/pharaoh/lib/src/_next/openapi.dart index 6b943c97..6384fe95 100644 --- a/packages/pharaoh/lib/src/_next/openapi.dart +++ b/packages/pharaoh/lib/src/_next/openapi.dart @@ -1,5 +1,5 @@ import 'package:collection/collection.dart'; -import 'package:pharaoh/src/_next/router.dart'; +import 'package:pharaoh/pharaoh_next.dart'; class OpenApiGenerator { static Map generateOpenApi( @@ -38,7 +38,17 @@ class OpenApiGenerator { if (parameters.isNotEmpty) "parameters": parameters, if (route.tags.isNotEmpty) "tags": route.tags, "responses": { - "200": {"description": "Successful response"} + "200": { + "description": "Successful response", + if (route.returnType != null && route.returnType != Response) + "content": { + "application/json": { + "schema": { + "\$ref": "#/components/schemas/${route.returnType}" + }, + }, + } + } } }; @@ -104,18 +114,31 @@ class OpenApiGenerator { } static Map _typeToOpenApiType(Type type) { - switch (type.toString()) { - case "String": + switch (type) { + case const (String): return {"type": "string"}; - case "int": + case const (int): return {"type": "integer", "format": "int32"}; - case "double": + case const (double): return {"type": "number", "format": "double"}; - case "bool": + case const (bool): return {"type": "boolean"}; - case "DateTime": + case const (DateTime): return {"type": "string", "format": "date-time"}; default: + final actualType = getActualType(type); + if (actualType == null) return {"type": "object"}; + + // final properties = []; + + // ClassMirror? clazz = reflectType(actualType); + // while (clazz?.superclass != null) { + // properties.addAll(clazz!.variables); + // clazz = clazz.superclass; + // } + + // print(properties); + return {"type": "object"}; } } @@ -124,6 +147,7 @@ class OpenApiGenerator { final schemas = {}; for (final route in routes) { + final returnType = route.returnType; for (final arg in route.args) { final dto = arg.dto; if (dto == null) continue; @@ -134,6 +158,18 @@ class OpenApiGenerator { (preV, curr) => preV..[curr.name] = _typeToOpenApiType(curr.type)) }; } + + if (returnType == null || returnType == Response) continue; + + final properties = reflectType(returnType).variables; + + schemas[returnType.toString()] = { + "type": "object", + "properties": properties.fold( + {}, + (preV, curr) => preV + ..[curr.simpleName] = _typeToOpenApiType(curr.reflectedType)) + }; } return schemas; diff --git a/packages/pharaoh/lib/src/_next/router.dart b/packages/pharaoh/lib/src/_next/router.dart index 232c10fd..60aba676 100644 --- a/packages/pharaoh/lib/src/_next/router.dart +++ b/packages/pharaoh/lib/src/_next/router.dart @@ -73,9 +73,13 @@ abstract interface class Route { return ControllerRouteMethodDefinition(defn, mapping); } - static RouteGroupDefinition group(String name, List routes, - {String? prefix}) => - RouteGroupDefinition._(name, definitions: routes, prefix: prefix); + static RouteGroupDefinition group( + String name, + List routes, { + String? prefix, + }) { + return RouteGroupDefinition._(name, definitions: routes, prefix: prefix); + } static RouteGroupDefinition resource(String resource, Type controller, {String? parameterName}) { @@ -103,6 +107,9 @@ abstract interface class Route { Route.route(method, '/*', handler); } -Middleware useAliasedMiddleware(String alias) => - ApplicationFactory.resolveMiddlewareForGroup(alias) - .reduce((val, e) => val.chain(e)); +@inject +abstract mixin class ApiResource { + const ApiResource(); + + Map toJson(); +} diff --git a/packages/pharaoh/test/pharaoh_next/core/application_factory_test.dart b/packages/pharaoh/test/pharaoh_next/core/application_factory_test.dart index 0ca92747..67db7673 100644 --- a/packages/pharaoh/test/pharaoh_next/core/application_factory_test.dart +++ b/packages/pharaoh/test/pharaoh_next/core/application_factory_test.dart @@ -35,7 +35,7 @@ void main() { test('for method with args', () async { final showMethod = ControllerMethod( (TestHttpController, #show), - [ControllerMethodParam('userId', int, meta: query)], + params: [ControllerMethodParam('userId', int, meta: query)], ); final handler = ApplicationFactory.buildControllerMethod(showMethod); From 0bfe053158d7bbaa995e9a64e28d27b13853fa69 Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Sat, 30 Nov 2024 01:46:03 +0300 Subject: [PATCH 7/7] _ --- packages/pharaoh/lib/src/_next/core.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/pharaoh/lib/src/_next/core.dart b/packages/pharaoh/lib/src/_next/core.dart index 37e5be83..11abefea 100644 --- a/packages/pharaoh/lib/src/_next/core.dart +++ b/packages/pharaoh/lib/src/_next/core.dart @@ -114,21 +114,21 @@ abstract class ApplicationFactory { final spanner = Spanner()..addMiddleware('/', bodyParser); Application._instance = _PharaohNextImpl(config, spanner); - final providerInstances = providers.map(createNewInstance); + final providerInstances = providers + .map(createNewInstance) + .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('/', 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) {