diff --git a/CHANGELOG.md b/CHANGELOG.md index c996c1c..15c3772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,3 +9,7 @@ - `bytes` and `fromBytes` for `BINARY` or `BLOB` columns. - `value` and `fromValue` for key-value stores. - **feature**: Use Crockford's Base32 encoding for improved readability and URL safety. + +## 0.2.0 + +- Complete rewrite to improve the API and implementation after real world usage feedback \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md index 36632cb..18cc06e 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,60 +1,20 @@ -# Gemini Code Understanding - -## Project Overview - -This project is a Dart library named `resource_id`. Its purpose is to provide a robust and immutable solution for creating, parsing, and validating resource identifiers, guided by modern API design principles. - -The core of the library is the `ResourceId` class, which is immutable and provides methods for generating new IDs, parsing from strings, and serializing to various formats for database storage (including `BigInt` for relational databases and `String` for key-value stores). - -## Design Philosophy - -The design of this library is based on a set of principles for creating "good" identifiers that are easy to use, unique, permanent, and secure. - -* **Uniqueness and Scoping**: To prevent identifier collisions between different types of resources (e.g., a book and a user having the same ID), the library uses **type-safe prefixes** (e.g., `books/` or `users/`). This creates a unique namespace for each resource type, ensuring that an ID is unambiguous. - -* **Readability and Shareability**: Identifiers should be easy for humans to read, share, and type. To achieve this, the library uses **Crockford's Base32 encoding**. This encoding avoids visually ambiguous characters (like `1`, `I`, and `L`, or `0` and `O`) and is case-insensitive, making it resilient to common transcription errors. It also avoids characters that have special meaning in URIs. - -* **Error Detection**: To help distinguish between a valid identifier that points to a non-existent resource and an identifier that has been mistyped, each `ResourceId` includes a **checksum**. This allows for immediate validation that the ID is structurally correct, improving the robustness of APIs that use it. - -* **Permanence and Immutability**: A resource identifier should be permanent and never change once assigned. The `ResourceId` class is immutable to enforce this principle. The library is designed to generate identifiers that should never be reused, even after a resource is deleted (a practice known as "tomb-stoning"). - -* **Unpredictability**: To enhance security, identifiers should not be sequential or predictable. The library generates identifiers from a large, random keyspace, making it difficult for attackers to guess valid IDs and probe for vulnerabilities. - -* **Minimum Size Enforcement**: To ensure the validity and usefulness of generated identifiers, the library enforces a minimum `sizeInBytes` of 1 for both generation and reconstruction from `BigInt`. This prevents the creation of 0-byte identifiers that lack unique information. - -The project is well-documented, with a detailed `README.md` that explains the rationale and usage, and the source code itself contains clear documentation comments. - -## Building and Running - -This is a Dart library, so there is no main executable to run. The primary way to interact with the project is by running its tests. - -### Running Tests - -The project uses the standard `test` package for testing. To run the tests, use the following command: - -```bash -dart test -``` - -## Development Conventions - -### Coding Style - -The project follows standard Dart conventions and uses the `lints` package to enforce code quality. The `analysis_options.yaml` file includes the recommended lints from `package:lints/recommended.yaml`. It also includes a large number of additional lints beyond what is typically recommended to enforce a higher code quality with stricter standards. - -### Testing - -The project has a comprehensive test suite in the `test/` directory. The tests are written using the `test` package and follow a clear "arrange, act, assert" pattern within `group` and `test` blocks. The tests cover: -- ID generation and parsing -- Checksum validation and typo detection -- "Friendly" parsing (case-insensitivity, ignoring hyphens) -- Hierarchical ID creation and parsing -- Equality checks -- Serialization and deserialization for different database types - -### Dependencies - -- `base32_codec`: Used for Crockford's Base32 encoding and decoding. -- `meta`: Used for annotations like `@immutable`. - -Development dependencies include `lints` for static analysis and `test` for unit testing. Dependencies are managed in the `pubspec.yaml` file. +# Resource ID Package + +## Purpose +The `resource_id` package implements a secure, human-friendly resource identifier scheme based on Crockford's Base32 encoding and Modulo-37 checksums. It is designed to be URL-safe, easy to communicate, and robust against typos. + +## Implementation Details + +### Core Logic +* **Encoding**: Uses `base32_codec` with the `Crockford` variant. +* **Checksum**: Custom implementation in `lib/src/checksum.dart`. Calculates `bytes % 37`. +* **Class**: `ResourceId` in `lib/src/resource_id.dart` encapsulates the logic. + * **Generation**: Uses `dart:math` `Random.secure()`. + * **Parsing**: Splits input string by `/`. Treats the last segment as the ID payload + checksum. Everything prior is the `type`. + * **Validation**: Decodes the Base32 payload, recalculates checksum, and compares with the check digit. + +### File Structure +* `lib/resource_id.dart`: Main export file. +* `lib/src/resource_id.dart`: Core `ResourceId` class. +* `lib/src/checksum.dart`: Checksum algorithms and character constants. +* `test/`: Unit tests covering generation, parsing, checksums, and edge cases. diff --git a/README.md b/README.md index 604e6d4..74c5193 100644 --- a/README.md +++ b/README.md @@ -1,157 +1,84 @@ -[![pub package](https://img.shields.io/pub/v/resource_id.svg)](https://pub.dev/packages/resource_id) -[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) -[![Open in Firebase Studio](https://cdn.firebasestudio.dev/btn/open_light_20.svg)](https://studio.firebase.google.com/import?url=https%3A%2F%2Fgithub.com%2Fdropbear-software%2Fresource_id) +# Resource ID -# ResourceId +A Dart package for generating, parsing, and verifying resource identifiers using +[Crockford's Base32](https://www.crockford.com/base32.html) encoding with a +modulo-37 checksum. -A Dart package for creating, parsing, and validating robust, immutable, and URL-safe resource identifiers. - -This package provides an immutable `ResourceId` class that enforces modern API design principles. It generates prefixed, URL-safe identifiers (e.g., `books/9V233V10702ETQW3S1WKTZ~`) that are easy to read, copy, and debug, with built-in checksums to prevent typos. - -This implementation is based on the strong recommendations for great resource identifiers in the book [API Design Patterns](https://www.manning.com/books/api-design-patterns) by J.J Geewax. - -## Why use ResourceId? - -While it can be tempting to use simple `String`s or `UUID`s for identifiers, they often fall short in real-world applications. This package solves common problems by providing IDs that are: - -- **Type-Safe:** Prevents you from accidentally using a `userId` where a `bookId` was expected. -- **Prefix-Aware:** The resource type is part of the ID (e.g., `books/...`), making debugging and logging much clearer. -- **Typo-Proof:** A built-in checksum immediately catches typos or copy-paste errors during parsing, preventing invalid queries. -- **Human-Readable:** Uses Crockford's Base32, an encoding designed to avoid ambiguous characters (like `I`, `L`, `O`, and `U`), making IDs easier for humans to read and transcribe. +This package implements the specification for robust, user-friendly, and secure +resource identifiers. ## Features -- **Immutable & Type-Safe:** Enforces correctness at compile time. -- **Prefix-Aware:** Includes the resource type (e.g., `books/`) in the ID, preventing ID-mixing bugs. -- **Checksum Validation:** The final character is a `mod-37` checksum. The `parse` method automatically validates this. -- **Crockford's Base32 Encoding:** Uses a highly readable, URL-safe character set. -- **Friendly Parsing:** The parser is case-insensitive and ignores hyphens, allowing for more human-readable formats like `books/bkb3-xyt4-65kz-69`. -- **Hierarchical Support:** Natively supports parent-child relationships (e.g., `books/1/pages/2`). -- **Database-First Serialization:** Easily and efficiently serialize to `Uint8List` (for `BINARY`), `BigInt` (for `BIGINT`), or a pure `String` (for key-value stores) and reconstruct with confidence. -- **Secure Generation:** Uses a cryptographically secure random number generator. - -## Getting Started - -Add the package to your `pubspec.yaml`: - -```yaml -dependencies: - resource_id: ^0.1.0 # Check pub.dev for latest version -``` - -Then, import the package in your Dart file: - -```dart -import 'package:resource_id/resource_id.dart'; -``` +- **Secure Random Generation**: Uses `Random.secure()` (cryptographically secure + random bytes). +- **Crockford Base32**: Human-readable, URL-safe, case-insensitive, and ignores + hyphens. +- **Checksum Verification**: Built-in modulo-37 checksum detects typos and + invalid IDs immediately. +- **Raw Byte Access**: Exposes the underlying random bytes for efficient + database storage (e.g., as `BINARY` or large integers). +- **Hierarchical Support**: Supports parsing identifiers with path contexts + (e.g., `books/123/pages/456` resolves to the leaf ID `456` with context + `books/123/pages`). ## Usage -### 1. Generating a New ID -Use `ResourceId.generate()` to create a new, secure identifier. The default size of 8 bytes is recommended for most resources as it fits perfectly in a database `BIGINT` column. +### 1. Generating Identifiers ```dart -// Generate a new ID for a "books" collection -final bookId = ResourceId.generate(resourceType: 'books'); +import 'package:resource_id/resource_id.dart'; -// The toString() method includes the type prefix and checksum -print(bookId); -// Output: books/8A1B2C3D4E5F6G7H8J~ (random part will vary) +void main() { + // Generate a random ID (default 15 bytes of entropy) + final id = ResourceId.generate(); + print(id); // e.g. "0123456789ABCDE~" + + // Generate with a type prefix + final bookId = ResourceId.generate(type: 'books'); + print(bookId); // e.g. "books/0123456789ABCDE~" + + // Access raw bytes for storage + List bytes = bookId.bytes; +} ``` -### 2. Parsing and Validating an ID -Use `ResourceId.parse()` to convert a string back into a `ResourceId`. The checksum is validated automatically, throwing a `FormatException` if there's a typo. +### 2. Parsing and Verifying ```dart try { - // Note the typo: 'P' was mistyped as 'A' - final badId = 'books/BKB3XYT465KZ6A'; - ResourceId.parse(badId); -} on FormatException catch (e) { - print(e.message); - // Output: Invalid identifier: Checksum mismatch. Possible typo. + // Parse a string (handles hyphens and casing) + final id = ResourceId.parse('books/0123-4567-89ab-cde~'); + + print('Type: ${id.type}'); // "books" + print('Valid checksum: ${id.checksumChar}'); + +} catch (e) { + print('Invalid ID: $e'); } -``` - -### 3. Creating Hierarchical IDs -Pass the `parent` ID during generation to create a child resource. -```dart -// 1. Create the parent ID -final bookId = ResourceId.parse('books/BKB3XYT465KZ69'); - -// 2. Generate a child ID, passing the parent -final pageId = ResourceId.generate( - resourceType: 'pages', - parent: bookId, -); - -// 3. The full path is included in the ID -print(pageId); -// Output: books/BKB3XYT465KZ69/pages/3N18Y6V9T0A2W4S~ +// Check validity without throwing +if (ResourceId.isValid('invalid-id')) { + // ... +} ``` -### 4. Storing in a Database -`ResourceId` provides multiple ways to get the raw identifier for efficient storage. +### 3. Hierarchical Identifiers -#### Option A: Relational Database (BIGINT) -This is the most performant option for relational databases, as they are highly optimized for indexing and joining on integer types. +The package supports hierarchical paths. When parsing a path, the last segment is +treated as the unique ID, and the preceding path is preserved as the `type`. ```dart -final id = ResourceId.generate(resourceType: 'users', sizeInBytes: 8); - -// Store in a BIGINT column -final BigInt intToStore = id.asBigInt; - -// Reconstruct from the database value -final reconstructedId = ResourceId.fromBigInt( - resourceType: 'users', - value: intToStore, - sizeInBytes: 8, // Provide the known, fixed size for this resource type -); -``` - -#### Option B: Relational Database (BINARY) -This is the best choice for IDs larger than 8 bytes (64 bits). - -```dart -final id = ResourceId.generate(resourceType: 'sessions', sizeInBytes: 16); - -// Store in a BINARY(16) or BLOB column -final Uint8List bytesToStore = id.bytes; +final path = 'books/AHM6/pages/B7K9~'; +final id = ResourceId.parse(path); -// Reconstruct from the database value -final reconstructedId = ResourceId.fromBytes( - resourceType: 'sessions', - bytes: bytesToStore, -); +print(id.type); // "books/AHM6/pages" +print(id.toString()); // "books/AHM6/pages/B7K9~" (normalized) ``` -#### Option C: Key-Value Store (String) -For databases like Firestore or DynamoDB, you can store the pure Base32 value. - -```dart -final id = ResourceId.generate(resourceType: 'invoices'); - -// Store in a string field -final String valueToStore = id.value; - -// Reconstruct from the database value -final reconstructedId = ResourceId.fromValue( - resourceType: 'invoices', - value: valueToStore, -); -``` - -## Comparison with UUIDs - -UUIDs (Universally Unique Identifiers) are a common standard for generating unique IDs. While they are an excellent choice for ensuring global uniqueness, `ResourceId` offers several practical advantages for application development: - -| Feature | Standard UUID | ResourceId | Benefit | -| :----------------- | :------------------------------------------- | :----------------------------- | :------------------------------------------------------------------------------------------------------------------ | -| **Readability** | `123e4567-e89b-12d3-a456-426655440000` | `books/BKB3X-YT465-KZ69` | Easier for humans to read, transcribe, and share. Uses a character set that avoids common visual ambiguities. | -| **Typo Detection** | None | Built-in Checksum | `ResourceId.parse()` instantly detects typos or copy-paste errors, preventing "not found" errors from ever hitting your database. | -| **Resource Context** | No | Prefixed (`books/...`) | Identifiers are self-describing, which makes debugging, logging, and reading API requests much clearer. | -| **Size Flexibility**| Fixed at 128 bits | Flexible (e.g., 64 bits) | Allows you to choose a smaller (1 byte minimum), more performant size (like a `BIGINT`) for resources that don't require global uniqueness, optimizing database performance. | +## Format Specification -While you can absolutely use a 128-bit `ResourceId` to wrap a UUID's raw bytes to get the best of both worlds, for most application use cases, a 64-bit `ResourceId` provides a more ergonomic, safer, and often more performant solution. +* **Encoding**: Crockford Base32. +* **Checksum**: Modulo-37 (`value % 37`). +* **Alphabet**: `0123456789ABCDEFGHJKMNPQRSTVWXYZ` (32 chars) + `*~$=U` (5 + checksum-only chars). +* **Structure**: `[optional_type_prefix/][base32_payload][checksum_char]` \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index cdf81a9..91a738a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,16 +1,7 @@ # This file configures the static analysis results for your project (errors, # warnings, and lints). # -# This enables the 'recommended' set of lints from `package:lints`. -# This set helps identify many issues that may lead to problems when running -# or consuming Dart code, and enforces writing Dart using a single, idiomatic -# style and format. -# -# If you want a smaller set of lints you can change this to specify -# 'package:lints/core.yaml'. These are just the most critical lints -# (the recommended set includes the core lints). -# The core lints are also what is used by pub.dev for scoring packages. - +# Use the same starting point as most of the Dart community. include: package:lints/recommended.yaml analyzer: @@ -24,7 +15,6 @@ analyzer: linter: rules: - always_put_required_named_parameters_first - - always_use_package_imports - annotate_redeclares - avoid_annotating_with_dynamic - avoid_bool_literals_in_conditional_expressions diff --git a/doc/api/__404error.html b/doc/api/__404error.html index acab788..ef2caa6 100644 --- a/doc/api/__404error.html +++ b/doc/api/__404error.html @@ -89,7 +89,7 @@
resource_id - 0.1.0 + 0.2.0
diff --git a/doc/api/index.html b/doc/api/index.html index f8bd4fc..0187510 100644 --- a/doc/api/index.html +++ b/doc/api/index.html @@ -43,154 +43,77 @@
-

