Class: Pubid::Identifier

Inherits:
Lutaml::Model::Serializable
  • Object
show all
Defined in:
lib/pubid/identifier.rb

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attrs = {}, options = {}) ⇒ Identifier

Returns a new instance of Identifier.



213
214
215
216
217
# File 'lib/pubid/identifier.rb', line 213

def initialize(attrs = {}, options = {})
  attrs = attrs.dup
  attrs[:_type] ||= self.class.polymorphic_name
  super
end

Class Attribute Details

.format_registryObject



6
7
8
# File 'lib/pubid/identifier.rb', line 6

def format_registry
  @format_registry || superclass&.format_registry
end

Class Method Details

.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_identifier). 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.



23
24
25
26
27
28
29
# File 'lib/pubid/identifier.rb', line 23

def from_hash(data, options = {})
  type = data && (data["_type"] || data[:_type])
  klass = polymorphic_type_map[type]
  return klass.from_hash(data, options) if klass && klass != self

  super
end

.polymorphic_nameObject



219
220
221
222
223
224
225
226
227
228
229
# File 'lib/pubid/identifier.rb', line 219

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_mapObject

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.



37
38
39
40
41
42
43
# File 'lib/pubid/identifier.rb', line 37

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_documentObject

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.



126
127
128
# File 'lib/pubid/identifier.rb', line 126

def base_document
  self
end

#base_identifierObject

base_identifier is declared by supplement subclasses with proper type



119
120
121
# File 'lib/pubid/identifier.rb', line 119

def base_identifier
  nil
end

#drop_supplementsObject

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.



349
350
351
# File 'lib/pubid/identifier.rb', line 349

def drop_supplements
  self
end

#eql?(other) ⇒ Boolean

Returns:

  • (Boolean)


395
396
397
398
399
# File 'lib/pubid/identifier.rb', line 395

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.



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/pubid/identifier.rb', line 326

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 since identifiers store years inside date
  excluded_args << :date if excluded_args.delete(: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
  self.class.new(attrs)
end

#hashObject



391
392
393
# File 'lib/pubid/identifier.rb', line 391

def hash
  @hash ||= compute_hash
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.

Returns:

  • (Boolean)


357
358
359
360
361
# File 'lib/pubid/identifier.rb', line 357

def matches?(other, ignore: [])
  return false unless other.is_a?(::Pubid::Identifier)

  exclude(*ignore) == other.exclude(*ignore)
end

#mr_numberObject



283
284
285
# File 'lib/pubid/identifier.rb', line 283

def mr_number
  number&.to_s
end

#mr_number_with_partObject



275
276
277
278
279
280
281
# File 'lib/pubid/identifier.rb', line 275

def mr_number_with_part
  num = mr_number
  return nil unless num

  prt = mr_part
  prt ? "#{num}-#{prt}" : num
end

#mr_partObject



287
288
289
# File 'lib/pubid/identifier.rb', line 287

def mr_part
  part&.to_s
end

#mr_publisherObject

MR string template methods — flavors override as needed



262
263
264
# File 'lib/pubid/identifier.rb', line 262

def mr_publisher
  publisher&.to_s
end

#mr_typeObject



266
267
268
269
270
271
272
273
# File 'lib/pubid/identifier.rb', line 266

def mr_type
  return nil unless typed_stage

  code = typed_stage.type_code
  return nil if code.nil? || code.empty? || code == "is"

  code
end

#mr_yearObject



291
292
293
# File 'lib/pubid/identifier.rb', line 291

def mr_year
  date&.year&.to_s
end

#new_edition_of?(other) ⇒ Boolean

Returns:

  • (Boolean)


363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/pubid/identifier.rb', line 363

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

#render(format: :human, **opts) ⇒ Object

Unified render — delegates to format registry



238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/pubid/identifier.rb', line 238

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

  context = build_rendering_context(renderer, format:, **opts)
  renderer.new(self).render(context:, **opts.slice(:with_edition))
end

#resolve_urn_generatorObject



314
315
316
317
318
319
# File 'lib/pubid/identifier.rb', line 314

def resolve_urn_generator
  flavor = self.class.name.split("::")[1]
  Object.const_get("Pubid::#{flavor}::UrnGenerator")
rescue NameError
  Pubid::UrnGenerator::Base
end

#rootObject



231
232
233
234
235
# File 'lib/pubid/identifier.rb', line 231

def root
  return base_identifier.root if base_identifier

  self
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.



148
149
150
151
152
# File 'lib/pubid/identifier.rb', line 148

def to_hash(*args)
  hash = super
  canonicalize_hash(self, hash) if hash.is_a?(::Hash)
  hash
end

#to_mr_stringObject



257
258
259
# File 'lib/pubid/identifier.rb', line 257

def to_mr_string
  render(format: :mr_string)
end

#to_s(**opts) ⇒ Object



253
254
255
# File 'lib/pubid/identifier.rb', line 253

def to_s(**opts)
  render(format: :human, **opts)
end

#to_supplement_s(**opts) ⇒ Object

Supplement rendering hook — flavors override for supplement-specific rendering



305
306
307
# File 'lib/pubid/identifier.rb', line 305

def to_supplement_s(**opts)
  to_s(**opts)
end

#to_urnObject

Default URN generation — resolves flavor's UrnGenerator class



310
311
312
# File 'lib/pubid/identifier.rb', line 310

def to_urn
  resolve_urn_generator.new(self).generate
end

#urn_supplement_typeObject



300
301
302
# File 'lib/pubid/identifier.rb', line 300

def urn_supplement_type
  nil
end

#urn_type_codeObject

URN template methods — flavors override as needed



296
297
298
# File 'lib/pubid/identifier.rb', line 296

def urn_type_code
  nil
end

#yearString?

Returns publication year from the date component.

Returns:

  • (String, nil)

    publication year from the date component



131
132
133
# File 'lib/pubid/identifier.rb', line 131

def year
  date&.year&.to_s
end