Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
francescovallone committed Jun 30, 2024
2 parents 2162eea + d7c14b6 commit ca3aaf2
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 41 deletions.
7 changes: 7 additions & 0 deletions packages/serinus/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.5.2

- fix: accept List of JsonObject as possible data in Response.json. [#42](https://github.com/francescovallone/serinus/issues/42)
- fix: Response.render & Response.renderString should close the request correctly. [#41](https://github.com/francescovallone/serinus/issues/41)
- fix: ParseSchema should insert back the parsed values in the request. [#45](https://github.com/francescovallone/serinus/issues/45)
- fix: The headers passed to the Response object are now set correctly.

## 0.5.1

- Add exports for Logger and ViewEngine
Expand Down
32 changes: 21 additions & 11 deletions packages/serinus/bin/serinus.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ import 'dart:io';
import 'package:serinus/serinus.dart';
import 'package:shelf/shelf.dart' as shelf;

class TestObj with JsonObject {
final String name;

TestObj(this.name);

@override
Map<String, dynamic> toJson() {
return {
'name': name,
};
}
}

class TestMiddleware extends Middleware {
int counter = 0;

Expand Down Expand Up @@ -100,19 +113,16 @@ class PostRoute extends Route {
class HomeController extends Controller {
HomeController({super.path = '/'}) {
on(GetRoute(path: '/'), (context) async {
return Response.text('Hello world');
return Response.json([
TestObj('Hello'),
TestObj('World'),
{'test': context.query['test']}
]);
},
schema: ParseSchema(
query: object({
'test': string().contains('a'),
}),
headers: object({
'test': string().contains('a'),
}),
// error: (errors) {
// return BadRequestException(message: 'Invalid query parameters');
// }
));
query: object({
'test': string().encode(),
}).optionals(['test'])));
on(PostRoute(path: '/*'), (context) async {
return Response.text(
'${context.request.getData('test')} ${context.params}');
Expand Down
3 changes: 2 additions & 1 deletion packages/serinus/lib/src/adapters/ws_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import 'package:stream_channel/stream_channel.dart';
import 'package:web_socket_channel/status.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

import '../../serinus.dart';
import '../contexts/contexts.dart';
import '../http/internal_request.dart';
import '../services/logger_service.dart';
import 'server_adapter.dart';

/// The [WsRequestHandler] is used to handle the web socket request
typedef WsRequestHandler = Future<void> Function(
Expand Down
3 changes: 2 additions & 1 deletion packages/serinus/lib/src/core/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ abstract class Controller {
final routeExists = _routes.values.any(
(r) => r.route.path == route.path && r.route.method == route.method);
if (routeExists) {
throw StateError('A route of type $R already exists in this controller');
throw StateError(
'A route with the same path and method already exists. [${route.path}] [${route.method}]');
}

_routes[UuidV4().generate()] =
Expand Down
12 changes: 8 additions & 4 deletions packages/serinus/lib/src/core/middleware.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,19 @@ abstract class Middleware {
///
/// It is used to create a middleware from a shelf middleware giving interoperability between Serinus and Shelf.
factory Middleware.shelf(Function handler,
{List<String> routes = const ['*']}) {
return _ShelfMiddleware(handler, routes: routes);
{List<String> routes = const ['*'], bool ignoreResponse = true}) {
return _ShelfMiddleware(handler,
routes: routes, ignoreResponse: ignoreResponse);
}
}

class _ShelfMiddleware extends Middleware {
final dynamic _handler;

_ShelfMiddleware(this._handler, {super.routes = const ['*']});
final bool ignoreResponse;

_ShelfMiddleware(this._handler,
{super.routes = const ['*'], this.ignoreResponse = true});

/// Most of the code has been taken from
/// https://github.com/codekeyz/pharaoh/tree/main/packages/pharaoh/lib/src/shelf_interop
Expand Down Expand Up @@ -71,7 +75,7 @@ class _ShelfMiddleware extends Middleware {
res.status(response.statusCode);
res.headers(headers);
final responseBody = await response.readAsString();
if (responseBody.isNotEmpty) {
if (responseBody.isNotEmpty && !ignoreResponse) {
await res.send(utf8.encode(responseBody));
}
}
Expand Down
5 changes: 3 additions & 2 deletions packages/serinus/lib/src/core/parse_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import '../exceptions/exceptions.dart';

/// The [ParseSchema] class is used to define the schema of the parsing process.
final class ParseSchema {
late AcanthisType _schema;
late AcanthisMap _schema;

/// The [error] property contains the error that will be thrown if the parsing fails.
final SerinusException Function(Map<String, dynamic>)? error;
Expand All @@ -29,7 +29,7 @@ final class ParseSchema {
}

/// The [tryParse] method is used to validate the data.
void tryParse({required Map<String, dynamic> value}) {
Map<String, dynamic> tryParse({required Map<String, dynamic> value}) {
AcanthisParseResult? result;
try {
result = _schema.tryParse(value);
Expand All @@ -40,5 +40,6 @@ final class ParseSchema {
throw error?.call(result.errors) ??
BadRequestException(message: jsonEncode(result.errors));
}
return result.value;
}
}
8 changes: 7 additions & 1 deletion packages/serinus/lib/src/handlers/request_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,19 @@ class RequestHandler extends Handler {
buildRequestContext(scopedProviders, wrappedRequest);
await route.transform(context);
if (schema != null) {
schema.tryParse(value: {
final result = schema.tryParse(value: {
'body': wrappedRequest.body?.value,
'query': wrappedRequest.query,
'params': wrappedRequest.params,
'headers': wrappedRequest.headers,
'session': wrappedRequest.session.all,
});
wrappedRequest.headers.addAll(result['headers']);
wrappedRequest.params.addAll(result['params']);
wrappedRequest.query.addAll(result['query']);
for (final key in result['session'].keys) {
wrappedRequest.session.put(key, result['session'][key]);
}
}
final middlewares = injectables.filterMiddlewaresByRoute(
routeData.path, wrappedRequest.params);
Expand Down
23 changes: 13 additions & 10 deletions packages/serinus/lib/src/http/internal_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class InternalResponse {
/// This method is used to set the headers of the response.
void headers(Map<String, String> headers) {
headers.forEach((key, value) {
_original.headers.add(key, value);
_original.headers.set(key, value);
});
}

Expand Down Expand Up @@ -121,6 +121,15 @@ class InternalResponse {
_events.add(ResponseEvent.error);
throw StateError('ViewEngine is required to render views');
}
headers(result.headers);
if (versioning != null && versioning.type == VersioningType.header) {
_original.headers.add(versioning.header!, versioning.version.toString());
}
contentType(result.contentType);
_original.headers.set(HttpHeaders.transferEncodingHeader, 'chunked');
if (result.contentLength != null) {
_original.headers.contentLength = result.contentLength!;
}
if (result.data is View || result.data is ViewString) {
contentType(ContentType.html);
final rendered = await (result.data is View
Expand All @@ -129,17 +138,11 @@ class InternalResponse {
for (final hook in hooks) {
await hook.onResponse(result);
}
headers({
HttpHeaders.contentLengthHeader: utf8.encode(rendered).length.toString()
});
return send(utf8.encode(rendered));
}
headers(result.headers);
if (versioning != null && versioning.type == VersioningType.header) {
_original.headers.add(versioning.header!, versioning.version.toString());
}
contentType(result.contentType);
_original.headers.set(HttpHeaders.transferEncodingHeader, 'chunked');
if (result.contentLength != null) {
_original.headers.contentLength = result.contentLength!;
}
final data = result.data;
final coding = _original.headers['transfer-encoding']?.join(';');
if (data is File) {
Expand Down
34 changes: 26 additions & 8 deletions packages/serinus/lib/src/http/response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,36 @@ class Response {
/// Throws a [FormatException] if the data is not a [Map<String, dynamic], a [List<Map<String, dynamic>>] or a [JsonObject].
factory Response.json(dynamic data,
{int statusCode = 200, ContentType? contentType}) {
dynamic responseData = _parseJsonableResponse(data);
final value = jsonEncode(responseData);
return Response._(value, statusCode, contentType ?? ContentType.json)
.._contentLength = value.length;
}

/// This method is used to parse a JSON response.
static dynamic _parseJsonableResponse(dynamic data) {
dynamic responseData;
if (data is Map<String, dynamic> || data is List<Map<String, dynamic>>) {
responseData = data;
if (data is Map<String, dynamic>) {
responseData = data.map((key, value) {
if (value is JsonObject) {
return MapEntry(key, _parseJsonableResponse(value.toJson()));
} else if (value is List<JsonObject>) {
return MapEntry(key,
value.map((e) => _parseJsonableResponse(e.toJson())).toList());
}
return MapEntry(key, value);
});
} else if (data is List<Map<String, dynamic>> || data is List<Object>) {
responseData = data.map((e) => _parseJsonableResponse(e)).toList();
} else if (data is JsonObject) {
responseData = data.toJson();
responseData = _parseJsonableResponse(data.toJson());
} else if (data is List<JsonObject>) {
responseData =
data.map((e) => _parseJsonableResponse(e.toJson())).toList();
} else {
throw FormatException(
'The data must be a Map<String, dynamic> or a JsonSerializableMixin');
throw FormatException('The data must be a json parsable type');
}
final value = jsonEncode(responseData);
return Response._(value, statusCode, contentType ?? ContentType.json)
.._contentLength = value.length;
return responseData;
}

/// Factory constructor to create a response with a HTML content type.
Expand Down
4 changes: 2 additions & 2 deletions packages/serinus/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Serinus is a framework written in Dart
documentation: https://docs.serinus.app
homepage: https://docs.serinus.app
repository: https://github.com/francescovallone/serinus
version: 0.5.1
version: 0.5.2
topics:
- server
- httpserver
Expand All @@ -28,7 +28,7 @@ dependencies:
web_socket_channel: ^3.0.0
collection: ^1.18.0
shelf: ^1.4.1
acanthis: ^0.1.2
acanthis: ^0.1.3

dev_dependencies:
test: ^1.16.0
Expand Down
3 changes: 2 additions & 1 deletion packages/serinus/test/core/middlewares_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class TestController extends Controller {
}

final shelfAltMiddleware = Middleware.shelf(
(req) => shelf.Response.ok('Hello world from shelf', headers: req.headers));
(req) => shelf.Response.ok('Hello world from shelf', headers: req.headers),
ignoreResponse: false);

final shelfMiddleware = Middleware.shelf((shelf.Handler innerHandler) {
return (shelf.Request request) {
Expand Down
23 changes: 23 additions & 0 deletions packages/serinus/test/http/responses_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import 'package:serinus/serinus.dart';
import 'package:serinus/src/containers/router.dart';
import 'package:test/test.dart';

import '../../bin/serinus.dart';

class TestRoute extends Route {
const TestRoute({
required super.path,
Expand Down Expand Up @@ -200,5 +202,26 @@ void main() async {
final response = await request.close();
expect(response.statusCode, 500);
});

test(
'''when a mixed json response is passed, then the data should be parsed correctly''',
() async {
final res = Response.json([
{'id': 1, 'name': 'John Doe', 'email': '', 'obj': TestJsonObject()},
TestObj('Jane Doe')
]);
expect(
res.data,
jsonEncode([
{
'id': 1,
'name': 'John Doe',
'email': '',
'obj': {'id': 'json-obj'}
},
{'name': 'Jane Doe'}
]));
},
);
});
}

0 comments on commit ca3aaf2

Please sign in to comment.