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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74a134259cf9d336b0e401139d10fcda58d1df883ec52312e04eadd2c6016bb9
|
|
4
|
+
data.tar.gz: b2c6cd320d420f7258c4116c218e0de50850d9ebb1745f81e4b06c33113196ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5147138e2581efbaf28bf224de9c56ed541040cdedec3569436c786419df35fc190a144982d78d53ca7d1ce4d4b7f73b7cd21847d5c3597e1987b0ade886db85
|
|
7
|
+
data.tar.gz: 9bd7ee2d030cfb9792177763875b02114aca94534754986fe17ba6c7b24dcf5cd47ad9361fd9f9533c8d3fcbdc4bcc296d4610b335809358d5a6d3ae9844f0f0
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
class Database
|
|
5
|
+
# Finalization: link class hierarchy, property↔class links,
|
|
6
|
+
# value-list links, normalize reference collections, rebuild
|
|
7
|
+
# the symbol table.
|
|
8
|
+
module Finalization
|
|
9
|
+
def finalize!
|
|
10
|
+
normalize_reference_collections!
|
|
11
|
+
link_class_hierarchy!
|
|
12
|
+
link_property_classes!
|
|
13
|
+
link_value_lists!
|
|
14
|
+
rebuild_symbol_table!
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def normalize_reference_collections!
|
|
19
|
+
each_reference_collection do |entity, pid, _raw, elements|
|
|
20
|
+
entity.write_property!(pid, Opencdd::StructuredValues.rejoin(elements))
|
|
21
|
+
end
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def each_reference_collection
|
|
26
|
+
return enum_for(:each_reference_collection) unless block_given?
|
|
27
|
+
set_property_ids = Opencdd::PropertyIds::REGISTRY
|
|
28
|
+
.select { |_, e| e.value_kind == :set_of_refs }
|
|
29
|
+
.keys
|
|
30
|
+
entities.each do |entity|
|
|
31
|
+
set_property_ids.each do |pid|
|
|
32
|
+
raw = entity.properties[pid]
|
|
33
|
+
next if raw.nil? || raw.to_s.strip.empty?
|
|
34
|
+
elements = Opencdd::StructuredValues.unwrap_and_split(raw)
|
|
35
|
+
next if elements.empty?
|
|
36
|
+
yield(entity, pid, raw, elements)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def rebuild_symbol_table!
|
|
42
|
+
entities.each { |e| register_entity_symbols(e) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def link_class_hierarchy!
|
|
48
|
+
classes.each do |klass|
|
|
49
|
+
next if klass.parent_irdi
|
|
50
|
+
parent_id = klass.parent_property_id || Opencdd::PropertyIds::MDC_P010
|
|
51
|
+
parent_raw = klass.properties[parent_id]
|
|
52
|
+
next if parent_raw.nil? || parent_raw.to_s.strip.empty?
|
|
53
|
+
target = resolve_reference(parent_raw)
|
|
54
|
+
next unless target
|
|
55
|
+
next unless target.is_a?(Opencdd::Klass)
|
|
56
|
+
klass.attach_parent_irdi(target.irdi)
|
|
57
|
+
target.add_child(klass)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def link_property_classes!
|
|
62
|
+
@class_by_property_irdi = Hash.new { |h, k| h[k] = [] }
|
|
63
|
+
link_property_classes_via_relations!
|
|
64
|
+
link_property_classes_via_definition_class!
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def link_property_classes_via_relations!
|
|
68
|
+
relations.each do |r|
|
|
69
|
+
next unless r.predication? || r.function?
|
|
70
|
+
next if r.domain_irdis.empty?
|
|
71
|
+
next unless r.codomain_irdi
|
|
72
|
+
r.domain_irdis.each do |d|
|
|
73
|
+
next unless d
|
|
74
|
+
src = find(d)
|
|
75
|
+
dst = find(r.codomain_irdi)
|
|
76
|
+
next unless src && dst
|
|
77
|
+
|
|
78
|
+
if src.is_a?(Opencdd::Klass) && dst.is_a?(Opencdd::Property)
|
|
79
|
+
src.declare_property(dst.irdi)
|
|
80
|
+
@class_by_property_irdi[dst.irdi] << src unless @class_by_property_irdi[dst.irdi].include?(src)
|
|
81
|
+
elsif src.is_a?(Opencdd::Property) && dst.is_a?(Opencdd::Klass)
|
|
82
|
+
dst.declare_property(src.irdi)
|
|
83
|
+
@class_by_property_irdi[src.irdi] << dst unless @class_by_property_irdi[src.irdi].include?(dst)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def link_property_classes_via_definition_class!
|
|
90
|
+
properties.each do |prop|
|
|
91
|
+
dc_raw = prop.properties[Opencdd::PropertyIds::MDC_P021]
|
|
92
|
+
next unless dc_raw
|
|
93
|
+
target = resolve_reference(dc_raw)
|
|
94
|
+
next unless target.is_a?(Opencdd::Klass)
|
|
95
|
+
target.declare_property(prop.irdi)
|
|
96
|
+
@class_by_property_irdi[prop.irdi] << target unless @class_by_property_irdi[prop.irdi].include?(target)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def link_value_lists!
|
|
101
|
+
properties.each do |prop|
|
|
102
|
+
next unless prop.enum?
|
|
103
|
+
identifier = value_list_identifier_of(prop)
|
|
104
|
+
vl_irdi = identifier && resolve_reference(identifier)&.irdi
|
|
105
|
+
next unless vl_irdi
|
|
106
|
+
vl = find(vl_irdi)
|
|
107
|
+
next unless vl.is_a?(Opencdd::ValueList)
|
|
108
|
+
@class_by_property_irdi[prop.irdi] << vl unless @class_by_property_irdi[prop.irdi].include?(vl)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
class Database
|
|
5
|
+
# Mutations: rename, merge, apply_change_request, remove,
|
|
6
|
+
# apply_view_control, and back-reference rewriting.
|
|
7
|
+
module Mutations
|
|
8
|
+
REFERENCE_VALUE_KINDS = %i[identifier_ref set_of_refs class_ref].freeze
|
|
9
|
+
|
|
10
|
+
def rename_entity(old_code, new_code)
|
|
11
|
+
old_code_s = old_code.to_s
|
|
12
|
+
new_code_s = new_code.to_s
|
|
13
|
+
return self if old_code_s == new_code_s
|
|
14
|
+
|
|
15
|
+
target = find_by_code(old_code_s)
|
|
16
|
+
return self unless target
|
|
17
|
+
|
|
18
|
+
clash = find_by_code(new_code_s)
|
|
19
|
+
if clash && clash.irdi != target.irdi
|
|
20
|
+
warn "Cannot rename #{old_code_s} → #{new_code_s}: target code already in use by #{clash.irdi}"
|
|
21
|
+
return self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
old_irdi = target.irdi
|
|
25
|
+
new_irdi = old_irdi.with_code(new_code_s)
|
|
26
|
+
|
|
27
|
+
@entities_by_irdi.delete(old_irdi)
|
|
28
|
+
@entities_by_code[old_code_s].delete(target)
|
|
29
|
+
|
|
30
|
+
ref_property_ids = Opencdd::PropertyIds::REGISTRY.select do |_, e|
|
|
31
|
+
REFERENCE_VALUE_KINDS.include?(e.value_kind)
|
|
32
|
+
end.keys
|
|
33
|
+
|
|
34
|
+
entities.each do |entity|
|
|
35
|
+
rewrite_back_references!(entity, old_irdi, new_irdi, ref_property_ids)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
code_pid = target.class.default_code_property_id(target.meta_class_irdi)
|
|
39
|
+
target.replace_code_value!(code_pid, new_code_s)
|
|
40
|
+
target.replace_irdi!(new_irdi)
|
|
41
|
+
|
|
42
|
+
@entities_by_irdi[new_irdi] = target
|
|
43
|
+
@entities_by_code[new_code_s] << target
|
|
44
|
+
|
|
45
|
+
rebuild_symbol_table!
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def merge(other)
|
|
50
|
+
raise TypeError, "merge expects a Opencdd::Database" unless other.is_a?(Opencdd::Database)
|
|
51
|
+
other.entities.each { |e| add_entity(e) }
|
|
52
|
+
finalize!
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def apply_change_request(cr, removals: [])
|
|
57
|
+
raise TypeError, "apply_change_request expects a Opencdd::Database" unless cr.is_a?(Opencdd::Database)
|
|
58
|
+
cr.entities.each { |e| add_entity(e) }
|
|
59
|
+
Array(removals).each { |r| remove_by_irdi(r) }
|
|
60
|
+
finalize!
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def remove_by_irdi(ref)
|
|
65
|
+
irdi = ref.is_a?(Opencdd::IRDI) ? ref : Opencdd::IRDI.parse(ref.to_s)
|
|
66
|
+
return unless irdi
|
|
67
|
+
remove_entity_by_irdi!(irdi)
|
|
68
|
+
@entity_sources.delete(irdi)
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def apply_view_control(klass, view_control)
|
|
73
|
+
properties = effective_properties.for(klass).to_a
|
|
74
|
+
return properties unless view_control.is_a?(Opencdd::ViewControl)
|
|
75
|
+
|
|
76
|
+
controlled = view_control.controlled_class_irdis
|
|
77
|
+
k = coerce_entity(klass)
|
|
78
|
+
return properties unless k.is_a?(Opencdd::Klass) && controlled.include?(k.irdi)
|
|
79
|
+
|
|
80
|
+
shown = view_control.shown_property_irdis
|
|
81
|
+
lookup = properties.each_with_object({}) { |p, h| h[p.irdi] = p }
|
|
82
|
+
shown.filter_map { |irdi| lookup[irdi] }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def rewrite_back_references!(entity, old_irdi, new_irdi, ref_property_ids)
|
|
88
|
+
ref_property_ids.each do |pid|
|
|
89
|
+
raw = entity.properties[pid]
|
|
90
|
+
next if raw.nil?
|
|
91
|
+
entry = Opencdd::PropertyIds::REGISTRY[pid]
|
|
92
|
+
case entry.value_kind
|
|
93
|
+
when :identifier_ref
|
|
94
|
+
parsed = Opencdd::IRDI.parse(raw.to_s)
|
|
95
|
+
next unless parsed == old_irdi
|
|
96
|
+
entity.write_property!(pid, new_irdi.to_s)
|
|
97
|
+
when :set_of_refs
|
|
98
|
+
elements = Opencdd::StructuredValues.unwrap_and_split(raw)
|
|
99
|
+
next if elements.empty?
|
|
100
|
+
mapped = elements.map { |e| e == old_irdi.to_s ? new_irdi.to_s : e }
|
|
101
|
+
entity.write_property!(pid, Opencdd::StructuredValues.rejoin(mapped))
|
|
102
|
+
when :class_ref
|
|
103
|
+
entity.write_property!(pid, substitute_class_ref_value(raw.to_s, old_irdi, new_irdi))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def substitute_class_ref_value(raw, old_irdi, new_irdi)
|
|
109
|
+
out = raw.sub(old_irdi.to_s, new_irdi.to_s)
|
|
110
|
+
return out if old_irdi.code.nil? || old_irdi.code == new_irdi.code
|
|
111
|
+
out.sub(old_irdi.code, new_irdi.code)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def remove_entity_by_irdi!(irdi)
|
|
115
|
+
entity = @entities_by_irdi.delete(irdi)
|
|
116
|
+
return unless entity
|
|
117
|
+
if entity.code
|
|
118
|
+
list = @entities_by_code[entity.code]
|
|
119
|
+
list.delete(entity)
|
|
120
|
+
@entities_by_code.delete(entity.code) if list.empty?
|
|
121
|
+
end
|
|
122
|
+
if entity.type
|
|
123
|
+
@entities_by_type[entity.type].delete(entity)
|
|
124
|
+
end
|
|
125
|
+
@symbol_table.delete_if { |_, e| e.equal?(entity) }
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
class Database
|
|
5
|
+
# Parcel workbook ingestion and dictionary lifecycle (add/drop/
|
|
6
|
+
# register_external). Sheetmap construction and translation-
|
|
7
|
+
# language parsing.
|
|
8
|
+
module ParcelIntegration
|
|
9
|
+
def add_workbook(workbook)
|
|
10
|
+
@workbooks << workbook
|
|
11
|
+
parcel_id = workbook.parcel_id
|
|
12
|
+
workbook.each_sheet do |sheet|
|
|
13
|
+
next unless sheet.type && sheet.rows.any?
|
|
14
|
+
type = sheet.type
|
|
15
|
+
entity_class = Opencdd::MetaClasses.entity_class_for_type(type)
|
|
16
|
+
raise "Unknown entity type #{type.inspect} for sheet #{sheet.name.inspect}" unless entity_class
|
|
17
|
+
|
|
18
|
+
sheet.rows.each do |row|
|
|
19
|
+
entity = entity_class.from_row(
|
|
20
|
+
row,
|
|
21
|
+
schema: sheet.schema,
|
|
22
|
+
meta_class_irdi: sheet.meta_class_irdi,
|
|
23
|
+
)
|
|
24
|
+
add_entity(entity)
|
|
25
|
+
if parcel_id && entity.irdi
|
|
26
|
+
prev = @entity_sources[entity.irdi]
|
|
27
|
+
@entity_sources[entity.irdi] = prev.nil? ? parcel_id : prev
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def add_dictionary(dict)
|
|
35
|
+
validate_parcel_id!(dict.parcel_id)
|
|
36
|
+
source_language = dict.source_language || "en"
|
|
37
|
+
translation_languages = Array(dict.translation_languages)
|
|
38
|
+
meta_irdis = normalize_meta_class_irdis(dict.meta_class_irdis)
|
|
39
|
+
|
|
40
|
+
sheets = meta_irdis.map do |meta_irdi|
|
|
41
|
+
Opencdd::Parcel::Sheet.scaffold(
|
|
42
|
+
meta_class_irdi: meta_irdi,
|
|
43
|
+
parcel_id: dict.parcel_id,
|
|
44
|
+
source_language: source_language,
|
|
45
|
+
translation_languages: translation_languages,
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
project = Opencdd::Parcel::Workbook::ProjectInfo.new(
|
|
50
|
+
project_id: dict.parcel_id,
|
|
51
|
+
parcel_id: dict.parcel_id,
|
|
52
|
+
multi_language: translation_languages.join(","),
|
|
53
|
+
base_language: source_language,
|
|
54
|
+
)
|
|
55
|
+
sheetmap = build_sheetmap_for(dict.parcel_id, sheets)
|
|
56
|
+
workbook = Opencdd::Parcel::Workbook.new(
|
|
57
|
+
sheets: sheets, sheetmap: sheetmap, project: project,
|
|
58
|
+
)
|
|
59
|
+
@workbooks << workbook
|
|
60
|
+
workbook
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def drop_dictionary(parcel_id)
|
|
64
|
+
matching = @workbooks.select { |wb| wb.parcel_id == parcel_id }
|
|
65
|
+
raise ArgumentError, "no dictionary with parcel_id #{parcel_id.inspect}" if matching.empty?
|
|
66
|
+
|
|
67
|
+
irdis_to_drop = @entity_sources.select { |_, pid| pid == parcel_id }.keys
|
|
68
|
+
irdis_to_drop.each do |irdi|
|
|
69
|
+
remove_entity_by_irdi!(irdi)
|
|
70
|
+
@entity_sources.delete(irdi)
|
|
71
|
+
end
|
|
72
|
+
@workbooks.reject! { |wb| wb.parcel_id == parcel_id }
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def register_external_sheet(sheet, parcel_id:)
|
|
77
|
+
validate_parcel_id!(parcel_id)
|
|
78
|
+
wb = @workbooks.find { |w| w.parcel_id == parcel_id }
|
|
79
|
+
if wb.nil?
|
|
80
|
+
project = Opencdd::Parcel::Workbook::ProjectInfo.new(
|
|
81
|
+
project_id: parcel_id, parcel_id: parcel_id,
|
|
82
|
+
multi_language: "", base_language: "en",
|
|
83
|
+
)
|
|
84
|
+
wb = Opencdd::Parcel::Workbook.new(sheets: [], sheetmap: [], project: project)
|
|
85
|
+
@workbooks << wb
|
|
86
|
+
end
|
|
87
|
+
wb.register_sheet(sheet)
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def dictionaries
|
|
92
|
+
@workbooks.map do |wb|
|
|
93
|
+
Dictionary.new(
|
|
94
|
+
parcel_id: wb.parcel_id,
|
|
95
|
+
source_language: wb.base_language,
|
|
96
|
+
translation_languages: parse_translation_languages(wb),
|
|
97
|
+
meta_class_irdis: wb.sheets.map(&:meta_class_irdi).compact.uniq,
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def validate_parcel_id!(parcel_id)
|
|
105
|
+
unless parcel_id.is_a?(String) && parcel_id.match?(PARCEL_ID_PATTERN)
|
|
106
|
+
raise ArgumentError, "invalid parcel_id: #{parcel_id.inspect}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def normalize_meta_class_irdis(raw)
|
|
111
|
+
codes = Array(raw).map { |v| v.to_s.split("#").last }
|
|
112
|
+
FORCED_META_CLASSES.each { |c| codes << c unless codes.include?(c) }
|
|
113
|
+
codes.uniq
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_sheetmap_for(parcel_id, sheets)
|
|
117
|
+
entries = [
|
|
118
|
+
Opencdd::Parcel::Workbook::SheetMapEntry.new(
|
|
119
|
+
project_id: parcel_id, parcel_id: parcel_id, class_irdi: nil,
|
|
120
|
+
content_no: 0, sheet_no: 2, sheet_name: "pcls_LOCAL",
|
|
121
|
+
type: "PARCEL_LIST", target: "",
|
|
122
|
+
),
|
|
123
|
+
]
|
|
124
|
+
sheets.each_with_index do |sheet, idx|
|
|
125
|
+
entries << Opencdd::Parcel::Workbook::SheetMapEntry.new(
|
|
126
|
+
project_id: parcel_id, parcel_id: parcel_id,
|
|
127
|
+
class_irdi: sheet.meta_class_irdi,
|
|
128
|
+
content_no: 0, sheet_no: idx + 3,
|
|
129
|
+
sheet_name: sheet.name,
|
|
130
|
+
type: Opencdd::MetaClasses.sheet_type_for_type(Opencdd::MetaClasses.type_for(sheet.meta_class_code)),
|
|
131
|
+
target: "",
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
entries
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def parse_translation_languages(workbook)
|
|
138
|
+
multi = workbook.project&.multi_language.to_s
|
|
139
|
+
multi.split(",").map(&:strip).reject(&:empty?)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
class Database
|
|
5
|
+
# YAML persistence: whole-database YAML (to_yaml via
|
|
6
|
+
# Model::YamlDatabase) and per-entity directory persistence
|
|
7
|
+
# (save_to_directory via Model::EntityStore). Class-method
|
|
8
|
+
# counterparts (from_yaml, load_from_directory) are defined
|
|
9
|
+
# on Database directly in database.rb.
|
|
10
|
+
module Persistence
|
|
11
|
+
def to_yaml(*args)
|
|
12
|
+
Opencdd::Model::YamlDatabase.from_database(self).to_yaml(*args)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def save_to_directory(path)
|
|
16
|
+
Opencdd::Model::EntityStore.new(path).save_database(self)
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def semantically_equal?(other)
|
|
21
|
+
return false unless other.is_a?(Opencdd::Database)
|
|
22
|
+
return false unless entities.size == other.entities.size
|
|
23
|
+
entities.all? do |e|
|
|
24
|
+
oe = other.find(e.irdi)
|
|
25
|
+
oe && e.type == oe.type && e.properties == oe.properties
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
class Database
|
|
5
|
+
# Graph queries: find, coerce, powertype API, tree walkers,
|
|
6
|
+
# property/class/value-list lookups, relation queries.
|
|
7
|
+
module Queries
|
|
8
|
+
def resolve_reference(ref)
|
|
9
|
+
return nil if ref.nil?
|
|
10
|
+
return ref if ref.is_a?(Opencdd::Entity)
|
|
11
|
+
|
|
12
|
+
key = ref.to_s.strip
|
|
13
|
+
return nil if key.empty?
|
|
14
|
+
|
|
15
|
+
if key.include?("/") || key.include?("#")
|
|
16
|
+
irdi = Opencdd::IRDI.parse(key)
|
|
17
|
+
return find(irdi)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
entity = @symbol_table[key]
|
|
21
|
+
return entity if entity
|
|
22
|
+
|
|
23
|
+
by_code = find_by_code(key)
|
|
24
|
+
return by_code if by_code
|
|
25
|
+
|
|
26
|
+
irdi = Opencdd::IRDI.parse(key)
|
|
27
|
+
irdi ? find(irdi) : nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def find(irdi)
|
|
31
|
+
return nil if irdi.nil?
|
|
32
|
+
key = Opencdd::IRDI === irdi ? irdi : Opencdd::IRDI.parse(irdi)
|
|
33
|
+
return nil unless key
|
|
34
|
+
@entities_by_irdi[key]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def find_by_code(code)
|
|
38
|
+
matches = @entities_by_code[code.to_s]
|
|
39
|
+
return nil if matches.empty?
|
|
40
|
+
matches.first
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def find_all_by_code(code)
|
|
44
|
+
@entities_by_code[code.to_s].dup
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def find_by_name(name, type: nil, lang: :en)
|
|
48
|
+
pools = type ? [@entities_by_type[type]].compact : @entities_by_type.values
|
|
49
|
+
pools.each do |pool|
|
|
50
|
+
hit = pool.find { |e| e.preferred_name(lang).to_s.casecmp(name.to_s).zero? }
|
|
51
|
+
return hit if hit
|
|
52
|
+
end
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def coerce_entity(value)
|
|
57
|
+
return nil if value.nil?
|
|
58
|
+
return value if value.is_a?(Opencdd::Entity)
|
|
59
|
+
find(value)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def coerce_irdi(value)
|
|
63
|
+
return nil if value.nil?
|
|
64
|
+
return value.irdi if value.is_a?(Opencdd::Entity)
|
|
65
|
+
return value if value.is_a?(Opencdd::IRDI)
|
|
66
|
+
Opencdd::IRDI.parse(value.to_s)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def root_classes
|
|
70
|
+
classes.select { |c| c.parent_irdi.nil? }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ── Powertype accessors (4-layer ontology) ──────────────
|
|
74
|
+
|
|
75
|
+
def categorical_classes
|
|
76
|
+
classes.select(&:powertype?)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def instances_of(categorical_klass)
|
|
80
|
+
k = coerce_entity(categorical_klass)
|
|
81
|
+
return [] unless k.is_a?(Opencdd::Klass)
|
|
82
|
+
k.categorical_instances(self)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def valid_class_reference?(categorical_klass, value)
|
|
86
|
+
target = coerce_entity(value)
|
|
87
|
+
return false unless target
|
|
88
|
+
instances_of(categorical_klass).any? { |c| c.irdi == target.irdi }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# ── Tree walkers ─────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
def class_tree(fields: Opencdd::ClassTree::DEFAULT_FIELDS)
|
|
94
|
+
Opencdd::ClassTree.new(self, fields: fields)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def effective_properties
|
|
98
|
+
@effective_properties ||= Opencdd::EffectiveProperties.new(self)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def composition_tree(klass, max_depth: 10)
|
|
102
|
+
Opencdd::CompositionTree.new(self).for(klass, max_depth: max_depth)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def relation_tree(root = nil, max_depth: 10)
|
|
106
|
+
Opencdd::RelationTree.new(self).for(root, max_depth: max_depth)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# ── Property / class / value-list lookups ────────────────
|
|
110
|
+
|
|
111
|
+
def properties_of(klass)
|
|
112
|
+
k = coerce_entity(klass)
|
|
113
|
+
return [] unless k.is_a?(Opencdd::Klass)
|
|
114
|
+
k.properties_on_class(self)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def classes_with_property(property)
|
|
118
|
+
irdi = coerce_irdi(property)
|
|
119
|
+
return [] unless irdi
|
|
120
|
+
@class_by_property_irdi&.fetch(irdi, []) || []
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def value_list_of(property)
|
|
124
|
+
prop = coerce_entity(property)
|
|
125
|
+
return nil unless prop.is_a?(Opencdd::Property)
|
|
126
|
+
|
|
127
|
+
relations.each do |r|
|
|
128
|
+
next unless r.predication?
|
|
129
|
+
next unless r.domain_irdis.include?(prop.irdi)
|
|
130
|
+
vl = r.codomain_irdi && find(r.codomain_irdi)
|
|
131
|
+
return vl if vl.is_a?(Opencdd::ValueList)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
return nil unless prop.enum?
|
|
135
|
+
identifier = value_list_identifier_of(prop)
|
|
136
|
+
return nil unless identifier
|
|
137
|
+
vl = resolve_reference(identifier)
|
|
138
|
+
vl if vl.is_a?(Opencdd::ValueList)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def value_list_identifier_of(property)
|
|
142
|
+
case property.parsed_data_type
|
|
143
|
+
when Opencdd::DataType::EnumStringType, Opencdd::DataType::EnumReferenceType
|
|
144
|
+
property.parsed_data_type.value_list_identifier
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def terms_of(value_list)
|
|
149
|
+
return [] if value_list.nil?
|
|
150
|
+
value_list.term_irdis.map { |i| find(i) }.compact
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def relations_for(domain: nil, codomain: nil)
|
|
154
|
+
dom = coerce_irdi(domain)
|
|
155
|
+
cod = coerce_irdi(codomain)
|
|
156
|
+
relations.select do |r|
|
|
157
|
+
(dom.nil? || r.domain_irdis.include?(dom)) &&
|
|
158
|
+
(cod.nil? || r.codomain_irdi == cod)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def functions_involving(property)
|
|
163
|
+
irdi = coerce_irdi(property)
|
|
164
|
+
return [] unless irdi
|
|
165
|
+
relations.select do |r|
|
|
166
|
+
r.function? && (r.domain_irdis.include?(irdi) || r.codomain_irdi == irdi)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|