opencdd 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,23 @@ module Opencdd
10
10
  FORCED_META_CLASSES = %w[MDC_C002 MDC_C003].freeze
11
11
  PARCEL_ID_PATTERN = /\A[A-Z0-9-]+\z/.freeze
12
12
 
13
+ # Database is split into focused concern modules. Each module
14
+ # is mixed in below and lives in its own file under
15
+ # +lib/opencdd/database/+. The core class (this file) holds
16
+ # entity identity, indexing, and the type-partitioned accessors.
17
+ autoload :ParcelIntegration, "opencdd/database/parcel_integration"
18
+ autoload :Queries, "opencdd/database/queries"
19
+ autoload :Finalization, "opencdd/database/finalization"
20
+ autoload :Mutations, "opencdd/database/mutations"
21
+ autoload :Persistence, "opencdd/database/persistence"
22
+
23
+ include Enumerable
24
+ include Opencdd::Database::ParcelIntegration
25
+ include Opencdd::Database::Queries
26
+ include Opencdd::Database::Finalization
27
+ include Opencdd::Database::Mutations
28
+ include Opencdd::Database::Persistence
29
+
13
30
  attr_reader :workbooks, :unresolved_refs, :alias_table
14
31
 
15
32
  def self.load(path)
@@ -28,6 +45,14 @@ module Opencdd
28
45
  Opencdd::Parcel::ShardedDirReader.new(path).load_into(new)
29
46
  end
30
47
 
48
+ def self.from_yaml(yaml)
49
+ Opencdd::Model::YamlDatabase.from_yaml(yaml).to_database
50
+ end
51
+
52
+ def self.load_from_directory(path)
53
+ Opencdd::Model::EntityStore.new(path).load_database
54
+ end
55
+
31
56
  def initialize
32
57
  @entities_by_irdi = {}
33
58
  @entities_by_code = Hash.new { |h, k| h[k] = [] }
@@ -40,31 +65,6 @@ module Opencdd
40
65
  @entity_sources = {}
41
66
  end
42
67
 
43
- def add_workbook(workbook)
44
- @workbooks << workbook
45
- parcel_id = workbook.parcel_id
46
- workbook.each_sheet do |sheet|
47
- next unless sheet.type && sheet.rows.any?
48
- type = sheet.type
49
- entity_class = Opencdd::MetaClasses.entity_class_for_type(type)
50
- raise "Unknown entity type #{type.inspect} for sheet #{sheet.name.inspect}" unless entity_class
51
-
52
- sheet.rows.each do |row|
53
- entity = entity_class.from_row(
54
- row,
55
- schema: sheet.schema,
56
- meta_class_irdi: sheet.meta_class_irdi,
57
- )
58
- add_entity(entity)
59
- if parcel_id && entity.irdi
60
- prev = @entity_sources[entity.irdi]
61
- @entity_sources[entity.irdi] = prev.nil? ? parcel_id : prev
62
- end
63
- end
64
- end
65
- self
66
- end
67
-
68
68
  def add_entity(entity)
69
69
  irdi = entity.irdi
70
70
  if irdi.nil?
@@ -102,111 +102,6 @@ module Opencdd
102
102
  self
103
103
  end
104
104
 
