From 39713efce09e1b160fbfe511da34138d7aa638db Mon Sep 17 00:00:00 2001 From: Roman Cinis <52065414+tsinis@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:53:42 +0100 Subject: [PATCH 1/8] feat: add new features and fixes for version 3.2.0 - Introduced `isError` getter for error checking on HTTP status codes. - Packaged AI assets for enhanced coding agent support. - Fixed various issues including null checks and regex patterns. - Updated documentation for clarity on handler priorities. --- CHANGELOG.md | 30 ++++++++++++++++++ README.md | 4 --- lib/src/num_status_code_extension.dart | 42 ++++++++++++++++++++------ lib/src/status_code.dart | 20 ++++++------ pubspec.yaml | 2 +- 5 files changed, 73 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 857a0a0..23afe67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +## 3.2.0 + +### New Features + +- **`isError` getter**: Convenience check for any client or server error (4xx or 5xx). Equivalent to `isClientError || isServerError`. + + ```dart + print(404.isError); // true + print(500.isError); // true + print(200.isError); // false + ``` + +- **Packaged AI assets** (`extension/mcp/`): The package now ships MCP-compatible resources and prompts for AI coding agents, following the [Dart/Flutter packaged AI assets proposal](https://flutter.dev/go/packaged-ai-assets). +Once the Dart MCP server implements the proposal, agents will automatically have access to: + - `@functional_status_codes/api_reference` — full API cheat-sheet + - `@functional_status_codes/patterns` — idiomatic usage patterns + - `/functional_status_codes/handle_response` — prompt to generate HTTP response handling code + - `/functional_status_codes/cache_strategy` — prompt to derive a caching strategy from a status code + +### Fixes + +- Fixed: `maybeMapStatusCode` now performs the `null` check before calling `toRegisteredStatusCode()`, which was previously invoked unnecessarily for `null` inputs. +- Fixed: `StatusCode.random()` no longer wraps the source iterable in an intermediate `List.unmodifiable(...)` just to call `elementAt`. The allocation was unnecessary since `Iterable.elementAt` works directly. +- Fixed: `StatusCode.pattern` (and `StatusCode.regExp`) now matches only the valid HTTP status code range (`[1-5]\d{2}`, i.e. 100–599) instead of any three-digit number (`\d{3}`). This avoids false matches on strings like `"timeout after 999ms"`. Note: `tryParse` behaviour is unchanged for all registered codes — only the raw pattern/regex changes. +- Fixed: `values` doc comment count corrected from 95 to 93. + +### Documentation + +- `maybeMapStatusCode` and `maybeWhenStatusCode`: added explicit note that the `isStatusCode` handler takes priority over all category handlers (`isSuccess`, `isClientError`, etc.) when both are provided. + ## 3.1.0 This release adds several new features for better HTTP status code handling, including cacheable/retryable checks, pre-sorted category collections, and a random status code generator. Also includes important bug fixes. diff --git a/README.md b/README.md index e4573ea..ad3e75c 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,3 @@ For more information on using this package, check out the API documentation and If you like this package, please give it a star or a like. For more information on using this package, check out the API documentation. **PRs or ideas are always welcome**. If you have any issues or suggestions for the package, please file them in the GitHub repository. - - -## License -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftsinis%2Ffunctional_status_codes.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftsinis%2Ffunctional_status_codes?ref=badge_large) diff --git a/lib/src/num_status_code_extension.dart b/lib/src/num_status_code_extension.dart index f338ef0..bf10ac2 100644 --- a/lib/src/num_status_code_extension.dart +++ b/lib/src/num_status_code_extension.dart @@ -9,7 +9,7 @@ import 'status_code.dart'; /// Extension on [num?] types to provide additional functionality when working /// with HTTP status codes. extension NumStatusCodeExtension on T? { - static const _outSideOfRangeMessage = + static const _outsideOfRangeMessage = // ignore: avoid-adjacent-strings, it's not being used on one line strings. 'Value is outside of ' '''${StatusCode.continueHttp100}-${StatusCode.networkConnectTimeoutErrorHttp599}''' @@ -216,6 +216,18 @@ extension NumStatusCodeExtension on T? { bool get isServerError => isStatusWithinRange(min: StatusCode.internalServerErrorHttp500); + /// Returns `true` if the value is within the range of client or server error + /// HTTP status codes (400-599), `false` otherwise. + /// + /// Example: + /// + /// ```dart + /// print(400.isError); // true + /// print(500.isError); // true + /// print(200.isError); // false + /// ``` + bool get isError => isClientError || isServerError; + /// Converts the [num?] value to a [StatusCode] if it exists within the range /// of valid HTTP status codes (100-599), or returns `null` if it is outside /// of that range or `null`. @@ -260,7 +272,7 @@ extension NumStatusCodeExtension on T? { }) { assert( isStatusCode, - '$_outSideOfRangeMessage. Consider using maybeMapStatusCode() instead', + '$_outsideOfRangeMessage. Consider using maybeMapStatusCode() instead', ); final thisValue = this; if (thisValue == null) throw FormatException('Null value provided!', this); @@ -269,13 +281,18 @@ extension NumStatusCodeExtension on T? { if (thisValue.isRedirection) return isRedirection(thisValue); if (thisValue.isClientError) return isClientError(thisValue); if (thisValue.isServerError) return isServerError(thisValue); - throw FormatException(_outSideOfRangeMessage, thisValue); + throw FormatException(_outsideOfRangeMessage, thisValue); } /// If the [num?] value is a valid HTTP status code, maps it to a result using /// the provided functions. If the value is not a valid HTTP status code, /// returns the result of calling [orElse]. /// + /// **Handler priority:** [isStatusCode] is checked first and matches any + /// valid HTTP status code (100–599). If provided alongside a category handler + /// (e.g. [isSuccess]), [isStatusCode] will always fire instead. Only omit + /// [isStatusCode] when you want category-specific matching. + /// /// Example: /// /// ```dart @@ -323,6 +340,11 @@ extension NumStatusCodeExtension on T? { /// redirection, client error, or server error). If the value is not a valid /// HTTP status code, returns the result of calling [orElse]. /// + /// **Handler priority:** [isStatusCode] is checked first and matches any + /// valid HTTP status code (100–599). If provided alongside a category handler + /// (e.g. [isSuccess]), [isStatusCode] will always fire instead. Only omit + /// [isStatusCode] when you want category-specific matching. + /// /// Example: /// /// ```dart @@ -391,14 +413,14 @@ extension NumStatusCodeExtension on T? { }) { assert( isStatusCode, - '$_outSideOfRangeMessage. Consider using maybeWhenStatusCode() instead', + '$_outsideOfRangeMessage. Consider using maybeWhenStatusCode() instead', ); if (this.isInformational) return isInformational(); if (this.isSuccess) return isSuccess(); if (this.isRedirection) return isRedirection(); if (this.isClientError) return isClientError(); if (this.isServerError) return isServerError(); - throw FormatException(_outSideOfRangeMessage, this); + throw FormatException(_outsideOfRangeMessage, this); } /// Evaluates the provided functions based on the HTTP status code represented @@ -471,14 +493,14 @@ extension NumStatusCodeExtension on T? { }) { assert( isStatusCode, - '''$_outSideOfRangeMessage. Consider using whenConstStatusCodeOrNull() instead''', + '''$_outsideOfRangeMessage. Consider using whenConstStatusCodeOrNull() instead''', ); if (this.isInformational) return isInformational; if (this.isSuccess) return isSuccess; if (this.isRedirection) return isRedirection; if (this.isClientError) return isClientError; if (this.isServerError) return isServerError; - throw FormatException(_outSideOfRangeMessage, this); + throw FormatException(_outsideOfRangeMessage, this); } /// A [Map] like equivalent of [whenStatusCodeOrNull] method. @@ -552,7 +574,7 @@ extension NumStatusCodeExtension on T? { }) { assert( isStatusCode, - '''$_outSideOfRangeMessage. Consider using maybeMapToRegisteredStatusCode() instead''', + '''$_outsideOfRangeMessage. Consider using maybeMapToRegisteredStatusCode() instead''', ); final thisValue = this; if (thisValue == null) throw FormatException('Null value provided!', this); @@ -564,7 +586,7 @@ extension NumStatusCodeExtension on T? { if (thisValue.isClientError) return isClientError(registeredStatusCode); if (thisValue.isServerError) return isServerError(registeredStatusCode); - throw FormatException(_outSideOfRangeMessage, thisValue); + throw FormatException(_outsideOfRangeMessage, thisValue); } /// If the [num?] value is a valid HTTP status code, maps it to a result using @@ -598,8 +620,8 @@ extension NumStatusCodeExtension on T? { R Function(StatusCode? serverErrorStatusCode)? isServerError, }) { final thisValue = this; + if (thisValue == null) return orElse(null, thisValue); final registeredStatusCode = thisValue.toRegisteredStatusCode(); - if (thisValue == null) return orElse(registeredStatusCode, thisValue); if (thisValue.isStatusCode && isStatusCode != null) { return isStatusCode(registeredStatusCode); diff --git a/lib/src/status_code.dart b/lib/src/status_code.dart index 8e32548..b8d2b40 100644 --- a/lib/src/status_code.dart +++ b/lib/src/status_code.dart @@ -683,27 +683,27 @@ extension type const StatusCode._(int _code) implements int { /// in front of the proxy. static const networkConnectTimeoutErrorHttp599 = StatusCode._(599); - /// A regular expression pattern that matches three consecutive digits. + /// A regular expression pattern that matches a valid HTTP status code range. /// /// This pattern is commonly used to identify HTTP status codes within a /// string. HTTP status codes are typically 3-digit integers ranging from 100 - /// to 599. The pattern is defined by the regular expression `\d{3}`, where - /// `\d` stands for any digit, and `{3}` specifies exactly three occurrences - /// of the digit. + /// to 599. The pattern is defined by the regular expression `[1-5]\d{2}`, + /// which matches digits starting with 1–5 followed by exactly two more + /// digits, covering the full 100–599 range. /// /// Examples of matching strings: /// - '200' for OK /// - '404' for Not Found /// - '500' for Internal Server Error /// - /// Note that this pattern does not validate the range of the status code and - /// will match any three-digit number. Additional checks should be implemented - /// if validation of the status code's range is required. + /// Note that this pattern does not validate whether the matched code is a + /// registered status code. Additional checks should be implemented if exact + /// code validation is required (e.g. via [maybeFromCode]). /// /// See also: /// - [RegExp], the class used to work with regular expressions in Dart. /// - [StatusCode], which contains standard HTTP status codes. - static const pattern = r'\d{3}'; + static const pattern = r'[1-5]\d{2}'; /// A getter that returns a [RegExp] object configured with a pattern to match /// three consecutive digits, typically representing an HTTP status code. @@ -775,7 +775,7 @@ extension type const StatusCode._(int _code) implements int { /// A complete list of all standard HTTP status codes defined in this package. /// /// This includes both official IANA registered codes and commonly used - /// unofficial codes. The list contains 95 status codes covering informational + /// unofficial codes. The list contains 93 status codes covering informational /// (`1xx`), success (`2xx`), redirection (`3xx`), client error (`4xx`), /// and server error (`5xx`) categories. /// @@ -1254,6 +1254,6 @@ extension type const StatusCode._(int _code) implements int { final elementAt = (random ?? Random()).nextInt(from.length); // ignore: avoid-unsafe-collection-methods, length is guaranteed to be > 0. - return List.unmodifiable(from).elementAt(elementAt); + return from.elementAt(elementAt); } } diff --git a/pubspec.yaml b/pubspec.yaml index ee505e0..fe0966c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,4 +1,4 @@ -version: 3.1.0 +version: 3.2.0 name: functional_status_codes description: Zero-cost HTTP status codes as int extension type with num? functional helpers maintainer: Roman Cinis From aee7bc0508b4c0de1f07819b218b6c7d9e425a93 Mon Sep 17 00:00:00 2001 From: Roman Cinis <52065414+tsinis@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:53:55 +0100 Subject: [PATCH 2/8] feat(llm): add api reference, functional patterns guide, and prompts Add comprehensive documentation for API reference, functional patterns, and prompts related to handling HTTP responses and caching strategies. --- extension/mcp/config.yaml | 35 ++++ extension/mcp/prompts/cache_strategy.md | 51 +++++ extension/mcp/prompts/handle_response.md | 45 +++++ extension/mcp/resources/api_reference.md | 239 +++++++++++++++++++++++ extension/mcp/resources/patterns.md | 218 +++++++++++++++++++++ 5 files changed, 588 insertions(+) create mode 100644 extension/mcp/config.yaml create mode 100644 extension/mcp/prompts/cache_strategy.md create mode 100644 extension/mcp/prompts/handle_response.md create mode 100644 extension/mcp/resources/api_reference.md create mode 100644 extension/mcp/resources/patterns.md diff --git a/extension/mcp/config.yaml b/extension/mcp/config.yaml new file mode 100644 index 0000000..16cb00f --- /dev/null +++ b/extension/mcp/config.yaml @@ -0,0 +1,35 @@ +resources: + - title: "API Reference" + description: "Concise cheat-sheet for all StatusCode constants, getters, methods, + and functional helpers. Include in agent context when working with HTTP status codes." + path: extension/mcp/resources/api_reference.md + + - title: "Functional Patterns Guide" + description: "Idiomatic patterns for when/map/maybeWhen/const variants, + isCacheable, isRetryable, and real-world HTTP client integration." + path: extension/mcp/resources/patterns.md + +prompts: + - title: "Handle HTTP Response" + description: "Generate idiomatic functional_status_codes code to handle an HTTP + response. Provide http_client (http, dio, or native) and optionally status_code." + path: extension/mcp/prompts/handle_response.md + arguments: + - name: http_client + description: "HTTP client in use: 'http', 'dio', or 'native' (dart:io)" + required: false + - name: status_code + description: "Specific status code to handle (e.g. 404). Leave blank for general handling." + required: false + + - title: "Cache Strategy" + description: "Derive a caching strategy for an HTTP response using isCacheable + and RFC 7231 semantics." + path: extension/mcp/prompts/cache_strategy.md + arguments: + - name: status_code + description: "HTTP status code to evaluate (e.g. 200)" + required: true + - name: cache_backend + description: "Cache storage backend being used, if any" + required: false diff --git a/extension/mcp/prompts/cache_strategy.md b/extension/mcp/prompts/cache_strategy.md new file mode 100644 index 0000000..50e5576 --- /dev/null +++ b/extension/mcp/prompts/cache_strategy.md @@ -0,0 +1,51 @@ +Derive a caching strategy for HTTP status code **{{status_code}}** using the `functional_status_codes` package and RFC 7231 semantics. + +{{#cache_backend}} +The cache backend in use is: **{{cache_backend}}** +{{/cache_backend}} +{{^cache_backend}} +Use a generic `cache.store(key, value, {Duration? ttl})` / `cache.get(key)` interface in the example. The caller can adapt it to their backend. +{{/cache_backend}} + +Follow these steps: + +1. **Evaluate cacheability** using `.isCacheable`: + + The RFC 7231 cacheable set is: `200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501`. + + ```dart + if ({{status_code}}.isCacheable) { + // safe to cache the response body + } + ``` + +2. **Assign a cache TTL** appropriate for code `{{status_code}}`: + - `200 OK` — moderate TTL (e.g. 5 minutes), respect `Cache-Control` headers when present + - `301 Moved Permanently` — long TTL (e.g. 1 day), permanent redirect is stable + - `404 Not Found` — short negative-cache TTL (e.g. 1-10 minutes) to avoid repeated misses + - `405 Method Not Allowed`, `410 Gone` — moderate TTL, these are stable server decisions + - `501 Not Implemented` — long TTL, unlikely to change soon + - `204`, `203`, `206`, `300`, `414` — short to moderate TTL depending on use case + + Use `whenConstOrNull` to express this as a compile-time mapping: + + ```dart + final ttl = statusCode.whenConstOrNull( + okHttp200: Duration(minutes: 5), + noContentHttp204: Duration(minutes: 1), + notFoundHttp404: Duration(minutes: 10), + movedPermanentlyHttp301: Duration(days: 1), + goneHttp410: Duration(hours: 1), + notImplementedHttp501: Duration(hours: 6), + ); + ``` + +3. **Generate the store/skip logic**: + - If `isCacheable` is `true` for `{{status_code}}`: generate a `cache.store(...)` call{{#cache_backend}} adapted to **{{cache_backend}}**{{/cache_backend}} + - If `isCacheable` is `false`: explain why this code should not be cached and suggest adding a `Cache-Control: no-store` header to the response if you control the server + +4. **Emit a complete, reusable function** that: + - Checks `isStatusCode` before doing anything (guards against invalid codes) + - Uses `isCacheable` to gate the write + - Uses `whenConstOrNull` or `whenConst` for TTL assignment (no runtime allocations) + - Falls back gracefully for non-cacheable codes (log and skip, do not throw) diff --git a/extension/mcp/prompts/handle_response.md b/extension/mcp/prompts/handle_response.md new file mode 100644 index 0000000..7dc862e --- /dev/null +++ b/extension/mcp/prompts/handle_response.md @@ -0,0 +1,45 @@ +Generate idiomatic Dart code to handle an HTTP response using the `functional_status_codes` package. + +{{#http_client}} +The HTTP client in use is: **{{http_client}}** +{{/http_client}} +{{^http_client}} +Detect the HTTP client from the existing code (look for `http.Response`, `dio.Response`, `HttpClientResponse`, or a plain `int` status code field). +{{/http_client}} + +{{#status_code}} +Focus specifically on handling status code **{{status_code}}** (in addition to general handling). +{{/status_code}} + +Follow these steps: + +1. **Extract the status code** as `int` or `num` from the response object: + - `package:http` → `response.statusCode` (already `int`) + - `package:dio` → `response.statusCode` (already `int?`) + - `dart:io HttpClientResponse` → `response.statusCode` (already `int`) + - Custom → use whatever field holds the integer status + +2. **Apply category-level branching** using `maybeWhenStatusCode` or `maybeMapStatusCode`: + - Use `maybeWhenStatusCode` when the callbacks do not need the code value passed in + - Use `maybeMapStatusCode` when you need `(code) =>` in the callback + - Provide `orElse` for codes outside 100-599 or unhandled categories + - Do **not** provide `isStatusCode` alongside category handlers unless you intentionally want it to catch all valid codes first + +3. **Drill into specific codes** when needed by converting first: + + ```dart + final registered = rawCode.toRegisteredStatusCode(); + final result = registered?.maybeMap( + orElse: (c) => defaultHandling(c), + notFoundHttp404: (_) => handleNotFound(), + ); + ``` + +4. **Emit null-safe, idiomatic Dart**: + - Prefer `?.` and `??` over null-checks + - Use `const` callbacks with `whenConstStatusCode` / `whenConstOrNull` when the return values are constants + - Avoid `if (code >= 200 && code < 300)` — use `.isSuccess` / `.isError` / `.isCacheable` etc. + +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 `Http`. + +Produce a complete, runnable function or method — not just a snippet — with appropriate return type and error handling. diff --git a/extension/mcp/resources/api_reference.md b/extension/mcp/resources/api_reference.md new file mode 100644 index 0000000..2688642 --- /dev/null +++ b/extension/mcp/resources/api_reference.md @@ -0,0 +1,239 @@ +# functional_status_codes — API Reference + +## Import + +```dart +import 'package:functional_status_codes/functional_status_codes.dart'; +``` + +## StatusCode Type + +`StatusCode` is an **extension type** over `int` — zero-cost, no boxing at runtime. + +```dart +// implements int, so works anywhere an int is expected +extension type const StatusCode(int _) implements int + +// Custom code (for internal/non-standard use) +const custom = StatusCode.custom(456); + +// Predefined constants — naming: Http +StatusCode.okHttp200 +StatusCode.notFoundHttp404 +StatusCode.internalServerErrorHttp500 +StatusCode.unauthorizedHttp401 +StatusCode.createdHttp201 +StatusCode.noContentHttp204 +StatusCode.badRequestHttp400 +StatusCode.forbiddenHttp403 +StatusCode.tooManyRequestsHttp429 +StatusCode.serviceUnavailableHttp503 +// ...93 total, covering 1xx–5xx including common unofficial codes +``` + +## Static Collections + +```dart +StatusCode.values // All 93 known codes (List) +StatusCode.officialCodes // IANA-registered subset +StatusCode.informationalCodes // 1xx +StatusCode.successCodes // 2xx +StatusCode.redirectionCodes // 3xx +StatusCode.clientErrorCodes // 4xx +StatusCode.serverErrorCodes // 5xx +StatusCode.cacheableCodes // RFC 7231: 200,203,204,206,300,301,404,405,410,414,501 +StatusCode.retryableCodes // Transient: 408,425,429,500,502,503,504,511,598,599 +``` + +## Static Methods + +```dart +StatusCode.random() // random from all values +StatusCode.random(from: StatusCode.serverErrorCodes) // random from subset +StatusCode.tryParse('Response: 404 Not Found') // → StatusCode? (first match) +StatusCode.fromCode(200) // → StatusCode (throws if unknown) +StatusCode.maybeFromCode(999) // → null (safe, no throw) +StatusCode.maybeFromCode(null) // → null +``` + +## num? Extension — NumStatusCodeExtension + +Applies to any `num?`, including raw `int` from HTTP clients. + +### Range and Category Checks + +```dart +200.isStatusCode // true — within 100-599 +999.isStatusCode // false +200.isInformational // false +200.isSuccess // true +301.isRedirection // true +404.isClientError // true +500.isServerError // true +404.isError // true — isClientError || isServerError +200.isError // false +``` + +### Semantic Checks + +```dart +200.isCacheable // true (RFC 7231) +201.isCacheable // false +503.isRetryable // true +200.isRetryable // false +200.isOneOf(StatusCode.cacheableCodes) // true +200.isOneOf(const [StatusCode.okHttp200, StatusCode.custom(222)]) // true +``` + +### Range Helpers + +```dart +// Custom numeric range +200.isStatusCodeWithinRange(min: 200, max: 299) // true + +// StatusCode-bounded range +200.isStatusWithinRange( + min: StatusCode.okHttp200, + max: StatusCode.imUsedHttp226, +) // true +``` + +### Conversion + +```dart +200.toRegisteredStatusCode() // → StatusCode.okHttp200 +299.toRegisteredStatusCode() // → null (not a registered code) +null.toRegisteredStatusCode() // → null +``` + +### Functional Methods on num? + +```dart +// Exhaustive category dispatch — throws FormatException if out of range +response.statusCode.mapStatusCode( + isInformational: (code) => ..., + isSuccess: (code) => ..., + isRedirection: (code) => ..., + isClientError: (code) => ..., + isServerError: (code) => ..., +) + +// Optional category dispatch — fallback to orElse +response.statusCode.maybeMapStatusCode( + orElse: (code) => handleUnknown(code), + isSuccess: (code) => handleSuccess(code), + isClientError: (code) => handleClientError(code), + // unspecified categories fall to orElse +) +// NOTE: isStatusCode handler (if provided) fires before any category handler. + +// Zero-arg variants (no value passed to callbacks) +response.statusCode.maybeWhenStatusCode( + orElse: () => false, + isSuccess: () => true, +) + +// Const-value variant (no closures — direct values) +response.statusCode.whenConstStatusCode( + isInformational: 'info', + isSuccess: 'ok', + isRedirection: 'redirect', + isClientError: 'client error', + isServerError: 'server error', + orElse: 'unknown', +) + +// Returns null instead of requiring orElse +response.statusCode.whenConstStatusCodeOrNull( + isSuccess: 'ok', + isClientError: 'client error', +) + +// Convert then map in one step +response.statusCode.mapToRegisteredStatusCode( + mapper: (statusCode) => statusCode.reason, + orElse: (raw) => 'Unknown: $raw', +) +``` + +## StatusCode Extension — StatusCodeExtension + +Applies to `StatusCode` values after conversion. + +### Properties + +```dart +StatusCode.okHttp200.reason // "OK" +StatusCode.notFoundHttp404.reason // "Not Found" + +StatusCode.okHttp200.isOfficial // true — IANA registered +StatusCode.custom(456).isOfficial // false +StatusCode.okHttp200.isCustom // false +StatusCode.custom(456).isCustom // true + +StatusCode.okHttp200.toStringShallow() +// → 'StatusCode(200, reason: "OK", isOfficial: true)' +``` + +### Individual Code Checks + +One `bool` getter per registered code: + +```dart +code.isOkHttp200 +code.isNotFoundHttp404 +code.isInternalServerErrorHttp500 +code.isUnauthorizedHttp401 +// ...one per registered constant +``` + +### Functional Methods on StatusCode + +```dart +// Exhaustive — must handle all 93 codes + wildcard fires for custom codes +statusCode.when( + okHttp200: () => 'success', + notFoundHttp404: () => 'missing', + // ...all codes required +) + +// Optional — return null for unhandled codes +statusCode.whenOrNull( + okHttp200: () => 'success', + notFoundHttp404: () => 'missing', +) + +// Optional with fallback +statusCode.maybeWhen( + orElse: () => 'other', + okHttp200: () => 'success', +) + +// map variants pass the StatusCode value to the callback +statusCode.maybeMap( + orElse: (code) => 'other: ${code.reason}', + okHttp200: (code) => 'success', +) + +// Const-value variant (no closures) +statusCode.whenConst( + okHttp200: 'success', + notFoundHttp404: 'missing', + // ...all codes required +) + +// Const + nullable +statusCode.whenConstOrNull( + okHttp200: 'success', + notFoundHttp404: 'missing', + // unspecified → null +) +``` + +## Dart 3.10 Dot-Shorthand + +```dart +// Enum-like dot syntax on static constants +200.isOneOf(const [.okHttp200, .custom(222)]) // true +response.statusCode.toRegisteredStatusCode() == .notFoundHttp404 +``` diff --git a/extension/mcp/resources/patterns.md b/extension/mcp/resources/patterns.md new file mode 100644 index 0000000..9450be5 --- /dev/null +++ b/extension/mcp/resources/patterns.md @@ -0,0 +1,218 @@ +# functional_status_codes — Functional Patterns Guide + +## 1. Handling Any num Response Code + +Use `maybeWhenStatusCode` or `maybeMapStatusCode` directly on the raw `int` from any HTTP client — no conversion required. + +```dart +import 'package:functional_status_codes/functional_status_codes.dart'; + +// Works with package:http, Dio, dart:io, or any int status code +Future handleResponse(int statusCode, T Function() parseBody) { + return statusCode.maybeMapStatusCode( + orElse: (code) { + throw StateError('Unexpected status $code'); + }, + isSuccess: (_) => parseBody(), + isClientError: (code) { + if (code == StatusCode.notFoundHttp404) return null; + throw HttpException('Client error: $code'); + }, + isServerError: (_) => null, // caller can retry + ); +} +``` + +**Note:** If you provide `isStatusCode` alongside a category handler, `isStatusCode` always fires first for any valid code (100–599). Omit it when you want category-specific matching. + +--- + +## 2. Specific Code Matching + +Convert to a `StatusCode` first, then use `maybeMap` or `maybeWhen` to match individual codes. + +```dart +StatusCode? code = rawStatusCode.toRegisteredStatusCode(); + +final message = code?.maybeMap( + orElse: (c) => 'HTTP ${c.reason}', + notFoundHttp404: (_) => 'Resource not found — check the URL', + unauthorizedHttp401: (_) => 'Please sign in', + forbiddenHttp403: (_) => 'You do not have access', + tooManyRequestsHttp429: (_) => 'Rate limited — slow down', + serviceUnavailableHttp503: (_) => 'Service down — try later', +) ?? 'Unknown status $rawStatusCode'; +``` + +Use `whenOrNull` when you only care about a few codes and want `null` for the rest: + +```dart +final retryDelay = code?.whenOrNull( + tooManyRequestsHttp429: () => Duration(seconds: 60), + serviceUnavailableHttp503: () => Duration(seconds: 5), + gatewayTimeoutHttp504: () => Duration(seconds: 2), +); +// null → do not retry +``` + +--- + +## 3. Const Mapping (Zero Allocations) + +Use `whenConst` / `whenConstOrNull` when mapping to constant values. No closures are created. + +```dart +// Map every status code to a log level at compile time +const level = statusCode.whenConst( + okHttp200: 'info', + createdHttp201: 'info', + noContentHttp204: 'info', + notFoundHttp404: 'warn', + badRequestHttp400: 'warn', + unauthorizedHttp401: 'warn', + internalServerErrorHttp500: 'error', + serviceUnavailableHttp503: 'error', + // ...all 93 codes required +); + +// Or just the codes you care about — null for everything else +final icon = statusCode.whenConstOrNull( + okHttp200: '✓', + notFoundHttp404: '✗', + internalServerErrorHttp500: '!', +); +``` + +Category-level const mapping via `whenConstStatusCode`: + +```dart +final label = rawCode.whenConstStatusCode( + isInformational: 'informational', + isSuccess: 'success', + isRedirection: 'redirect', + isClientError: 'client error', + isServerError: 'server error', + orElse: 'unknown', +); +``` + +--- + +## 4. Caching Logic (RFC 7231) + +`isCacheable` returns `true` for the RFC 7231 cacheable set: +`200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501`. + +```dart +Future fetchAndCache(String key, Future Function() fetch) async { + final response = await fetch(); + final code = response.statusCode; + + if (!code.isStatusCode) { + throw FormatException('Invalid status code: $code'); + } + + if (code.isCacheable) { + await cache.store(key, response.body); + } +} +``` + +Pair with `whenConst` to assign cache TTLs per code: + +```dart +final ttl = statusCode.whenConstOrNull( + okHttp200: Duration(minutes: 5), + noContentHttp204: Duration(minutes: 1), + notFoundHttp404: Duration(minutes: 10), // negative-cache 404s + movedPermanentlyHttp301: Duration(days: 1), +); + +if (ttl != null) await cache.store(key, body, ttl: ttl); +``` + +--- + +## 5. Retry Logic + +`isRetryable` returns `true` for transient errors: +`408, 425, 429, 500, 502, 503, 504, 511, 598, 599`. + +```dart +Future fetchWithRetry( + Uri uri, { + int maxAttempts = 3, + Duration baseDelay = const Duration(seconds: 1), +}) async { + for (var attempt = 1; attempt <= maxAttempts; attempt++) { + final response = await http.get(uri); + + if (!response.statusCode.isRetryable) return response; + + if (attempt == maxAttempts) return response; + + // Exponential backoff + await Future.delayed(baseDelay * (1 << (attempt - 1))); + } + throw StateError('unreachable'); +} +``` + +Combine `isCacheable` and `isRetryable` with category checks: + +```dart +response.statusCode.maybeWhenStatusCode( + orElse: () => Response.error(), + isSuccess: () => processResponse(response), + isClientError: () { + if (response.statusCode.isRetryable) scheduleRetry(); + return Response.clientError(response.statusCode); + }, + isServerError: () { + if (response.statusCode.isRetryable) scheduleRetry(); + return Response.serverError(response.statusCode); + }, +); +``` + +--- + +## 6. Testing with Random Codes + +`StatusCode.random()` generates random codes from any collection — ideal for property-based tests. + +```dart +import 'package:test/test.dart'; +import 'package:functional_status_codes/functional_status_codes.dart'; + +test('handler returns null for any client error', () { + for (var i = 0; i < 50; i++) { + final code = StatusCode.random(from: StatusCode.clientErrorCodes); + expect(handleResponse(code), isNull); + } +}); + +test('cache is written for any cacheable code', () { + for (var i = 0; i < 20; i++) { + final code = StatusCode.random(from: StatusCode.cacheableCodes); + expect(code.isCacheable, isTrue); + fetchAndCache('key', () async => Response(statusCode: code)); + expect(cache.has('key'), isTrue); + } +}); + +test('retry is attempted for retryable server errors', () { + for (var i = 0; i < 30; i++) { + final code = StatusCode.random(from: StatusCode.retryableCodes); + expect(shouldRetry(code), isTrue); + } +}); + +// Combine with custom codes for edge-case testing +test('unknown code is handled gracefully', () { + final unknown = StatusCode.custom(999); + expect(unknown.isCustom, isTrue); + expect(unknown.toRegisteredStatusCode(), isNull); + expect(handleResponse(unknown), isNull); +}); +``` From 75dff9bc0ef5188c94e163a4088a91da00c96ffd Mon Sep 17 00:00:00 2001 From: Roman Cinis <52065414+tsinis@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:57:01 +0100 Subject: [PATCH 3/8] fix(spell): correct regex for status code pattern matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated `StatusCode.pattern` and `StatusCode.regExp` to match only valid HTTP status codes (100–599) instead of any three-digit number. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23afe67..8fa32a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ Once the Dart MCP server implements the proposal, agents will automatically have - Fixed: `maybeMapStatusCode` now performs the `null` check before calling `toRegisteredStatusCode()`, which was previously invoked unnecessarily for `null` inputs. - Fixed: `StatusCode.random()` no longer wraps the source iterable in an intermediate `List.unmodifiable(...)` just to call `elementAt`. The allocation was unnecessary since `Iterable.elementAt` works directly. -- Fixed: `StatusCode.pattern` (and `StatusCode.regExp`) now matches only the valid HTTP status code range (`[1-5]\d{2}`, i.e. 100–599) instead of any three-digit number (`\d{3}`). This avoids false matches on strings like `"timeout after 999ms"`. Note: `tryParse` behaviour is unchanged for all registered codes — only the raw pattern/regex changes. +- Fixed: `StatusCode.pattern` (and `StatusCode.regExp`) now matches only the valid HTTP status code range (`[1-5]\d{2}`, i.e. 100-599) instead of any three-digit number (`\d{3}`). This avoids false matches on strings like `"timeout after 999ms"`. Note: `tryParse` behavior is unchanged for all registered codes — only the raw pattern/regex changes. - Fixed: `values` doc comment count corrected from 95 to 93. ### Documentation From 11201029300d9e98185be9c6b3e0ce13efe38078 Mon Sep 17 00:00:00 2001 From: Roman Cinis <52065414+tsinis@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:16:23 +0100 Subject: [PATCH 4/8] feat: enhance api reference and patterns documentation - Updated API reference with clearer examples for StatusCode usage. - Improved patterns guide to reflect changes in response handling. - Added tests for error handling in NumStatusCodeExtension. --- extension/mcp/resources/api_reference.md | 37 +++++++++----------- extension/mcp/resources/patterns.md | 6 ++-- lib/src/num_status_code_extension.dart | 4 +-- lib/src/status_code.dart | 23 ++++++------ test/src/num_status_code_extension_test.dart | 31 ++++++++++++++++ 5 files changed, 65 insertions(+), 36 deletions(-) diff --git a/extension/mcp/resources/api_reference.md b/extension/mcp/resources/api_reference.md index 2688642..6c102ed 100644 --- a/extension/mcp/resources/api_reference.md +++ b/extension/mcp/resources/api_reference.md @@ -11,24 +11,21 @@ import 'package:functional_status_codes/functional_status_codes.dart'; `StatusCode` is an **extension type** over `int` — zero-cost, no boxing at runtime. ```dart -// implements int, so works anywhere an int is expected -extension type const StatusCode(int _) implements int +// Use predefined constants — naming: Http +const ok = StatusCode.okHttp200; // == 200 +const notFound = StatusCode.notFoundHttp404; // == 404 +const ise = StatusCode.internalServerErrorHttp500; -// Custom code (for internal/non-standard use) +// Custom code for non-standard/internal use const custom = StatusCode.custom(456); -// Predefined constants — naming: Http -StatusCode.okHttp200 -StatusCode.notFoundHttp404 -StatusCode.internalServerErrorHttp500 -StatusCode.unauthorizedHttp401 -StatusCode.createdHttp201 -StatusCode.noContentHttp204 -StatusCode.badRequestHttp400 -StatusCode.forbiddenHttp403 -StatusCode.tooManyRequestsHttp429 -StatusCode.serviceUnavailableHttp503 -// ...93 total, covering 1xx–5xx including common unofficial codes +// Interchangeable with int — no casting needed +void send(int code) {} +send(StatusCode.okHttp200); // valid + +// Convert from int +final code = StatusCode.maybeFromCode(response.statusCode); // StatusCode? +// ...93 predefined codes covering 1xx-5xx including common unofficial codes ``` ## Static Collections @@ -48,12 +45,12 @@ StatusCode.retryableCodes // Transient: 408,425,429,500,502,503,504,511,598 ## Static Methods ```dart -StatusCode.random() // random from all values +StatusCode.random() // random from all values StatusCode.random(from: StatusCode.serverErrorCodes) // random from subset -StatusCode.tryParse('Response: 404 Not Found') // → StatusCode? (first match) -StatusCode.fromCode(200) // → StatusCode (throws if unknown) -StatusCode.maybeFromCode(999) // → null (safe, no throw) -StatusCode.maybeFromCode(null) // → null +StatusCode.tryParse('Response: 404 Not Found') // → StatusCode? (first match) +StatusCode.maybeFromCode(200) // → StatusCode.okHttp200 +StatusCode.maybeFromCode(999) // → null (unregistered) +StatusCode.maybeFromCode(null) // → null ``` ## num? Extension — NumStatusCodeExtension diff --git a/extension/mcp/resources/patterns.md b/extension/mcp/resources/patterns.md index 9450be5..1771a0c 100644 --- a/extension/mcp/resources/patterns.md +++ b/extension/mcp/resources/patterns.md @@ -8,7 +8,7 @@ Use `maybeWhenStatusCode` or `maybeMapStatusCode` directly on the raw `int` from import 'package:functional_status_codes/functional_status_codes.dart'; // Works with package:http, Dio, dart:io, or any int status code -Future handleResponse(int statusCode, T Function() parseBody) { +T? handleResponse(int statusCode, T Function() parseBody) { return statusCode.maybeMapStatusCode( orElse: (code) { throw StateError('Unexpected status $code'); @@ -16,14 +16,14 @@ Future handleResponse(int statusCode, T Function() parseBody) { isSuccess: (_) => parseBody(), isClientError: (code) { if (code == StatusCode.notFoundHttp404) return null; - throw HttpException('Client error: $code'); + throw ArgumentError('Client error: $code'); }, isServerError: (_) => null, // caller can retry ); } ``` -**Note:** If you provide `isStatusCode` alongside a category handler, `isStatusCode` always fires first for any valid code (100–599). Omit it when you want category-specific matching. +**Note:** If you provide `isStatusCode` alongside a category handler, `isStatusCode` always fires first for any valid code (100-599). Omit it when you want category-specific matching. --- diff --git a/lib/src/num_status_code_extension.dart b/lib/src/num_status_code_extension.dart index bf10ac2..2f015e2 100644 --- a/lib/src/num_status_code_extension.dart +++ b/lib/src/num_status_code_extension.dart @@ -289,7 +289,7 @@ extension NumStatusCodeExtension on T? { /// returns the result of calling [orElse]. /// /// **Handler priority:** [isStatusCode] is checked first and matches any - /// valid HTTP status code (100–599). If provided alongside a category handler + /// valid HTTP status code (100-599). If provided alongside a category handler /// (e.g. [isSuccess]), [isStatusCode] will always fire instead. Only omit /// [isStatusCode] when you want category-specific matching. /// @@ -341,7 +341,7 @@ extension NumStatusCodeExtension on T? { /// HTTP status code, returns the result of calling [orElse]. /// /// **Handler priority:** [isStatusCode] is checked first and matches any - /// valid HTTP status code (100–599). If provided alongside a category handler + /// valid HTTP status code (100-599). If provided alongside a category handler /// (e.g. [isSuccess]), [isStatusCode] will always fire instead. Only omit /// [isStatusCode] when you want category-specific matching. /// diff --git a/lib/src/status_code.dart b/lib/src/status_code.dart index b8d2b40..6eacf1f 100644 --- a/lib/src/status_code.dart +++ b/lib/src/status_code.dart @@ -688,8 +688,8 @@ extension type const StatusCode._(int _code) implements int { /// This pattern is commonly used to identify HTTP status codes within a /// string. HTTP status codes are typically 3-digit integers ranging from 100 /// to 599. The pattern is defined by the regular expression `[1-5]\d{2}`, - /// which matches digits starting with 1–5 followed by exactly two more - /// digits, covering the full 100–599 range. + /// which matches digits starting with 1-5 followed by exactly two more + /// digits, covering the full 100-599 range. /// /// Examples of matching strings: /// - '200' for OK @@ -705,27 +705,28 @@ extension type const StatusCode._(int _code) implements int { /// - [StatusCode], which contains standard HTTP status codes. static const pattern = r'[1-5]\d{2}'; - /// A getter that returns a [RegExp] object configured with a pattern to match - /// three consecutive digits, typically representing an HTTP status code. + /// A getter that returns a [RegExp] object configured with [pattern] to match + /// HTTP status codes in the range 100–599. /// /// The matching is unanchored, meaning that this regular expression can find /// matches anywhere in the input string. This allows for the extraction of /// status codes from within larger bodies of text. /// - /// The [pattern] is defined by the regular expression `\d{3}`, which matches - /// any sequence of exactly three digits. + /// The [pattern] is defined by the regular expression `[1-5]\d{2}`, which + /// matches a digit 1–5 followed by exactly two more digits, covering the + /// full valid HTTP status code range (100–599). /// /// Example usage: /// ```dart /// // Assuming `inputString` contains an HTTP status code. /// String? statusCode = regExp.firstMatch(inputString)?.group(0); - /// // `statusCode` will contain the first sequence of three digits found in `inputString`. + /// // `statusCode` will contain the first 100–599 match found in `inputString`. /// ``` /// - /// Note: While the regular expression matches any three-digit number, it does - /// not ascertain that the number is a valid HTTP status code. For such - /// validation, the matched number should be further checked against known - /// HTTP status code ranges via the tools that this package provides. + /// Note: The regular expression matches any number in the 100–599 range, but + /// does not verify that the number is a registered HTTP status code. For such + /// validation, the matched number should be further checked using + /// [maybeFromCode] or the `num` extension helpers this package provides. /// /// See also: /// - [pattern], the raw regular expression string this getter utilizes. diff --git a/test/src/num_status_code_extension_test.dart b/test/src/num_status_code_extension_test.dart index ef8034c..95c6010 100644 --- a/test/src/num_status_code_extension_test.dart +++ b/test/src/num_status_code_extension_test.dart @@ -231,6 +231,37 @@ void main() => group('NumStatusCodeExtension', () { } }); + group('isError', () { + const combinedErrorsTrueCases = { + StatusCode.badRequestHttp400, + StatusCode.badRequestHttp400 + 1, + StatusCode.nginxClientClosedRequestHttp499, + StatusCode.internalServerErrorHttp500, + StatusCode.internalServerErrorHttp500 + 1, + StatusCode.networkConnectTimeoutErrorHttp599, + }; + + const isErrorFalseCases = { + StatusCode.okHttp200, + StatusCode.multipleChoicesHttp300, + StatusCode.networkConnectTimeoutErrorHttp599 + 1, + }; + + for (final number in [...globalWrongCases, ...isErrorFalseCases]) { + test( + 'should return false for $number', + () => expect(number.isError, isFalse), + ); + } + + for (final number in combinedErrorsTrueCases) { + test( + 'should return true for $number', + () => expect(number.isError, isTrue), + ); + } + }); + group('toRegisteredStatusCode', () { for (final number in globalWrongCases) { test( From 2ca9fe91c1db9f0756c0d87692c25d35f4ca2818 Mon Sep 17 00:00:00 2001 From: Roman Cinis <52065414+tsinis@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:43:31 +0100 Subject: [PATCH 5/8] 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 --- extension/mcp/prompts/cache_strategy.md | 6 +-- extension/mcp/prompts/handle_response.md | 2 +- extension/mcp/resources/api_reference.md | 15 ++++--- extension/mcp/resources/patterns.md | 12 ++--- lib/src/status_code.dart | 24 +++++----- test/src/status_code_test.dart | 57 +++++++++++++++++++++++- 6 files changed, 87 insertions(+), 29 deletions(-) diff --git a/extension/mcp/prompts/cache_strategy.md b/extension/mcp/prompts/cache_strategy.md index 50e5576..609a0dd 100644 --- a/extension/mcp/prompts/cache_strategy.md +++ b/extension/mcp/prompts/cache_strategy.md @@ -27,10 +27,10 @@ Follow these steps: - `501 Not Implemented` — long TTL, unlikely to change soon - `204`, `203`, `206`, `300`, `414` — short to moderate TTL depending on use case - Use `whenConstOrNull` to express this as a compile-time mapping: + Use `whenConstOrNull` to express this as a compile-time mapping (`whenConstOrNull` is on `StatusCode`, so convert from raw `int` first): ```dart - final ttl = statusCode.whenConstOrNull( + final ttl = rawStatusCode.toRegisteredStatusCode()?.whenConstOrNull( okHttp200: Duration(minutes: 5), noContentHttp204: Duration(minutes: 1), notFoundHttp404: Duration(minutes: 10), @@ -47,5 +47,5 @@ Follow these steps: 4. **Emit a complete, reusable function** that: - Checks `isStatusCode` before doing anything (guards against invalid codes) - Uses `isCacheable` to gate the write - - Uses `whenConstOrNull` or `whenConst` for TTL assignment (no runtime allocations) + - Uses `toRegisteredStatusCode()?.whenConstOrNull(...)` for TTL assignment — convert first since these methods are on `StatusCode`, not raw `int` (no runtime allocations) - Falls back gracefully for non-cacheable codes (log and skip, do not throw) diff --git a/extension/mcp/prompts/handle_response.md b/extension/mcp/prompts/handle_response.md index 7dc862e..b734c6e 100644 --- a/extension/mcp/prompts/handle_response.md +++ b/extension/mcp/prompts/handle_response.md @@ -37,7 +37,7 @@ Follow these steps: 4. **Emit null-safe, idiomatic Dart**: - Prefer `?.` and `??` over null-checks - - Use `const` callbacks with `whenConstStatusCode` / `whenConstOrNull` when the return values are constants + - 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 - Avoid `if (code >= 200 && code < 300)` — use `.isSuccess` / `.isError` / `.isCacheable` etc. 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 `Http`. diff --git a/extension/mcp/resources/api_reference.md b/extension/mcp/resources/api_reference.md index 6c102ed..890c774 100644 --- a/extension/mcp/resources/api_reference.md +++ b/extension/mcp/resources/api_reference.md @@ -130,26 +130,29 @@ response.statusCode.maybeWhenStatusCode( isSuccess: () => true, ) -// Const-value variant (no closures — direct values) +// Const-value variant — all 5 categories required, no orElse, asserts in-range response.statusCode.whenConstStatusCode( isInformational: 'info', isSuccess: 'ok', isRedirection: 'redirect', isClientError: 'client error', isServerError: 'server error', - orElse: 'unknown', ) -// Returns null instead of requiring orElse +// Nullable variant — all params optional, orElse is also optional response.statusCode.whenConstStatusCodeOrNull( isSuccess: 'ok', isClientError: 'client error', + orElse: 'unknown', ) -// Convert then map in one step +// Convert then map per category in one step response.statusCode.mapToRegisteredStatusCode( - mapper: (statusCode) => statusCode.reason, - orElse: (raw) => 'Unknown: $raw', + isInformational: (code) => code?.reason ?? 'info', + isSuccess: (code) => code?.reason ?? 'success', + isRedirection: (code) => code?.reason ?? 'redirect', + isClientError: (code) => code?.reason ?? 'client error', + isServerError: (code) => code?.reason ?? 'server error', ) ``` diff --git a/extension/mcp/resources/patterns.md b/extension/mcp/resources/patterns.md index 1771a0c..95a72f4 100644 --- a/extension/mcp/resources/patterns.md +++ b/extension/mcp/resources/patterns.md @@ -83,10 +83,10 @@ final icon = statusCode.whenConstOrNull( ); ``` -Category-level const mapping via `whenConstStatusCode`: +Category-level const mapping via `whenConstStatusCodeOrNull`: ```dart -final label = rawCode.whenConstStatusCode( +final label = rawCode.whenConstStatusCodeOrNull( isInformational: 'informational', isSuccess: 'success', isRedirection: 'redirect', @@ -188,15 +188,15 @@ import 'package:functional_status_codes/functional_status_codes.dart'; test('handler returns null for any client error', () { for (var i = 0; i < 50; i++) { final code = StatusCode.random(from: StatusCode.clientErrorCodes); - expect(handleResponse(code), isNull); + expect(handleResponse(code, () => 'body'), isNull); } }); -test('cache is written for any cacheable code', () { +test('cache is written for any cacheable code', () async { for (var i = 0; i < 20; i++) { final code = StatusCode.random(from: StatusCode.cacheableCodes); expect(code.isCacheable, isTrue); - fetchAndCache('key', () async => Response(statusCode: code)); + await fetchAndCache('key', () async => Response(statusCode: code)); expect(cache.has('key'), isTrue); } }); @@ -213,6 +213,6 @@ test('unknown code is handled gracefully', () { final unknown = StatusCode.custom(999); expect(unknown.isCustom, isTrue); expect(unknown.toRegisteredStatusCode(), isNull); - expect(handleResponse(unknown), isNull); + expect(handleResponse(unknown, () => 'body'), isNull); }); ``` diff --git a/lib/src/status_code.dart b/lib/src/status_code.dart index 6eacf1f..b2a7ec9 100644 --- a/lib/src/status_code.dart +++ b/lib/src/status_code.dart @@ -687,9 +687,10 @@ extension type const StatusCode._(int _code) implements int { /// /// This pattern is commonly used to identify HTTP status codes within a /// string. HTTP status codes are typically 3-digit integers ranging from 100 - /// to 599. The pattern is defined by the regular expression `[1-5]\d{2}`, - /// which matches digits starting with 1-5 followed by exactly two more - /// digits, covering the full 100-599 range. + /// to 599. The pattern is defined by the regular expression + /// `(? from = values, Random? random, }) { - assert(from.isNotEmpty, 'The provided `from` iterable must not be empty'); - final elementAt = (random ?? Random()).nextInt(from.length); + final list = from is List ? from : from.toList(); + assert(list.isNotEmpty, 'The provided `from` iterable must not be empty'); + final elementAt = (random ?? Random()).nextInt(list.length); // ignore: avoid-unsafe-collection-methods, length is guaranteed to be > 0. - return from.elementAt(elementAt); + return list[elementAt]; } } diff --git a/test/src/status_code_test.dart b/test/src/status_code_test.dart index 9c07489..9c6f5e4 100644 --- a/test/src/status_code_test.dart +++ b/test/src/status_code_test.dart @@ -215,8 +215,9 @@ void main() => group('$StatusCode', () { ); test( - 'should extract first three-digit match from longer number', - // '12003' matches '120' first — not a registered code. + 'should return null for longer number with embedded valid range digits', + // '12003': lookahead/lookbehind prevents matching '120' (followed by digit) + // or '200' (preceded by digit) — no isolated [1-5]xx sequence found. () => expect(StatusCode.tryParse('12003'), isNull), ); @@ -232,6 +233,58 @@ void main() => group('$StatusCode', () { ); }); + group('pattern and regExp', () { + test( + 'pattern matches the expected regex string', + () => expect(StatusCode.pattern, r'(? expect(StatusCode.regExp, isA()), + ); + + test( + 'regExp matches a bare valid code', + () => expect(StatusCode.regExp.hasMatch('200'), isTrue), + ); + + test( + 'regExp matches a code preceded by non-digit characters', + () => expect(StatusCode.regExp.hasMatch('status: 404.'), isTrue), + ); + + test( + 'regExp matches a code followed by non-digit characters', + () => expect(StatusCode.regExp.hasMatch('200 OK'), isTrue), + ); + + test( + 'regExp does not match a 6xx code', + () => expect(StatusCode.regExp.hasMatch('600'), isFalse), + ); + + test( + 'regExp does not match a 9xx code', + () => expect(StatusCode.regExp.hasMatch('999'), isFalse), + ); + + test( + 'regExp does not match a valid-range code embedded in a longer number', + () => expect(StatusCode.regExp.hasMatch('12003'), isFalse), + ); + + test( + 'regExp does not match when code is preceded by a digit', + () => expect(StatusCode.regExp.hasMatch('1200'), isFalse), + ); + + test( + 'regExp does not match when code is followed by a digit', + () => expect(StatusCode.regExp.hasMatch('2001'), isFalse), + ); + }); + group('custom constructor edge cases', () { test('valid custom code in gap between standard codes', () { const custom = StatusCode.custom(109); From fb5adc7a251afc4a129d033d4f189a0f6cd54350 Mon Sep 17 00:00:00 2001 From: Roman Cinis <52065414+tsinis@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:48:52 +0100 Subject: [PATCH 6/8] fix: update comments and tests for status code handling - Clarified the description of officialCodes in api_reference.md - Updated tests in patterns.md to focus on specific status codes - Improved list creation in status_code.dart for better performance --- extension/mcp/resources/api_reference.md | 2 +- extension/mcp/resources/patterns.md | 15 ++++++--------- lib/src/status_code.dart | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/extension/mcp/resources/api_reference.md b/extension/mcp/resources/api_reference.md index 890c774..78b9259 100644 --- a/extension/mcp/resources/api_reference.md +++ b/extension/mcp/resources/api_reference.md @@ -32,7 +32,7 @@ final code = StatusCode.maybeFromCode(response.statusCode); // StatusCode? ```dart StatusCode.values // All 93 known codes (List) -StatusCode.officialCodes // IANA-registered subset +StatusCode.officialCodes // IANA-registered + widely adopted unofficial codes StatusCode.informationalCodes // 1xx StatusCode.successCodes // 2xx StatusCode.redirectionCodes // 3xx diff --git a/extension/mcp/resources/patterns.md b/extension/mcp/resources/patterns.md index 95a72f4..6079f86 100644 --- a/extension/mcp/resources/patterns.md +++ b/extension/mcp/resources/patterns.md @@ -185,11 +185,8 @@ response.statusCode.maybeWhenStatusCode( import 'package:test/test.dart'; import 'package:functional_status_codes/functional_status_codes.dart'; -test('handler returns null for any client error', () { - for (var i = 0; i < 50; i++) { - final code = StatusCode.random(from: StatusCode.clientErrorCodes); - expect(handleResponse(code, () => 'body'), isNull); - } +test('handler returns null for 404 Not Found', () { + expect(handleResponse(StatusCode.notFoundHttp404, () => 'body'), isNull); }); test('cache is written for any cacheable code', () async { @@ -208,11 +205,11 @@ test('retry is attempted for retryable server errors', () { } }); -// Combine with custom codes for edge-case testing -test('unknown code is handled gracefully', () { - final unknown = StatusCode.custom(999); +// Custom codes in the 5xx range hit isServerError → null +test('custom server error is handled gracefully', () { + final unknown = StatusCode.custom(555); // valid: not registered, within 103–598 expect(unknown.isCustom, isTrue); expect(unknown.toRegisteredStatusCode(), isNull); - expect(handleResponse(unknown, () => 'body'), isNull); + expect(handleResponse(unknown, () => 'body'), isNull); // hits isServerError }); ``` diff --git a/lib/src/status_code.dart b/lib/src/status_code.dart index b2a7ec9..9dce159 100644 --- a/lib/src/status_code.dart +++ b/lib/src/status_code.dart @@ -1252,7 +1252,7 @@ extension type const StatusCode._(int _code) implements int { Iterable from = values, Random? random, }) { - final list = from is List ? from : from.toList(); + final list = from is List ? from : from.toList(growable: false); assert(list.isNotEmpty, 'The provided `from` iterable must not be empty'); final elementAt = (random ?? Random()).nextInt(list.length); From ae828e8f8af5fec524bc21fa0b44564083f11b72 Mon Sep 17 00:00:00 2001 From: Roman Cinis <52065414+tsinis@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:52:48 +0100 Subject: [PATCH 7/8] fix: improve regex explanation for http status code pattern matching Updated the comments to clarify the use of negative look-ahead and look-behind assertions in the regex pattern for matching HTTP status codes. --- lib/src/status_code.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/status_code.dart b/lib/src/status_code.dart index 9dce159..490f0b9 100644 --- a/lib/src/status_code.dart +++ b/lib/src/status_code.dart @@ -689,8 +689,9 @@ extension type const StatusCode._(int _code) implements int { /// string. HTTP status codes are typically 3-digit integers ranging from 100 /// to 599. The pattern is defined by the regular expression /// `(? Date: Mon, 9 Mar 2026 22:55:29 +0100 Subject: [PATCH 8/8] fix: clarify regex assertions in status code pattern matching The pattern now specifies negative look-ahead and look-behind assertions to ensure accurate extraction of HTTP status codes from text. --- lib/src/status_code.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/status_code.dart b/lib/src/status_code.dart index 490f0b9..3c8e8a1 100644 --- a/lib/src/status_code.dart +++ b/lib/src/status_code.dart @@ -714,9 +714,10 @@ extension type const StatusCode._(int _code) implements int { /// matches anywhere in the input string. This allows for the extraction of /// status codes from within larger bodies of text. /// - /// The [pattern] uses negative lookarounds (`(?