ResourceId

-

A Dart package for creating, parsing, and validating robust, immutable, and URL-safe resource identifiers.

-

This package provides an immutable ResourceId class that enforces modern API design principles. It generates prefixed, URL-safe identifiers (e.g., books/9V233V10702ETQW3S1WKTZ~) that are easy to read, copy, and debug, with built-in checksums to prevent typos.

-

This implementation is based on the strong recommendations for great resource identifiers in the book API Design Patterns by J.J Geewax.

-

Why use ResourceId?

-

While it can be tempting to use simple Strings or UUIDs for identifiers, they often fall short in real-world applications. This package solves common problems by providing IDs that are:

-
    -
  • Type-Safe: Prevents you from accidentally using a userId where a bookId was expected.
  • -
  • Prefix-Aware: The resource type is part of the ID (e.g., books/...), making debugging and logging much clearer.
  • -
  • Typo-Proof: A built-in checksum immediately catches typos or copy-paste errors during parsing, preventing invalid queries.
  • -
  • Human-Readable: Uses Crockford's Base32, an encoding designed to avoid ambiguous characters (like I, L, O, and U), making IDs easier for humans to read and transcribe.
  • -
+

Resource ID

+

A Dart package for generating, parsing, and verifying resource identifiers using +Crockford's Base32 encoding with a +modulo-37 checksum.