105
- def resolve_reference(ref)
106
- return nil if ref.nil?
107
- return ref if ref.is_a?(Opencdd::Entity)
108
-
109
- key = ref.to_s.strip
110
- return nil if key.empty?
111
-
112
- if key.include?("/") || key.include?("#")
113
- irdi = Opencdd::IRDI.parse(key)
114
- return find(irdi)
115
- end
116
-
117
- entity = @symbol_table[key]
118
- return entity if entity
119
-
120
- by_code = find_by_code(key)
121
- return by_code if by_code
122
-
123
- irdi = Opencdd::IRDI.parse(key)
124
- irdi ? find(irdi) : nil
125
- end
126
-
127
- def finalize!
128
- normalize_reference_collections!
129
- link_class_hierarchy!
130
- link_property_classes!
131
- link_value_lists!
132
- rebuild_symbol_table!
133
- self
134
- end
135
-
136
- def normalize_reference_collections!
137
- each_reference_collection do |entity, pid, _raw, elements|
138
- entity.properties[pid] = Opencdd::StructuredValues.rejoin(elements)
139
- end
140
- self
141
- end
142
-
143
- # Single iteration shape for any method that walks every
144
- # set_of_refs property across every entity. Yields
145
- # (entity, property_id, raw_value, elements) where +elements+
146
- # is the unwrapped Array<String> via StructuredValues.
147
- # Returns an Enumerator when no block is given.
148
- def each_reference_collection
149
- return enum_for(:each_reference_collection) unless block_given?
150
- set_property_ids = Opencdd::PropertyIds::REGISTRY
151
- .select { |_, e| e.value_kind == :set_of_refs }
152
- .keys
153
- entities.each do |entity|
154
- set_property_ids.each do |pid|
155
- raw = entity.properties[pid]
156
- next if raw.nil? || raw.to_s.strip.empty?
157
- elements = Opencdd::StructuredValues.unwrap_and_split(raw)
158
- next if elements.empty?
159
- yield(entity, pid, raw, elements)
160
- end
161
- end
162
- end
163
-
164
- def find(irdi)
165
- return nil if irdi.nil?
166
- key = Opencdd::IRDI === irdi ? irdi : Opencdd::IRDI.parse(irdi)
167
- return nil unless key
168
- @entities_by_irdi[key]
169
- end
170
-
171
- # Coerce +value+ (Entity, IRDI, String, or nil) to an Entity.
172
- # Returns nil if the value cannot be resolved. Single entry
173
- # point for the "entity or irdi" pattern that used to be
174
- # inlined at every call site.
175
- def coerce_entity(value)
176
- return nil if value.nil?
177
- return value if value.is_a?(Opencdd::Entity)
178
- find(value)
179
- end
180
-
181
- # Coerce +value+ to an IRDI. Accepts an Entity (returns its
182
- # irdi), an IRDI (pass-through), or a String (parsed). Returns
183
- # nil if the value cannot be parsed.
184
- def coerce_irdi(value)
185
- return nil if value.nil?
186
- return value.irdi if value.is_a?(Opencdd::Entity)
187
- return value if value.is_a?(Opencdd::IRDI)
188
- Opencdd::IRDI.parse(value.to_s)
189
- end
190
-
191
- def find_by_code(code)
192
- matches = @entities_by_code[code.to_s]
193
- return nil if matches.empty?
194
- matches.first
195
- end
196
-
197
- def find_all_by_code(code)
198
- @entities_by_code[code.to_s].dup
199
- end
200
-
201
- def find_by_name(name, type: nil, lang: :en)
202
- pools = type ? [@entities_by_type[type]].compact : @entities_by_type.values
203
- pools.each do |pool|
204
- hit = pool.find { |e| e.preferred_name(lang).to_s.casecmp(name.to_s).zero? }
205
- return hit if hit
206
- end
207
- nil
208
- end
209
-
210
105
  def classes ; @entities_by_type[:class] || [] ; end
211
106
  def properties ; @entities_by_type[:property] || [] ; end
212
107
  def units ; @entities_by_type[:unit] || [] ; end
@@ -227,315 +122,10 @@ module Opencdd
227
122
  type ? (@entities_by_type[type] || []).size : @entities_by_irdi.size
228
123
  end
229
124
 
