etna 0.1.12 → 0.1.18

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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/bin/etna +63 -0
  3. data/etna.completion +926 -0
  4. data/etna_app.completion +133 -0
  5. data/ext/completions/extconf.rb +20 -0
  6. data/lib/commands.rb +368 -0
  7. data/lib/etna.rb +6 -0
  8. data/lib/etna/application.rb +46 -22
  9. data/lib/etna/client.rb +82 -48
  10. data/lib/etna/clients.rb +4 -0
  11. data/lib/etna/clients/enum.rb +9 -0
  12. data/lib/etna/clients/janus.rb +2 -0
  13. data/lib/etna/clients/janus/client.rb +73 -0
  14. data/lib/etna/clients/janus/models.rb +78 -0
  15. data/lib/etna/clients/magma.rb +4 -0
  16. data/lib/etna/clients/magma/client.rb +80 -0
  17. data/lib/etna/clients/magma/formatting.rb +1 -0
  18. data/lib/etna/clients/magma/formatting/models_csv.rb +345 -0
  19. data/lib/etna/clients/magma/models.rb +579 -0
  20. data/lib/etna/clients/magma/workflows.rb +10 -0
  21. data/lib/etna/clients/magma/workflows/add_project_models_workflow.rb +78 -0
  22. data/lib/etna/clients/magma/workflows/attribute_actions_from_json_workflow.rb +62 -0
  23. data/lib/etna/clients/magma/workflows/create_project_workflow.rb +117 -0
  24. data/lib/etna/clients/magma/workflows/crud_workflow.rb +85 -0
  25. data/lib/etna/clients/magma/workflows/ensure_containing_record_workflow.rb +44 -0
  26. data/lib/etna/clients/magma/workflows/file_attributes_blank_workflow.rb +68 -0
  27. data/lib/etna/clients/magma/workflows/file_linking_workflow.rb +115 -0
  28. data/lib/etna/clients/magma/workflows/json_converters.rb +81 -0
  29. data/lib/etna/clients/magma/workflows/json_validators.rb +447 -0
  30. data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +306 -0
  31. data/lib/etna/clients/magma/workflows/record_synchronization_workflow.rb +63 -0
  32. data/lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb +178 -0
  33. data/lib/etna/clients/metis.rb +3 -0
  34. data/lib/etna/clients/metis/client.rb +239 -0
  35. data/lib/etna/clients/metis/models.rb +313 -0
  36. data/lib/etna/clients/metis/workflows.rb +2 -0
  37. data/lib/etna/clients/metis/workflows/metis_download_workflow.rb +37 -0
  38. data/lib/etna/clients/metis/workflows/metis_upload_workflow.rb +137 -0
  39. data/lib/etna/clients/polyphemus.rb +3 -0
  40. data/lib/etna/clients/polyphemus/client.rb +33 -0
  41. data/lib/etna/clients/polyphemus/models.rb +68 -0
  42. data/lib/etna/clients/polyphemus/workflows.rb +1 -0
  43. data/lib/etna/clients/polyphemus/workflows/set_configuration_workflow.rb +47 -0
  44. data/lib/etna/command.rb +243 -5
  45. data/lib/etna/controller.rb +4 -0
  46. data/lib/etna/directed_graph.rb +56 -0
  47. data/lib/etna/environment_scoped.rb +19 -0
  48. data/lib/etna/generate_autocompletion_script.rb +130 -0
  49. data/lib/etna/hmac.rb +1 -0
  50. data/lib/etna/json_serializable_struct.rb +37 -0
  51. data/lib/etna/logger.rb +15 -1
  52. data/lib/etna/multipart_serializable_nested_hash.rb +50 -0
  53. data/lib/etna/parse_body.rb +1 -1
  54. data/lib/etna/route.rb +1 -1
  55. data/lib/etna/server.rb +3 -0
  56. data/lib/etna/spec/vcr.rb +98 -0
  57. data/lib/etna/templates/attribute_actions_template.json +43 -0
  58. data/lib/etna/test_auth.rb +4 -2
  59. data/lib/etna/user.rb +11 -1
  60. data/lib/helpers.rb +81 -0
  61. metadata +70 -7
