Repository: VeryGoodOpenSource/dart_frog (dart)

Issue:
feat: Allow routing parameters in `middleware`
**Description**
Currently is not possible to access routing parameters on middlewares. Would be nice in cases where we have nested routing parameters and have different logic dependent on that parameter (e.g. `/[userId]/tasks/[taskId]`, middleware on `/[userId]/_middleware.dart` that checks if we have permissions for that `[userId`) .

My current workaround relies on private implementations 😕:

**Middleware `/[test]/_middleware.dart`:**
```dart
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_frog/src/_internal.dart' show toShelfHandler;

Handler middleware(Handler handler /* String test */) { // `String test` does not work
  return fromShelfHandler((request) async {
    print((request.context['shelf_router/params'] as Map<String, String>?)?['test']);
    return toShelfHandler(handler)(request);
  });
}
```

**Index `/[test]/index.dart`:**
```dart
Future<Response> onRequest(RequestContext context, String test) async {
  return Response(body: '$test');
}
```

Pull request:
<!--
  Thanks for contributing!

  Provide a description of your changes below and a general summary in the title

  Please look at the following checklist to ensure that your PR can be accepted quickly:
-->

## Status

**READY**

## Description

<!--- Describe your changes in detail -->
- feat(dart_frog): expose captured request params
  - closes #773
  - closes #1262

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [X] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore

Patch:
diff --git a/packages/dart_frog/lib/src/request.dart b/packages/dart_frog/lib/src/request.dart
index 516a5e071..bd180da86 100644
--- a/packages/dart_frog/lib/src/request.dart
+++ b/packages/dart_frog/lib/src/request.dart
@@ -135,6 +135,12 @@ class Request {
     );
   }
 
+  /// Returns the url parameters captured by the [Router].
+  /// Returns an empty map if no parameters are captured.
+  ///
+  /// The returned map is unmodifiable.
+  Map<String, String> get params => _request.params;
+
   /// Returns a [Stream] representing the body.
   Stream<List<int>> bytes() => _request.read();
 


Test patch:
diff --git a/packages/dart_frog/test/src/request_test.dart b/packages/dart_frog/test/src/request_test.dart
index 564660fdc..9cb4016da 100644
--- a/packages/dart_frog/test/src/request_test.dart
+++ b/packages/dart_frog/test/src/request_test.dart
@@ -1,4 +1,5 @@
 import 'dart:async';
+import 'dart:collection';
 import 'dart:convert';
 import 'dart:io';
 
@@ -79,6 +80,14 @@ ${HttpMethod.values.map((m) => m.value.toUpperCase()).join(', ')}.'''),
       expect(request.headers['foo'], equals(headers['foo']));
     });
 
+    test('has correct params (empty)', () {
+      final request = Request('GET', localhost);
+      expect(
+        request.params,
+        equals(UnmodifiableMapView<String, String>(const {})),
+      );
+    });
+
     test('body can be read multiple times (sync)', () {
       final body = json.encode({'test': 'body'});
       final request = Request('GET', localhost, body: body);
diff --git a/packages/dart_frog/test/src/router_test.dart b/packages/dart_frog/test/src/router_test.dart
index c6c1eb467..9c159fa8f 100644
--- a/packages/dart_frog/test/src/router_test.dart
+++ b/packages/dart_frog/test/src/router_test.dart
@@ -16,6 +16,7 @@
 @TestOn('vm')
 library;
 
+import 'dart:collection';
 import 'dart:convert';
 import 'dart:io';
 
@@ -291,10 +292,90 @@ void main() {
     expect(response.body, equals('hello'));
   });
 
+  test('request exposes captured params (empty)', () async {
+    final context = _MockRequestContext();
+    final app = Router()
+      ..get('/hello', (RequestContext context) {
+        expect(
+          context.request.params,
+          equals(UnmodifiableMapView<String, String>(const {})),
+        );
+        return Response(body: 'hello world');
+      });
+
+    server.mount((request) async {
+      when(() => context.request).thenReturn(
+        Request(request.method, request.requestedUri),
+      );
+      final response = await app(context);
+      final body = await response.body();
+      return shelf.Response(response.statusCode, body: body);
+    });
+
+    final response = await http.get(Uri.parse('${server.url}/hello'));
+    expect(response.statusCode, equals(HttpStatus.ok));
+    expect(response.body, equals('hello world'));
+  });
+
+  test('request exposes captured params (single)', () async {
+    final context = _MockRequestContext();
+    final app = Router()
+      ..get('/users/<id>/greet', (RequestContext context, String id) {
+        expect(
+          context.request.params,
+          equals(UnmodifiableMapView<String, String>({'id': id})),
+        );
+        return Response(body: 'hello $id');
+      });
+
+    server.mount((request) async {
+      when(() => context.request).thenReturn(
+        Request(request.method, request.requestedUri),
+      );
+      final response = await app(context);
+      final body = await response.body();
+      return shelf.Response(response.statusCode, body: body);
+    });
+
+    final response = await http.get(Uri.parse('${server.url}/users/42/greet'));
+    expect(response.statusCode, equals(HttpStatus.ok));
+    expect(response.body, equals('hello 42'));
+  });
+
+  test('request exposes captured params (multiple)', () async {
+    final context = _MockRequestContext();
+    final app = Router()
+      ..get('/users/<id>/greet/<name>', (
+        RequestContext context,
+        String id,
+        String name,
+      ) {
+        expect(
+          context.request.params,
+          equals(UnmodifiableMapView<String, String>({'id': id, 'name': name})),
+        );
+        return Response(body: 'hello $name ($id)');
+      });
+
+    server.mount((request) async {
+      when(() => context.request).thenReturn(
+        Request(request.method, request.requestedUri),
+      );
+      final response = await app(context);
+      final body = await response.body();
+      return shelf.Response(response.statusCode, body: body);
+    });
+
+    final response =
+        await http.get(Uri.parse('${server.url}/users/42/greet/felangel'));
+    expect(response.statusCode, equals(HttpStatus.ok));
+    expect(response.body, equals('hello felangel (42)'));
+  });
+
   test('mount(Router)', () async {
     final context = _MockRequestContext();
     final api = Router()
-      ..all('/user/<user>/info', (RequestContext request, String user) {
+      ..all('/user/<user>/info', (RequestContext context, String user) {
         return Response(body: 'Hello $user');
       });