+

This package implements the specification for robust, user-friendly, and secure +resource identifiers.

Features

    -
  • Immutable & Type-Safe: Enforces correctness at compile time.
  • -
  • Prefix-Aware: Includes the resource type (e.g., books/) in the ID, preventing ID-mixing bugs.
  • -
  • Checksum Validation: The final character is a mod-37 checksum. The parse method automatically validates this.
  • -
  • Crockford's Base32 Encoding: Uses a highly readable, URL-safe character set.
  • -
  • Friendly Parsing: The parser is case-insensitive and ignores hyphens, allowing for more human-readable formats like books/bkb3-xyt4-65kz-69.
  • -
  • Hierarchical Support: Natively supports parent-child relationships (e.g., books/1/pages/2).
  • -
  • Database-First Serialization: Easily and efficiently serialize to Uint8List (for BINARY), BigInt (for BIGINT), or a pure String (for key-value stores) and reconstruct with confidence.
  • -
  • Secure Generation: Uses a cryptographically secure random number generator.
  • +
  • Secure Random Generation: Uses Random.secure() (cryptographically secure +random bytes).
  • +
  • Crockford Base32: Human-readable, URL-safe, case-insensitive, and ignores +hyphens.
  • +
  • Checksum Verification: Built-in modulo-37 checksum detects typos and +invalid IDs immediately.
  • +
  • Raw Byte Access: Exposes the underlying random bytes for efficient +database storage (e.g., as BINARY or large integers).
  • +
  • Hierarchical Support: Supports parsing identifiers with path contexts +(e.g., books/123/pages/456 resolves to the leaf ID 456 with context +books/123/pages).