@@ -0,0 +1,306 @@
1
+ require 'ostruct'
2
+
3
+ module Etna
4
+ module Clients
5
+ class Magma
6
+ # Note! These workflows are not perfectly atomic, nor perfectly synchronized due to nature of the backend.
7
+ # These primitives are best effort locally synchronized, but cannot defend the backend or simultaneous
8
+ # system updates.
9
+ class ModelSynchronizationWorkflow < Struct.new(
10
+ :target_client, :source_models, :target_project, :update_block,
11
+ :model_name, :plan_only, :validate, :use_versions, :renames,
12
+ keyword_init: true)
13
+ def target_models
14
+ @target_models ||= begin
15
+ target_client.retrieve(RetrievalRequest.new(project_name: self.target_project, model_name: 'all')).models
16
+ end
17
+ end
18
+
19
+ def planned_actions
20
+ @planned_actions ||= []
21
+ end
22
+
23
+ def queue_update(action)
24
+ if plan_only
25
+ plan_update(action)
26
+ else
27
+ execute_update(action)
28
+ end
29
+ end
30
+
31
+ def execute_planned!
32
+ @planned_actions.each do |action|
33
+ execute_update(action)
34
+ end
35
+ end
36
+
37
+ def execute_update(action)
38
+ update = UpdateModelRequest.new(project_name: self.target_project)
39
+ update_block.call(action) if update_block
40
+ update.add_action(action)
41
+ @target_models = nil
42
+ target_client.update_model(update)
43
+ end
44
+
45
+ def copy_link_into_target(link, reciprocal)
46
+ attr = target_models.build_model(link.model_name).build_template.build_attributes.build_attribute(link.attribute_name)
47
+ attr.attribute_type = link.type
48
+ attr.name = attr.attribute_name = link.attribute_name
49
+ attr.link_model_name = reciprocal.model_name
50
+ end
51
+
52
+ # Applies the given action to the source models and 'plans' its execution.
53
+ def plan_update(action)
54
+ case action
55
+ when UpdateAttributeAction
56
+ attribute_update = target_models.build_model(action.model_name).build_template.build_attributes.build_attribute(action.attribute_name)
57
+ Attribute.copy(action, attribute_update, attributes: Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES)
58
+ when AddAttributeAction
59
+ new_attribute = target_models.build_model(action.model_name).build_template.build_attributes.build_attribute(action.attribute_name)
60
+ Attribute.copy(action, new_attribute)
61
+ when AddLinkAction
62
+ first_link = action.links[0]
63
+ second_link = action.links[1]
64
+ copy_link_into_target(first_link, second_link)
65
+ copy_link_into_target(second_link, first_link)
66
+ when AddModelAction
67
+ template = target_models.build_model(action.model_name).build_template
68
+ template.name = action.model_name
69
+ template.identifier = action.identifier
70
+ template.parent = action.parent_model_name
71
+
72
+ child_link = AddLinkDefinition.new(type: AttributeType::PARENT, model_name: template.name, attribute_name: template.parent)
73
+ parent_link = AddLinkDefinition.new(type: action.parent_link_type, model_name: template.parent, attribute_name: template.name)
74
+
75
+ copy_link_into_target(child_link, parent_link)
76
+ copy_link_into_target(parent_link, child_link)
77
+
78
+ ['created_at', 'updated_at'].each do |time_attr_name|
79
+ template.build_attributes.build_attribute(time_attr_name).tap do |attr|
80
+ attr.attribute_type = Etna::Clients::Magma::AttributeType::DATE_TIME
81
+ attr.attribute_name = time_attr_name
82
+ end
83
+ end
84
+
85
+ if action.parent_link_type != Etna::Clients::Magma::AttributeType::TABLE
86
+ template.build_attributes.build_attribute(template.identifier).tap do |attr|
87
+ attr.attribute_name = template.identifier
88
+ attr.attribute_type = Etna::Clients::Magma::AttributeType::STRING
89
+ end
90
+ end
91
+ when RenameAttributeAction
92
+ attributes = target_models.model(action.model_name).template.attributes
93
+ attributes.raw[action.new_attribute_name] = attributes.raw.delete(action.attribute_name)
94
+ else
95
+ raise "Unexpected plan_action #{action}"
96
+ end
97
+
98
+ planned_actions << action
99
+ end
100
+
101
+
102
+ def self.from_api_source(source_project:, source_client:, **kwds)
103
+ self.new(
104
+ source_models: source_client.retrieve(RetrievalRequest.new(project_name: source_project, model_name: 'all')).models,
105
+ **kwds
106
+ )
107
+ end
108
+
109
+ # Subclass and override when the source <-> target mapping is not 1:1.
110
+ def target_of_source(model_name)
111
+ model_name
112
+ end
113
+
114
+ # Subclass and override when the source <-> target attribute mapping is not 1:1.
115
+ def target_attribute_of_source(model_name, attribute_name)
116
+ attribute_name
117
+ end
118
+
119
+ # Potentially cyclical, protected against re-entry via the seen cache.
120
+ # Establishes the link attributes in a given model graph.
121
+ def ensure_model_tree(model_name, seen = Set.new)
122
+ return unless (source_model = source_models.model(model_name))
123
+ return if seen.include?(model_name)
124
+ seen.add(model_name)
125
+ ensure_model(model_name)
126
+
127
+ attributes = source_model.template.attributes
128
+
129
+ attributes.all.each do |attribute|
130
+ # Don't copy or update parent links. Once a model has been setup with a parent someway.
131
+ unless attribute.attribute_type == AttributeType::PARENT
132
+ if attribute.link_model_name
133
+ ensure_model(attribute.link_model_name)
134
+ ensure_model_link(model_name, attribute.link_model_name, attribute.attribute_name)
135
+ else
136
+ ensure_model_attribute(model_name, attribute.attribute_name)
137
+ end
138
+ end
139
+
140
+ # Even if it's a parent node, however, we still want to cascade the tree expansion to all links.
141
+ unless attribute.link_model_name.nil?
142
+ ensure_model_tree(attribute.link_model_name, seen)
143
+ end
144
+ end
145
+ end
146
+
147
+ def ensure_model_link(model_name, link_model_name, attribute_name)
148
+ return unless (model = source_models.model(model_name))
149
+ return unless (source_attribute = model.template.attributes.attribute(attribute_name))
150
+
151
+ return unless (link_model = source_models.model(link_model_name))
152
+ link_model_attributes = link_model.template.attributes
153
+ reciprocal = link_model_attributes.all.find do |attr|
154
+ attr.link_model_name == model_name
155
+ end
156
+
157
+ target_model_name = target_of_source(model_name)
158
+ target_link_model_name = target_of_source(link_model_name)
159
+
160
+ target_attributes = target_models.model(target_model_name).template.attributes
161
+ return if target_attributes.attribute_keys.include?(target_link_model_name)
162
+
163
+ add_link = AddLinkAction.new
164
+ add_link.links << AddLinkDefinition.new(model_name: target_model_name, attribute_name: target_link_model_name, type: source_attribute.attribute_type)
165
+ add_link.links << AddLinkDefinition.new(model_name: target_link_model_name, attribute_name: reciprocal.attribute_name, type: reciprocal.attribute_type)
166
+
167
+ queue_update(add_link)
168
+ end
169
+
170
+ def ensure_model_attribute(model_name, attribute_name)
171
+ return unless (model = source_models.model(model_name))
172
+ return unless (source_attribute = model.template.attributes.attribute(attribute_name))
173
+
174
+ target_model_name = target_of_source(model_name)
175
+ target_attribute, target_attribute_name = ensure_model_attribute_target_rename(model_name, attribute_name)
176
+
177
+ if target_attribute.nil?
178
+ add_attribute = AddAttributeAction.new(
179
+ model_name: target_model_name,
180
+ attribute_name: target_attribute_name,
181
+ )
182
+
183
+ Attribute.copy(source_attribute, add_attribute)
184
+ queue_update(add_attribute)
185
+ else
186
+ # If there are is no diff, don't produce an action.
187
+ target_attribute = Attribute.new(target_attribute.raw)
188
+ target_attribute.set_field_defaults!
189
+
190
+ source_attribute = Attribute.new(source_attribute.raw)
191
+ source_attribute.set_field_defaults!
192
+
193
+ source_editable = source_attribute.raw.slice(*Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES.map(&:to_s))
194
+ target_editable = target_attribute.raw.slice(*Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES.map(&:to_s))
195
+
196
+ if source_editable == target_editable
197
+ return
198
+ end
199
+
200
+ update_attribute = UpdateAttributeAction.new(
201
+ model_name: target_model_name,
202
+ attribute_name: target_attribute_name,
203
+ )
204
+
205
+ Attribute.copy(source_attribute, update_attribute, attributes: Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES)
206
+ queue_update(update_attribute)
207
+ end
208
+ end
209
+
210
+ # Returns a tuple of the target's attribute, post rename if necessary, if it exists, and the name of the target attribute
211
+ # cases here:
212
+ # 1. There is no rename for the attribute
213
+ # a. There target attribute already exists -> [target_attribute, attribute_name]
214
+ # b. The target attribute does not exist -> [nil, attribute_name]
215
+ # 2. There is an expected rename from the source
216
+ # a. The target has neither the renamed attribute or the original attribute -> [nil, new_attribute_name]
217
+ # b. The target has the renamed attribute already -> [renamed_attribute, new_attribute_name]
218
+ # c. The target has the source attribute, which is not yet renamed. -> [renamed_attribute, new_attribute_name]
219
+ def ensure_model_attribute_target_rename(model_name, attribute_name)
220
+ target_model_name = target_of_source(model_name)
221
+ target_attribute_name = target_attribute_of_source(model_name, attribute_name)
222
+ return nil unless (target_model = target_models.model(target_model_name))
223
+
224
+ target_original_attribute = target_model.template.attributes.attribute(target_attribute_name)
225
+
226
+ if renames && (attribute_renames = renames[model_name]) && (new_name = attribute_renames[attribute_name])
227
+ new_name = target_attribute_of_source(model_name, new_name)
228
+
229
+ unless target_model.template.attributes.include?(new_name)
230
+ if target_original_attribute
231
+ rename = RenameAttributeAction.new(model_name: target_model_name, attribute_name: target_attribute_name, new_attribute_name: new_name)
232
+ queue_update(rename)
233
+ else
234
+ return [nil, new_name]
235
+ end
236
+ end
237
+
238
+ return [target_model.template.attributes.attribute(new_name), new_name]
239
+ end
240
+
241
+ [target_original_attribute, target_attribute_name]
242
+ end
243
+
244
+ # Non cyclical, non re-entrant due to the requirement that parents cannot form a cycle.
245
+ # This method, and it's partner prepare_parent, should never call into any re-entrant or potentially
246
+ # cyclical method, like ensure_model_tree.
247
+ def ensure_model(model_name)
248
+ return unless (source_model = source_models.model(model_name))
249
+ target_model_name = target_of_source(model_name)
250
+ return if target_models.model_keys.include?(target_model_name)
251
+
252
+ template = source_model.template
253
+
254
+ add_model_action = AddModelAction.new(
255
+ {
256
+ model_name: target_model_name,
257
+ identifier: template.identifier,
258
+ }
259
+ )
260
+
261
+ parents = template.attributes.all.select { |a| a.attribute_type == AttributeType::PARENT }
262
+ parent_model_name, parent_link_type = prepare_parent(model_name, template, parents)
263
+ unless parent_model_name.nil?
264
+ add_model_action.parent_model_name = parent_model_name
265
+ add_model_action.parent_link_type = parent_link_type
266
+ end
267
+
268
+ queue_update(add_model_action)
269
+ end
270
+
271
+ # Non cyclical, non re-entrant due to the requirement that parents cannot form a cycle.
272
+ # This method, and it's partner and ensure_model, should never call into any re-entrant or potentially
273
+ # cyclical method, like ensure_model_tree.
274
+ def prepare_parent(model_name, template, parents)
275
+ return [nil, nil] if parents.empty?
276
+ return [nil, nil] unless (parent_model = source_models.model(template.parent))
277
+ return [nil, nil] unless (child_attribute = parent_model.template.attributes.attribute(model_name))
278
+
279
+ ensure_model(template.parent)
280
+
281
+ [
282
+ target_of_source(template.parent),
283
+ child_attribute.attribute_type
284
+ ]
285
+ end
286
+
287
+ def self.models_affected_by(action)
288
+ case action
289
+ when Etna::Clients::Magma::RenameAttributeAction
290
+ [action.model_name]
291
+ when Etna::Clients::Magma::UpdateAttributeAction
292
+ [action.model_name]
293
+ when Etna::Clients::Magma::AddAttributeAction
294
+ [action.model_name]
295
+ when Etna::Clients::Magma::AddModelAction
296
+ [action.model_name]
297
+ when Etna::Clients::Magma::AddLinkAction
298
+ action.links.map(&:model_name)
299
+ else
300
+ []
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,63 @@
1
+ require 'ostruct'
2
+
3
+ module Etna
4
+ module Clients
5
+ class Magma
6
+ class RecordSynchronizationWorkflow < Struct.new(:target_client, :source_client, :project_name, :ignore_update_errors, keyword_init: true)
7
+ def target_models
8
+ @target_models ||= begin
9
+ target_client.retrieve(RetrievalRequest.new(project_name: self.project_name, model_name: 'all')).models
10
+ end
11
+ end
12
+
13
+ def crud
14
+ @crud ||= MagmaCrudWorkflow.new(project_name: project_name, magma_client: target_client)
15
+ end
16
+
17
+ def source_models
18
+ @source_models ||= source_client.retrieve(
19
+ RetrievalRequest.new(
20
+ project_name: project_name, model_name: 'all')).models
21
+ end
22
+
23
+ # TODO: Add paging here to support large payloads
24
+ def copy_model(model_name, copied_models = Set.new)
25
+ if copied_models.include?(model_name)
26
+ return
27
+ end
28
+
29
+ copied_models.add(model_name)
30
+
31
+ descendants = source_models.to_directed_graph.descendants("project")
32
+ (descendants[model_name] || []).each do |required_model|
33
+ copy_model(required_model, copied_models) do |update|
34
+ yield update if block_given?
35
+ end
36
+ end
37
+
38
+ crud.page_records(model_name) do |documents|
39
+ yield [model_name, documents] if block_given?
40
+ begin
41
+ crud.update_records do |update_request|
42
+ documents.document_keys.each do |identifier|
43
+ record = documents.document(identifier)
44
+ record = record.each.select { |k, v| v != nil && !v.is_a?(Array) }.to_h
45
+ update_request.update_revision(model_name, identifier, record)
46
+ end
47
+ end
48
+ rescue => e
49
+ raise unless ignore_update_errors
50
+ end
51
+ end
52
+ end
53
+
54
+ def copy_all_models
55
+ copied_models = Set.new
56
+ target_models.model_keys.each do |model_name|
57
+ copy_model(model_name, copied_models)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,178 @@
1
+ require 'csv'
2
+ require 'ostruct'
3
+ require_relative './crud_workflow'
4
+
5
+ module Etna
6
+ module Clients
7
+ class Magma
8
+ class UpdateAttributesFromCsvWorkflowBase < Struct.new(:magma_crud, :project_name, :filepath, :model_name, keyword_init: true)
9
+ def initialize(opts)
10
+ super(**{}.update(opts))
11
+ end
12
+
13
+ def magma_client
14
+ magma_crud.magma_client
15
+ end
16
+
17
+ def parse_input_file
18
+ raise "Must be implemented in a subclass"
19
+ end
20
+
21
+ def model_exists?(model_name)
22
+ models.model(model_name)
23
+ end
24
+
25
+ def consolidate_attributes_to_hash(row)
26
+ raise "Must be implemented in a subclass"
27
+ end
28
+
29
+ def find_attribute(model_name, attribute_name)
30
+ models.model(model_name).template.attributes.attribute(attribute_name)
31
+ end
32
+
33
+ def each_revision
34
+ parse_input_file do |model_name, record_name, revision|
35
+ yield [model_name, record_name, revision]
36
+ end
37
+ end
38
+
39
+ def update_attributes
40
+ magma_crud.update_records do |update_request|
41
+ each_revision do |model_name, record_name, revision|
42
+ update_request.update_revision(model_name, record_name, revision)
43
+ end
44
+ end
45
+ end
46
+
47
+ def models
48
+ @models ||= begin
49
+ magma_client.retrieve(RetrievalRequest.new(project_name: self.project_name, model_name: 'all')).models
50
+ end
51
+ end
52
+ end
53
+
54
+ class RowBase
55
+ def stripped_value(attribute_value)
56
+ attribute_value ? attribute_value.strip : attribute_value
57
+ end
58
+
59
+ def nil_or_empty?(value)
60
+ value.nil? || value.empty?
61
+ end
62
+ end
63
+
64
+ class UpdateAttributesFromCsvWorkflowMultiModel < UpdateAttributesFromCsvWorkflowBase
65
+ def parse_input_file
66
+ CSV.parse(File.read(filepath)).map do |csv_row|
67
+ row = Row.new(csv_row, self)
68
+
69
+ raise "Invalid model \"#{row.model_name}\" for project #{project_name}." unless model_exists?(row.model_name)
70
+
71
+ yield [row.model_name, row.record_name, row.to_h]
72
+ end
73
+ end
74
+
75
+ class Row < RowBase
76
+ attr_reader :model_name, :record_name
77
+ def initialize(raw, workflow)
78
+ # Assumes rows are in pairs, where
79
+ # [0] = model_name
80
+ # [1] = record_name / identifier
81
+ # [2], [4], etc. = attribute_name
82
+ # [3], [5], etc. = attribute value
83
+ # So not every row needs the same number of columns
84
+ @raw = raw
85
+ @workflow = workflow
86
+
87
+ raise "Invalid revision row #{@raw}. Must include at least 4 column values (model,record_name,attribute_name,attribute_value)." if @raw.length < 4
88
+ raise "Invalid revision row #{@raw}. Must have an even number of columns." if @raw.length.odd?
89
+
90
+ @model_name = raw[0]
91
+
92
+ raise "Invalid model name: \"#{@model_name}\"." if nil_or_empty?(@model_name)
93
+
94
+ @model_name.strip!
95
+
96
+ @record_name = raw[1]
97
+
98
+ raise "Invalid record name: \"#{@record_name}\"." if nil_or_empty?(@record_name)
99
+
100
+ @record_name.strip!
101
+ end
102
+
103
+ def to_h
104
+ # Take attribute index values (even) and put them into a hash.
105
+ # The assigned value will be the subsequent odd index.
106
+ # {attribute_name: attribute_value}
107
+ {}.tap do |attributes|
108
+ (2..(@raw.length - 1)).to_a.each do |index|
109
+ if index.even?
110
+ attribute_name = @raw[index]
111
+
112
+ raise "Invalid attribute name: \"#{attribute_name}\"." if nil_or_empty?(attribute_name)
113
+ attribute_name.strip!
114
+
115
+ raise "Invalid attribute #{attribute_name} for model #{model_name}." unless attribute = @workflow.find_attribute(model_name, attribute_name)
116
+
117
+ attributes[attribute_name] = stripped_value(@raw[index + 1])
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ class UpdateAttributesFromCsvWorkflowSingleModel < UpdateAttributesFromCsvWorkflowBase
126
+ def initialize(opts)
127
+ super(**{}.update(opts))
128
+ raise "Single Model invokation must include keyword :model_name." if !opts[:model_name]
129
+ raise "Invalid model #{model_name} for project #{project_name}." unless model_exists?(model_name)
130
+ end
131
+
132
+ def parse_input_file
133
+ CSV.parse(File.read(filepath), headers: true).map do |csv_row|
134
+ row = Row.new(csv_row, model_name, self)
135
+
136
+ yield [model_name, row.record_name, row.to_h]
137
+ end
138
+ end
139
+
140
+ class Row < RowBase
141
+ attr_reader :record_name
142
+ def initialize(raw, model_name, workflow)
143
+ # Assumes CSV includes a column header to identify the attribute_name
144
+ # Assumes index 0 is the record_name
145
+ @raw = raw
146
+ @model_name = model_name
147
+ @workflow = workflow
148
+
149
+ @record_name = @raw[0]
150
+ raise "Invalid record name: \"#{record_name}\"." if nil_or_empty?(record_name)
151
+
152
+ @record_name.strip!
153
+ end
154
+
155
+ def to_h
156
+ # Row can be converted to a hash, where keys are attribute_names and the
157
+ # values come from the CSV
158
+ # {attribute_name: attribute_value}
159
+ {}.tap do |attributes|
160
+ row_hash = @raw.to_h
161
+ row_keys = row_hash.keys
162
+ row_keys[1..row_keys.length - 1].each do |attribute_name|
163
+
164
+ raise "Invalid attribute name: \"#{attribute_name}\"." if nil_or_empty?(attribute_name)
165
+
166
+ attribute_name_clean = attribute_name.strip
167
+ raise "Invalid attribute \"#{attribute_name_clean}\" for model #{@model_name}." unless attribute = @workflow.find_attribute(@model_name, attribute_name_clean)
168
+
169
+ attributes[attribute_name_clean] = stripped_value(@raw[attribute_name])
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+