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.
- checksums.yaml +7 -0
- data/CLAUDE.md +486 -0
- data/README.md +304 -0
- data/bin/lint-no-raw-mdc +41 -0
- data/lib/cdd.rb +11 -0
- data/lib/opencdd/alias_table.rb +52 -0
- data/lib/opencdd/cddal/ast.rb +151 -0
- data/lib/opencdd/cddal/builder.rb +374 -0
- data/lib/opencdd/cddal/fetcher/in_memory.rb +32 -0
- data/lib/opencdd/cddal/fetcher/net_http.rb +84 -0
- data/lib/opencdd/cddal/fetcher.rb +18 -0
- data/lib/opencdd/cddal/generated_parser.rb +805 -0
- data/lib/opencdd/cddal/lexer.rb +193 -0
- data/lib/opencdd/cddal/parser.rb +19 -0
- data/lib/opencdd/cddal/resolver.rb +100 -0
- data/lib/opencdd/cddal/serializer.rb +210 -0
- data/lib/opencdd/cddal/value_serializer.rb +60 -0
- data/lib/opencdd/cddal.rb +63 -0
- data/lib/opencdd/class_tree.rb +80 -0
- data/lib/opencdd/class_type.rb +33 -0
- data/lib/opencdd/codegen/ts.rb +185 -0
- data/lib/opencdd/codegen.rb +7 -0
- data/lib/opencdd/composition_tree.rb +119 -0
- data/lib/opencdd/condition.rb +120 -0
- data/lib/opencdd/data_type.rb +143 -0
- data/lib/opencdd/database.rb +719 -0
- data/lib/opencdd/effective_properties.rb +119 -0
- data/lib/opencdd/entity/field_reader.rb +141 -0
- data/lib/opencdd/entity/field_registry.rb +99 -0
- data/lib/opencdd/entity/version_history.rb +62 -0
- data/lib/opencdd/entity.rb +255 -0
- data/lib/opencdd/exporters/json.rb +235 -0
- data/lib/opencdd/exporters/mermaid.rb +62 -0
- data/lib/opencdd/exporters/yaml.rb +16 -0
- data/lib/opencdd/exporters.rb +9 -0
- data/lib/opencdd/guid.rb +27 -0
- data/lib/opencdd/instance_rule.rb +70 -0
- data/lib/opencdd/irdi.rb +128 -0
- data/lib/opencdd/klass.rb +230 -0
- data/lib/opencdd/languages.rb +70 -0
- data/lib/opencdd/meta_class.rb +274 -0
- data/lib/opencdd/model/entity_store.rb +85 -0
- data/lib/opencdd/model/yaml_database.rb +49 -0
- data/lib/opencdd/model/yaml_entity.rb +196 -0
- data/lib/opencdd/model.rb +13 -0
- data/lib/opencdd/parcel/csv_reader.rb +35 -0
- data/lib/opencdd/parcel/csv_writer.rb +114 -0
- data/lib/opencdd/parcel/entity_manifest.rb +119 -0
- data/lib/opencdd/parcel/flat_dir_reader.rb +134 -0
- data/lib/opencdd/parcel/layout_detector.rb +115 -0
- data/lib/opencdd/parcel/metadata.rb +112 -0
- data/lib/opencdd/parcel/referenced_irdis.rb +91 -0
- data/lib/opencdd/parcel/scrape_verifier.rb +122 -0
- data/lib/opencdd/parcel/selector.rb +115 -0
- data/lib/opencdd/parcel/sharded_dir_reader.rb +151 -0
- data/lib/opencdd/parcel/sheet.rb +287 -0
- data/lib/opencdd/parcel/sheet_emitter.rb +171 -0
- data/lib/opencdd/parcel/sheet_schema.rb +253 -0
- data/lib/opencdd/parcel/workbook.rb +172 -0
- data/lib/opencdd/parcel/workbook_reader.rb +200 -0
- data/lib/opencdd/parcel/writer.rb +185 -0
- data/lib/opencdd/parcel.rb +175 -0
- data/lib/opencdd/parse_helpers.rb +73 -0
- data/lib/opencdd/property.rb +120 -0
- data/lib/opencdd/property_data_element_type.rb +44 -0
- data/lib/opencdd/property_ids.rb +202 -0
- data/lib/opencdd/reader.rb +103 -0
- data/lib/opencdd/relation.rb +88 -0
- data/lib/opencdd/relation_tree.rb +74 -0
- data/lib/opencdd/relation_type.rb +58 -0
- data/lib/opencdd/structured_values.rb +183 -0
- data/lib/opencdd/unit.rb +16 -0
- data/lib/opencdd/validator/class_reference_rule.rb +77 -0
- data/lib/opencdd/validator/condition_rule.rb +27 -0
- data/lib/opencdd/validator/data_type_rule.rb +26 -0
- data/lib/opencdd/validator/enum_rule.rb +27 -0
- data/lib/opencdd/validator/format_rule.rb +54 -0
- data/lib/opencdd/validator/hierarchy_rule.rb +65 -0
- data/lib/opencdd/validator/irdi_rule.rb +57 -0
- data/lib/opencdd/validator/mandatory_rule.rb +25 -0
- data/lib/opencdd/validator/pattern_rule.rb +26 -0
- data/lib/opencdd/validator/reference_rule.rb +36 -0
- data/lib/opencdd/validator/rule.rb +65 -0
- data/lib/opencdd/validator/runner.rb +101 -0
- data/lib/opencdd/validator/set_rule.rb +41 -0
- data/lib/opencdd/validator/synonym_rule.rb +30 -0
- data/lib/opencdd/validator/type_rule.rb +102 -0
- data/lib/opencdd/validator/uniqueness_rule.rb +32 -0
- data/lib/opencdd/validator.rb +68 -0
- data/lib/opencdd/value_format.rb +71 -0
- data/lib/opencdd/value_list.rb +42 -0
- data/lib/opencdd/value_term.rb +24 -0
- data/lib/opencdd/version.rb +5 -0
- data/lib/opencdd/view_control.rb +10 -0
- data/lib/opencdd/visitor.rb +93 -0
- data/lib/opencdd.rb +57 -0
- 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
|