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,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Opencdd
6
+ class ClassTree
7
+ DEFAULT_FIELDS = %i[code name].freeze
8
+ AVAILABLE_FIELDS = %i[code name irdi definition class_type].freeze
9
+
10
+ include Enumerable
11
+
12
+ attr_reader :database, :fields
13
+
14
+ def initialize(database, fields: DEFAULT_FIELDS)
15
+ @database = database
16
+ @fields = fields
17
+ end
18
+
19
+ def roots
20
+ @database.root_classes
21
+ end
22
+
23
+ def each
24
+ return enum_for(:each) unless block_given?
25
+ walk(roots, 0) { |k, _d| yield k }
26
+ end
27
+
28
+ def each_node
29
+ return enum_for(:each_node) unless block_given?
30
+ walk(roots, 0) { |k, d| yield k, d }
31
+ end
32
+
33
+ def to_h(max_depth: nil, fields: @fields)
34
+ roots.map { |k| node_h(k, depth: 0, max_depth: max_depth, fields: fields) }
35
+ end
36
+
37
+ def to_yaml(max_depth: nil, fields: @fields)
38
+ to_h(max_depth: max_depth, fields: fields).to_yaml
39
+ end
40
+
41
+ def subtree(klass, max_depth: nil, fields: @fields)
42
+ node_h(klass, depth: 0, max_depth: max_depth, fields: fields)
43
+ end
44
+
45
+ def subtree_yaml(klass, **opts)
46
+ subtree(klass, **opts).to_yaml
47
+ end
48
+
49
+ private
50
+
51
+ def walk(nodes, depth)
52
+ nodes.each do |node|
53
+ yield node, depth
54
+ walk(node.children, depth + 1) { |n, d| yield n, d }
55
+ end
56
+ end
57
+
58
+ def node_h(klass, depth:, max_depth:, fields:)
59
+ h = {}
60
+ fields.each { |f| merge_field!(h, klass, f) }
61
+
62
+ if max_depth.nil? || depth < max_depth
63
+ kids = klass.children
64
+ h["children"] = kids.map { |c| node_h(c, depth: depth + 1, max_depth: max_depth, fields: fields) } unless kids.empty?
65
+ end
66
+ h
67
+ end
68
+
69
+ def merge_field!(h, klass, field)
70
+ case field
71
+ when :code then h["code"] = klass.code
72
+ when :name then h["name"] = klass.preferred_name
73
+ when :irdi then h["irdi"] = klass.irdi&.to_s
74
+ when :definition then h["definition"] = klass.definition unless klass.definition.to_s.empty?
75
+ when :class_type then h["class_type"] = klass.class_type.to_s if klass.class_type
76
+ else raise ArgumentError, "unknown field #{field.inspect}; valid: #{AVAILABLE_FIELDS.inspect}"
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class ClassType
5
+ VALUES = %w[ITEM_CLASS CATEGORICAL_CLASS VALUE_CLASS MESSAGE_CLASS].freeze
6
+
7
+ attr_reader :value
8
+
9
+ def initialize(value)
10
+ raise ArgumentError, "unknown class_type: #{value}" unless VALUES.include?(value.to_s)
11
+ @value = value.to_s
12
+ end
13
+
14
+ def item? = @value == "ITEM_CLASS"
15
+ def categorical? = @value == "CATEGORICAL_CLASS"
16
+ def value_class? = @value == "VALUE_CLASS"
17
+ def message? = @value == "MESSAGE_CLASS"
18
+
19
+ def to_s = @value
20
+ def to_sym = @value.downcase.to_sym
21
+
22
+ def self.parse(raw)
23
+ return nil if raw.nil? || raw.to_s.strip.empty?
24
+ s = raw.to_s.strip.upcase
25
+ return nil unless VALUES.include?(s)
26
+ new(s)
27
+ end
28
+
29
+ VALUES.each do |v|
30
+ define_method("#{v.downcase}?") { @value == v }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Opencdd
6
+ module Codegen
7
+ # Serializes the Ruby registries (Opencdd::PropertyIds::REGISTRY and
8
+ # Opencdd::MetaClasses) to TypeScript files that the OpenCDD Editor imports
9
+ # at build time. The editor has no runtime dependency on Ruby.
10
+ module Ts
11
+ module_function
12
+
13
+ def generate_all(editor_dir)
14
+ generate_property_ids(File.join(editor_dir, "src", "models", "PropertyIds.generated.ts"))
15
+ generate_meta_classes(File.join(editor_dir, "src", "models", "MetaClasses.generated.ts"))
16
+ end
17
+
18
+ def generate_property_ids(path)
19
+ write_file(path, render_property_ids)
20
+ end
21
+
22
+ # Returns the generated TS source as a string (no filesystem
23
+ # access). Testable in-memory. See TODO.impl/31.
24
+ def render_property_ids
25
+ registry = Opencdd::PropertyIds::REGISTRY
26
+
27
+ out = []
28
+ out << "// AUTO-GENERATED by `rake generate_ts`. Do not edit by hand."
29
+ out << "// Source: Opencdd::PropertyIds::REGISTRY in lib/opencdd/property_ids.rb"
30
+ out << ""
31
+ out << union_type("AppliesTo", registry.values.map { |e| e.applies_to.to_s }.uniq.sort)
32
+ out << union_type("ValueKind", registry.values.map { |e| e.value_kind.to_s }.uniq.sort)
33
+ out << ""
34
+ out << "export interface PropertyEntry {"
35
+ out << " readonly id: string;"
36
+ out << " readonly aliases: readonly string[];"
37
+ out << " readonly appliesTo: AppliesTo;"
38
+ out << " readonly multilingual: boolean;"
39
+ out << " readonly valueKind: ValueKind;"
40
+ out << "}"
41
+ out << ""
42
+ out << "export const REGISTRY: Readonly<Record<string, PropertyEntry>> = {"
43
+ registry.each_value { |e| out << " \"#{e.id}\": #{entry_literal(e)}," }
44
+ out << "};"
45
+ out << ""
46
+ registry.each_key { |id| out << "export const #{id} = \"#{id}\";" }
47
+ out << ""
48
+ out << PROPERTY_HELPERS
49
+ out << ""
50
+ out << alias_map_literal(registry)
51
+ out.join("\n") + "\n"
52
+ end
53
+
54
+ def generate_meta_classes(path)
55
+ write_file(path, render_meta_classes)
56
+ end
57
+
58
+ def render_meta_classes
59
+ Opencdd::MetaClasses.reset!
60
+ metas = Opencdd::MetaClasses.all
61
+ type_map = Opencdd::MetaClasses::TYPE_BY_META_CLASS
62
+ code_map = Opencdd::MetaClasses::CODE_PROPERTY_IDS
63
+
64
+ entity_types = type_map.values.map(&:to_s).uniq.sort
65
+
66
+ out = []
67
+ out << "// AUTO-GENERATED by `rake generate_ts`. Do not edit by hand."
68
+ out << "// Source: Opencdd::MetaClasses in lib/opencdd/meta_class.rb"
69
+ out << ""
70
+ out << union_type("EntityType", entity_types)
71
+ out << ""
72
+ out << "export interface MetaClassEntry {"
73
+ out << " readonly irdi: string;"
74
+ out << " readonly name: string;"
75
+ out << " readonly entityType: EntityType;"
76
+ out << " readonly codePropertyId: string;"
77
+ out << " readonly allowedPropertyIds: readonly string[];"
78
+ out << "}"
79
+ out << ""
80
+ out << "export const REGISTRY: Readonly<Record<string, MetaClassEntry>> = {"
81
+ metas.each do |mc|
82
+ out << " \"#{mc.irdi}\": {"
83
+ out << " irdi: \"#{mc.irdi}\","
84
+ out << " name: \"#{mc.name}\","
85
+ out << " entityType: \"#{type_map[mc.irdi]}\","
86
+ out << " codePropertyId: \"#{code_map[mc.irdi]}\","
87
+ ids = mc.allowed_property_ids.map { |id| "\"#{id}\"" }.join(", ")
88
+ out << " allowedPropertyIds: [#{ids}],"
89
+ out << " },"
90
+ end
91
+ out << "};"
92
+ out << ""
93
+ metas.each { |mc| out << "export const #{mc.irdi} = \"#{mc.irdi}\";" }
94
+ out << ""
95
+ out << META_HELPERS
96
+ out.join("\n") + "\n"
97
+ end
98
+
99
+ def union_type(name, values)
100
+ body = values.map { |v| " | \"#{v}\"" }.join("\n")
101
+ "export type #{name} =\n#{body};"
102
+ end
103
+
104
+ def entry_literal(entry)
105
+ aliases = entry.aliases.map { |a| "\"#{a}\"" }.join(", ")
106
+ "{ id: \"#{entry.id}\", aliases: [#{aliases}], appliesTo: \"#{entry.applies_to}\", " \
107
+ "multilingual: #{entry.multilingual}, valueKind: \"#{entry.value_kind}\" }"
108
+ end
109
+
110
+ def alias_map_literal(registry)
111
+ pairs = []
112
+ registry.each_value do |e|
113
+ e.aliases.each { |a| pairs << " \"#{a}\": \"#{e.id}\"," }
114
+ end
115
+ "const ALIAS_MAP: Readonly<Record<string, string>> = {\n#{pairs.join("\n")}\n};"
116
+ end
117
+
118
+ def write_file(path, content)
119
+ FileUtils.mkdir_p(File.dirname(path))
120
+ File.write(path, content + "\n")
121
+ puts "Generated #{path}"
122
+ end
123
+
124
+ PROPERTY_HELPERS = <<~TS.freeze
125
+ export function entry(id: string): PropertyEntry | undefined {
126
+ return REGISTRY[id];
127
+ }
128
+
129
+ export function multilingual(id: string): boolean {
130
+ return REGISTRY[id]?.multilingual ?? false;
131
+ }
132
+
133
+ export function appliesTo(id: string): AppliesTo | undefined {
134
+ return REGISTRY[id]?.appliesTo;
135
+ }
136
+
137
+ export function valueKind(id: string): ValueKind | undefined {
138
+ return REGISTRY[id]?.valueKind;
139
+ }
140
+
141
+ export const ALL_IDS: readonly string[] = Object.keys(REGISTRY);
142
+
143
+ export function canonicalId(name: string): string | undefined {
144
+ if (name in REGISTRY) return name;
145
+ return ALIAS_MAP[name];
146
+ }
147
+
148
+ export function normalize(rawId: string | null | undefined): string | null {
149
+ if (!rawId) return null;
150
+ const s = rawId.trim();
151
+ if (!s) return null;
152
+ const match = s.match(/\\.([A-Za-z0-9-]+)$/);
153
+ if (!match) return canonicalId(s) ?? s;
154
+ const base = s.slice(0, s.length - match[0].length);
155
+ const lang = match[1];
156
+ const canonical = canonicalId(base) ?? base;
157
+ return `${canonical}.${lang}`;
158
+ }
159
+ TS
160
+
161
+ META_HELPERS = <<~TS.freeze
162
+ export function entry(irdi: string): MetaClassEntry | undefined {
163
+ return REGISTRY[irdi];
164
+ }
165
+
166
+ export function entityTypeFor(irdi: string): EntityType | undefined {
167
+ return REGISTRY[irdi]?.entityType;
168
+ }
169
+
170
+ export function codePropertyIdFor(irdi: string): string | undefined {
171
+ return REGISTRY[irdi]?.codePropertyId;
172
+ }
173
+
174
+ export function metaClassForType(type: EntityType): string | undefined {
175
+ for (const [irdi, e] of Object.entries(REGISTRY)) {
176
+ if (e.entityType === type) return irdi;
177
+ }
178
+ return undefined;
179
+ }
180
+
181
+ export const ALL_CODES: readonly string[] = Object.keys(REGISTRY);
182
+ TS
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Codegen
5
+ autoload :Ts, "opencdd/codegen/ts"
6
+ end
7
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Opencdd
6
+ class CompositionTree
7
+ Node = Struct.new(:entity, :children, keyword_init: true) do
8
+ include Enumerable
9
+
10
+ def each(&block)
11
+ return enum_for(:each) unless block
12
+ walk(&block)
13
+ end
14
+
15
+ def walk(&block)
16
+ return enum_for(:walk) unless block
17
+ yield self
18
+ children&.each { |c| c.walk(&block) }
19
+ self
20
+ end
21
+
22
+ def leaf?
23
+ children.nil? || children.empty?
24
+ end
25
+
26
+ def depth
27
+ return 1 if leaf?
28
+ 1 + children.map(&:depth).max
29
+ end
30
+
31
+ def size
32
+ walk.count
33
+ end
34
+
35
+ # Polymorphic dispatch via Entity#type — no `is_a?` chains.
36
+ # Adding a new entity type that should participate in trees
37
+ # means giving it the right `type` (already required); no
38
+ # edits to this file.
39
+ def classes
40
+ walk.filter_map { |n| n.entity if n.entity.type == :class }
41
+ end
42
+
43
+ def properties
44
+ walk.filter_map { |n| n.entity if n.entity.type == :property }
45
+ end
46
+ end
47
+
48
+ attr_reader :database
49
+
50
+ def initialize(database)
51
+ @database = database
52
+ end
53
+
54
+ def for(klass, max_depth: 10)
55
+ build_class_node(klass, Set.new, 0, max_depth)
56
+ end
57
+
58
+ private
59
+
60
+ def build_class_node(value, path, depth, max_depth)
61
+ klass = resolve_klass(value)
62
+ return nil unless klass
63
+
64
+ children =
65
+ if path.include?(klass.irdi) || depth >= max_depth
66
+ []
67
+ else
68
+ build_property_children(klass, path + [klass.irdi], depth + 1, max_depth)
69
+ end
70
+
71
+ Node.new(entity: klass, children: children)
72
+ end
73
+
74
+ def build_property_children(klass, path, depth, max_depth)
75
+ result = @database.effective_properties.for(klass)
76
+ result.map do |prop|
77
+ build_property_node(prop, path, depth, max_depth)
78
+ end
79
+ end
80
+
81
+ def build_property_node(prop, path, depth, max_depth)
82
+ sub = []
83
+ if depth < max_depth && prop.type == :property
84
+ class_reference_target(prop) do |target|
85
+ node = build_class_node(target, path, depth, max_depth)
86
+ sub << node if node
87
+ end
88
+ definition_class_subclasses(prop) do |target|
89
+ node = build_class_node(target, path, depth, max_depth)
90
+ sub << node if node
91
+ end
92
+ end
93
+ Node.new(entity: prop, children: sub)
94
+ end
95
+
96
+ def class_reference_target(prop)
97
+ return unless prop.class_reference?
98
+ dt = prop.parsed_data_type
99
+ return unless dt&.class_reference?
100
+ target = @database.resolve_reference(dt.class_identifier)
101
+ yield target if target&.type == :class
102
+ end
103
+
104
+ def definition_class_subclasses(prop)
105
+ dc_irdi = prop.definition_class_irdi
106
+ return unless dc_irdi
107
+ definition_class = @database.find(dc_irdi)
108
+ return unless definition_class&.type == :class && definition_class.categorical?
109
+ definition_class.children.each do |child|
110
+ yield child if child&.type == :class
111
+ end
112
+ end
113
+
114
+ def resolve_klass(value)
115
+ entity = value.is_a?(Opencdd::Entity) ? value : @database.resolve_reference(value)
116
+ entity&.type == :class ? entity : nil
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Opencdd
6
+ class Condition
7
+ SET_PATTERN = /\A\{(?<body>.*)\}\z/m.freeze
8
+ EXPRESSION_PATTERN = /\A
9
+ (?<left>[^=!<>]+?)
10
+ \s*
11
+ (?<op>==|!=)
12
+ \s*
13
+ (?<right>.+)
14
+ \z/x.freeze
15
+
16
+ attr_reader :left, :operator, :right
17
+
18
+ def initialize(left, operator, right)
19
+ @left = left.to_s.strip
20
+ unless %w[== !=].include?(operator.to_s)
21
+ raise ArgumentError, "unsupported condition operator: #{operator.inspect}"
22
+ end
23
+ @operator = operator.to_s
24
+ @right = right
25
+ end
26
+
27
+ def set?
28
+ @right.is_a?(Set)
29
+ end
30
+
31
+ def satisfied_by?(bindings)
32
+ hash = bindings.is_a?(Hash) ? bindings : bindings.to_h
33
+ actual = hash[@left] || hash[@left.to_sym]
34
+ return false if actual.nil?
35
+
36
+ if set?
37
+ contained = @right.any? { |r| matches?(actual, r) }
38
+ @operator == "==" ? contained : !contained
39
+ else
40
+ equal = matches?(actual, @right)
41
+ @operator == "==" ? equal : !equal
42
+ end
43
+ end
44
+
45
+ def to_s
46
+ rhs = if set?
47
+ "{ #{@right.to_a.join(", ")} }"
48
+ elsif quoted_literal?(@right)
49
+ "\"#{@right}\""
50
+ else
51
+ @right.to_s
52
+ end
53
+ "#{@left} #{@operator} #{rhs}"
54
+ end
55
+
56
+ alias_method :inspect, :to_s
57
+
58
+ def ==(other)
59
+ other.is_a?(Opencdd::Condition) &&
60
+ @left == other.left &&
61
+ @operator == other.operator &&
62
+ normalize_for_eq(@right) == normalize_for_eq(other.right)
63
+ end
64
+ alias_method :eql?, :==
65
+
66
+ def hash
67
+ [@left, @operator, normalize_for_eq(@right)].hash
68
+ end
69
+
70
+ class << self
71
+ def parse(raw)
72
+ return nil if raw.nil? || raw.to_s.strip.empty?
73
+ s = raw.to_s.strip
74
+ if s =~ EXPRESSION_PATTERN
75
+ left = Regexp.last_match(:left).strip
76
+ op = Regexp.last_match(:op)
77
+ raw_right = Regexp.last_match(:right).strip
78
+ new(left, op, parse_rhs(raw_right))
79
+ else
80
+ raise ArgumentError, "invalid condition expression: #{s.inspect}"
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def parse_rhs(rhs)
87
+ if rhs =~ SET_PATTERN
88
+ body = Regexp.last_match(:body)
89
+ elements = body.split(/[,\s]+/).map(&:strip).reject(&:empty?)
90
+ Set.new(elements)
91
+ else
92
+ unquote(rhs)
93
+ end
94
+ end
95
+
96
+ def unquote(s)
97
+ s = s.strip
98
+ return s[1..-2] if s.start_with?('"') && s.end_with?('"')
99
+ return s[1..-2] if s.start_with?("'") && s.end_with?("'")
100
+ s
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def matches?(actual, expected)
107
+ actual.to_s == expected.to_s
108
+ end
109
+
110
+ def quoted_literal?(value)
111
+ s = value.to_s
112
+ (s.start_with?('"') && s.end_with?('"')) ||
113
+ (s.start_with?("'") && s.end_with?("'"))
114
+ end
115
+
116
+ def normalize_for_eq(value)
117
+ value.is_a?(Set) ? value.to_a.sort : value
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class DataType
5
+ SIMPLE = %w[
6
+ STRING_TYPE TRANSLATABLE_STRING_TYPE REAL_TYPE INTEGER_TYPE INT_TYPE
7
+ BOOLEAN_TYPE DATE_TYPE DATE_TIME_TYPE DATETIME_TYPE TIME_TYPE
8
+ IRDI_TYPE ICID_STRING ICID_STRING_TYPE URL_TYPE MIME_TYPE FILE_TYPE
9
+ COMPLEX_TYPE
10
+ ].freeze
11
+
12
+ MEASURE = %w[REAL_MEASURE_TYPE INTEGER_MEASURE_TYPE INT_MEASURE_TYPE].freeze
13
+
14
+ PARAMETERIZED = %w[CLASS_REFERENCE ENUM_STRING_TYPE ENUM_REFERENCE_TYPE].freeze
15
+
16
+ attr_reader :kind
17
+
18
+ def initialize(kind)
19
+ @kind = kind.to_s
20
+ end
21
+
22
+ def simple? = SIMPLE.include?(@kind)
23
+ def measure? = MEASURE.include?(@kind)
24
+ # Base default — overridden by reference-carrying subclasses.
25
+ # Replaces the previous `is_a?` chain across ClassReference /
26
+ # EnumStringType / EnumReferenceType with per-subclass override.
27
+ def reference? = false
28
+
29
+ def parameterized? = PARAMETERIZED.include?(@kind)
30
+
31
+ def class_reference? = false
32
+ def enum? = false
33
+
34
+ def ==(other)
35
+ other.is_a?(Opencdd::DataType) && to_s == other.to_s
36
+ end
37
+ alias_method :eql?, :==
38
+
39
+ def hash
40
+ to_s.hash
41
+ end
42
+
43
+ def to_s
44
+ @kind
45
+ end
46
+
47
+ alias_method :inspect, :to_s
48
+
49
+ class RealMeasureType < DataType
50
+ def initialize
51
+ super("REAL_MEASURE_TYPE")
52
+ end
53
+ end
54
+
55
+ class IntegerMeasureType < DataType
56
+ def initialize
57
+ super("INTEGER_MEASURE_TYPE")
58
+ end
59
+ end
60
+
61
+ class ClassReference < DataType
62
+ attr_reader :class_identifier
63
+
64
+ def initialize(class_identifier)
65
+ super("CLASS_REFERENCE")
66
+ @class_identifier = class_identifier.to_s
67
+ end
68
+
69
+ def class_reference? = true
70
+ def reference? = true
71
+
72
+ def to_s
73
+ "CLASS_REFERENCE(#{@class_identifier})"
74
+ end
75
+ end
76
+
77
+ class EnumStringType < DataType
78
+ attr_reader :value_list_identifier
79
+
80
+ def initialize(value_list_identifier)
81
+ super("ENUM_STRING_TYPE")
82
+ @value_list_identifier = value_list_identifier.to_s
83
+ end
84
+
85
+ def enum? = true
86
+ def reference? = true
87
+
88
+ def to_s
89
+ "ENUM_STRING_TYPE(#{@value_list_identifier})"
90
+ end
91
+ end
92
+
93
+ class EnumReferenceType < DataType
94
+ attr_reader :value_list_identifier
95
+
96
+ def initialize(value_list_identifier)
97
+ super("ENUM_REFERENCE_TYPE")
98
+ @value_list_identifier = value_list_identifier.to_s
99
+ end
100
+
101
+ def enum? = true
102
+ def reference? = true
103
+
104
+ def to_s
105
+ "ENUM_REFERENCE_TYPE(#{@value_list_identifier})"
106
+ end
107
+ end
108
+
109
+ class << self
110
+ def parse(raw)
111
+ return nil if raw.nil? || raw.to_s.strip.empty?
112
+ s = raw.to_s.strip
113
+
114
+ if s =~ /\ACLASS_REFERENCE\s*\(\s*(.+?)\s*\)\z/
115
+ return ClassReference.new(Regexp.last_match(1))
116
+ end
117
+ if s =~ /\AENUM_STRING_TYPE\s*\(\s*(.+?)\s*\)\z/
118
+ return EnumStringType.new(Regexp.last_match(1))
119
+ end
120
+ if s =~ /\AENUM_REFERENCE_TYPE\s*\(\s*(.+?)\s*\)\z/
121
+ return EnumReferenceType.new(Regexp.last_match(1))
122
+ end
123
+
124
+ case s
125
+ when "REAL_MEASURE_TYPE", "REAL_MEASURE"
126
+ RealMeasureType.new
127
+ when "INTEGER_MEASURE_TYPE", "INT_MEASURE_TYPE", "INTEGER_MEASURE", "INT_MEASURE"
128
+ IntegerMeasureType.new
129
+ when *SIMPLE
130
+ new(s)
131
+ else
132
+ raise ArgumentError, "unknown data_type: #{s.inspect}"
133
+ end
134
+ end
135
+
136
+ def parse_or_string(raw)
137
+ parse(raw)
138
+ rescue ArgumentError
139
+ raw.to_s
140
+ end
141
+ end
142
+ end
143
+ end