-

Getting Started

-

Add the package to your pubspec.yaml:

-
dependencies:
-  resource_id: ^0.1.0 # Check pub.dev for latest version
-
-

Then, import the package in your Dart file:

-
import 'package:resource_id/resource_id.dart';
-

Usage

-

1. Generating a New ID

-

Use ResourceId.generate() to create a new, secure identifier. The default size of 8 bytes is recommended for most resources as it fits perfectly in a database BIGINT column.

-
// Generate a new ID for a "books" collection
-final bookId = ResourceId.generate(resourceType: 'books');
-
-// The toString() method includes the type prefix and checksum
-print(bookId);
-// Output: books/8A1B2C3D4E5F6G7H8J~ (random part will vary)
+

1. Generating Identifiers

+
import 'package:resource_id/resource_id.dart';
+
+void main() {
+  // Generate a random ID (default 15 bytes of entropy)
+  final id = ResourceId.generate();
+  print(id); // e.g. "0123456789ABCDE~"
+
+  // Generate with a type prefix
+  final bookId = ResourceId.generate(type: 'books');
+  print(bookId); // e.g. "books/0123456789ABCDE~"
+  
+  // Access raw bytes for storage
+  List<int> bytes = bookId.bytes;
+}
 
-

2. Parsing and Validating an ID

