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.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/CLAUDE.md +486 -0
  3. data/README.md +304 -0
  4. data/bin/lint-no-raw-mdc +41 -0
  5. data/lib/cdd.rb +11 -0
  6. data/lib/opencdd/alias_table.rb +52 -0
  7. data/lib/opencdd/cddal/ast.rb +151 -0
  8. data/lib/opencdd/cddal/builder.rb +374 -0
  9. data/lib/opencdd/cddal/fetcher/in_memory.rb +32 -0
  10. data/lib/opencdd/cddal/fetcher/net_http.rb +84 -0
  11. data/lib/opencdd/cddal/fetcher.rb +18 -0
  12. data/lib/opencdd/cddal/generated_parser.rb +805 -0
  13. data/lib/opencdd/cddal/lexer.rb +193 -0
  14. data/lib/opencdd/cddal/parser.rb +19 -0
  15. data/lib/opencdd/cddal/resolver.rb +100 -0
  16. data/lib/opencdd/cddal/serializer.rb +210 -0
  17. data/lib/opencdd/cddal/value_serializer.rb +60 -0
  18. data/lib/opencdd/cddal.rb +63 -0
  19. data/lib/opencdd/class_tree.rb +80 -0
  20. data/lib/opencdd/class_type.rb +33 -0
  21. data/lib/opencdd/codegen/ts.rb +185 -0
  22. data/lib/opencdd/codegen.rb +7 -0
  23. data/lib/opencdd/composition_tree.rb +119 -0
  24. data/lib/opencdd/condition.rb +120 -0
  25. data/lib/opencdd/data_type.rb +143 -0
  26. data/lib/opencdd/database.rb +719 -0
  27. data/lib/opencdd/effective_properties.rb +119 -0
  28. data/lib/opencdd/entity/field_reader.rb +141 -0
  29. data/lib/opencdd/entity/field_registry.rb +99 -0
  30. data/lib/opencdd/entity/version_history.rb +62 -0
  31. data/lib/opencdd/entity.rb +255 -0
  32. data/lib/opencdd/exporters/json.rb +235 -0
  33. data/lib/opencdd/exporters/mermaid.rb +62 -0
  34. data/lib/opencdd/exporters/yaml.rb +16 -0
  35. data/lib/opencdd/exporters.rb +9 -0
  36. data/lib/opencdd/guid.rb +27 -0
  37. data/lib/opencdd/instance_rule.rb +70 -0
  38. data/lib/opencdd/irdi.rb +128 -0
  39. data/lib/opencdd/klass.rb +230 -0
  40. data/lib/opencdd/languages.rb +70 -0
  41. data/lib/opencdd/meta_class.rb +274 -0
  42. data/lib/opencdd/model/entity_store.rb +85 -0
  43. data/lib/opencdd/model/yaml_database.rb +49 -0
  44. data/lib/opencdd/model/yaml_entity.rb +196 -0
  45. data/lib/opencdd/model.rb +13 -0
  46. data/lib/opencdd/parcel/csv_reader.rb +35 -0
  47. data/lib/opencdd/parcel/csv_writer.rb +114 -0
  48. data/lib/opencdd/parcel/entity_manifest.rb +119 -0
  49. data/lib/opencdd/parcel/flat_dir_reader.rb +134 -0
  50. data/lib/opencdd/parcel/layout_detector.rb +115 -0
  51. data/lib/opencdd/parcel/metadata.rb +112 -0
  52. data/lib/opencdd/parcel/referenced_irdis.rb +91 -0
  53. data/lib/opencdd/parcel/scrape_verifier.rb +122 -0
  54. data/lib/opencdd/parcel/selector.rb +115 -0
  55. data/lib/opencdd/parcel/sharded_dir_reader.rb +151 -0
  56. data/lib/opencdd/parcel/sheet.rb +287 -0
  57. data/lib/opencdd/parcel/sheet_emitter.rb +171 -0
  58. data/lib/opencdd/parcel/sheet_schema.rb +253 -0
  59. data/lib/opencdd/parcel/workbook.rb +172 -0
  60. data/lib/opencdd/parcel/workbook_reader.rb +200 -0
  61. data/lib/opencdd/parcel/writer.rb +185 -0
  62. data/lib/opencdd/parcel.rb +175 -0
  63. data/lib/opencdd/parse_helpers.rb +73 -0
  64. data/lib/opencdd/property.rb +120 -0
  65. data/lib/opencdd/property_data_element_type.rb +44 -0
  66. data/lib/opencdd/property_ids.rb +202 -0
  67. data/lib/opencdd/reader.rb +103 -0
  68. data/lib/opencdd/relation.rb +88 -0
  69. data/lib/opencdd/relation_tree.rb +74 -0
  70. data/lib/opencdd/relation_type.rb +58 -0
  71. data/lib/opencdd/structured_values.rb +183 -0
  72. data/lib/opencdd/unit.rb +16 -0
  73. data/lib/opencdd/validator/class_reference_rule.rb +77 -0
  74. data/lib/opencdd/validator/condition_rule.rb +27 -0
  75. data/lib/opencdd/validator/data_type_rule.rb +26 -0
  76. data/lib/opencdd/validator/enum_rule.rb +27 -0
  77. data/lib/opencdd/validator/format_rule.rb +54 -0
  78. data/lib/opencdd/validator/hierarchy_rule.rb +65 -0
  79. data/lib/opencdd/validator/irdi_rule.rb +57 -0
  80. data/lib/opencdd/validator/mandatory_rule.rb +25 -0
  81. data/lib/opencdd/validator/pattern_rule.rb +26 -0
  82. data/lib/opencdd/validator/reference_rule.rb +36 -0
  83. data/lib/opencdd/validator/rule.rb +65 -0
  84. data/lib/opencdd/validator/runner.rb +101 -0
  85. data/lib/opencdd/validator/set_rule.rb +41 -0
  86. data/lib/opencdd/validator/synonym_rule.rb +30 -0
  87. data/lib/opencdd/validator/type_rule.rb +102 -0
  88. data/lib/opencdd/validator/uniqueness_rule.rb +32 -0
  89. data/lib/opencdd/validator.rb +68 -0
  90. data/lib/opencdd/value_format.rb +71 -0
  91. data/lib/opencdd/value_list.rb +42 -0
  92. data/lib/opencdd/value_term.rb +24 -0
  93. data/lib/opencdd/version.rb +5 -0
  94. data/lib/opencdd/view_control.rb +10 -0
  95. data/lib/opencdd/visitor.rb +93 -0
  96. data/lib/opencdd.rb +57 -0
  97. 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