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