From 2c9c9ec2c894ac79dd99c1322a3c65e67b9492c3 Mon Sep 17 00:00:00 2001 From: Mehmet Esen Date: Thu, 21 May 2026 09:48:36 +0300 Subject: [PATCH 1/3] feat: v4.1.0 networking + skill polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Identity & networking test data — 12 new primitives, no breaking changes. Added - Networking: email, ipv4, ipv6, mac, hex - Strings: semver, otp, slug, base64 - Collections: enumValue, shuffled - Geo: geoPoint (named record) Polish - example/recipes.dart — composer recipes (User, Address, Order, Paginated, ChatHistory) for building structured fixtures on flat primitives - SKILL.md description tightened to Claude Code convention (~140w → ~70w, action-verb lead, "Use when" trigger, "Skip for" negative triggers) - SKILL.md "Common tasks" section — 10 prompt→snippet rows - README "Pitfalls" table consolidating common traps - CHANGELOG migration table normalized to |---|---| Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 51 +++++++++-- README.md | 121 ++++++++++++------------ example/main.dart | 18 ++++ example/recipes.dart | 165 +++++++++++++++++++++++++++++++++ lib/rand.dart | 174 +++++++++++++++++++++++++++++++++++ lib/src/_collections.dart | 4 + lib/src/_crypto.dart | 7 ++ lib/src/_networking.dart | 43 +++++++++ lib/src/_numbers.dart | 18 ++++ lib/src/_text.dart | 7 ++ lib/src/rand_impl.dart | 3 +- pubspec.yaml | 2 +- skills/dart-rand/SKILL.md | 66 +++++++++++--- test/rand_test.dart | 187 ++++++++++++++++++++++++++++++++++++++ 14 files changed, 786 insertions(+), 80 deletions(-) create mode 100644 example/recipes.dart create mode 100644 lib/src/_networking.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index d35f634..3076c64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 4.1.0 + +**Identity & networking test data.** 12 new primitives, no breaking changes. + +### Added + +- **Networking** — `Rand.email({domain})`, `Rand.ipv4()`, `Rand.ipv6()`, + `Rand.mac({separator})`, `Rand.hex({length})`. `email` defaults to RFC 2606 + example/test TLDs (`example.com`, `test.com`, etc.) — fixture-safe. + `ipv6` is returned in full uncompressed form so length is stable. + `hex` is general-purpose lowercase hex (git SHAs at `length: 40`, ETags, etc.) + and reproducible under `Rand.seed`. +- **String synthesis** — `Rand.semver({maxMajor, maxMinor, maxPatch})` (no + pre-release suffix), `Rand.otp({length})` (zero-padded decimal digits), + `Rand.slug({wordCount, separator})` (unique lorem words), + `Rand.base64({byteLength})` (crypto-secure parallel to `nonce`). +- **Collection ergonomics** — `Rand.enumValue(values)` + (type-safe wrapper over `element` for enums) and `Rand.shuffled(list)` + (non-mutating copy + shuffle; distinct from `sample`/`subSet`). +- **Geo** — `Rand.geoPoint({precision})` returns a named record + `({double lat, double lng})` composing `latitude` + `longitude`. +- New mixin `_Networking` under `lib/src/_networking.dart`. +- Example app (`example/main.dart`) gains Networking section and uses + the new methods in Geo, Collections, and Cryptographic sections. + +### Changed + +- README adds a Networking section and extends Collections with `enumValue` + / `shuffled` rows. SKILL.md description and "Picking the right call" + table updated to cover the new methods. + ## 4.0.0 **Breaking — major overhaul.** @@ -12,16 +43,16 @@ namespaces). ### Breaking -| v3.x | v4.0 | -| --- | --- | -| `CSSColors` (enum) | `CssColors` — modern Dart PascalCase | -| `c.color` (int field) | `c.argb` — clarifies it's a 32-bit ARGB packed int | -| `c.isDark` (stored field) | `c.isDark` (computed via `CssColorsX` extension, YIQ luminance) | -| `Rand.bytes(32, true)` / `Rand.bytes(32, secure: true)` | `Rand.bytes(32)` — always secure | -| `Rand.nonce(secure: true)` | `Rand.nonce()` — always secure | -| `Rand.subSet([1, 2, 2], 2)` | `Rand.subSet({1, 2}, 2)` — `Set` only | -| `Rand.sample(..., secure: true)` | `Rand.useRng(Random.secure()); Rand.sample(...)` | -| `Rand.element([])` → `RangeError` | `Rand.element([])` → `StateError` | +|v3.x|v4.0| +|---|---| +|`CSSColors` (enum)|`CssColors` — modern Dart PascalCase| +|`c.color` (int field)|`c.argb` — clarifies it's a 32-bit ARGB packed int| +|`c.isDark` (stored field)|`c.isDark` (computed via `CssColorsX` extension, YIQ luminance)| +|`Rand.bytes(32, true)` / `Rand.bytes(32, secure: true)`|`Rand.bytes(32)` — always secure| +|`Rand.nonce(secure: true)`|`Rand.nonce()` — always secure| +|`Rand.subSet([1, 2, 2], 2)`|`Rand.subSet({1, 2}, 2)` — `Set` only| +|`Rand.sample(..., secure: true)`|`Rand.useRng(Random.secure()); Rand.sample(...)`| +|`Rand.element([])` → `RangeError`|`Rand.element([])` → `StateError`| ### Fixed diff --git a/README.md b/README.md index 1d01a5a..5bab5cb 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,14 @@ [![pub points](https://img.shields.io/pub/points/rand)](https://pub.dev/packages/rand/score) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -**Random data for Dart.** Numbers, text, names, dates, CSS colors, -cryptographic tokens. One static class, two RNGs, all six platforms. +**Random data for Dart.** Numbers, text, names, dates, networking, +CSS colors, cryptographic tokens. One static class, two RNGs, all six +platforms. ```dart Rand.fullName(); // → 'Emma Rodriguez' +Rand.email(); // → 'olivia42@example.com' +Rand.ipv4(); // → '203.0.113.42' Rand.password(); // → 'k9#Mx!pL2@qR' Rand.color(); // → CssColors.coral Rand.dateTime(); // → 2024-03-15 14:32:07.000Z @@ -35,6 +38,10 @@ Rand.sample(from: ['rare', 'common'], count: 10, weights: [1, 100]); production secrets. Defaults may shift across major versions. - **Not a UUID library.** `Rand.nonce()` is opaque base62, not RFC 4122. +For composing structured fixtures (User, Address, Order, paginated responses, +chat history) on top of these primitives, see +[`example/recipes.dart`](example/recipes.dart). + --- ## Install @@ -89,6 +96,31 @@ Rand.charCode(); // base62 code point (int) --- +## Networking + +```dart +Rand.email(); // 'olivia42@example.com' +Rand.email(domain: 'mycompany.io'); // 'james7@mycompany.io' +Rand.ipv4(); // '203.0.113.42' +Rand.ipv6(); // '2001:0db8:85a3:0000:0000:8a2e:0370:7334' +Rand.mac(); // '3a:5f:9c:8e:2d:71' +Rand.mac(separator: '-'); // '3a-5f-9c-8e-2d-71' +Rand.hex(length: 40); // git-SHA-shaped opaque hex +Rand.semver(); // '3.7.42' +Rand.otp(); // '047215' +Rand.slug(); // 'lorem-ipsum-dolor' +``` + +`email` defaults pick from a built-in list of RFC 2606 example/test +TLDs — safe to ship in fixtures without collision risk. `ipv4` is not +filtered for reserved ranges; compose your own filter if you need only +routable addresses. `ipv6` is returned in full form (no `::` collapse) +so fixture length stays stable. `hex` is general-purpose lowercase hex +— git SHAs, ETags, opaque content hashes. `semver`, `otp`, `slug` use +the non-secure RNG and are reproducible under `Rand.seed`. + +--- + ## Cryptographic — secure vs non-secure ```dart @@ -97,6 +129,7 @@ Rand.nonce(); // 16-char base62, always Random.secure() Rand.nonce(length: 32); Rand.password(); // 12-char mixed-charset, always Random.secure() Rand.password(length: 20, symbols: false); +Rand.base64(); // 16 bytes encoded, always Random.secure() Rand.secureCharCode(); // base62 code point, always Random.secure() ``` @@ -122,6 +155,7 @@ Rand.lastName(); // 'Thompson' Rand.fullName(); // 'James Michael Wilson' — 0..2 weighted middle names Rand.alias(); // 'ShadowHunter' Rand.city(); // 'Tokyo' +Rand.geoPoint(); // (lat: 42.36011, lng: -71.05891) — named record ``` Corpora are US/English-leaning. For locale-aware data, reach for @@ -161,25 +195,30 @@ Rand.duration(min: const Duration(days: 1), max: const Duration(days: 30)); ## Collections ```dart +enum Status { active, suspended, deleted } final fruits = ['apple', 'orange', 'lemon', 'grape', 'kiwi']; final scores = {'Alice': 95, 'Bob': 87}; -Rand.element(fruits); // 'orange' -Rand.subSet({1, 2, 3, 4, 5}, 3); // {2, 5, 1} — unique elements -Rand.mapKey(scores); // 'Bob' -Rand.mapValue(scores); // 95 -Rand.mapEntry(scores); // MapEntry('Alice', 95) +Rand.element(fruits); // 'orange' +Rand.enumValue(Status.values); // Status.suspended — typed enum draw +Rand.subSet({1, 2, 3, 4, 5}, 3); // {2, 5, 1} — unique elements +Rand.shuffled(fruits); // ['kiwi', 'grape', 'apple', ...] — copy +Rand.mapKey(scores); // 'Bob' +Rand.mapValue(scores); // 95 +Rand.mapEntry(scores); // MapEntry('Alice', 95) ``` Pick the right call: -| Need | Use | -| ----------------------------------- | -------------------------------- | -| One element | `element(iterable)` | -| N unique elements | `subSet(set, N)` | -| N elements, repeats okay | `sample(from: list, count: N)` | -| N elements with weighted frequency | `sample(..., weights: [...])` | -| One key / value / entry of a Map | `mapKey` / `mapValue` / `mapEntry` | +|Need|Use| +|---|---| +|One element|`element(iterable)`| +|One enum member|`enumValue(MyEnum.values)`| +|N unique elements|`subSet(set, N)`| +|N elements, repeats okay|`sample(from: list, count: N)`| +|N elements with weighted frequency|`sample(..., weights: [...])`| +|Whole list, reordered|`shuffled(list)`| +|One key / value / entry of a Map|`mapKey` / `mapValue` / `mapEntry`| `subSet` requires `Set` — dedupe explicitly with `.toSet()` if your source has duplicates. @@ -237,49 +276,17 @@ Rand.nullable('value', 90); // 90% null --- -## LLM skill - -A tool-agnostic skill ships at -[`skills/dart-rand/SKILL.md`](skills/dart-rand/SKILL.md). The folder name -is `dart-rand` to avoid colliding with other languages' `rand` namespaces -(Go, Rust, etc.). Unlike an always-on rule, the skill activates **only** -when its description matches the current task (file import, asked feature, -keyword), so it stays out of the way for unrelated work. Vendor it into -your agent's skills directory: - -```bash -# Claude Code (user-level) -mkdir -p ~/.claude/skills/dart-rand -curl -L https://raw.githubusercontent.com/esenmx/rand/master/skills/dart-rand/SKILL.md \ - -o ~/.claude/skills/dart-rand/SKILL.md - -# Claude Code (project-level) -mkdir -p .claude/skills/dart-rand -curl -L https://raw.githubusercontent.com/esenmx/rand/master/skills/dart-rand/SKILL.md \ - -o .claude/skills/dart-rand/SKILL.md -``` - -Other agents: copy the SKILL.md body wherever your tool reads -description-triggered context (`.cursor/`, `AGENTS.md` snippets, etc.). - ---- - -## Migrating from 3.x - -v4.0 is a breaking release. Highlights: - -- `CSSColors` → `CssColors`, `.color` → `.argb`. -- `CssColors.isDark` is now a `CssColorsX` extension getter (computed - from `argb` via YIQ luminance), not a stored field. The boundary - values may shift slightly from the v3 hand-curated table. -- `bytes()` and `nonce()` are always secure; the `secure:` parameter is gone. -- `nonce()` now returns true base62 (the v3 implementation was bytes). -- `subSet()` requires `Set` — dedupe with `.toSet()` at the call site. -- `sample()` drops `secure:` — call `Rand.useRng(Random.secure())` first. -- `element([])` throws `StateError` (was a cryptic `RangeError`). -- New `Rand.useRng(Random)` mutator; `seed(N)` is now `useRng(Random(N))`. - -Full table in [CHANGELOG.md](CHANGELOG.md). +## Pitfalls + +|❌|✅| +|---|---| +|`Rand.integer(max: list.length)` — inclusive, can return `list.length`|`Rand.element(list)` or `Rand.integer(max: list.length - 1)`| +|`Rand.seed(42); Rand.password()` — seed never reaches CSPRNG|Build deterministic tokens from your own `Random(42)`| +|`Rand.subSet([1, 2, 2], 2)` — list literal, duplicates collapse|Pass a `Set`: `Rand.subSet({1, 2}, 2)`| +|`Rand.nullable(x, 80)` thinking "80% present"|Arg is `nullChance` — 80% returns **null**. Flip to `Rand.nullable(x, 20)` for 80% present| +|Shipping `Rand.nonce` / `Rand.password` as production secrets|Use `package:cryptography` or a platform keystore| +|`setUpAll(() => Rand.seed(42))` for parallel tests|Use `setUp` — the global RNG is shared| +|`list..shuffle()` to "get a random order" without keeping the original|`Rand.shuffled(list)` — non-mutating copy| --- diff --git a/example/main.dart b/example/main.dart index b0d58ac..50709f0 100644 --- a/example/main.dart +++ b/example/main.dart @@ -21,6 +21,8 @@ void main() { _section('Geo'); print(' Latitude: ${Rand.latitude()}'); print(' Longitude: ${Rand.longitude()}'); + final point = Rand.geoPoint(); + print(' Geo Point: (${point.lat}, ${point.lng})'); print(' City: ${Rand.city()}'); _section('Identity'); @@ -37,11 +39,23 @@ void main() { print(' Paragraph (3 sentences):'); print(' ${_indent(Rand.paragraph(3))}'); + _section('Networking'); + print(' Email: ${Rand.email()}'); + print(' Email: ${Rand.email(domain: 'mycompany.io')}'); + print(' IPv4: ${Rand.ipv4()}'); + print(' IPv6: ${Rand.ipv6()}'); + print(' MAC: ${Rand.mac()}'); + print(' Hex (40): ${Rand.hex(length: 40)}'); + print(' Slug: ${Rand.slug()}'); + print(' Semver: ${Rand.semver()}'); + print(' OTP: ${Rand.otp()}'); + _section('Cryptographic'); print(' Nonce (32): ${Rand.nonce(length: 32)}'); print(' Password: ${Rand.password()}'); print(' Password (no symbols): ${Rand.password(symbols: false)}'); print(' Bytes (8): ${Rand.bytes(8)}'); + print(' Base64 (32): ${Rand.base64(byteLength: 32)}'); _section('Time'); print(' DateTime: ${Rand.dateTime()}'); @@ -68,8 +82,10 @@ void main() { final scores = {'Alice': 95, 'Bob': 87, 'Charlie': 92}; print(' Element: ${Rand.element(fruits)} from $fruits'); print(' SubSet(3): ${Rand.subSet(fruits, 3)}'); + print(' Shuffled: ${Rand.shuffled(fruits.toList())}'); print(' Map Key: ${Rand.mapKey(scores)} from ${scores.keys}'); print(' Map Value: ${Rand.mapValue(scores)} from ${scores.values}'); + print(' Enum: ${Rand.enumValue(_DemoLevel.values)}'); _section('Sampling'); final positions = Rand.sample( @@ -109,3 +125,5 @@ String _indent(String text, [int spaces = 2]) { String _hex(int color) => '#${(color & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toUpperCase()}'; + +enum _DemoLevel { info, warn, error } diff --git a/example/recipes.dart b/example/recipes.dart new file mode 100644 index 0000000..c7e6c80 --- /dev/null +++ b/example/recipes.dart @@ -0,0 +1,165 @@ +// coverage:ignore-file +// Composer recipes — assembling composite fixtures from `rand` primitives. +// +// `rand` exposes flat values by design (no `User`, `Address`, `ApiResponse`). +// This file shows the patterns to reach for when building structured test +// data on top of those primitives. Treat each `_build…` as a copyable seed. +// +// Run: dart run example/recipes.dart +// ignore_for_file: avoid_print + +import 'package:rand/rand.dart'; + +void main() { + Rand.seed(42); // reproducible output for the recipes + + _section('User fixture (record)'); + print(_buildUser()); + + _section('Address fixture'); + print(_buildAddress()); + + _section('Order with line items'); + final order = _buildOrder(itemCount: 3); + print('id=${order.id} total=\$${order.total} items=${order.items.length}'); + for (final line in order.items) { + print(' - ${line.sku} x${line.qty} @\$${line.price}'); + } + + _section('Paginated API response (page 2 of 7)'); + final page = _buildPage<({String id, String name})>( + page: 2, + pageSize: 5, + totalPages: 7, + item: () => (id: Rand.hex(length: 12), name: Rand.fullName()), + ); + print('page ${page.page}/${page.totalPages}, ${page.items.length} items'); + for (final user in page.items) { + print(' ${user.id} ${user.name}'); + } + + _section('Chat history (10 messages, chronological)'); + final chat = _buildChatHistory(messageCount: 10); + for (final msg in chat) { + print(' [${msg.at.toIso8601String()}] ${msg.sender}: ${msg.body}'); + } +} + +// ──────────────────────────────────────────────────────────────────── +// Recipes +// ──────────────────────────────────────────────────────────────────── + +/// User as a named record. Records keep test fixtures ceremony-free. +({ + String id, + String name, + String email, + String? avatar, + DateTime joinedAt, +}) +_buildUser() { + return ( + id: Rand.hex(length: 24), + name: Rand.fullName(), + email: Rand.email(), + // `nullable(x, 20)` → 20% null. Arg is `nullChance`, not `presenceChance`. + avatar: Rand.nullable( + 'https://i.pravatar.cc/${Rand.integer(min: 64, max: 256)}', + 20, + ), + joinedAt: Rand.dateTime(DateTime(2020), DateTime.now()), + ); +} + +({String street, String city, String postalCode, String country}) +_buildAddress() { + return ( + street: '${Rand.integer(min: 1, max: 9999)} ${Rand.word()} St', + city: Rand.city(), + postalCode: Rand.otp(length: 5), + country: Rand.element(const ['US', 'GB', 'DE', 'JP', 'BR']), + ); +} + +class _LineItem { + _LineItem({required this.sku, required this.qty, required this.price}); + + final String sku; + final int qty; + final double price; +} + +class _Order { + _Order({required this.id, required this.items, required this.total}); + + final String id; + final List<_LineItem> items; + final double total; +} + +_Order _buildOrder({required int itemCount}) { + final items = List.generate(itemCount, (_) { + return _LineItem( + sku: Rand.hex(length: 10).toUpperCase(), + qty: Rand.integer(min: 1, max: 5), + price: double.parse(Rand.float(min: 5, max: 200).toStringAsFixed(2)), + ); + }); + final total = items.fold(0, (sum, it) => sum + it.price * it.qty); + return _Order( + id: 'ord_${Rand.hex(length: 12)}', + items: items, + total: double.parse(total.toStringAsFixed(2)), + ); +} + +/// Generic paginated envelope. `item` produces one element of T per call — +/// pass any fixture builder. +({ + int page, + int pageSize, + int totalPages, + int totalItems, + List items, +}) +_buildPage({ + required int page, + required int pageSize, + required int totalPages, + required T Function() item, +}) { + return ( + page: page, + pageSize: pageSize, + totalPages: totalPages, + totalItems: totalPages * pageSize, + items: List.generate(pageSize, (_) => item()), + ); +} + +({String sender, String body, DateTime at}) _chatMessage(String sender) { + return ( + sender: sender, + body: Rand.sentence(), + at: Rand.dateTime( + DateTime.now().subtract(const Duration(hours: 1)), + DateTime.now(), + ), + ); +} + +List<({String sender, String body, DateTime at})> _buildChatHistory({ + required int messageCount, +}) { + // `sample` with equal weights → realistic sender alternation. + final senders = Rand.sample( + from: const ['user', 'assistant'], + count: messageCount, + ); + return [for (final s in senders) _chatMessage(s)] + ..sort((a, b) => a.at.compareTo(b.at)); +} + +void _section(String title) { + print('\n\x1B[33m▸ $title\x1B[0m'); +} diff --git a/lib/rand.dart b/lib/rand.dart index e679313..fe98f80 100644 --- a/lib/rand.dart +++ b/lib/rand.dart @@ -20,6 +20,7 @@ /// ``` library; +import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; @@ -33,6 +34,7 @@ part 'src/_sampling.dart'; part 'src/_text.dart'; part 'src/_identity.dart'; part 'src/_colors.dart'; +part 'src/_networking.dart'; part 'src/rand_impl.dart'; part 'data/alias.dart'; part 'data/cities.dart'; @@ -165,6 +167,20 @@ final class Rand { /// See also: [latitude]. static double longitude([int precision = 5]) => _i.longitude(precision); + /// Random `(lat, lng)` record — composes [latitude] and [longitude]. + /// + /// Returns a named record `({double lat, double lng})` so callers + /// destructure cleanly: + /// + /// ```dart + /// final (:lat, :lng) = Rand.geoPoint(); + /// Rand.geoPoint(precision: 2); // (lat: 42.36, lng: -71.06) + /// ``` + /// + /// See also: [latitude], [longitude]. + static ({double lat, double lng}) geoPoint({int precision = 5}) => + _i.geoPoint(precision: precision); + /// Random base62 character code (int). /// /// Returns a code point from `[0-9A-Za-z]`. Use [String.fromCharCode] @@ -173,6 +189,36 @@ final class Rand { /// See also: [secureCharCode], [base62]. static int charCode() => _i.charCode(); + /// Random semantic-version string `"major.minor.patch"`. + /// + /// Each component independently uniform in `[0, maxX]` (inclusive). + /// + /// ```dart + /// Rand.semver(); // '3.7.42' + /// Rand.semver(maxMajor: 1); // '0.5.91' or '1.2.13' — 0 or 1 only + /// ``` + /// + /// No pre-release suffixes (`-rc.1` etc.) — compose your own if needed. + static String semver({ + int maxMajor = 9, + int maxMinor = 9, + int maxPatch = 99, + }) => + _i.semver(maxMajor: maxMajor, maxMinor: maxMinor, maxPatch: maxPatch); + + /// Random zero-padded decimal OTP code. + /// + /// Each digit drawn uniformly from `[0-9]`. Reproducible under + /// [Rand.seed] — useful for test fixtures, not for real one-time codes. + /// + /// ```dart + /// Rand.otp(); // '047215' + /// Rand.otp(length: 4); // '8203' + /// ``` + /// + /// Throws [ArgumentError] when [length] is less than 1. + static String otp({int length = 6}) => _i.otp(length: length); + /// Cryptographically secure random base62 character code (int). /// /// {@template rand.always_secure} @@ -250,6 +296,24 @@ final class Rand { symbols: symbols, ); + /// Cryptographically secure random base64-encoded string. + /// + /// {@macro rand.always_secure} + /// + /// Encodes [byteLength] random bytes via [base64Encode]. Output length + /// is `4 * ceil(byteLength / 3)` characters (padded with `=`). + /// + /// ```dart + /// Rand.base64(); // 22 chars + '==' for 16 bytes + /// Rand.base64(byteLength: 32); // 44 chars + /// ``` + /// + /// Throws [ArgumentError] when [byteLength] is less than 1. + /// + /// See also: [bytes], [nonce]. + static String base64({int byteLength = 16}) => + _i.base64(byteLength: byteLength); + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Time // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -329,6 +393,31 @@ final class Rand { /// See also: [mapKey], [mapEntry]. static V mapValue(Map from) => _i.mapValue(from); + /// Random value from an enum. + /// + /// Pass `MyEnum.values` — the `T extends Enum` bound makes this + /// type-safe and IDE-discoverable. + /// + /// ```dart + /// enum Status { active, suspended, deleted } + /// Rand.enumValue(Status.values); // Status.suspended + /// ``` + /// + /// See also: [element]. + static T enumValue(List values) => _i.enumValue(values); + + /// Returns a new shuffled copy of [from] — input is not mutated. + /// + /// Uses the global non-cryptographic RNG; reproducible under [Rand.seed]. + /// + /// ```dart + /// Rand.shuffled([1, 2, 3, 4]); // [3, 1, 4, 2] + /// ``` + /// + /// For sampling with replacement, see [sample]. For unique-subset + /// draws, see [subSet]. + static List shuffled(List from) => _i.shuffled(from); + /// Random subset of [count] unique elements from [from]. /// /// ```dart @@ -428,6 +517,22 @@ final class Rand { /// Random article of [count] paragraphs (default 3..7), joined by `"\n\n"`. static String article([int? count]) => _i.article(count); + /// Random URL slug — [wordCount] unique lorem words joined by [separator]. + /// + /// Words never repeat within one slug (drawn via [subSet]). + /// + /// ```dart + /// Rand.slug(); // 'lorem-ipsum-dolor' + /// Rand.slug(wordCount: 5); // 'amet-consectetur-adipiscing-elit-sed' + /// Rand.slug(separator: '_'); // 'lorem_ipsum_dolor' + /// ``` + /// + /// Throws [ArgumentError] when [wordCount] is less than 1. + /// + /// See also: [words]. + static String slug({int wordCount = 3, String separator = '-'}) => + _i.slug(wordCount: wordCount, separator: separator); + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Colors // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -458,6 +563,75 @@ final class Rand { /// See also: [color], [colorDark]. static CssColors colorLight() => _i.colorLight(); + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + // Networking + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + /// Random email address. + /// + /// Synthesises `firstName + 1..99 + '@' + domain`. When [domain] is + /// `null`, picks from a built-in list of test/example TLDs (RFC 2606 + /// safe — no real-world collisions). + /// + /// ```dart + /// Rand.email(); // 'olivia42@example.com' + /// Rand.email(domain: 'mycompany.io'); // 'james7@mycompany.io' + /// ``` + /// + /// See also: [firstName]. + static String email({String? domain}) => _i.email(domain: domain); + + /// Random IPv4 address as a dotted-quad string. + /// + /// Each octet uniform in `[0, 255]`. Output is not filtered for + /// reserved ranges (0.0.0.0/8, 127.0.0.0/8, etc.); compose your own + /// filter if you need only routable addresses. + /// + /// ```dart + /// Rand.ipv4(); // '203.0.113.42' + /// ``` + /// + /// See also: [ipv6]. + static String ipv4() => _i.ipv4(); + + /// Random IPv6 address — 8 hex groups joined by `:`. + /// + /// Returned in full form (no `::` collapse) so length is stable for + /// fixtures. + /// + /// ```dart + /// Rand.ipv6(); // '2001:0db8:85a3:0000:0000:8a2e:0370:7334' + /// ``` + /// + /// See also: [ipv4]. + static String ipv6() => _i.ipv6(); + + /// Random MAC address — 6 hex bytes joined by [separator]. + /// + /// Common separators: `':'` (Unix-style, default), `'-'` (Windows-style). + /// + /// ```dart + /// Rand.mac(); // '3a:5f:9c:8e:2d:71' + /// Rand.mac(separator: '-'); // '3a-5f-9c-8e-2d-71' + /// ``` + static String mac({String separator = ':'}) => _i.mac(separator: separator); + + /// Random lowercase hex string of [length] characters. + /// + /// General-purpose hex: covers git SHAs (`length: 40`), ETags, content + /// hashes, opaque test IDs. Uses the non-secure RNG — reproducible + /// under [Rand.seed]. + /// + /// ```dart + /// Rand.hex(); // 'a3f2c91e' + /// Rand.hex(length: 40); // git-SHA-shaped + /// ``` + /// + /// Throws [ArgumentError] when [length] is less than 1. + /// + /// See also: [nonce] for crypto-secure base62 tokens. + static String hex({int length = 8}) => _i.hex(length: length); + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Sampling // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/lib/src/_collections.dart b/lib/src/_collections.dart index fc8d0f2..fbc99ea 100644 --- a/lib/src/_collections.dart +++ b/lib/src/_collections.dart @@ -37,4 +37,8 @@ mixin _Collections { } return result; } + + T enumValue(List values) => element(values); + + List shuffled(List from) => List.of(from)..shuffle(rng); } diff --git a/lib/src/_crypto.dart b/lib/src/_crypto.dart index cd53903..d59e74f 100644 --- a/lib/src/_crypto.dart +++ b/lib/src/_crypto.dart @@ -39,4 +39,11 @@ mixin _Crypto { chars.codeUnitAt(secureRng.nextInt(chars.length)), ]); } + + String base64({int byteLength = 16}) { + if (byteLength < 1) { + throw ArgumentError('byteLength must be >= 1, got $byteLength'); + } + return base64Encode(bytes(byteLength)); + } } diff --git a/lib/src/_networking.dart b/lib/src/_networking.dart new file mode 100644 index 0000000..272129b --- /dev/null +++ b/lib/src/_networking.dart @@ -0,0 +1,43 @@ +part of '../rand.dart'; + +const List _domains = [ + 'example.com', + 'example.org', + 'example.net', + 'test.com', + 'mail.test', + 'demo.dev', + 'sample.app', + 'fake.io', +]; + +const String _hexChars = '0123456789abcdef'; + +mixin _Networking on _Numbers, _Collections, _Identity { + String email({String? domain}) { + final user = '${firstName().toLowerCase()}${integer(min: 1, max: 99)}'; + return '$user@${domain ?? element(_domains)}'; + } + + String ipv4() { + return List.generate(4, (_) => integer(max: 255)).join('.'); + } + + String ipv6() { + return List.generate(8, (_) => hex(length: 4)).join(':'); + } + + String mac({String separator = ':'}) { + return List.generate(6, (_) => hex(length: 2)).join(separator); + } + + String hex({int length = 8}) { + if (length < 1) { + throw ArgumentError('length must be >= 1, got $length'); + } + return String.fromCharCodes([ + for (var i = 0; i < length; i++) + _hexChars.codeUnitAt(rng.nextInt(16)), + ]); + } +} diff --git a/lib/src/_numbers.dart b/lib/src/_numbers.dart index 6f2a997..3682b5d 100644 --- a/lib/src/_numbers.dart +++ b/lib/src/_numbers.dart @@ -24,7 +24,25 @@ mixin _Numbers { return double.parse(float(min: -180, max: 180).toStringAsFixed(precision)); } + ({double lat, double lng}) geoPoint({int precision = 5}) { + return (lat: latitude(precision), lng: longitude(precision)); + } + int charCode() => rng.charCode(); int secureCharCode() => secureRng.charCode(); + + String semver({int maxMajor = 9, int maxMinor = 9, int maxPatch = 99}) { + final major = integer(max: maxMajor); + final minor = integer(max: maxMinor); + final patch = integer(max: maxPatch); + return '$major.$minor.$patch'; + } + + String otp({int length = 6}) { + if (length < 1) { + throw ArgumentError('length must be >= 1, got $length'); + } + return List.generate(length, (_) => rng.nextInt(10)).join(); + } } diff --git a/lib/src/_text.dart b/lib/src/_text.dart index 643b393..1f5bdf4 100644 --- a/lib/src/_text.dart +++ b/lib/src/_text.dart @@ -19,4 +19,11 @@ mixin _Text on _Collections, _Numbers { final n = count ?? integer(min: 3, max: 7); return List.generate(n, (_) => paragraph()).join('\n\n'); } + + String slug({int wordCount = 3, String separator = '-'}) { + if (wordCount < 1) { + throw ArgumentError('wordCount must be >= 1, got $wordCount'); + } + return subSet(_words.toSet(), wordCount).join(separator); + } } diff --git a/lib/src/rand_impl.dart b/lib/src/rand_impl.dart index f9ec9cb..6273125 100644 --- a/lib/src/rand_impl.dart +++ b/lib/src/rand_impl.dart @@ -10,7 +10,8 @@ class _RandImpl _Sampling, _Text, _Identity, - _Colors { + _Colors, + _Networking { @override Random rng = Random(); diff --git a/pubspec.yaml b/pubspec.yaml index ecd5f72..80f5415 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: rand description: >- Random data generator for Dart. Numbers, text, names, dates, CSS colors, cryptographic tokens. Static API, seedable, pure Dart, all platforms. -version: 4.0.0 +version: 4.1.0 homepage: https://mehmetesen.com repository: https://github.com/esenmx/rand issue_tracker: https://github.com/esenmx/rand/issues diff --git a/skills/dart-rand/SKILL.md b/skills/dart-rand/SKILL.md index a304f26..1dc8625 100644 --- a/skills/dart-rand/SKILL.md +++ b/skills/dart-rand/SKILL.md @@ -1,6 +1,6 @@ --- name: dart-rand -description: Use the `rand` Dart package correctly when generating random test data, fixtures, mock values, names, dates, CSS colors, weighted samples, or cryptographic tokens for testing. Covers the secure vs non-secure RNG split, `Rand.useRng` / `Rand.seed` semantics, `subSet` vs `sample` (without vs with replacement, weighted vs uniform), `CssColors` ARGB usage and the computed `isDark` extension, and common misuse patterns (off-by-one on `integer(max:)`, seeding crypto methods, using `nonce()` as a production secret). Trigger when the user's file imports `package:rand/rand.dart`, when the user asks for "random test data", "fake names", "test fixtures", "Lorem ipsum", "loot box weights", "weighted sampling", "CSS colors", or "secure tokens for tests". Skip for production secret/password generation (use `package:cryptography` or platform keystore), RFC 4122 UUIDs (use `package:uuid`), realistic locale-aware fake data (use `package:faker`), or a single `dart:math.Random.nextInt` call (no dep needed). +description: Generate random test data and fixtures in Dart via `package:rand/rand.dart` — names, emails, IPv4/IPv6/MAC, hex, slugs, OTP, semver, lorem, CSS colors, weighted sampling, geo points, crypto tokens. Use when importing `package:rand`, building Dart test fixtures, mocking API responses, seeding demos, or the user asks for fake/random/seeded data in Dart. Skip for production secrets (use `package:cryptography`), UUIDs (use `package:uuid`), or locale-aware names (use `package:faker`). --- # rand @@ -9,10 +9,10 @@ description: Use the `rand` Dart package correctly when generating random test d ## Two-RNG split — load-bearing -| RNG | Methods | Reset by `useRng` / `seed`? | +|RNG|Methods|Reset by `useRng` / `seed`?| |---|---|---| -| `Random` (replaceable) | everything not in the next row | yes | -| `Random.secure()` (fixed) | `password`, `nonce`, `bytes`, `secureCharCode` | **no** | +|`Random` (replaceable)|everything not in the next row|yes| +|`Random.secure()` (fixed)|`password`, `nonce`, `bytes`, `secureCharCode`|**no**| Consequences: @@ -22,22 +22,66 @@ Consequences: ## Picking the right call -| Goal | Use | +|Goal|Use| |---|---| -| One element | `Rand.element(iterable)` | -| N unique elements | `Rand.subSet(set, N)` — requires `Set` | -| N elements, repeats okay | `Rand.sample(from: list, count: N)` | -| Weighted draws | `Rand.sample(..., weights: [...])` | -| Map key / value / entry | `Rand.mapKey` / `mapValue` / `mapEntry` | -| Maybe-null fixture field | `Rand.nullable(value, chance)` | +|One element|`Rand.element(iterable)`| +|One enum member|`Rand.enumValue(MyEnum.values)`| +|N unique elements|`Rand.subSet(set, N)` — requires `Set`| +|N elements, repeats okay|`Rand.sample(from: list, count: N)`| +|Weighted draws|`Rand.sample(..., weights: [...])`| +|Whole list, reordered|`Rand.shuffled(list)` — non-mutating copy| +|Map key / value / entry|`Rand.mapKey` / `mapValue` / `mapEntry`| +|Maybe-null fixture field|`Rand.nullable(value, chance)`| `subSet` is `Set` only — dedupe explicitly with `.toSet()`. `weights.length >= from.length` for `sample`. +## Common tasks + +Recipe shapes that show up in fixture / mock requests. `rand` is intentionally flat — compose the primitives. Full composer examples (User, Address, Order, Paginated, ChatHistory) live in [`example/recipes.dart`](../../example/recipes.dart). + +|Prompt|Recipe| +|---|---| +|*"10 fake users"*|`List.generate(10, (_) => (id: Rand.hex(length: 24), name: Rand.fullName(), email: Rand.email()))`| +|*"Paginated response, 5 items"*|`(page: 1, totalPages: 7, items: List.generate(5, (_) => buildItem()))`| +|*"Loot box: 1% legendary, 10% rare, 90% common"*|`Rand.sample(from: ['L','R','C'], count: 100, weights: [1, 10, 100])`| +|*"Timestamps for the past hour, sorted"*|`[for (var i = 0; i < 20; i++) Rand.dateTime(DateTime.now().subtract(const Duration(hours: 1)), DateTime.now())]..sort((a, b) => a.compareTo(b))`| +|*"Reproducible fixture I can rerun"*|`setUp(() => Rand.seed(42));` — non-crypto reproduces; `password`/`nonce`/`bytes`/`base64` stay live CSPRNG| +|*"Opaque session/auth token for tests"*|`Rand.base64(byteLength: 32)` — crypto-secure source| +|*"Git-style SHA"*|`Rand.hex(length: 40)`| +|*"Mostly-present nullable field"*|`Rand.nullable(value, 20)` — 20% null (arg is `nullChance`, **not** `presenceChance`)| +|*"Pick a random enum"*|`Rand.enumValue(MyEnum.values)`| +|*"Shuffle a list non-destructively"*|`Rand.shuffled(list)` — returns a copy; `list..shuffle()` mutates| + ## Bounds - `Rand.integer({min, max})` — **inclusive both ends**. - `Rand.float`, `Rand.duration`, `Rand.dateTime` — `[min, max)` half-open. +## Networking primitives + +```dart +Rand.email(); // 'olivia42@example.com' — RFC 2606 safe TLDs +Rand.email(domain: 'mycompany.io'); +Rand.ipv4(); // not filtered for reserved ranges +Rand.ipv6(); // full form, no `::` collapse +Rand.mac({separator: ':'}); // default colon; '-' also common +Rand.hex({length: 8}); // generic lowercase hex — git SHAs (length: 40), ETags +Rand.semver(); // 'major.minor.patch', no pre-release suffix +Rand.otp({length: 6}); // zero-padded decimal digits +Rand.slug({wordCount: 3}); // unique lorem words, '-' separator +``` + +All use the non-secure RNG — reproducible under `Rand.seed`. `Rand.base64({byteLength: 16})` is the crypto-secure parallel for opaque payload fixtures. + +## Geo + +```dart +final (:lat, :lng) = Rand.geoPoint(); // named record +Rand.geoPoint(precision: 2); // (lat: 42.36, lng: -71.06) +``` + +`geoPoint` composes `latitude` + `longitude` — same precision contract. + ## Colors `Rand.color()` / `colorDark()` / `colorLight()` → `CssColors` (148 variants). `.argb` is a 32-bit ARGB int (`Color(c.argb)` in Flutter). `.isDark` is a `CssColorsX` extension getter — computed YIQ luminance, not stored. For proper Flutter contrast picking, use `fluiver`'s `Color.contrastText`. diff --git a/test/rand_test.dart b/test/rand_test.dart index 400cb94..8eca46e 100644 --- a/test/rand_test.dart +++ b/test/rand_test.dart @@ -1,9 +1,12 @@ +import 'dart:convert'; import 'dart:math'; import 'package:checks/checks.dart'; import 'package:rand/rand.dart'; import 'package:test/test.dart'; +enum _TestEnum { alpha, beta, gamma, delta } + void main() { setUp(() => Rand.seed(42)); @@ -145,6 +148,16 @@ void main() { } }); + test('geoPoint returns record in valid lat/lng range', () { + for (var i = 0; i < 100; i++) { + final (:lat, :lng) = Rand.geoPoint(); + check(lat).isGreaterOrEqual(-90); + check(lat).isLessOrEqual(90); + check(lng).isGreaterOrEqual(-180); + check(lng).isLessOrEqual(180); + } + }); + test('charCode returns base62 character', () { for (var i = 0; i < 1000; i++) { final c = Rand.charCode(); @@ -159,6 +172,42 @@ void main() { check(Rand.bytes(10)).length.equals(10); check(Rand.bytes(100)).length.equals(100); }); + + test('semver returns dotted triple within bounds', () { + final pattern = RegExp(r'^(\d+)\.(\d+)\.(\d+)$'); + for (var i = 0; i < 100; i++) { + final v = Rand.semver(maxMajor: 3, maxMinor: 4, maxPatch: 5); + final m = pattern.firstMatch(v); + check(m).isNotNull(); + check(int.parse(m!.group(1)!)).isLessOrEqual(3); + check(int.parse(m.group(2)!)).isLessOrEqual(4); + check(int.parse(m.group(3)!)).isLessOrEqual(5); + } + }); + + test('otp returns digit string of requested length', () { + const digits = '0123456789'; + for (final len in [1, 6, 12]) { + final code = Rand.otp(length: len); + check(code).length.equals(len); + for (var i = 0; i < code.length; i++) { + check(digits.contains(code[i])).isTrue(); + } + } + }); + + test('otp throws on non-positive length', () { + check(() => Rand.otp(length: 0)).throws(); + check(() => Rand.otp(length: -3)).throws(); + }); + + test('otp is reproducible under seed', () { + Rand.seed(42); + final a = List.generate(20, (_) => Rand.otp()); + Rand.seed(42); + final b = List.generate(20, (_) => Rand.otp()); + check(a).deepEquals(b); + }); }); group('Cryptographic', () { @@ -211,6 +260,25 @@ void main() { ), ).throws(); }); + + test('base64 decodes to byteLength bytes', () { + for (final len in [1, 16, 32, 64]) { + final encoded = Rand.base64(byteLength: len); + check(base64Decode(encoded)).length.equals(len); + } + }); + + test('base64 throws on non-positive byteLength', () { + check(() => Rand.base64(byteLength: 0)).throws(); + }); + + test('seed does not affect base64', () { + Rand.seed(42); + final a = Rand.base64(); + Rand.seed(42); + final b = Rand.base64(); + check(a).not((it) => it.equals(b)); + }); }); group('Time', () { @@ -282,6 +350,36 @@ void main() { test('subSet throws when count exceeds set size', () { check(() => Rand.subSet({1, 2}, 3)).throws(); }); + + test('enumValue returns a member of the enum', () { + for (var i = 0; i < 50; i++) { + check(_TestEnum.values.contains(Rand.enumValue(_TestEnum.values))) + .isTrue(); + } + }); + + test('shuffled returns same elements in a different order', () { + final input = List.generate(50, (i) => i); + final out = Rand.shuffled(input); + check(out).length.equals(input.length); + check(out.toSet()).deepEquals(input.toSet()); + check(out).not((it) => it.deepEquals(input)); + }); + + test('shuffled does not mutate input', () { + final input = [1, 2, 3, 4, 5]; + final snapshot = List.of(input); + Rand.shuffled(input); + check(input).deepEquals(snapshot); + }); + + test('shuffled is reproducible under seed', () { + Rand.seed(42); + final a = Rand.shuffled(List.generate(20, (i) => i)); + Rand.seed(42); + final b = Rand.shuffled(List.generate(20, (i) => i)); + check(a).deepEquals(b); + }); }); group('Text', () { @@ -339,6 +437,20 @@ void main() { check(a).isNotEmpty(); check(a.split('\n\n').length).isGreaterOrEqual(3); }); + + test('slug returns wordCount unique words joined by separator', () { + for (final wc in [1, 3, 5]) { + final s = Rand.slug(wordCount: wc); + final parts = s.split('-'); + check(parts.length).equals(wc); + check(parts.toSet().length).equals(wc); + } + check(Rand.slug(separator: '_').split('_')).length.equals(3); + }); + + test('slug throws on non-positive wordCount', () { + check(() => Rand.slug(wordCount: 0)).throws(); + }); }); group('Miscellaneous', () { @@ -379,6 +491,81 @@ void main() { }); }); + group('Networking', () { + test('email contains @ and a domain', () { + final ipv4Pattern = RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'); + for (var i = 0; i < 50; i++) { + final e = Rand.email(); + check(e.contains('@')).isTrue(); + final parts = e.split('@'); + check(parts.length).equals(2); + check(parts[0]).isNotEmpty(); + check(parts[1]).isNotEmpty(); + check(ipv4Pattern.hasMatch(parts[1])).isFalse(); + } + }); + + test('email honors explicit domain', () { + for (var i = 0; i < 20; i++) { + check(Rand.email(domain: 'mycompany.io')).endsWith('@mycompany.io'); + } + }); + + test('ipv4 is dotted quad with each octet in 0..255', () { + final pattern = RegExp(r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$'); + for (var i = 0; i < 200; i++) { + final ip = Rand.ipv4(); + final m = pattern.firstMatch(ip); + check(m).isNotNull(); + for (var g = 1; g <= 4; g++) { + final octet = int.parse(m!.group(g)!); + check(octet).isGreaterOrEqual(0); + check(octet).isLessOrEqual(255); + } + } + }); + + test('ipv6 is 8 groups of 4 lowercase hex chars', () { + final pattern = RegExp(r'^([0-9a-f]{4}:){7}[0-9a-f]{4}$'); + for (var i = 0; i < 100; i++) { + check(pattern.hasMatch(Rand.ipv6())).isTrue(); + } + }); + + test('mac is 6 hex bytes with configurable separator', () { + final colon = RegExp(r'^([0-9a-f]{2}:){5}[0-9a-f]{2}$'); + final dash = RegExp(r'^([0-9a-f]{2}-){5}[0-9a-f]{2}$'); + for (var i = 0; i < 50; i++) { + check(colon.hasMatch(Rand.mac())).isTrue(); + check(dash.hasMatch(Rand.mac(separator: '-'))).isTrue(); + } + }); + + test('hex returns lowercase hex of requested length', () { + const hexPool = '0123456789abcdef'; + for (final len in [1, 8, 40, 64, 128]) { + final h = Rand.hex(length: len); + check(h).length.equals(len); + for (var i = 0; i < h.length; i++) { + check(hexPool.contains(h[i])).isTrue(); + } + } + }); + + test('hex throws on non-positive length', () { + check(() => Rand.hex(length: 0)).throws(); + check(() => Rand.hex(length: -1)).throws(); + }); + + test('hex is reproducible under seed', () { + Rand.seed(42); + final a = List.generate(20, (_) => Rand.hex(length: 16)); + Rand.seed(42); + final b = List.generate(20, (_) => Rand.hex(length: 16)); + check(a).deepEquals(b); + }); + }); + group('Sampling', () { test('sample without weights uses equal probability', () { final result = Rand.sample(from: [1, 2, 3], count: 100); From c43938f39517bf48b3d7951847ad2a4591799ac4 Mon Sep 17 00:00:00 2001 From: Mehmet Esen Date: Thu, 21 May 2026 09:50:21 +0300 Subject: [PATCH 2/3] style: dart format example/recipes.dart + _networking.dart Co-Authored-By: Claude Opus 4.7 (1M context) --- example/recipes.dart | 8 +++----- lib/src/_networking.dart | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/example/recipes.dart b/example/recipes.dart index c7e6c80..1d6f21a 100644 --- a/example/recipes.dart +++ b/example/recipes.dart @@ -56,8 +56,7 @@ void main() { String email, String? avatar, DateTime joinedAt, -}) -_buildUser() { +}) _buildUser() { return ( id: Rand.hex(length: 24), name: Rand.fullName(), @@ -72,7 +71,7 @@ _buildUser() { } ({String street, String city, String postalCode, String country}) -_buildAddress() { + _buildAddress() { return ( street: '${Rand.integer(min: 1, max: 9999)} ${Rand.word()} St', city: Rand.city(), @@ -121,8 +120,7 @@ _Order _buildOrder({required int itemCount}) { int totalPages, int totalItems, List items, -}) -_buildPage({ +}) _buildPage({ required int page, required int pageSize, required int totalPages, diff --git a/lib/src/_networking.dart b/lib/src/_networking.dart index 272129b..a8b7aac 100644 --- a/lib/src/_networking.dart +++ b/lib/src/_networking.dart @@ -36,8 +36,7 @@ mixin _Networking on _Numbers, _Collections, _Identity { throw ArgumentError('length must be >= 1, got $length'); } return String.fromCharCodes([ - for (var i = 0; i < length; i++) - _hexChars.codeUnitAt(rng.nextInt(16)), + for (var i = 0; i < length; i++) _hexChars.codeUnitAt(rng.nextInt(16)), ]); } } From be68bad3fa8cb0451d14de6ea5431544b3cf1601 Mon Sep 17 00:00:00 2001 From: Mehmet Esen Date: Thu, 21 May 2026 09:52:00 +0300 Subject: [PATCH 3/3] chore: refresh pubspec description with v4.1 surface Adds networking, emails, weighted samples to the description. Co-Authored-By: Claude Opus 4.7 (1M context) --- pubspec.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 80f5415..a696b8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,8 @@ name: rand description: >- - Random data generator for Dart. Numbers, text, names, dates, CSS colors, - cryptographic tokens. Static API, seedable, pure Dart, all platforms. + Random data generator for Dart. Names, emails, networking (IPv4/IPv6/MAC), + text, dates, CSS colors, weighted samples, crypto tokens. Static, seedable, + pure Dart, all platforms. version: 4.1.0 homepage: https://mehmetesen.com repository: https://github.com/esenmx/rand