Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
80 changes: 20 additions & 60 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -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.
187 changes: 57 additions & 130 deletions README.md
Original file line number Diff line number Diff line change
@@ -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<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

```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]`
12 changes: 1 addition & 11 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion doc/api/__404error.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ <h5>
<footer>
<span class="no-break">
resource_id
0.1.0
0.2.0
</span>

</footer>
Expand Down
Loading