Skip to content

Commit 2ca9fe9

Browse files
committed
refactor: update caching strategy and response handling documentation
- Clarified usage of `whenConstOrNull` and `toRegisteredStatusCode()` - Improved regex pattern for status code matching with negative lookarounds - Enhanced test cases for regex pattern validation
1 parent 1120102 commit 2ca9fe9

6 files changed

Lines changed: 87 additions & 29 deletions

File tree

extension/mcp/prompts/cache_strategy.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ Follow these steps:
2727
- `501 Not Implemented` — long TTL, unlikely to change soon
2828
- `204`, `203`, `206`, `300`, `414` — short to moderate TTL depending on use case
2929

30-
Use `whenConstOrNull` to express this as a compile-time mapping:
30+
Use `whenConstOrNull` to express this as a compile-time mapping (`whenConstOrNull` is on `StatusCode`, so convert from raw `int` first):
3131

3232
```dart
33-
final ttl = statusCode.whenConstOrNull(
33+
final ttl = rawStatusCode.toRegisteredStatusCode()?.whenConstOrNull(
3434
okHttp200: Duration(minutes: 5),
3535
noContentHttp204: Duration(minutes: 1),
3636
notFoundHttp404: Duration(minutes: 10),
@@ -47,5 +47,5 @@ Follow these steps:
4747
4. **Emit a complete, reusable function** that:
4848
- Checks `isStatusCode` before doing anything (guards against invalid codes)
4949
- Uses `isCacheable` to gate the write
50-
- Uses `whenConstOrNull` or `whenConst` for TTL assignment (no runtime allocations)
50+
- Uses `toRegisteredStatusCode()?.whenConstOrNull(...)` for TTL assignment — convert first since these methods are on `StatusCode`, not raw `int` (no runtime allocations)
5151
- Falls back gracefully for non-cacheable codes (log and skip, do not throw)

extension/mcp/prompts/handle_response.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Follow these steps:
3737

3838
4. **Emit null-safe, idiomatic Dart**:
3939
- Prefer `?.` and `??` over null-checks
40-
- Use `const` callbacks with `whenConstStatusCode` / `whenConstOrNull` when the return values are constants
40+
- Use `whenConstStatusCode` / `whenConstOrNull` when the return values are constants — these accept **direct values**, not closures (e.g. `isSuccess: 'ok'`, never `isSuccess: () => 'ok'`); they must be called on a `StatusCode` value after `toRegisteredStatusCode()` conversion
4141
- Avoid `if (code >= 200 && code < 300)` — use `.isSuccess` / `.isError` / `.isCacheable` etc.
4242

4343
5. **Handle the specific code** {{#status_code}}({{status_code}}){{/status_code}} with a dedicated branch inside `maybeMap` or `maybeWhen` on the `StatusCode`, using the constant name `<camelName>Http<NNN>`.

extension/mcp/resources/api_reference.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,26 +130,29 @@ response.statusCode.maybeWhenStatusCode(
130130
isSuccess: () => true,
131131
)
132132
133-
// Const-value variant (no closures — direct values)
133+
// Const-value variant — all 5 categories required, no orElse, asserts in-range
134134
response.statusCode.whenConstStatusCode(
135135
isInformational: 'info',
136136
isSuccess: 'ok',
137137
isRedirection: 'redirect',
138138
isClientError: 'client error',
139139
isServerError: 'server error',
140-
orElse: 'unknown',
141140
)
142141
143-
// Returns null instead of requiring orElse
142+
// Nullable variant — all params optional, orElse is also optional
144143
response.statusCode.whenConstStatusCodeOrNull(
145144
isSuccess: 'ok',
146145
isClientError: 'client error',
146+
orElse: 'unknown',
147147
)
148148
149-
// Convert then map in one step
149+
// Convert then map per category in one step
150150
response.statusCode.mapToRegisteredStatusCode(
151-
mapper: (statusCode) => statusCode.reason,
152-
orElse: (raw) => 'Unknown: $raw',
151+
isInformational: (code) => code?.reason ?? 'info',
152+
isSuccess: (code) => code?.reason ?? 'success',
153+
isRedirection: (code) => code?.reason ?? 'redirect',
154+
isClientError: (code) => code?.reason ?? 'client error',
155+
isServerError: (code) => code?.reason ?? 'server error',
153156
)
154157
```
155158

