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(#61): generate client for Serinus Applications #100

Merged
merged 17 commits into from
Nov 13, 2024
Merged
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*.ipr
*.iws
.idea/

*.DS_Store
# Conventional directory for build output.
build/

Expand Down
21 changes: 19 additions & 2 deletions .website/techniques/cli/generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,29 @@ The `generate` command is used to generate code for your Serinus project. This c
| `provider` | Generate a provider class for your project. |
| `module` | Generate a module class for your project |
| `resource` | Generate a controller, a provider and a module for your project |
| `client` | Generate a client for your project |

## Models

The `models` sub-command is used to generate a model provider class for your project. This class is used to convert JSON objects to Dart objects and vice versa.

It also execute build_runner to generate the necessary files. This allows you to use libraries like `json_serializable`, `dart_mappable`, `freezed` to generate your data classes.

This commands can also be tweaked by the usage of the configuration in the `pubspec.yaml` file.
[Here](/techniques/configuration#models-configuration) is how you can configure the models generation.
This commands can also be tweaked thanks to the configuration in the `pubspec.yaml` file.
[Here](/techniques/configuration#models-configuration) you can see the available options.

## Client

The `client` sub-command is used to generate a client for your project.
The client provide an immediate way to interact with your API from your frontend application.

Right now the client supports the following languages and libraries:

| Language | Library |
| --- | --- |
| Dart | `http` |
| Dart | `dio` |
| Dart | `chopper` |

This commands can also be tweaked thanks to the configuration in the `pubspec.yaml` file.
[Here](/techniques/configuration#client-configuration) you can see the available options.
10 changes: 9 additions & 1 deletion packages/serinus/bin/serinus.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,21 @@ class AppModule extends Module {
: super(imports: [
AnotherModule(),
CircularDependencyModule()
], controllers: [], providers: [
], controllers: [
AppController()
], providers: [
TestProvider(isGlobal: true),
Provider.deferred((TestProviderThree tp) => TestProviderTwo(tp),
inject: [TestProviderThree], type: TestProviderTwo),
], middlewares: []);
}

class AppController extends Controller {
AppController({super.path = '/'}) {
onStatic(Route.get('/'), 'ok!');
}
}

void main(List<String> arguments) async {
SerinusApplication application = await serinus.createApplication(
entrypoint: AppModule(), host: InternetAddress.anyIPv4.address);
Expand Down
5 changes: 4 additions & 1 deletion packages/serinus/example/lib/app_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import 'app_routes.dart';
/// The [AppController] class is used to create the application controller.
class AppController extends Controller {
/// The constructor of the [AppController] class.
AppController({super.path = '/'}) {
AppController({super.path = '/users'}) {
on(HelloWorldRoute(), _handleEcho);
on(Route.get('/<id>/details/<name>'), (context) async {
return 'Hello';
}, body: String);
}

Future<Map<String, dynamic>> _handleEcho(RequestContext context) async {
Expand Down
6 changes: 5 additions & 1 deletion packages/serinus/example/lib/app_routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ import 'package:serinus/serinus.dart';
/// The [HelloWorldRoute] class is used to create a route that returns 'Hello, World!'.
class HelloWorldRoute extends Route {
/// The constructor of the [HelloWorldRoute] class.
HelloWorldRoute() : super(path: '/', method: HttpMethod.get);
HelloWorldRoute()
: super(
path: '/',
method: HttpMethod.get,
queryParameters: {'hello': String});
}
16 changes: 16 additions & 0 deletions packages/serinus/example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.7"
dio:
dependency: "direct main"
description:
name: dio
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
url: "https://pub.dev"
source: hosted
version: "5.7.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
email_validator:
dependency: transitive
description:
Expand Down
7 changes: 6 additions & 1 deletion packages/serinus/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ environment:
sdk: '>=3.0.0 <4.0.0'

dependencies:
serinus: ^0.6.0
serinus: ^0.6.2
dart_mappable: ^4.2.2
freezed_annotation: ^2.4.4
json_annotation: ^4.9.0
dio: ^5.7.0
dev_dependencies:
freezed: ^2.5.7
build_runner: ^2.4.12
Expand All @@ -25,4 +26,8 @@ serinus:
static_method: true
serialize_keywords:
- keyword: "toBody"
client:
verbose: false
language: 'Dart'
httpClient: 'dio'

4 changes: 3 additions & 1 deletion packages/serinus/lib/src/containers/module_container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ final class ModulesContainer {
...providers.providers.whereNot((e) => e is DeferredProvider),
...providers.exportedProviders.whereNot((e) => e is DeferredProvider),
...injectables.providers.whereNot((e) => e is DeferredProvider),
...globalProviders
},
);
_moduleInjectables[token]?.providers.addAllIfAbsent(globalProviders);
Expand Down Expand Up @@ -369,7 +370,8 @@ final class ModulesContainer {
providers: {
...providersInjectable,
...subModuleInjectables.providers,
...exportedProvidersInjectables
...exportedProvidersInjectables,
...globalProviders,
},
);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/serinus/lib/src/containers/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,18 @@ class RouteData {
/// The [isStatic] property defines if a route is a static one.
final bool isStatic;

/// The [spec] property contains the specification of the route.
final RouteHandler spec;

/// The [RouteData] constructor is used to create a new instance of the [RouteData] class.
RouteData({
const RouteData({
required this.id,
required this.path,
required this.method,
required this.controller,
required this.routeCls,
required this.moduleToken,
required this.spec,
this.isStatic = false,
this.queryParameters = const {},
});
Expand Down
2 changes: 1 addition & 1 deletion packages/serinus/lib/src/contexts/request_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class RequestContext extends BaseContext {

/// The [stream] method is used to stream data to the response.
StreamableResponse stream() {
return _streamable..init();
return _streamable;
}
}

Expand Down
10 changes: 2 additions & 8 deletions packages/serinus/lib/src/contexts/ws_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,13 @@ final class WebSocketContext extends BaseContext {
///
/// The [broadcast] parameter is used to broadcast the data to all clients.
void send(dynamic data) {
if (_serializer != null) {
data = _serializer!.serialize(data);
}
_wsAdapter.send(data, key: id);
_wsAdapter.send(_serializer?.serialize(data) ?? data, key: id);
}

/// This method is used to broadcast data to all clients.
///
/// The [data] parameter is the data to be sent.
void broadcast(dynamic data) {
if (_serializer != null) {
data = _serializer!.serialize(data);
}
_wsAdapter.send(data);
_wsAdapter.send(_serializer?.serialize(data) ?? data);
}
}
5 changes: 2 additions & 3 deletions packages/serinus/lib/src/core/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:uuid/v4.dart';

import '../containers/router.dart';
import '../contexts/contexts.dart';
import 'metadata.dart';
import 'parse_schema.dart';
Expand Down Expand Up @@ -37,8 +36,8 @@ abstract class Controller {
Map<String, RouteHandler> get routes => UnmodifiableMapView(_routes);

/// The [get] method is used to get a route.
RouteHandler? get(RouteData routeData) {
return _routes[routeData.id];
RouteHandler? get(String routeId) {
return _routes[routeId];
}

/// The [metadata] property contains the metadata of the controller.
Expand Down
2 changes: 1 addition & 1 deletion packages/serinus/lib/src/extensions/object_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension JsonParsing on Object {

/// This method is used to check if the object can be converted to a json.
bool canBeJson() {
if (this is Uint8List) {
if (this is Uint8List || isPrimitive()) {
return false;
}
return this is Map ||
Expand Down
7 changes: 1 addition & 6 deletions packages/serinus/lib/src/handlers/request_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,8 @@ class RequestHandler extends Handler {

final injectables =
modulesContainer.getModuleInjectablesByToken(routeData.moduleToken);
final routeSpec = routeData.spec;
final controller = routeData.controller;
final routeSpec = controller.get(routeData);
if (routeSpec == null) {
throw InternalServerErrorException(
message: 'Route spec not found for route ${routeData.path}');
}

final route = routeSpec.route;
final handler = routeSpec.handler;
final schema = routeSpec.schema;
Expand Down
3 changes: 3 additions & 0 deletions packages/serinus/lib/src/http/internal_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class InternalResponse {

/// This method is used to set the status code of the response.
void status(int statusCode) {
if (statusCode == _original.statusCode) {
return;
}
_original.statusCode = statusCode;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/serinus/lib/src/http/streamable_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ class StreamableResponse {

/// This method is used to send data to the response.
void send(Object data) {
if (!_initialized) {
init();
}
_response.write(data.toString());
}

Expand Down
25 changes: 12 additions & 13 deletions packages/serinus/lib/src/injector/explorer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,18 @@ final class Explorer {
}
routePath = normalizePath(routePath);
final routeMethod = spec.route.method;
_router.registerRoute(
RouteData(
id: entry.key,
path: routePath,
controller: controller,
routeCls: spec.route.runtimeType,
method: routeMethod,
moduleToken: module.token.isEmpty
? module.runtimeType.toString()
: module.token,
isStatic: spec.handler is! Function,
queryParameters: spec.route.queryParameters),
);
_router.registerRoute(RouteData(
id: entry.key,
path: routePath,
controller: controller,
routeCls: spec.route.runtimeType,
method: routeMethod,
moduleToken:
module.token.isEmpty ? module.runtimeType.toString() : module.token,
isStatic: spec.handler is! Function,
queryParameters: spec.route.queryParameters,
spec: spec,
));
logger.info('Mapped {$routePath, $routeMethod} route');
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/serinus/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ topics:
- http

environment:
sdk: '>=3.0.0 <4.0.0'
sdk: '>=3.5.0 <4.0.0'

dependencies:
async: ^2.12.0
Expand Down
24 changes: 21 additions & 3 deletions packages/serinus/test/containers/router_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ void main() async {
method: HttpMethod.get,
controller: TestController(),
routeCls: Type,
moduleToken: 'moduleToken');
moduleToken: 'moduleToken',
spec: (
body: null,
schema: null,
handler: 'hi',
route: Route.get('/test'),
));
router.registerRoute(routeData);
});

Expand All @@ -47,7 +53,13 @@ void main() async {
method: HttpMethod.get,
controller: TestController(),
routeCls: Type,
moduleToken: 'moduleToken');
moduleToken: 'moduleToken',
spec: (
body: null,
schema: null,
handler: 'hi',
route: Route.get('/test'),
));
router.registerRoute(routeData);
final result = router.getRouteByPathAndMethod('/test', HttpMethod.get);
expect(result.route, routeData);
Expand All @@ -64,7 +76,13 @@ void main() async {
method: HttpMethod.get,
controller: TestController(),
routeCls: Type,
moduleToken: 'moduleToken');
moduleToken: 'moduleToken',
spec: (
body: null,
schema: null,
handler: 'hi',
route: Route.get('/test'),
));
router.registerRoute(routeData);
final result = router.getRouteByPathAndMethod('/test', HttpMethod.post);
expect(result.route, isNull);
Expand Down
Loading
Loading