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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ default = []

# Domain modules — opt-in so you only pay for what you use
contact = ["dep:once_cell", "dep:regex", "dep:url"]
identifiers = []
primitives = ["dep:rust_decimal", "dep:base64"]

# Cross-cutting concerns can be combined with any module
Expand All @@ -28,6 +29,7 @@ sql = ["dep:sqlx"]
# Everything at once
full = [
"contact",
"identifiers",
"primitives",
]

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ let parsed: EmailAddress = serde_json::from_str(r#""hello@example.com""#)?;
| Feature | Highlights | Types | Status |
|:---|:---|:---:|:---:|
| `contact` | `EmailAddress`, `PhoneNumber`, `CountryCode`, `PostalAddress`, `Website` | 5 | 5 / 5 ✅ |
| `identifiers` | `Slug`, `Ean13`, `Isbn13`, `Vin` | 7 | 0 / 7 |
| `identifiers` | `Slug`, `Ean13`, `Isbn13`, `Vin` | 7 | 7 / 7 |
| `finance` | `Money`, `Iban`, `Bic`, `VatNumber`, `CreditCardNumber` | 9 | 0 / 9 |
| `temporal` | `BirthDate`, `ExpiryDate`, `TimeRange`, `BusinessHours` | 5 | 0 / 5 |
| `geo` | `Latitude`, `Longitude`, `Coordinate`, `BoundingBox`, `TimeZone` | 6 | 0 / 6 |
Expand Down
18 changes: 9 additions & 9 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@

| Type | Status | Notes |
|---|---|---|
| `Slug` | | lowercase, alphanumeric + hyphens, no leading/trailing hyphens |
| `Ean13` | | EAN-13 barcode with checksum validation |
| `Ean8` | | EAN-8 barcode with checksum validation |
| `Isbn13` | | ISBN-13 with check digit |
| `Isbn10` | | ISBN-10 with check digit |
| `Issn` | | ISSN with check digit |
| `Vin` | | Vehicle Identification Number, 17 chars, checksum validated |
| `Slug` | | lowercase, alphanumeric + hyphens, no leading/trailing hyphens |
| `Ean13` | | EAN-13 barcode with checksum validation |
| `Ean8` | | EAN-8 barcode with checksum validation |
| `Isbn13` | | ISBN-13 with check digit |
| `Isbn10` | | ISBN-10 with check digit |
| `Issn` | | ISSN with check digit |
| `Vin` | | Vehicle Identification Number, 17 chars, checksum validated |

---

Expand Down Expand Up @@ -135,11 +135,11 @@
| Feature | Total | Done | Remaining |
|---|---|---|---|
| `contact` | 5 | 5 | 0 |
| `identifiers` | 7 | 0 | 7 |
| `identifiers` | 7 | 7 | 0 |
| `finance` | 9 | 0 | 9 |
| `temporal` | 5 | 0 | 5 |
| `geo` | 6 | 0 | 6 |
| `net` | 10 | 0 | 10 |
| `measurement` | 10 | 0 | 10 |
| `primitives` | 10 | 10 | 0 |
| **Total** | **62** | **15** | **47** |
| **Total** | **62** | **22** | **40** |
253 changes: 253 additions & 0 deletions docs/identifiers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# identifiers module

Feature flag: `identifiers`

```toml
[dependencies]
arvo = { version = "0.3", features = ["identifiers"] }
```

---

## Slug

A URL-safe slug: lowercase alphanumeric characters and hyphens only.

**Normalisation:** trimmed, lowercased.
**Validation:** non-empty; only `[a-z0-9-]`; no leading, trailing, or consecutive hyphens.

```rust,ignore
use arvo::identifiers::Slug;
use arvo::traits::ValueObject;

let slug = Slug::new("Hello-World".into())?;
assert_eq!(slug.value(), "hello-world");

assert!(Slug::new("-bad".into()).is_err());
assert!(Slug::new("has--double".into()).is_err());

let s: Slug = "my-slug".try_into()?;
```

### Accessors

| Method | Returns | Example |
|---|---|---|
| `value()` | `&String` | `"hello-world"` |
| `into_inner()` | `String` | `"hello-world"` |

### Errors

| Input | Error |
|---|---|
| `""` | `ValidationError::Empty` |
| `"-bad"` | `ValidationError::InvalidFormat` |
| `"has--double"` | `ValidationError::InvalidFormat` |
| `"has space"` | `ValidationError::InvalidFormat` |

---

## Ean13

A validated EAN-13 barcode number.

**Normalisation:** spaces and hyphens stripped; only digits retained.
**Validation:** exactly 13 digits; check digit valid per GS1 algorithm (alternating weights from right, total mod 10 == 0).

```rust,ignore
use arvo::identifiers::Ean13;
use arvo::traits::ValueObject;

let ean = Ean13::new("4006381333931".into())?;
assert_eq!(ean.value(), "4006381333931");
assert_eq!(ean.check_digit(), 1);
```

### Accessors

| Method | Returns | Example |
|---|---|---|
| `value()` | `&String` | `"4006381333931"` |
| `check_digit()` | `u8` | `1` |
| `into_inner()` | `String` | `"4006381333931"` |

### Errors

| Input | Error |
|---|---|
| wrong digit count | `ValidationError::InvalidFormat` |
| invalid checksum | `ValidationError::InvalidFormat` |

---

## Ean8

A validated EAN-8 barcode number.

**Normalisation:** spaces and hyphens stripped; only digits retained.
**Validation:** exactly 8 digits; check digit valid per GS1 algorithm.

```rust,ignore
use arvo::identifiers::Ean8;
use arvo::traits::ValueObject;

let ean = Ean8::new("73513537".into())?;
assert_eq!(ean.value(), "73513537");
assert_eq!(ean.check_digit(), 7);
```

### Accessors

| Method | Returns | Example |
|---|---|---|
| `value()` | `&String` | `"73513537"` |
| `check_digit()` | `u8` | `7` |
| `into_inner()` | `String` | `"73513537"` |

### Errors

| Input | Error |
|---|---|
| wrong digit count | `ValidationError::InvalidFormat` |
| invalid checksum | `ValidationError::InvalidFormat` |

---

## Isbn13

A validated ISBN-13 number.

**Normalisation:** hyphens and spaces stripped; output is 13 bare digits.
**Validation:** exactly 13 digits; must start with `978` or `979`; check digit valid per EAN-13 algorithm.

```rust,ignore
use arvo::identifiers::Isbn13;
use arvo::traits::ValueObject;

let isbn = Isbn13::new("978-0-306-40615-7".into())?;
assert_eq!(isbn.value(), "9780306406157");
assert_eq!(isbn.prefix(), "978");
```

### Accessors

| Method | Returns | Example |
|---|---|---|
| `value()` | `&String` | `"9780306406157"` |
| `prefix()` | `&str` | `"978"` or `"979"` |
| `into_inner()` | `String` | `"9780306406157"` |

### Errors

| Input | Error |
|---|---|
| wrong digit count | `ValidationError::InvalidFormat` |
| wrong prefix | `ValidationError::InvalidFormat` |
| invalid checksum | `ValidationError::InvalidFormat` |

---

## Isbn10

A validated ISBN-10 number.

**Normalisation:** hyphens and spaces stripped; trailing `x` uppercased to `X`; output is 10 bare characters.
**Validation:** exactly 10 characters (9 digits + check `0–9` or `X`); validated using ISBN-10 weighted sum (weights 10 down to 2, total mod 11 == 0; `X` = 10).

```rust,ignore
use arvo::identifiers::Isbn10;
use arvo::traits::ValueObject;

let isbn = Isbn10::new("0-306-40615-2".into())?;
assert_eq!(isbn.value(), "0306406152");

let isbn_x = Isbn10::new("047191536x".into())?;
assert_eq!(isbn_x.value(), "047191536X");
```

### Accessors

| Method | Returns | Example |
|---|---|---|
| `value()` | `&String` | `"0306406152"` |
| `into_inner()` | `String` | `"0306406152"` |

### Errors

| Input | Error |
|---|---|
| wrong length | `ValidationError::InvalidFormat` |
| invalid checksum | `ValidationError::InvalidFormat` |

---

## Issn

A validated ISSN (International Standard Serial Number).

**Normalisation:** spaces and hyphens stripped; trailing `x` uppercased; output formatted as `XXXX-XXXX`.
**Validation:** exactly 8 characters (7 digits + check `0–9` or `X`); validated using ISSN weighted sum (weights 8 down to 2, total mod 11 == 0; `X` = 10).

```rust,ignore
use arvo::identifiers::Issn;
use arvo::traits::ValueObject;

let issn = Issn::new("0317-8471".into())?;
assert_eq!(issn.value(), "0317-8471");

let issn_x = Issn::new("0000006x".into())?;
assert_eq!(issn_x.value(), "0000-006X");
```

### Accessors

| Method | Returns | Example |
|---|---|---|
| `value()` | `&String` | `"0317-8471"` |
| `into_inner()` | `String` | `"0317-8471"` |

### Errors

| Input | Error |
|---|---|
| wrong length | `ValidationError::InvalidFormat` |
| invalid checksum | `ValidationError::InvalidFormat` |

---

## Vin

A validated Vehicle Identification Number (VIN) per ISO 3779.

**Normalisation:** trimmed, uppercased.
**Validation:** exactly 17 characters from the VIN alphabet (letters and digits; `I`, `O`, `Q` forbidden); check digit at position 9 (1-indexed) validated via the standard transliteration table and positional weights (mod 11; `X` = 10).

```rust,ignore
use arvo::identifiers::Vin;
use arvo::traits::ValueObject;

let vin = Vin::new("1HGBH41JXMN109186".into())?;
assert_eq!(vin.wmi(), "1HG");
assert_eq!(vin.vds(), "BH41JX");
assert_eq!(vin.vis(), "MN109186");
assert_eq!(vin.model_year(), 'M');
```

### Accessors

| Method | Returns | Example |
|---|---|---|
| `value()` | `&String` | `"1HGBH41JXMN109186"` |
| `wmi()` | `&str` | `"1HG"` (World Manufacturer Identifier) |
| `vds()` | `&str` | `"BH41JX"` (Vehicle Descriptor Section) |
| `vis()` | `&str` | `"MN109186"` (Vehicle Identifier Section) |
| `model_year()` | `char` | `'M'` |
| `into_inner()` | `String` | `"1HGBH41JXMN109186"` |

### Errors

| Input | Error |
|---|---|
| wrong length | `ValidationError::InvalidFormat` |
| contains `I`, `O`, or `Q` | `ValidationError::InvalidFormat` |
| invalid check digit | `ValidationError::InvalidFormat` |
Loading
Loading