-

Use ResourceId.parse() to convert a string back into a ResourceId. The checksum is validated automatically, throwing a FormatException if there's a typo.

+

2. Parsing and Verifying

try {
-  // Note the typo: 'P' was mistyped as 'A'
-  final badId = 'books/BKB3XYT465KZ6A'; 
-  ResourceId.parse(badId);
-} on FormatException catch (e) {
-  print(e.message);
-  // Output: Invalid identifier: Checksum mismatch. Possible typo.
+  // Parse a string (handles hyphens and casing)
+  final id = ResourceId.parse('books/0123-4567-89ab-cde~');
+  
+  print('Type: ${id.type}'); // "books"
+  print('Valid checksum: ${id.checksumChar}');
+  
+} catch (e) {
+  print('Invalid ID: $e');
+}
+
+// Check validity without throwing
+if (ResourceId.isValid('invalid-id')) {
+  // ...
 }
 
-

3. Creating Hierarchical IDs

-

Pass the parent ID during generation to create a child resource.

-
// 1. Create the parent ID
-final bookId = ResourceId.parse('books/BKB3XYT465KZ69');
-
-// 2. Generate a child ID, passing the parent
-final pageId = ResourceId.generate(
-  resourceType: 'pages',
-  parent: bookId,
-);
-
-// 3. The full path is included in the ID
-print(pageId); 
-// Output: books/BKB3XYT465KZ69/pages/3N18Y6V9T0A2W4S~
-
-

