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,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
|
data/lib/opencdd/unit.rb
ADDED
|
@@ -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
|