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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class FormatRule < Rule
6
+ TOKEN_PATTERN = %r{
7
+ \A
8
+ (?<type>NR1|NR2|NR3|M|B|Date|DT|Bool)
9
+ (?:\s+(?<sign>S|U))?
10
+ (?:\.\.(?<width>\d+))?
11
+ (?:\.(?<decimals>\d+))?
12
+ \z
13
+ }x.freeze
14
+
15
+ def id
16
+ "R05"
17
+ end
18
+
19
+ def applies?(context)
20
+ !context.value_format.nil? && !context.value_format.to_s.strip.empty?
21
+ end
22
+
23
+ def call(value, context)
24
+ return true if value.nil? || value.to_s.strip.empty?
25
+ match = TOKEN_PATTERN.match(context.value_format.to_s.strip)
26
+ return false unless match
27
+ type_check(value, match)
28
+ end
29
+
30
+ def message(value, context)
31
+ "R05: #{value.inspect} does not match value format #{context.value_format}"
32
+ end
33
+
34
+ private
35
+
36
+ def type_check(value, match)
37
+ width = match[:width]
38
+ case match[:type]
39
+ when "NR1" then !!(Integer(value.to_s) rescue nil)
40
+ when "NR2", "NR3" then !!(Float(value.to_s) rescue nil)
41
+ when "M" then value.to_s.length <= (width || "255").to_i
42
+ when "B" then value.to_s.match?(/\A[01]+\z/) && value.to_s.length <= (width || "8").to_i
43
+ when "Date" then !!(Date.iso8601(value.to_s) rescue nil)
44
+ when "DT" then !!(Time.iso8601(value.to_s) rescue nil)
45
+ when "Bool" then %w[true false].include?(value.to_s.downcase)
46
+ else false
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ require "date"
54
+ require "time"
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class HierarchyRule < Rule
6
+ def id
7
+ "R14"
8
+ end
9
+
10
+ def applies?(_context)
11
+ false
12
+ end
13
+
14
+ def call(_value, _context)
15
+ true
16
+ end
17
+
18
+ def self.class_hierarchy_acyclic?(database)
19
+ database.classes.each do |klass|
20
+ return false if cycle_from?(klass, database)
21
+ end
22
+ true
23
+ end
24
+
25
+ def self.composition_hierarchy_acyclic?(database)
26
+ visited = {}
27
+ database.classes.each do |klass|
28
+ return false if composition_cycle?(klass, database, visited, [])
29
+ end
30
+ true
31
+ end
32
+
33
+ class << self
34
+ private
35
+
36
+ def cycle_from?(klass, database, seen = [])
37
+ return false if klass.nil?
38
+ code = klass.irdi&.code
39
+ return true if seen.include?(code)
40
+ return false if code.nil?
41
+ super_irdi = klass.superclass_irdi
42
+ return false if super_irdi.nil?
43
+ parent = database.find(super_irdi) || database.find_by_code(super_irdi.code)
44
+ cycle_from?(parent, database, seen + [code])
45
+ end
46
+
47
+ def composition_cycle?(klass, database, visited, path)
48
+ return false if klass.nil?
49
+ code = klass.irdi&.code
50
+ return true if path.include?(code)
51
+ return false if visited[code]
52
+ visited[code] = true
53
+ props = klass.is_a?(Opencdd::Klass) && database ? klass.effective_properties(database) : []
54
+ props.any? do |prop|
55
+ dc = prop.is_a?(Opencdd::Property) ? prop.definition_class_irdi : nil
56
+ next false if dc.nil?
57
+ target = database.find(dc) || database.find_by_code(dc.code)
58
+ next false if target.nil?
59
+ composition_cycle?(target, database, visited, path + [code])
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class IrdiRule < Rule
6
+ FULL_PATTERN = %r{
7
+ \A
8
+ [^/#\s]+
9
+ /
10
+ [^/#\s]*
11
+ ///
12
+ [^/#\s]+
13
+ \#
14
+ [^#\s]+
15
+ (?:\#\#\d+)?
16
+ (?:\#\#\#[^#]+)?
17
+ \z
18
+ }x.freeze
19
+
20
+ SHORT_PATTERN = /\A[A-Za-z0-9_]+\z/.freeze
21
+
22
+ def id
23
+ "R01"
24
+ end
25
+
26
+ def applies?(context)
27
+ code_column?(context) || reference_column?(context)
28
+ end
29
+
30
+ def call(value, context)
31
+ return true if value.nil? || value.to_s.strip.empty?
32
+ s = value.to_s.strip
33
+ if reference_column?(context)
34
+ return true unless Opencdd::IRDI.parse(s).nil?
35
+ return false
36
+ end
37
+ FULL_PATTERN.match?(s) || SHORT_PATTERN.match?(s) ? true : false
38
+ end
39
+
40
+ def message(value, _context)
41
+ "R01: malformed IRDI/ICID #{value.inspect}"
42
+ end
43
+
44
+ private
45
+
46
+ def code_column?(context)
47
+ context.column_iri&.start_with?(Opencdd::PropertyIds::MDC_P001) ||
48
+ context.column_iri == Opencdd::PropertyIds::EXT_P001
49
+ end
50
+
51
+ def reference_column?(context)
52
+ %i[identifier_ref set_of_refs class_ref].include?(context.value_kind)
53
+ end
54
+ end
55
+ end
56
+ end
57
+
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class MandatoryRule < Rule
6
+ def id
7
+ "R07"
8
+ end
9
+
10
+ def applies?(context)
11
+ context.requirement_mandatory?
12
+ end
13
+
14
+ def call(value, _context)
15
+ return false if value.nil?
16
+ return false if value.to_s.strip.empty?
17
+ true
18
+ end
19
+
20
+ def message(_value, context)
21
+ "R07: mandatory column #{context.column_iri} is empty"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class PatternRule < Rule
6
+ def id
7
+ "R06"
8
+ end
9
+
10
+ def applies?(context)
11
+ !context.pattern.nil? && !context.pattern.to_s.strip.empty?
12
+ end
13
+
14
+ def call(value, context)
15
+ return true if value.nil? || value.to_s.strip.empty?
16
+ Regexp.new(context.pattern.to_s).match?(value.to_s)
17
+ rescue RegexpError
18
+ false
19
+ end
20
+
21
+ def message(value, context)
22
+ "R06: #{value.inspect} does not match pattern #{context.pattern.inspect}"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class ReferenceRule < Rule
6
+ def id
7
+ "R08"
8
+ end
9
+
10
+ def applies?(context)
11
+ %i[identifier_ref set_of_refs class_ref].include?(context.value_kind)
12
+ end
13
+
14
+ def call(value, context)
15
+ return true if value.nil? || value.to_s.strip.empty?
16
+ return false unless context.database
17
+ refs(value).all? do |ref|
18
+ parsed = Opencdd::IRDI.parse(ref)
19
+ context.database.find(parsed) || context.database.find_by_code(ref.to_s)
20
+ end
21
+ end
22
+
23
+ def message(value, _context)
24
+ "R08: reference #{value.inspect} does not resolve in this database"
25
+ end
26
+
27
+ private
28
+
29
+ def refs(value)
30
+ # SSOT: same brace/paren/comma handling as every other
31
+ # collection reader in the gem.
32
+ Opencdd::StructuredValues.unwrap_and_split(value)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ # Deep base class for all validator rules. Owns shared guards
6
+ # that were previously copy-pasted across 12 rule subclasses:
7
+ #
8
+ # - blank-value guard (skip empty cells unless +skip_blank?+
9
+ # is overridden to false)
10
+ # - code-column predicate
11
+ # - reference-kind predicate
12
+ #
13
+ # Subclasses implement +value_passes?+ (the narrow check) and
14
+ # optionally override +applies?+ and +message+.
15
+ class Rule
16
+ REFERENCE_VALUE_KINDS = %i[identifier_ref set_of_refs class_ref].freeze
17
+
18
+ def id
19
+ raise NotImplementedError
20
+ end
21
+
22
+ # Default: applies to every entity. Override to narrow.
23
+ def applies?(context)
24
+ true
25
+ end
26
+
27
+ # Template method: skip blank values (unless the subclass
28
+ # overrides +skip_blank?+), then delegate to +value_passes?+.
29
+ def call(value, context)
30
+ return true if skip_blank? && blank?(value)
31
+ value_passes?(value, context)
32
+ end
33
+
34
+ def message(value, _context)
35
+ "#{id}: value #{value.inspect} is invalid"
36
+ end
37
+
38
+ # ── Hooks for subclasses ────────────────────────────────────
39
+ def value_passes?(value, context)
40
+ raise NotImplementedError
41
+ end
42
+
43
+ # Most rules skip empty values — MandatoryRule overrides to
44
+ # +false+ so it can flag missing mandatory cells.
45
+ def skip_blank?
46
+ true
47
+ end
48
+
49
+ # ── Shared predicates ───────────────────────────────────────
50
+ def blank?(value)
51
+ value.nil? || value.to_s.strip.empty?
52
+ end
53
+
54
+ def code_column?(context)
55
+ return false unless context.entity && context.entity.meta_class_irdi
56
+ code_pid = Opencdd::MetaClasses.code_property_id_for(context.entity.meta_class_irdi.code)
57
+ context.column_iri == code_pid
58
+ end
59
+
60
+ def reference_kind?(context)
61
+ REFERENCE_VALUE_KINDS.include?(context.value_kind)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class Runner
6
+ RuleContext = Struct.new(
7
+ :database, :entity, :column_iri, :value_kind,
8
+ :data_type, :value_format, :pattern, :requirement,
9
+ :enum_terms_resolver,
10
+ keyword_init: true,
11
+ ) do
12
+ def requirement_mandatory?
13
+ requirement.to_s.upcase == "MAND" || requirement.to_s.upcase == "KEY"
14
+ end
15
+
16
+ def enum_terms_for(data_type)
17
+ return [] unless enum_terms_resolver
18
+ enum_terms_resolver.call(data_type)
19
+ end
20
+ end
21
+
22
+ RULES = [
23
+ IrdiRule.new,
24
+ UniquenessRule.new,
25
+ MandatoryRule.new,
26
+ TypeRule.new,
27
+ EnumRule.new,
28
+ FormatRule.new,
29
+ PatternRule.new,
30
+ ReferenceRule.new,
31
+ SetRule.new,
32
+ SynonymRule.new,
33
+ ConditionRule.new,
34
+ DataTypeRule.new,
35
+ ClassReferenceRule.new,
36
+ ].freeze
37
+
38
+ def self.run(database, enum_terms_resolver: nil)
39
+ errors = []
40
+ database.entities.each do |entity|
41
+ validate_entity(entity, database, errors, enum_terms_resolver)
42
+ end
43
+ unless HierarchyRule.class_hierarchy_acyclic?(database)
44
+ errors << ValidationError.new(
45
+ sheet: nil, row: nil, column: nil, rule: "R14",
46
+ message: "R14: class hierarchy contains a cycle",
47
+ )
48
+ end
49
+ errors
50
+ end
51
+
52
+ def self.validate_entity(entity, database, errors, enum_terms_resolver)
53
+ meta = Opencdd::MetaClasses.for(entity.meta_class_irdi&.code)
54
+ schema = entity.schema
55
+ entity.each_property do |column_iri, value|
56
+ next if column_iri == "__row_index__"
57
+ context = build_context(
58
+ database: database, entity: entity, column_iri: column_iri,
59
+ value: value, schema: schema, meta: meta,
60
+ enum_terms_resolver: enum_terms_resolver,
61
+ )
62
+ RULES.each do |rule|
63
+ next unless rule.applies?(context)
64
+ next if rule.call(value, context)
65
+ errors << ValidationError.new(
66
+ sheet: entity.meta_class_irdi&.code,
67
+ row: entity.irdi&.to_s,
68
+ column: column_iri,
69
+ rule: rule.id,
70
+ message: rule.message(value, context),
71
+ )
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.build_context(database:, entity:, column_iri:, value:, schema:, meta:, enum_terms_resolver:)
77
+ base = column_iri.to_s.split(".").first
78
+ entry = Opencdd::PropertyIds::REGISTRY[base]
79
+ kind = entry&.value_kind
80
+ column_schema = schema&.column_for(base)
81
+ RuleContext.new(
82
+ database: database,
83
+ entity: entity,
84
+ column_iri: base,
85
+ value_kind: kind,
86
+ data_type: derived_data_type(entity, base, column_schema),
87
+ value_format: column_schema&.value_format,
88
+ pattern: column_schema&.pattern,
89
+ requirement: column_schema&.requirement,
90
+ enum_terms_resolver: enum_terms_resolver,
91
+ )
92
+ end
93
+
94
+ def self.derived_data_type(entity, base, column_schema)
95
+ return column_schema&.data_type if column_schema&.data_type
96
+ return entity.data_type if entity.is_a?(Opencdd::Property) && base == Opencdd::PropertyIds::MDC_P022
97
+ nil
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class SetRule < Rule
6
+ def id
7
+ "R09"
8
+ end
9
+
10
+ def applies?(context)
11
+ context.value_kind == :set_of_refs
12
+ end
13
+
14
+ def call(value, _context)
15
+ return true if value.nil? || value.to_s.strip.empty?
16
+ s = value.to_s.strip
17
+ return false unless s.start_with?("{") && s.end_with?("}")
18
+ body = s[1..-2]
19
+ balanced?(body)
20
+ end
21
+
22
+ def message(value, _context)
23
+ "R09: set literal #{value.inspect} is not well-formed"
24
+ end
25
+
26
+ private
27
+
28
+ def balanced?(body)
29
+ depth = 0
30
+ body.each_char do |ch|
31
+ case ch
32
+ when "(" then depth += 1
33
+ when ")" then depth -= 1
34
+ end
35
+ return false if depth.negative?
36
+ end
37
+ depth.zero?
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class SynonymRule < Rule
6
+ def id
7
+ "R10"
8
+ end
9
+
10
+ def applies?(context)
11
+ context.column_iri == Opencdd::PropertyIds::MDC_P004_2 ||
12
+ context.column_iri == Opencdd::PropertyIds::MDC_P007
13
+ end
14
+
15
+ def call(value, _context)
16
+ return true if value.nil? || value.to_s.strip.empty?
17
+ s = value.to_s.strip
18
+ return false unless s.start_with?("{") && s.end_with?("}")
19
+ body = s[1..-2].strip
20
+ return true if body.empty?
21
+ tuples = body.split(/\)\s*,\s*/).map(&:strip)
22
+ tuples.all? { |t| t.start_with?("(") }
23
+ end
24
+
25
+ def message(value, _context)
26
+ "R10: synonymous-name literal #{value.inspect} is not well-formed"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class TypeRule < Rule
6
+ SIMPLE_PREDICATES = {
7
+ "BOOLEAN_TYPE" => ->(v) { %w[true false].include?(v.to_s.downcase.strip) },
8
+ "STRING_TYPE" => ->(v) { v.is_a?(String) || !v.nil? },
9
+ "TRANSLATABLE_STRING_TYPE" => ->(v) { v.is_a?(String) || structured_pairs?(v) },
10
+ "IRDI_TYPE" => ->(v) { !Opencdd::IRDI.parse(v.to_s).nil? },
11
+ "IRDI_STRING_TYPE" => ->(v) { !Opencdd::IRDI.parse(v.to_s).nil? },
12
+ "ICID_STRING" => ->(v) { !Opencdd::IRDI.parse(v.to_s).nil? },
13
+ "ICID_STRING_TYPE" => ->(v) { !Opencdd::IRDI.parse(v.to_s).nil? },
14
+ "DATE_TYPE" => ->(v) { date?(v) },
15
+ "DATE_TIME_TYPE" => ->(v) { date_time?(v) },
16
+ "DATETIME_TYPE" => ->(v) { date_time?(v) },
17
+ "REAL_TYPE" => ->(v) { real?(v) },
18
+ "INTEGER_TYPE" => ->(v) { integer?(v) },
19
+ "INT_TYPE" => ->(v) { integer?(v) },
20
+ "RATIONAL_TYPE" => ->(v) { rational?(v) },
21
+ }.freeze
22
+
23
+ def id
24
+ "R03"
25
+ end
26
+
27
+ def applies?(context)
28
+ !context.data_type.nil?
29
+ end
30
+
31
+ def call(value, context)
32
+ return true if value.nil? || value.to_s.strip.empty?
33
+ token = context.data_type.to_s
34
+ return enum_member?(value, context) if token.start_with?("ENUM_")
35
+ predicate = SIMPLE_PREDICATES[token]
36
+ return true if predicate.nil?
37
+ predicate.call(value) ? true : false
38
+ end
39
+
40
+ def message(value, context)
41
+ "R03: value #{value.inspect} does not satisfy data type #{context.data_type}"
42
+ end
43
+
44
+ def self.structured_pairs?(value)
45
+ s = value.to_s.strip
46
+ s.start_with?("{") || s.start_with?("(")
47
+ end
48
+
49
+ def self.date?(value)
50
+ !!begin
51
+ Date.iso8601(value.to_s)
52
+ rescue ArgumentError, TypeError
53
+ nil
54
+ end
55
+ end
56
+
57
+ def self.date_time?(value)
58
+ !!begin
59
+ Time.iso8601(value.to_s)
60
+ rescue ArgumentError, TypeError
61
+ nil
62
+ end
63
+ end
64
+
65
+ def self.real?(value)
66
+ !!begin
67
+ Float(value.to_s)
68
+ rescue ArgumentError, TypeError
69
+ nil
70
+ end
71
+ end
72
+
73
+ def self.integer?(value)
74
+ !!begin
75
+ Integer(value.to_s)
76
+ rescue ArgumentError, TypeError
77
+ nil
78
+ end
79
+ end
80
+
81
+ def self.rational?(value)
82
+ !!begin
83
+ Rational(value.to_s)
84
+ rescue ArgumentError, TypeError, ZeroDivisionError
85
+ nil
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def enum_member?(value, context)
92
+ return true unless context.database
93
+ terms = context.enum_terms_for(context.data_type)
94
+ return true if terms.empty?
95
+ terms.any? { |t| t.to_s == value.to_s }
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ require "time"
102
+ require "date"
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Validator
5
+ class UniquenessRule < Rule
6
+ def id
7
+ "R02"
8
+ end
9
+
10
+ def applies?(context)
11
+ code_column?(context)
12
+ end
13
+
14
+ def call(value, context)
15
+ return true if value.nil? || value.to_s.strip.empty?
16
+ peers = context.database.entities.filter_map { |e| e.irdi&.code if e.type == context.entity.type }
17
+ peers.tally[value.to_s] == 1
18
+ end
19
+
20
+ def message(value, _context)
21
+ "R02: code #{value.inspect} is not unique within its sheet"
22
+ end
23
+
24
+ private
25
+
26
+ def code_column?(context)
27
+ context.column_iri&.start_with?(Opencdd::PropertyIds::MDC_P001) ||
28
+ context.column_iri == Opencdd::PropertyIds::EXT_P001
29
+ end
30
+ end
31
+ end
32
+ end