Class: Pubid::Identifier
- Inherits:
-
Lutaml::Model::Serializable
- Object
- Lutaml::Model::Serializable
- Pubid::Identifier
- Defined in:
- lib/pubid/identifier.rb
Direct Known Subclasses
Adobe::Identifier, Amca::Identifier, Ansi::Identifier, Api::Identifier, Ashrae::Identifier, Asme::Identifier, Astm::Identifier, Bipm::Identifier, Bsi::Identifier, BundledIdentifier, Calconnect::Identifier, Ccsds::Identifier, Ccsds::SingleIdentifier, CenCenelec::Identifier, Cie::Identifier, Csa::Identifier, Doi::Identifier, Easc::Identifier, Ecma::Identifier, Etsi::Identifier, Gb::Identifier, Gost::Identifier, Pubid::Iala::Identifier, Pubid::Iana::Identifier, Pubid::Idf::Identifier, Pubid::Iec::Identifier, Pubid::Ieee::Identifier, Pubid::Ietf::Identifier, Pubid::Ietf::Identifiers::Bcp, Pubid::Ietf::Identifiers::Fyi, Pubid::Ietf::Identifiers::Rfc, Pubid::Ietf::Identifiers::Std, Pubid::Iho::Identifier, Pubid::Isbn::Identifier, Pubid::Iso::Identifier, Pubid::Itu::Identifier, Jcgm::Identifier, Jis::Identifier, Nist::Identifier, Oasis::Identifier, Ogc::Identifier, Oiml::Identifier, Omg::Identifier, Plateau::Identifier, Plateau::SupplementIdentifier, Sae::Identifier, Tgpp::Identifier, Un::Identifier, W3c::Identifier, Xsf::Identifier
Class Attribute Summary collapse
Class Method Summary collapse
-
.apply_mappings(doc, format, options = {}) ⇒ Object
lutaml's nested polymorphic cast (Attribute#cast → apply_mappings) bypasses Identifier.from_hash, so cross-flavor dispatch wouldn't fire for a nested attribute that carries another flavor's _type.
-
.concrete_class_for(data) ⇒ Object
Resolve a polymorphic _type to the concrete class that owns it: this flavor's own map first, then any registered flavor's map via TypeResolver.
-
.from_hash(data, options = {}) ⇒ Object
Polymorphic deserialization shared by every flavor base.
- .polymorphic_name ⇒ Object
-
.polymorphic_type_map ⇒ Object
Map of polymorphic_name ("pubid:iso:corrigendum") => concrete class for the flavor this base belongs to.
Instance Method Summary collapse
-
#base ⇒ Object
base is declared by supplement subclasses with proper type.
-
#base_document ⇒ Object
The underlying standard, with amendment / corrigendum / Expert-commentary / Flex-version wrappers peeled recursively.
-
#dated_version_of?(other) ⇒ Boolean
Self and
otherrefer to the same document but differ only in the trailing date / year — the "dated vs undated" reference pattern. -
#draft_of?(other) ⇒ Boolean
Self is a draft version of
other(the published identifier). -
#drop_supplements ⇒ Object
The standard this identifier supplements, dropping its own supplement layer (one level).
-
#edition_of?(other) ⇒ Boolean
Self and
otherhave the same publisher + number + part but differ in edition / revision marker. - #eql?(other) ⇒ Boolean
-
#exclude(*args) ⇒ Object
Excluded attributes are nilled; every other value is passed through #exclude_from_nested so the exclusion also propagates into nested identifiers — wrapper types (adopted standards, consolidated amendments, expert-commentary wrappers) delegate their date to an inner identifier rather than storing it in their own attribute.
-
#has_supplement?(other) ⇒ Boolean
Inverse of #supplement_of? — self is the base document and
otheris a supplement attached to it. - #hash ⇒ Object
-
#includes?(other) ⇒ Boolean
Self is an all-parts collection that covers
other. -
#initialize(attrs = {}, options = {}) ⇒ Identifier
constructor
A new instance of Identifier.
-
#matches?(other, ignore: []) ⇒ Boolean
Fuzz-level equality: two identifiers match when they are equal after excluding the given aspects.
- #mr_all_parts ⇒ Object
- #mr_edition ⇒ Object
- #mr_languages ⇒ Object
- #mr_number ⇒ Object
- #mr_number_with_part ⇒ Object
- #mr_part ⇒ Object
-
#mr_publisher ⇒ Object
MR string template methods — flavors override as needed.
- #mr_subpart ⇒ Object
-
#mr_supplement_suffix ⇒ Object
Hook: supplement / wrapper subclasses return the
{type}.{number}.{year}suffix that distinguishes them from their base. - #mr_type ⇒ Object
- #mr_year ⇒ Object
- #new_edition_of?(other) ⇒ Boolean
-
#related_to?(other) ⇒ Boolean
Any relational predicate holds.
-
#render(format: :human, **opts) ⇒ Object
Unified render — delegates to format registry.
- #resolve_urn_generator ⇒ Object
- #root ⇒ Object
-
#sibling_of?(other) ⇒ Boolean
Self and
otherhave the same publisher + number but differ in part, subpart, or edition. -
#supplement_of?(other) ⇒ Boolean
Self is a supplement (amendment / corrigendum / errata / supplement) of
other. -
#to_hash(*args) ⇒ Object
Canonicalize the serialized hash so it never carries a defaulted attribute still at its default (or empty) value.
- #to_mr_string ⇒ Object
- #to_s(**opts) ⇒ Object
-
#to_slug ⇒ Object
Filesystem-/URL-safe slug derived from the MR string.
-
#to_supplement_s(**opts) ⇒ Object
Supplement rendering hook — flavors override for supplement-specific rendering.
-
#to_urn ⇒ Object
Default URN generation — resolves flavor's UrnGenerator class.
- #urn_supplement_type ⇒ Object
-
#urn_type_code ⇒ Object
URN template methods — flavors override as needed.
-
#year ⇒ String?
Publication year from the date component.
Constructor Details
#initialize(attrs = {}, options = {}) ⇒ Identifier
Returns a new instance of Identifier.
245 246 247 248 249 |
# File 'lib/pubid/identifier.rb', line 245 def initialize(attrs = {}, = {}) attrs = attrs.dup attrs[:_type] ||= self.class.polymorphic_name super end |
Class Attribute Details
.format_registry ⇒ Object
6 7 8 |
# File 'lib/pubid/identifier.rb', line 6 def format_registry @format_registry || superclass&.format_registry end |
Class Method Details
.apply_mappings(doc, format, options = {}) ⇒ Object
lutaml's nested polymorphic cast (Attribute#cast → apply_mappings) bypasses from_hash, so cross-flavor dispatch wouldn't fire for a nested attribute that carries another flavor's _type. Intercept apply_mappings and route to from_hash so the concrete class's own mappings (with their flavor-specific custom cast methods, e.g. ISO's number_from_kv) drive deserialization.
37 38 39 40 41 42 43 44 |
# File 'lib/pubid/identifier.rb', line 37 def apply_mappings(doc, format, = {}) return super unless hash_with_type?(doc, format) klass = concrete_class_for(doc) return super unless klass && klass != self klass.from_hash(doc, ) end |
.concrete_class_for(data) ⇒ Object
Resolve a polymorphic _type to the concrete class that owns it: this flavor's own map first, then any registered flavor's map via TypeResolver. Returns nil for blank or unknown types.
49 50 51 52 53 54 |
# File 'lib/pubid/identifier.rb', line 49 def concrete_class_for(data) type = data && (data["_type"] || data[:_type]) return nil unless type polymorphic_type_map[type] || ::Pubid::TypeResolver.resolve(type) end |
.from_hash(data, options = {}) ⇒ Object
Polymorphic deserialization shared by every flavor base. lutaml's
key_value polymorphic_map reads _type only to VALIDATE; it does not
re-instantiate the concrete subclass. So a base-class from_hash would
return a bare base object and drop subtype-specific attributes (e.g. a
supplement's base). Route by _type to the concrete class
named in polymorphic_type_map, then let its inherited from_hash (this
method again, where klass == self) fall through to lutaml's real work.
This generalizes the per-flavor *_TYPE_MAP dispatch that ISO, JIS,
IEC, CCSDS and IHO each hand-rolled — one implementation, inherited by
every flavor base. Cross-flavor dispatch (a nested adopted identifier
whose _type belongs to a different flavor) is delegated to
TypeResolver, which knows about every registered flavor.
25 26 27 28 29 |
# File 'lib/pubid/identifier.rb', line 25 def from_hash(data, = {}) klass = concrete_class_for(data) return klass.from_hash(data, ) if klass && klass != self super end |
.polymorphic_name ⇒ Object
251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/pubid/identifier.rb', line 251 def self.polymorphic_name return nil unless name parts = name.split("::") flavor = parts[1]&.downcase type_kebab = parts.last .gsub(/([A-Z]+)([A-Z][a-z])/, '\1-\2') .gsub(/([a-z\d])([A-Z])/, '\1-\2') .downcase "pubid:#{flavor}:#{type_kebab}" end |
.polymorphic_type_map ⇒ Object
Map of polymorphic_name ("pubid:iso:corrigendum") => concrete class for
the flavor this base belongs to. Built once by scanning the flavor's
Identifiers namespace for Pubid::Identifier descendants and unioning
the flavor's identifier_types registry (which may register classes
living outside that namespace, e.g. ISO's BundledIdentifier). Memoized
per class. Public so TypeResolver can read another flavor's map for
cross-flavor polymorphic dispatch.
63 64 65 66 67 68 69 |
# File 'lib/pubid/identifier.rb', line 63 def polymorphic_type_map @polymorphic_type_map ||= identifier_registry_classes.each_with_object({}) do |klass, map| poly = klass.polymorphic_name map[poly] ||= klass if poly end end |
Instance Method Details
#base ⇒ Object
base is declared by supplement subclasses with proper type
151 152 153 |
# File 'lib/pubid/identifier.rb', line 151 def base nil end |
#base_document ⇒ Object
The underlying standard, with amendment / corrigendum / Expert-commentary / Flex-version wrappers peeled recursively. Wrapper subclasses override this; a plain identifier is its own base document.
158 159 160 |
# File 'lib/pubid/identifier.rb', line 158 def base_document self end |
#dated_version_of?(other) ⇒ Boolean
Self and other refer to the same document but differ only in the
trailing date / year — the "dated vs undated" reference pattern.
524 525 526 527 528 |
# File 'lib/pubid/identifier.rb', line 524 def dated_version_of?(other) return false unless other.is_a?(::Pubid::Identifier) matches?(other, ignore: [:date, :year]) && self != other end |
#draft_of?(other) ⇒ Boolean
Self is a draft version of other (the published identifier).
IEEE uses draft_status + draft; ISO/IEC use typed_stage.
551 552 553 554 555 556 557 |
# File 'lib/pubid/identifier.rb', line 551 def draft_of?(other) return false unless other.is_a?(::Pubid::Identifier) root == other.root && self != other && matches?(other, ignore: [:date, :year, :stage, :typed_stage]) end |
#drop_supplements ⇒ Object
The standard this identifier supplements, dropping its own supplement layer (one level). Overridden by ConsolidatedIdentifier / Amendment / Corrigendum; a non-supplement identifier supplements nothing, so it is returned unchanged.
458 459 460 |
# File 'lib/pubid/identifier.rb', line 458 def drop_supplements self end |
#edition_of?(other) ⇒ Boolean
Self and other have the same publisher + number + part but differ
in edition / revision marker.
561 562 563 564 565 566 567 568 |
# File 'lib/pubid/identifier.rb', line 561 def edition_of?(other) return false unless other.is_a?(::Pubid::Identifier) publisher == other.publisher && number == other.number && part == other.part && self != other end |
#eql?(other) ⇒ Boolean
582 583 584 585 586 |
# File 'lib/pubid/identifier.rb', line 582 def eql?(other) return false unless other.is_a?(self.class) hash == other.hash && self == other end |
#exclude(*args) ⇒ Object
Excluded attributes are nilled; every other value is passed through #exclude_from_nested so the exclusion also propagates into nested identifiers — wrapper types (adopted standards, consolidated amendments, expert-commentary wrappers) delegate their date to an inner identifier rather than storing it in their own attribute.
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 |
# File 'lib/pubid/identifier.rb', line 428 def exclude(*args) # :amendment / :supplement are structural, not attributes — they reduce # a supplemented identifier to the standard it wraps (#drop_supplements). supplement_keys = args & %i[amendment supplement] unless supplement_keys.empty? return drop_supplements.exclude(*(args - supplement_keys)) end excluded_args = args.dup # Map :year to :date (year-in-date flavors), but ALSO keep :year so a # flavor that models the edition as a plain `year` attribute (e.g. GOST) # has it excluded too. The loop below nils whichever name the flavor has. excluded_args << :date if excluded_args.include?(:year) attrs = self.class.attributes.each_with_object({}) do |(name, _), h| value = excluded_args.include?(name) ? nil : public_send(name) h[name] = exclude_from_nested(value, args) end # Splat the rebuilt attributes as keywords. Flavors whose identifier # #initialize is keyword-only (ITU, IEEE, …) would raise ArgumentError on # a positional hash under Ruby 3 kwarg separation; the base # initialize(attrs = {}, options = {}) still accepts **attrs because Ruby # passes keywords to a no-keyword method as a trailing positional Hash. self.class.new(**attrs) end |
#has_supplement?(other) ⇒ Boolean
Inverse of #supplement_of? — self is the base document and other
is a supplement attached to it.
518 519 520 |
# File 'lib/pubid/identifier.rb', line 518 def has_supplement?(other) other.is_a?(::Pubid::Identifier) && other.supplement_of?(self) end |
#hash ⇒ Object
578 579 580 |
# File 'lib/pubid/identifier.rb', line 578 def hash @hash ||= compute_hash end |
#includes?(other) ⇒ Boolean
Self is an all-parts collection that covers other.
542 543 544 545 546 547 |
# File 'lib/pubid/identifier.rb', line 542 def includes?(other) return false unless other.is_a?(::Pubid::Identifier) return false unless all_parts root == other.root end |
#matches?(other, ignore: []) ⇒ Boolean
Fuzz-level equality: two identifiers match when they are equal after
excluding the given aspects. ignore accepts the same symbols as #exclude
(e.g. :date, :edition, :amendment). This is the primitive relaton uses to
match a reference against catalogue hits at varying strictness.
466 467 468 469 470 |
# File 'lib/pubid/identifier.rb', line 466 def matches?(other, ignore: []) return false unless other.is_a?(::Pubid::Identifier) exclude(*ignore) == other.exclude(*ignore) end |
#mr_all_parts ⇒ Object
382 383 384 385 386 |
# File 'lib/pubid/identifier.rb', line 382 def mr_all_parts return nil unless all_parts "all-parts" end |
#mr_edition ⇒ Object
369 370 371 372 373 |
# File 'lib/pubid/identifier.rb', line 369 def mr_edition return nil unless edition&.number "ed#{edition.number}" end |
#mr_languages ⇒ Object
375 376 377 378 379 380 |
# File 'lib/pubid/identifier.rb', line 375 def mr_languages return nil unless languages&.any? # Hyphen-joined, no parens — parens are shell-/URL-unsafe. languages.map { |l| l.code.to_s.downcase }.join("-") end |
#mr_number ⇒ Object
346 347 348 |
# File 'lib/pubid/identifier.rb', line 346 def mr_number number&.to_s&.downcase end |
#mr_number_with_part ⇒ Object
336 337 338 339 340 341 342 343 344 |
# File 'lib/pubid/identifier.rb', line 336 def mr_number_with_part num = mr_number return nil unless num segments = [num] segments << mr_part if part segments << mr_subpart if subpart segments.compact.join("-").downcase end |
#mr_part ⇒ Object
350 351 352 |
# File 'lib/pubid/identifier.rb', line 350 def mr_part part&.to_s&.downcase end |
#mr_publisher ⇒ Object
MR string template methods — flavors override as needed.
The MR format is a lossless, dot-separated, all-lowercase, filename-safe
slug mirroring to_s's structure:
{publisher}[-{copublisher}…][.{type}].{number}[-{part}[-{subpart}…]]
[.{year}[-{month}[-{day}]]|’--’}][.{edition}][.{language}-{language}…]
[.all-parts][_supplements…]
Supplements append _{type}.{number}.{year} recursively, so a chained
supplement like …/Amd 3:2016/Cor 1:2017 round-trips as
…_amd.3.2016_cor.1.2017. Copublishers join with -
(iso-iec.17031-1.2020) so the slug never contains a path separator.
Distinct identifiers never collide on to_mr_string (issue #142).
323 324 325 |
# File 'lib/pubid/identifier.rb', line 323 def mr_publisher publisher&.to_s&.downcase&.tr("/", "-") end |
#mr_subpart ⇒ Object
354 355 356 |
# File 'lib/pubid/identifier.rb', line 354 def mr_subpart subpart&.to_s&.downcase end |
#mr_supplement_suffix ⇒ Object
Hook: supplement / wrapper subclasses return the {type}.{number}.{year}
suffix that distinguishes them from their base. nil for non-supplements.
The base MrString renderer recurses into base whenever this returns
a non-nil value, so chained supplements (Cor → Amd → IS) render fully.
The suffix is appended with _ (not /) so the MR stays filename-safe.
393 394 395 |
# File 'lib/pubid/identifier.rb', line 393 def mr_supplement_suffix nil end |
#mr_type ⇒ Object
327 328 329 330 331 332 333 334 |
# File 'lib/pubid/identifier.rb', line 327 def mr_type return nil unless typed_stage code = typed_stage.type_code return nil if code.nil? || code.empty? || code.to_s == "is" code.to_s.downcase end |
#mr_year ⇒ Object
358 359 360 361 362 363 364 365 366 367 |
# File 'lib/pubid/identifier.rb', line 358 def mr_year return nil unless date return "--" if date.respond_to?(:undated?) && date.undated? return nil unless date.year result = date.year.to_s result += "-#{date.month}" if date.respond_to?(:month) && date.month result += "-#{date.day}" if date.respond_to?(:day) && date.day result end |
#new_edition_of?(other) ⇒ Boolean
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 |
# File 'lib/pubid/identifier.rb', line 472 def new_edition_of?(other) unless publisher == other.publisher raise ArgumentError, "Cannot compare edition: different publisher" end unless number == other.number raise ArgumentError, "Cannot compare edition: different number" end unless part == other.part raise ArgumentError, "Cannot compare edition: different part" end unless date && other.date raise ArgumentError, "Cannot compare identifier without date/year" end return date.year > other.date.year if date.year != other.date.year if edition && other.edition return edition.number > other.edition.number end false end |
#related_to?(other) ⇒ Boolean
Any relational predicate holds.
571 572 573 574 575 576 |
# File 'lib/pubid/identifier.rb', line 571 def (other) %i[supplement_of? has_supplement? dated_version_of? edition_of? sibling_of? includes? draft_of?].any? do |m| public_send(m, other) end end |
#render(format: :human, **opts) ⇒ Object
Unified render — delegates to format registry
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/pubid/identifier.rb', line 270 def render(format: :human, **opts) registry = self.class.format_registry unless registry raise ArgumentError, "No format registry configured on #{self.class}" end renderer = registry.renderer_for(format) unless renderer raise ArgumentError, "No renderer registered for format: #{format}" end # `:trademark` is a render-time flag consumed by the renderer, not the # rendering context — and some flavors override build_rendering_context # with a strict signature, so strip it here. ctx_opts = opts.except(:trademark) context = build_rendering_context(renderer, format:, **ctx_opts) render_opts = opts.slice(:with_edition, :trademark) renderer.new(self).render(context:, **render_opts) end |
#resolve_urn_generator ⇒ Object
416 417 418 419 420 421 |
# File 'lib/pubid/identifier.rb', line 416 def resolve_urn_generator flavor = self.class.name.split("::")[1] Object.const_get("Pubid::#{flavor}::UrnGenerator") rescue NameError Pubid::UrnGenerator::Base end |
#root ⇒ Object
263 264 265 266 267 |
# File 'lib/pubid/identifier.rb', line 263 def root return base.root if base self end |
#sibling_of?(other) ⇒ Boolean
Self and other have the same publisher + number but differ in
part, subpart, or edition. This is the "sibling documents" pattern:
same base standard, different sub-component.
533 534 535 536 537 538 539 |
# File 'lib/pubid/identifier.rb', line 533 def sibling_of?(other) return false unless other.is_a?(::Pubid::Identifier) publisher == other.publisher && number == other.number && self != other end |
#supplement_of?(other) ⇒ Boolean
Self is a supplement (amendment / corrigendum / errata / supplement)
of other. Self must be a wrapper (has a base attribute) whose
inner base equals other.
509 510 511 512 513 514 |
# File 'lib/pubid/identifier.rb', line 509 def supplement_of?(other) return false unless other.is_a?(::Pubid::Identifier) return false unless self.class.attributes.key?(:base) base == other end |
#to_hash(*args) ⇒ Object
Canonicalize the serialized hash so it never carries a defaulted attribute still at its default (or empty) value. This makes to_hash a pure function of the identifier's values, independent of how the object was built.
Motivation: lutaml materializes each attribute's default as an explicit assignment during deserialization (flipping using_default? to false), so a naive from_hash(x).to_hash re-emits defaults that parse(x).to_hash omits — breaking the exact-equality round-trip relaton-index relies on (from_hash(raw).to_hash == raw). pubid never consults lutaml's unset tracking and defines no render_default: true, so a defaulted attribute at its default/empty value carries no meaning and does not belong in the canonical hash. Dropping it here (rather than repairing from_hash) fixes the round-trip through every construction path — parse, from_hash, manual.
180 181 182 183 184 |
# File 'lib/pubid/identifier.rb', line 180 def to_hash(*args) hash = super canonicalize_hash(self, hash) if hash.is_a?(::Hash) hash end |
#to_mr_string ⇒ Object
294 295 296 |
# File 'lib/pubid/identifier.rb', line 294 def to_mr_string render(format: :mr_string) end |
#to_s(**opts) ⇒ Object
290 291 292 |
# File 'lib/pubid/identifier.rb', line 290 def to_s(**opts) render(format: :human, **opts) end |
#to_slug ⇒ Object
Filesystem-/URL-safe slug derived from the MR string. Defaults to
to_mr_string because every flavor except NIST already emits an
all-lowercase, filename-safe MR (only [a-z0-9.-] characters, plus
_ as the supplement separator and - as the copublisher separator).
Flavors whose MR is not slug-safe (notably NIST, whose MR format is
fixed by the pubid standard) override this to project the MR into a
slug-safe form.
305 306 307 |
# File 'lib/pubid/identifier.rb', line 305 def to_slug to_mr_string end |
#to_supplement_s(**opts) ⇒ Object
Supplement rendering hook — flavors override for supplement-specific rendering
407 408 409 |
# File 'lib/pubid/identifier.rb', line 407 def to_supplement_s(**opts) to_s(**opts) end |
#to_urn ⇒ Object
Default URN generation — resolves flavor's UrnGenerator class
412 413 414 |
# File 'lib/pubid/identifier.rb', line 412 def to_urn resolve_urn_generator.new(self).generate end |
#urn_supplement_type ⇒ Object
402 403 404 |
# File 'lib/pubid/identifier.rb', line 402 def urn_supplement_type nil end |
#urn_type_code ⇒ Object
URN template methods — flavors override as needed
398 399 400 |
# File 'lib/pubid/identifier.rb', line 398 def urn_type_code nil end |
#year ⇒ String?
Returns publication year from the date component.
163 164 165 |
# File 'lib/pubid/identifier.rb', line 163 def year date&.year&.to_s end |