4. Storing in a Database

-

ResourceId provides multiple ways to get the raw identifier for efficient storage.

-

Option A: Relational Database (BIGINT)

-

This is the most performant option for relational databases, as they are highly optimized for indexing and joining on integer types.

-
final id = ResourceId.generate(resourceType: 'users', sizeInBytes: 8);
-
-// Store in a BIGINT column
-final BigInt intToStore = id.asBigInt;
-
-// Reconstruct from the database value
-final reconstructedId = ResourceId.fromBigInt(
-  resourceType: 'users',
-  value: intToStore,
-  sizeInBytes: 8, // Provide the known, fixed size for this resource type
-);
-
-

Option B: Relational Database (BINARY)

-

This is the best choice for IDs larger than 8 bytes (64 bits).

-
final id = ResourceId.generate(resourceType: 'sessions', sizeInBytes: 16);
-
-// Store in a BINARY(16) or BLOB column
-final Uint8List bytesToStore = id.bytes;
-
-// Reconstruct from the database value
-final reconstructedId = ResourceId.fromBytes(
-  resourceType: 'sessions',
-  bytes: bytesToStore,
-);
-
-

Option C: Key-Value Store (String)

-

For databases like Firestore or DynamoDB, you can store the pure Base32 value.

-
final id = ResourceId.generate(resourceType: 'invoices');
-
-// Store in a string field
-final String valueToStore = id.value;
-
-// Reconstruct from the database value
-final reconstructedId = ResourceId.fromValue(
-  resourceType: 'invoices',
-  value: valueToStore,
-);
+

3. Hierarchical Identifiers

+

The package supports hierarchical paths. When parsing a path, the last segment is +treated as the unique ID, and the preceding path is preserved as the type.

+
final path = 'books/AHM6/pages/B7K9~';
+final id = ResourceId.parse(path);
+
+print(id.type); // "books/AHM6/pages"
+print(id.toString()); // "books/AHM6/pages/B7K9~" (normalized)
 
-

Comparison with UUIDs

-

UUIDs (Universally Unique Identifiers) are a common standard for generating unique IDs. While they are an excellent choice for ensuring global uniqueness, ResourceId offers several practical advantages for application development:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FeatureStandard UUIDResourceIdBenefit
Readability123e4567-e89b-12d3-a456-426655440000books/BKB3X-YT465-KZ69Easier for humans to read, transcribe, and share. Uses a character set that avoids common visual ambiguities.
Typo DetectionNoneBuilt-in ChecksumResourceId.parse() instantly detects typos or copy-paste errors, preventing "not found" errors from ever hitting your database.
Resource ContextNoPrefixed (books/...)Identifiers are self-describing, which makes debugging, logging, and reading API requests much clearer.
Size FlexibilityFixed at 128 bitsFlexible (e.g., 64 bits)Allows you to choose a smaller (1 byte minimum), more performant size (like a BIGINT) for resources that don't require global uniqueness, optimizing database performance.
-

While you can absolutely use a 128-bit ResourceId to wrap a UUID's raw bytes to get the best of both worlds, for most application use cases, a 64-bit ResourceId provides a more ergonomic, safer, and often more performant solution.

+

Format Specification

