Skip to content

Expose alternate publication identifiers via metadata.altIdentifiers#837

Draft
raphi011 wants to merge 2 commits into
readium:developfrom
raphi011:feat/epub-isbn-metadata
Draft

Expose alternate publication identifiers via metadata.altIdentifiers#837
raphi011 wants to merge 2 commits into
readium:developfrom
raphi011:feat/epub-isbn-metadata

Conversation

@raphi011

@raphi011 raphi011 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Following the review feedback on the original metadata.isbns proposal: rather than a specialized accessor, this exposes all alternate publication identifiers through the generic RWPM altIdentifier concept — already implemented in go-toolkit and ts-toolkit, but missing from Swift.

The EPUB parser splits the publication's dc:identifiers: the one matching the package @unique-identifier stays the primary metadata.identifier, and every other becomes an entry in the new metadata.altIdentifiers. An ISBN — usually a secondary dc:identifier — is therefore surfaced as one alternate identifier among others (DOI, ISSN, a second UUID, …).

altIdentifier is also added to Contributor (parsing/serialization only), matching go-toolkit and ts-toolkit, both of which expose it on Contributor as well.

API

New Shared type, mirroring go/ts:

public struct AltIdentifier: Hashable, Sendable {
    public var value: String
    public var scheme: String?
}

public extension Metadata {
    var altIdentifiers: [AltIdentifier]
}

public extension Contributor {
    var altIdentifiers: [AltIdentifier]
}

AltIdentifier parses from RWPM as either a bare URI string or a { value, scheme } object (per altIdentifier.schema.json) and serializes back to a bare string when no scheme is set. Metadata.altIdentifiers round-trips under the altIdentifier JSON key.

Parsing

Mirrors go-toolkit's EPUB behavior: identifier values are returned as declared (only trimmed), in document order. No normalization and no scheme synthesis — the opf:scheme attribute / ONIX identifier-type refinements are not interpreted, matching go-toolkit's parseDcElement.

Testing

  • AltIdentifierTests: string/object parsing, required value, and string-collapse serialization.
  • MetadataTests / ContributorTests: full JSON round-trips extended with a mixed bare-string + { value, scheme } array.
  • EPUBMetadataParserTests + OPF fixtures: alt extraction, verbatim values (hyphens preserved, opf:scheme not mapped to a scheme), urn:isbn, multiple alts in document order, and the no-alt case.

make format reports no changes.

Notes

  • Follows the schema + go-toolkit in modeling altIdentifier as an array. (ts-toolkit's Metadata currently models it as a single value, which diverges from both the schema and its own Contributor.altIdentifiers.)
  • Contributor.altIdentifiers is parsing/serialization only — the EPUB parser doesn't populate it, consistent with it not parsing Contributor.identifier from EPUB today either.
  • Kotlin has not implemented altIdentifier yet.
  • scheme is left empty when parsing EPUB — an ISBN declared via opf:scheme="ISBN" comes through as a bare value rather than a urn:isbn scheme. This matches every toolkit: go-toolkit keeps identifier values verbatim with no scheme (its parseDcElement ignores opf:scheme), ts-toolkit has the AltIdentifier model but no EPUB parser, and kotlin-toolkit hasn't implemented altIdentifier yet. So no Readium toolkit currently derives a scheme from EPUB markup; deriving well-known schemes (ISBN/DOI/ISSN) here is feasible but would make Swift the first to do so — happy to add it if you'd like that.
  • No JavaScript bundle changes.

@HadrienGardeur

Copy link
Copy Markdown
Member

In general, we don't want specialized properties when a generic one could fulfill the same goal and provide extensibility at the same time.

The shared models for Swift/Kotlin/TS are based on RWPM where we included the concept of alt identifiers: https://readium.org/webpub-manifest/contexts/default/#identifier

Therefore, additional identifiers (that do not identify themselves as the unique identifier of the publication) should be parsed and exposed in altIdentifier using well-known URI schemes (there's a list in the link that I've shared above).

@raphi011 raphi011 force-pushed the feat/epub-isbn-metadata branch from edcebe1 to c131a92 Compare June 28, 2026 12:35
@raphi011 raphi011 changed the title Expose EPUB ISBNs via metadata.isbns Expose alternate publication identifiers via metadata.altIdentifiers Jun 28, 2026
raphi011 and others added 2 commits June 28, 2026 14:55
Add the RWPM altIdentifier model — already shipped in the go and ts
toolkits — to Metadata and Contributor, surfacing identifiers other than
the primary one.

A new Shared AltIdentifier type ({value, scheme?}) parses from a bare URI
string or an object and collapses back to a string when no scheme is set.
For EPUB, Metadata.altIdentifiers is populated from every dc:identifier
other than the package's unique-identifier, mirroring go-toolkit: values
are returned as declared (only trimmed), in document order, with no scheme
synthesis.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@raphi011

Copy link
Copy Markdown
Contributor Author

In general, we don't want specialized properties when a generic one could fulfill the same goal and provide extensibility at the same time.

The shared models for Swift/Kotlin/TS are based on RWPM where we included the concept of alt identifiers: https://readium.org/webpub-manifest/contexts/default/#identifier

Therefore, additional identifiers (that do not identify themselves as the unique identifier of the publication) should be parsed and exposed in altIdentifier using well-known URI schemes (there's a list in the link that I've shared above).

Makes sense 👍 I've updated the PR to match how the go implementation does it. Let me know what you think!

@mickael-menu mickael-menu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @raphi011!

Currently we're focusing on migrating the toolkit to Swift 6 with strict concurrency support in the swift6 branch. We accept bug fixes but new features are frozen until this is done (we're almost there). I'm putting your PR as a draft for now.

