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,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Parcel
5
+ META_CLASS_TYPES = Opencdd::MetaClasses::TYPE_BY_META_CLASS
6
+ TYPE_TO_META_CLASS = META_CLASS_TYPES.invert.freeze
7
+
8
+ autoload :Metadata, "opencdd/parcel/metadata"
9
+ autoload :SheetSchema, "opencdd/parcel/sheet_schema"
10
+ autoload :Sheet, "opencdd/parcel/sheet"
11
+ autoload :Workbook, "opencdd/parcel/workbook"
12
+ autoload :WorkbookReader, "opencdd/parcel/workbook_reader"
13
+ autoload :FlatDirReader, "opencdd/parcel/flat_dir_reader"
14
+ autoload :ShardedDirReader, "opencdd/parcel/sharded_dir_reader"
15
+ autoload :LayoutDetector, "opencdd/parcel/layout_detector"
16
+ autoload :EntityManifest, "opencdd/parcel/entity_manifest"
17
+ autoload :ReferencedIrdis, "opencdd/parcel/referenced_irdis"
18
+ autoload :Selector, "opencdd/parcel/selector"
19
+ autoload :SheetEmitter, "opencdd/parcel/sheet_emitter"
20
+ autoload :Writer, "opencdd/parcel/writer"
21
+ autoload :CsvWriter, "opencdd/parcel/csv_writer"
22
+ autoload :CsvReader, "opencdd/parcel/csv_reader"
23
+ autoload :ScrapeVerifier, "opencdd/parcel/scrape_verifier"
24
+
25
+ module_function
26
+
27
+ # Aggregate multiple Parcel-shaped paths into a single Database.
28
+ # The +paths+ array may contain .xlsx files, .xls files, flat .xls
29
+ # directories, or sharded per-class subdirs. Each is read
30
+ # polymorphically via +Opencdd::Reader.detect+ and merged into a
31
+ # single target database.
32
+ #
33
+ # Returns the populated Opencdd::Database.
34
+ def aggregate(*paths)
35
+ paths.flatten
36
+ .map { |p| Opencdd::Database.load(p) }
37
+ .reduce(Opencdd::Database.new) { |acc, db| acc.merge(db) }
38
+ end
39
+
40
+ # Split +database+ into multiple sub-databases partitioned along
41
+ # +by+:
42
+ #
43
+ # :entity_type — one partition per entity type
44
+ # (the legacy .xls layout, one file per type).
45
+ # :root_class — one partition per root class
46
+ # (full tree per parcel, lifted cross-type deps).
47
+ # :each_class — one partition per class
48
+ # (the class + its declared properties / value_lists /
49
+ # relations / units).
50
+ # callable — partition key is +callable.call(entity)+;
51
+ # the result is a +Hash{ key => Database }+.
52
+ #
53
+ # Returns +Hash{ key => Opencdd::Database }+.
54
+ def split(database, by:, lift_dependencies: true)
55
+ partitions = case by
56
+ when :entity_type then split_by_type(database)
57
+ when :root_class then split_by_root_class(database, lift_dependencies: lift_dependencies)
58
+ when :each_class then split_by_each_class(database, lift_dependencies: lift_dependencies)
59
+ when Proc then split_by_callable(database, by)
60
+ else
61
+ raise ArgumentError, "split `by:` must be :entity_type, :root_class, :each_class, or a Proc"
62
+ end
63
+ partitions
64
+ end
65
+
66
+ def split_by_type(database)
67
+ database.entities.group_by(&:type).each_with_object({}) do |(type, entities), h|
68
+ h[type] = build_partition(database, entities)
69
+ end
70
+ end
71
+
72
+ def split_by_root_class(database, lift_dependencies: true)
73
+ roots = database.root_classes
74
+ roots.each_with_object({}) do |root, h|
75
+ tree = collect_root_subtree(database, root)
76
+ entities = lift_dependencies ? lift_for(database, tree) : tree
77
+ h[root.code || root.irdi.to_s] = build_partition(database, entities)
78
+ end
79
+ end
80
+
81
+ def split_by_each_class(database, lift_dependencies: true)
82
+ database.classes.each_with_object({}) do |klass, h|
83
+ entities = [klass]
84
+ if lift_dependencies
85
+ klass_props = database.properties_of(klass)
86
+ entities.concat(klass_props)
87
+ entities.concat(database.relations_for(domain: klass.irdi))
88
+ klass_props.each do |prop|
89
+ next unless prop.is_a?(Opencdd::Property)
90
+ if prop.unit_irdi
91
+ unit = database.find(prop.unit_irdi)
92
+ entities << unit if unit.is_a?(Opencdd::Unit)
93
+ end
94
+ vl = database.value_list_of(prop)
95
+ entities << vl if vl
96
+ end
97
+ end
98
+ h[klass.code || klass.irdi.to_s] = build_partition(database, entities)
99
+ end
100
+ end
101
+
102
+ def split_by_callable(database, callable)
103
+ database.entities.group_by { |e| callable.call(e) }.each_with_object({}) do |(key, entities), h|
104
+ h[key] = build_partition(database, entities)
105
+ end
106
+ end
107
+
108
+ def collect_root_subtree(database, root)
109
+ collected = [root]
110
+ queue = [root]
111
+ seen = { root.irdi => true }
112
+ until queue.empty?
113
+ current = queue.shift
114
+ children = current.children
115
+ children.each do |c|
116
+ next if seen[c.irdi]
117
+ seen[c.irdi] = true
118
+ collected << c
119
+ queue << c
120
+ end
121
+ end
122
+ collected
123
+ end
124
+
125
+ # Lifts cross-type dependencies for the given set of classes:
126
+ # declared properties, value_lists those properties reference,
127
+ # units those properties use, and relations where the class is
128
+ # in domain or codomain. This makes a per-tree partition
129
+ # self-sufficient when written out.
130
+ def lift_for(database, classes)
131
+ entities = classes.dup
132
+ classes.each do |klass|
133
+ database.properties_of(klass).each { |p| entities << p unless entities.include?(p) }
134
+ database.relations_for(domain: klass.irdi).each { |r| entities << r unless entities.include?(r) }
135
+ database.relations_for(codomain: klass.irdi).each { |r| entities << r unless entities.include?(r) }
136
+ database.properties_of(klass).each do |prop|
137
+ next unless prop.is_a?(Opencdd::Property)
138
+ if prop.unit_irdi
139
+ unit = database.find(prop.unit_irdi)
140
+ entities << unit if unit.is_a?(Opencdd::Unit) && !entities.include?(unit)
141
+ end
142
+ vl = database.value_list_of(prop)
143
+ entities << vl if vl && !entities.include?(vl)
144
+ end
145
+ end
146
+ entities.uniq
147
+ end
148
+
149
+ def build_partition(_database, entities)
150
+ db = Opencdd::Database.new
151
+ entities.each { |e| db.add_entity(deep_copy_entity(e)) }
152
+ db.finalize!
153
+ db
154
+ end
155
+
156
+ # Returns a detached copy of +entity+ — same +irdi+, +properties+,
157
+ # +schema+, +meta_class_irdi+, but no +@database+ back-reference and
158
+ # empty Klass-specific state (+@children+, +@declared_property_irdis+).
159
+ #
160
+ # The destination partition Database reattaches the entity and
161
+ # rebuilds class-tree state via +add_entity+ and +finalize!+.
162
+ #
163
+ # We can't use Marshal here because Entity indirectly references its
164
+ # source Database, which uses Hash.new { ... } (a default proc) for
165
+ # its indexes — Marshal refuses to dump hashes with default procs.
166
+ def deep_copy_entity(entity)
167
+ entity.class.new(
168
+ irdi: entity.irdi,
169
+ properties: entity.properties.dup,
170
+ schema: entity.schema,
171
+ meta_class_irdi: entity.meta_class_irdi,
172
+ )
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ # Legacy module. The canonical home for collection parsing is now
6
+ # +Opencdd::StructuredValues+ — see +unwrap_and_split+, +rejoin+,
7
+ # +parse_ref_set+, +parse_synonyms+. ParseHelpers remains as a
8
+ # thin back-compat layer for code that hasn't migrated yet (notably
9
+ # the +Entity+ mixin). Do not add new callers; prefer StructuredValues.
10
+ module Opencdd
11
+ module ParseHelpers
12
+ module_function
13
+
14
+ # Delegates to StructuredValues — single source of truth for
15
+ # brace/paren/comma handling. Whitespace-separated forms are a
16
+ # Parcel-specific quirk preserved here for back-compat.
17
+ def parse_irdi_list(raw)
18
+ return [] if raw.nil? || raw.to_s.strip.empty?
19
+ tokens = Opencdd::StructuredValues.unwrap_and_split(raw)
20
+ # Some Parcel sources use whitespace as the separator inside
21
+ # a single "set" cell. Honour both shapes.
22
+ tokens.flat_map { |t| t.split(/\s+/) }.reject(&:empty?).filter_map do |t|
23
+ Opencdd::IRDI.parse(t)
24
+ end
25
+ end
26
+
27
+ def parse_string_list(raw)
28
+ Opencdd::StructuredValues.unwrap_and_split(raw)
29
+ end
30
+
31
+ def parse_synonym_tuples(raw)
32
+ return [] if raw.nil? || raw.to_s.strip.empty?
33
+ Opencdd::StructuredValues.parse_synonyms(raw)
34
+ end
35
+
36
+ # Parses both the {(name,lang),...} structured form and the
37
+ # legacy flat (lang, name, lang, name, ...) form. Kept on
38
+ # ParseHelpers because the flat form isn't a synonym-specific
39
+ # concept — it's an ad-hoc legacy wire shape.
40
+ def parse_pair_list(raw)
41
+ return [] if raw.nil? || raw.to_s.strip.empty?
42
+ s = raw.to_s.strip
43
+ return parse_synonym_tuples(s) if brace_wrapped?(s)
44
+ return [[nil, s]] unless paren_wrapped?(s)
45
+ unwrap_parens(s)
46
+ .split(",")
47
+ .each_slice(2)
48
+ .map { |lang, name| [lang&.strip, name&.strip] }
49
+ end
50
+
51
+ # ── Thin legacy predicates (kept for back-compat) ──────────
52
+ def unwrap_parens(s)
53
+ s = s.to_s
54
+ paren_wrapped?(s) ? s[1..-2] : s
55
+ end
56
+
57
+ def unwrap_delimiters(s)
58
+ s = s.to_s
59
+ return s[1..-2] if paren_wrapped?(s) || brace_wrapped?(s)
60
+ s
61
+ end
62
+
63
+ def paren_wrapped?(s)
64
+ s = s.to_s
65
+ s.start_with?("(") && s.end_with?(")")
66
+ end
67
+
68
+ def brace_wrapped?(s)
69
+ s = s.to_s
70
+ s.start_with?("{") && s.end_with?("}")
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class Property < Opencdd::Entity
5
+ # Wire-format aliases for the data_type string. Used by #data_type
6
+ # to canonicalize the raw value before exposing it.
7
+ DATA_TYPE_ALIASES = {
8
+ "STRING_TYPE" => :string,
9
+ "TRANSLATABLE_STRING_TYPE" => :translatable_string,
10
+ "REAL_MEASURE_TYPE" => :real_measure,
11
+ "INTEGER_MEASURE_TYPE" => :integer_measure,
12
+ "INT_MEASURE_TYPE" => :integer_measure,
13
+ "REAL_TYPE" => :real,
14
+ "INTEGER_TYPE" => :integer,
15
+ "INT_TYPE" => :integer,
16
+ "BOOLEAN_TYPE" => :boolean,
17
+ "DATE_TYPE" => :date,
18
+ "DATETIME_TYPE" => :date_time,
19
+ "DATE_TIME_TYPE" => :date_time,
20
+ "TIME_TYPE" => :time,
21
+ "IRDI_TYPE" => :irdi,
22
+ "ICID_STRING" => :irdi,
23
+ "ICID_STRING_TYPE" => :irdi,
24
+ "URL_TYPE" => :url,
25
+ "MIME_TYPE" => :mime,
26
+ "FILE_TYPE" => :file,
27
+ "COMPLEX_TYPE" => :complex,
28
+ }.freeze
29
+
30
+ # ── Pure field reads (value_kind + multilingual resolved from
31
+ # PropertyIds::REGISTRY). Explicit :string override where the
32
+ # original code returned the raw value (REGISTRY kind would
33
+ # otherwise coerce to IRDI etc.). ──────────────────────────
34
+ field :symbol_in_text, "MDC_P025_1", :string
35
+ field :value_format, "MDC_P024", :string
36
+ field :constraint, "MDC_P068", :string
37
+ field :type_classification, "MDC_P033", :string
38
+ field :source_document, "MDC_P006_1", :string
39
+ field :coded_name, "MDC_P018", :string
40
+ field :definition_class_irdi, "MDC_P021"
41
+ field :class_value_assignment, "MDC_P017"
42
+ field :unit_irdi, "MDC_P041"
43
+ field :alternative_unit_irdis, "MDC_P042"
44
+
45
+ # ── Computed fields with block-form readers (synthetic: true).
46
+ # Blocks are evaluated via instance_exec on the entity, so
47
+ # they have access to private helpers without send dispatch.
48
+ # `data_type` is NOT a DSL field — the model's #data_type method
49
+ # returns a symbol (:real, :string, etc.) for callers. The wire-
50
+ # format string ("REAL_TYPE") comes from parsed_data_type.to_s
51
+ # and is emitted explicitly by Exporters::Json#property_node.
52
+ PARSED_DATA_TYPE_BLOCK = lambda {
53
+ Opencdd::DataType.parse_or_string(properties[Opencdd::PropertyIds::MDC_P022])
54
+ }
55
+ DATA_ELEMENT_TYPE_BLOCK = lambda {
56
+ @property_data_element_type ||=
57
+ Opencdd::PropertyDataTypeElement.parse(properties[Opencdd::PropertyIds::MDC_P020])
58
+ }
59
+
60
+ field :parsed_data_type, synthetic: true, &PARSED_DATA_TYPE_BLOCK
61
+ field :data_element_type, synthetic: true, as: "data_element_type", &DATA_ELEMENT_TYPE_BLOCK
62
+ field :property_data_element_type,
63
+ synthetic: true, &DATA_ELEMENT_TYPE_BLOCK
64
+ field :parsed_value_format, synthetic: true do
65
+ Opencdd::ValueFormat.parse(properties[Opencdd::PropertyIds::MDC_P024])
66
+ end
67
+ field :condition_raw, "MDC_P028", synthetic: true do
68
+ properties[Opencdd::PropertyIds::MDC_P028]
69
+ end
70
+ field :condition, synthetic: true do
71
+ @condition ||= Opencdd::Condition.parse(properties[Opencdd::PropertyIds::MDC_P028])
72
+ rescue ArgumentError
73
+ # IEC CDD occasionally stores a bare class-reference set in the
74
+ # condition column (e.g. "{0112/2///62683#ACE132}") instead of
75
+ # a `left OP right` boolean expression. Condition's grammar
76
+ # rejects that shape; treat it as nil rather than crashing the
77
+ # import. Tracked in TODO.full-cdd/17-condition-grammar-fix.md.
78
+ @condition = nil
79
+ end
80
+ field :formula, synthetic: true do
81
+ properties[Opencdd::PropertyIds::MDC_P027_1] ||
82
+ properties[Opencdd::PropertyIds::MDC_P027_2]
83
+ end
84
+
85
+ # Model-side data_type — returns the canonical symbol form.
86
+ def data_type
87
+ raw = properties[Opencdd::PropertyIds::MDC_P022]
88
+ DATA_TYPE_ALIASES[raw] || raw
89
+ end
90
+
91
+ # ── Predicates and methods with arguments stay as regular
92
+ # methods. The DSL is for fields, not behavior. ───────────
93
+
94
+ def class_reference?
95
+ parsed_data_type.is_a?(Opencdd::DataType::ClassReference)
96
+ end
97
+
98
+ def enum?
99
+ parsed_data_type.is_a?(Opencdd::DataType::EnumStringType) ||
100
+ parsed_data_type.is_a?(Opencdd::DataType::EnumReferenceType)
101
+ end
102
+
103
+ def conditional?
104
+ det = property_data_element_type
105
+ det && (det.condition? || det.dependent?)
106
+ end
107
+
108
+ def active_for?(bindings)
109
+ return true unless conditional?
110
+ cond = condition
111
+ cond ? cond.satisfied_by?(bindings) : true
112
+ end
113
+
114
+ def attaches_to(*_args)
115
+ raise NotImplementedError,
116
+ "Property#attaches_to is provided by Opencdd::Database; use db.properties_of(klass)"
117
+ end
118
+ alias_method :applies_to, :attaches_to
119
+ end
120
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class PropertyDataTypeElement
5
+ VALUES = %w[NON_DEPENDENT_P_DET CONDITION_DET DEPENDENT_P_DET].freeze
6
+
7
+ attr_reader :value
8
+
9
+ def initialize(value)
10
+ v = value.to_s.strip.upcase
11
+ raise ArgumentError, "unknown property data element type: #{value.inspect}" unless VALUES.include?(v)
12
+ @value = v
13
+ end
14
+
15
+ def non_dependent? = @value == "NON_DEPENDENT_P_DET"
16
+ def condition? = @value == "CONDITION_DET"
17
+ def dependent? = @value == "DEPENDENT_P_DET"
18
+
19
+ def conditional?
20
+ condition? || dependent?
21
+ end
22
+
23
+ def to_s = @value
24
+ def to_sym = @value.downcase.to_sym
25
+
26
+ def ==(other)
27
+ other.is_a?(Opencdd::PropertyDataTypeElement) && @value == other.value
28
+ end
29
+ alias_method :eql?, :==
30
+
31
+ def hash
32
+ @value.hash
33
+ end
34
+
35
+ alias_method :inspect, :to_s
36
+
37
+ def self.parse(raw)
38
+ return nil if raw.nil? || raw.to_s.strip.empty?
39
+ s = raw.to_s.strip.upcase
40
+ return nil unless VALUES.include?(s)
41
+ new(s)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module PropertyIds
5
+ Entry = Struct.new(:id, :aliases, :applies_to, :multilingual, :value_kind, keyword_init: true)
6
+
7
+ REGISTRY = {
8
+ # ── Identity & version (applies_to: :all) ─────────────────────────
9
+ "MDC_P001" => Entry.new(id: "MDC_P001", aliases: %w[code_generic], applies_to: :all, multilingual: false, value_kind: :string),
10
+ "MDC_P001_1" => Entry.new(id: "MDC_P001_1", aliases: %w[code_dictionary], applies_to: :all, multilingual: false, value_kind: :string),
11
+ "MDC_P001_2" => Entry.new(id: "MDC_P001_2", aliases: %w[code_supplier], applies_to: :all, multilingual: false, value_kind: :string),
12
+ "MDC_P001_5" => Entry.new(id: "MDC_P001_5", aliases: %w[code], applies_to: :all, multilingual: false, value_kind: :string),
13
+ "MDC_P001_6" => Entry.new(id: "MDC_P001_6", aliases: %w[code_property], applies_to: :all, multilingual: false, value_kind: :string),
14
+ "MDC_P001_7" => Entry.new(id: "MDC_P001_7", aliases: %w[code_datatype], applies_to: :all, multilingual: false, value_kind: :string),
15
+ "MDC_P001_8" => Entry.new(id: "MDC_P001_8", aliases: %w[code_document], applies_to: :all, multilingual: false, value_kind: :string),
16
+ "MDC_P001_10" => Entry.new(id: "MDC_P001_10", aliases: %w[code_unit], applies_to: :all, multilingual: false, value_kind: :string),
17
+ "MDC_P001_11" => Entry.new(id: "MDC_P001_11", aliases: %w[code_value_term], applies_to: :all, multilingual: false, value_kind: :string),
18
+ "MDC_P001_12" => Entry.new(id: "MDC_P001_12", aliases: %w[code_value_list], applies_to: :all, multilingual: false, value_kind: :string),
19
+ "MDC_P001_13" => Entry.new(id: "MDC_P001_13", aliases: %w[code_relation], applies_to: :all, multilingual: false, value_kind: :string),
20
+ "EXT_P001" => Entry.new(id: "EXT_P001", aliases: %w[code_view_control], applies_to: :all, multilingual: false, value_kind: :string),
21
+ "MDC_P002_1" => Entry.new(id: "MDC_P002_1", aliases: %w[version], applies_to: :all, multilingual: false, value_kind: :string),
22
+ "MDC_P002_2" => Entry.new(id: "MDC_P002_2", aliases: %w[revision], applies_to: :all, multilingual: false, value_kind: :string),
23
+ "MDC_P003_1" => Entry.new(id: "MDC_P003_1", aliases: %w[original_definition_date], applies_to: :all, multilingual: false, value_kind: :date),
24
+ "MDC_P003_2" => Entry.new(id: "MDC_P003_2", aliases: %w[current_version_date], applies_to: :all, multilingual: false, value_kind: :date),
25
+ "MDC_P003_3" => Entry.new(id: "MDC_P003_3", aliases: %w[current_revision_date], applies_to: :all, multilingual: false, value_kind: :date),
26
+
27
+ # ── Names & definitions (applies_to: :all, mostly multilingual) ──
28
+ "MDC_P004" => Entry.new(id: "MDC_P004", aliases: %w[preferred_name], applies_to: :all, multilingual: true, value_kind: :string),
29
+ "MDC_P004_1" => Entry.new(id: "MDC_P004_1", aliases: %w[preferred_name_localized], applies_to: :all, multilingual: true, value_kind: :string),
30
+ "MDC_P004_2" => Entry.new(id: "MDC_P004_2", aliases: %w[synonymous_names], applies_to: :all, multilingual: true, value_kind: :string),
31
+ "MDC_P004_3" => Entry.new(id: "MDC_P004_3", aliases: %w[short_name_localized], applies_to: :all, multilingual: true, value_kind: :string),
32
+ "MDC_P004_4" => Entry.new(id: "MDC_P004_4", aliases: %w[name_icon], applies_to: :all, multilingual: false, value_kind: :identifier_ref),
33
+ "MDC_P005" => Entry.new(id: "MDC_P005", aliases: %w[short_name], applies_to: :all, multilingual: true, value_kind: :string),
34
+ "MDC_P005_1" => Entry.new(id: "MDC_P005_1", aliases: %w[short_name_variant], applies_to: :all, multilingual: true, value_kind: :string),
35
+ "MDC_P006" => Entry.new(id: "MDC_P006", aliases: %w[definition], applies_to: :all, multilingual: true, value_kind: :string),
36
+ "MDC_P006_1" => Entry.new(id: "MDC_P006_1", aliases: %w[source_document_of_definition], applies_to: :all, multilingual: false, value_kind: :identifier_ref),
37
+ "MDC_P007" => Entry.new(id: "MDC_P007", aliases: %w[synonym], applies_to: :all, multilingual: true, value_kind: :string),
38
+ "MDC_P007_1" => Entry.new(id: "MDC_P007_1", aliases: %w[note_localized], applies_to: :all, multilingual: true, value_kind: :string),
39
+ "MDC_P007_2" => Entry.new(id: "MDC_P007_2", aliases: %w[remark_localized], applies_to: :all, multilingual: true, value_kind: :string),
40
+ "MDC_P008" => Entry.new(id: "MDC_P008", aliases: %w[note], applies_to: :all, multilingual: true, value_kind: :string),
41
+ "MDC_P008_1" => Entry.new(id: "MDC_P008_1", aliases: %w[simplified_drawing], applies_to: :all, multilingual: false, value_kind: :identifier_ref),
42
+ "MDC_P008_2" => Entry.new(id: "MDC_P008_2", aliases: %w[graphics], applies_to: :all, multilingual: false, value_kind: :identifier_ref),
43
+ "MDC_P009" => Entry.new(id: "MDC_P009", aliases: %w[remark], applies_to: :all, multilingual: true, value_kind: :string),
44
+
45
+ # ── Class-level (applies_to: :class) ──────────────────────────────
46
+ "MDC_P010" => Entry.new(id: "MDC_P010", aliases: %w[superclass], applies_to: :class, multilingual: false, value_kind: :identifier_ref),
47
+ "MDC_P010_1" => Entry.new(id: "MDC_P010_1", aliases: %w[superclass_localized known_superclasses], applies_to: :class, multilingual: false, value_kind: :identifier_ref),
48
+ "MDC_P011" => Entry.new(id: "MDC_P011", aliases: %w[class_type], applies_to: :class, multilingual: false, value_kind: :string),
49
+ "MDC_P012" => Entry.new(id: "MDC_P012", aliases: %w[supplier_class_ref], applies_to: :class, multilingual: false, value_kind: :identifier_ref),
50
+ "MDC_P013" => Entry.new(id: "MDC_P013", aliases: %w[is_case_of], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
51
+ "MDC_P014" => Entry.new(id: "MDC_P014", aliases: %w[applicable_properties], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
52
+ "MDC_P014_1" => Entry.new(id: "MDC_P014_1", aliases: %w[known_applicable_properties], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
53
+ "MDC_P014_2" => Entry.new(id: "MDC_P014_2", aliases: %w[visible_properties], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
54
+ "MDC_P015" => Entry.new(id: "MDC_P015", aliases: %w[applicable_types], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
55
+ "MDC_P015_1" => Entry.new(id: "MDC_P015_1", aliases: %w[known_applicable_types], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
56
+ "MDC_P015_2" => Entry.new(id: "MDC_P015_2", aliases: %w[visible_types], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
57
+ "MDC_P016" => Entry.new(id: "MDC_P016", aliases: %w[sub_class_selection], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
58
+ "MDC_P033" => Entry.new(id: "MDC_P033", aliases: %w[type_classification], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
59
+ "MDC_P040" => Entry.new(id: "MDC_P040", aliases: %w[det_classification], applies_to: :property, multilingual: false, value_kind: :string),
60
+ "MDC_P090" => Entry.new(id: "MDC_P090", aliases: %w[imported_properties], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
61
+ "MDC_P091" => Entry.new(id: "MDC_P091", aliases: %w[imported_types], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
62
+ "MDC_P093" => Entry.new(id: "MDC_P093", aliases: %w[imported_documents], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
63
+ "MDC_P094" => Entry.new(id: "MDC_P094", aliases: %w[applicable_documents], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
64
+ "MDC_P094_1" => Entry.new(id: "MDC_P094_1", aliases: %w[known_applicable_documents], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
65
+ "MDC_P094_2" => Entry.new(id: "MDC_P094_2", aliases: %w[visible_documents], applies_to: :class, multilingual: false, value_kind: :set_of_refs),
66
+
67
+ # ── Property-level (applies_to: :property unless noted) ──────────
68
+ "MDC_P017" => Entry.new(id: "MDC_P017", aliases: %w[class_value_assignment], applies_to: :property, multilingual: false, value_kind: :identifier_ref),
69
+ "MDC_P018" => Entry.new(id: "MDC_P018", aliases: %w[coded_name], applies_to: :property, multilingual: false, value_kind: :string),
70
+ "MDC_P018_1" => Entry.new(id: "MDC_P018_1", aliases: %w[value_term_data_type], applies_to: :all, multilingual: false, value_kind: :class_ref),
71
+ "MDC_P020" => Entry.new(id: "MDC_P020", aliases: %w[property_data_element_type property_det], applies_to: :property, multilingual: false, value_kind: :string),
72
+ "MDC_P021" => Entry.new(id: "MDC_P021", aliases: %w[definition_class], applies_to: :property, multilingual: false, value_kind: :identifier_ref),
73
+ "MDC_P022" => Entry.new(id: "MDC_P022", aliases: %w[data_type], applies_to: :property, multilingual: false, value_kind: :class_ref),
74
+ "MDC_P023" => Entry.new(id: "MDC_P023", aliases: %w[unit_structure unit_symbol], applies_to: :property, multilingual: false, value_kind: :identifier_ref),
75
+ "MDC_P023_1" => Entry.new(id: "MDC_P023_1", aliases: %w[unit_in_text], applies_to: :property, multilingual: false, value_kind: :identifier_ref),
76
+ "MDC_P023_2" => Entry.new(id: "MDC_P023_2", aliases: %w[unit_in_sgml], applies_to: :property, multilingual: false, value_kind: :identifier_ref),
77
+ "MDC_P024" => Entry.new(id: "MDC_P024", aliases: %w[value_format], applies_to: :property, multilingual: false, value_kind: :string),
78
+ "MDC_P025_1" => Entry.new(id: "MDC_P025_1", aliases: %w[symbol preferred_symbol_text symbol_in_text], applies_to: :property, multilingual: false, value_kind: :string),
79
+ "MDC_P025_2" => Entry.new(id: "MDC_P025_2", aliases: %w[preferred_symbol_sgml], applies_to: :property, multilingual: false, value_kind: :string),
80
+ "MDC_P025_3" => Entry.new(id: "MDC_P025_3", aliases: %w[synonymous_symbol], applies_to: :property, multilingual: false, value_kind: :string),
81
+ "MDC_P026" => Entry.new(id: "MDC_P026", aliases: [], applies_to: :property, multilingual: false, value_kind: :string),
82
+ "MDC_P026_1" => Entry.new(id: "MDC_P026_1", aliases: [], applies_to: :property, multilingual: false, value_kind: :string),
83
+ "MDC_P027_1" => Entry.new(id: "MDC_P027_1", aliases: %w[formula_text property_formula], applies_to: :property, multilingual: false, value_kind: :string),
84
+ "MDC_P027_2" => Entry.new(id: "MDC_P027_2", aliases: %w[formula_sgml property_formula_localized], applies_to: :property, multilingual: false, value_kind: :string),
85
+ "MDC_P028" => Entry.new(id: "MDC_P028", aliases: %w[condition], applies_to: :property, multilingual: false, value_kind: :condition),
86
+ "MDC_P030" => Entry.new(id: "MDC_P030", aliases: [], applies_to: :property, multilingual: false, value_kind: :identifier_ref),
87
+ "MDC_P031" => Entry.new(id: "MDC_P031", aliases: [], applies_to: :property, multilingual: false, value_kind: :set_of_refs),
88
+ "MDC_P032" => Entry.new(id: "MDC_P032", aliases: [], applies_to: :property, multilingual: false, value_kind: :string),
89
+ "MDC_P041" => Entry.new(id: "MDC_P041", aliases: %w[unit unit_irdi codefor_unit], applies_to: :property, multilingual: false, value_kind: :identifier_ref),
90
+ "MDC_P042" => Entry.new(id: "MDC_P042", aliases: %w[alternative_units codefor_alt_unit], applies_to: :property, multilingual: false, value_kind: :set_of_refs),
91
+ "MDC_P043" => Entry.new(id: "MDC_P043", aliases: %w[enumerated_terms term_irdis], applies_to: :value_list, multilingual: false, value_kind: :set_of_refs),
92
+ "MDC_P044" => Entry.new(id: "MDC_P044", aliases: %w[enumerated_values code_list], applies_to: :value_list, multilingual: false, value_kind: :set_of_refs),
93
+ "MDC_P045" => Entry.new(id: "MDC_P045", aliases: %w[selection_count], applies_to: :value_list, multilingual: false, value_kind: :string),
94
+ "MDC_P046" => Entry.new(id: "MDC_P046", aliases: %w[list_type], applies_to: :value_list, multilingual: false, value_kind: :string),
95
+ "MDC_P068" => Entry.new(id: "MDC_P068", aliases: %w[constraint property_constraint], applies_to: :property, multilingual: false, value_kind: :string),
96
+ "MDC_P096" => Entry.new(id: "MDC_P096", aliases: %w[property_classification], applies_to: :property, multilingual: false, value_kind: :string),
97
+ "MDC_P097" => Entry.new(id: "MDC_P097", aliases: %w[requirement], applies_to: :property, multilingual: false, value_kind: :string),
98
+ "MDC_P101" => Entry.new(id: "MDC_P101", aliases: %w[alternate_id], applies_to: :all, multilingual: false, value_kind: :string),
99
+ "MDC_P102" => Entry.new(id: "MDC_P102", aliases: %w[alternate_class_id], applies_to: :all, multilingual: false, value_kind: :string),
100
+ "MDC_P110" => Entry.new(id: "MDC_P110", aliases: %w[super_property], applies_to: :property, multilingual: false, value_kind: :identifier_ref),
101
+ "MDC_P111" => Entry.new(id: "MDC_P111", aliases: %w[alternative_units_list], applies_to: :property, multilingual: false, value_kind: :set_of_refs),
102
+ "MDC_P112" => Entry.new(id: "MDC_P112", aliases: %w[description], applies_to: :all, multilingual: true, value_kind: :string),
103
+ "MDC_P113" => Entry.new(id: "MDC_P113", aliases: %w[example], applies_to: :all, multilingual: false, value_kind: :string),
104
+ "MDC_P114" => Entry.new(id: "MDC_P114", aliases: %w[quantity], applies_to: :property, multilingual: false, value_kind: :identifier_ref),
105
+
106
+ # ── Relation-level (applies_to: :relation) ────────────────────────
107
+ "MDC_P200" => Entry.new(id: "MDC_P200", aliases: %w[relation_type], applies_to: :relation, multilingual: false, value_kind: :string),
108
+ "MDC_P201" => Entry.new(id: "MDC_P201", aliases: %w[domain domain_of_relation], applies_to: :relation, multilingual: false, value_kind: :set_of_refs),
109
+ "MDC_P202" => Entry.new(id: "MDC_P202", aliases: %w[domain_of_function], applies_to: :relation, multilingual: false, value_kind: :set_of_refs),
110
+ "MDC_P203" => Entry.new(id: "MDC_P203", aliases: %w[codomain codomain_of_function], applies_to: :relation, multilingual: false, value_kind: :identifier_ref),
111
+ "MDC_P204" => Entry.new(id: "MDC_P204", aliases: %w[formula relation_formula], applies_to: :relation, multilingual: false, value_kind: :string),
112
+ "MDC_P205" => Entry.new(id: "MDC_P205", aliases: %w[formula_language], applies_to: :relation, multilingual: false, value_kind: :string),
113
+ "MDC_P206" => Entry.new(id: "MDC_P206", aliases: %w[external_solver], applies_to: :relation, multilingual: false, value_kind: :string),
114
+ "MDC_P207" => Entry.new(id: "MDC_P207", aliases: %w[trigger_event], applies_to: :relation, multilingual: false, value_kind: :string),
115
+ "MDC_P208" => Entry.new(id: "MDC_P208", aliases: %w[domain_element_type], applies_to: :relation, multilingual: false, value_kind: :string),
116
+ "MDC_P209" => Entry.new(id: "MDC_P209", aliases: %w[codomain_element_type], applies_to: :relation, multilingual: false, value_kind: :string),
117
+ "MDC_P210" => Entry.new(id: "MDC_P210", aliases: %w[role], applies_to: :relation, multilingual: false, value_kind: :string),
118
+ "MDC_P211" => Entry.new(id: "MDC_P211", aliases: %w[segment], applies_to: :relation, multilingual: false, value_kind: :string),
119
+ "MDC_P212" => Entry.new(id: "MDC_P212", aliases: %w[super_relation_irdi super_relation], applies_to: :relation, multilingual: false, value_kind: :identifier_ref),
120
+ "MDC_P230" => Entry.new(id: "MDC_P230", aliases: %w[applicable_relations], applies_to: :relation, multilingual: false, value_kind: :set_of_refs),
121
+ "MDC_P231" => Entry.new(id: "MDC_P231", aliases: %w[applicable_terms], applies_to: :relation, multilingual: false, value_kind: :set_of_refs),
122
+
123
+ # ── CIM extension ─────────────────────────────────────────────────
124
+ "CIM_P001" => Entry.new(id: "CIM_P001", aliases: %w[cim_uml_id], applies_to: :all, multilingual: false, value_kind: :string),
125
+ "CIM_P002" => Entry.new(id: "CIM_P002", aliases: %w[cim_package], applies_to: :all, multilingual: false, value_kind: :string),
126
+ "CIM_P003" => Entry.new(id: "CIM_P003", aliases: %w[cim_property_type], applies_to: :all, multilingual: false, value_kind: :string),
127
+ "CIM_P004" => Entry.new(id: "CIM_P004", aliases: %w[cim_basic_datatype], applies_to: :all, multilingual: false, value_kind: :string),
128
+ "CIM_P005" => Entry.new(id: "CIM_P005", aliases: %w[cim_multiplicity], applies_to: :all, multilingual: false, value_kind: :string),
129
+
130
+ # ── Global (applies_to: :all) ─────────────────────────────────────
131
+ "MDC_P066" => Entry.new(id: "MDC_P066", aliases: %w[data_object_identifier], applies_to: :all, multilingual: false, value_kind: :string),
132
+ "MDC_P067" => Entry.new(id: "MDC_P067", aliases: %w[time_stamp], applies_to: :all, multilingual: false, value_kind: :date_time),
133
+
134
+ # ── View-control extension ────────────────────────────────────────
135
+ "EXT_P002" => Entry.new(id: "EXT_P002", aliases: %w[controlled_classes], applies_to: :view_control, multilingual: false, value_kind: :set_of_refs),
136
+ "EXT_P003" => Entry.new(id: "EXT_P003", aliases: %w[shown_properties], applies_to: :view_control, multilingual: false, value_kind: :set_of_refs),
137
+ }.freeze
138
+
139
+ # PARCEL_VARIANT_TO_CANONICAL moved to
140
+ # Opencdd::Parcel::SheetSchema::VARIANT_TO_CANONICAL (format-
141
+ # specific mapping belongs in the format module, not the
142
+ # ontology registry). Kept here as an alias for back-compat.
143
+ PARCEL_VARIANT_TO_CANONICAL = Opencdd::Parcel::SheetSchema::VARIANT_TO_CANONICAL
144
+
145
+ REGISTRY.each_key do |id|
146
+ const_set(id, id.freeze) unless const_defined?(id, false)
147
+ end
148
+
149
+ def self.entry(id)
150
+ REGISTRY[id.to_s]
151
+ end
152
+
153
+ def self.canonical_id(name)
154
+ key = name.to_s
155
+ return key if REGISTRY.key?(key)
156
+ alias_map[key]
157
+ end
158
+
159
+ def self.multilingual?(id)
160
+ e = entry(id)
161
+ e ? e.multilingual : false
162
+ end
163
+
164
+ def self.applies_to(id)
165
+ e = entry(id)
166
+ e ? e.applies_to : nil
167
+ end
168
+
169
+ def self.value_kind(id)
170
+ e = entry(id)
171
+ e ? e.value_kind : nil
172
+ end
173
+
174
+ def self.all_ids
175
+ @all_ids ||= REGISTRY.keys.freeze
176
+ end
177
+
178
+ def self.alias_map
179
+ @alias_map ||= REGISTRY.each_with_object({}) do |(_id, e), h|
180
+ e.aliases.each { |a| h[a] = e.id }
181
+ end.freeze
182
+ end
183
+
184
+ def self.normalize(raw_id)
185
+ return nil if raw_id.nil?
186
+ s = raw_id.to_s.strip
187
+ return nil if s.empty?
188
+ match = s.match(/\.(?<lang>[A-Za-z0-9-]+)\z/)
189
+ base = match ? match.pre_match : s
190
+ lang = match && match[:lang]
191
+ canonical = canonical_id(base) || base
192
+ lang ? "#{canonical}.#{lang}" : canonical
193
+ end
194
+
195
+ # Back-compat delegate. The canonical implementation lives at
196
+ # Opencdd::Parcel::SheetSchema.canonical_id (format-specific
197
+ # concern, not ontology).
198
+ def self.canonical_parcel_id(raw_id)
199
+ Opencdd::Parcel::SheetSchema.canonical_id(raw_id)
200
+ end
201
+ end
202
+ end