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
151 changes: 151 additions & 0 deletions src/content/library/constructing-identifiers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
title: "Constructing Identifiers"
description: "Build identifiers programmatically with the pubid gem."
---

# Constructing Identifiers

There are three ways to obtain an identifier: parsing a printed reference,
rebuilding from a serialized hash, and constructing from typed attributes.
The v1 entry point — `Pubid::Iso::Identifier.create(...)` — no longer exists;
the v2 constructor is `.new` with keyword arguments.

## Parsing

`Pubid.parse` routes on the leading prefix token (longest match first), so the
right grammar is chosen even when the caller does not know the flavor:

```ruby
require 'pubid'

Pubid.parse("ISO 8601-1:2019").class # => Pubid::Iso::Identifiers::InternationalStandard
Pubid.parse("JIS Z 8301:2019").class # => Pubid::Jis::Identifier
```

When the flavor is known, name it directly: `Pubid::Iso.parse("ISO 9001:2015")`.

## The constructor

`Pubid::<Flavor>::Identifier.new` takes one keyword argument per declared
attribute, coerces scalars into the components the attributes declare, and
**raises `ArgumentError` naming any key the class does not declare** — a
silently-dropped key would otherwise produce an identifier with no year and
no diagnostic.

```ruby
Pubid::Iso::Identifier.new(number: "8601", part: "1", year: "2019")
# => ISO 8601-1:2019
```

For a non-default document type, construct the concrete class — the class is
the type:

```ruby
Pubid::Iso::Identifiers::TechnicalReport.new(number: 123).to_s
# => "ISO/TR 123"
```

Scalars coerce into degenerate components — `year:`, `month:`, `day:` fold
into the date, `edition: 2` wraps into the edition component, and IEC accepts
`languages: ["en"]` and `copublishers: ["ISO"]`:

```ruby
Pubid::Iec::Identifiers::InternationalStandard.new(
number: "1000", part: "1", year: 2023, edition: 2, languages: ["en"],
).to_s # => "IEC 1000-1:2023 ED2(en)"
```

The coercion is type-aware: a flavor that declares an attribute as a plain
string keeps the string, and `number` is never wrapped.

## Stages (ISO)

ISO stages are typed stages — a stage and document type combined (`WD`,
`DIS`, `FDIS`, `WD Amd`, `DAM`, `FDCor`). Look the registry entry up and set
it:

```ruby
Pubid::Iso::Identifiers::InternationalStandard.new(
number: 123, typed_stage: Pubid::Iso.locate_stage("WD"),
).to_s # => "ISO/WD 123"

ts = Pubid::Iso.locate_stage("FDIS")
ts.abbreviation # => "FDIS"
ts.name # => "Final Draft International Standard"
```

ISO type and stage are derived from `typed_stage` — there is no separate
`type:` or `stage:` keyword to set. The stage iteration (`.2` in
`ISO/DIS 123-1.2`) is its own keyword, `stage_iteration:`.

## Supplements with a base

A supplement wraps the document it modifies under the uniform `base`
attribute; the supplement is its own class with the ordinal in `number`:

```ruby
base = Pubid::Iso::Identifiers::InternationalStandard.new(
number: "123", part: "1", year: 2019,
)

Pubid::Iso::Identifiers::Amendment.new(number: 1, year: 2021, base: base).to_s
# => "ISO 123-1:2019/Amd 1:2021"

Pubid::Iso::Identifiers::Corrigendum.new(number: "1", year: "2021", base: base).to_s
# => "ISO 123-1:2019/Cor 1:2021"
```

IEC renders the compact supplement face:

```ruby
base = Pubid::Iec::Identifiers::InternationalStandard.new(
number: "60050", part: "351", year: 2013,
)
Pubid::Iec::Identifiers::Amendment.new(number: "1", year: 2016, base: base).to_s
# => "IEC 60050-351:2013/AMD1:2016"
```

IEEE carries its identity in a `code:` keyword:

```ruby
Pubid::Ieee::Identifier.new(code: "802.11", year: 2020).to_s
# => "IEEE Std 802.11-2020"
```

## Copublishers

Joint publications are modeled per flavor. IEC coerces copublisher scalars;
ISO takes its own `Publisher` component:

