diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c5685daf..8b7ad6f4 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -115,29 +115,35 @@ The CI runs on multiple Dart versions (3.5.0, stable, beta) and OS (Ubuntu, Wind **Example test structure (one approach):** ```dart -group('Given a NewContext, when withRequest is called with a new Request,', () { - late NewContext context; - late Request newRequest; - late NewContext newContext; - - setUp(() { - // Arrange - context = Request(Method.get, Uri.parse('http://test.com')).toContext(Object()); - newRequest = Request(Method.post, Uri.parse('http://test.com/new')); - // Act (shared action for all tests in this group) - newContext = context.withRequest(newRequest); - }); - - test('then it returns a NewContext instance', () { - // Assert - expect(newContext, isA()); - }); - - test('then the new context contains the new request', () { - // Assert - expect(newContext.request, same(newRequest)); - }); -}); +group( + 'Given a NewContext, when withRequest is called with a new Request,', + () { + late NewContext context; + late Request newRequest; + late NewContext newContext; + + setUp(() { + // Arrange + context = Request( + Method.get, + Uri.parse('http://test.com'), + ).toContext(Object()); + newRequest = Request(Method.post, Uri.parse('http://test.com/new')); + // Act (shared action for all tests in this group) + newContext = context.withRequest(newRequest); + }); + + test('then it returns a NewContext instance', () { + // Assert + expect(newContext, isA()); + }); + + test('then the new context contains the new request', () { + // Assert + expect(newContext.request, same(newRequest)); + }); + }, +); ``` ### Common Development Patterns @@ -153,8 +159,11 @@ group('Given a NewContext, when withRequest is called with a new Request,', () { ResponseContext hello(final NewContext ctx) { final name = ctx.pathParameters[#name]; final age = int.parse(ctx.pathParameters[#age]!); - return ctx.respond(Response.ok( - body: Body.fromString('Hello $name! To think you are $age years old.'))); + return ctx.respond( + Response.ok( + body: Body.fromString('Hello $name! To think you are $age years old.'), + ), + ); } ``` diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 40cd739c..221027c7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,7 +44,10 @@ jobs: run: dart pub get - name: Verify Formatting - run: dart format --output=none --set-exit-if-changed . + run: >- + dart format --output=none --set-exit-if-changed . && + dart pub global activate --source git https://github.com/nielsenko/snip.git && + snip format . --no-apply - name: Analyze (downgraded) run: >- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d7cfa90..834091d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -126,8 +126,7 @@ Comprehensive testing is crucial for maintaining the quality and stability of Re void main() { group('My Feature Logic', () { - test( - 'Given a specific input string, ' + test('Given a specific input string, ' 'when the processing function is called, ' 'then the output should be correctly formatted', () { // Arrange: Set up preconditions @@ -177,8 +176,7 @@ Based on `relic/test/router/normalized_path_test.dart`: ```dart // relic/test/router/normalized_path_test.dart group('Normalization Logic', () { - test( - 'Given a simple path, ' + test('Given a simple path, ' 'when normalized, ' 'then segments are correct and toString is canonical', () { // Arrange @@ -192,8 +190,7 @@ group('Normalization Logic', () { expect(normalized.toString(), equals('/a/b/c')); }); - test( - 'Given path with ".." segments, ' + test('Given path with ".." segments, ' 'when normalized, ' 'then ".." navigates up correctly', () { // Arrange diff --git a/README.md b/README.md index 068db3cc..a95d1dfa 100644 --- a/README.md +++ b/README.md @@ -48,24 +48,24 @@ import 'package:relic/relic.dart'; /// A simple 'Hello World' server demonstrating basic Relic usage. Future main() async { // Setup the app. - final app = - RelicApp() - // Route with parameters (:name & :age). - ..get('/user/:name/age/:age', helloHandler) - // Middleware on all paths below '/'. - ..use('/', logRequests()) - // Custom fallback - optional (default is 404 Not Found). - ..fallback = respondWith( - (_) => Response.notFound( - body: Body.fromString("Sorry, that doesn't compute.\n"), - ), - ); + final app = RelicApp() + // Route with parameters (:name & :age). + ..get('/user/:name/age/:age', helloHandler) + // Middleware on all paths below '/'. + ..use('/', logRequests()) + // Custom fallback - optional (default is 404 Not Found). + ..fallback = respondWith( + (_) => Response.notFound( + body: Body.fromString("Sorry, that doesn't compute.\n"), + ), + ); // Start the server (defaults to using port 8080). await app.serve(); } const _ageParam = PathParam(#age, int.parse); + /// Handles requests to the hello endpoint with path parameters. Response helloHandler(final Request req) { final name = req.pathParameters.raw[#name]; diff --git a/doc/history.md b/doc/history.md index 94b32e8e..35439787 100644 --- a/doc/history.md +++ b/doc/history.md @@ -76,7 +76,9 @@ updateHeader(headers, {'Cache-Control': 'max-age: 3600'}); // in shelf ``` you do: ```dart -headers.transform((mh) => mh.cacheControl = CacheControlHeader(maxAge: 3600)); // in relic +headers.transform( + (mh) => mh.cacheControl = CacheControlHeader(maxAge: 3600), +); // in relic ``` A bit longer, but fully type safe @@ -109,7 +111,7 @@ Middleware authMiddleware() { return (RequestContext ctx) { final token = ctx.request.headers.authorization?.credentials; final user = validateToken(token); - _currentUser[ctx] = user; // Type-safe assignment + _currentUser[ctx] = user; // Type-safe assignment return innerHandler(ctx); }; }; @@ -117,9 +119,9 @@ Middleware authMiddleware() { // Access it in handlers Handler protectedResource = (RequestContext ctx) { - final user = ctx.currentUser; // Type-safe access, no casting needed + final user = ctx.currentUser; // Type-safe access, no casting needed return (ctx as RespondableContext).respond( - Response.ok(body: Body.fromString('Hello ${user.name}')) + Response.ok(body: Body.fromString('Hello ${user.name}')), ); }; ``` @@ -129,8 +131,10 @@ Handler protectedResource = (RequestContext ctx) { In Shelf, you'd use the context bag: ```dart // Shelf approach - no type safety, potential name conflicts -request = request.change(context: {'user': user}); // Set is inherently unsafe (untyped) -final user = request.context['user'] as User; // Get (runtime cast can fail) +request = request.change( + context: {'user': user}, +); // Set is inherently unsafe (untyped) +final user = request.context['user'] as User; // Get (runtime cast can fail) ``` **Advantages of ContextProperty:** @@ -190,7 +194,7 @@ final class PathMiss extends LookupResult { } final class MethodMiss extends LookupResult { - final Set allowed; // Path exists but wrong method + final Set allowed; // Path exists but wrong method // Return 405 Method Not Allowed with Allow header } @@ -222,7 +226,7 @@ api.get('/users', listUsers); // Nested groups work too final v1 = api.group('/v1'); -v1.get('/posts', listPosts); // Accessible at /api/v1/posts +v1.get('/posts', listPosts); // Accessible at /api/v1/posts // Sub-routers can be created in separate packages for modularity ``` @@ -233,7 +237,7 @@ Path parameters use symbols instead of strings: ```dart router.get('/users/:id/posts/:postId', (RequestContext ctx) { - final userId = ctx.pathParameters[#id]; // Symbol, not string + final userId = ctx.pathParameters[#id]; // Symbol, not string final postId = ctx.pathParameters[#postId]; }); ``` @@ -250,10 +254,10 @@ While Shelf uses `Uint8List` at runtime (since v1.1.1), its type signature decla ```dart // Shelf -Stream> body; // Runtime is Uint8List, but type says List +Stream> body; // Runtime is Uint8List, but type says List // Relic -Stream body; // Type matches runtime, making intent clear +Stream body; // Type matches runtime, making intent clear ``` This eliminates potential type confusion and makes the API contract explicit. @@ -266,7 +270,7 @@ In Shelf, the content-type header and encoding live separately from the body, wh class Body { final Stream stream; final int? contentLength; - final BodyType? bodyType; // Combines mimeType + encoding + final BodyType? bodyType; // Combines mimeType + encoding } ``` @@ -332,7 +336,7 @@ if (ws.trySendText('Hello')) { } // Standard throwing variant also available -ws.sendText('Hello'); // Throws WebSocketConnectionClosed if closed +ws.sendText('Hello'); // Throws WebSocketConnectionClosed if closed // Check state explicitly if (!ws.isClosed) { @@ -349,15 +353,15 @@ This makes it easier to write robust WebSocket handlers that gracefully handle c import 'package:shelf_web_socket/shelf_web_socket.dart'; var handler = webSocketHandler((webSocket) { - webSocket.stream.listen(...); - webSocket.sink.add(...); + webSocket.stream.listen(/* ... */); + webSocket.sink.add(/* ... */); }); // Relic approach (built-in, state machine integration) Handler handler = (NewContext ctx) { return ctx.connect((RelicWebSocket ws) { - ws.events.listen(...); - ws.trySendText(...); // Non-throwing variant + ws.events.listen(/* ... */); + ws.trySendText(/* ... */); // Non-throwing variant }); }; ``` diff --git a/doc/site/docs/01-getting-started/03-shelf-migration.md b/doc/site/docs/01-getting-started/03-shelf-migration.md index e854a641..8da03eb9 100644 --- a/doc/site/docs/01-getting-started/03-shelf-migration.md +++ b/doc/site/docs/01-getting-started/03-shelf-migration.md @@ -140,9 +140,7 @@ Relic uses an explicit body type that unifies content, encoding, and MIME type: ```dart // Relic - explicit Body object is required. -final response = Response.ok( - body: Body.fromString('Hello, World!'), -); +final response = Response.ok(body: Body.fromString('Hello, World!')); // Content-Length is automatically calculated and Content-Type and // encoding are part of the Body. @@ -180,10 +178,9 @@ final app = Router() }); final handler = Pipeline() - .addMiddleware(logRequests()) - .addMiddleware(authentication()) - .addHandler(app); - + .addMiddleware(logRequests()) + .addMiddleware(authentication()) + .addHandler(app); ``` Relic: @@ -203,13 +200,12 @@ Shelf: ```dart // Shelf - Dynamic types. -final modifiedRequest = request.change(context: { - 'user': currentUser, - 'session': sessionData, -}); +final modifiedRequest = request.change( + context: {'user': currentUser, 'session': sessionData}, +); // Later... -final user = request.context['user'] as User?; // Manual casting +final user = request.context['user'] as User?; // Manual casting ``` Relic: @@ -267,9 +263,7 @@ void main() async { return Response.ok('User $id: $name'); }); - final handler = Pipeline() - .addMiddleware(logRequests()) - .addHandler(router); + final handler = Pipeline().addMiddleware(logRequests()).addHandler(router); await shelf_io.serve(handler, 'localhost', 8080); } diff --git a/doc/site/docs/02-reference/03-routing.md b/doc/site/docs/02-reference/03-routing.md index ef502218..37f609d4 100644 --- a/doc/site/docs/02-reference/03-routing.md +++ b/doc/site/docs/02-reference/03-routing.md @@ -95,9 +95,7 @@ Use a colon-prefixed name to capture a segment. Access the value with the `Symbo final app = RelicApp() ..get('/users/:id', (final Request request) { final userId = request.pathParameters.raw[#id]; - return Response.ok( - body: Body.fromString('User $userId'), - ); + return Response.ok(body: Body.fromString('User $userId')); }); ``` @@ -176,7 +174,7 @@ than a linear scan. Consider these routes: ```dart -router.get('/:entity/:id', entityHandler); // Route 1 +router.get('/:entity/:id', entityHandler); // Route 1 router.get('/users/:id/profile', profileHandler); // Route 2 ``` @@ -194,7 +192,7 @@ Without backtracking, the request would fail because the router would commit to Tail segments (`/**`) act as catch-alls and benefit from backtracking: ```dart -router.get('/files/**', catchAllHandler); // Route 1 +router.get('/files/**', catchAllHandler); // Route 1 router.get('/files/special/report', reportHandler); // Route 2 ``` diff --git a/doc/site/docs/02-reference/04-requests.md b/doc/site/docs/02-reference/04-requests.md index 097a9b8e..0744fa7e 100644 --- a/doc/site/docs/02-reference/04-requests.md +++ b/doc/site/docs/02-reference/04-requests.md @@ -186,9 +186,7 @@ Use `copyWith` to create a new request with different values. Any field you don' ```dart // Change just the URL -final rewritten = request.copyWith( - url: request.url.replace(path: '/new-path'), -); +final rewritten = request.copyWith(url: request.url.replace(path: '/new-path')); // Change URL and headers final modified = request.copyWith( diff --git a/doc/site/docs/02-reference/06-body.md b/doc/site/docs/02-reference/06-body.md index cdfd81fa..664df16b 100644 --- a/doc/site/docs/02-reference/06-body.md +++ b/doc/site/docs/02-reference/06-body.md @@ -66,10 +66,7 @@ final body = Body.fromData(bytes); When you know a file is binary or want to enforce a specific type, you can set the MIME type explicitly. This can help when serving files with uncommon formats or when autodetection is not desirable: ```dart -final binaryBody = Body.fromData( - data, - mimeType: MimeType.octetStream, -); +final binaryBody = Body.fromData(data, mimeType: MimeType.octetStream); ``` For large files or data that arrives incrementally, prefer streaming to avoid excessive memory usage. Streaming lets clients start receiving data immediately and keeps your server responsive under load: @@ -143,10 +140,10 @@ Relic automatically detects MIME types for common formats, reducing boilerplate JSON, HTML, XML, and plain text are automatically detected: ```dart -Body.fromString('{"key": "value"}') // → application/json -Body.fromString('...') // → text/html -Body.fromString('...') // → application/xml -Body.fromString('Plain text') // → text/plain +Body.fromString('{"key": "value"}'); // → application/json +Body.fromString('...'); // → text/html +Body.fromString('...'); // → application/xml +Body.fromString('Plain text'); // → text/plain ``` #### Binary content detection @@ -154,10 +151,10 @@ Body.fromString('Plain text') // → text/plain Common image formats, documents, and other binary files are automatically detected: ```dart -Body.fromData(pngBytes) // → image/png -Body.fromData(jpegBytes) // → image/jpeg -Body.fromData(pdfBytes) // → application/pdf -Body.fromData(unknownBytes) // → application/octet-stream +Body.fromData(pngBytes); // → image/png +Body.fromData(jpegBytes); // → image/jpeg +Body.fromData(pdfBytes); // → application/pdf +Body.fromData(unknownBytes); // → application/octet-stream ``` ### Encoding handling @@ -172,10 +169,7 @@ print(body.bodyType?.encoding); // utf8 If you must use a different character set, set the encoding explicitly. This keeps your intent clear and ensures the Content-Type header reflects the actual encoding used: ```dart -final latinBody = Body.fromString( - 'Café', - encoding: latin1, -); +final latinBody = Body.fromString('Café', encoding: latin1); ``` Binary content does not use a character encoding, so the encoding field is absent. This distinction helps prevent accidental misinterpretation of raw bytes as text: @@ -220,10 +214,7 @@ final file = File('large-file.dat'); final fileStream = file.openRead(); final fileSize = await file.length(); -final body = Body.fromDataStream( - fileStream, - contentLength: fileSize, -); +final body = Body.fromDataStream(fileStream, contentLength: fileSize); ``` If the size is unknown, Relic will use chunked transfer encoding so that data can start flowing immediately. This is ideal for generated content or pipelines that produce output over time: diff --git a/doc/site/docs/02-reference/07-middleware.md b/doc/site/docs/02-reference/07-middleware.md index 71c7fd54..b4afebc1 100644 --- a/doc/site/docs/02-reference/07-middleware.md +++ b/doc/site/docs/02-reference/07-middleware.md @@ -60,10 +60,8 @@ import 'package:relic/relic.dart'; final router = RelicApp() // Apply logging to all routes ..use('/', logRequests()) - // Apply authentication to API routes ..use('/api', authMiddleware()) - // Define your routes ..get('/api/users', usersHandler) ..post('/api/users', createUserHandler); @@ -80,7 +78,6 @@ final app = RelicApp() // Global middleware - applies to ALL routes ..use('/', logRequests()) ..use('/', corsMiddleware()) - // Your routes ..get('/users', usersHandler) ..get('/posts', postsHandler); @@ -98,13 +95,11 @@ This means that when you use `router.use('/', middleware)`, the middleware appli final app = RelicApp() // Global logging ..use('/', logRequests()) - // Authentication only for API routes ..use('/api', authMiddleware()) - // Routes - ..get('/', homeHandler) // Only logging - ..get('/api/users', usersHandler) // Logging + auth + ..get('/', homeHandler) // Only logging + ..get('/api/users', usersHandler); // Logging + auth ``` :::info Built-in logging @@ -128,9 +123,12 @@ This means that within the same path scope, different middleware are applied in ```dart final app = RelicApp() - ..use('/api', middlewareC) // Registered first, but specific to /api - ..use('/', middlewareA) // Registered second and applicable to all paths below / - ..use('/', middlewareB) // Registered last and applicable to all paths below / + ..use('/api', middlewareC) // Registered first, but specific to /api + ..use( + '/', + middlewareA, + ) // Registered second and applicable to all paths below / + ..use('/', middlewareB) // Registered last and applicable to all paths below / ..get('/api/foo', fooHandler); ``` @@ -140,10 +138,10 @@ Let's look at an example of how middleware will work in same path scope: ```dart final app = RelicApp() - ..use('/api', middleware1) // MW1 - outermost (registered first) - ..use('/api', middleware2) // MW2 - middle - ..use('/api', middleware3) // MW3 - innermost (registered last) - ..get('/api/users', usersHandler); // H - handler + ..use('/api', middleware1) // MW1 - outermost (registered first) + ..use('/api', middleware2) // MW2 - middle + ..use('/api', middleware3) // MW3 - innermost (registered last) + ..get('/api/users', usersHandler); // H - handler ``` The request flows from the outermost middleware to the innermost handler, and the response flows back out in reverse. The diagram below shows execution for a request to `/api/users` with three middleware layers registered at the same path. diff --git a/doc/site/versioned_docs/version-1.0.0/01-getting-started/03-shelf-migration.md b/doc/site/versioned_docs/version-1.0.0/01-getting-started/03-shelf-migration.md index e854a641..8da03eb9 100644 --- a/doc/site/versioned_docs/version-1.0.0/01-getting-started/03-shelf-migration.md +++ b/doc/site/versioned_docs/version-1.0.0/01-getting-started/03-shelf-migration.md @@ -140,9 +140,7 @@ Relic uses an explicit body type that unifies content, encoding, and MIME type: ```dart // Relic - explicit Body object is required. -final response = Response.ok( - body: Body.fromString('Hello, World!'), -); +final response = Response.ok(body: Body.fromString('Hello, World!')); // Content-Length is automatically calculated and Content-Type and // encoding are part of the Body. @@ -180,10 +178,9 @@ final app = Router() }); final handler = Pipeline() - .addMiddleware(logRequests()) - .addMiddleware(authentication()) - .addHandler(app); - + .addMiddleware(logRequests()) + .addMiddleware(authentication()) + .addHandler(app); ``` Relic: @@ -203,13 +200,12 @@ Shelf: ```dart // Shelf - Dynamic types. -final modifiedRequest = request.change(context: { - 'user': currentUser, - 'session': sessionData, -}); +final modifiedRequest = request.change( + context: {'user': currentUser, 'session': sessionData}, +); // Later... -final user = request.context['user'] as User?; // Manual casting +final user = request.context['user'] as User?; // Manual casting ``` Relic: @@ -267,9 +263,7 @@ void main() async { return Response.ok('User $id: $name'); }); - final handler = Pipeline() - .addMiddleware(logRequests()) - .addHandler(router); + final handler = Pipeline().addMiddleware(logRequests()).addHandler(router); await shelf_io.serve(handler, 'localhost', 8080); } diff --git a/doc/site/versioned_docs/version-1.0.0/02-reference/03-routing.md b/doc/site/versioned_docs/version-1.0.0/02-reference/03-routing.md index be0b3eef..995ef42b 100644 --- a/doc/site/versioned_docs/version-1.0.0/02-reference/03-routing.md +++ b/doc/site/versioned_docs/version-1.0.0/02-reference/03-routing.md @@ -95,9 +95,7 @@ Use a colon-prefixed name to capture a segment. Access the value with the `Symbo final app = RelicApp() ..get('/users/:id', (final Request request) { final userId = request.pathParameters.raw[#id]; - return Response.ok( - body: Body.fromString('User $userId'), - ); + return Response.ok(body: Body.fromString('User $userId')); }); ``` @@ -176,7 +174,7 @@ than a linear scan. Consider these routes: ```dart -router.get('/:entity/:id', entityHandler); // Route 1 +router.get('/:entity/:id', entityHandler); // Route 1 router.get('/users/:id/profile', profileHandler); // Route 2 ``` @@ -194,7 +192,7 @@ Without backtracking, the request would fail because the router would commit to Tail segments (`/**`) act as catch-alls and benefit from backtracking: ```dart -router.get('/files/**', catchAllHandler); // Route 1 +router.get('/files/**', catchAllHandler); // Route 1 router.get('/files/special/report', reportHandler); // Route 2 ``` diff --git a/doc/site/versioned_docs/version-1.0.0/02-reference/06-body.md b/doc/site/versioned_docs/version-1.0.0/02-reference/06-body.md index cdfd81fa..664df16b 100644 --- a/doc/site/versioned_docs/version-1.0.0/02-reference/06-body.md +++ b/doc/site/versioned_docs/version-1.0.0/02-reference/06-body.md @@ -66,10 +66,7 @@ final body = Body.fromData(bytes); When you know a file is binary or want to enforce a specific type, you can set the MIME type explicitly. This can help when serving files with uncommon formats or when autodetection is not desirable: ```dart -final binaryBody = Body.fromData( - data, - mimeType: MimeType.octetStream, -); +final binaryBody = Body.fromData(data, mimeType: MimeType.octetStream); ``` For large files or data that arrives incrementally, prefer streaming to avoid excessive memory usage. Streaming lets clients start receiving data immediately and keeps your server responsive under load: @@ -143,10 +140,10 @@ Relic automatically detects MIME types for common formats, reducing boilerplate JSON, HTML, XML, and plain text are automatically detected: ```dart -Body.fromString('{"key": "value"}') // → application/json -Body.fromString('...') // → text/html -Body.fromString('...') // → application/xml -Body.fromString('Plain text') // → text/plain +Body.fromString('{"key": "value"}'); // → application/json +Body.fromString('...'); // → text/html +Body.fromString('...'); // → application/xml +Body.fromString('Plain text'); // → text/plain ``` #### Binary content detection @@ -154,10 +151,10 @@ Body.fromString('Plain text') // → text/plain Common image formats, documents, and other binary files are automatically detected: ```dart -Body.fromData(pngBytes) // → image/png -Body.fromData(jpegBytes) // → image/jpeg -Body.fromData(pdfBytes) // → application/pdf -Body.fromData(unknownBytes) // → application/octet-stream +Body.fromData(pngBytes); // → image/png +Body.fromData(jpegBytes); // → image/jpeg +Body.fromData(pdfBytes); // → application/pdf +Body.fromData(unknownBytes); // → application/octet-stream ``` ### Encoding handling @@ -172,10 +169,7 @@ print(body.bodyType?.encoding); // utf8 If you must use a different character set, set the encoding explicitly. This keeps your intent clear and ensures the Content-Type header reflects the actual encoding used: ```dart -final latinBody = Body.fromString( - 'Café', - encoding: latin1, -); +final latinBody = Body.fromString('Café', encoding: latin1); ``` Binary content does not use a character encoding, so the encoding field is absent. This distinction helps prevent accidental misinterpretation of raw bytes as text: @@ -220,10 +214,7 @@ final file = File('large-file.dat'); final fileStream = file.openRead(); final fileSize = await file.length(); -final body = Body.fromDataStream( - fileStream, - contentLength: fileSize, -); +final body = Body.fromDataStream(fileStream, contentLength: fileSize); ``` If the size is unknown, Relic will use chunked transfer encoding so that data can start flowing immediately. This is ideal for generated content or pipelines that produce output over time: diff --git a/doc/site/versioned_docs/version-1.0.0/02-reference/07-middleware.md b/doc/site/versioned_docs/version-1.0.0/02-reference/07-middleware.md index 17187fe1..cb45433c 100644 --- a/doc/site/versioned_docs/version-1.0.0/02-reference/07-middleware.md +++ b/doc/site/versioned_docs/version-1.0.0/02-reference/07-middleware.md @@ -60,10 +60,8 @@ import 'package:relic/relic.dart'; final router = RelicApp() // Apply logging to all routes ..use('/', logRequests()) - // Apply authentication to API routes ..use('/api', authMiddleware()) - // Define your routes ..get('/api/users', usersHandler) ..post('/api/users', createUserHandler); @@ -80,7 +78,6 @@ final app = RelicApp() // Global middleware - applies to ALL routes ..use('/', logRequests()) ..use('/', corsMiddleware()) - // Your routes ..get('/users', usersHandler) ..get('/posts', postsHandler); @@ -98,13 +95,11 @@ This means that when you use `router.use('/', middleware)`, the middleware appli final app = RelicApp() // Global logging ..use('/', logRequests()) - // Authentication only for API routes ..use('/api', authMiddleware()) - // Routes - ..get('/', homeHandler) // Only logging - ..get('/api/users', usersHandler) // Logging + auth + ..get('/', homeHandler) // Only logging + ..get('/api/users', usersHandler); // Logging + auth ``` :::info Built-in logging @@ -128,9 +123,12 @@ This means that within the same path scope, different middleware are applied in ```dart final app = RelicApp() - ..use('/api', middlewareC) // Registered first, but specific to /api - ..use('/', middlewareA) // Registered second and applicable to all paths below / - ..use('/', middlewareB) // Registered last and applicable to all paths below / + ..use('/api', middlewareC) // Registered first, but specific to /api + ..use( + '/', + middlewareA, + ) // Registered second and applicable to all paths below / + ..use('/', middlewareB) // Registered last and applicable to all paths below / ..get('/api/foo', fooHandler); ``` @@ -140,10 +138,10 @@ Let's look at an example of how middleware will work in same path scope: ```dart final app = RelicApp() - ..use('/api', middleware1) // MW1 - outermost (registered first) - ..use('/api', middleware2) // MW2 - middle - ..use('/api', middleware3) // MW3 - innermost (registered last) - ..get('/api/users', usersHandler); // H - handler + ..use('/api', middleware1) // MW1 - outermost (registered first) + ..use('/api', middleware2) // MW2 - middle + ..use('/api', middleware3) // MW3 - innermost (registered last) + ..get('/api/users', usersHandler); // H - handler ``` The request flows from the outermost middleware to the innermost handler, and the response flows back out in reverse. The diagram below shows execution for a request to `/api/users` with three middleware layers registered at the same path. diff --git a/doc/site/versioned_docs/version-1.1.0/01-getting-started/03-shelf-migration.md b/doc/site/versioned_docs/version-1.1.0/01-getting-started/03-shelf-migration.md index e854a641..8da03eb9 100644 --- a/doc/site/versioned_docs/version-1.1.0/01-getting-started/03-shelf-migration.md +++ b/doc/site/versioned_docs/version-1.1.0/01-getting-started/03-shelf-migration.md @@ -140,9 +140,7 @@ Relic uses an explicit body type that unifies content, encoding, and MIME type: ```dart // Relic - explicit Body object is required. -final response = Response.ok( - body: Body.fromString('Hello, World!'), -); +final response = Response.ok(body: Body.fromString('Hello, World!')); // Content-Length is automatically calculated and Content-Type and // encoding are part of the Body. @@ -180,10 +178,9 @@ final app = Router() }); final handler = Pipeline() - .addMiddleware(logRequests()) - .addMiddleware(authentication()) - .addHandler(app); - + .addMiddleware(logRequests()) + .addMiddleware(authentication()) + .addHandler(app); ``` Relic: @@ -203,13 +200,12 @@ Shelf: ```dart // Shelf - Dynamic types. -final modifiedRequest = request.change(context: { - 'user': currentUser, - 'session': sessionData, -}); +final modifiedRequest = request.change( + context: {'user': currentUser, 'session': sessionData}, +); // Later... -final user = request.context['user'] as User?; // Manual casting +final user = request.context['user'] as User?; // Manual casting ``` Relic: @@ -267,9 +263,7 @@ void main() async { return Response.ok('User $id: $name'); }); - final handler = Pipeline() - .addMiddleware(logRequests()) - .addHandler(router); + final handler = Pipeline().addMiddleware(logRequests()).addHandler(router); await shelf_io.serve(handler, 'localhost', 8080); } diff --git a/doc/site/versioned_docs/version-1.1.0/02-reference/03-routing.md b/doc/site/versioned_docs/version-1.1.0/02-reference/03-routing.md index ef502218..37f609d4 100644 --- a/doc/site/versioned_docs/version-1.1.0/02-reference/03-routing.md +++ b/doc/site/versioned_docs/version-1.1.0/02-reference/03-routing.md @@ -95,9 +95,7 @@ Use a colon-prefixed name to capture a segment. Access the value with the `Symbo final app = RelicApp() ..get('/users/:id', (final Request request) { final userId = request.pathParameters.raw[#id]; - return Response.ok( - body: Body.fromString('User $userId'), - ); + return Response.ok(body: Body.fromString('User $userId')); }); ``` @@ -176,7 +174,7 @@ than a linear scan. Consider these routes: ```dart -router.get('/:entity/:id', entityHandler); // Route 1 +router.get('/:entity/:id', entityHandler); // Route 1 router.get('/users/:id/profile', profileHandler); // Route 2 ``` @@ -194,7 +192,7 @@ Without backtracking, the request would fail because the router would commit to Tail segments (`/**`) act as catch-alls and benefit from backtracking: ```dart -router.get('/files/**', catchAllHandler); // Route 1 +router.get('/files/**', catchAllHandler); // Route 1 router.get('/files/special/report', reportHandler); // Route 2 ``` diff --git a/doc/site/versioned_docs/version-1.1.0/02-reference/04-requests.md b/doc/site/versioned_docs/version-1.1.0/02-reference/04-requests.md index 097a9b8e..0744fa7e 100644 --- a/doc/site/versioned_docs/version-1.1.0/02-reference/04-requests.md +++ b/doc/site/versioned_docs/version-1.1.0/02-reference/04-requests.md @@ -186,9 +186,7 @@ Use `copyWith` to create a new request with different values. Any field you don' ```dart // Change just the URL -final rewritten = request.copyWith( - url: request.url.replace(path: '/new-path'), -); +final rewritten = request.copyWith(url: request.url.replace(path: '/new-path')); // Change URL and headers final modified = request.copyWith( diff --git a/doc/site/versioned_docs/version-1.1.0/02-reference/06-body.md b/doc/site/versioned_docs/version-1.1.0/02-reference/06-body.md index cdfd81fa..664df16b 100644 --- a/doc/site/versioned_docs/version-1.1.0/02-reference/06-body.md +++ b/doc/site/versioned_docs/version-1.1.0/02-reference/06-body.md @@ -66,10 +66,7 @@ final body = Body.fromData(bytes); When you know a file is binary or want to enforce a specific type, you can set the MIME type explicitly. This can help when serving files with uncommon formats or when autodetection is not desirable: ```dart -final binaryBody = Body.fromData( - data, - mimeType: MimeType.octetStream, -); +final binaryBody = Body.fromData(data, mimeType: MimeType.octetStream); ``` For large files or data that arrives incrementally, prefer streaming to avoid excessive memory usage. Streaming lets clients start receiving data immediately and keeps your server responsive under load: @@ -143,10 +140,10 @@ Relic automatically detects MIME types for common formats, reducing boilerplate JSON, HTML, XML, and plain text are automatically detected: ```dart -Body.fromString('{"key": "value"}') // → application/json -Body.fromString('...') // → text/html -Body.fromString('...') // → application/xml -Body.fromString('Plain text') // → text/plain +Body.fromString('{"key": "value"}'); // → application/json +Body.fromString('...'); // → text/html +Body.fromString('...'); // → application/xml +Body.fromString('Plain text'); // → text/plain ``` #### Binary content detection @@ -154,10 +151,10 @@ Body.fromString('Plain text') // → text/plain Common image formats, documents, and other binary files are automatically detected: ```dart -Body.fromData(pngBytes) // → image/png -Body.fromData(jpegBytes) // → image/jpeg -Body.fromData(pdfBytes) // → application/pdf -Body.fromData(unknownBytes) // → application/octet-stream +Body.fromData(pngBytes); // → image/png +Body.fromData(jpegBytes); // → image/jpeg +Body.fromData(pdfBytes); // → application/pdf +Body.fromData(unknownBytes); // → application/octet-stream ``` ### Encoding handling @@ -172,10 +169,7 @@ print(body.bodyType?.encoding); // utf8 If you must use a different character set, set the encoding explicitly. This keeps your intent clear and ensures the Content-Type header reflects the actual encoding used: ```dart -final latinBody = Body.fromString( - 'Café', - encoding: latin1, -); +final latinBody = Body.fromString('Café', encoding: latin1); ``` Binary content does not use a character encoding, so the encoding field is absent. This distinction helps prevent accidental misinterpretation of raw bytes as text: @@ -220,10 +214,7 @@ final file = File('large-file.dat'); final fileStream = file.openRead(); final fileSize = await file.length(); -final body = Body.fromDataStream( - fileStream, - contentLength: fileSize, -); +final body = Body.fromDataStream(fileStream, contentLength: fileSize); ``` If the size is unknown, Relic will use chunked transfer encoding so that data can start flowing immediately. This is ideal for generated content or pipelines that produce output over time: diff --git a/doc/site/versioned_docs/version-1.1.0/02-reference/07-middleware.md b/doc/site/versioned_docs/version-1.1.0/02-reference/07-middleware.md index 71c7fd54..b4afebc1 100644 --- a/doc/site/versioned_docs/version-1.1.0/02-reference/07-middleware.md +++ b/doc/site/versioned_docs/version-1.1.0/02-reference/07-middleware.md @@ -60,10 +60,8 @@ import 'package:relic/relic.dart'; final router = RelicApp() // Apply logging to all routes ..use('/', logRequests()) - // Apply authentication to API routes ..use('/api', authMiddleware()) - // Define your routes ..get('/api/users', usersHandler) ..post('/api/users', createUserHandler); @@ -80,7 +78,6 @@ final app = RelicApp() // Global middleware - applies to ALL routes ..use('/', logRequests()) ..use('/', corsMiddleware()) - // Your routes ..get('/users', usersHandler) ..get('/posts', postsHandler); @@ -98,13 +95,11 @@ This means that when you use `router.use('/', middleware)`, the middleware appli final app = RelicApp() // Global logging ..use('/', logRequests()) - // Authentication only for API routes ..use('/api', authMiddleware()) - // Routes - ..get('/', homeHandler) // Only logging - ..get('/api/users', usersHandler) // Logging + auth + ..get('/', homeHandler) // Only logging + ..get('/api/users', usersHandler); // Logging + auth ``` :::info Built-in logging @@ -128,9 +123,12 @@ This means that within the same path scope, different middleware are applied in ```dart final app = RelicApp() - ..use('/api', middlewareC) // Registered first, but specific to /api - ..use('/', middlewareA) // Registered second and applicable to all paths below / - ..use('/', middlewareB) // Registered last and applicable to all paths below / + ..use('/api', middlewareC) // Registered first, but specific to /api + ..use( + '/', + middlewareA, + ) // Registered second and applicable to all paths below / + ..use('/', middlewareB) // Registered last and applicable to all paths below / ..get('/api/foo', fooHandler); ``` @@ -140,10 +138,10 @@ Let's look at an example of how middleware will work in same path scope: ```dart final app = RelicApp() - ..use('/api', middleware1) // MW1 - outermost (registered first) - ..use('/api', middleware2) // MW2 - middle - ..use('/api', middleware3) // MW3 - innermost (registered last) - ..get('/api/users', usersHandler); // H - handler + ..use('/api', middleware1) // MW1 - outermost (registered first) + ..use('/api', middleware2) // MW2 - middle + ..use('/api', middleware3) // MW3 - innermost (registered last) + ..get('/api/users', usersHandler); // H - handler ``` The request flows from the outermost middleware to the innermost handler, and the response flows back out in reverse. The diagram below shows execution for a request to `/api/users` with three middleware layers registered at the same path. diff --git a/packages/relic/skills/relic-app-setup/SKILL.md b/packages/relic/skills/relic-app-setup/SKILL.md index b4c2c9bc..86676bb7 100644 --- a/packages/relic/skills/relic-app-setup/SKILL.md +++ b/packages/relic/skills/relic-app-setup/SKILL.md @@ -24,15 +24,11 @@ Future main() async { final app = RelicApp() ..get('/hello/:name', (Request req) { final name = req.rawPathParameters[#name]; - return Response.ok( - body: Body.fromString('Hello, $name!'), - ); + return Response.ok(body: Body.fromString('Hello, $name!')); }) ..use('/', logRequests()) ..fallback = respondWith( - (_) => Response.notFound( - body: Body.fromString('Not found'), - ), + (_) => Response.notFound(body: Body.fromString('Not found')), ); await app.serve(); @@ -44,10 +40,7 @@ Future main() async { ```dart await app.serve(); // defaults to 0.0.0.0:8080 -await app.serve( - address: InternetAddress.loopbackIPv4, - port: 3000, -); +await app.serve(address: InternetAddress.loopbackIPv4, port: 3000); ``` ## Fallback handler @@ -56,9 +49,7 @@ The fallback handles requests that don't match any route. Default is 404 with an ```dart app.fallback = respondWith( - (_) => Response.notFound( - body: Body.fromString('Page not found'), - ), + (_) => Response.notFound(body: Body.fromString('Page not found')), ); ``` diff --git a/packages/relic/skills/relic-middleware/SKILL.md b/packages/relic/skills/relic-middleware/SKILL.md index cf745093..9b2e5d3f 100644 --- a/packages/relic/skills/relic-middleware/SKILL.md +++ b/packages/relic/skills/relic-middleware/SKILL.md @@ -38,13 +38,11 @@ final app = RelicApp() // Global -- applies to all matched routes ..use('/', logRequests()) ..use('/', corsMiddleware()) - // Scoped -- only /api routes ..use('/api', authMiddleware()) - // Routes - ..get('/', homeHandler) // logRequests + cors - ..get('/api/users', usersHandler) // logRequests + cors + auth + ..get('/', homeHandler) // logRequests + cors + ..get('/api/users', usersHandler); // logRequests + cors + auth ``` ## Execution order @@ -53,9 +51,9 @@ Path hierarchy first, then registration order within the same path scope: ```dart final app = RelicApp() - ..use('/api', middlewareC) // specific to /api - ..use('/', middlewareA) // all paths - ..use('/', middlewareB) // all paths + ..use('/api', middlewareC) // specific to /api + ..use('/', middlewareA) // all paths + ..use('/', middlewareB) // all paths ..get('/api/foo', fooHandler); ``` @@ -162,7 +160,7 @@ final requestIdProperty = ContextProperty('requestId'); requestIdProperty[req] = 'req_${DateTime.now().millisecondsSinceEpoch}'; // Read in handler -final id = requestIdProperty[req]; // String? -- null if not set +final id = requestIdProperty[req]; // String? -- null if not set final id = requestIdProperty.get(req); // String -- throws if missing ``` @@ -171,12 +169,9 @@ final id = requestIdProperty.get(req); // String -- throws if missing ```dart final _userProperty = ContextProperty('user'); final _sessionProperty = ContextProperty('session'); +``` -extension AuthContext on Request { - User get currentUser => _userProperty.get(this); - Session get session => _sessionProperty.get(this); -} - +```dart // In middleware: set values _userProperty[req] = authenticatedUser; diff --git a/packages/relic/skills/relic-request-response/SKILL.md b/packages/relic/skills/relic-request-response/SKILL.md index 823bc6f2..0f6d4753 100644 --- a/packages/relic/skills/relic-request-response/SKILL.md +++ b/packages/relic/skills/relic-request-response/SKILL.md @@ -38,10 +38,12 @@ const limitParam = IntQueryParam('limit'); const priceParam = DoubleQueryParam('price'); app.get('/products', (req) { - final page = req.queryParameters.get(pageParam); // int (throws if missing) - final limit = req.queryParameters.get(limitParam); // int + final page = req.queryParameters.get(pageParam); // int (throws if missing) + final limit = req.queryParameters.get(limitParam); // int final maxPrice = req.queryParameters.get(priceParam); // double - return Response.ok(body: Body.fromString('page=$page limit=$limit price=$maxPrice')); + return Response.ok( + body: Body.fromString('page=$page limit=$limit price=$maxPrice'), + ); }); ``` @@ -80,9 +82,9 @@ app.get('/filter', (req) { ```dart app.get('/info', (req) { - final userAgent = req.headers.userAgent; // String? + final userAgent = req.headers.userAgent; // String? final contentLength = req.headers.contentLength; // int? - final mimeType = req.mimeType; // MimeType? + final mimeType = req.mimeType; // MimeType? // ... }); ``` @@ -144,7 +146,10 @@ app.post('/api/users', (req) async { ); } catch (e) { return Response.badRequest( - body: Body.fromString(jsonEncode({'error': 'Invalid JSON: $e'}), mimeType: MimeType.json), + body: Body.fromString( + jsonEncode({'error': 'Invalid JSON: $e'}), + mimeType: MimeType.json, + ), ); } }); @@ -187,43 +192,43 @@ try { ### Status code constructors ```dart -Response.ok(body: Body.fromString('Success')) // 200 -Response.noContent() // 204 -Response.badRequest(body: Body.fromString('Bad input')) // 400 -Response.unauthorized() // 401 -Response.notFound() // 404 -Response.internalServerError() // 500 -Response(418, body: Body.fromString('I am a teapot')) // custom +Response.ok(body: Body.fromString('Success')); // 200 +Response.noContent(); // 204 +Response.badRequest(body: Body.fromString('Bad input')); // 400 +Response.unauthorized(); // 401 +Response.notFound(); // 404 +Response.internalServerError(); // 500 +Response(418, body: Body.fromString('I am a teapot')); // custom ``` ### Body types ```dart // Text (auto-detects MIME: JSON, HTML, XML, or plain text) -Body.fromString('Hello') // text/plain -Body.fromString('{"key": "value"}') // application/json -Body.fromString('...') // text/html +Body.fromString('Hello'); // text/plain +Body.fromString('{"key": "value"}'); // application/json +Body.fromString('...'); // text/html // Explicit MIME type -Body.fromString('...', mimeType: MimeType.html) -Body.fromString(jsonEncode(data), mimeType: MimeType.json) +Body.fromString('...', mimeType: MimeType.html); +Body.fromString(jsonEncode(data), mimeType: MimeType.json); // Binary (auto-detects PNG, JPEG, PDF, etc.) -Body.fromData(imageBytes) // image/png, etc. -Body.fromData(data, mimeType: MimeType.octetStream) // explicit +Body.fromData(imageBytes); // image/png, etc. +Body.fromData(data, mimeType: MimeType.octetStream); // explicit // Streaming (for large payloads) -Body.fromDataStream(fileStream, contentLength: fileSize) // known size -Body.fromDataStream(dynamicStream) // chunked encoding +Body.fromDataStream(fileStream, contentLength: fileSize); // known size +Body.fromDataStream(dynamicStream); // chunked encoding // Empty -Body.empty() +Body.empty(); ``` Encoding defaults to UTF-8. Override with: ```dart -Body.fromString('Café', mimeType: MimeType.plainText, encoding: latin1) +Body.fromString('Café', mimeType: MimeType.plainText, encoding: latin1); ``` ### Response headers @@ -237,7 +242,10 @@ app.get('/api/data', (req) { return Response.ok( headers: headers, - body: Body.fromString(jsonEncode({'status': 'ok'}), mimeType: MimeType.json), + body: Body.fromString( + jsonEncode({'status': 'ok'}), + mimeType: MimeType.json, + ), ); }); ``` @@ -273,9 +281,7 @@ app.get('/stream', (req) async { `Request` is immutable. Use `copyWith` to create a modified copy: ```dart -final rewritten = request.copyWith( - url: request.url.replace(path: '/new-path'), -); +final rewritten = request.copyWith(url: request.url.replace(path: '/new-path')); final modified = request.copyWith( url: request.url.replace(path: '/other'), diff --git a/packages/relic/skills/relic-routing/SKILL.md b/packages/relic/skills/relic-routing/SKILL.md index 404da51c..f67dfd7e 100644 --- a/packages/relic/skills/relic-routing/SKILL.md +++ b/packages/relic/skills/relic-routing/SKILL.md @@ -22,7 +22,10 @@ final app = RelicApp() ..get('/', (req) => Response.ok(body: Body.fromString('Hello World!'))) ..post('/', (req) => Response.ok(body: Body.fromString('Got POST'))) ..put('/user', (req) => Response.ok(body: Body.fromString('PUT /user'))) - ..delete('/user', (req) => Response.ok(body: Body.fromString('DELETE /user'))); + ..delete( + '/user', + (req) => Response.ok(body: Body.fromString('DELETE /user')), + ); ``` ### Core `add` method diff --git a/packages/relic/skills/relic-shelf-migration/SKILL.md b/packages/relic/skills/relic-shelf-migration/SKILL.md index c7b1b738..44621b99 100644 --- a/packages/relic/skills/relic-shelf-migration/SKILL.md +++ b/packages/relic/skills/relic-shelf-migration/SKILL.md @@ -102,10 +102,7 @@ final router = RelicApp() ```dart Response.ok('Hello, World!'); -Response.ok( - '...', - headers: {'content-type': 'text/html'}, -); +Response.ok('...', headers: {'content-type': 'text/html'}); ``` **Relic:** Requires `Body` object. Content-Length is automatic. MIME type is auto-detected or explicit: @@ -124,16 +121,16 @@ Response.ok( ```dart final contentType = request.headers['content-type']; // String? -final cookies = request.headers['cookie']; // String? -final date = request.headers['date']; // String? +final cookies = request.headers['cookie']; // String? +final date = request.headers['date']; // String? ``` **Relic:** Type-safe accessors with automatic parsing: ```dart -final contentType = request.body.bodyType?.mimeType; // MimeType? -final cookies = request.headers.cookie; // CookieHeader? -final date = request.headers.date; // DateTime? +final contentType = request.body.bodyType?.mimeType; // MimeType? +final cookies = request.headers.cookie; // CookieHeader? +final date = request.headers.date; // DateTime? ``` ## 7. Middleware @@ -147,9 +144,9 @@ final app = Router() }); final handler = Pipeline() - .addMiddleware(logRequests()) - .addMiddleware(authentication()) - .addHandler(app); + .addMiddleware(logRequests()) + .addMiddleware(authentication()) + .addHandler(app); ``` **Relic:** `router.use()` scopes middleware by path and only runs on matched routes: @@ -170,10 +167,9 @@ Unmatched requests (404s) bypass middleware and go directly to the fallback hand **Shelf:** Dynamic map with manual casting: ```dart -final modifiedRequest = request.change(context: { - 'user': currentUser, - 'session': sessionData, -}); +final modifiedRequest = request.change( + context: {'user': currentUser, 'session': sessionData}, +); // Later... final user = request.context['user'] as User?; @@ -184,12 +180,9 @@ final user = request.context['user'] as User?; ```dart final userProperty = ContextProperty('user'); final sessionProperty = ContextProperty('session'); +``` -extension AuthContext on Request { - User get currentUser => userProperty[this]; - Session get session => sessionProperty[this]; -} - +```dart // Set in middleware userProperty[req] = authenticatedUser; @@ -243,9 +236,7 @@ void main() async { return Response.ok('User $id: $name'); }); - final handler = Pipeline() - .addMiddleware(logRequests()) - .addHandler(router); + final handler = Pipeline().addMiddleware(logRequests()).addHandler(router); await shelf_io.serve(handler, 'localhost', 8080); } diff --git a/packages/relic/skills/relic-static-files/SKILL.md b/packages/relic/skills/relic-static-files/SKILL.md index 64de9360..32f9d7cf 100644 --- a/packages/relic/skills/relic-static-files/SKILL.md +++ b/packages/relic/skills/relic-static-files/SKILL.md @@ -43,10 +43,10 @@ app.get( StaticHandler.directory( dir, cacheControl: (req, fileInfo) => CacheControlHeader( - maxAge: 3600, // 1 hour - publicCache: true, // allow CDN/proxy caching + maxAge: 3600, // 1 hour + publicCache: true, // allow CDN/proxy caching ), -).asHandler +).asHandler; ``` ### Long-term immutable (versioned assets) @@ -55,11 +55,11 @@ StaticHandler.directory( StaticHandler.directory( dir, cacheControl: (req, fileInfo) => CacheControlHeader( - maxAge: 31536000, // 1 year + maxAge: 31536000, // 1 year publicCache: true, - immutable: true, // browsers never revalidate + immutable: true, // browsers never revalidate ), -).asHandler +).asHandler; ``` ## Cache busting @@ -73,11 +73,14 @@ final buster = CacheBustingConfig( ); // Generate cache-busted URLs (e.g., /static/hello@6cb65f8d.txt) -app.get('/', respondWith((req) async { - final cssUrl = await buster.assetPath('/static/style.css'); - final html = ''; - return Response.ok(body: Body.fromString(html, mimeType: MimeType.html)); -})); +app.get( + '/', + respondWith((req) async { + final cssUrl = await buster.assetPath('/static/style.css'); + final html = ''; + return Response.ok(body: Body.fromString(html, mimeType: MimeType.html)); + }), +); // Serve with aggressive caching (safe because URL changes on content change) app.anyOf( diff --git a/packages/relic/skills/relic-websockets/SKILL.md b/packages/relic/skills/relic-websockets/SKILL.md index a46edb42..e322eb6d 100644 --- a/packages/relic/skills/relic-websockets/SKILL.md +++ b/packages/relic/skills/relic-websockets/SKILL.md @@ -33,8 +33,8 @@ app.get('/ws', (Request req) { ### Sending data ```dart -webSocket.sendText('Hello!'); // throws on failure -webSocket.trySendText('Hello!'); // silent on failure +webSocket.sendText('Hello!'); // throws on failure +webSocket.trySendText('Hello!'); // silent on failure ``` ### Event types @@ -71,9 +71,7 @@ app.get('/sse', (Request req) { final timer = Timer.periodic( Duration(seconds: 1), - (_) => channel.sink.add( - utf8.encode('data: ${DateTime.now()}\n\n'), - ), + (_) => channel.sink.add(utf8.encode('data: ${DateTime.now()}\n\n')), ); await channel.sink.done; diff --git a/packages/relic_core/lib/src/body/body.dart b/packages/relic_core/lib/src/body/body.dart index 049cb0b3..55495b93 100644 --- a/packages/relic_core/lib/src/body/body.dart +++ b/packages/relic_core/lib/src/body/body.dart @@ -19,16 +19,13 @@ import 'types/mime_type.dart'; /// /// ### Text Body /// ```dart -/// Body.fromString('Hello, World!') +/// Body.fromString('Hello, World!'); /// ``` /// /// ### JSON Body /// ```dart /// final data = {'name': 'Alice', 'age': 30}; -/// Body.fromString( -/// jsonEncode(data), -/// mimeType: MimeType.json, -/// ) +/// Body.fromString(jsonEncode(data), mimeType: MimeType.json); /// ``` /// /// ### HTML Body @@ -36,13 +33,13 @@ import 'types/mime_type.dart'; /// Body.fromString( /// '

Welcome!

', /// mimeType: MimeType.html, -/// ) +/// ); /// ``` /// /// ### Binary Data /// ```dart /// final bytes = Uint8List.fromList([1, 2, 3, 4]); -/// Body.fromData(bytes) +/// Body.fromData(bytes); /// ``` /// /// ### Streaming Data @@ -52,12 +49,12 @@ import 'types/mime_type.dart'; /// dataStream, /// contentLength: fileSize, /// mimeType: MimeType.octetStream, -/// ) +/// ); /// ``` /// /// ### Empty Body /// ```dart -/// Body.empty() +/// Body.empty(); /// ``` /// /// ## Reading Request Bodies @@ -246,15 +243,12 @@ class Body { /// Examples: /// ```dart /// // Binary data with automatic format detection - /// final imageData = Uint8List.fromList([0x89, 0x50, 0x4E, 0x47, ...]); + /// final imageData = Uint8List.fromList([0x89, 0x50, 0x4E, 0x47]); /// final imageBody = Body.fromData(imageData); /// // Automatically detects image/png from magic bytes /// /// // Binary data with explicit MIME type - /// final binaryBody = Body.fromData( - /// data, - /// mimeType: MimeType.octetStream, - /// ); + /// final binaryBody = Body.fromData(data, mimeType: MimeType.octetStream); /// /// // PDF document detection /// final pdfBytes = utf8.encode('%PDF-1.4...'); diff --git a/packages/relic_core/lib/src/body/types/body_type.dart b/packages/relic_core/lib/src/body/types/body_type.dart index 978b7a56..6fb6400d 100644 --- a/packages/relic_core/lib/src/body/types/body_type.dart +++ b/packages/relic_core/lib/src/body/types/body_type.dart @@ -10,10 +10,7 @@ import 'mime_type.dart'; /// Examples: /// ```dart /// // Text content with encoding -/// const textType = BodyType( -/// mimeType: MimeType.plainText, -/// encoding: utf8, -/// ); +/// const textType = BodyType(mimeType: MimeType.plainText, encoding: utf8); /// print(textType.toHeaderValue()); // "text/plain; charset=utf-8" /// /// // Binary content without encoding @@ -21,10 +18,7 @@ import 'mime_type.dart'; /// print(binaryType.toHeaderValue()); // "application/octet-stream" /// /// // JSON content -/// const jsonType = BodyType( -/// mimeType: MimeType.json, -/// encoding: utf8, -/// ); +/// const jsonType = BodyType(mimeType: MimeType.json, encoding: utf8); /// print(jsonType.toHeaderValue()); // "application/json; charset=utf-8" /// ``` class BodyType { diff --git a/packages/relic_core/lib/src/context/request.dart b/packages/relic_core/lib/src/context/request.dart index 0a176f95..651fa88a 100644 --- a/packages/relic_core/lib/src/context/request.dart +++ b/packages/relic_core/lib/src/context/request.dart @@ -28,9 +28,7 @@ part of 'result.dart'; /// // Access headers /// final userAgent = req.headers.userAgent; /// -/// return Response.ok( -/// body: Body.fromString('User request'), -/// ); +/// return Response.ok(body: Body.fromString('User request')); /// }); /// /// // Reading request body diff --git a/packages/relic_core/lib/src/context/response.dart b/packages/relic_core/lib/src/context/response.dart index a1bdd4e0..292b0549 100644 --- a/packages/relic_core/lib/src/context/response.dart +++ b/packages/relic_core/lib/src/context/response.dart @@ -9,61 +9,56 @@ part of 'result.dart'; /// /// ```dart /// // 200 OK - Standard success -/// Response.ok(body: Body.fromString('Success!')) +/// Response.ok(body: Body.fromString('Success!')); /// /// // 204 No Content - Success without body -/// Response.noContent() +/// Response.noContent(); /// ``` /// /// ## Redirect Responses /// /// ```dart /// // 301 Moved Permanently -/// Response.movedPermanently(Uri.parse('/new-location')) +/// Response.movedPermanently(Uri.parse('/new-location')); /// /// // 302 Found - Temporary redirect -/// Response.found(Uri.parse('/temporary')) +/// Response.found(Uri.parse('/temporary')); /// /// // 303 See Other - Redirect after POST -/// Response.seeOther(Uri.parse('/success')) +/// Response.seeOther(Uri.parse('/success')); /// ``` /// /// ## Client Error Responses /// /// ```dart /// // 400 Bad Request -/// Response.badRequest(body: Body.fromString('Invalid input')) +/// Response.badRequest(body: Body.fromString('Invalid input')); /// /// // 401 Unauthorized -/// Response.unauthorized(body: Body.fromString('Please log in')) +/// Response.unauthorized(body: Body.fromString('Please log in')); /// /// // 403 Forbidden -/// Response.forbidden(body: Body.fromString('Access denied')) +/// Response.forbidden(body: Body.fromString('Access denied')); /// /// // 404 Not Found -/// Response.notFound(body: Body.fromString('Page not found')) +/// Response.notFound(body: Body.fromString('Page not found')); /// ``` /// /// ## Server Error Responses /// /// ```dart /// // 500 Internal Server Error -/// Response.internalServerError(body: Body.fromString('Server error')) +/// Response.internalServerError(body: Body.fromString('Server error')); /// /// // 501 Not Implemented -/// Response.notImplemented(body: Body.fromString('Coming soon')) +/// Response.notImplemented(body: Body.fromString('Coming soon')); /// ``` /// /// ## JSON Response /// /// ```dart /// final data = {'name': 'Alice', 'age': 30}; -/// Response.ok( -/// body: Body.fromString( -/// jsonEncode(data), -/// mimeType: MimeType.json, -/// ), -/// ) +/// Response.ok(body: Body.fromString(jsonEncode(data), mimeType: MimeType.json)); /// ``` /// /// ## HTML Response @@ -74,7 +69,7 @@ part of 'result.dart'; /// '

Hello!

', /// mimeType: MimeType.html, /// ), -/// ) +/// ); /// ``` class Response extends Message implements Result { /// The HTTP status code of the response. diff --git a/packages/relic_core/lib/src/context/result.dart b/packages/relic_core/lib/src/context/result.dart index 5bc79921..2e54ce27 100644 --- a/packages/relic_core/lib/src/context/result.dart +++ b/packages/relic_core/lib/src/context/result.dart @@ -27,7 +27,8 @@ sealed class Result {} /// log('Connection hijacked for custom protocol'); /// /// // Send a custom HTTP response manually -/// const response = 'HTTP/1.1 200 OK\r\n' +/// const response = +/// 'HTTP/1.1 200 OK\r\n' /// 'Content-Type: text/plain\r\n' /// 'Connection: close\r\n' /// '\r\n' diff --git a/packages/relic_core/lib/src/handler/cascade.dart b/packages/relic_core/lib/src/handler/cascade.dart index acb72df2..61706619 100644 --- a/packages/relic_core/lib/src/handler/cascade.dart +++ b/packages/relic_core/lib/src/handler/cascade.dart @@ -18,11 +18,11 @@ typedef _ShouldCascade = bool Function(Response response); /// returned. /// /// ```dart -/// var handler = new Cascade() -/// .add(webSocketHandler) -/// .add(staticFileHandler) -/// .add(application) -/// .handler; +/// var handler = new Cascade() +/// .add(webSocketHandler) +/// .add(staticFileHandler) +/// .add(application) +/// .handler; /// ``` class Cascade { /// The function used to determine whether the cascade should continue on to diff --git a/packages/relic_core/lib/src/handler/handler.dart b/packages/relic_core/lib/src/handler/handler.dart index 09b4671d..dac15dc0 100644 --- a/packages/relic_core/lib/src/handler/handler.dart +++ b/packages/relic_core/lib/src/handler/handler.dart @@ -22,9 +22,7 @@ import '../router/router.dart'; /// /// ```dart /// Response myHandler(Request req) { -/// return Response.ok( -/// body: Body.fromString('Hello, World!'), -/// ); +/// return Response.ok(body: Body.fromString('Hello, World!')); /// } /// ``` /// @@ -45,9 +43,7 @@ import '../router/router.dart'; /// // Route: /users/:id /// Handler userHandler(Request req) { /// final id = req.pathParameters[#id]; -/// return Response.ok( -/// body: Body.fromString('User ID: $id'), -/// ); +/// return Response.ok(body: Body.fromString('User ID: $id')); /// } /// ``` typedef Handler = FutureOr Function(Request req); @@ -68,9 +64,7 @@ typedef Responder = FutureOr Function(Request); /// Example: /// ```dart /// final handler = respondWith( -/// (final request) => Response.ok( -/// body: Body.fromString('Hello, Relic!'), -/// ), +/// (final request) => Response.ok(body: Body.fromString('Hello, Relic!')), /// ); /// ``` Handler respondWith(final Responder responder) { diff --git a/packages/relic_core/lib/src/handler/pipeline.dart b/packages/relic_core/lib/src/handler/pipeline.dart index 8f605f44..171f904f 100644 --- a/packages/relic_core/lib/src/handler/pipeline.dart +++ b/packages/relic_core/lib/src/handler/pipeline.dart @@ -27,9 +27,9 @@ import 'handler.dart'; /// // 5. Logging middleware (response) /// /// final handler = const Pipeline() -/// .addMiddleware(loggingMiddleware) // First to see request -/// .addMiddleware(authMiddleware) // Second to see request -/// .addHandler(apiHandler); // Last to process +/// .addMiddleware(loggingMiddleware) // First to see request +/// .addMiddleware(authMiddleware) // Second to see request +/// .addHandler(apiHandler); // Last to process /// ``` /// /// Note: this package also provides `addMiddleware` and `addHandler` extensions diff --git a/packages/relic_core/lib/src/headers/extension/string_list_extensions.dart b/packages/relic_core/lib/src/headers/extension/string_list_extensions.dart index fa2a1114..2306802f 100644 --- a/packages/relic_core/lib/src/headers/extension/string_list_extensions.dart +++ b/packages/relic_core/lib/src/headers/extension/string_list_extensions.dart @@ -9,7 +9,7 @@ extension StringListExtensions on Iterable { /// /// Example: /// ```dart - /// ['apple, banana', 'banana, orange'].splitTrimAndFilterUnique() + /// ['apple, banana', 'banana, orange'].splitTrimAndFilterUnique(); /// // Returns: ['apple', 'banana', 'orange'] /// ``` /// @@ -38,7 +38,7 @@ extension StringExtensions on String { /// /// Example: /// ```dart - /// 'apple, banana, banana, orange'.splitTrimAndFilterUnique() + /// 'apple, banana, banana, orange'.splitTrimAndFilterUnique(); /// // Returns: ['apple', 'banana', 'orange'] /// ``` /// diff --git a/packages/relic_core/lib/src/headers/headers.dart b/packages/relic_core/lib/src/headers/headers.dart index 73069519..51006ee3 100644 --- a/packages/relic_core/lib/src/headers/headers.dart +++ b/packages/relic_core/lib/src/headers/headers.dart @@ -46,10 +46,7 @@ class HeadersBase extends UnmodifiableMapView> { /// charset: 'utf-8', /// ); /// -/// h.cacheControl = CacheControlHeader( -/// maxAge: 3600, -/// publicCache: true, -/// ); +/// h.cacheControl = CacheControlHeader(maxAge: 3600, publicCache: true); /// /// // Set custom headers /// h['X-API-Version'] = ['2.0']; @@ -64,16 +61,16 @@ class HeadersBase extends UnmodifiableMapView> { /// ```dart /// // Request headers /// headers.authorization; // AuthorizationHeader? -/// headers.cookie; // List -/// headers.accept; // List -/// headers.userAgent; // String? -/// headers.host; // HostHeader? +/// headers.cookie; // List +/// headers.accept; // List +/// headers.userAgent; // String? +/// headers.host; // HostHeader? /// /// // Response headers -/// headers.cacheControl; // CacheControlHeader? -/// headers.setCookie; // SetCookieHeader? -/// headers.location; // Uri? -/// headers.contentType; // ContentTypeHeader? +/// headers.cacheControl; // CacheControlHeader? +/// headers.setCookie; // SetCookieHeader? +/// headers.location; // Uri? +/// headers.contentType; // ContentTypeHeader? /// ``` class Headers extends HeadersBase { factory Headers.fromMap(final Map>? values) { diff --git a/packages/relic_core/lib/src/middleware/middleware.dart b/packages/relic_core/lib/src/middleware/middleware.dart index a1642e7b..d1b59ad0 100644 --- a/packages/relic_core/lib/src/middleware/middleware.dart +++ b/packages/relic_core/lib/src/middleware/middleware.dart @@ -127,9 +127,7 @@ typedef Middleware = Handler Function(Handler next); /// final errorHandler = createMiddleware( /// onError: (error, stackTrace) { /// print('Error: $error'); -/// return Response.internalServerError( -/// body: Body.fromString('Server error'), -/// ); +/// return Response.internalServerError(body: Body.fromString('Server error')); /// }, /// ); /// ``` diff --git a/packages/relic_core/lib/src/middleware/routing_middleware.dart b/packages/relic_core/lib/src/middleware/routing_middleware.dart index 27929528..27e57f1f 100644 --- a/packages/relic_core/lib/src/middleware/routing_middleware.dart +++ b/packages/relic_core/lib/src/middleware/routing_middleware.dart @@ -47,7 +47,6 @@ final _routerProperty = ContextProperty('router'); /// avoids the need for [Pipeline] and provides better composability. /// /// Preferred approach: -/// ```dart /// final router = RelicRouter() /// ..get('/users/:id', userHandler) /// ..use('/', logRequests()) @@ -60,16 +59,16 @@ final _routerProperty = ContextProperty('router'); /// [Handler], requiring conversion via [toHandler]: /// /// ```dart -/// final router = Router() -/// ..get('/hello', 'Hello World'); +/// final router = Router()..get('/hello', 'Hello World'); /// /// final handler = const Pipeline() -/// .addMiddleware(routeWith( -/// router, -/// toHandler: (message) => respondWith( -/// (_) => Response.ok(body: Body.fromString(message)) +/// .addMiddleware( +/// routeWith( +/// router, +/// toHandler: (message) => +/// respondWith((_) => Response.ok(body: Body.fromString(message))), /// ), -/// )) +/// ) /// .addHandler(notFoundHandler); /// ``` Middleware routeWith( @@ -204,9 +203,7 @@ extension ForwardingRequestEx on Request { /// Typically used with [Request.copyWith] to change the URL: /// ```dart /// router.get('/old-path', (req) { - /// final newReq = req.copyWith( - /// url: req.url.replace(path: '/new-path'), - /// ); + /// final newReq = req.copyWith(url: req.url.replace(path: '/new-path')); /// return req.forwardTo(newReq); /// }); /// ``` diff --git a/packages/relic_core/lib/src/router/relic_app.dart b/packages/relic_core/lib/src/router/relic_app.dart index d41c86c1..12077e15 100644 --- a/packages/relic_core/lib/src/router/relic_app.dart +++ b/packages/relic_core/lib/src/router/relic_app.dart @@ -16,7 +16,7 @@ part of 'router.dart'; /// final app = RelicApp(useHostWhenRouting: true) /// ..get('api.example.com/users', apiUsersHandler) /// ..get('www.example.com/about', aboutHandler) -/// ..get('*/health', healthHandler); // Matches any host +/// ..get('*/health', healthHandler); // Matches any host /// ``` /// /// When `useHostWhenRouting` is `false` (the default), routing works as normal @@ -77,10 +77,10 @@ final class RelicApp implements RelicRouter, _Reloadable { /// /// Example: /// ```dart - /// final app = RelicApp() - /// ..get('/', (req) => req.ok('Hello!')); + /// final app = RelicApp()..get('/', (req) => req.ok('Hello!')); /// - /// final adapterFactory = () => IOAdapter.bind(InternetAddress.loopbackIPv4, port: 8080); + /// final adapterFactory = () => + /// IOAdapter.bind(InternetAddress.loopbackIPv4, port: 8080); /// final server = await app.run(adapterFactory); /// /// // later .. when done diff --git a/packages/relic_core/lib/src/router/router.dart b/packages/relic_core/lib/src/router/router.dart index d4d3e0dc..c659723e 100644 --- a/packages/relic_core/lib/src/router/router.dart +++ b/packages/relic_core/lib/src/router/router.dart @@ -267,17 +267,13 @@ extension RouteEx on Router { /// /// // Static routes /// router.get('/', (req) { -/// return Response.ok( -/// body: Body.fromString('Home'), -/// ); +/// return Response.ok(body: Body.fromString('Home')); /// }); /// /// // Route with parameters /// router.get('/users/:id', (req) { /// final id = req.pathParameters['id']; -/// return Response.ok( -/// body: Body.fromString('User $id'), -/// ); +/// return Response.ok(body: Body.fromString('User $id')); /// }); /// /// // Multiple parameters @@ -291,7 +287,6 @@ extension RouteEx on Router { /// /// ## HTTP Methods /// -/// ```dart /// router.get('/users', (req) => /* list users */); /// router.post('/users', (req) => /* create user */); /// router.put('/users/:id', (req) => /* update user */); @@ -301,7 +296,6 @@ extension RouteEx on Router { /// /// ## Sub-routers /// -/// ```dart /// final apiRouter = Router(); /// apiRouter.get('/users', (req) => /* users */); /// apiRouter.get('/posts', (req) => /* posts */); @@ -341,10 +335,10 @@ typedef RelicRouter = Router; /// ..delete('/', delete); /// } /// -/// Response create(final Request req) { } -/// Response read(final Request req) { } -/// Response update(final Request req) { } -/// Response delete(final Request req) { } +/// Response create(final Request req) {} +/// Response read(final Request req) {} +/// Response update(final Request req) {} +/// Response delete(final Request req) {} /// } /// ``` typedef RouterInjectable = InjectableIn; diff --git a/packages/relic_io/lib/src/io/static/static_handler.dart b/packages/relic_io/lib/src/io/static/static_handler.dart index 2573bd64..397a6434 100644 --- a/packages/relic_io/lib/src/io/static/static_handler.dart +++ b/packages/relic_io/lib/src/io/static/static_handler.dart @@ -74,33 +74,33 @@ Future getStaticFileInfo( /// ### Examples /// /// ```dart -/// // Basic directory, 1 day cache -/// StaticHandler.directory( -/// Directory('static'), -/// cacheControl: (_, __) => CacheControlHeader(maxAge: 86400), -/// ); +/// // Basic directory, 1 day cache +/// StaticHandler.directory( +/// Directory('static'), +/// cacheControl: (_, __) => CacheControlHeader(maxAge: 86400), +/// ); /// -/// // Long term/immutable + cache busting -/// final staticDir = Directory('static'); -/// final buster = CacheBustingConfig( -/// mountPrefix: '/static', -/// fileSystemRoot: staticDir, -/// ); -/// StaticHandler.directory( -/// staticDir, -/// cacheControl: (_, __) => CacheControlHeader( -/// maxAge: 31536000, -/// publicCache: true, -/// immutable: true, -/// ), -/// cacheBustingConfig: buster, -/// ); +/// // Long term/immutable + cache busting +/// final staticDir = Directory('static'); +/// final buster = CacheBustingConfig( +/// mountPrefix: '/static', +/// fileSystemRoot: staticDir, +/// ); +/// StaticHandler.directory( +/// staticDir, +/// cacheControl: (_, __) => CacheControlHeader( +/// maxAge: 31536000, +/// publicCache: true, +/// immutable: true, +/// ), +/// cacheBustingConfig: buster, +/// ); /// -/// // Single file, 1 day cache -/// StaticHandler.file( -/// File('assets/favicon.ico'), -/// cacheControl: (_, __) => CacheControlHeader(maxAge: 86400), -/// ); +/// // Single file, 1 day cache +/// StaticHandler.file( +/// File('assets/favicon.ico'), +/// cacheControl: (_, __) => CacheControlHeader(maxAge: 86400), +/// ); /// ``` /// /// ## Security Features diff --git a/packages/test_utils/lib/src/test_utils_base.dart b/packages/test_utils/lib/src/test_utils_base.dart index bf22ecd9..82e9fd47 100644 --- a/packages/test_utils/lib/src/test_utils_base.dart +++ b/packages/test_utils/lib/src/test_utils_base.dart @@ -72,15 +72,13 @@ final isOhNoStateError = isA().having( /// /// Example: /// ```dart -/// parameterizedGroup( -/// (protocol) => 'Testing protocol: $protocol', -/// (protocol) { -/// test('connects successfully', () { -/// // Test using the protocol parameter -/// }); -/// }, -/// variants: ['http', 'https', 'ws'], -/// ); +/// parameterizedGroup((protocol) => 'Testing protocol: $protocol', ( +/// protocol, +/// ) { +/// test('connects successfully', () { +/// // Test using the protocol parameter +/// }); +/// }, variants: ['http', 'https', 'ws']); /// ``` @isTestGroup void parameterizedGroup( @@ -105,13 +103,9 @@ void parameterizedGroup( /// /// Example: /// ```dart -/// parameterizedTest( -/// (value) => 'Test with value: $value', -/// (value) { -/// expect(value * 2, greaterThan(value)); -/// }, -/// variants: [1, 2, 3, 4, 5], -/// ); +/// parameterizedTest((value) => 'Test with value: $value', (value) { +/// expect(value * 2, greaterThan(value)); +/// }, variants: [1, 2, 3, 4, 5]); /// ``` @isTest void parameterizedTest(