opencdd 0.1.1
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 +7 -0
- data/CLAUDE.md +486 -0
- data/README.md +304 -0
- data/bin/lint-no-raw-mdc +41 -0
- data/lib/cdd.rb +11 -0
- data/lib/opencdd/alias_table.rb +52 -0
- data/lib/opencdd/cddal/ast.rb +151 -0
- data/lib/opencdd/cddal/builder.rb +374 -0
- data/lib/opencdd/cddal/fetcher/in_memory.rb +32 -0
- data/lib/opencdd/cddal/fetcher/net_http.rb +84 -0
- data/lib/opencdd/cddal/fetcher.rb +18 -0
- data/lib/opencdd/cddal/generated_parser.rb +805 -0
- data/lib/opencdd/cddal/lexer.rb +193 -0
- data/lib/opencdd/cddal/parser.rb +19 -0
- data/lib/opencdd/cddal/resolver.rb +100 -0
- data/lib/opencdd/cddal/serializer.rb +210 -0
- data/lib/opencdd/cddal/value_serializer.rb +60 -0
- data/lib/opencdd/cddal.rb +63 -0
- data/lib/opencdd/class_tree.rb +80 -0
- data/lib/opencdd/class_type.rb +33 -0
- data/lib/opencdd/codegen/ts.rb +185 -0
- data/lib/opencdd/codegen.rb +7 -0
- data/lib/opencdd/composition_tree.rb +119 -0
- data/lib/opencdd/condition.rb +120 -0
- data/lib/opencdd/data_type.rb +143 -0
- data/lib/opencdd/database.rb +719 -0
- data/lib/opencdd/effective_properties.rb +119 -0
- data/lib/opencdd/entity/field_reader.rb +141 -0
- data/lib/opencdd/entity/field_registry.rb +99 -0
- data/lib/opencdd/entity/version_history.rb +62 -0
- data/lib/opencdd/entity.rb +255 -0
- data/lib/opencdd/exporters/json.rb +235 -0
- data/lib/opencdd/exporters/mermaid.rb +62 -0
- data/lib/opencdd/exporters/yaml.rb +16 -0
- data/lib/opencdd/exporters.rb +9 -0
- data/lib/opencdd/guid.rb +27 -0
- data/lib/opencdd/instance_rule.rb +70 -0
- data/lib/opencdd/irdi.rb +128 -0
- data/lib/opencdd/klass.rb +230 -0
- data/lib/opencdd/languages.rb +70 -0
- data/lib/opencdd/meta_class.rb +274 -0
- data/lib/opencdd/model/entity_store.rb +85 -0
- data/lib/opencdd/model/yaml_database.rb +49 -0
- data/lib/opencdd/model/yaml_entity.rb +196 -0
- data/lib/opencdd/model.rb +13 -0
- data/lib/opencdd/parcel/csv_reader.rb +35 -0
- data/lib/opencdd/parcel/csv_writer.rb +114 -0
- data/lib/opencdd/parcel/entity_manifest.rb +119 -0
- data/lib/opencdd/parcel/flat_dir_reader.rb +134 -0
- data/lib/opencdd/parcel/layout_detector.rb +115 -0
- data/lib/opencdd/parcel/metadata.rb +112 -0
- data/lib/opencdd/parcel/referenced_irdis.rb +91 -0
- data/lib/opencdd/parcel/scrape_verifier.rb +122 -0
- data/lib/opencdd/parcel/selector.rb +115 -0
- data/lib/opencdd/parcel/sharded_dir_reader.rb +151 -0
- data/lib/opencdd/parcel/sheet.rb +287 -0
- data/lib/opencdd/parcel/sheet_emitter.rb +171 -0
- data/lib/opencdd/parcel/sheet_schema.rb +253 -0
- data/lib/opencdd/parcel/workbook.rb +172 -0
- data/lib/opencdd/parcel/workbook_reader.rb +200 -0
- data/lib/opencdd/parcel/writer.rb +185 -0
- data/lib/opencdd/parcel.rb +175 -0
- data/lib/opencdd/parse_helpers.rb +73 -0
- data/lib/opencdd/property.rb +120 -0
- data/lib/opencdd/property_data_element_type.rb +44 -0
- data/lib/opencdd/property_ids.rb +202 -0
- data/lib/opencdd/reader.rb +103 -0
- data/lib/opencdd/relation.rb +88 -0
- data/lib/opencdd/relation_tree.rb +74 -0
- data/lib/opencdd/relation_type.rb +58 -0
- data/lib/opencdd/structured_values.rb +183 -0
- data/lib/opencdd/unit.rb +16 -0
- data/lib/opencdd/validator/class_reference_rule.rb +77 -0
- data/lib/opencdd/validator/condition_rule.rb +27 -0
- data/lib/opencdd/validator/data_type_rule.rb +26 -0
- data/lib/opencdd/validator/enum_rule.rb +27 -0
- data/lib/opencdd/validator/format_rule.rb +54 -0
- data/lib/opencdd/validator/hierarchy_rule.rb +65 -0
- data/lib/opencdd/validator/irdi_rule.rb +57 -0
- data/lib/opencdd/validator/mandatory_rule.rb +25 -0
- data/lib/opencdd/validator/pattern_rule.rb +26 -0
- data/lib/opencdd/validator/reference_rule.rb +36 -0
- data/lib/opencdd/validator/rule.rb +65 -0
- data/lib/opencdd/validator/runner.rb +101 -0
- data/lib/opencdd/validator/set_rule.rb +41 -0
- data/lib/opencdd/validator/synonym_rule.rb +30 -0
- data/lib/opencdd/validator/type_rule.rb +102 -0
- data/lib/opencdd/validator/uniqueness_rule.rb +32 -0
- data/lib/opencdd/validator.rb +68 -0
- data/lib/opencdd/value_format.rb +71 -0
- data/lib/opencdd/value_list.rb +42 -0
- data/lib/opencdd/value_term.rb +24 -0
- data/lib/opencdd/version.rb +5 -0
- data/lib/opencdd/view_control.rb +10 -0
- data/lib/opencdd/visitor.rb +93 -0
- data/lib/opencdd.rb +57 -0
- metadata +325 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
class Klass < Opencdd::Entity
|
|
5
|
+
PARENT_PROPERTY_IDS = [Opencdd::PropertyIds::MDC_P010_1, Opencdd::PropertyIds::MDC_P010].freeze
|
|
6
|
+
|
|
7
|
+
# Read-only accessors for state that the Database mutates via
|
|
8
|
+
# the explicit mutator methods below. Exposing only readers
|
|
9
|
+
# (instead of attr_accessor) keeps the entity's invariants
|
|
10
|
+
# under the class's control: parent linkage, child registration,
|
|
11
|
+
# and declared-property tracking each have a single mutator
|
|
12
|
+
# that can guard against duplicates, cycles, and stale state.
|
|
13
|
+
attr_reader :parent_irdi, :children, :declared_property_irdis, :database
|
|
14
|
+
|
|
15
|
+
def initialize(irdi: nil, properties:, schema: nil, meta_class_irdi: nil)
|
|
16
|
+
super
|
|
17
|
+
@children = []
|
|
18
|
+
@declared_property_irdis = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# ── Mutators (called by Opencdd::Database and Opencdd::Cddal::Builder
|
|
22
|
+
# during finalize/link phases). Single entry points keep
|
|
23
|
+
# invariant checks (dedup, validity) in one place. ──────
|
|
24
|
+
|
|
25
|
+
def attach_parent_irdi(irdi)
|
|
26
|
+
@parent_irdi = irdi
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_child(klass)
|
|
31
|
+
return self if @children.include?(klass)
|
|
32
|
+
@children << klass
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def declare_property(irdi)
|
|
37
|
+
return self if @declared_property_irdis.include?(irdi)
|
|
38
|
+
@declared_property_irdis << irdi
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def attach_database(database)
|
|
43
|
+
@database = database
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# ── Pure field reads ─────────────────────────────────────────
|
|
48
|
+
field :is_case_of_irdis, "MDC_P013", as: "is_case_of"
|
|
49
|
+
field :applicable_property_irdis, "MDC_P014", as: "applicable_properties"
|
|
50
|
+
field :imported_property_irdis, "MDC_P090", as: "imported_properties"
|
|
51
|
+
field :sub_class_selection_irdis, "MDC_P016", as: "sub_class_selection"
|
|
52
|
+
field :applicable_documents, "MDC_P094"
|
|
53
|
+
field :imported_documents, "MDC_P093"
|
|
54
|
+
|
|
55
|
+
# ── Computed fields with block-form readers ──────────────────
|
|
56
|
+
field(:class_type, synthetic: true) do
|
|
57
|
+
@class_type ||= Opencdd::ClassType.parse(properties[Opencdd::PropertyIds::MDC_P011])
|
|
58
|
+
end
|
|
59
|
+
field(:superclass_irdi, synthetic: true, as: "superclass") do
|
|
60
|
+
raw = properties[Opencdd::PropertyIds::MDC_P010_1] || properties[Opencdd::PropertyIds::MDC_P010]
|
|
61
|
+
next nil if raw.nil? || raw.to_s.strip.empty?
|
|
62
|
+
Opencdd::IRDI.parse(raw)
|
|
63
|
+
end
|
|
64
|
+
field(:superclass_type_property, synthetic: true) do
|
|
65
|
+
properties[Opencdd::PropertyIds::MDC_P010_1] || properties[Opencdd::PropertyIds::MDC_P010]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
alias_method :parent_property_value, :superclass_irdi
|
|
69
|
+
|
|
70
|
+
def parent_property_id
|
|
71
|
+
return @parent_property_id if defined?(@parent_property_id)
|
|
72
|
+
@parent_property_id = detect_parent_property_id
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parent
|
|
76
|
+
return nil unless @parent_irdi && @database
|
|
77
|
+
@database.find(@parent_irdi)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def ancestors
|
|
81
|
+
out = []
|
|
82
|
+
cur = self
|
|
83
|
+
while cur
|
|
84
|
+
out << cur
|
|
85
|
+
ref = cur.parent_irdi || cur.superclass_irdi
|
|
86
|
+
break unless ref
|
|
87
|
+
break if out.any? { |a| a.irdi == ref }
|
|
88
|
+
parent_obj = cur.database&.find(ref)
|
|
89
|
+
break unless parent_obj
|
|
90
|
+
cur = parent_obj
|
|
91
|
+
end
|
|
92
|
+
out
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def descendants
|
|
96
|
+
out = []
|
|
97
|
+
queue = children.dup
|
|
98
|
+
seen = { irdi => true }
|
|
99
|
+
until queue.empty?
|
|
100
|
+
c = queue.shift
|
|
101
|
+
next if seen[c.irdi]
|
|
102
|
+
seen[c.irdi] = true
|
|
103
|
+
out << c
|
|
104
|
+
queue.concat(c.children)
|
|
105
|
+
end
|
|
106
|
+
out
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def subclasses
|
|
110
|
+
children
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def properties_on_class(database = @database)
|
|
114
|
+
return [] unless database
|
|
115
|
+
declared_property_irdis.map { |i| database.find(i) }.compact
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def all_properties(database = @database)
|
|
119
|
+
return effective_properties if database
|
|
120
|
+
properties_on_class(database)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def effective_properties(database = @database)
|
|
124
|
+
return [] unless database
|
|
125
|
+
database.effective_properties.for(self)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def is_case_of(database = @database)
|
|
129
|
+
return [] unless database
|
|
130
|
+
is_case_of_irdis.map { |i| database.find(i) }.compact
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def sub_class_selection(database = @database)
|
|
134
|
+
return [] unless database
|
|
135
|
+
sub_class_selection_irdis.map { |i| database.find(i) }.compact
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# ── Powertype semantics (CDD four-layer ontology) ───────────
|
|
139
|
+
#
|
|
140
|
+
# A CATEGORICAL_CLASS at M1 occupies the powertype position: its
|
|
141
|
+
# subclasses ARE themselves classes, but they are also treated
|
|
142
|
+
# as *instances* of the categorical class for the purpose of
|
|
143
|
+
# CLASS_REFERENCE data types and sub_class_selection. This is
|
|
144
|
+
# the distinguishing capability of CDD vs UML/RDF/OWL.
|
|
145
|
+
#
|
|
146
|
+
# Example (OceanRunner):
|
|
147
|
+
# EngineType (CATEGORICAL_CLASS)
|
|
148
|
+
# ├── SingleDieselEngine (ITEM_CLASS)
|
|
149
|
+
# ├── TwinDieselEngine (ITEM_CLASS)
|
|
150
|
+
# └── ElectricHybridEngine (ITEM_CLASS)
|
|
151
|
+
#
|
|
152
|
+
# EngineType#powertype? returns true.
|
|
153
|
+
# EngineType#categorical_instances returns [SingleDiesel, TwinDiesel,
|
|
154
|
+
# ElectricHybrid] — the valid values for any CLASS_REFERENCE(EngineType)
|
|
155
|
+
# property.
|
|
156
|
+
|
|
157
|
+
def powertype?
|
|
158
|
+
categorical?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Returns the powertype instances of this categorical class —
|
|
162
|
+
# direct children that are themselves classes (ITEM_CLASS or
|
|
163
|
+
# VALUE_CLASS terminal types). Returns empty for non-powertype
|
|
164
|
+
# classes or when +database+ is unavailable.
|
|
165
|
+
def categorical_instances(database = @database)
|
|
166
|
+
return [] unless powertype? && database
|
|
167
|
+
children.select { |c| c.item? || c.value_class? || c.powertype? }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Sub-powertypes: categorical classes that specialize this one.
|
|
171
|
+
# E.g. PrimaryColor (CATEGORICAL) under Color (CATEGORICAL) is a
|
|
172
|
+
# sub-powertype. Useful for hierarchical option trees.
|
|
173
|
+
def sub_powertypes(database = @database)
|
|
174
|
+
return [] unless powertype? && database
|
|
175
|
+
children.select(&:powertype?)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Walks the ancestor chain and returns the categorical classes
|
|
179
|
+
# this class is an instance of (in the powertype sense). For
|
|
180
|
+
# SingleDiesel: [EngineType]. For a configured product subclass
|
|
181
|
+
# under EngineType + InteriorPackage: [EngineType, InteriorPackage].
|
|
182
|
+
def powertype_owners(database = @database)
|
|
183
|
+
return [] unless database
|
|
184
|
+
ancestors.filter_map do |a|
|
|
185
|
+
next if a == self
|
|
186
|
+
a if a.powertype?
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def attach_database(database)
|
|
191
|
+
@database = database
|
|
192
|
+
self
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def item?
|
|
196
|
+
class_type&.item?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def categorical?
|
|
200
|
+
class_type&.categorical?
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def value_class?
|
|
204
|
+
class_type&.value_class?
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def message?
|
|
208
|
+
class_type&.message?
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private
|
|
212
|
+
|
|
213
|
+
def detect_parent_property_id
|
|
214
|
+
return Opencdd::PropertyIds::MDC_P010 unless @schema
|
|
215
|
+
|
|
216
|
+
PARENT_PROPERTY_IDS.each do |id|
|
|
217
|
+
return id if @schema.find_by_property_id(id)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
@schema.each do |col|
|
|
221
|
+
next unless col.name.to_s =~ /superclass|subclass of|parent/i
|
|
222
|
+
next if col.name.to_s =~ /sub alternate/i
|
|
223
|
+
return col.property_id
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
col = @schema.find_by_name("Subclass of") || @schema.find_by_name("Superclass")
|
|
227
|
+
col&.property_id
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
# Value object representing the language configuration of a CDD
|
|
5
|
+
# dictionary. The source language is the one the data was originally
|
|
6
|
+
# authored in; translation languages are additional languages that
|
|
7
|
+
# appear in the .xls property keys (e.g. +MDC_P004.de+).
|
|
8
|
+
#
|
|
9
|
+
# The exporter scans each entity's +@properties+ hash for
|
|
10
|
+
# +<property_id>.<lang>+ keys to build per-field language maps
|
|
11
|
+
# (TODO.work/08). This class is the model object that aggregates
|
|
12
|
+
# "what languages does this dictionary have?" — used by the browser
|
|
13
|
+
# to render a language switcher and by the data pipeline to report
|
|
14
|
+
# language coverage.
|
|
15
|
+
class Languages
|
|
16
|
+
attr_reader :source, :translations
|
|
17
|
+
|
|
18
|
+
def initialize(source: "en", translations: [])
|
|
19
|
+
@source = source.to_s
|
|
20
|
+
@translations = Array(translations).map(&:to_s).uniq - [@source]
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def all
|
|
25
|
+
[@source] + @translations
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def include?(lang)
|
|
29
|
+
all.include?(lang.to_s)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def empty?
|
|
33
|
+
@source.nil? || @source.empty?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def size
|
|
37
|
+
all.size
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_a
|
|
41
|
+
all
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ==(other)
|
|
45
|
+
other.is_a?(Opencdd::Languages) && source == other.source &&
|
|
46
|
+
translations == other.translations
|
|
47
|
+
end
|
|
48
|
+
alias_method :eql?, :==
|
|
49
|
+
|
|
50
|
+
def hash
|
|
51
|
+
[source, translations].hash
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Scan a properties hash for +<property_id>.<lang>+ keys and
|
|
55
|
+
# return a Languages object covering every language seen. The
|
|
56
|
+
# source language defaults to +default_source+ when no explicit
|
|
57
|
+
# source-language key is present.
|
|
58
|
+
def self.from_properties(properties, default_source: "en")
|
|
59
|
+
langs = properties.keys.each_with_object(Set.new) do |key, acc|
|
|
60
|
+
next unless key.include?(".")
|
|
61
|
+
prefix, lang = key.split(".", 2)
|
|
62
|
+
next unless lang =~ /\A[a-z]{2}(-[a-z0-9]+)?\z/i
|
|
63
|
+
acc << lang
|
|
64
|
+
end
|
|
65
|
+
source = langs.include?(default_source) ? default_source : (langs.first || default_source)
|
|
66
|
+
translations = langs.to_a - [source]
|
|
67
|
+
new(source: source, translations: translations)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
# CDD is a four-layer ontology (IEC 61360 §5):
|
|
5
|
+
#
|
|
6
|
+
# M2 (meta-model) — the meta-classes themselves: MDC_C002 Class,
|
|
7
|
+
# MDC_C003 Property, MDC_C009 Unit, etc.
|
|
8
|
+
# Fixed set per the standard. Each is modeled
|
|
9
|
+
# by an +Opencdd::MetaClass+ instance.
|
|
10
|
+
# M1 (model) — dictionary content: AAA001 Vehicle, AAAP001
|
|
11
|
+
# vehicle_length, etc. Instances of M2
|
|
12
|
+
# meta-classes, modeled by Opencdd::Entity
|
|
13
|
+
# subclasses (Klass, Property, Unit, ...).
|
|
14
|
+
# M0 (data) — real-world individuals: ORCA30-TH-0001 etc.
|
|
15
|
+
# Instances of M1 classes.
|
|
16
|
+
#
|
|
17
|
+
# CDD's defining feature vs UML/RDF/OWL: at M1, a class declared
|
|
18
|
+
# +class_type=CATEGORICAL_CLASS+ has subclasses that ARE themselves
|
|
19
|
+
# classes but are also "instances" of the categorical class in the
|
|
20
|
+
# powertype sense (used in CLASS_REFERENCE data types and
|
|
21
|
+
# +sub_class_selection+). This two-level capability is the central
|
|
22
|
+
# abstraction the model must preserve.
|
|
23
|
+
class MetaClass
|
|
24
|
+
# Parcel sheet-type label (string written to the sheetmap "type"
|
|
25
|
+
# column). Single source of truth — Database, Writer, and
|
|
26
|
+
# WorkbookReader all read from MetaClass#sheet_type instead of
|
|
27
|
+
# maintaining parallel lookup tables.
|
|
28
|
+
PARCEL_SHEET_TYPES = {
|
|
29
|
+
class: "CLASS",
|
|
30
|
+
property: "PROPERTY",
|
|
31
|
+
value_list: "ENUM",
|
|
32
|
+
value_term: "TERMINOLOGY",
|
|
33
|
+
unit: "UoM",
|
|
34
|
+
relation: "RELATION",
|
|
35
|
+
view_control: "VIEWCONTROL",
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
attr_reader :irdi, :name, :allowed_property_ids, :entity_class,
|
|
39
|
+
:type, :sheet_type
|
|
40
|
+
|
|
41
|
+
def initialize(irdi:, name:, entity_class: nil, allowed_property_ids: [],
|
|
42
|
+
type: nil, sheet_type: nil)
|
|
43
|
+
@irdi = irdi.to_s
|
|
44
|
+
@name = name.to_s
|
|
45
|
+
@entity_class = entity_class
|
|
46
|
+
@allowed_property_ids = allowed_property_ids.map(&:to_s).freeze
|
|
47
|
+
@type = type
|
|
48
|
+
@sheet_type = sheet_type || (type ? PARCEL_SHEET_TYPES[type] : nil)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def code = @irdi
|
|
52
|
+
|
|
53
|
+
def allows_property?(id)
|
|
54
|
+
@allowed_property_ids.include?(id.to_s)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def merge(other)
|
|
58
|
+
raise ArgumentError, "meta-class IRDI mismatch: #{other.irdi} != #{@irdi}" unless other.irdi == @irdi
|
|
59
|
+
MetaClass.new(
|
|
60
|
+
irdi: @irdi,
|
|
61
|
+
name: @name,
|
|
62
|
+
entity_class: @entity_class || other.entity_class,
|
|
63
|
+
allowed_property_ids: (@allowed_property_ids + other.allowed_property_ids).uniq,
|
|
64
|
+
type: @type || other.type,
|
|
65
|
+
sheet_type: @sheet_type || other.sheet_type,
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_s
|
|
70
|
+
"#<#{self.class.name} #{@irdi} (#{@name}) type=#{@type} properties=#{@allowed_property_ids.size}>"
|
|
71
|
+
end
|
|
72
|
+
alias_method :inspect, :to_s
|
|
73
|
+
|
|
74
|
+
module MetaClasses
|
|
75
|
+
CODE_PROPERTY_IDS = {
|
|
76
|
+
"MDC_C002" => "MDC_P001_5",
|
|
77
|
+
"MDC_C003" => "MDC_P001_6",
|
|
78
|
+
"MDC_C005" => "MDC_P001_12",
|
|
79
|
+
"MDC_C011" => "MDC_P001_13",
|
|
80
|
+
"MDC_C009" => "MDC_P001_10",
|
|
81
|
+
"MDC_C010" => "MDC_P001_11",
|
|
82
|
+
"EXT_C001" => "EXT_P001",
|
|
83
|
+
}.freeze
|
|
84
|
+
|
|
85
|
+
TYPE_BY_META_CLASS = {
|
|
86
|
+
"MDC_C002" => :class,
|
|
87
|
+
"MDC_C003" => :property,
|
|
88
|
+
"MDC_C005" => :value_list,
|
|
89
|
+
"MDC_C011" => :relation,
|
|
90
|
+
"MDC_C009" => :unit,
|
|
91
|
+
"MDC_C010" => :value_term,
|
|
92
|
+
"EXT_C001" => :view_control,
|
|
93
|
+
}.freeze
|
|
94
|
+
|
|
95
|
+
@registry = nil
|
|
96
|
+
|
|
97
|
+
class << self
|
|
98
|
+
def common_property_ids
|
|
99
|
+
Opencdd::PropertyIds::REGISTRY.select { |_, e| e.applies_to == :all }.keys.freeze
|
|
100
|
+
end
|
|
101
|
+
def register(meta_class)
|
|
102
|
+
ensure_loaded
|
|
103
|
+
existing = @registry[meta_class.irdi]
|
|
104
|
+
merged = existing ? existing.merge(meta_class) : meta_class
|
|
105
|
+
@registry[meta_class.irdi] = merged
|
|
106
|
+
merged
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def for(irdi)
|
|
110
|
+
ensure_loaded
|
|
111
|
+
@registry[irdi.to_s]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def all
|
|
115
|
+
ensure_loaded
|
|
116
|
+
@registry.values
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def codes
|
|
120
|
+
ensure_loaded
|
|
121
|
+
@registry.keys
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def reset!
|
|
125
|
+
@registry = built_in_registry
|
|
126
|
+
define_code_constants!
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def type_for(irdi)
|
|
131
|
+
TYPE_BY_META_CLASS[irdi.to_s]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def code_property_id_for(irdi)
|
|
135
|
+
CODE_PROPERTY_IDS[irdi.to_s]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Reverse of +meta_class_for_type+: returns the Ruby entity
|
|
139
|
+
# class (Opencdd::Klass, Opencdd::Property, etc.) registered
|
|
140
|
+
# for a given +type+ Symbol. Single source of truth — used
|
|
141
|
+
# by Database#add_workbook to dispatch entity construction
|
|
142
|
+
# without maintaining its own type→class map.
|
|
143
|
+
def entity_class_for_type(type)
|
|
144
|
+
code = meta_class_for_type(type)
|
|
145
|
+
return nil unless code
|
|
146
|
+
ensure_loaded
|
|
147
|
+
@registry[code]&.entity_class
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Parcel sheet-type label ("CLASS", "PROPERTY", "ENUM",
|
|
151
|
+
# "TERMINOLOGY", "UoM", "RELATION", "VIEWCONTROL") for a
|
|
152
|
+
# given +type+ Symbol. Used by Database#build_sheetmap_for
|
|
153
|
+
# and Parcel::Writer instead of local lookup tables.
|
|
154
|
+
def sheet_type_for_type(type)
|
|
155
|
+
MetaClass::PARCEL_SHEET_TYPES[type.to_sym]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def sheet_type_for(irdi)
|
|
159
|
+
ensure_loaded
|
|
160
|
+
@registry[irdi.to_s]&.sheet_type
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def meta_class_for_type(type)
|
|
164
|
+
TYPE_BY_META_CLASS.key(type.to_sym) || TYPE_BY_META_CLASS.key(type.to_s.to_sym)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def ensure_loaded
|
|
170
|
+
return if @registry
|
|
171
|
+
@registry = built_in_registry
|
|
172
|
+
define_code_constants!
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def define_code_constants!
|
|
176
|
+
@registry.each_key do |code|
|
|
177
|
+
unless const_defined?(code, false)
|
|
178
|
+
const_set(code, code.freeze)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def built_in_registry
|
|
184
|
+
common = common_property_ids
|
|
185
|
+
klass_class = MetaClass.new(
|
|
186
|
+
irdi: "MDC_C002",
|
|
187
|
+
name: "Class",
|
|
188
|
+
entity_class: Opencdd::Klass,
|
|
189
|
+
type: :class,
|
|
190
|
+
allowed_property_ids: common + %w[
|
|
191
|
+
MDC_P010 MDC_P010_1 MDC_P011 MDC_P012 MDC_P013 MDC_P014 MDC_P014_1 MDC_P014_2
|
|
192
|
+
MDC_P015 MDC_P015_1 MDC_P015_2 MDC_P016
|
|
193
|
+
MDC_P090 MDC_P091 MDC_P093 MDC_P094 MDC_P094_1 MDC_P094_2
|
|
194
|
+
],
|
|
195
|
+
)
|
|
196
|
+
property_class = MetaClass.new(
|
|
197
|
+
irdi: "MDC_C003",
|
|
198
|
+
name: "Property",
|
|
199
|
+
entity_class: Opencdd::Property,
|
|
200
|
+
type: :property,
|
|
201
|
+
allowed_property_ids: common + %w[
|
|
202
|
+
MDC_P017 MDC_P018 MDC_P020 MDC_P021 MDC_P022 MDC_P023 MDC_P023_1 MDC_P023_2
|
|
203
|
+
MDC_P024 MDC_P025_1 MDC_P025_2 MDC_P025_3
|
|
204
|
+
MDC_P027_1 MDC_P027_2 MDC_P028
|
|
205
|
+
MDC_P030 MDC_P031 MDC_P032 MDC_P033 MDC_P040
|
|
206
|
+
MDC_P041 MDC_P042 MDC_P068 MDC_P096 MDC_P097
|
|
207
|
+
MDC_P101 MDC_P102 MDC_P110 MDC_P111 MDC_P114
|
|
208
|
+
],
|
|
209
|
+
)
|
|
210
|
+
value_list = MetaClass.new(
|
|
211
|
+
irdi: "MDC_C005",
|
|
212
|
+
name: "ValueList",
|
|
213
|
+
entity_class: Opencdd::ValueList,
|
|
214
|
+
type: :value_list,
|
|
215
|
+
allowed_property_ids: common + %w[
|
|
216
|
+
MDC_P043 MDC_P044 MDC_P045 MDC_P046
|
|
217
|
+
],
|
|
218
|
+
)
|
|
219
|
+
relation = MetaClass.new(
|
|
220
|
+
irdi: "MDC_C011",
|
|
221
|
+
name: "Relation",
|
|
222
|
+
entity_class: Opencdd::Relation,
|
|
223
|
+
type: :relation,
|
|
224
|
+
allowed_property_ids: common + %w[
|
|
225
|
+
MDC_P200 MDC_P201 MDC_P202 MDC_P203 MDC_P204 MDC_P205
|
|
226
|
+
MDC_P206 MDC_P207 MDC_P208 MDC_P209 MDC_P210 MDC_P211 MDC_P212
|
|
227
|
+
MDC_P230 MDC_P231
|
|
228
|
+
],
|
|
229
|
+
)
|
|
230
|
+
unit = MetaClass.new(
|
|
231
|
+
irdi: "MDC_C009",
|
|
232
|
+
name: "Unit",
|
|
233
|
+
entity_class: Opencdd::Unit,
|
|
234
|
+
type: :unit,
|
|
235
|
+
allowed_property_ids: common + %w[
|
|
236
|
+
MDC_P021 MDC_P023 MDC_P023_1 MDC_P023_2
|
|
237
|
+
],
|
|
238
|
+
)
|
|
239
|
+
value_term = MetaClass.new(
|
|
240
|
+
irdi: "MDC_C010",
|
|
241
|
+
name: "ValueTerm",
|
|
242
|
+
entity_class: Opencdd::ValueTerm,
|
|
243
|
+
type: :value_term,
|
|
244
|
+
allowed_property_ids: common + %w[
|
|
245
|
+
MDC_P018_1 MDC_P021 MDC_P022 MDC_P044 MDC_P045
|
|
246
|
+
],
|
|
247
|
+
)
|
|
248
|
+
view_control = MetaClass.new(
|
|
249
|
+
irdi: "EXT_C001",
|
|
250
|
+
name: "ViewControl",
|
|
251
|
+
entity_class: Opencdd::ViewControl,
|
|
252
|
+
type: :view_control,
|
|
253
|
+
allowed_property_ids: common + %w[EXT_P002 EXT_P003],
|
|
254
|
+
)
|
|
255
|
+
{
|
|
256
|
+
klass_class.irdi => klass_class,
|
|
257
|
+
property_class.irdi => property_class,
|
|
258
|
+
value_list.irdi => value_list,
|
|
259
|
+
relation.irdi => relation,
|
|
260
|
+
unit.irdi => unit,
|
|
261
|
+
value_term.irdi => value_term,
|
|
262
|
+
view_control.irdi => view_control,
|
|
263
|
+
}
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
MetaClasses = MetaClass::MetaClasses
|
|
270
|
+
|
|
271
|
+
MetaClass::MetaClasses::TYPE_BY_META_CLASS.each_key do |code|
|
|
272
|
+
MetaClasses.const_set(code, code.freeze) unless MetaClasses.const_defined?(code, false)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/store"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Opencdd
|
|
7
|
+
module Model
|
|
8
|
+
# Per-entity YAML persistence using lutaml-store as the backend.
|
|
9
|
+
# Each entity is stored as a single YAML file in a directory layout,
|
|
10
|
+
# serialized via Lutaml::Model (YamlEntity). Diff-friendly at the
|
|
11
|
+
# entity level — one git diff shows exactly which entity changed.
|
|
12
|
+
#
|
|
13
|
+
# Directory layout (managed by lutaml-store's FileSystem adapter):
|
|
14
|
+
#
|
|
15
|
+
# data/
|
|
16
|
+
# └── entities/
|
|
17
|
+
# ├── AA/
|
|
18
|
+
# │ ├── AAA001.data
|
|
19
|
+
# │ └── AAA010.data
|
|
20
|
+
# └── ...
|
|
21
|
+
#
|
|
22
|
+
# The FileSystem adapter shards by first 2 chars for scalability.
|
|
23
|
+
class EntityStore
|
|
24
|
+
attr_reader :path, :store
|
|
25
|
+
|
|
26
|
+
def initialize(path)
|
|
27
|
+
@path = File.expand_path(path.to_s)
|
|
28
|
+
FileUtils.mkdir_p(@path)
|
|
29
|
+
@store = Lutaml::Store.new(
|
|
30
|
+
adapter: :filesystem,
|
|
31
|
+
adapter_options: { path: @path },
|
|
32
|
+
models: [
|
|
33
|
+
{ model: Opencdd::Model::YamlEntity, key: :irdi, dir: "entities" },
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Save all entities from +database+ to individual YAML files
|
|
39
|
+
# via lutaml-store's FileSystem backend. Returns self.
|
|
40
|
+
def save_database(database)
|
|
41
|
+
database.entities.each do |entity|
|
|
42
|
+
yaml_entity = Opencdd::Model::YamlEntity.from_entity(entity)
|
|
43
|
+
key = safe_key(yaml_entity.irdi || yaml_entity.code)
|
|
44
|
+
next unless key
|
|
45
|
+
yaml_str = yaml_entity.to_yaml
|
|
46
|
+
@store.store.adapter.set(key, yaml_str)
|
|
47
|
+
end
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Load all YAML files from the store into a Database.
|
|
52
|
+
# Returns a finalized Database.
|
|
53
|
+
def load_database(database = nil)
|
|
54
|
+
database ||= Opencdd::Database.new
|
|
55
|
+
@store.store.adapter.keys.each do |key|
|
|
56
|
+
raw = @store.store.adapter.get(key)
|
|
57
|
+
next unless raw
|
|
58
|
+
begin
|
|
59
|
+
yaml_entity = Opencdd::Model::YamlEntity.from_yaml(raw)
|
|
60
|
+
entity = yaml_entity.to_entity(database)
|
|
61
|
+
database.add_entity(entity)
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
warn "EntityStore: skipping #{key}: #{e.message}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
database.finalize!
|
|
67
|
+
database
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Fetch a single entity by key.
|
|
71
|
+
def fetch(key)
|
|
72
|
+
raw = @store.store.adapter.get(safe_key(key))
|
|
73
|
+
return nil unless raw
|
|
74
|
+
Opencdd::Model::YamlEntity.from_yaml(raw)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def safe_key(key)
|
|
80
|
+
return nil if key.nil? || key.to_s.strip.empty?
|
|
81
|
+
key.to_s.split("#").last || key.to_s
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Opencdd
|
|
6
|
+
module Model
|
|
7
|
+
# CDD-native YAML database. Wraps a collection of YamlEntity
|
|
8
|
+
# objects with dictionary-level metadata (source language,
|
|
9
|
+
# translation languages). Serializes to YAML via lutaml-model
|
|
10
|
+
# — no hand-rolled serialization on the model class itself.
|
|
11
|
+
#
|
|
12
|
+
# YAML shape:
|
|
13
|
+
#
|
|
14
|
+
# ---
|
|
15
|
+
# source_language: en
|
|
16
|
+
# translation_languages:
|
|
17
|
+
# - fr
|
|
18
|
+
# - ja
|
|
19
|
+
# entities:
|
|
20
|
+
# - irdi: 0112/2///61360_4#AAA001
|
|
21
|
+
# type: class
|
|
22
|
+
# code: AAA001
|
|
23
|
+
# preferred_name:
|
|
24
|
+
# en: Vehicle
|
|
25
|
+
# ...
|
|
26
|
+
class YamlDatabase < Lutaml::Model::Serializable
|
|
27
|
+
attribute :source_language, :string, default: "en"
|
|
28
|
+
attribute :translation_languages, :string, collection: true
|
|
29
|
+
attribute :entities, YamlEntity, collection: true
|
|
30
|
+
|
|
31
|
+
# ── Conversion: Database → YamlDatabase ─────────────────
|
|
32
|
+
def self.from_database(database)
|
|
33
|
+
new(
|
|
34
|
+
source_language: "en",
|
|
35
|
+
translation_languages: [],
|
|
36
|
+
entities: database.entities.map { |e| YamlEntity.from_entity(e) },
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# ── Conversion: YamlDatabase → Database ─────────────────
|
|
41
|
+
def to_database(database = nil)
|
|
42
|
+
database ||= Opencdd::Database.new
|
|
43
|
+
entities.each { |ye| database.add_entity(ye.to_entity(database)) }
|
|
44
|
+
database.finalize!
|
|
45
|
+
database
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|