extension/mcp/resources/patterns.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ final icon = statusCode.whenConstOrNull(
8383
);
8484
```
8585

86-
Category-level const mapping via `whenConstStatusCode`:
86+
Category-level const mapping via `whenConstStatusCodeOrNull`:
8787

8888
```dart
89-
final label = rawCode.whenConstStatusCode(
89+
final label = rawCode.whenConstStatusCodeOrNull(
9090
isInformational: 'informational',
9191
isSuccess: 'success',
9292
isRedirection: 'redirect',
@@ -188,15 +188,15 @@ import 'package:functional_status_codes/functional_status_codes.dart';
188188
test('handler returns null for any client error', () {
189189
for (var i = 0; i < 50; i++) {
190190
final code = StatusCode.random(from: StatusCode.clientErrorCodes);
191-
expect(handleResponse(code), isNull);
191+
expect(handleResponse(code, () => 'body'), isNull);
192192
}
193193
});
194194
195-
test('cache is written for any cacheable code', () {
195+
test('cache is written for any cacheable code', () async {
196196
for (var i = 0; i < 20; i++) {
197197
final code = StatusCode.random(from: StatusCode.cacheableCodes);
198198
expect(code.isCacheable, isTrue);
199-
fetchAndCache('key', () async => Response(statusCode: code));
199+
await fetchAndCache('key', () async => Response(statusCode: code));
200200
expect(cache.has('key'), isTrue);
201201
}
202202
});
@@ -213,6 +213,6 @@ test('unknown code is handled gracefully', () {
213213
final unknown = StatusCode.custom(999);
214214
expect(unknown.isCustom, isTrue);
215215
expect(unknown.toRegisteredStatusCode(), isNull);
216-
expect(handleResponse(unknown), isNull);
216+
expect(handleResponse(unknown, () => 'body'), isNull);
217217
});
218218
```

lib/src/status_code.dart

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -687,9 +687,10 @@ extension type const StatusCode._(int _code) implements int {
687687
///
688688
/// This pattern is commonly used to identify HTTP status codes within a
689689
/// string. HTTP status codes are typically 3-digit integers ranging from 100
690-
/// to 599. The pattern is defined by the regular expression `[1-5]\d{2}`,
691-
/// which matches digits starting with 1-5 followed by exactly two more
692-
/// digits, covering the full 100-599 range.
690+
/// to 599. The pattern is defined by the regular expression
691+
/// `(?<!\d)[1-5]\d{2}(?!\d)`, which matches a digit 1–5 followed by exactly
692+
/// two more digits and uses negative lookarounds to prevent matching
693+
/// sub-sequences inside longer digit runs (e.g. `6100` will not yield `100`).
693694
///
694695
/// Examples of matching strings:
695696
/// - '200' for OK
@@ -703,18 +704,18 @@ extension type const StatusCode._(int _code) implements int {
703704
/// See also:
704705
/// - [RegExp], the class used to work with regular expressions in Dart.
705706
/// - [StatusCode], which contains standard HTTP status codes.
706-
static const pattern = r'[1-5]\d{2}';
707+
static const pattern = r'(?<!\d)[1-5]\d{2}(?!\d)';
707708

708709
/// A getter that returns a [RegExp] object configured with [pattern] to match
709-
/// HTTP status codes in the range 100599.
710+
/// HTTP status codes in the range 100-599.
710711
///
711712
/// The matching is unanchored, meaning that this regular expression can find
712713
/// matches anywhere in the input string. This allows for the extraction of
713714
/// status codes from within larger bodies of text.
714715
///
715-
/// The [pattern] is defined by the regular expression `[1-5]\d{2}`, which
716-
/// matches a digit 1–5 followed by exactly two more digits, covering the
717-
/// full valid HTTP status code range (100599).
716+
/// The [pattern] uses negative lookarounds (`(?<!\d)` / `(?!\d)`) to ensure
717+
/// that a three-digit sequence is not part of a longer number, covering the
718+
/// full valid HTTP status code range (100-599) without false positives.
718719
///
719720
/// Example usage:
720721
/// ```dart
@@ -1251,10 +1252,11 @@ extension type const StatusCode._(int _code) implements int {
12511252
Iterable<StatusCode> from = values,
12521253
Random? random,
12531254
}) {
1254-
assert(from.isNotEmpty, 'The provided `from` iterable must not be empty');
1255-
final elementAt = (random ?? Random()).nextInt(from.length);
1255+
final list = from is List<StatusCode> ? from : from.toList();
1256+
assert(list.isNotEmpty, 'The provided `from` iterable must not be empty');
1257+
final elementAt = (random ?? Random()).nextInt(list.length);
12561258

12571259
// ignore: avoid-unsafe-collection-methods, length is guaranteed to be > 0.
1258-
return from.elementAt(elementAt);
1260+
return list[elementAt];
12591261
}
12601262
}

test/src/status_code_test.dart

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,9 @@ void main() => group('$StatusCode', () {
215215
);
216216

217217
test(
218-
'should extract first three-digit match from longer number',
219-
// '12003' matches '120' first — not a registered code.
218+
'should return null for longer number with embedded valid range digits',
219+
// '12003': lookahead/lookbehind prevents matching '120' (followed by digit)
220+
// or '200' (preceded by digit) — no isolated [1-5]xx sequence found.
220221
() => expect(StatusCode.tryParse('12003'), isNull),
221222
);
222223

@@ -232,6 +233,58 @@ void main() => group('$StatusCode', () {
232233
);
233234
});
234235

236+
group('pattern and regExp', () {
237+
test(
238+
'pattern matches the expected regex string',
239+
() => expect(StatusCode.pattern, r'(?<!\d)[1-5]\d{2}(?!\d)'),
240+
);
241+
242+
test(
243+
'regExp is a RegExp built from pattern',
244+
() => expect(StatusCode.regExp, isA<RegExp>()),
245+
);
246+
247+
test(
248+
'regExp matches a bare valid code',
249+
() => expect(StatusCode.regExp.hasMatch('200'), isTrue),
250+
);
251+
252+
test(
253+
'regExp matches a code preceded by non-digit characters',
254+
() => expect(StatusCode.regExp.hasMatch('status: 404.'), isTrue),
255+
);
256+
257+
test(
258+
'regExp matches a code followed by non-digit characters',
259+
() => expect(StatusCode.regExp.hasMatch('200 OK'), isTrue),
260+
);
261+
262+
test(
263+
'regExp does not match a 6xx code',
264+
() => expect(StatusCode.regExp.hasMatch('600'), isFalse),
265+
);
266+
267+
test(
268+
'regExp does not match a 9xx code',
269+
() => expect(StatusCode.regExp.hasMatch('999'), isFalse),
270+
);
271+
272+
test(
273+
'regExp does not match a valid-range code embedded in a longer number',
274+
() => expect(StatusCode.regExp.hasMatch('12003'), isFalse),
275+
);
276+
277+
test(
278+
'regExp does not match when code is preceded by a digit',
279+
() => expect(StatusCode.regExp.hasMatch('1200'), isFalse),
280+
);
281+
282+
test(
283+
'regExp does not match when code is followed by a digit',
284+
() => expect(StatusCode.regExp.hasMatch('2001'), isFalse),
285+
);
286+
});
287+
235288
group('custom constructor edge cases', () {
236289
test('valid custom code in gap between standard codes', () {
237290
const custom = StatusCode.custom(109);

0 commit comments

Comments
 (0)