```ruby
Pubid::Iec::Identifiers::InternationalStandard.new(
number: "1000", copublishers: ["ISO"],
).to_s # => "IEC/ISO 1000"

Pubid::Iso::Identifier.new(
publisher: Pubid::Iso::Components::Publisher.new(copublisher: ["IEC"]),
number: 123,
).to_s # => "ISO/IEC 123"
```

## from_hash

`#to_hash` emits the canonical hash, and `Pubid.from_hash` rebuilds the
concrete class from it — reading the `_type` key
(`pubid:<flavor>:<type>`), so the caller never names the flavor:

```ruby
id = Pubid::Iso.parse("ISO 9001:2015/Amd 1:2020/Cor 1:2021")
Pubid.from_hash(id.to_hash).to_s # => "ISO 9001:2015/Amd 1:2020/Cor 1:2021"
```

The round-trip invariant `from_hash(id.to_hash) == id` holds for every flavor
— equality is what index lookups are made of, so a construction path that
holds a value the other path does not would break matching.

## Errors

`Pubid.parse` raises `Pubid::Errors::ParseError` (a `Parslet::ParseFailed`)
for an unparseable string; unknown constructor keys raise `ArgumentError`;
a hash with no or unknown `_type` raises `Pubid::Errors::InvalidInputError`.
Rescue `Pubid::Errors::Error` to catch all of them.
1 change: 1 addition & 0 deletions src/content/library/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ The library uses a layered architecture:

## Next Steps

- [Constructing Identifiers](/library/constructing-identifiers)
- [Quick Start Guide](/library/quick-start)
- [API Reference](/library/api)
- [Contributing](/library/contributing)
6 changes: 4 additions & 2 deletions src/data/__tests__/registry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import type { Publisher, Category } from '~/data/types'

