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,287 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
module Parcel
|
|
5
|
+
class Sheet
|
|
6
|
+
DEFAULT_REQUIREMENT_FOR_CODE = "KEY".freeze
|
|
7
|
+
DEFAULT_REQUIREMENT_OTHER = "OPT".freeze
|
|
8
|
+
DEFAULT_SOURCE_LANGUAGE = "en".freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :name, :metadata, :schema, :rows, :raw_row_count
|
|
11
|
+
|
|
12
|
+
def self.scaffold(meta_class_irdi:, parcel_id:, source_language: DEFAULT_SOURCE_LANGUAGE,
|
|
13
|
+
translation_languages: [], sheet_name: nil)
|
|
14
|
+
meta = Opencdd::MetaClasses.for(meta_class_irdi.to_s) or
|
|
15
|
+
raise ArgumentError, "unknown meta-class #{meta_class_irdi}"
|
|
16
|
+
|
|
17
|
+
code_property_id = Opencdd::MetaClasses.code_property_id_for(meta.irdi)
|
|
18
|
+
languages = ([source_language] + translation_languages).map(&:to_s).uniq
|
|
19
|
+
|
|
20
|
+
columns = meta.allowed_property_ids.each_with_index.map do |property_id, idx|
|
|
21
|
+
SheetSchema::Column.new(
|
|
22
|
+
index: idx + 1,
|
|
23
|
+
property_id: property_id,
|
|
24
|
+
name_by_lang: languages.to_h { |l| [l, display_name_for(property_id, l)] },
|
|
25
|
+
datatype: default_datatype_for(property_id),
|
|
26
|
+
value_format: nil,
|
|
27
|
+
pattern: nil,
|
|
28
|
+
requirement: property_id == code_property_id ? DEFAULT_REQUIREMENT_FOR_CODE : DEFAULT_REQUIREMENT_OTHER,
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
schema = SheetSchema.new
|
|
33
|
+
columns.each do |col|
|
|
34
|
+
schema.add_column(col)
|
|
35
|
+
end
|
|
36
|
+
schema.finalize_for_scaffold!
|
|
37
|
+
|
|
38
|
+
metadata = Metadata.new
|
|
39
|
+
metadata.add("#CLASS_ID := #{meta.irdi}")
|
|
40
|
+
metadata.add("#CLASS_NAME.#{source_language} := #{meta.name}")
|
|
41
|
+
metadata.add("#SOURCE_LANGUAGE := #{source_language}")
|
|
42
|
+
unless translation_languages.empty?
|
|
43
|
+
metadata.add("#TRANSLATION_LANGUAGE := #{translation_languages.join(',')}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
name = sheet_name || "#{parcel_id}_#{meta.name.upcase}"
|
|
47
|
+
new(name: name, metadata: metadata, schema: schema, raw_rows: [])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.display_name_for(property_id, _lang)
|
|
51
|
+
entry = Opencdd::PropertyIds::REGISTRY[property_id.to_s]
|
|
52
|
+
return property_id.to_s unless entry
|
|
53
|
+
entry.aliases.first || property_id.to_s
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.default_datatype_for(property_id)
|
|
57
|
+
entry = Opencdd::PropertyIds::REGISTRY[property_id.to_s]
|
|
58
|
+
return nil unless entry
|
|
59
|
+
case entry.value_kind
|
|
60
|
+
when :identifier_ref, :class_ref then "ICID_STRING"
|
|
61
|
+
when :set_of_refs then "ICID_STRING"
|
|
62
|
+
when :date then "DATE_TYPE"
|
|
63
|
+
when :date_time then "DATE_TIME_TYPE"
|
|
64
|
+
when :condition then "STRING_TYPE"
|
|
65
|
+
else "STRING_TYPE"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.from_rows(rows, name: nil)
|
|
70
|
+
metadata = Opencdd::Parcel::Metadata.new
|
|
71
|
+
header_rows = []
|
|
72
|
+
data_rows = []
|
|
73
|
+
|
|
74
|
+
rows.each do |raw_row|
|
|
75
|
+
row = Array(raw_row)
|
|
76
|
+
first_cell = row.first&.to_s&.strip.to_s
|
|
77
|
+
|
|
78
|
+
if first_cell.start_with?("#") && first_cell.include?(":=")
|
|
79
|
+
metadata.add(first_cell)
|
|
80
|
+
next
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if first_cell.start_with?("#")
|
|
84
|
+
header_rows << row
|
|
85
|
+
next
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if row_has_data?(row)
|
|
89
|
+
data_rows << row
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
schema = Opencdd::Parcel::SheetSchema.from_header_rows(header_rows)
|
|
94
|
+
new(name: name, metadata: metadata, schema: schema, raw_rows: data_rows)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.row_has_data?(row)
|
|
98
|
+
row.each_with_index do |v, i|
|
|
99
|
+
next if i.zero?
|
|
100
|
+
next if v.nil?
|
|
101
|
+
s = v.to_s.strip
|
|
102
|
+
return true unless s.empty?
|
|
103
|
+
end
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def initialize(name:, metadata:, schema:, raw_rows:)
|
|
108
|
+
@name = name
|
|
109
|
+
@metadata = metadata
|
|
110
|
+
@schema = schema
|
|
111
|
+
@raw_row_count = raw_rows.size
|
|
112
|
+
@rows = build_rows(raw_rows)
|
|
113
|
+
freeze
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def meta_class_irdi
|
|
117
|
+
@metadata.meta_class_irdi
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def meta_class_code
|
|
121
|
+
@metadata.meta_class_code
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def type
|
|
125
|
+
@metadata.type
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def each(&block)
|
|
129
|
+
@rows.each(&block)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
include Enumerable
|
|
133
|
+
|
|
134
|
+
def size
|
|
135
|
+
@rows.size
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
alias_method :count, :size
|
|
139
|
+
alias_method :length, :size
|
|
140
|
+
|
|
141
|
+
def first
|
|
142
|
+
@rows.first
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def last
|
|
146
|
+
@rows.last
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def entities(database = nil)
|
|
150
|
+
EntitiesProxy.new(self, database)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def apply_default_values!(property_id:)
|
|
154
|
+
col = @schema.find_by_property_id(property_id.to_s)
|
|
155
|
+
return if col.nil? || col.default_value.nil?
|
|
156
|
+
storage_key = storage_key_for(col)
|
|
157
|
+
@rows.each do |row|
|
|
158
|
+
current = row[storage_key]
|
|
159
|
+
next if !current.nil? && !current.to_s.empty?
|
|
160
|
+
row[storage_key] = col.default_value
|
|
161
|
+
end
|
|
162
|
+
self
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def apply_all_default_values!
|
|
166
|
+
@schema.columns.each do |col|
|
|
167
|
+
next if col.default_value.nil?
|
|
168
|
+
apply_default_values!(property_id: col.property_id)
|
|
169
|
+
end
|
|
170
|
+
self
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def merge_rows_from(other)
|
|
174
|
+
unless meta_codes_match?(meta_class_irdi, other.meta_class_irdi)
|
|
175
|
+
raise ArgumentError,
|
|
176
|
+
"meta-class mismatch: #{meta_class_irdi} vs #{other.meta_class_irdi}"
|
|
177
|
+
end
|
|
178
|
+
other.rows.each do |src|
|
|
179
|
+
new_row = src.reject { |k, _| k == "__row_index__" }
|
|
180
|
+
next if new_row.empty?
|
|
181
|
+
new_row["__row_index__"] = @rows.size
|
|
182
|
+
@rows << new_row
|
|
183
|
+
end
|
|
184
|
+
self
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def dup_with(name:)
|
|
188
|
+
raw_rows = @rows.map { |row| row_to_array(row) }
|
|
189
|
+
self.class.new(name: name, metadata: @metadata, schema: @schema, raw_rows: raw_rows)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
def meta_codes_match?(a, b)
|
|
195
|
+
return false if a.nil? || b.nil?
|
|
196
|
+
a.to_s.split("#").last == b.to_s.split("#").last
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def row_to_array(row)
|
|
200
|
+
array = Array.new(@schema.columns.map(&:index).max.to_i + 1)
|
|
201
|
+
@schema.columns.each do |col|
|
|
202
|
+
key = storage_key_for(col)
|
|
203
|
+
val = row[key]
|
|
204
|
+
array[col.index] = val if val
|
|
205
|
+
end
|
|
206
|
+
array
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def build_rows(raw_rows)
|
|
210
|
+
seen = {}
|
|
211
|
+
result = []
|
|
212
|
+
raw_rows.each do |row|
|
|
213
|
+
instance = build_instance(row)
|
|
214
|
+
next unless instance
|
|
215
|
+
key = instance["__row_index__"] = result.size
|
|
216
|
+
result << instance
|
|
217
|
+
seen[key] = true
|
|
218
|
+
end
|
|
219
|
+
result
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def build_instance(row)
|
|
223
|
+
h = {}
|
|
224
|
+
any_value = false
|
|
225
|
+
@schema.columns.each do |col|
|
|
226
|
+
v = row[col.index]
|
|
227
|
+
next if v.nil?
|
|
228
|
+
s = value_to_string(v)
|
|
229
|
+
next if s.empty?
|
|
230
|
+
key = storage_key_for(col)
|
|
231
|
+
h[key] = s
|
|
232
|
+
any_value = true
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
return nil unless any_value
|
|
236
|
+
h["__row_index__"] = nil
|
|
237
|
+
h
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def storage_key_for(col)
|
|
241
|
+
pid = col.property_id
|
|
242
|
+
return pid if pid.include?(".")
|
|
243
|
+
entry = Opencdd::PropertyIds::REGISTRY[pid]
|
|
244
|
+
return pid unless entry&.multilingual
|
|
245
|
+
langs = (col.name_by_lang || {}).keys
|
|
246
|
+
return pid if langs.size != 1
|
|
247
|
+
"#{pid}.#{langs.first}"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def value_to_string(v)
|
|
251
|
+
return "" if v.nil?
|
|
252
|
+
case v
|
|
253
|
+
when String then v.strip
|
|
254
|
+
when Float
|
|
255
|
+
if v == v.to_i
|
|
256
|
+
v.to_i.to_s
|
|
257
|
+
else
|
|
258
|
+
v.to_s
|
|
259
|
+
end
|
|
260
|
+
when Integer then v.to_s
|
|
261
|
+
when Date, Time then v.iso8601
|
|
262
|
+
when Symbol then v.to_s
|
|
263
|
+
else v.to_s
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
class EntitiesProxy
|
|
268
|
+
attr_reader :sheet, :database
|
|
269
|
+
|
|
270
|
+
def initialize(sheet, database)
|
|
271
|
+
@sheet = sheet
|
|
272
|
+
@database = database
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def each
|
|
276
|
+
return enum_for(:each) unless block_given?
|
|
277
|
+
|
|
278
|
+
sheet.each do |row|
|
|
279
|
+
yield row
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
include Enumerable
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
module Parcel
|
|
5
|
+
class SheetEmitter
|
|
6
|
+
DEFAULT_SOURCE_LANGUAGE = "en".freeze
|
|
7
|
+
|
|
8
|
+
META_DIRECTIVE_ORDER = %w[
|
|
9
|
+
CLASS_ID ALTERNATE_CLASSID SUPER_ALT_CLASSID SUB_ALT_CLASSID
|
|
10
|
+
CLASS_NAME CLASS_DEFINITION CLASS_NOTE
|
|
11
|
+
SOURCE_LANGUAGE PARCEL_MODE PARCEL_ID PARCEL_CC
|
|
12
|
+
DEFAULT_SUPPLIER DEFAULT_VERSION
|
|
13
|
+
ID_ENCODE DELIMITER DECIMAL OBJECT_ID_NAME
|
|
14
|
+
TRANSLATION_LANGUAGE
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
HEADER_DIRECTIVE_ORDER = %w[
|
|
18
|
+
PROPERTY_ID ALTERNATE_ID SUPER_ALTERNATE_ID SUB_ALTERNATE_ID EQUIVALENT_ID
|
|
19
|
+
SUPER_PROPERTY
|
|
20
|
+
DATATYPE UNIT VARIABLE_PREFIX_UNIT UNIT_ID ALTERNATIVE_UNITS
|
|
21
|
+
VALUE_FORMAT PATTERN RELATION DEFAULT_VALUE
|
|
22
|
+
DEFAULT_DATA_SUPPLIER DEFAULT_DATA_VERSION REQUIREMENT
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
MULTILINGUAL_DIRECTIVES = %w[PROPERTY_NAME DEFINITION NOTE].freeze
|
|
26
|
+
|
|
27
|
+
attr_reader :source_language
|
|
28
|
+
|
|
29
|
+
def initialize(source_language: "en")
|
|
30
|
+
@source_language = source_language.to_s
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def emit(sheet, entities)
|
|
34
|
+
rows = []
|
|
35
|
+
emit_metadata_rows(sheet, rows)
|
|
36
|
+
emit_header_rows(sheet, rows)
|
|
37
|
+
entities.each { |e| emit_data_row(sheet, e, rows) }
|
|
38
|
+
rows
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def emit_data_rows(sheet, entities)
|
|
42
|
+
rows = []
|
|
43
|
+
entities.each { |e| emit_data_row(sheet, e, rows) }
|
|
44
|
+
rows
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def emit_metadata_rows(sheet, rows)
|
|
50
|
+
present = sheet.metadata.directives.transform_keys(&:to_s)
|
|
51
|
+
META_DIRECTIVE_ORDER.each do |base|
|
|
52
|
+
emit_meta_base(present, base, rows)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def emit_meta_base(present, base, rows)
|
|
57
|
+
matched = present.select { |k, _| k == base || k.start_with?("#{base}.") }
|
|
58
|
+
if matched.empty?
|
|
59
|
+
rows << ["##{base}:="]
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
matched.each do |key, val|
|
|
63
|
+
rows << ["##{key}:=#{val}"]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def emit_header_rows(sheet, rows)
|
|
68
|
+
present = column_directives_from(sheet)
|
|
69
|
+
HEADER_DIRECTIVE_ORDER.each do |name|
|
|
70
|
+
emit_directive_row(present, name, rows)
|
|
71
|
+
end
|
|
72
|
+
MULTILINGUAL_DIRECTIVES.each do |base|
|
|
73
|
+
present.keys.select { |k| k == base || k.start_with?("#{base}.") }.sort.each do |key|
|
|
74
|
+
emit_directive_row(present, key, rows)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def column_directives_from(sheet)
|
|
80
|
+
cols = sheet.schema.columns
|
|
81
|
+
{}.tap do |h|
|
|
82
|
+
HEADER_DIRECTIVE_ORDER.each do |name|
|
|
83
|
+
vals = cols.map { |col| column_scalar_value(col, name) }
|
|
84
|
+
h[name] = vals
|
|
85
|
+
end
|
|
86
|
+
MULTILINGUAL_DIRECTIVES.each do |base|
|
|
87
|
+
langs = cols.flat_map { |col| langs_for(col, base) }.uniq
|
|
88
|
+
langs.each do |lang|
|
|
89
|
+
h["#{base}.#{lang}"] = cols.map { |col| localized_value(col, base, lang) }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def emit_directive_row(present, name, rows)
|
|
96
|
+
values = present[name]
|
|
97
|
+
rows << ["##{name}", *(values || [])]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def column_scalar_value(col, name)
|
|
101
|
+
case name
|
|
102
|
+
when "PROPERTY_ID" then col.raw_property_id || col.property_id
|
|
103
|
+
when "ALTERNATE_ID" then col.alternate_id
|
|
104
|
+
when "SUPER_ALTERNATE_ID" then col.super_alternate_id
|
|
105
|
+
when "SUB_ALTERNATE_ID" then col.sub_alternate_id
|
|
106
|
+
when "EQUIVALENT_ID" then col.equivalent_id
|
|
107
|
+
when "SUPER_PROPERTY" then col.super_property
|
|
108
|
+
when "DATATYPE" then col.datatype
|
|
109
|
+
when "UNIT" then col.unit
|
|
110
|
+
when "VARIABLE_PREFIX_UNIT" then col.variable_prefix_unit
|
|
111
|
+
when "UNIT_ID" then col.unit_id
|
|
112
|
+
when "ALTERNATIVE_UNITS" then col.alternative_units
|
|
113
|
+
when "VALUE_FORMAT" then col.value_format
|
|
114
|
+
when "PATTERN" then col.pattern
|
|
115
|
+
when "RELATION" then col.relation
|
|
116
|
+
when "DEFAULT_VALUE" then col.default_value
|
|
117
|
+
when "DEFAULT_DATA_SUPPLIER" then col.default_data_supplier
|
|
118
|
+
when "DEFAULT_DATA_VERSION" then col.default_data_version
|
|
119
|
+
when "REQUIREMENT" then col.requirement
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def langs_for(col, base)
|
|
124
|
+
case base
|
|
125
|
+
when "PROPERTY_NAME" then (col.name_by_lang || {}).keys
|
|
126
|
+
when "DEFINITION" then (col.definition_by_lang || {}).keys
|
|
127
|
+
when "NOTE" then (col.note_by_lang || {}).keys
|
|
128
|
+
else []
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def localized_value(col, base, lang)
|
|
133
|
+
case base
|
|
134
|
+
when "PROPERTY_NAME" then (col.name_by_lang || {})[lang]
|
|
135
|
+
when "DEFINITION" then (col.definition_by_lang || {})[lang]
|
|
136
|
+
when "NOTE" then (col.note_by_lang || {})[lang]
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def emit_data_row(sheet, entity, rows)
|
|
141
|
+
values = sheet.schema.columns.map { |col| entity_value_for(entity, col) }
|
|
142
|
+
rows << [nil, *values]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def entity_value_for(entity, col)
|
|
146
|
+
pid = col.property_id
|
|
147
|
+
val = entity.properties[pid]
|
|
148
|
+
return val unless val.nil?
|
|
149
|
+
multilingual_lookup(entity, pid)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Multilingual fallback. The raw properties hash is the
|
|
153
|
+
# canonical store; the lookup here mirrors FieldReader's
|
|
154
|
+
# raw_multilingual resolution (single source of truth for
|
|
155
|
+
# "where do I find the language-tagged variant of this field").
|
|
156
|
+
# Routed through Entity#read_field when the column maps to a
|
|
157
|
+
# declared field, so any future field-DTL changes apply here
|
|
158
|
+
# too — see TODO.impl/26.
|
|
159
|
+
def multilingual_lookup(entity, pid)
|
|
160
|
+
entry = Opencdd::PropertyIds::REGISTRY[pid]
|
|
161
|
+
return entity.properties["#{pid}.#{@source_language}"] if entry && entry.multilingual
|
|
162
|
+
if pid.include?(".")
|
|
163
|
+
base, = pid.split(".", 2)
|
|
164
|
+
return entity.properties[pid] if entity.properties.key?(pid)
|
|
165
|
+
return entity.properties[base]
|
|
166
|
+
end
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
module Parcel
|
|
5
|
+
class SheetSchema
|
|
6
|
+
# Parcel-specific column-ID canonicalization. Maps the
|
|
7
|
+
# ParcelMaker variant headers (MDC_P004_1, MDC_P005, ...)
|
|
8
|
+
# to the canonical IEC 61360 IDs (MDC_P004, MDC_P006, ...).
|
|
9
|
+
#
|
|
10
|
+
# Owned by SheetSchema because this mapping only exists for
|
|
11
|
+
# the Parcel sheet layout — the PropertyIds registry is
|
|
12
|
+
# ontology-only and shouldn't carry format-specific details.
|
|
13
|
+
VARIANT_TO_CANONICAL = {
|
|
14
|
+
"MDC_P004_1" => "MDC_P004",
|
|
15
|
+
"MDC_P004_2" => "MDC_P007",
|
|
16
|
+
"MDC_P004_3" => "MDC_P005",
|
|
17
|
+
"MDC_P005" => "MDC_P006",
|
|
18
|
+
"MDC_P007_1" => "MDC_P008",
|
|
19
|
+
"MDC_P007_2" => "MDC_P009",
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Canonicalize a Parcel column ID. Splits language tags
|
|
23
|
+
# (<id>.<lang>) so they round-trip cleanly. Returns the
|
|
24
|
+
# canonical ID (or the input unchanged if no mapping exists).
|
|
25
|
+
def self.canonical_id(raw_id)
|
|
26
|
+
return nil if raw_id.nil?
|
|
27
|
+
s = raw_id.to_s.strip
|
|
28
|
+
return nil if s.empty?
|
|
29
|
+
match = s.match(/\.(?<lang>[A-Za-z0-9-]+)\z/)
|
|
30
|
+
base = match ? match.pre_match : s
|
|
31
|
+
lang = match && match[:lang]
|
|
32
|
+
canonical = VARIANT_TO_CANONICAL[base] || base
|
|
33
|
+
lang ? "#{canonical}.#{lang}" : canonical
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
DIRECTIVE_ROWS = %w[
|
|
37
|
+
PROPERTY_ID
|
|
38
|
+
ALTERNATE_ID
|
|
39
|
+
SUPER_ALTERNATE_ID
|
|
40
|
+
SUB_ALTERNATE_ID
|
|
41
|
+
EQUIVALENT_ID
|
|
42
|
+
SUPER_PROPERTY
|
|
43
|
+
PROPERTY_NAME
|
|
44
|
+
DEFINITION
|
|
45
|
+
NOTE
|
|
46
|
+
DATATYPE
|
|
47
|
+
UNIT
|
|
48
|
+
VARIABLE_PREFIX_UNIT
|
|
49
|
+
UNIT_ID
|
|
50
|
+
ALTERNATIVE_UNITS
|
|
51
|
+
VALUE_FORMAT
|
|
52
|
+
PATTERN
|
|
53
|
+
RELATION
|
|
54
|
+
DEFAULT_VALUE
|
|
55
|
+
DEFAULT_DATA_SUPPLIER
|
|
56
|
+
DEFAULT_DATA_VERSION
|
|
57
|
+
REQUIREMENT
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
class Column < Struct.new(
|
|
61
|
+
:index,
|
|
62
|
+
:property_id,
|
|
63
|
+
:raw_property_id,
|
|
64
|
+
:alternate_id,
|
|
65
|
+
:super_alternate_id,
|
|
66
|
+
:sub_alternate_id,
|
|
67
|
+
:equivalent_id,
|
|
68
|
+
:super_property,
|
|
69
|
+
:name_by_lang,
|
|
70
|
+
:definition_by_lang,
|
|
71
|
+
:note_by_lang,
|
|
72
|
+
:datatype,
|
|
73
|
+
:unit,
|
|
74
|
+
:variable_prefix_unit,
|
|
75
|
+
:unit_id,
|
|
76
|
+
:alternative_units,
|
|
77
|
+
:value_format,
|
|
78
|
+
:pattern,
|
|
79
|
+
:relation,
|
|
80
|
+
:default_value,
|
|
81
|
+
:default_data_supplier,
|
|
82
|
+
:default_data_version,
|
|
83
|
+
:requirement,
|
|
84
|
+
keyword_init: true,
|
|
85
|
+
)
|
|
86
|
+
def name(lang = :en)
|
|
87
|
+
name_by_lang[lang.to_s]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def definition(lang = :en)
|
|
91
|
+
definition_by_lang[lang.to_s]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def note(lang = :en)
|
|
95
|
+
note_by_lang[lang.to_s]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def required?
|
|
99
|
+
requirement == "MAND"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def key?
|
|
103
|
+
requirement == "KEY"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def obsolete?
|
|
107
|
+
requirement == "OBS"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
DIRECTIVE_ROW_PREFIX = "#".freeze
|
|
112
|
+
|
|
113
|
+
attr_reader :columns, :columns_by_id, :column_directives
|
|
114
|
+
|
|
115
|
+
def initialize
|
|
116
|
+
@columns = []
|
|
117
|
+
@columns_by_id = {}
|
|
118
|
+
@column_directives = {}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.from_header_rows(rows)
|
|
122
|
+
schema = new
|
|
123
|
+
rows.each do |row|
|
|
124
|
+
schema.add_directive_row(row)
|
|
125
|
+
end
|
|
126
|
+
schema.finalize!
|
|
127
|
+
schema
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def add_directive_row(row)
|
|
131
|
+
return self if row.nil? || row.empty?
|
|
132
|
+
|
|
133
|
+
label_cell = row[0].to_s
|
|
134
|
+
directive = parse_directive(label_cell)
|
|
135
|
+
return self unless directive
|
|
136
|
+
|
|
137
|
+
values = row[1..].to_a
|
|
138
|
+
|
|
139
|
+
unless @column_directives.key?(directive)
|
|
140
|
+
@column_directives[directive] = []
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
values.each_with_index do |val, idx|
|
|
144
|
+
@column_directives[directive][idx] = val
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
self
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def add_column(column)
|
|
151
|
+
@columns << column
|
|
152
|
+
@columns_by_id[column.property_id] = column
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def finalize_for_scaffold!
|
|
157
|
+
freeze
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def finalize!
|
|
161
|
+
ids = @column_directives["PROPERTY_ID"] || []
|
|
162
|
+
ids.each_with_index do |id, idx|
|
|
163
|
+
next if id.nil? || id.to_s.strip.empty?
|
|
164
|
+
|
|
165
|
+
col = Column.new(
|
|
166
|
+
index: idx + 1,
|
|
167
|
+
property_id: Opencdd::Parcel::SheetSchema.canonical_id(id.to_s.strip),
|
|
168
|
+
raw_property_id: id.to_s.strip,
|
|
169
|
+
alternate_id: lookup("ALTERNATE_ID", idx),
|
|
170
|
+
super_alternate_id: lookup("SUPER_ALTERNATE_ID", idx),
|
|
171
|
+
sub_alternate_id: lookup("SUB_ALTERNATE_ID", idx),
|
|
172
|
+
equivalent_id: lookup("EQUIVALENT_ID", idx),
|
|
173
|
+
super_property: lookup("SUPER_PROPERTY", idx),
|
|
174
|
+
name_by_lang: lang_hash_for("PROPERTY_NAME", idx),
|
|
175
|
+
definition_by_lang: lang_hash_for("DEFINITION", idx),
|
|
176
|
+
note_by_lang: lang_hash_for("NOTE", idx),
|
|
177
|
+
datatype: lookup("DATATYPE", idx),
|
|
178
|
+
unit: lookup("UNIT", idx),
|
|
179
|
+
variable_prefix_unit: lookup("VARIABLE_PREFIX_UNIT", idx),
|
|
180
|
+
unit_id: lookup("UNIT_ID", idx),
|
|
181
|
+
alternative_units: lookup("ALTERNATIVE_UNITS", idx),
|
|
182
|
+
value_format: lookup("VALUE_FORMAT", idx),
|
|
183
|
+
pattern: lookup("PATTERN", idx),
|
|
184
|
+
relation: lookup("RELATION", idx),
|
|
185
|
+
default_value: lookup("DEFAULT_VALUE", idx),
|
|
186
|
+
default_data_supplier: lookup("DEFAULT_DATA_SUPPLIER", idx),
|
|
187
|
+
default_data_version: lookup("DEFAULT_DATA_VERSION", idx),
|
|
188
|
+
requirement: lookup("REQUIREMENT", idx),
|
|
189
|
+
)
|
|
190
|
+
@columns << col
|
|
191
|
+
@columns_by_id[col.property_id] = col
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
freeze
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def [](property_id_or_name)
|
|
198
|
+
find_by_property_id(property_id_or_name) || find_by_name(property_id_or_name)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def find_by_property_id(id)
|
|
202
|
+
@columns_by_id[id.to_s]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def find_by_name(name, lang = :en)
|
|
206
|
+
@columns.find { |c| c.name(lang).to_s.casecmp(name.to_s).zero? }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def size
|
|
210
|
+
@columns.size
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def each(&block)
|
|
214
|
+
@columns.each(&block)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
include Enumerable
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
|
|
221
|
+
def parse_directive(cell)
|
|
222
|
+
return nil if cell.nil?
|
|
223
|
+
s = cell.to_s.strip
|
|
224
|
+
return nil unless s.start_with?("#")
|
|
225
|
+
s[1..]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def lookup(directive, col_idx)
|
|
229
|
+
vals = @column_directives[directive]
|
|
230
|
+
return nil unless vals
|
|
231
|
+
v = vals[col_idx]
|
|
232
|
+
return nil if v.nil?
|
|
233
|
+
s = v.to_s.strip
|
|
234
|
+
s.empty? ? nil : s
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def lang_hash_for(directive, col_idx)
|
|
238
|
+
h = {}
|
|
239
|
+
@column_directives.each do |key, vals|
|
|
240
|
+
base, lang = key.split(".", 2)
|
|
241
|
+
next unless base == directive
|
|
242
|
+
v = vals[col_idx]
|
|
243
|
+
next if v.nil?
|
|
244
|
+
s = v.to_s.strip
|
|
245
|
+
next if s.empty?
|
|
246
|
+
lang = "en" if lang.nil? || lang.empty?
|
|
247
|
+
h[lang] = s
|
|
248
|
+
end
|
|
249
|
+
h
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|