230
- def root_classes
231
- classes.select { |c| c.parent_irdi.nil? }
232
- end
233
-
234
- # ── Powertype accessors (4-layer ontology) ──────────────────
235
- #
236
- # Powertype semantics: a CATEGORICAL_CLASS at M1 has subclasses
237
- # that are themselves classes but also act as its instances
238
- # (used in CLASS_REFERENCE data types and sub_class_selection).
239
- # See Opencdd module docstring + Klass#powertype?.
240
-
241
- # All categorical classes registered in this database. Each is
242
- # a candidate target for a CLASS_REFERENCE data type.
243
- def categorical_classes
244
- classes.select(&:powertype?)
245
- end
246
-
247
- # Powertype instances of +categorical_klass+ — its valid options
248
- # for CLASS_REFERENCE values. Accepts a Klass, IRDI, or code
249
- # String. Returns [] if +categorical_klass+ is not a powertype.
250
- def instances_of(categorical_klass)
251
- k = coerce_entity(categorical_klass)
252
- return [] unless k.is_a?(Opencdd::Klass)
253
- k.categorical_instances(self)
254
- end
255
-
256
- # Predicate: is +value+ a valid powertype instance of the given
257
- # categorical class? Used by the validator's CLASS_REFERENCE
258
- # check (R04/R08 extension).
259
- def valid_class_reference?(categorical_klass, value)
260
- target = coerce_entity(value)
261
- return false unless target
262
- instances_of(categorical_klass).any? { |c| c.irdi == target.irdi }
263
- end
264
-
265
- def class_tree(fields: Opencdd::ClassTree::DEFAULT_FIELDS)
266
- Opencdd::ClassTree.new(self, fields: fields)
267
- end
268
-
269
- def effective_properties
270
- @effective_properties ||= Opencdd::EffectiveProperties.new(self)
271
- end
272
-
273
- def composition_tree(klass, max_depth: 10)
274
- Opencdd::CompositionTree.new(self).for(klass, max_depth: max_depth)
275
- end
276
-
277
- def relation_tree(root = nil, max_depth: 10)
278
- Opencdd::RelationTree.new(self).for(root, max_depth: max_depth)
279
- end
280
-
281
- def add_dictionary(dict)
282
- validate_parcel_id!(dict.parcel_id)
283
- source_language = dict.source_language || "en"
284
- translation_languages = Array(dict.translation_languages)
285
- meta_irdis = normalize_meta_class_irdis(dict.meta_class_irdis)
286
-
287
- sheets = meta_irdis.map do |meta_irdi|
288
- Opencdd::Parcel::Sheet.scaffold(
289
- meta_class_irdi: meta_irdi,
290
- parcel_id: dict.parcel_id,
291
- source_language: source_language,
292
- translation_languages: translation_languages,
293
- )
294
- end
295
-
296
- project = Opencdd::Parcel::Workbook::ProjectInfo.new(
297
- project_id: dict.parcel_id,
298
- parcel_id: dict.parcel_id,
299
- multi_language: translation_languages.join(","),
300
- base_language: source_language,
301
- )
302
- sheetmap = build_sheetmap_for(dict.parcel_id, sheets)
303
- workbook = Opencdd::Parcel::Workbook.new(
304
- sheets: sheets, sheetmap: sheetmap, project: project,
305
- )
306
- @workbooks << workbook
307
- workbook
308
- end
309
-
310
- def drop_dictionary(parcel_id)
311
- matching = @workbooks.select { |wb| wb.parcel_id == parcel_id }
312
- raise ArgumentError, "no dictionary with parcel_id #{parcel_id.inspect}" if matching.empty?
313
-
314
- irdis_to_drop = @entity_sources.select { |_, pid| pid == parcel_id }.keys
315
- irdis_to_drop.each do |irdi|
316
- remove_entity_by_irdi!(irdi)
317
- @entity_sources.delete(irdi)
318
- end
319
- @workbooks.reject! { |wb| wb.parcel_id == parcel_id }
320
- self
321
- end
322
-
323
- def register_external_sheet(sheet, parcel_id:)
324
- validate_parcel_id!(parcel_id)
325
- wb = @workbooks.find { |w| w.parcel_id == parcel_id }
326
- if wb.nil?
327
- project = Opencdd::Parcel::Workbook::ProjectInfo.new(
328
- project_id: parcel_id, parcel_id: parcel_id,
329
- multi_language: "", base_language: "en",
330
- )
331
- wb = Opencdd::Parcel::Workbook.new(sheets: [], sheetmap: [], project: project)
332
- @workbooks << wb
333
- end
334
- wb.register_sheet(sheet)
335
- self
336
- end
337
-
338
- def dictionaries
339
- @workbooks.map do |wb|
340
- Dictionary.new(
341
- parcel_id: wb.parcel_id,
342
- source_language: wb.base_language,
343
- translation_languages: parse_translation_languages(wb),
344
- meta_class_irdis: wb.sheets.map(&:meta_class_irdi).compact.uniq,
345
- )
346
- end
347
- end
348
-
349
- def rename_entity(old_code, new_code)
350
- old_code_s = old_code.to_s
351
- new_code_s = new_code.to_s
352
- return self if old_code_s == new_code_s
353
-
354
- target = find_by_code(old_code_s)
355
- return self unless target
356
-
357
- clash = find_by_code(new_code_s)
358
- if clash && clash.irdi != target.irdi
359
- warn "Cannot rename #{old_code_s} → #{new_code_s}: target code already in use by #{clash.irdi}"
360
- return self
361
- end
362
-
363
- old_irdi = target.irdi
364
- new_irdi = old_irdi.with_code(new_code_s)
365
-
366
- @entities_by_irdi.delete(old_irdi)
367
- @entities_by_code[old_code_s].delete(target)
368
-
369
- ref_property_ids = Opencdd::PropertyIds::REGISTRY.select do |_, e|
370
- REFERENCE_VALUE_KINDS.include?(e.value_kind)
371
- end.keys
372
-
373
- entities.each do |entity|
374
- rewrite_back_references!(entity, old_irdi, new_irdi, ref_property_ids)
375
- end
376
-
377
- code_pid = target.class.default_code_property_id(target.meta_class_irdi)
378
- target.replace_code_value!(code_pid, new_code_s)
379
- target.replace_irdi!(new_irdi)
380
-
381
- @entities_by_irdi[new_irdi] = target
382
- @entities_by_code[new_code_s] << target
383
-
384
- rebuild_symbol_table!
385
- self
386
- end
387
-
388
- def apply_view_control(klass, view_control)
389
- properties = effective_properties.for(klass).to_a
390
- return properties unless view_control.is_a?(Opencdd::ViewControl)
391
-
392
- controlled = view_control.controlled_class_irdis
393
- k = coerce_entity(klass)
394
- return properties unless k.is_a?(Opencdd::Klass) && controlled.include?(k.irdi)
395
-
396
- shown = view_control.shown_property_irdis
397
- lookup = properties.each_with_object({}) { |p, h| h[p.irdi] = p }
398
- shown.filter_map { |irdi| lookup[irdi] }
399
- end
400
-
401
- def properties_of(klass)
402
- k = coerce_entity(klass)
403
- return [] unless k.is_a?(Opencdd::Klass)
404
- k.properties_on_class(self)
405
- end
406
-
407
- def classes_with_property(property)
408
- irdi = coerce_irdi(property)
409
- return [] unless irdi
410
- @class_by_property_irdi&.fetch(irdi, []) || []
411
- end
412
-
413
- def value_list_of(property)
414
- prop = coerce_entity(property)
415
- return nil unless prop.is_a?(Opencdd::Property)
416
-
417
- relations.each do |r|
418
- next unless r.predication?
419
- next unless r.domain_irdis.include?(prop.irdi)
420
- vl = r.codomain_irdi && find(r.codomain_irdi)
421
- return vl if vl.is_a?(Opencdd::ValueList)
422
- end
423
-
424
- return nil unless prop.enum?
425
- identifier = value_list_identifier_of(prop)
426
- return nil unless identifier
427
- vl = resolve_reference(identifier)
428
- vl if vl.is_a?(Opencdd::ValueList)
429
- end
430
-
431
- def value_list_identifier_of(property)
432
- case property.parsed_data_type
433
- when Opencdd::DataType::EnumStringType, Opencdd::DataType::EnumReferenceType
434
- property.parsed_data_type.value_list_identifier
435
- end
436
- end
437
-
438
- def terms_of(value_list)
439
- return [] if value_list.nil?
440
- value_list.term_irdis.map { |i| find(i) }.compact
441
- end
442
-
443
- def relations_for(domain: nil, codomain: nil)
444
- dom = coerce_irdi(domain)
445
- cod = coerce_irdi(codomain)
446
- relations.select do |r|
447
- (dom.nil? || r.domain_irdis.include?(dom)) &&
448
- (cod.nil? || r.codomain_irdi == cod)
449
- end
450
- end
451
-
452
- def functions_involving(property)
453
- irdi = coerce_irdi(property)
454
- return [] unless irdi
455
- relations.select do |r|
456
- r.function? && (r.domain_irdis.include?(irdi) || r.codomain_irdi == irdi)
457
- end
458
- end
459
-
460
- def merge(other)
461
- raise TypeError, "merge expects a Opencdd::Database" unless other.is_a?(Opencdd::Database)
462
- other.entities.each { |e| add_entity(e) }
463
- finalize!
464
- self
465
- end
466
-
467
- # Applies a Parcel change request (CR) to this database.
468
- #
469
- # +cr+ is a +Opencdd::Database+ whose entities represent the
470
- # additions and updates to apply. +removals:+ is an Array of
471
- # IRDIs (Strings or +Opencdd::IRDI+ instances) identifying entities
472
- # to delete from this database.
473
- #
474
- # Unlike +#merge+, which is purely additive, +#apply_change_request+
475
- # honors an explicit removal list. The CR's own entities are
476
- # upserted first (so an update-then-remove ordering resolves to
477
- # remove), then removals are applied.
478
- #
479
- # Returns +self+ for chaining.
480
- def apply_change_request(cr, removals: [])
481
- raise TypeError, "apply_change_request expects a Opencdd::Database" unless cr.is_a?(Opencdd::Database)
482
- cr.entities.each { |e| add_entity(e) }
483
- Array(removals).each { |r| remove_by_irdi(r) }
484
- finalize!
485
- self
486
- end
487
-
488
- def remove_by_irdi(ref)
489
- irdi = ref.is_a?(Opencdd::IRDI) ? ref : Opencdd::IRDI.parse(ref.to_s)
490
- return unless irdi
491
- remove_entity_by_irdi!(irdi)
492
- @entity_sources.delete(irdi)
493
- self
494
- end
495
-
496
125
  def each(&block)