Overall your PR looks great and should be ready to merge without much changes. I'm wondering if we could potentially add a helper to access the values of known schemes directly, and where would that live (an ISBN could be in identifier too).

As a side note, would you mind opening an issue or discussion on the repo before you start working on a new feature or API change? I would like to make sure the proposal aligns with our vision and existing APIs. It also helps ensure that no one else has already started working on the same thing.

Thank you for your understanding 🙏

/// The identifier value.
public var value: String

/// URI of the identifier's scheme, when known (e.g. `urn:isbn`).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scheme is only for identifiers that cannot be represented as a URI. For example, the scheme https://www.example.com/ has the identifier abc123. An ISBN would be represented as urn:isbn:9789510184356 without a scheme.

@mickael-menu mickael-menu marked this pull request as draft July 1, 2026 08:37
@mickael-menu mickael-menu requested a review from Copilot July 1, 2026 09:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for RWPM altIdentifier across the Swift toolkit by introducing a shared AltIdentifier model, wiring it into Metadata and Contributor JSON round-trips, and extracting alternate identifiers from EPUB OPF (dc:identifier) during parsing.

Changes:

  • Introduces AltIdentifier (value + optional scheme) with compact JSON serialization (string when scheme is absent).
  • Adds altIdentifiers to Metadata and Contributor, including JSON parsing/serialization under the altIdentifier key.
  • Updates EPUB metadata parsing and fixtures/tests to surface all non-unique dc:identifier values as Metadata.altIdentifiers.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Tests/StreamerTests/Parser/EPUB/EPUBMetadataParserTests.swift Adds coverage for EPUB alt identifier extraction, order, and verbatim preservation.
Tests/StreamerTests/Fixtures/OPF/alt-identifier-urn.opf New OPF fixture exercising urn: alternate identifier handling.
Tests/StreamerTests/Fixtures/OPF/alt-identifier-multiple.opf New OPF fixture covering multiple alternate identifiers and document order.
Tests/StreamerTests/Fixtures/OPF/alt-identifier-epub2.opf New OPF2 fixture ensuring verbatim identifier value handling (e.g. hyphens).
Tests/SharedTests/Publication/MetadataTests.swift Extends JSON round-trip tests for Metadata.altIdentifiers.
Tests/SharedTests/Publication/ContributorTests.swift Extends JSON round-trip tests for Contributor.altIdentifiers.
Tests/SharedTests/Publication/AltIdentifierTests.swift New unit tests for string/object parsing and compact serialization.
Sources/Streamer/Parser/EPUB/EPUBMetadataParser.swift Populates Metadata.altIdentifiers from OPF dc:identifier entries excluding the unique identifier.
Sources/Shared/Publication/Metadata.swift Adds stored property + JSON key mapping for altIdentifiers.
Sources/Shared/Publication/Contributor.swift Adds stored property + JSON key mapping for altIdentifiers.
Sources/Shared/Publication/AltIdentifier.swift New shared model type implementing RWPM parsing/serialization rules.
CHANGELOG.md Documents the new altIdentifiers API and EPUB parsing behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

private lazy var altIdentifiers: [AltIdentifier] = {
let uniqueIdentifierID = document.firstChild(xpath: "/opf:package")?.attr("unique-identifier")
return metas["identifier", in: .dcterms]
.filter { $0.id != uniqueIdentifierID }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants