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,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Opencdd
6
+ module Exporters
7
+ class Json < Opencdd::Visitor
8
+ attr_reader :nodes
9
+
10
+ def initialize
11
+ super
12
+ @nodes = []
13
+ end
14
+
15
+ def to_json(database, pretty: true)
16
+ reset!
17
+ @nodes.clear
18
+ visit_database(database)
19
+ pretty ? JSON.pretty_generate(@nodes) : JSON.generate(@nodes)
20
+ end
21
+
22
+ def visit_database(database)
23
+ @database = database
24
+ super
25
+ end
26
+
27
+ def visit_class(klass)
28
+ @nodes << class_node(klass)
29
+ super
30
+ end
31
+
32
+ def visit_property(prop)
33
+ @nodes << property_node(prop)
34
+ end
35
+
36
+ def visit_unit(unit)
37
+ @nodes << unit_node(unit)
38
+ end
39
+
40
+ def visit_value_list(vl)
41
+ @nodes << value_list_node(vl)
42
+ end
43
+
44
+ def visit_value_term(vt)
45
+ @nodes << value_term_node(vt)
46
+ end
47
+
48
+ def visit_relation(rel)
49
+ @nodes << relation_node(rel)
50
+ end
51
+
52
+ def visit_view_control(vc)
53
+ @nodes << view_control_node(vc)
54
+ end
55
+
56
+ private
57
+
58
+ # ─────────────────────────────────────────────────────────────
59
+ # Open/closed payload builders
60
+ #
61
+ # Each per-type _node method is a thin wrapper that adds `type:`
62
+ # and any type-specific computed fields (e.g. Property's value_list
63
+ # cross-link). The bulk of every entity's payload comes from
64
+ # `entity_payload`, which iterates the field DSL registry
65
+ # (Opencdd::Entity::FieldRegistry.fields_for) — adding a field to
66
+ # the model is a single `field` declaration; no edits here.
67
+ # ─────────────────────────────────────────────────────────────
68
+
69
+ def class_node(klass)
70
+ entity_payload(klass).merge(type: "class").compact
71
+ end
72
+
73
+ def property_node(prop)
74
+ entity_payload(prop).merge(
75
+ type: "property",
76
+ data_type: prop.parsed_data_type&.to_s,
77
+ value_list: value_list_irdi_of(prop),
78
+ ).compact
79
+ end
80
+
81
+ def unit_node(unit)
82
+ entity_payload(unit).merge(type: "unit").compact
83
+ end
84
+
85
+ def value_list_node(vl)
86
+ entity_payload(vl).merge(type: "value_list").compact
87
+ end
88
+
89
+ def value_term_node(vt)
90
+ entity_payload(vt).merge(type: "value_term").compact
91
+ end
92
+
93
+ def relation_node(rel)
94
+ entity_payload(rel).merge(type: "relation").compact
95
+ end
96
+
97
+ def view_control_node(vc)
98
+ entity_payload(vc).merge(type: "view_control").compact
99
+ end
100
+
101
+ # Iterates every declared field on the entity's class (walking
102
+ # the ancestor chain via FieldRegistry.fields_for). Each field's
103
+ # value is read by name (synthetic fields call their custom
104
+ # reader; pure fields go through FieldReader's typed coercion).
105
+ #
106
+ # Serialization is driven by the field's value_kind so the wire
107
+ # shape stays consistent: IRDI → string, set_of_refs → array of
108
+ # strings, synonym_pairs → array of {lang, name}, etc.
109
+ #
110
+ # Deduplicates by property_id: when two field names alias the
111
+ # same MDC_P### (e.g. source_document and source_document_of_definition),
112
+ # only the first-seen declaration is emitted. Declaration order
113
+ # is base-class first, so the canonical name lives on Entity.
114
+ def entity_payload(entity)
115
+ # Baseline identity fields. Always present (when value is non-nil).
116
+ # These mirror the existing wire shape and are emitted even
117
+ # though they're not DSL field declarations (the model's `irdi`
118
+ # accessor must return the Opencdd::IRDI object, not a string).
119
+ payload = {
120
+ irdi: entity.irdi&.to_s,
121
+ code: entity.code,
122
+ }.compact
123
+
124
+ seen_property_ids = Set.new
125
+
126
+ Opencdd::Entity::FieldRegistry.fields_for(entity.class).each do |field|
127
+ next if field.property_id && !seen_property_ids.add?(field.property_id)
128
+
129
+ value = entity.public_send(field.name)
130
+ next if value.nil?
131
+
132
+ serialized = serialize_value(value, field)
133
+ next if serialized.nil?
134
+
135
+ payload[field.wire_name] = serialized
136
+
137
+ # For multilingual fields, also emit a language map keyed
138
+ # as <wire_name>_ml. Scans entity.properties for every
139
+ # <property_id>.<lang> key. The browser can use this for
140
+ # a language switcher without parsing raw_properties.
141
+ if field.multilingual? && field.property_id
142
+ ml = build_language_map(entity, field)
143
+ payload["#{field.wire_name}_ml"] = ml if ml && !ml.empty?
144
+ end
145
+ end
146
+ payload
147
+ end
148
+
149
+ def serialize_value(value, field)
150
+ # raw_properties: emit the full hash as-is, no coercion.
151
+ # This preserves every key from the .xls, including
152
+ # multilingual variants and C### workbook-specific codes.
153
+ return value if field.name == :raw_properties && value.is_a?(Hash)
154
+
155
+ case field.value_kind
156
+ when :synonym_pairs
157
+ return nil if value.nil?
158
+ value.map { |lang, name| { lang: lang, name: name } }
159
+ when :irdi, :identifier_ref, :class_ref
160
+ return nil if value.nil?
161
+ value.to_s
162
+ when :set_of_refs
163
+ return nil if value.nil?
164
+ value.map { |irdi| irdi.to_s }
165
+ when :string_list
166
+ return nil if value.nil?
167
+ value
168
+ else
169
+ serialize_basic(value)
170
+ end
171
+ end
172
+
173
+ def serialize_basic(value)
174
+ case value
175
+ when Opencdd::IRDI then value.to_s
176
+ when Array
177
+ # Preserve empty arrays — callers use them as "no entries"
178
+ # signals (e.g. sub_class_selection: [] on a class with no
179
+ # composition children). The payload's final .compact only
180
+ # drops nils, not empties.
181
+ value.map { |v| serialize_basic(v) }
182
+ when Struct
183
+ hash = value.to_h.transform_values { |v| serialize_basic(v) }
184
+ hash.compact!
185
+ hash.empty? ? nil : hash
186
+ when Opencdd::Entity::VersionHistory
187
+ return nil if value.empty?
188
+ value.entries.map { |e| serialize_basic(e) }
189
+ when String, Integer, Float, TrueClass, FalseClass, NilClass, Symbol
190
+ value
191
+ else
192
+ # Value objects (Opencdd::ClassType, Opencdd::Condition,
193
+ # Opencdd::ValueFormat, Opencdd::PropertyDataTypeElement, etc.)
194
+ # serialize via their canonical to_s. Keeps the wire shape
195
+ # stable without per-type branching here.
196
+ value.to_s
197
+ end
198
+ end
199
+
200
+ # Scans +entity.properties+ for +<property_id>.<lang>+ keys
201
+ # and builds a +{ lang => value }+ hash. Used for multilingual
202
+ # fields where the exporter emits a +<wire_name>_ml+ map
203
+ # alongside the source-language string.
204
+ def build_language_map(entity, field)
205
+ return nil unless field.property_id
206
+ prefix = "#{field.property_id}."
207
+ map = {}
208
+ entity.properties.each do |key, val|
209
+ next unless key.start_with?(prefix)
210
+ lang = key.sub(prefix, "")
211
+ next unless val && !val.to_s.strip.empty?
212
+ next unless lang =~ /\A[a-z]{2}(-[a-z0-9]+)?\z/i
213
+ map[lang] = val
214
+ end
215
+ bare = entity.properties[field.property_id]
216
+ if bare && map.empty?
217
+ map["en"] = bare
218
+ end
219
+ map
220
+ end
221
+
222
+
223
+ # when a property's data type is an enumeration, its values come
224
+ # from a named value list linked via a predication relation.
225
+ # The Database resolves this at finalize time; we emit the IRDI
226
+ # so the browser can render the cross-link without re-walking
227
+ # the relations.
228
+ def value_list_irdi_of(prop)
229
+ return nil unless @database
230
+ vl = @database.value_list_of(prop)
231
+ vl&.irdi&.to_s
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Exporters
5
+ class Mermaid < Opencdd::Visitor
6
+ attr_reader :lines
7
+
8
+ def initialize
9
+ super
10
+ @lines = ["classDiagram"]
11
+ end
12
+
13
+ def to_diagram(database)
14
+ reset!
15
+ @lines = ["classDiagram"]
16
+ visit_classes(database)
17
+ @lines.uniq.join("\n")
18
+ end
19
+
20
+ def visit_class(klass)
21
+ emit_class_block(klass)
22
+ parent_ref = klass.parent_irdi || klass.superclass_irdi
23
+ if parent_ref
24
+ parent = klass.database&.find(parent_ref)
25
+ if parent
26
+ safe = mermaid_id(parent)
27
+ child = mermaid_id(klass)
28
+ @lines << " #{safe} <|-- #{child}"
29
+ end
30
+ end
31
+ klass.is_case_of_irdis.each do |ref|
32
+ target = klass.database&.find(ref)
33
+ next unless target
34
+ @lines << " #{mermaid_id(target)} <.. #{mermaid_id(klass)} : is_case_of"
35
+ end
36
+ super
37
+ end
38
+
39
+ private
40
+
41
+ def emit_class_block(klass)
42
+ id = mermaid_id(klass)
43
+ @lines << " class #{id} {"
44
+ @lines << " <<#{klass.class_type || 'ITEM_CLASS'}>>"
45
+ @lines << " +code #{klass.code}"
46
+ @lines << " }"
47
+ note = klass.preferred_name
48
+ @lines << " #{id} : #{quote_label(note)}" if note && !note.empty?
49
+ end
50
+
51
+ def mermaid_id(klass)
52
+ code = klass.code&.to_s
53
+ return "Class_#{klass.irdi.to_s.gsub(/[^A-Za-z0-9]/, "_")}" if code.nil? || code.empty?
54
+ code.gsub(/[^A-Za-z0-9_]/, "_")
55
+ end
56
+
57
+ def quote_label(text)
58
+ text.to_s.include?(":") ? "\"#{text}\"" : text.to_s
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Opencdd
6
+ module Exporters
7
+ class Yaml < Json
8
+ def to_yaml(database)
9
+ reset!
10
+ @nodes.clear
11
+ visit_database(database)
12
+ @nodes.to_yaml
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Exporters
5
+ autoload :Json, "opencdd/exporters/json"
6
+ autoload :Yaml, "opencdd/exporters/yaml"
7
+ autoload :Mermaid, "opencdd/exporters/mermaid"
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Opencdd
6
+ module GUID
7
+ PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i.freeze
8
+
9
+ def self.generate
10
+ SecureRandom.uuid
11
+ end
12
+
13
+ def self.valid?(value)
14
+ return false unless value.is_a?(String)
15
+ PATTERN.match?(value)
16
+ end
17
+
18
+ def self.set_on(entity)
19
+ guid = generate
20
+ # Use the field-access seam so writes go through canonical-id
21
+ # resolution. Keeps GUID writes consistent with reads and
22
+ # avoids bypassing the Field DSL.
23
+ entity.write_property!(Opencdd::PropertyIds::MDC_P066, guid)
24
+ guid
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class InstanceRule
5
+ Group = Struct.new(:name, :values_by_property, keyword_init: true)
6
+ Exception = Struct.new(:group_name, :line_id, keyword_init: true)
7
+
8
+ attr_reader :klass, :groups, :exceptions
9
+
10
+ def initialize(klass:, groups:, exceptions: [])
11
+ @klass = klass
12
+ @groups = groups
13
+ @exceptions = exceptions
14
+ freeze
15
+ end
16
+
17
+ def expand
18
+ return [] if @groups.empty?
19
+
20
+ per_group = @groups.map { |g| expand_group(g) }
21
+ return [] if per_group.any?(&:empty?)
22
+
23
+ product = per_group.reduce do |acc, rows|
24
+ acc.product(rows).map { |a, b| a.merge(b) }
25
+ end
26
+
27
+ product.reject { |row| exception_match?(row) }
28
+ .map { |row| strip_internal(row) }
29
+ end
30
+
31
+ private
32
+
33
+ def expand_group(group)
34
+ values = group.values_by_property.transform_values do |list|
35
+ Array(list)
36
+ end
37
+ return [] if values.empty?
38
+
39
+ property_ids = values.keys
40
+ length = values.values.map(&:length).max || 0
41
+ length.times.map do |i|
42
+ row = {}
43
+ property_ids.each do |pid|
44
+ v = values[pid][i]
45
+ row[pid] = v unless v.nil?
46
+ end
47
+ row["__line_id__"] = "LINE#{i + 1}"
48
+ row["__group_name__"] = group.name
49
+ row
50
+ end
51
+ end
52
+
53
+ def exception_match?(row)
54
+ return false if @exceptions.empty?
55
+ group_name = row["__group_name__"]
56
+ line_id = row["__line_id__"]
57
+ @exceptions.any? do |ex|
58
+ ex.group_name == group_name && ex.line_id == line_id
59
+ end
60
+ end
61
+
62
+ def strip_internal(row)
63
+ out = row.dup
64
+ out.delete("__line_id__")
65
+ out.delete("__group_name__")
66
+ out.delete("__group_indices__")
67
+ out
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class IRDI
5
+ FULL_REGEX = %r{
6
+ \A
7
+ (?<registrant>[^/#\s]+)
8
+ /
9
+ (?<semantic>[^/#\s]*)
10
+ ///
11
+ (?<scheme>[^/#\s]+)
12
+ \#
13
+ (?<code>[^#\s]+)
14
+ (?:\#\#(?<smver>\d+))?
15
+ \z
16
+ }x
17
+
18
+ SHORT_REGEX = /\A[^#\/\s]+\z/
19
+
20
+ TREE_REGEX = %r{
21
+ \A
22
+ (?<registrant>[^-]+)
23
+ -
24
+ (?<semantic>[^-]*)
25
+ ---
26
+ (?<scheme>[^-]+)
27
+ %23
28
+ (?<code>[^%\s]+)
29
+ \z
30
+ }x
31
+
32
+ attr_reader :registrant, :semantic, :scheme, :code, :version
33
+
34
+ def self.parse(value)
35
+ return nil if value.nil?
36
+ s = value.to_s.strip
37
+ return nil if s.empty?
38
+
39
+ if (m = FULL_REGEX.match(s)) then from_match(m)
40
+ elsif (m = TREE_REGEX.match(s)) then from_match(m)
41
+ elsif SHORT_REGEX.match?(s) then from_short(s)
42
+ elsif s.include?("#") && (m = FULL_REGEX.match(s.gsub(/\s+/, ""))) then from_match(m)
43
+ else from_short(s)
44
+ end
45
+ end
46
+
47
+ def self.from_short(code)
48
+ new(registrant: nil, semantic: nil, scheme: nil, code: code.to_s)
49
+ end
50
+
51
+ def self.from_match(m)
52
+ caps = m.named_captures.transform_keys(&:to_sym)
53
+ new(
54
+ registrant: caps[:registrant],
55
+ semantic: caps[:semantic],
56
+ scheme: caps[:scheme],
57
+ code: caps[:code],
58
+ version: caps[:smver],
59
+ )
60
+ end
61
+
62
+ def initialize(registrant:, semantic:, scheme:, code:, version: nil)
63
+ @registrant = registrant&.to_s
64
+ @semantic = semantic&.to_s
65
+ @scheme = scheme&.to_s
66
+ @code = code.to_s
67
+ @version = version&.to_s
68
+ freeze
69
+ end
70
+
71
+ def full?
72
+ !@registrant.nil?
73
+ end
74
+
75
+ def short?
76
+ @registrant.nil?
77
+ end
78
+
79
+ alias_method :short, :code
80
+
81
+ def sheetmap_version
82
+ @version
83
+ end
84
+
85
+ def to_s
86
+ return @code if short?
87
+ "#{@registrant}/#{@semantic}///#{@scheme}##{@code}"
88
+ end
89
+
90
+ alias_method :to_str, :to_s
91
+
92
+ def to_tree_path
93
+ return @code if short?
94
+ "#{@registrant}-#{@semantic}---#{@scheme}%23#{@code}"
95
+ end
96
+
97
+ def with_code(new_code)
98
+ self.class.new(
99
+ registrant: @registrant,
100
+ semantic: @semantic,
101
+ scheme: @scheme,
102
+ code: new_code.to_s,
103
+ version: @version,
104
+ )
105
+ end
106
+
107
+ def eql?(other)
108
+ other.is_a?(Opencdd::IRDI) && to_s == other.to_s
109
+ end
110
+
111
+ def hash
112
+ to_s.hash
113
+ end
114
+
115
+ def ==(other)
116
+ eql?(other)
117
+ end
118
+
119
+ def inspect
120
+ "#<Opencdd::IRDI #{to_s.inspect}>"
121
+ end
122
+
123
+ def self.coerce(value)
124
+ return value if value.is_a?(Opencdd::IRDI)
125
+ parse(value)
126
+ end
127
+ end
128
+ end