497
126
  @entities_by_irdi.each_value(&block)
498
127
  end
499
128
 
500
- include Enumerable
501
-
502
- # ── YAML persistence (lutaml-model, CDD-native) ──────────
503
- # Canonical YAML shape using semantic CDD attribute names
504
- # (preferred_name, superclass, class_type) — not wire-format
505
- # keys (MDC_P004, MDC_P010). See TODO.impl/33.
506
-
507
- def to_yaml(*args)
508
- Opencdd::Model::YamlDatabase.from_database(self).to_yaml(*args)
509
- end
510
-
511
- def self.from_yaml(yaml)
512
- ydb = Opencdd::Model::YamlDatabase.from_yaml(yaml)
513
- ydb.to_database
514
- end
515
-
516
- # ── Per-entity YAML persistence (lutaml-store) ───────────
517
- # One .yaml file per entity in a directory layout. Diff-friendly
518
- # at the entity level — one git diff shows exactly what changed.
519
- # See TODO.impl/34.
520
-
521
- def save_to_directory(path)
522
- Opencdd::Model::EntityStore.new(path).save_database(self)
523
- self
524
- end
525
-
526
- def self.load_from_directory(path)
527
- Opencdd::Model::EntityStore.new(path).load_database
528
- end
529
-
530
- def semantically_equal?(other)
531
- return false unless other.is_a?(Opencdd::Database)
532
- return false unless entities.size == other.entities.size
533
- entities.all? do |e|
534
- oe = other.find(e.irdi)
535
- oe && e.type == oe.type && e.properties == oe.properties
536
- end
537
- end
538
-
539
129
  def to_s
