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,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class Reader
5
+ XLSX_EXTENSIONS = %w[.xlsx .xlsm .xltx].freeze
6
+
7
+ # Layout detection is owned by Opencdd::Parcel::LayoutDetector
8
+ # (single source of truth). These regexes are exposed here as
9
+ # backward-compat constants for external callers; new code
10
+ # should call LayoutDetector directly.
11
+ SHARDED_CLASS_CODE = Opencdd::Parcel::LayoutDetector::CLASS_CODE_PATTERN
12
+ UNID_PATTERN = Opencdd::Parcel::LayoutDetector::UNID_PATTERN
13
+ LEGACY_XLS_RE = Opencdd::Parcel::LayoutDetector::FILE_PATTERN
14
+
15
+ class << self
16
+ def load_database(path)
17
+ new(path).load
18
+ end
19
+
20
+ def detect(path)
21
+ case File.basename(path)
22
+ when LEGACY_XLS_RE then :legacy_single
23
+ else
24
+ if File.directory?(path)
25
+ children = Dir.children(path)
26
+ if children.any? { |f| f =~ LEGACY_XLS_RE }
27
+ :legacy_dir
28
+ elsif children.any? { |f| sharded_class_subdir?(File.join(path, f)) }
29
+ :sharded_dir
30
+ else
31
+ :unknown_dir
32
+ end
33
+ elsif XLSX_EXTENSIONS.include?(File.extname(path).downcase)
34
+ :xlsx
35
+ elsif File.extname(path).downcase == ".xls"
36
+ :legacy_single
37
+ else
38
+ :unknown
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ # A class-code subdir is "sharded" if it contains export_*.xls
45
+ # directly (legacy flat layout) OR exposes the per-version layout
46
+ # (+_entity.json+ pointing at a UNID subfolder, or a single UNID
47
+ # subfolder holding export_*.xls).
48
+ def self.sharded_class_subdir?(path)
49
+ return false unless File.directory?(path)
50
+ return false unless File.basename(path) =~ SHARDED_CLASS_CODE
51
+
52
+ children = Dir.children(path)
53
+ return true if children.any? { |f| f =~ LEGACY_XLS_RE }
54
+
55
+ # Per-version layout: _entity.json names a UNID subfolder.
56
+ entity_idx = File.join(path, "_entity.json")
57
+ if File.file?(entity_idx)
58
+ require "json"
59
+ begin
60
+ data = JSON.parse(File.read(entity_idx))
61
+ current = data["current_version_dir"]
62
+ return true if current && current =~ UNID_PATTERN &&
63
+ File.directory?(File.join(path, current))
64
+ rescue JSON::ParserError
65
+ # fall through
66
+ end
67
+ end
68
+
69
+ # Per-version layout without _entity.json: a UNID subfolder holds XLS.
70
+ unid_subdirs = children.select { |n| n =~ UNID_PATTERN }
71
+ .map { |n| File.join(path, n) }
72
+ unid_subdirs.any? do |p|
73
+ File.directory?(p) && Dir.children(p).any? { |f| f =~ LEGACY_XLS_RE }
74
+ end
75
+ end
76
+
77
+ attr_reader :path
78
+
79
+ def initialize(path)
80
+ @path = path
81
+ end
82
+
83
+ def load
84
+ db = Opencdd::Database.new
85
+ load_into(db)
86
+ db.finalize!
87
+ db
88
+ end
89
+
90
+ def load_into(db)
91
+ case Opencdd::Reader.detect(@path)
92
+ when :xlsx, :legacy_single
93
+ Opencdd::Parcel::WorkbookReader.new(@path).load_into(db)
94
+ when :legacy_dir
95
+ Opencdd::Parcel::FlatDirReader.new(@path).load_into(db)
96
+ when :sharded_dir
97
+ Opencdd::Parcel::ShardedDirReader.new(@path).load_into(db)
98
+ else
99
+ raise ArgumentError, "Cannot detect Parcel/Excel format at #{@path.inspect}"
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class Relation < Opencdd::Entity
5
+ RELATION_TYPES = {
6
+ "FUNCTION" => :function,
7
+ "PREDICATION" => :predication,
8
+ }.freeze
9
+
10
+ # ── Pure field reads ─────────────────────────────────────────
11
+ field :domain_of_function_irdis, "MDC_P202"
12
+ field :codomain_irdi, "MDC_P203"
13
+ field :formula, "MDC_P204", :string
14
+ field :formula_language, "MDC_P205", :string
15
+ field :external_solver, "MDC_P206", :string
16
+ field :trigger_event, "MDC_P207", :string
17
+ field :domain_element_type, "MDC_P208", :string
18
+ field :codomain_element_type, "MDC_P209", :string
19
+ field :role, "MDC_P210", :string
20
+ field :segment, "MDC_P211", :string
21
+ field :super_relation_irdi, "MDC_P212"
22
+
23
+ # ── Computed fields with block-form readers ──────────────────
24
+ field(:relation_type, synthetic: true) do
25
+ @relation_type_value ||= Opencdd::RelationType.parse(properties[Opencdd::PropertyIds::MDC_P200]) ||
26
+ parse_legacy_relation_type
27
+ end
28
+ field(:domain_irdis, synthetic: true) do
29
+ list = []
30
+ [Opencdd::PropertyIds::MDC_P201, Opencdd::PropertyIds::MDC_P202].each do |key|
31
+ list.concat(parse_irdi_list(properties[key]))
32
+ end
33
+ list
34
+ end
35
+
36
+ def relation_type_symbol
37
+ value = relation_type
38
+ value ? value.to_sym : nil
39
+ end
40
+
41
+ def predication?
42
+ rt = relation_type
43
+ rt ? rt.predication? : false
44
+ end
45
+
46
+ def function?
47
+ rt = relation_type
48
+ rt ? rt.function? : false
49
+ end
50
+
51
+ def association?
52
+ rt = relation_type
53
+ rt ? rt.association? : false
54
+ end
55
+
56
+ def aggregation?
57
+ rt = relation_type
58
+ rt ? rt.aggregation? : false
59
+ end
60
+
61
+ def composition?
62
+ rt = relation_type
63
+ rt ? rt.composition? : false
64
+ end
65
+
66
+ def generalization?
67
+ rt = relation_type
68
+ rt ? rt.generalization? : false
69
+ end
70
+
71
+ def specialization?
72
+ rt = relation_type
73
+ rt ? rt.specialization? : false
74
+ end
75
+
76
+ private
77
+
78
+ def parse_legacy_relation_type
79
+ raw = properties[Opencdd::PropertyIds::MDC_P200]
80
+ sym = RELATION_TYPES[raw]
81
+ return nil unless sym
82
+ case sym
83
+ when :function then Opencdd::RelationType.new("FUNCTION")
84
+ when :predication then Opencdd::RelationType.new("PREDICATION")
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class RelationTree
5
+ class Node < Struct.new(:relation, :children, keyword_init: true)
6
+ include Enumerable
7
+
8
+ def each(&block)
9
+ block.call(self)
10
+ (children || []).each { |c| c.each(&block) }
11
+ end
12
+
13
+ def depth
14
+ return 1 if children.nil? || children.empty?
15
+ 1 + children.map(&:depth).max
16
+ end
17
+
18
+ def size
19
+ 1 + (children || []).sum(&:size)
20
+ end
21
+ end
22
+
23
+ attr_reader :database
24
+
25
+ def initialize(database)
26
+ @database = database
27
+ end
28
+
29
+ def for(root = nil, max_depth: 10)
30
+ roots = root.nil? ? root_relations : [lookup_relation(root)].compact
31
+ roots.map { |r| build_node(r, Set.new, max_depth) }
32
+ end
33
+
34
+ private
35
+
36
+ def build_node(relation, path, max_depth)
37
+ return nil if relation.nil?
38
+ return nil if path.include?(relation.irdi)
39
+ return nil if max_depth <= 0
40
+
41
+ child_path = path + [relation.irdi]
42
+ children = children_of(relation).map { |c| build_node(c, child_path, max_depth - 1) }.compact
43
+ Node.new(relation: relation, children: children)
44
+ end
45
+
46
+ def children_of(relation)
47
+ index.fetch(relation.irdi, [])
48
+ end
49
+
50
+ def root_relations
51
+ database.relations.reject { |r| r.super_relation_irdi }
52
+ end
53
+
54
+ def lookup_relation(ref)
55
+ database.resolve_reference(ref)
56
+ end
57
+
58
+ def index
59
+ @index ||= build_index
60
+ end
61
+
62
+ def build_index
63
+ hash = Hash.new { |h, k| h[k] = [] }
64
+ database.relations.each do |r|
65
+ parent_raw = r.properties[Opencdd::PropertyIds::MDC_P212]
66
+ next unless parent_raw && !parent_raw.to_s.strip.empty?
67
+ parent = database.resolve_reference(parent_raw)
68
+ next unless parent
69
+ hash[parent.irdi] << r unless hash[parent.irdi].include?(r)
70
+ end
71
+ hash
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class RelationType
5
+ VALUES = %w[
6
+ PREDICATION FUNCTION ASSOCIATION AGGREGATION COMPOSITION
7
+ GENERALIZATION SPECIALIZATION
8
+ ].freeze
9
+
10
+ attr_reader :value
11
+
12
+ def initialize(value)
13
+ v = value.to_s.strip.upcase
14
+ raise ArgumentError, "unknown relation type: #{value.inspect}" unless VALUES.include?(v)
15
+ @value = v
16
+ end
17
+
18
+ def predication? = @value == "PREDICATION"
19
+ def function? = @value == "FUNCTION"
20
+ def association? = @value == "ASSOCIATION"
21
+ def aggregation? = @value == "AGGREGATION"
22
+ def composition? = @value == "COMPOSITION"
23
+ def generalization? = @value == "GENERALIZATION"
24
+ def specialization? = @value == "SPECIALIZATION"
25
+
26
+ def hierarchical?
27
+ generalization? || specialization? || aggregation? || composition?
28
+ end
29
+
30
+ def to_s = @value
31
+ def to_sym = @value.downcase.to_sym
32
+
33
+ def ==(other)
34
+ other.is_a?(Opencdd::RelationType) && @value == other.value
35
+ end
36
+ alias_method :eql?, :==
37
+
38
+ def hash
39
+ @value.hash
40
+ end
41
+
42
+ alias_method :inspect, :to_s
43
+
44
+ def self.parse(raw)
45
+ return nil if raw.nil? || raw.to_s.strip.empty?
46
+ s = raw.to_s.strip.upcase
47
+ return nil unless VALUES.include?(s)
48
+ new(s)
49
+ end
50
+
51
+ def self.parse_or_symbol(raw)
52
+ parsed = parse(raw)
53
+ return parsed if parsed
54
+ return nil if raw.nil? || raw.to_s.strip.empty?
55
+ raw.to_s.strip.upcase.to_sym
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ # Single source of truth for parsing/serialising CDD's structured
5
+ # property-value wire forms: reference sets, synonym tuples,
6
+ # class references, data types.
7
+ #
8
+ # Every format reader, writer, validator, and exporter that touches
9
+ # a `{a,b,c}` or `(a,b)` collection goes through this module.
10
+ # The brace/paren/comma convention lives here exactly once; a bug
11
+ # fixed here is fixed everywhere.
12
+ module StructuredValues
13
+ module_function
14
+
15
+ # ── Generic collection seam ─────────────────────────────────
16
+ #
17
+ # Used by Database#normalize_reference_collections!,
18
+ # Database#rewrite_back_references!, Cddal::Builder,
19
+ # Cddal::Serializer, Validator::ReferenceRule,
20
+ # Validator::ClassReferenceRule. Replaces six hand-rolled
21
+ # variants of "strip delimiters, split, reject empties".
22
+
23
+ # Split a `{a,b,c}` or `(a,b,c)` literal into `[a, b, c]`.
24
+ # Accepts unwrapped strings too (split directly). Empty/blank
25
+ # input returns []. Respects nested `{}`/`()` so
26
+ # `{(a,b),(c,d)}` yields two tuple strings, not four fragments.
27
+ def unwrap_and_split(value)
28
+ return [] if value.nil? || value.to_s.strip.empty?
29
+ s = value.to_s.strip
30
+ s = s[1..-2] if (s.start_with?("{") && s.end_with?("}")) ||
31
+ (s.start_with?("(") && s.end_with?(")"))
32
+ split_at_top_level_commas(s)
33
+ end
34
+
35
+ # Inverse of +unwrap_and_split+: join with `,` and wrap in `{}`.
36
+ # Empty input returns "".
37
+ def rejoin(elements)
38
+ elements = Array(elements).map(&:to_s).reject { |e| e.nil? || e.strip.empty? }
39
+ return "" if elements.empty?
40
+ "{#{elements.join(",")}}"
41
+ end
42
+
43
+ def parse_synonyms(value)
44
+ return [] if value.nil? || value.to_s.strip.empty?
45
+ SynonymTupleScanner.new(value.to_s).scan
46
+ end
47
+
48
+ def serialize_synonyms(pairs)
49
+ return "" if pairs.nil? || pairs.empty?
50
+ tuples = Array(pairs).map do |pair|
51
+ lang, name = Array(pair)
52
+ "(#{name},#{lang})"
53
+ end
54
+ "{#{tuples.join(",")}}"
55
+ end
56
+
57
+ def parse_ref_set(value)
58
+ return [] if value.nil? || value.to_s.strip.empty?
59
+ unwrap_and_split(value).filter_map { |t| Opencdd::IRDI.parse(t) }
60
+ end
61
+
62
+ def serialize_ref_set(irdis)
63
+ return "" if irdis.nil? || irdis.empty?
64
+ elements = Array(irdis).map { |i| i.is_a?(Opencdd::IRDI) ? i.to_s : i.to_s }
65
+ "{#{elements.join(",")}}"
66
+ end
67
+
68
+ def parse_class_ref(value)
69
+ return nil if value.nil? || value.to_s.strip.empty?
70
+ Opencdd::IRDI.parse(value.to_s.strip)
71
+ end
72
+
73
+ def serialize_class_ref(irdi)
74
+ return "" if irdi.nil?
75
+ irdi.is_a?(Opencdd::IRDI) ? irdi.to_s : irdi.to_s
76
+ end
77
+
78
+ def parse_data_type(value)
79
+ return nil if value.nil? || value.to_s.strip.empty?
80
+ Opencdd::DataType.parse_or_string(value)
81
+ end
82
+
83
+ # ── Internal: split on commas at the top nesting level only ─
84
+ # Needed so `{(a,b),(c,d)}` → `["(a,b)", "(c,d)"]`, not four
85
+ # fragments.
86
+ def split_at_top_level_commas(s)
87
+ result = []
88
+ current = +""
89
+ depth = 0
90
+ s.each_char do |ch|
91
+ case ch
92
+ when "{", "(" then depth += 1; current << ch
93
+ when "}", ")" then depth -= 1; current << ch
94
+ when ","
95
+ if depth.zero?
96
+ result << current.strip unless current.strip.empty?
97
+ current = +""
98
+ else
99
+ current << ch
100
+ end
101
+ else
102
+ current << ch
103
+ end
104
+ end
105
+ result << current.strip unless current.strip.empty?
106
+ result
107
+ end
108
+ private_class_method :split_at_top_level_commas
109
+
110
+ def serialize_data_type(data_type)
111
+ return "" if data_type.nil?
112
+ data_type.to_s
113
+ end
114
+
115
+ def parse_condition(value)
116
+ return nil if value.nil? || value.to_s.strip.empty?
117
+ Opencdd::Condition.parse(value)
118
+ rescue ArgumentError
119
+ nil
120
+ end
121
+
122
+ def serialize_condition(condition)
123
+ return "" if condition.nil?
124
+ condition.to_s
125
+ end
126
+
127
+ def parse_value_format(value)
128
+ return nil if value.nil? || value.to_s.strip.empty?
129
+ Opencdd::ValueFormat.parse(value)
130
+ end
131
+
132
+ def serialize_value_format(format)
133
+ return "" if format.nil?
134
+ format.to_s
135
+ end
136
+ end
137
+ end
138
+
139
+ # Internal scanner for the `{(name,lang),(name,lang)}` synonym wire
140
+ # form. Lives here so StructuredValues is the single home for all
141
+ # collection parsing. Parsed via StringScanner because the format
142
+ # can include quoted commas inside names.
143
+ class Opencdd::StructuredValues::SynonymTupleScanner
144
+ def initialize(source)
145
+ require "strscan"
146
+ @ss = StringScanner.new(source)
147
+ end
148
+
149
+ def scan
150
+ @ss.skip(/\s*\{?\s*/)
151
+ collect_tuples
152
+ end
153
+
154
+ private
155
+
156
+ def collect_tuples
157
+ result = []
158
+ while @ss.check(/\(/)
159
+ result << scan_tuple
160
+ @ss.skip(/\s*,\s*/)
161
+ end
162
+ result
163
+ end
164
+
165
+ def scan_tuple
166
+ @ss.skip(/\(\s*/)
167
+ name = scan_element_until(",")
168
+ @ss.skip(/,\s*/)
169
+ lang = scan_element_until(")")
170
+ @ss.skip(/\)/)
171
+ [lang&.strip, name&.strip]
172
+ end
173
+
174
+ def scan_element_until(delimiter)
175
+ start = @ss.pos
176
+ until @ss.eos?
177
+ ch = @ss.peek(1)
178
+ return @ss.string[start...@ss.pos] if ch == delimiter || ch == ")"
179
+ @ss.getch
180
+ end
181
+ @ss.string[start...@ss.pos]
182
+ end
183
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class Unit < Opencdd::Entity
5
+ # ── Pure field reads ─────────────────────────────────────────
6
+ # MDC_P023/023_1/023_2 are :identifier_ref in REGISTRY but used
7
+ # as raw strings for unit symbols/representations. Explicit :string
8
+ # override preserves historical behavior.
9
+ field :structure, "MDC_P023", :string
10
+ field :text_representation, "MDC_P023_1", :string
11
+ field :sgml_representation, "MDC_P023_2", :string
12
+ field :definition_class_irdi, "MDC_P021"
13
+
14
+ alias_method :symbol, :text_representation
15
+ end
16
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ # R16 — CLASS_REFERENCE target validation.
6
+ #
7
+ # When a property's data_type is +CLASS_REFERENCE(CategoricalClass)+
8
+ # (or the column carries a CLASS_REFERENCE schema), the value IRDI
9
+ # must resolve to an entity that is a valid powertype instance of
10
+ # the named categorical class — i.e. one of its
11
+ # +categorical_instances+.
12
+ #
13
+ # This rule closes the loop on CDD's powertype semantics: a
14
+ # CLASS_REFERENCE data type doesn't just say "this is an IRDI";
15
+ # it says "this is an IRDI of an instance of this categorical
16
+ # class." Plain reference resolution (R08) doesn't catch the
17
+ # categorical constraint.
18
+ #
19
+ # Example: a Property `engine_type: CLASS_REFERENCE(EngineType)`
20
+ # whose value is "AAA001" (Vehicle) should fail — Vehicle is
21
+ # not a categorical instance of EngineType. Only SingleDiesel /
22
+ # TwinDiesel / ElectricHybrid (EngineType's subclasses) pass.
23
+ class ClassReferenceRule < Rule
24
+ def id
25
+ "R16"
26
+ end
27
+
28
+ def applies?(context)
29
+ return false unless context.database
30
+ parsed = parse_data_type(context)
31
+ parsed.is_a?(Opencdd::DataType::ClassReference)
32
+ end
33
+
34
+ def call(value, context)
35
+ return true if value.nil? || value.to_s.strip.empty?
36
+ target = categorical_target(context)
37
+ return true unless target # cannot validate without target resolution
38
+
39
+ refs(value).all? { |ref| context.database.valid_class_reference?(target, ref) }
40
+ end
41
+
42
+ def message(value, context)
43
+ target = categorical_target(context)
44
+ name = target ? target.code : "<unresolved>"
45
+ "R16: CLASS_REFERENCE(#{name}) value #{value.inspect} is not a valid powertype instance"
46
+ end
47
+
48
+ private
49
+
50
+ def parse_data_type(context)
51
+ raw = context.data_type
52
+ return nil if raw.nil? || raw.to_s.strip.empty?
53
+ Opencdd::DataType.parse(raw.to_s)
54
+ rescue ArgumentError
55
+ nil
56
+ end
57
+
58
+ def categorical_target(context)
59
+ @cache ||= {}
60
+ key = context.data_type
61
+ return @cache[key] if @cache.key?(key)
62
+ parsed = parse_data_type(context)
63
+ @cache[key] = parsed ? resolve_target(parsed, context) : nil
64
+ end
65
+
66
+ def resolve_target(parsed, context)
67
+ identifier = parsed.class_identifier
68
+ return nil unless identifier
69
+ context.database.resolve_reference(identifier)
70
+ end
71
+
72
+ def refs(value)
73
+ Opencdd::StructuredValues.unwrap_and_split(value)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class ConditionRule < Rule
6
+ def id
7
+ "R11"
8
+ end
9
+
10
+ def applies?(context)
11
+ context.column_iri == Opencdd::PropertyIds::MDC_P028 ||
12
+ context.value_kind == :condition
13
+ end
14
+
15
+ def call(value, _context)
16
+ return true if value.nil? || value.to_s.strip.empty?
17
+ !Opencdd::Condition.parse(value.to_s).nil?
18
+ rescue ArgumentError
19
+ false
20
+ end
21
+
22
+ def message(value, _context)
23
+ "R11: condition expression #{value.inspect} is not well-formed"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class DataTypeRule < Rule
6
+ def id
7
+ "R12"
8
+ end
9
+
10
+ def applies?(context)
11
+ context.column_iri == Opencdd::PropertyIds::MDC_P022
12
+ end
13
+
14
+ def call(value, _context)
15
+ return true if value.nil? || value.to_s.strip.empty?
16
+ !Opencdd::DataType.parse(value.to_s).nil?
17
+ rescue ArgumentError
18
+ false
19
+ end
20
+
21
+ def message(value, _context)
22
+ "R12: data type expression #{value.inspect} is not well-formed"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class EnumRule < Rule
6
+ def id
7
+ "R04"
8
+ end
9
+
10
+ def applies?(context)
11
+ return false unless context.data_type
12
+ context.data_type.to_s.start_with?("ENUM_")
13
+ end
14
+
15
+ def call(value, context)
16
+ return true if value.nil? || value.to_s.strip.empty?
17
+ terms = context.enum_terms_for(context.data_type)
18
+ return true if terms.empty?
19
+ terms.any? { |t| t.to_s == value.to_s }
20
+ end
21
+
22
+ def message(value, _context)
23
+ "R04: #{value.inspect} is not a member of the value list"
24
+ end
25
+ end
26
+ end
27
+ end