describe('registry', () => {
it('contains exactly the expected publisher count', () => {
// 38 = 37 (post-Astro-cutover baseline on main) + CalConnect (added 2026-07-16)
expect(publishers).toHaveLength(38)
// 44 = 38 (37 post-Astro-cutover baseline + CalConnect, 2026-07-16)
// + 6 flavors ported from the pubid 2 registry (doi, evs, gb, isbn,
// omg, un — 2026-11). 3GPP already had a page (flavor key "tgpp").
expect(publishers).toHaveLength(44)
})

it('publishers are sorted alphabetically by flavor', () => {
Expand Down
58 changes: 58 additions & 0 deletions src/data/flavors/doi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Publisher } from '../types'

export const doi: Publisher = {
flavor: "doi",
name: "DOI",
fullName: "Digital Object Identifier",
category: "international",
description: "The DOI system provides an actionable, persistent identifier for digital objects. PubID parses DOI names — including NIST's DOI-based standard designations such as \"doi:10.6028/NIST.SP.800-53r5\" — as identifiers alongside the SDO schemes.",
website: "https://www.doi.org",
syntaxNotes: "DOI syntax is \"doi:10.{prefix}/{suffix}\" (case-insensitive scheme, normalized to lowercase). PubID accepts the canonical \"doi:\" form, the bare \"10.x/suffix\" name, the \"DOI:\" upper-scheme spelling, and the resolver-URL form on the doi.org host — all render canonical.",
urnPattern: "urn:pubid:doi:resource:prefix-{10.NNNN}:suffix-{suffix}",
relatedFlavors: ["nist", "isbn"],
docTypes: [
{
key: "resource",
title: "DOI Resource",
abbr: ["doi"],
description: "A DOI name for any resource. The prefix (10.NNNN) names the registrant; the suffix is registrant-defined — an opaque string, a journal-style path (10.1006/jmbi.1998.2354), or an SDO designation (NIST.SP.800-53r5).",
examples: [
{ input: "doi:10.1000/182" },
{ input: "10.1006/jmbi.1998.2354", output: "doi:10.1006/jmbi.1998.2354" },
{ input: "DOI:10.6028/NIST.2022-04-15.001", output: "doi:10.6028/NIST.2022-04-15.001" },
{ input: "https://doi.org/10.1000/182", output: "doi:10.1000/182" },
],
},
],
components: [
{
name: "Scheme",
description: "The \"doi:\" scheme token",
dataType: "enum",
values: ["doi:"],
format: "Case-insensitive on parse (DOI: accepted); rendered lowercase",
example: "doi:",
},
{
name: "Prefix",
description: "The registrant prefix, always beginning 10.",
attribute: "prefix",
dataType: "string",
format: "10.{NNNN} — 4+ digits naming the registrant",
example: "10.6028",
},
{
name: "Suffix",
description: "The registrant's opaque resource suffix",
attribute: "suffix",
dataType: "string",
format: "Any string; SDO designations use dotted series (NIST.SP.800-53r5)",
example: "NIST.2022-04-15.001",
},
],
algebra: [
{ type: "URL form", description: "The resolver URL parses and renders as the canonical DOI name", syntax: "doi.org/10.{prefix}/{suffix}", example: "https://doi.org/10.1000/182" },
],
}

export default doi
87 changes: 87 additions & 0 deletions src/data/flavors/evs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { Publisher } from '../types'

export const evs: Publisher = {
flavor: "evs",
name: "EVS",
fullName: "Estonian Centre for Standardisation and Accreditation",
category: "national",
description: "EVS is Estonia's national standardization body. It publishes national EVS standards and adopts European (EN) and international (ISO, ISO/IEC) standards under the EVS prefix — e.g. \"EVS-EN ISO 9001:2015\".",
website: "https://www.evs.ee",
syntaxNotes: "EVS identifiers follow: EVS[-EN][-ISO[/IEC]] [Number]:[Year] with supplements after a slash (\"/A1:2024\"). Types: EVS (national), EVS-EN (European adoption), EVS-EN ISO / EVS-EN ISO/IEC (international adoption via EN), EVS-TS (technical specification), EVS-TR (technical report).",
relatedFlavors: ["cen-cenelec", "iso", "iec"],
docTypes: [
{
key: "estonian_standard",
title: "Estonian Standard",
abbr: ["EVS", "EVS-TS", "EVS-TR"],
description: "A national EVS deliverable: the main EVS standard, plus EVS-TS (Technical Specification) and EVS-TR (Technical Report).",
examples: [
{ input: "EVS 921:2019" },
{ input: "EVS-TS 1:2019" },
],
},
{
key: "international_adoption",
title: "European Adoption",
abbr: ["EVS-EN"],
description: "A European Norm adopted as an Estonian standard: \"EVS EN\". The EN itself may adopt an ISO or ISO/IEC document, giving three levels (EVS-EN ISO 9001).",
examples: [
{ input: "EVS-EN 18216:2026" },
],
},
{
key: "national_adoption",
title: "International Adoption",
abbr: ["EVS-EN ISO", "EVS-EN ISO/IEC"],
description: "An ISO or ISO/IEC document adopted through a European Norm (EVS-EN ISO…) or directly. Supplements of the adopted document are carried after a slash (EVS-EN ISO 9001:2015/A1:2024).",
examples: [
{ input: "EVS-EN ISO 9001:2015/A1:2024" },
{ input: "EVS-EN ISO/IEC 27017:2026" },
{ input: "EVS-EN ISO/IEC 27701:2025" },
],
},
],
components: [
{
name: "Publisher",
description: "EVS, with adoption chain prefixes EN, ISO, ISO/IEC",
dataType: "enum",
values: ["EVS", "EVS-EN", "EVS-EN ISO", "EVS-EN ISO/IEC"],
example: "EVS-EN ISO",
},
{
name: "Document Type",
description: "Standard (default), TS, TR",
dataType: "enum",
values: ["", "TS", "TR"],
format: "Between publisher and number (EVS-TS 1)",
example: "TS",
},
{
name: "Number",
description: "The document number",
attribute: "number",
dataType: "string",
example: "9001",
},
{
name: "Year",
description: "Publication year after the colon",
attribute: "year",
dataType: "year",
example: "2015",
},
{
name: "Supplement",
description: "Amendment/corrigendum of the adopted document, after a slash",
format: "A{n}:{year} (e.g. /A1:2024)",
example: "/A1:2024",
},
],
algebra: [
{ type: "Adoption", description: "A national adoption of a European or international document", syntax: "EVS-EN ISO [Number]:[Year]", example: "EVS-EN ISO 9001:2015/A1:2024" },
{ type: "Amendment", description: "Amendment to the adopted document", syntax: "EVS [Number]:[Year]/A[n]:[Year]", example: "EVS-EN ISO 9001:2015/A1:2024" },
],
}

export default evs
Loading
Loading