540
130
  "#<#{self.class.name} classes=#{classes.size} properties=#{properties.size} " \
541
131
  "units=#{units.size} value_lists=#{value_lists.size} value_terms=#{value_terms.size} " \
@@ -546,36 +136,6 @@ module Opencdd
546
136
 
547
137
  private
548
138
 
549
- REFERENCE_VALUE_KINDS = %i[identifier_ref set_of_refs class_ref].freeze
550
-
551
- def rewrite_back_references!(entity, old_irdi, new_irdi, ref_property_ids)
552
- ref_property_ids.each do |pid|
553
- raw = entity.properties[pid]
554
- next if raw.nil?
555
- entry = Opencdd::PropertyIds::REGISTRY[pid]
556
- case entry.value_kind
557
- when :identifier_ref
558
- parsed = Opencdd::IRDI.parse(raw.to_s)
559
- next unless parsed == old_irdi
560
- entity.properties[pid] = new_irdi.to_s
561
- when :set_of_refs
562
- # SSOT: split + rejoin via StructuredValues.
563
- elements = Opencdd::StructuredValues.unwrap_and_split(raw)
564
- next if elements.empty?
565
- mapped = elements.map { |e| e == old_irdi.to_s ? new_irdi.to_s : e }
566
- entity.properties[pid] = Opencdd::StructuredValues.rejoin(mapped)
567
- when :class_ref
568
- entity.properties[pid] = substitute_class_ref_value(raw.to_s, old_irdi, new_irdi)
569
- end
570
- end
571
- end
572
-
573
- def substitute_class_ref_value(raw, old_irdi, new_irdi)
574
- out = raw.sub(old_irdi.to_s, new_irdi.to_s)
575
- return out if old_irdi.code.nil? || old_irdi.code == new_irdi.code
576
- out.sub(old_irdi.code, new_irdi.code)
577
- end
578
-
579
139
  def bind_symbol(name, entity)
