Skip to content
Open
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
59 changes: 34 additions & 25 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<NewContext>());
});

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<NewContext>());
});

test('then the new context contains the new request', () {
// Assert
expect(newContext.request, same(newRequest));
});
},
);
```

### Common Development Patterns
Expand All @@ -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.'),
),
);
}
```

Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +47 to +50
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Is it possible to preserver trailing commas on the formatting? I would say that some of the snippets lost a bit in readability due to not preserving them.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.. 2m


- name: Analyze (downgraded)
run: >-
Expand Down
9 changes: 3 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,24 @@ import 'package:relic/relic.dart';
/// A simple 'Hello World' server demonstrating basic Relic usage.
Future<void> 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<int>(#age, int.parse);

/// Handles requests to the hello endpoint with path parameters.
Response helloHandler(final Request req) {
final name = req.pathParameters.raw[#name];
Expand Down
38 changes: 21 additions & 17 deletions doc/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -109,17 +111,17 @@ 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);
};
};
}

// 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}')),
);
};
```
Expand All @@ -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:**
Expand Down Expand Up @@ -190,7 +194,7 @@ final class PathMiss<T> extends LookupResult<T> {
}

final class MethodMiss<T> extends LookupResult<T> {
final Set<Method> allowed; // Path exists but wrong method
final Set<Method> allowed; // Path exists but wrong method
// Return 405 Method Not Allowed with Allow header
}

Expand Down Expand Up @@ -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
```
Expand All @@ -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];
});
```
Expand All @@ -250,10 +254,10 @@ While Shelf uses `Uint8List` at runtime (since v1.1.1), its type signature decla

```dart
// Shelf
Stream<List<int>> body; // Runtime is Uint8List, but type says List<int>
Stream<List<int>> body; // Runtime is Uint8List, but type says List<int>

// Relic
Stream<Uint8List> body; // Type matches runtime, making intent clear
Stream<Uint8List> body; // Type matches runtime, making intent clear
```

This eliminates potential type confusion and makes the API contract explicit.
Expand All @@ -266,7 +270,7 @@ In Shelf, the content-type header and encoding live separately from the body, wh
class Body {
final Stream<Uint8List> stream;
final int? contentLength;
final BodyType? bodyType; // Combines mimeType + encoding
final BodyType? bodyType; // Combines mimeType + encoding
}
```

Expand Down Expand Up @@ -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) {
Expand All @@ -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
});
};
```
Expand Down
24 changes: 9 additions & 15 deletions doc/site/docs/01-getting-started/03-shelf-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -180,10 +178,9 @@ final app = Router()
});

final handler = Pipeline()
.addMiddleware(logRequests())
.addMiddleware(authentication())
.addHandler(app);

.addMiddleware(logRequests())
.addMiddleware(authentication())
.addHandler(app);
```

Relic:
Expand All @@ -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:
Expand Down Expand Up @@ -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);
}
Expand Down
8 changes: 3 additions & 5 deletions doc/site/docs/02-reference/03-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
```

Expand Down Expand Up @@ -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
```

Expand All @@ -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
```

Expand Down
4 changes: 1 addition & 3 deletions doc/site/docs/02-reference/04-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading