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,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Opencdd
|
|
6
|
+
module Parcel
|
|
7
|
+
class Workbook
|
|
8
|
+
SheetMapEntry = Struct.new(:project_id, :parcel_id, :class_irdi, :content_no,
|
|
9
|
+
:sheet_no, :sheet_name, :type, :target, keyword_init: true)
|
|
10
|
+
ProjectInfo = Struct.new(:project_id, :parcel_id, :multi_language, :base_language,
|
|
11
|
+
keyword_init: true)
|
|
12
|
+
|
|
13
|
+
HEADER_ROW_NAMES = %i[
|
|
14
|
+
class_id class_name source_language translation_language
|
|
15
|
+
property_id property_name datatype value_format pattern
|
|
16
|
+
default_value requirement unit
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
MANDATORY_HEADER_ROWS = %i[class_id property_id].freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :sheets, :sheetmap, :project, :source_path
|
|
22
|
+
|
|
23
|
+
def initialize(sheets:, sheetmap: [], project: nil, source_path: nil,
|
|
24
|
+
hidden_header_rows: nil)
|
|
25
|
+
@sheets = sheets
|
|
26
|
+
@sheetmap = sheetmap
|
|
27
|
+
@project = project
|
|
28
|
+
@source_path = source_path
|
|
29
|
+
@hidden_header_rows = validate_hidden(hidden_header_rows)
|
|
30
|
+
reindex
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def hidden_header_rows
|
|
35
|
+
@hidden_header_rows.dup
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def hide_header_row!(name)
|
|
39
|
+
sym = name.to_sym
|
|
40
|
+
raise ArgumentError, "unknown header row: #{sym.inspect}" unless HEADER_ROW_NAMES.include?(sym)
|
|
41
|
+
raise ArgumentError, "cannot hide mandatory header row: #{sym.inspect}" if MANDATORY_HEADER_ROWS.include?(sym)
|
|
42
|
+
@hidden_header_rows << sym
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def show_header_row!(name)
|
|
47
|
+
@hidden_header_rows.delete(name.to_sym)
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def register_sheet(sheet)
|
|
52
|
+
raise ArgumentError, "sheet already registered: #{sheet.name}" if @sheets_by_name.key?(sheet.name.to_s)
|
|
53
|
+
@sheets << sheet
|
|
54
|
+
@sheets_by_name[sheet.name.to_s] = sheet
|
|
55
|
+
@sheetmap << SheetMapEntry.new(
|
|
56
|
+
project_id: @project&.project_id || "LOCAL",
|
|
57
|
+
parcel_id: @project&.parcel_id,
|
|
58
|
+
class_irdi: sheet.meta_class_irdi,
|
|
59
|
+
content_no: 0,
|
|
60
|
+
sheet_no: @sheets.size,
|
|
61
|
+
sheet_name: sheet.name,
|
|
62
|
+
type: parcel_type_label_for(sheet.type),
|
|
63
|
+
target: "",
|
|
64
|
+
)
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def import_into(sheet_name, file_path, format: nil)
|
|
69
|
+
target = sheet(sheet_name)
|
|
70
|
+
raise ArgumentError, "unknown sheet: #{sheet_name.inspect}" unless target
|
|
71
|
+
|
|
72
|
+
format ||= detect_format(file_path)
|
|
73
|
+
temp = read_external_sheet(file_path, format, target.meta_class_irdi)
|
|
74
|
+
unless meta_codes_match?(target.meta_class_irdi, temp.meta_class_irdi)
|
|
75
|
+
raise ArgumentError,
|
|
76
|
+
"meta-class mismatch: target=#{target.meta_class_irdi} source=#{temp.meta_class_irdi}"
|
|
77
|
+
end
|
|
78
|
+
target.merge_rows_from(temp)
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def sheet(name)
|
|
83
|
+
@sheets_by_name[name.to_s]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def sheets_of_type(type)
|
|
87
|
+
type = type.to_sym
|
|
88
|
+
@sheets.select { |s| s.type == type }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def class_sheet ; sheets_of_type(:class).first ; end
|
|
92
|
+
def property_sheet ; sheets_of_type(:property).first ; end
|
|
93
|
+
def unit_sheet ; sheets_of_type(:unit).first ; end
|
|
94
|
+
def value_list_sheet ; sheets_of_type(:value_list).first ; end
|
|
95
|
+
def value_term_sheet ; sheets_of_type(:value_term).first ; end
|
|
96
|
+
def relation_sheet ; sheets_of_type(:relation).first ; end
|
|
97
|
+
def view_control_sheet; sheets_of_type(:view_control).first; end
|
|
98
|
+
|
|
99
|
+
def each_sheet(&block)
|
|
100
|
+
@sheets.each(&block)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parcel_id
|
|
104
|
+
@project&.parcel_id
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def project_id
|
|
108
|
+
@project&.project_id
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def base_language
|
|
112
|
+
@project&.base_language || "en"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def merge(other)
|
|
116
|
+
merged_sheets = @sheets + other.sheets
|
|
117
|
+
merged_map = @sheetmap + other.sheetmap
|
|
118
|
+
merged_proj = @project || other.project
|
|
119
|
+
Workbook.new(sheets: merged_sheets, sheetmap: merged_map, project: merged_proj,
|
|
120
|
+
source_path: @source_path)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def validate_hidden(rows)
|
|
126
|
+
return Set.new if rows.nil?
|
|
127
|
+
set = rows.to_set
|
|
128
|
+
invalid = set - HEADER_ROW_NAMES.to_set
|
|
129
|
+
raise ArgumentError, "unknown header rows: #{invalid.to_a.inspect}" unless invalid.empty?
|
|
130
|
+
mandatory = set & MANDATORY_HEADER_ROWS.to_set
|
|
131
|
+
raise ArgumentError, "cannot hide mandatory rows: #{mandatory.to_a.inspect}" unless mandatory.empty?
|
|
132
|
+
set
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def detect_format(path)
|
|
136
|
+
case File.extname(path.to_s).downcase
|
|
137
|
+
when ".csv" then :csv
|
|
138
|
+
when ".txt" then :csv_tab
|
|
139
|
+
when ".xlsx", ".xlsm" then :xlsx
|
|
140
|
+
when ".xls" then :xls
|
|
141
|
+
else raise ArgumentError, "cannot detect format for: #{path.inspect}"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def read_external_sheet(path, format, meta_class_irdi)
|
|
146
|
+
case format
|
|
147
|
+
when :csv then Opencdd::Parcel::CsvReader.read(path, meta_class_irdi: meta_class_irdi)
|
|
148
|
+
when :csv_tab then Opencdd::Parcel::CsvReader.read(path, meta_class_irdi: meta_class_irdi, col_sep: "\t")
|
|
149
|
+
when :xlsx, :xls
|
|
150
|
+
Opencdd::Parcel::WorkbookReader.new(path).read_workbook.sheets.first
|
|
151
|
+
else raise ArgumentError, "unknown format: #{format.inspect}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def parcel_type_label_for(type)
|
|
156
|
+
return nil if type.nil?
|
|
157
|
+
Opencdd::MetaClasses.sheet_type_for_type(type) || type.to_s.upcase
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def meta_codes_match?(a, b)
|
|
161
|
+
return false if a.nil? || b.nil?
|
|
162
|
+
a_code = a.to_s.split("#").last
|
|
163
|
+
b_code = b.to_s.split("#").last
|
|
164
|
+
a_code == b_code
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def reindex
|
|
168
|
+
@sheets_by_name = @sheets.to_h { |s| [s.name.to_s, s] }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
module Parcel
|
|
5
|
+
# Reads a single Parcel workbook file (.xlsx, .xlsm, .xltx, or
|
|
6
|
+
# legacy single-sheet .xls) and produces a +Opencdd::Parcel::Workbook+.
|
|
7
|
+
#
|
|
8
|
+
# This is the canonical IEC 62656-1 Parcel container: one file
|
|
9
|
+
# with Project / sheetmap / pcls_LOCAL / data sheets. Use
|
|
10
|
+
# +Opencdd::Parcel::FlatDirReader+ for the legacy 6-file-per-directory
|
|
11
|
+
# layout, and +Opencdd::Parcel::ShardedDirReader+ for the per-class
|
|
12
|
+
# sharded layout.
|
|
13
|
+
class WorkbookReader
|
|
14
|
+
LEGACY_TYPE_BY_PREFIX = {
|
|
15
|
+
"CLASS" => :class,
|
|
16
|
+
"PROPERTY" => :property,
|
|
17
|
+
"RELATION" => :relation,
|
|
18
|
+
"UNIT" => :unit,
|
|
19
|
+
"VALUELIST" => :value_list,
|
|
20
|
+
"VALUETERMS" => :value_term,
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
LEGACY_TYPE_TO_PARCEL_NAME = {
|
|
24
|
+
class: "CLASS",
|
|
25
|
+
property: "PROPERTY",
|
|
26
|
+
value_list: "ENUM",
|
|
27
|
+
value_term: "TERMINOLOGY",
|
|
28
|
+
unit: "UoM",
|
|
29
|
+
relation: "RELATION",
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
META_CLASS_BY_LEGACY_TYPE = Opencdd::MetaClasses::TYPE_BY_META_CLASS.invert.freeze
|
|
33
|
+
|
|
34
|
+
attr_reader :path
|
|
35
|
+
|
|
36
|
+
def initialize(path)
|
|
37
|
+
@path = path
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def read_workbook
|
|
41
|
+
sheets = []
|
|
42
|
+
sheetmap = []
|
|
43
|
+
project = nil
|
|
44
|
+
|
|
45
|
+
workbook = open_workbook
|
|
46
|
+
|
|
47
|
+
if workbook.sheet_names.include?("sheetmap")
|
|
48
|
+
sheetmap = parse_sheetmap(workbook.rows_for("sheetmap"))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if workbook.sheet_names.include?("Project")
|
|
52
|
+
project = parse_project(workbook.rows_for("Project"))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
workbook.sheet_names.each do |sheet_name|
|
|
56
|
+
next if ["Project", "sheetmap", "pcls_LOCAL"].include?(sheet_name)
|
|
57
|
+
sheet = Opencdd::Parcel::Sheet.from_rows(workbook.rows_for(sheet_name).each, name: sheet_name)
|
|
58
|
+
sheets << sheet if sheet
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if sheets.empty? && legacy?
|
|
62
|
+
sheets = [Opencdd::Parcel::Sheet.from_rows(
|
|
63
|
+
workbook.rows_for(workbook.sheet_names.first).each,
|
|
64
|
+
name: File.basename(@path.to_s, ".*"),
|
|
65
|
+
)].compact
|
|
66
|
+
sheetmap = sheets.map { |s|
|
|
67
|
+
type = s.type
|
|
68
|
+
next unless type
|
|
69
|
+
Workbook::SheetMapEntry.new(
|
|
70
|
+
project_id: project&.project_id || "LOCAL",
|
|
71
|
+
parcel_id: project&.parcel_id,
|
|
72
|
+
class_irdi: Opencdd::IRDI.parse("0112/2///62656_1##{META_CLASS_BY_LEGACY_TYPE[type]}"),
|
|
73
|
+
content_no: 0,
|
|
74
|
+
sheet_no: nil,
|
|
75
|
+
sheet_name: s.name,
|
|
76
|
+
type: LEGACY_TYPE_TO_PARCEL_NAME[type] || type.to_s.upcase,
|
|
77
|
+
target: "",
|
|
78
|
+
)
|
|
79
|
+
}.compact
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
Opencdd::Parcel::Workbook.new(
|
|
83
|
+
sheets: sheets,
|
|
84
|
+
sheetmap: sheetmap,
|
|
85
|
+
project: project,
|
|
86
|
+
source_path: @path.to_s,
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def load_into(database)
|
|
91
|
+
workbook = read_workbook
|
|
92
|
+
database.add_workbook(workbook)
|
|
93
|
+
database.finalize!
|
|
94
|
+
database
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def open_workbook
|
|
100
|
+
ext = File.extname(@path.to_s).downcase
|
|
101
|
+
if ext == ".xls"
|
|
102
|
+
SpreadsheetSource.new(@path.to_s)
|
|
103
|
+
else
|
|
104
|
+
RooSource.new(@path.to_s, ext)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def legacy?
|
|
109
|
+
File.basename(@path.to_s) =~ /\Aexport_(CLASS|PROPERTY|RELATION|UNIT|VALUELIST|VALUETERMS)_.*\z/i
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parse_sheetmap(rows)
|
|
113
|
+
header = rows.first&.map { |c| c.to_s.strip.downcase } || []
|
|
114
|
+
|
|
115
|
+
rows[1..].map do |r|
|
|
116
|
+
vals = r.to_a
|
|
117
|
+
h = {}
|
|
118
|
+
header.each_with_index do |k, i|
|
|
119
|
+
next if k.nil? || k.empty?
|
|
120
|
+
v = vals[i]
|
|
121
|
+
h[k.to_sym] = v.nil? || v.to_s.strip.empty? ? nil : v.to_s.strip
|
|
122
|
+
end
|
|
123
|
+
next unless h[:sheetname]
|
|
124
|
+
Workbook::SheetMapEntry.new(
|
|
125
|
+
project_id: h[:projectid],
|
|
126
|
+
parcel_id: h[:parcelid],
|
|
127
|
+
class_irdi: h[:classid] && Opencdd::IRDI.parse(h[:classid]),
|
|
128
|
+
content_no: h[:contentno]&.to_i,
|
|
129
|
+
sheet_no: h[:sheetno]&.to_i,
|
|
130
|
+
sheet_name: h[:sheetname],
|
|
131
|
+
type: h[:type],
|
|
132
|
+
target: h[:target] || "",
|
|
133
|
+
)
|
|
134
|
+
end.compact
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def parse_project(rows)
|
|
138
|
+
header = rows.first&.map(&:to_s) || []
|
|
139
|
+
vals = rows[1]&.to_a || []
|
|
140
|
+
h = header.each_with_index.to_h { |k, i| [k.to_s.strip, vals[i]] }
|
|
141
|
+
Workbook::ProjectInfo.new(
|
|
142
|
+
project_id: h["Project ID"].to_s,
|
|
143
|
+
parcel_id: h["Parcel ID"].to_s,
|
|
144
|
+
multi_language: h["Multi language"].to_s,
|
|
145
|
+
base_language: (h["Base language"].to_s.empty? ? "en" : h["Base language"].to_s),
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
class RooSource < Struct.new(:path, :extension)
|
|
150
|
+
def initialize(path, ext)
|
|
151
|
+
require "roo"
|
|
152
|
+
roo_ext =
|
|
153
|
+
case ext
|
|
154
|
+
when ".xlsx" then :xlsx
|
|
155
|
+
when ".xlsm" then :xlsm
|
|
156
|
+
when ".xls" then :xls
|
|
157
|
+
when ".csv" then :csv
|
|
158
|
+
else :xlsx
|
|
159
|
+
end
|
|
160
|
+
super(path, roo_ext)
|
|
161
|
+
@roo = Roo::Spreadsheet.open(path, extension: extension)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def sheet_names
|
|
165
|
+
@sheet_names ||= @roo.sheets
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def rows_for(name)
|
|
169
|
+
sheet = @roo.sheet(name)
|
|
170
|
+
(1..sheet.last_row.to_i).map { |i| sheet.row(i) }
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
class SpreadsheetSource < Struct.new(:path)
|
|
175
|
+
def initialize(path)
|
|
176
|
+
require "spreadsheet"
|
|
177
|
+
super(path)
|
|
178
|
+
@book = Spreadsheet.open(path)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def sheet_names
|
|
182
|
+
@sheet_names ||= @book.worksheets.map { |ws| decode_name(ws.name) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def rows_for(name)
|
|
186
|
+
ws = @book.worksheets.find { |w| decode_name(w.name) == name } || @book.worksheet(0)
|
|
187
|
+
ws.rows.map(&:to_a)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def decode_name(s)
|
|
193
|
+
s.to_s.encode("UTF-8", "UTF-16LE", invalid: :replace, undef: :replace).scrub("")
|
|
194
|
+
rescue Encoding::ConverterNotFoundError
|
|
195
|
+
s.to_s
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "set"
|
|
5
|
+
require "caxlsx"
|
|
6
|
+
|
|
7
|
+
module Opencdd
|
|
8
|
+
module Parcel
|
|
9
|
+
class Writer
|
|
10
|
+
DEFAULT_PROJECT_ID = "LOCAL"
|
|
11
|
+
DEFAULT_SOURCE_LANGUAGE = "en"
|
|
12
|
+
|
|
13
|
+
attr_reader :database
|
|
14
|
+
|
|
15
|
+
def initialize(database)
|
|
16
|
+
@database = database
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def write(target, parcel_id:, project_id: DEFAULT_PROJECT_ID,
|
|
20
|
+
source_language: DEFAULT_SOURCE_LANGUAGE, translation_languages: [],
|
|
21
|
+
sheet_types: :all, selector: nil)
|
|
22
|
+
package = build_package(
|
|
23
|
+
parcel_id: parcel_id, project_id: project_id,
|
|
24
|
+
source_language: source_language, translation_languages: translation_languages,
|
|
25
|
+
sheet_types: sheet_types, selector: selector,
|
|
26
|
+
)
|
|
27
|
+
serialize(package, target)
|
|
28
|
+
target
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def write_sheet(sheet, entities, target, source_language: DEFAULT_SOURCE_LANGUAGE)
|
|
32
|
+
emitter = SheetEmitter.new(source_language: source_language)
|
|
33
|
+
rows = emitter.emit(sheet, entities)
|
|
34
|
+
package = Axlsx::Package.new
|
|
35
|
+
package.workbook.add_worksheet(name: sheet.name) do |ws|
|
|
36
|
+
rows.each { |row| ws.add_row(row) }
|
|
37
|
+
end
|
|
38
|
+
serialize(package, target)
|
|
39
|
+
target
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def build_package(parcel_id:, project_id:, source_language:, translation_languages:, sheet_types:, selector: nil)
|
|
45
|
+
package = Axlsx::Package.new
|
|
46
|
+
workbook = package.workbook
|
|
47
|
+
|
|
48
|
+
effective_entities = selector ? selector.resolve(@database) : nil
|
|
49
|
+
|
|
50
|
+
built_sheets = build_sheets(
|
|
51
|
+
parcel_id: parcel_id, source_language: source_language,
|
|
52
|
+
translation_languages: translation_languages, sheet_types: sheet_types,
|
|
53
|
+
effective_entities: effective_entities,
|
|
54
|
+
)
|
|
55
|
+
hidden_directives = hidden_header_rows_from_database
|
|
56
|
+
|
|
57
|
+
add_project_sheet(workbook, parcel_id, project_id, source_language, translation_languages)
|
|
58
|
+
add_sheetmap_sheet(workbook, built_sheets, parcel_id, project_id)
|
|
59
|
+
add_data_sheets(workbook, built_sheets, source_language, hidden_directives)
|
|
60
|
+
|
|
61
|
+
package
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
ALL_SHEET_TYPES = %i[class property value_list value_term unit relation view_control].freeze
|
|
65
|
+
|
|
66
|
+
def resolve_types(sheet_types)
|
|
67
|
+
return ALL_SHEET_TYPES if sheet_types == :all
|
|
68
|
+
Array(sheet_types).map(&:to_sym)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_sheets(parcel_id:, source_language:, translation_languages:, sheet_types:, effective_entities: nil)
|
|
72
|
+
types = resolve_types(sheet_types)
|
|
73
|
+
if @database.workbooks.any?
|
|
74
|
+
build_sheets_from_workbook(types, parcel_id, source_language, translation_languages, effective_entities)
|
|
75
|
+
else
|
|
76
|
+
build_scaffolds(types, parcel_id, source_language, translation_languages, effective_entities)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_sheets_from_workbook(types, parcel_id, source_language, translation_languages, effective_entities)
|
|
81
|
+
source_workbook = @database.workbooks.first
|
|
82
|
+
picked = source_workbook.sheets.select { |s| s.type && types.include?(s.type) }
|
|
83
|
+
picked.group_by(&:type).flat_map do |_type, sheets|
|
|
84
|
+
primary = sheets.first
|
|
85
|
+
entities = filtered_entities_of_type(primary.type, effective_entities)
|
|
86
|
+
[BuiltSheet.new(sheet: primary, entities: entities, type: primary.type)]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_scaffolds(types, parcel_id, source_language, translation_languages, effective_entities)
|
|
91
|
+
types.filter_map do |type|
|
|
92
|
+
meta_code = Opencdd::MetaClasses.meta_class_for_type(type)
|
|
93
|
+
next nil unless meta_code
|
|
94
|
+
sheet = Sheet.scaffold(
|
|
95
|
+
meta_class_irdi: meta_code,
|
|
96
|
+
parcel_id: parcel_id,
|
|
97
|
+
source_language: source_language,
|
|
98
|
+
translation_languages: translation_languages,
|
|
99
|
+
)
|
|
100
|
+
entities = filtered_entities_of_type(type, effective_entities)
|
|
101
|
+
BuiltSheet.new(sheet: sheet, entities: entities, type: type)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def filtered_entities_of_type(type, effective_entities)
|
|
106
|
+
all = @database.entities_of_type(type)
|
|
107
|
+
return all if effective_entities.nil?
|
|
108
|
+
allowed = effective_entities.select { |e| e.type == type }.to_set
|
|
109
|
+
all.select { |e| allowed.include?(e) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def add_project_sheet(workbook, parcel_id, project_id, source_language, translation_languages)
|
|
113
|
+
workbook.add_worksheet(name: "Project") do |ws|
|
|
114
|
+
ws.add_row(["Project ID", "Parcel ID", "Multi language", "Base language"])
|
|
115
|
+
multi = translation_languages.empty? ? "" : translation_languages.join(",")
|
|
116
|
+
ws.add_row([project_id, parcel_id, multi, source_language])
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def add_sheetmap_sheet(workbook, built_sheets, parcel_id, project_id)
|
|
121
|
+
workbook.add_worksheet(name: "sheetmap") do |ws|
|
|
122
|
+
ws.add_row(%w[projectid parcelid classid contentno sheetno sheetname type target])
|
|
123
|
+
ws.add_row([project_id, "", "", 0, 2, "pcls_LOCAL", "PARCEL_LIST", ""])
|
|
124
|
+
built_sheets.each_with_index do |built, idx|
|
|
125
|
+
sheet = built.sheet
|
|
126
|
+
ws.add_row([
|
|
127
|
+
project_id,
|
|
128
|
+
parcel_id,
|
|
129
|
+
"#{sheet.meta_class_irdi&.to_s}##1",
|
|
130
|
+
0,
|
|
131
|
+
idx + 3,
|
|
132
|
+
sheet.name,
|
|
133
|
+
parcel_type_label(built.type),
|
|
134
|
+
"",
|
|
135
|
+
])
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parcel_type_label(type)
|
|
141
|
+
Opencdd::MetaClasses.sheet_type_for_type(type) || type.to_s.upcase
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def add_data_sheets(workbook, built_sheets, source_language, hidden_directives = Set.new)
|
|
145
|
+
emitter = SheetEmitter.new(source_language: source_language)
|
|
146
|
+
built_sheets.each do |built|
|
|
147
|
+
rows = emitter.emit(built.sheet, built.entities)
|
|
148
|
+
workbook.add_worksheet(name: built.sheet.name) do |ws|
|
|
149
|
+
rows.each do |row|
|
|
150
|
+
emitted = ws.add_row(row)
|
|
151
|
+
emitted.hidden = true if row_hidden?(row, hidden_directives)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def hidden_header_rows_from_database
|
|
158
|
+
wb = @database.workbooks.first
|
|
159
|
+
return Set.new unless wb
|
|
160
|
+
wb.hidden_header_rows
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def row_hidden?(row, hidden_directives)
|
|
164
|
+
return false if hidden_directives.empty?
|
|
165
|
+
first = row.first.to_s
|
|
166
|
+
return false unless first.start_with?("#")
|
|
167
|
+
base = first.sub(/\A#/, "").split(/[:=.]/).first
|
|
168
|
+
return false if base.nil? || base.empty?
|
|
169
|
+
hidden_directives.include?(base.downcase.to_sym)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def serialize(package, target)
|
|
173
|
+
case target
|
|
174
|
+
when String then package.serialize(target)
|
|
175
|
+
when Pathname then package.serialize(target.to_s)
|
|
176
|
+
when IO, StringIO then target.write(package.to_stream.read)
|
|
177
|
+
else
|
|
178
|
+
raise ArgumentError, "unsupported write target: #{target.inspect}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
BuiltSheet = Struct.new(:sheet, :entities, :type, keyword_init: true)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|