580
140
  key = name.to_s
581
141
  existing = @symbol_table[key]
@@ -586,134 +146,5 @@ module Opencdd
586
146
  @symbol_table[key] = entity
587
147
  self
588
148
  end
589
- def rebuild_symbol_table!
590
- entities.each { |e| register_entity_symbols(e) }
591
- end
592
-
593
- def link_class_hierarchy!
594
- classes.each do |klass|
595
- next if klass.parent_irdi
596
- parent_id = klass.parent_property_id || Opencdd::PropertyIds::MDC_P010
597
- parent_raw = klass.properties[parent_id]
598
- next if parent_raw.nil? || parent_raw.to_s.strip.empty?
599
- target = resolve_reference(parent_raw)
600
- next unless target
601
- klass.attach_parent_irdi(target.irdi)
602
- target.add_child(klass)
603
- end
604
- end
605
-
606
- def link_property_classes!
607
- @class_by_property_irdi = Hash.new { |h, k| h[k] = [] }
608
- link_property_classes_via_relations!
609
- link_property_classes_via_definition_class!
610
- end
611
-
612
- # Strategy 1: walk Relation entities. Parcel files are
613
- # relation-centric — each Relation row says "this domain
614
- # entity is linked to this codomain entity via this relation
615
- # type." Predication and function relations imply
616
- # "Klass declares Property" (or vice versa).
617
- def link_property_classes_via_relations!
618
- relations.each do |r|
619
- next unless r.predication? || r.function?
620
- next if r.domain_irdis.empty?
621
- next unless r.codomain_irdi
622
- r.domain_irdis.each do |d|
623
- next unless d
624
- src = find(d)
625
- dst = find(r.codomain_irdi)
626
- next unless src && dst
627
-
628
- if src.is_a?(Opencdd::Klass) && dst.is_a?(Opencdd::Property)
629
- src.declare_property(dst.irdi)
630
- @class_by_property_irdi[dst.irdi] << src unless @class_by_property_irdi[dst.irdi].include?(src)
631
- elsif src.is_a?(Opencdd::Property) && dst.is_a?(Opencdd::Klass)
632
- dst.declare_property(src.irdi)
633
- @class_by_property_irdi[src.irdi] << dst unless @class_by_property_irdi[src.irdi].include?(dst)
634
- end
635
- end
636
- end
637
- end
638
-
639
- # Strategy 2: walk Property entities' definition_class
640
- # (MDC_P021). CDDAL files are often property-centric and
641
- # don't carry explicit Relation rows — the link is implied
642
- # by each property's definition_class field. declare_property
643
- # deduplicates, so running both strategies is safe.
644
- def link_property_classes_via_definition_class!
645
- properties.each do |prop|
646
- dc_raw = prop.properties[Opencdd::PropertyIds::MDC_P021]
647
- next unless dc_raw
648
- target = resolve_reference(dc_raw)
649
- next unless target.is_a?(Opencdd::Klass)
650
- target.declare_property(prop.irdi)
651
- @class_by_property_irdi[prop.irdi] << target unless @class_by_property_irdi[prop.irdi].include?(target)
652
- end
653
- end
654
-
655
- def link_value_lists!
656
- properties.each do |prop|
657
- next unless prop.enum?
658
- identifier = value_list_identifier_of(prop)
659
- vl_irdi = identifier && resolve_reference(identifier)&.irdi
660
- next unless vl_irdi
661
- vl = find(vl_irdi)
662
- next unless vl.is_a?(Opencdd::ValueList)
663
- @class_by_property_irdi[prop.irdi] << vl unless @class_by_property_irdi[prop.irdi].include?(vl)
664
- end
665
- end
666
-
667
- def validate_parcel_id!(parcel_id)
668
- unless parcel_id.is_a?(String) && parcel_id.match?(PARCEL_ID_PATTERN)
669
- raise ArgumentError, "invalid parcel_id: #{parcel_id.inspect}"
670
- end
671
- end
672
-
673
- def normalize_meta_class_irdis(raw)
674
- codes = Array(raw).map { |v| v.to_s.split("#").last }
675
- FORCED_META_CLASSES.each { |c| codes << c unless codes.include?(c) }
676
- codes.uniq
677
- end
678
-
679
- def build_sheetmap_for(parcel_id, sheets)
680
- entries = [
681
- Opencdd::Parcel::Workbook::SheetMapEntry.new(
682
- project_id: parcel_id, parcel_id: parcel_id, class_irdi: nil,
683
- content_no: 0, sheet_no: 2, sheet_name: "pcls_LOCAL",
684
- type: "PARCEL_LIST", target: "",
685
- ),
686
- ]
687
- sheets.each_with_index do |sheet, idx|
688
- entries << Opencdd::Parcel::Workbook::SheetMapEntry.new(
689
- project_id: parcel_id, parcel_id: parcel_id,
690
- class_irdi: sheet.meta_class_irdi,
691
- content_no: 0, sheet_no: idx + 3,
692
- sheet_name: sheet.name,
693
- type: Opencdd::MetaClasses.sheet_type_for_type(Opencdd::MetaClasses.type_for(sheet.meta_class_code)),
694
- target: "",
695
- )
696
- end
697
- entries
698
- end
699
-
700
- def parse_translation_languages(workbook)
701
- multi = workbook.project&.multi_language.to_s
702
- multi.split(",").map(&:strip).reject(&:empty?)
703
- end
704
-
705
- def remove_entity_by_irdi!(irdi)
706
- entity = @entities_by_irdi.delete(irdi)
707
- return unless entity
708
- if entity.code
709
- list = @entities_by_code[entity.code]
710
- list.delete(entity)
711
- @entities_by_code.delete(entity.code) if list.empty?
712
- end
713
- if entity.type
714
- @entities_by_type[entity.type].delete(entity)
715
- end
716
- @symbol_table.delete_if { |_, e| e.equal?(entity) }
717
- end
718
149
  end
719
150
  end