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.
- checksums.yaml +4 -4
- data/lib/opencdd/database/finalization.rb +113 -0
- data/lib/opencdd/database/mutations.rb +129 -0
- data/lib/opencdd/database/parcel_integration.rb +143 -0
- data/lib/opencdd/database/persistence.rb +30 -0
- data/lib/opencdd/database/queries.rb +171 -0
- data/lib/opencdd/database.rb +25 -594
- data/lib/opencdd/{model/yaml_entity.rb → entity/yaml.rb} +121 -50
- data/lib/opencdd/entity.rb +30 -7
- data/lib/opencdd/model/entity_store.rb +38 -40
- data/lib/opencdd/model/yaml_database.rb +3 -5
- data/lib/opencdd/model.rb +4 -5
- data/lib/opencdd/version.rb +1 -1
- metadata +12 -4
data/lib/opencdd/database.rb
CHANGED
|
@@ -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
|