etna 0.1.19 → 0.1.20
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 +4 -4
- data/etna.completion +78 -3
- data/lib/commands.rb +39 -4
- data/lib/etna.rb +1 -0
- data/lib/etna/clients/magma/formatting/models_csv.rb +280 -271
- data/lib/etna/clients/magma/models.rb +51 -0
- data/lib/etna/clients/magma/workflows/add_project_models_workflow.rb +8 -19
- data/lib/etna/clients/magma/workflows/create_project_workflow.rb +20 -14
- data/lib/etna/clients/magma/workflows/crud_workflow.rb +2 -2
- data/lib/etna/clients/magma/workflows/json_validators.rb +8 -3
- data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +1 -1
- data/lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb +75 -7
- data/lib/etna/csvs.rb +159 -0
- data/lib/etna/errors.rb +6 -0
- data/lib/etna/logger.rb +9 -1
- data/lib/etna/spec/vcr.rb +1 -0
- data/lib/helpers.rb +9 -6
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66704260fa8f715e91ccb8469029e93b2b0f4574af76f2700dd7001b0d7e3bbe
|
4
|
+
data.tar.gz: d16a63b2302f4933741eaade615cd001f615de2ba4bd67774b0c77785a920ace
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23158f8c57ebb498b2055354d05ccada416b8d74c4b5b9e894d261467ff28591b757bfef9e6ea3d37016c465a68bdb179a76621c23a861d4247f560a84925d58
|
7
|
+
data.tar.gz: 4fb492dae70d2ae361e6270b7d8178f8544ecbd388e09721a3550246ea17a2f99aea1ec466f783cdfc592d80e502a320ce17015d44757039266b4f4394e0df28
|
data/etna.completion
CHANGED
@@ -136,13 +136,87 @@ all_flag_completion_names="$all_flag_completion_names "
|
|
136
136
|
string_flag_completion_names="$string_flag_completion_names "
|
137
137
|
while [[ "$#" != "0" ]]; do
|
138
138
|
if [[ "$#" == "1" ]]; then
|
139
|
-
all_completion_names="help update_from_csv"
|
139
|
+
all_completion_names="create_file_linking_csv help update_from_csv"
|
140
140
|
all_completion_names="$all_completion_names $all_flag_completion_names"
|
141
141
|
if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
|
142
142
|
return
|
143
143
|
fi
|
144
144
|
COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
|
145
145
|
return
|
146
|
+
elif [[ "$1" == "create_file_linking_csv" ]]; then
|
147
|
+
shift
|
148
|
+
if [[ "$#" == "1" ]]; then
|
149
|
+
all_completion_names="__project_name__"
|
150
|
+
if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
|
151
|
+
return
|
152
|
+
fi
|
153
|
+
COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
|
154
|
+
return
|
155
|
+
fi
|
156
|
+
shift
|
157
|
+
if [[ "$#" == "1" ]]; then
|
158
|
+
all_completion_names="__bucket_name__"
|
159
|
+
if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
|
160
|
+
return
|
161
|
+
fi
|
162
|
+
COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
|
163
|
+
return
|
164
|
+
fi
|
165
|
+
shift
|
166
|
+
if [[ "$#" == "1" ]]; then
|
167
|
+
all_completion_names="__attribute_name__"
|
168
|
+
if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
|
169
|
+
return
|
170
|
+
fi
|
171
|
+
COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
|
172
|
+
return
|
173
|
+
fi
|
174
|
+
shift
|
175
|
+
if [[ "$#" == "1" ]]; then
|
176
|
+
all_completion_names="__extension__"
|
177
|
+
if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
|
178
|
+
return
|
179
|
+
fi
|
180
|
+
COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
|
181
|
+
return
|
182
|
+
fi
|
183
|
+
shift
|
184
|
+
all_flag_completion_names="$all_flag_completion_names --collection --file --regex --folder "
|
185
|
+
string_flag_completion_names="$string_flag_completion_names --file --regex --folder "
|
186
|
+
declare _completions_for_file="__file__"
|
187
|
+
declare _completions_for_regex="__regex__"
|
188
|
+
declare _completions_for_folder="__folder__"
|
189
|
+
while [[ "$#" != "0" ]]; do
|
190
|
+
if [[ "$#" == "1" ]]; then
|
191
|
+
all_completion_names=""
|
192
|
+
all_completion_names="$all_completion_names $all_flag_completion_names"
|
193
|
+
if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
|
194
|
+
return
|
195
|
+
fi
|
196
|
+
COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
|
197
|
+
return
|
198
|
+
elif [[ -z "$(echo $all_flag_completion_names | xargs)" ]]; then
|
199
|
+
return
|
200
|
+
elif [[ "$all_flag_completion_names" =~ $1\ ]]; then
|
201
|
+
all_flag_completion_names="${all_flag_completion_names//$1\ /}"
|
202
|
+
a=$1
|
203
|
+
shift
|
204
|
+
if [[ "$string_flag_completion_names" =~ $a\ ]]; then
|
205
|
+
if [[ "$#" == "1" ]]; then
|
206
|
+
a="${a//--/}"
|
207
|
+
a="${a//-/_}"
|
208
|
+
i="_completions_for_$a"
|
209
|
+
all_completion_names="${!i}"
|
210
|
+
COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
|
211
|
+
return
|
212
|
+
fi
|
213
|
+
shift
|
214
|
+
fi
|
215
|
+
else
|
216
|
+
return
|
217
|
+
fi
|
218
|
+
done
|
219
|
+
return
|
146
220
|
elif [[ "$1" == "help" ]]; then
|
147
221
|
shift
|
148
222
|
all_flag_completion_names="$all_flag_completion_names "
|
@@ -207,8 +281,9 @@ COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
|
|
207
281
|
return
|
208
282
|
fi
|
209
283
|
shift
|
210
|
-
all_flag_completion_names="$all_flag_completion_names
|
211
|
-
string_flag_completion_names="$string_flag_completion_names
|
284
|
+
all_flag_completion_names="$all_flag_completion_names --json-values --hole-value "
|
285
|
+
string_flag_completion_names="$string_flag_completion_names --hole-value "
|
286
|
+
declare _completions_for_hole_value="__hole_value__"
|
212
287
|
while [[ "$#" != "0" ]]; do
|
213
288
|
if [[ "$#" == "1" ]]; then
|
214
289
|
all_completion_names=""
|
data/lib/commands.rb
CHANGED
@@ -56,7 +56,7 @@ class EtnaApp
|
|
56
56
|
def execute(host, ignore_ssl: false)
|
57
57
|
polyphemus_client ||= Etna::Clients::Polyphemus.new(
|
58
58
|
host: host,
|
59
|
-
token: token,
|
59
|
+
token: token(ignore_environment: true),
|
60
60
|
ignore_ssl: ignore_ssl)
|
61
61
|
workflow = Etna::Clients::Polyphemus::SetConfigurationWorkflow.new(
|
62
62
|
polyphemus_client: polyphemus_client,
|
@@ -143,7 +143,7 @@ class EtnaApp
|
|
143
143
|
tf = Tempfile.new
|
144
144
|
|
145
145
|
begin
|
146
|
-
File.open(tf.path, 'wb') { |f| workflow.write_models_template_csv(
|
146
|
+
File.open(tf.path, 'wb') { |f| workflow.write_models_template_csv(project_name, target_model, io: f) }
|
147
147
|
FileUtils.cp(tf.path, file)
|
148
148
|
ensure
|
149
149
|
tf.close!
|
@@ -247,23 +247,58 @@ class EtnaApp
|
|
247
247
|
include WithEtnaClients
|
248
248
|
include WithLogger
|
249
249
|
|
250
|
+
boolean_flags << '--json-values'
|
251
|
+
string_flags << '--hole-value'
|
252
|
+
|
250
253
|
def magma_crud
|
251
254
|
@magma_crud ||= Etna::Clients::Magma::MagmaCrudWorkflow.new(
|
252
255
|
magma_client: magma_client,
|
253
256
|
project_name: @project_name)
|
254
257
|
end
|
255
258
|
|
256
|
-
def execute(project_name, model_name, filepath)
|
259
|
+
def execute(project_name, model_name, filepath, hole_value: '_', json_values: false)
|
257
260
|
@project_name = project_name
|
258
261
|
|
259
262
|
update_attributes_workflow = Etna::Clients::Magma::UpdateAttributesFromCsvWorkflowSingleModel.new(
|
260
263
|
magma_crud: magma_crud,
|
261
264
|
project_name: project_name,
|
262
265
|
model_name: model_name,
|
263
|
-
filepath: filepath
|
266
|
+
filepath: filepath,
|
267
|
+
hole_value: hole_value,
|
268
|
+
json_values: json_values)
|
264
269
|
update_attributes_workflow.update_attributes
|
265
270
|
end
|
266
271
|
end
|
272
|
+
|
273
|
+
class CreateFileLinkingCsv < Etna::Command
|
274
|
+
include WithEtnaClients
|
275
|
+
|
276
|
+
string_flags << '--file'
|
277
|
+
string_flags << '--regex'
|
278
|
+
string_flags << '--folder'
|
279
|
+
boolean_flags << '--collection'
|
280
|
+
|
281
|
+
def execute(project_name, bucket_name, attribute_name, extension, collection: false, regex: "**/*/(?<identifier>.+)\\.#{extension}$", folder: "", file: "#{project_name}_#{attribute_name}.csv")
|
282
|
+
if folder.start_with?("/")
|
283
|
+
folder = folder.slice(1..-1)
|
284
|
+
end
|
285
|
+
|
286
|
+
regex = Regexp.new(regex)
|
287
|
+
|
288
|
+
workflow = Etna::Clients::Magma::SimpleFileLinkingWorkflow.new(
|
289
|
+
metis_client: metis_client,
|
290
|
+
project_name: project_name,
|
291
|
+
bucket_name: bucket_name,
|
292
|
+
folder: folder,
|
293
|
+
extension: extension,
|
294
|
+
attribute_name: attribute_name,
|
295
|
+
regex: regex,
|
296
|
+
file_collection: collection,
|
297
|
+
)
|
298
|
+
|
299
|
+
workflow.write_csv_io(filename: file)
|
300
|
+
end
|
301
|
+
end
|
267
302
|
end
|
268
303
|
end
|
269
304
|
end
|
data/lib/etna.rb
CHANGED
@@ -1,342 +1,351 @@
|
|
1
1
|
require 'ostruct'
|
2
|
+
require_relative '../../../csvs'
|
2
3
|
|
3
4
|
module Etna
|
4
5
|
module Clients
|
5
6
|
class Magma
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
:new_attribute_name,
|
13
|
-
:attribute_type,
|
14
|
-
:link_model_name,
|
15
|
-
:description,
|
16
|
-
:display_name,
|
17
|
-
:format_hint,
|
18
|
-
:restricted,
|
19
|
-
:read_only,
|
20
|
-
:options,
|
21
|
-
:match,
|
22
|
-
:attribute_group,
|
23
|
-
:hidden,
|
24
|
-
:unique,
|
25
|
-
:matrix_constant,
|
26
|
-
]
|
27
|
-
|
28
|
-
COLUMN_AS_BOOLEAN = -> (s) { ['true', 't', 'y', 'yes'].include?(s.downcase) }
|
29
|
-
|
30
|
-
COLUMNS_TO_ATTRIBUTES = {
|
31
|
-
attribute_name: :attribute_name,
|
32
|
-
attribute_type: [:attribute_type, -> (s) { AttributeType.new(s) }],
|
33
|
-
link_model_name: :link_model_name,
|
34
|
-
description: :desc,
|
35
|
-
display_name: :display_name,
|
36
|
-
format_hint: :format_hint,
|
37
|
-
restricted: [:restricted, COLUMN_AS_BOOLEAN],
|
38
|
-
read_only: [:read_only, COLUMN_AS_BOOLEAN],
|
39
|
-
options: [:validation, -> (s) { {"type" => "Array", "value" => s.split(',').map(&:strip)} }],
|
40
|
-
match: [:validation, -> (s) { {"type" => "Regexp", "value" => Regexp.new(s).source} }],
|
41
|
-
attribute_group: :attribute_group,
|
42
|
-
hidden: [:hidden, COLUMN_AS_BOOLEAN],
|
43
|
-
unique: [:unique, COLUMN_AS_BOOLEAN],
|
44
|
-
}
|
45
|
-
|
46
|
-
def self.apply_csv_row(changeset = ModelsChangeset.new, row = {}, &err_block)
|
47
|
-
changeset.tap do
|
48
|
-
models = changeset.models
|
7
|
+
module ModelsCsv
|
8
|
+
module Prettify
|
9
|
+
def prettify(name)
|
10
|
+
name.split('_').map(&:capitalize).join(' ')
|
11
|
+
end
|
12
|
+
end
|
49
13
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
14
|
+
class Exporter < Etna::CsvExporter
|
15
|
+
include Prettify
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
super([
|
19
|
+
:comments,
|
20
|
+
:model_name, :identifier, :parent_model_name, :parent_link_type,
|
21
|
+
:attribute_name,
|
22
|
+
:new_attribute_name,
|
23
|
+
:attribute_type,
|
24
|
+
:link_model_name,
|
25
|
+
:description,
|
26
|
+
:display_name,
|
27
|
+
:format_hint,
|
28
|
+
:restricted,
|
29
|
+
:read_only,
|
30
|
+
:options,
|
31
|
+
:match,
|
32
|
+
:attribute_group,
|
33
|
+
:hidden,
|
34
|
+
:unique,
|
35
|
+
:matrix_constant,
|
36
|
+
])
|
37
|
+
end
|
58
38
|
|
59
|
-
|
60
|
-
|
61
|
-
models
|
62
|
-
else
|
63
|
-
last_model = changeset.last_model_key
|
64
|
-
if last_model.nil?
|
65
|
-
nil
|
66
|
-
else
|
67
|
-
models.model(last_model).build_template
|
68
|
-
end
|
39
|
+
def write_models(models, model_keys = models.model_keys.sort, output_io: nil, filename: nil)
|
40
|
+
with_row_writeable(filename: filename, output_io: output_io) do |row_writeable|
|
41
|
+
write_model_rows(models, model_keys, row_writeable)
|
69
42
|
end
|
43
|
+
end
|
70
44
|
|
71
|
-
|
72
|
-
|
73
|
-
yield "Found identifier #{identifier} but no model_name had been given!"
|
74
|
-
next
|
75
|
-
end
|
76
|
-
template.identifier = identifier
|
77
|
-
end
|
45
|
+
def write_model_rows(models, model_keys, row_writeable)
|
46
|
+
ensure_parents(models, model_keys, row_writeable)
|
78
47
|
|
79
|
-
|
80
|
-
|
81
|
-
end
|
48
|
+
matrix_constants = {}
|
49
|
+
model_keys.each { |model_name| each_model_row(models, model_name, matrix_constants, row_writeable) }
|
82
50
|
|
83
|
-
|
84
|
-
|
51
|
+
matrix_constants.each do |digest, options|
|
52
|
+
row_writeable.write
|
53
|
+
row_writeable.write(matrix_constant: COPY_OPTIONS_SENTINEL + digest)
|
54
|
+
options.each do |option|
|
55
|
+
row_writeable.write(matrix_constant: option)
|
56
|
+
end
|
85
57
|
end
|
58
|
+
end
|
86
59
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
60
|
+
def ensure_parents(models, model_keys, row_writeable)
|
61
|
+
q = model_keys.dup
|
62
|
+
seen = Set.new
|
63
|
+
|
64
|
+
until q.empty?
|
65
|
+
model_key = q.shift
|
66
|
+
next if model_key.nil?
|
67
|
+
next if seen.include?(model_key)
|
68
|
+
seen.add(model_key)
|
69
|
+
|
70
|
+
# For models that are only part of the trunk, but not of the tree of model_keys,
|
71
|
+
# we still need their basic information (identifier / parent) for validation and for
|
72
|
+
# potentially creating the required tree dependencies to connect it to a remote tree.
|
73
|
+
unless model_keys.include?(model_key)
|
74
|
+
each_model_trunk_row(models, model_key, row_writeable)
|
91
75
|
end
|
76
|
+
|
77
|
+
q.push(*models.model(model_key).template.all_linked_model_names)
|
92
78
|
end
|
93
79
|
end
|
94
|
-
end
|
95
80
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
81
|
+
def each_model_trunk_row(models, model_name, row_writeable)
|
82
|
+
return unless (model = models.model(model_name))
|
83
|
+
|
84
|
+
# Empty link for better visual separation
|
85
|
+
row_writeable.write
|
86
|
+
row_writeable.write(model_name: model_name)
|
87
|
+
|
88
|
+
if !model.template.parent.nil?
|
89
|
+
parent_model = models.model(model.template.parent)
|
90
|
+
reciprocal = models.find_reciprocal(model: model, link_model: parent_model)
|
91
|
+
|
92
|
+
row_writeable.write(
|
93
|
+
identifier: model.template.identifier,
|
94
|
+
parent_model_name: model.template.parent,
|
95
|
+
parent_link_type: reciprocal.attribute_type.to_s
|
96
|
+
)
|
97
|
+
else
|
98
|
+
row_writeable.write(
|
99
|
+
identifier: model.template.identifier,
|
100
|
+
)
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
104
|
-
|
105
|
-
|
104
|
+
def each_model_row(models, model_name, matrix_constants, row_writeable)
|
105
|
+
return unless (model = models.model(model_name))
|
106
106
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
107
|
+
each_model_trunk_row(models, model_name, row_writeable)
|
108
|
+
model.template.attributes.all.each do |attribute|
|
109
|
+
each_attribute_row(models, model, attribute, matrix_constants, row_writeable)
|
110
|
+
end
|
111
111
|
end
|
112
112
|
|
113
|
-
|
114
|
-
|
115
|
-
|
113
|
+
def each_attribute_row(models, model, attribute, matrix_constants, row_writeable)
|
114
|
+
if attribute.attribute_type == Etna::Clients::Magma::AttributeType::IDENTIFIER
|
115
|
+
# Identifiers for models whose parent link type ends up being a table are non configurable, so we don't
|
116
|
+
# want to include them in the CSV.
|
117
|
+
if models.find_reciprocal(model: model, link_attribute_name: model.template.parent)&.attribute_type == Etna::Clients::Magma::AttributeType::TABLE
|
118
|
+
return
|
119
|
+
end
|
120
|
+
else
|
121
|
+
return unless Etna::Clients::Magma::AttributeValidator.valid_add_row_attribute_types.include?(attribute.attribute_type)
|
122
|
+
end
|
116
123
|
|
117
|
-
|
124
|
+
options = attribute.options&.join(', ')
|
125
|
+
if attribute.attribute_type == Etna::Clients::Magma::AttributeType::MATRIX
|
126
|
+
# Matrix attribute validations are massive, and functional. For now, we don't support showing and editing
|
127
|
+
# them inline with this spreadsheet. In the future, I think we should possibly introduce the concept of
|
128
|
+
# CONSTANTS or Matrix Types that are managed separately.
|
129
|
+
options = options || ''
|
130
|
+
digest = Digest::MD5.hexdigest(options)
|
131
|
+
matrix_constants[digest] ||= options.split(',').map(&:strip)
|
118
132
|
|
119
|
-
|
120
|
-
|
121
|
-
parent_att.attribute_type = AttributeType::PARENT
|
122
|
-
parent_att.link_model_name = parent_model_name
|
123
|
-
parent_att.desc = self.prettify(parent_model_name)
|
124
|
-
parent_att.display_name = self.prettify(parent_model_name)
|
125
|
-
end
|
126
|
-
end
|
133
|
+
options = COPY_OPTIONS_SENTINEL + digest
|
134
|
+
end
|
127
135
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
136
|
+
row_writeable.write(
|
137
|
+
attribute_name: attribute.name,
|
138
|
+
attribute_type: attribute.attribute_type,
|
139
|
+
link_model_name: attribute.link_model_name,
|
140
|
+
reciprocal_link_type: models.find_reciprocal(model: model, attribute: attribute)&.attribute_type,
|
141
|
+
description: attribute.desc,
|
142
|
+
display_name: attribute.display_name,
|
143
|
+
match: attribute.match,
|
144
|
+
format_hint: attribute.format_hint,
|
145
|
+
restricted: attribute.restricted,
|
146
|
+
read_only: attribute.read_only,
|
147
|
+
options: options,
|
148
|
+
attribute_group: attribute.attribute_group,
|
149
|
+
hidden: attribute.hidden,
|
150
|
+
unique: attribute.unique,
|
151
|
+
)
|
132
152
|
end
|
153
|
+
end
|
133
154
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
end
|
155
|
+
class ModelsChangeset < Struct.new(:models, :renames, :matrix_constants, keyword_init: true)
|
156
|
+
def initialize(*args)
|
157
|
+
super
|
138
158
|
|
139
|
-
|
140
|
-
|
141
|
-
|
159
|
+
self.models ||= Models.new
|
160
|
+
self.renames ||= {}
|
161
|
+
self.matrix_constants ||= {}
|
142
162
|
end
|
143
163
|
|
144
|
-
|
145
|
-
|
164
|
+
def build_renames(model_name)
|
165
|
+
renames[model_name] ||= {}
|
146
166
|
end
|
167
|
+
end
|
147
168
|
|
148
|
-
|
149
|
-
parent_model_attribute_by_model_name = parent_model.build_template.build_attributes.attribute(template.name)
|
150
|
-
if parent_model_attribute_by_model_name && !reciprocal
|
151
|
-
yield "Model #{template.parent} is linked as a parent to #{template.name}, but it already has an attribute named #{template.name} #{parent_model_attribute_by_model_name.raw}." if block_given?
|
152
|
-
end
|
169
|
+
COPY_OPTIONS_SENTINEL = '$copy_from_original$'
|
153
170
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
171
|
+
class Importer < Etna::CsvImporter
|
172
|
+
include Prettify
|
173
|
+
|
174
|
+
# Columns of the row, _after format_row is called_, that should be applied to an attribute.
|
175
|
+
# This should line up with the attribute names _on the model itself_.
|
176
|
+
ATTRIBUTE_ROW_ENTRIES = [
|
177
|
+
:attribute_type,
|
178
|
+
:link_model_name, :desc,
|
179
|
+
:display_name, :format_hint,
|
180
|
+
:restricted, :read_only,
|
181
|
+
:validation, :attribute_group,
|
182
|
+
:hidden, :unique,
|
183
|
+
]
|
184
|
+
|
185
|
+
def initialize
|
186
|
+
super(&method(:format_row))
|
161
187
|
end
|
162
|
-
end
|
163
188
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
189
|
+
def format_row(row)
|
190
|
+
replace_row_column(row, :attribute_type) { |s| AttributeType.new(s) }
|
191
|
+
replace_row_column(row, :desc) { row.delete(:description) }
|
192
|
+
replace_row_column(row, :restricted, &COLUMN_AS_BOOLEAN)
|
193
|
+
replace_row_column(row, :read_only, &COLUMN_AS_BOOLEAN)
|
194
|
+
replace_row_column(row, :options) { |s| {"type" => "Array", "value" => s.split(',').map(&:strip)} }
|
195
|
+
replace_row_column(row, :match) { |s| {"type" => "Regexp", "value" => Regexp.new(s).source} }
|
196
|
+
replace_row_column(row, :validation) { row[:options] || row[:match] }
|
197
|
+
replace_row_column(row, :hidden, &COLUMN_AS_BOOLEAN)
|
198
|
+
replace_row_column(row, :unique, &COLUMN_AS_BOOLEAN)
|
168
199
|
end
|
169
200
|
|
170
|
-
|
171
|
-
|
172
|
-
if existing_attribute
|
173
|
-
if existing_attribute.attribute_type != AttributeType::COLLECTION
|
174
|
-
yield "Attribute #{attribute_name} of model #{template.name} has duplicate definitions!" if block_given?
|
175
|
-
end
|
201
|
+
def process_row(row_processor, changeset)
|
202
|
+
models = changeset.models
|
176
203
|
|
177
|
-
|
204
|
+
process_matrix_constants(changeset, row_processor)
|
205
|
+
process_model_config(models, row_processor)
|
206
|
+
process_parent_config(models, row_processor)
|
207
|
+
process_attribute_properties(changeset, row_processor)
|
178
208
|
end
|
179
209
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
210
|
+
def prepare_changeset(filename: nil, input_io: nil, &validation_err_block)
|
211
|
+
ModelsChangeset.new.tap do |changeset|
|
212
|
+
context = {}
|
213
|
+
each_csv_row(filename: filename, input_io: input_io) do |row, lineno|
|
214
|
+
p = NestedRowProcessor.new(row, lineno, context)
|
215
|
+
process_row(p, changeset)
|
186
216
|
end
|
187
217
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
end
|
194
|
-
|
195
|
-
if att.attribute_type == AttributeType::LINK && models.find_reciprocal(model_name: template.name, attribute: att).nil?
|
196
|
-
models.build_model(att.link_model_name).build_template.build_attributes.build_attribute(template.name).tap do |rec_att|
|
197
|
-
rec_att.attribute_name = template.name
|
198
|
-
rec_att.display_name = self.prettify(template.name)
|
199
|
-
rec_att.desc = self.prettify(template.name)
|
200
|
-
rec_att.attribute_type = AttributeType::COLLECTION
|
201
|
-
rec_att.link_model_name = template.name
|
218
|
+
# After configuring attributes, set defaults for certain attribute properties.
|
219
|
+
changeset.models.all.each do |model|
|
220
|
+
model.template.attributes.all.each do |attribute|
|
221
|
+
attribute.set_field_defaults!
|
222
|
+
end
|
202
223
|
end
|
203
224
|
end
|
204
|
-
|
205
|
-
|
225
|
+
rescue ImportError => e
|
226
|
+
validation_err_block.call(e.message)
|
206
227
|
end
|
207
|
-
end
|
208
228
|
|
209
|
-
def self.prettify(name)
|
210
|
-
name.split('_').map(&:capitalize).join(' ')
|
211
|
-
end
|
212
229
|
|
213
|
-
|
214
|
-
c = row[col]&.chomp
|
215
|
-
return nil if c&.empty?
|
216
|
-
c
|
217
|
-
end
|
218
|
-
|
219
|
-
def self.each_csv_row(models = Models.new, model_keys = models.model_keys.sort, &block)
|
220
|
-
yield COLUMNS.map(&:to_s)
|
221
|
-
|
222
|
-
self.ensure_parents(models, model_keys, &block)
|
223
|
-
matrix_constants = {}
|
224
|
-
model_keys.each { |model_name| self.each_model_row(models, model_name, matrix_constants, &block) }
|
230
|
+
private
|
225
231
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
options.each do |option|
|
230
|
-
yield row_from_columns(matrix_constant: option)
|
232
|
+
def process_model_config(models, row_processor)
|
233
|
+
row_processor.process(:model_name) do |model_name|
|
234
|
+
models.build_model(model_name).build_template.tap { |t| t.name = model_name }
|
231
235
|
end
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
def self.ensure_parents(models, model_keys, &block)
|
236
|
-
q = model_keys.dup
|
237
|
-
seen = Set.new
|
238
|
-
|
239
|
-
until q.empty?
|
240
|
-
model_key = q.shift
|
241
|
-
next if model_key.nil?
|
242
|
-
next if seen.include?(model_key)
|
243
|
-
seen.add(model_key)
|
244
|
-
|
245
|
-
# For models that are only part of the trunk, but not of the tree of model_keys,
|
246
|
-
# we still need their basic information (identifier / parent) for validation and for
|
247
|
-
# potentially creating the required tree dependencies to connect it to a remote tree.
|
248
|
-
unless model_keys.include?(model_key)
|
249
|
-
self.each_model_trunk_row(models, model_key, &block)
|
236
|
+
row_processor.process(:identifier, :model_name) do |identifier, template|
|
237
|
+
template.identifier = identifier
|
250
238
|
end
|
251
|
-
|
252
|
-
q.push(*models.model(model_key).template.all_linked_model_names)
|
253
239
|
end
|
254
|
-
end
|
255
240
|
|
256
|
-
|
257
|
-
|
241
|
+
def process_parent_config(models, row_processor)
|
242
|
+
row_processor.process(:parent_model_name, :model_name) do |parent_model_name, template|
|
243
|
+
if template.parent && !template.parent.empty? && template.parent != parent_model_name
|
244
|
+
raise ImportError.new("Model #{template.name} was provided multiple parent_model_names: #{template.parent} and #{parent_model_name}")
|
245
|
+
end
|
258
246
|
|
259
|
-
|
260
|
-
yield row_from_columns
|
261
|
-
yield row_from_columns(model_name: model_name)
|
247
|
+
template.parent = parent_model_name
|
262
248
|
|
263
|
-
|
264
|
-
|
265
|
-
|
249
|
+
template.build_attributes.build_attribute(template.parent).tap do |parent_att|
|
250
|
+
parent_att.name = parent_att.attribute_name = parent_model_name
|
251
|
+
parent_att.attribute_type = Etna::Clients::Magma::AttributeType::PARENT
|
252
|
+
parent_att.link_model_name = parent_model_name
|
253
|
+
parent_att.desc = prettify(parent_model_name)
|
254
|
+
parent_att.display_name = prettify(parent_model_name)
|
255
|
+
end
|
256
|
+
end
|
266
257
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
else
|
273
|
-
yield row_from_columns(
|
274
|
-
identifier: model.template.identifier,
|
275
|
-
)
|
276
|
-
end
|
277
|
-
end
|
258
|
+
row_processor.process(:parent_link_type, :model_name, :parent_model_name) do |parent_link_type, template|
|
259
|
+
reciprocal = models.find_reciprocal(model_name: template.name, link_attribute_name: template.parent)
|
260
|
+
if reciprocal && reciprocal.attribute_type.to_s != parent_link_type
|
261
|
+
raise ImportError.new("Model #{template.name} was provided multiple parent_link_types: #{reciprocal.attribute_type} and #{parent_link_type}")
|
262
|
+
end
|
278
263
|
|
279
|
-
|
280
|
-
|
264
|
+
if reciprocal && reciprocal.attribute_name != template.name
|
265
|
+
raise ImportError.new("Model #{template.name} is linked to #{template.parent}, but the reciprocal link is misnamed as '#{reciprocal.attribute_name}'.")
|
266
|
+
end
|
281
267
|
|
282
|
-
|
283
|
-
|
284
|
-
|
268
|
+
models.build_model(template.parent).tap do |parent_model|
|
269
|
+
parent_model_attribute_by_model_name = parent_model.build_template.build_attributes.attribute(template.name)
|
270
|
+
if parent_model_attribute_by_model_name && !reciprocal
|
271
|
+
raise ImportError.new("Model #{template.parent} is linked as a parent to #{template.name}, but it already has an attribute named #{template.name} #{parent_model_attribute_by_model_name.raw}.")
|
272
|
+
end
|
273
|
+
|
274
|
+
parent_model.build_template.build_attributes.build_attribute(template.name).tap do |attr|
|
275
|
+
attr.attribute_name = attr.name = template.name
|
276
|
+
attr.attribute_type = parent_link_type
|
277
|
+
attr.link_model_name = template.name
|
278
|
+
attr.desc = prettify(template.name)
|
279
|
+
attr.display_name = prettify(template.name)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
285
283
|
end
|
286
|
-
end
|
287
284
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
285
|
+
def process_matrix_constants(changeset, row_processor)
|
286
|
+
row_processor.process(:matrix_constant) do |matrix_constant|
|
287
|
+
# Lines that start with the sentinel are a distinct 'group'
|
288
|
+
if matrix_constant.start_with?(COPY_OPTIONS_SENTINEL)
|
289
|
+
matrix_constant = matrix_constant.slice((COPY_OPTIONS_SENTINEL.length)..-1)
|
290
|
+
changeset.matrix_constants[matrix_constant] = []
|
291
|
+
else
|
292
|
+
# Until the sentinel is seen again, all further entries belong to the set.
|
293
|
+
changeset.matrix_constants[changeset.matrix_constants.keys.last] << matrix_constant
|
294
|
+
end
|
295
|
+
|
296
|
+
matrix_constant
|
294
297
|
end
|
295
|
-
else
|
296
|
-
return unless AttributeValidator.valid_add_row_attribute_types.include?(attribute.attribute_type)
|
297
298
|
end
|
298
299
|
|
299
|
-
|
300
|
-
|
301
|
-
# Matrix attribute validations are massive, and functional. For now, we don't support showing and editing
|
302
|
-
# them inline with this spreadsheet. In the future, I think we should possibly introduce the concept of
|
303
|
-
# CONSTANTS or Matrix Types that are managed separately.
|
304
|
-
options = options || ''
|
305
|
-
digest = Digest::MD5.hexdigest(options)
|
306
|
-
matrix_constants[digest] ||= COLUMNS_TO_ATTRIBUTES[:options][1].call(options)["value"]
|
300
|
+
def process_attribute_properties(changeset, row_processor)
|
301
|
+
models = changeset.models
|
307
302
|
|
308
|
-
|
309
|
-
|
303
|
+
row_processor.process(:attribute_name, :model_name) do |attribute_name, template|
|
304
|
+
attributes = template.build_attributes
|
310
305
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
description: attribute.desc,
|
317
|
-
display_name: attribute.display_name,
|
318
|
-
match: attribute.match,
|
319
|
-
format_hint: attribute.format_hint,
|
320
|
-
restricted: attribute.restricted,
|
321
|
-
read_only: attribute.read_only,
|
322
|
-
options: options,
|
323
|
-
attribute_group: attribute.attribute_group,
|
324
|
-
hidden: attribute.hidden,
|
325
|
-
unique: attribute.unique,
|
326
|
-
)
|
327
|
-
end
|
306
|
+
existing_attribute = attributes.attribute(attribute_name)
|
307
|
+
if existing_attribute
|
308
|
+
if existing_attribute.attribute_type != Etna::Clients::Magma::AttributeType::COLLECTION
|
309
|
+
raise ImportError.new("Attribute #{attribute_name} of model #{template.name} has duplicate definitions!")
|
310
|
+
end
|
328
311
|
|
329
|
-
|
330
|
-
|
331
|
-
|
312
|
+
# Clear its definition; implicit attributes are overwritten by explicit definitions if they exist.
|
313
|
+
attributes.raw.delete(attribute_name)
|
314
|
+
end
|
332
315
|
|
333
|
-
|
334
|
-
|
335
|
-
super
|
316
|
+
attributes.build_attribute(attribute_name).tap { |att| att.name = att.attribute_name = attribute_name }
|
317
|
+
end
|
336
318
|
|
337
|
-
|
338
|
-
|
339
|
-
|
319
|
+
row_processor.process(:new_attribute_name, :attribute_name, :model_name) do |new_attribute_name, att, template|
|
320
|
+
renames = changeset.build_renames(template.name)
|
321
|
+
if renames.include?(att.name) && renames[att.name] != new_attribute_name
|
322
|
+
raise ImportError.new("Found multiple new_attribute_name values for #{template.name}'s #{att.name}': #{new_attribute_name} or #{renames[att.name]}?")
|
323
|
+
end
|
324
|
+
|
325
|
+
renames[att.attribute_name] = new_attribute_name
|
326
|
+
end
|
327
|
+
|
328
|
+
ATTRIBUTE_ROW_ENTRIES.each do |prop_name|
|
329
|
+
row_processor.process(prop_name, :model_name, :attribute_name) do |value, template, att|
|
330
|
+
if att.raw.include?(prop_name.to_s)
|
331
|
+
raise ImportError.new("Value for #{prop_name} on attribute #{att.attribute_name} has duplicate definitions!")
|
332
|
+
end
|
333
|
+
|
334
|
+
att.send(:"#{prop_name}=", value)
|
335
|
+
|
336
|
+
if att.attribute_type && att.link_model_name
|
337
|
+
if att.attribute_type == Etna::Clients::Magma::AttributeType::LINK && models.find_reciprocal(model_name: template.name, attribute: att).nil?
|
338
|
+
models.build_model(att.link_model_name).build_template.build_attributes.build_attribute(template.name).tap do |rec_att|
|
339
|
+
rec_att.attribute_name = rec_att.name = template.name
|
340
|
+
rec_att.display_name = prettify(template.name)
|
341
|
+
rec_att.desc = prettify(template.name)
|
342
|
+
rec_att.attribute_type = Etna::Clients::Magma::AttributeType::COLLECTION
|
343
|
+
rec_att.link_model_name = template.name
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
340
349
|
end
|
341
350
|
end
|
342
351
|
end
|