+
    +
  • Encoding: Crockford Base32.
  • +
  • Checksum: Modulo-37 (value % 37).
  • +
  • Alphabet: 0123456789ABCDEFGHJKMNPQRSTVWXYZ (32 chars) + *~$=U (5 +checksum-only chars).
  • +
  • Structure: [optional_type_prefix/][base32_payload][checksum_char]
  • +
@@ -201,8 +124,7 @@

Libraries

resource_id -
A Dart package for creating, parsing, and validating robust, immutable, -and URL-safe resource identifiers. +
A library for generating, parsing, and verifying secure resource identifiers.
@@ -232,7 +154,7 @@
@@ -332,7 +269,7 @@
resource_id library
resource_id - 0.1.0 + 0.2.0
diff --git a/doc/api/resource_id/ResourceId/ResourceId.generate.html b/doc/api/resource_id/ResourceId/ResourceId.generate.html index 6c14573..194bdb5 100644 --- a/doc/api/resource_id/ResourceId/ResourceId.generate.html +++ b/doc/api/resource_id/ResourceId/ResourceId.generate.html @@ -54,51 +54,32 @@

ResourceId.generate constructor
- ResourceId.generate({
  1. required String resourceType,
  2. -
  3. ResourceId? parent,
  4. -
  5. int sizeInBytes = 8,
  6. + ResourceId.generate({
    1. String? type,
    2. +
    3. int byteLength = 15,
    })
-

Generates a new, cryptographically secure ResourceId.

-

This is the recommended way to create new identifiers.

-

resourceType is the collection name (e.g., "books"). -parent is an optional parent ID for hierarchical resources. -sizeInBytes defaults to 8 (64 bits), which is suitable for storage -in a database BIGINT column and is recommended for most resources. -For globally unique IDs (like UUIDs), 15 or 16 bytes (120-128 bits) -is recommended.

+

Generates a new random ResourceId.

+

The type is an optional resource type prefix. +The byteLength determines the size of the random payload (defaults to 15, approx 120 bits of entropy).

Implementation

-
factory ResourceId.generate({
-  required String resourceType,
-  ResourceId? parent,
-  int sizeInBytes = 8,
-}) {
-  if (sizeInBytes < 1) {
-    throw ArgumentError.value(
-      sizeInBytes,
-      'sizeInBytes',
-      'Must be at least 1',
-    );
-  }
-  // Use a cryptographically secure random number generator.
+  
factory ResourceId.generate({String? type, int byteLength = 15}) {
   final random = Random.secure();
-  final bytes = Uint8List(sizeInBytes);
-  for (var i = 0; i < sizeInBytes; i++) {
+  final bytes = Uint8List(byteLength);
+  for (var i = 0; i < byteLength; i++) {
     bytes[i] = random.nextInt(256);
   }
 
-  return ResourceId._(
-    resourceType: resourceType,
-    bytes: bytes,
-    parent: parent,
-  );
+  final checksumVal = calculateChecksum(bytes);
+  final checksumChar = getChecksumCharacter(checksumVal);
+
+  return ResourceId._(type, bytes, checksumChar);
 }
@@ -127,7 +108,7 @@
ResourceId class
resource_id - 0.1.0 + 0.2.0
diff --git a/doc/api/resource_id/ResourceId/ResourceId.parse.html b/doc/api/resource_id/ResourceId/ResourceId.parse.html index 00df0c1..8d0a331 100644 --- a/doc/api/resource_id/ResourceId/ResourceId.parse.html +++ b/doc/api/resource_id/ResourceId/ResourceId.parse.html @@ -54,76 +54,70 @@

ResourceId.parse constructor
- ResourceId.parse(
  1. String fullId
  2. + ResourceId.parse(
    1. String input
    )
-

Parses a full identifier string (e.g., "books/AHM6A83HENMP~") -into a validated ResourceId instance.

-

This method recursively parses hierarchical paths and validates -the checksum of the final ID segment.

-

Throws a FormatException if the string is invalid, has a -checksum mismatch, or is improperly formatted.

+

Parses a string representation of a ResourceId.

+

Accepts formats like:

+
    +
  • AHM6A83HENMP~
  • +
  • books/AHM6A83HENMP~
  • +
  • books/ahm6-a83h-enmp~ (hyphens and lowercase allowed)
  • +
+

Throws a FormatException if the input is invalid (e.g., bad checksum, invalid characters).

Implementation

-
factory ResourceId.parse(String fullId) {
-  if (fullId.isEmpty) {
-    throw FormatException('Identifier cannot be empty.');
+  
factory ResourceId.parse(String input) {
+  if (input.isEmpty) {
+    throw FormatException('ResourceId cannot be empty');
   }
 
-  final parts = fullId.split('/');
-  if (parts.length < 2 || parts.any((p) => p.isEmpty)) {
-    throw FormatException(
-      "Invalid ID format. Must be at least 'resourceType/id'.",
-    );
-  }
+  String? type;
+  var idPart = input;
 
-  // The last two parts are the resource type and the ID value
-  final idWithChecksum = parts.last;
-  final resourceType = parts[parts.length - 2];
-
-  // Everything else forms the parent path, which we parse recursively
-  ResourceId? parent;
-  final parentParts = parts.sublist(0, parts.length - 2);
-  if (parentParts.isNotEmpty) {
-    parent = ResourceId.parse(parentParts.join('/'));
+  // 1. Separate Type
+  if (input.contains('/')) {
+    // Treats the last segment as the ID and the preceding path as the type.
+    final lastSlashIndex = input.lastIndexOf('/');
+    type = input.substring(0, lastSlashIndex);
+    idPart = input.substring(lastSlashIndex + 1);
   }
 
-  // Now, validate the final ID part
-  if (idWithChecksum.length < 2) {
-    throw FormatException('Invalid ID: Missing value or checksum.');
+  if (idPart.length < 2) {
+    throw FormatException('ID part too short');
   }
 
-  final encodedValue = idWithChecksum.substring(0, idWithChecksum.length - 1);
-  final checksumChar = idWithChecksum[idWithChecksum.length - 1];
+  // 2. Extract Checksum (last char)
+  final checksumChar = idPart.substring(idPart.length - 1);
+  final payloadStr = idPart.substring(0, idPart.length - 1);
 
-  // Decode using the "friendly" Crockford's codec.
-  final Uint8List bytes;
+  // 3. Decode Payload
+  // The decoder handles case insensitivity and ignores hyphens.
+  List<int> decodedBytes;
   try {
-    bytes = _codec.decode(encodedValue);
+    decodedBytes = _codec.decode(payloadStr);
   } catch (e) {
-    throw FormatException('Invalid Base32 format: $e');
+    throw FormatException('Invalid Base32 encoding: $e');
   }
 
-  // Verify the checksum
-  final expectedChar = _getChecksumCharacter(_calculateChecksum(bytes));
-  if (checksumChar != expectedChar) {
-    // This is a key feature: distinguishing typos from "not found"
+  // 4. Verify Checksum
+  final calculatedChecksum = calculateChecksum(decodedBytes);
+  final expectedChar = getChecksumCharacter(calculatedChecksum);
+
+  // Normalize checksum char for comparison.
+  if (checksumChar.toUpperCase() != expectedChar) {
     throw FormatException(
-      'Invalid identifier: Checksum mismatch. Possible typo.',
+      'Invalid checksum. Expected $expectedChar, got $checksumChar',
     );
   }
 
-  return ResourceId._(
-    resourceType: resourceType,
-    bytes: bytes,
-    parent: parent,
-  );
+  return ResourceId._(type, decodedBytes, expectedChar);
 }
@@ -152,7 +146,7 @@
ResourceId class
resource_id - 0.1.0 + 0.2.0
diff --git a/doc/api/resource_id/ResourceId/bytes.html b/doc/api/resource_id/ResourceId/bytes.html index 26b88fd..6722d22 100644 --- a/doc/api/resource_id/ResourceId/bytes.html +++ b/doc/api/resource_id/ResourceId/bytes.html @@ -53,26 +53,32 @@

bytes property

-
- - Uint8List - bytes -
final
-
+ + +
+
+ List<int> + get + bytes + + +
+
-

The raw, unique bytes for this resource.

+

Returns the raw bytes for storage.

- +

Implementation

-
final Uint8List bytes;
+
List<int> get bytes => _bytes;
+
+ -