etna 0.1.16 → 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 (57) 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 +38 -20
  9. data/lib/etna/client.rb +60 -29
  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 +2 -0
  16. data/lib/etna/clients/magma/client.rb +24 -9
  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 +323 -9
  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 +1 -0
  34. data/lib/etna/clients/metis/client.rb +207 -5
  35. data/lib/etna/clients/metis/models.rb +174 -3
  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 +235 -5
  45. data/lib/etna/controller.rb +4 -0
  46. data/lib/etna/environment_scoped.rb +19 -0
  47. data/lib/etna/generate_autocompletion_script.rb +130 -0
  48. data/lib/etna/json_serializable_struct.rb +6 -3
  49. data/lib/etna/logger.rb +0 -3
  50. data/lib/etna/multipart_serializable_nested_hash.rb +6 -1
  51. data/lib/etna/route.rb +1 -1
  52. data/lib/etna/spec/vcr.rb +98 -0
  53. data/lib/etna/templates/attribute_actions_template.json +43 -0
  54. data/lib/etna/test_auth.rb +3 -1
  55. data/lib/etna/user.rb +4 -0
  56. data/lib/helpers.rb +81 -0
  57. metadata +47 -7
@@ -0,0 +1,115 @@
1
+ require 'ostruct'
2
+ require_relative './crud_workflow'
3
+
4
+ module Etna
5
+ module Clients
6
+ class Magma
7
+ class FileLinkingWorkflow < Struct.new(:magma_crud, :model_name, :metis_client, :project_name, :bucket_name, :matching_expressions, :attribute_options, keyword_init: true)
8
+ PATIENT_TIMEPOINT_REGEX = /([^-]+-[^-]+)-(DN?[0-9]+).*/
9
+
10
+ def initialize(opts)
11
+ super(**{attribute_options: {}, matching_expressions: []}.update(opts))
12
+ end
13
+
14
+ def magma_client
15
+ magma_crud.magma_client
16
+ end
17
+
18
+ def find_matches
19
+ {}.tap do |all_matches|
20
+ metis_client.folders(
21
+ project_name: project_name,
22
+ bucket_name: bucket_name
23
+ ).each do |folder|
24
+ metis_client.list_folder(
25
+ Etna::Clients::Metis::ListFolderRequest.new(
26
+ project_name: project_name,
27
+ bucket_name: bucket_name,
28
+ folder_path: folder.folder_path,
29
+ ),
30
+ ).files.all.each do |file|
31
+ matches = matching_expressions
32
+ .map { |regex, attribute_name| [regex.match(file.file_path), regex, attribute_name] }
33
+ .select { |match, regex, attribute_name| !match.nil? }
34
+
35
+ if matches.length > 1
36
+ raise "File path #{file.file_path} matches multiple regex, #{matches.map(&:second)}. Please modify the matching expressions to disambiguate"
37
+ end
38
+
39
+ if matches.length == 1
40
+ match, _, attribute_name = matches.first
41
+ match_map = match.names.zip(match.captures).to_h
42
+ key = [match_map, attribute_name]
43
+
44
+ if attribute_options.dig(attribute_name, :file_collection)
45
+ (all_matches[key] ||= []).push(file.file_path)
46
+ else
47
+ if all_matches.include?(key)
48
+ raise "Field #{attribute_name} for #{match_map} identified for two files, #{file.file_path} and #{all_matches[key]}. Please modify the existing matching expressionts to disambiguate"
49
+ end
50
+
51
+ all_matches[key] = [file.file_path]
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ # Subclasses should override this to implement custom logic for how regex matches should match to linking.
60
+ def matches_to_record_identifiers(match_data)
61
+ {"project" => project_name}
62
+ end
63
+
64
+ def patient_timepoint_from(word)
65
+ match = PATIENT_TIMEPOINT_REGEX.match(word)
66
+ return {} unless match
67
+
68
+ return {
69
+ 'patient' => match[1],
70
+ 'timepoint' => "#{match[1]}-#{match[2]}",
71
+ }
72
+ end
73
+
74
+ def each_revision
75
+ find_matches.each do |key, file_paths|
76
+ match_map, attribute_name = key
77
+ record_identifiers = matches_to_record_identifiers(match_map)
78
+ id = containing_record_workflow.ensure_record(model_name, record_identifiers)
79
+ file_paths.each do |file_path|
80
+ yield [id, revision_for(id, attribute_name, file_path, match_map, record_identifiers)]
81
+ end
82
+ end
83
+ end
84
+
85
+ def link_files
86
+ magma_crud.update_records do |update_request|
87
+ each_revision do |id, revision|
88
+ update_request.update_revision(model_name, id, revision)
89
+ end
90
+ end
91
+ end
92
+
93
+ def revision_for(id, attribute_name, file_path, match_map, record_identifiers)
94
+ if attribute_options.dig(attribute_name, :file_collection)
95
+ file_path = ::File.dirname(file_path)
96
+ {attribute_name => "https://metis.ucsf.edu/#{project_name}/browse/#{bucket_name}/#{file_path}"}
97
+ else
98
+ {attribute_name => {path: "metis://#{project_name}/#{bucket_name}/#{file_path}"}}
99
+ end
100
+ end
101
+
102
+ def containing_record_workflow
103
+ @containing_record_workflow ||= EnsureContainingRecordWorkflow.new(magma_crud: magma_crud, models: models)
104
+ end
105
+
106
+ def models
107
+ @models ||= begin
108
+ magma_client.retrieve(RetrievalRequest.new(project_name: self.project_name, model_name: 'all')).models
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+
@@ -0,0 +1,81 @@
1
+ require 'json'
2
+
3
+ module Etna
4
+ module Clients
5
+ class Magma
6
+ class ConverterBase
7
+ def self.convert_attribute_user_json_to_magma_json(user_model_json)
8
+ json_attribute = Marshal.load(Marshal.dump(user_model_json['attributes']))
9
+ return unless json_attribute
10
+
11
+ json_attribute.keys.each do |attribute_name|
12
+ json_attribute[attribute_name]['attribute_type'] = Etna::Clients::Magma::AttributeType::IDENTIFIER if user_model_json['identifier'] == attribute_name
13
+ end
14
+ json_attribute
15
+ end
16
+
17
+ def self.convert_model_user_json_to_magma_json(model_name, user_json)
18
+ json_model = Marshal.load(Marshal.dump(user_json))
19
+ json_model['template'] = {
20
+ 'name' => model_name,
21
+ 'identifier' => user_json['identifier'],
22
+ 'parent' => user_json['parent_model_name'],
23
+ 'attributes' => convert_attribute_user_json_to_magma_json(json_model)
24
+ }
25
+ json_model
26
+ end
27
+
28
+ def self.convert_project_user_json_to_magma_json(user_json)
29
+ magma_models_json = {}
30
+ user_json['models'].keys.each do |model_name|
31
+ magma_models_json[model_name] = convert_model_user_json_to_magma_json(
32
+ model_name,
33
+ user_json['models'][model_name])
34
+ end
35
+ user_json['models'] = magma_models_json
36
+ user_json
37
+ end
38
+
39
+ def prettify(name)
40
+ name.split('_').map(&:capitalize).join(' ')
41
+ end
42
+ end
43
+
44
+ class AttributeActionsConverter < ConverterBase
45
+ attr_reader :actions
46
+ def initialize(actions)
47
+ @actions = JSON.parse(actions.to_json, symbolize_names: true)
48
+ end
49
+
50
+ def camelize(action_name)
51
+ action_name.split('_').map(&:capitalize).join('')
52
+ end
53
+
54
+ def clazz_name(action_name)
55
+ "Etna::Clients::Magma::#{camelize(action_name)}Action"
56
+ end
57
+
58
+ def convert
59
+ actions.map do |action_json|
60
+ # We use desc and attribute_type to be consistent
61
+ # with the other JSON actions...but the
62
+ # Magma Model takes type and description.
63
+ if action_json[:action_name] == 'add_attribute'
64
+ action_json[:type] = action_json.delete(:attribute_type)
65
+ action_json[:description] = action_json.delete(:desc)
66
+ elsif action_json[:action_name] == 'add_link'
67
+ action_json[:links].first[:type] = action_json[:links].first.delete(:attribute_type)
68
+ action_json[:links].last[:type] = action_json[:links].last.delete(:attribute_type)
69
+ end
70
+
71
+ clazz = Object.const_get(clazz_name(action_json[:action_name]))
72
+ clazz.new(**action_json)
73
+ rescue ArgumentError => e
74
+ modified_message = "Exception while parsing #{action_json}.\n" + e.message
75
+ raise ArgumentError.new(modified_message)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,447 @@
1
+ require 'json'
2
+ require 'set'
3
+
4
+ module Etna
5
+ module Clients
6
+ class Magma
7
+ class ValidatorBase
8
+ attr_reader :errors
9
+
10
+ def initialize
11
+ @errors = []
12
+ end
13
+
14
+ def valid?
15
+ @errors.length == 0
16
+ end
17
+
18
+ def nil_or_empty?(value)
19
+ value.nil? || value.empty?
20
+ end
21
+
22
+ def check_key(label, raw, key)
23
+ @errors << "Missing required key for #{label}: \"#{key}\"." if !raw.dig(key)
24
+ @errors << "Invalid empty #{key} for #{label}: \"#{raw[key]}\"." if raw.dig(key) && nil_or_empty?(raw[key])
25
+ end
26
+
27
+ def check_in_set(label, raw, key, valid_values)
28
+ @errors << "Invalid #{key} for #{label}: \"#{raw[key]}\".\nShould be one of #{valid_values}." if raw.dig(key) && !valid_values.include?(raw[key])
29
+ end
30
+
31
+ def name_regex_with_numbers
32
+ /\A[a-z][a-z0-9]*(_[a-z0-9]+)*\Z/
33
+ end
34
+
35
+ def check_valid_name_with_numbers(label, proposed_name)
36
+ @errors << "#{label} name \"#{proposed_name}\" must be snake_case and can only consist of letters, numbers, and \"_\"." unless proposed_name =~ name_regex_with_numbers
37
+ end
38
+
39
+ def name_regex_no_numbers
40
+ /\A[a-z]*(_[a-z]+)*\Z/
41
+ end
42
+
43
+ def model_exists_in_project?(project_magma_models, model_name)
44
+ !!project_magma_models.model(model_name)
45
+ end
46
+
47
+ def validate!(err_message)
48
+ @errors = []
49
+ self.validate
50
+ raise "#{err_message}\n#{format_errors}" unless valid?
51
+ end
52
+
53
+ def format_errors
54
+ errors.map { |e| e.gsub("\n", "\n\t") }.join("\n * ")
55
+ end
56
+ end
57
+
58
+ class ProjectValidator < ValidatorBase
59
+ attr_reader :create_args
60
+
61
+ def initialize(**create_args)
62
+ super()
63
+ @create_args = create_args
64
+ end
65
+
66
+ def validate
67
+ validate_project_names
68
+ end
69
+
70
+ def validate_project_names
71
+ check_key('root project', create_args, :project_name)
72
+ check_key('root project', create_args, :project_name_full)
73
+ name = create_args[:project_name]
74
+ @errors << "Project name #{name} must be snake_case and cannot start with a number or \"pg_\"." unless name =~ name_regex_with_numbers && !name.start_with?('pg_')
75
+ end
76
+ end
77
+
78
+ class RenamesValidator < ValidatorBase
79
+ attr_reader :models, :renames
80
+ def initialize(models = Models.new, renames = {})
81
+ @models = models
82
+ @renames = renames
83
+ super()
84
+ end
85
+
86
+ def validate
87
+ renames.each do |model_name, attribute_renames|
88
+ attribute_renames.each do |old_name, new_name|
89
+ keys = @models.build_model(model_name).build_template.build_attributes.attribute_keys
90
+ if keys.include?(new_name)
91
+ @errors << "Model #{model_name} trying to rename #{old_name} to #{new_name}, but a different #{new_name} already exists."
92
+ end
93
+
94
+ if !keys.include?(old_name)
95
+ @errors << "Model #{model_name} trying to rename #{old_name} to #{new_name}, but #{old_name} does not exist."
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ class AddModelValidator < ValidatorBase
103
+ attr_reader :model
104
+
105
+ def initialize(models = Models.new, model_name = 'project')
106
+ super()
107
+ @model = models.build_model(model_name)
108
+ @models = models
109
+ end
110
+
111
+ def parent_reciprocal_attribute
112
+ @models.find_reciprocal(model: @model, link_attribute_name: @model.template.parent)
113
+ end
114
+
115
+ def name
116
+ model&.name
117
+ end
118
+
119
+ def raw
120
+ model&.template&.raw
121
+ end
122
+
123
+ def is_project?
124
+ name == 'project'
125
+ end
126
+
127
+ def validate
128
+ @errors << "Model name #{name} must be snake_case and can only consist of letters and \"_\"." unless name =~ name_regex_no_numbers
129
+
130
+ if parent_reciprocal_attribute&.attribute_type != Etna::Clients::Magma::ParentLinkType::TABLE
131
+ check_key("model #{name}", raw, 'identifier')
132
+ end
133
+
134
+ validate_links
135
+ validate_attributes
136
+ end
137
+
138
+ def validate_attributes
139
+ model.template.attributes.attribute_keys.each do |attribute_name|
140
+ attribute = model.template.attributes.attribute(attribute_name)
141
+
142
+ if attribute_name == model.template.identifier
143
+ attribute_types = [AttributeType::IDENTIFIER]
144
+ elsif attribute_name == model.template.parent
145
+ attribute_types = [AttributeType::PARENT]
146
+ elsif @models.find_reciprocal(model: model, attribute: attribute)&.attribute_type == AttributeType::PARENT
147
+ attribute_types = AttributeValidator.valid_parent_link_attribute_types
148
+ else
149
+ attribute_types = AttributeValidator.valid_add_row_attribute_types
150
+ end
151
+
152
+ attribute_validator = AttributeValidator.new(attribute, attribute_types, @models)
153
+ attribute_validator.validate
154
+ @errors += attribute_validator.errors
155
+ end
156
+ end
157
+
158
+ def validate_links
159
+ if !is_project? && !@models.model_keys.include?(@model.template.parent)
160
+ @errors << "Parent model \"#{@model.template.parent}\" for #{name} does not exist in project."
161
+ end
162
+
163
+ if !is_project? && parent_reciprocal_attribute.nil?
164
+ @errors << "Parent link attributes not defined for model #{name}."
165
+ end
166
+
167
+ link_attributes.each do |attribute|
168
+ check_key("attribute #{attribute.attribute_name}", attribute.raw, 'link_model_name')
169
+
170
+ if attribute.attribute_name != attribute.link_model_name
171
+ @errors << "Linked model, \"#{attribute.link_model_name}\", does not match attribute #{attribute.attribute_name}, link attribute names must match the model name."
172
+ end
173
+
174
+ unless @models.model_keys.include?(attribute.link_model_name)
175
+ @errors << "Linked model, \"#{attribute.link_model_name}\", on attribute #{attribute.attribute_name} does not exist!"
176
+ end
177
+
178
+ reciprocal = @models.find_reciprocal(model: @model, attribute: attribute)
179
+ if reciprocal.nil?
180
+ @errors << "Linked model, \"#{attribute.link_model_name}\", on attribute #{attribute.attribute_name} does not have a reciprocal link defined."
181
+ end
182
+ end
183
+ end
184
+
185
+ def link_attributes
186
+ @model.template.attributes.all.select do |attribute|
187
+ attribute.attribute_type == Etna::Clients::Magma::AttributeType::LINK
188
+ end
189
+ end
190
+ end
191
+
192
+ class AttributeValidator < ValidatorBase
193
+ attr_reader :attribute
194
+
195
+ def initialize(attribute, valid_attribute_types, project_models)
196
+ super()
197
+ @attribute = attribute
198
+ @valid_attribute_types = valid_attribute_types
199
+ @valid_validation_types = self.class.valid_validation_types
200
+ @project_models = project_models
201
+ end
202
+
203
+ def self.valid_add_row_attribute_types
204
+ Etna::Clients::Magma::AttributeType.entries.reject { |a|
205
+ a == Etna::Clients::Magma::AttributeType::CHILD ||
206
+ a == Etna::Clients::Magma::AttributeType::IDENTIFIER ||
207
+ a == Etna::Clients::Magma::AttributeType::PARENT
208
+ }.sort
209
+ end
210
+
211
+ def self.valid_parent_link_attribute_types
212
+ [
213
+ AttributeType::COLLECTION,
214
+ AttributeType::TABLE,
215
+ AttributeType::CHILD,
216
+ ]
217
+ end
218
+
219
+ def self.valid_update_attribute_types
220
+ Etna::Clients::Magma::AttributeType.entries.sort
221
+ end
222
+
223
+ def self.valid_validation_types
224
+ Etna::Clients::Magma::AttributeValidationType.entries.sort
225
+ end
226
+
227
+ def validate
228
+ validate_basic_attribute_data
229
+ validate_attribute_validation
230
+ end
231
+
232
+ def validate_basic_attribute_data
233
+ check_valid_name_with_numbers('Attribute', attribute.attribute_name)
234
+ check_key("attribute #{attribute.attribute_name}", attribute.raw, 'attribute_type')
235
+
236
+ if attribute.link_model_name && ![AttributeType::TABLE, AttributeType::LINK, AttributeType::COLLECTION, AttributeType::PARENT, AttributeType::CHILD].include?(attribute.attribute_type)
237
+ @errors << "attribute #{attribute.attribute_name} has link_model_name set, but has attribute_type #{attribute.attribute_type}"
238
+ end
239
+
240
+ if attribute.link_model_name && !@project_models.model_keys.include?(attribute.link_model_name)
241
+ @errors << "attribute #{attribute.attribute_name} has link_model_name value of #{attribute.link_model_name}, but a model by that name does not exist."
242
+ end
243
+
244
+ check_in_set("attribute #{attribute.attribute_name}", attribute.raw, 'attribute_type', @valid_attribute_types)
245
+ end
246
+
247
+ def validate_attribute_validation
248
+ return unless attribute.validation
249
+ check_key("attribute #{attribute.attribute_name}, validation", attribute.validation, 'type')
250
+ check_key("attribute #{attribute.attribute_name}, validation", attribute.validation, 'value')
251
+ check_in_set("attribute #{attribute.attribute_name}, validation", attribute.validation, 'type', @valid_validation_types)
252
+ end
253
+
254
+ def is_link_attribute?
255
+ attribute.attribute_type == Etna::Clients::Magma::AttributeType::LINK
256
+ end
257
+
258
+ def is_identifier_attribute?
259
+ attribute.attribute_type == Etna::Clients::Magma::AttributeType::IDENTIFIER
260
+ end
261
+ end
262
+
263
+ class AttributeActionValidatorBase < ValidatorBase
264
+ attr_reader :action, :project_models
265
+
266
+ def initialize(action, project_models)
267
+ super()
268
+ @action = action
269
+ @project_models = project_models
270
+ end
271
+
272
+ def action_to_attribute(action)
273
+ action_json = JSON.parse(action.to_json)
274
+ # Magma Model uses type and description for Actions,
275
+ # but Attribute uses attribute_type and desc.
276
+ action_json['attribute_type'] = action_json.delete('type')
277
+ action_json['desc'] = action_json.delete('description')
278
+ Etna::Clients::Magma::Attribute.new(action_json)
279
+ end
280
+
281
+ def validate
282
+ raise "Subclasses must implement this method."
283
+ end
284
+
285
+ def exists_in_magma_model?(magma_model_name, attribute_name)
286
+ !!project_models.model(magma_model_name).template.attributes.attribute(attribute_name)
287
+ end
288
+
289
+ def validate_model_exists(magma_model_name)
290
+ @errors << "Model \"#{magma_model_name}\" does not exist in project." unless model_exists_in_project?(project_models, magma_model_name)
291
+ end
292
+
293
+ def check_already_exists_in_model(magma_model_name, attribute_name)
294
+ return unless model_exists_in_project?(project_models, magma_model_name)
295
+ @errors << "Attribute \"#{attribute_name}\" already exists in model #{magma_model_name}." if exists_in_magma_model?(magma_model_name, attribute_name)
296
+ end
297
+
298
+ def check_does_not_exist_in_model(magma_model_name, attribute_name)
299
+ return unless model_exists_in_project?(project_models, magma_model_name)
300
+ @errors << "Attribute \"#{attribute_name}\" does not exist in model #{magma_model_name}." unless exists_in_magma_model?(magma_model_name, attribute_name)
301
+ end
302
+ end
303
+
304
+ class AddAttributeActionValidator < AttributeActionValidatorBase
305
+ def initialize(action, project_models)
306
+ super
307
+ @attribute = action_to_attribute(action)
308
+ end
309
+
310
+ def validate
311
+ validate_attribute_data
312
+ validate_model_exists(action.model_name)
313
+ end
314
+
315
+ def validate_attribute_data
316
+ validator = AttributeValidator.new(@attribute, AttributeValidator.valid_add_row_attribute_types, project_models)
317
+ validator.validate
318
+ @errors += validator.errors unless validator.valid?
319
+
320
+ check_already_exists_in_model(action.model_name, action.attribute_name)
321
+ end
322
+ end
323
+
324
+ class AddLinkActionValidator < AttributeActionValidatorBase
325
+ def source
326
+ action.links.first
327
+ end
328
+
329
+ def dest
330
+ action.links.last
331
+ end
332
+
333
+ def validate
334
+ validate_links
335
+ validate_both_models_exist
336
+ validate_link_data
337
+ end
338
+
339
+ def validate_links
340
+ check_key("action #{action}", action, :links)
341
+ @errors << "Must include two link entries, each with \"model_name\", \"attribute_name\", and \"type\"." unless action.links.length == 2
342
+ check_key("link #{source}", source, :model_name)
343
+ check_key("link #{source}", source, :attribute_name)
344
+ check_key("link #{source}", source, :type)
345
+ check_key("link #{dest}", dest, :model_name)
346
+ check_key("link #{dest}", dest, :attribute_name)
347
+ check_key("link #{dest}", dest, :type)
348
+ end
349
+
350
+ def validate_both_models_exist
351
+ validate_model_exists(source[:model_name])
352
+ validate_model_exists(dest[:model_name])
353
+ end
354
+
355
+ def validate_link_data
356
+ # Make sure the attribute names don't already exist in the models,
357
+ # and that the types are valid.
358
+ link_types = Set.new([source[:type], dest[:type]])
359
+ expected_link_types = Set.new([
360
+ Etna::Clients::Magma::AttributeType::LINK,
361
+ Etna::Clients::Magma::AttributeType::COLLECTION
362
+ ])
363
+ if link_types != expected_link_types
364
+ @errors << "You must have one \"link\" and one \"collection\" type in the links."
365
+ else
366
+ check_already_exists_in_model(source[:model_name], source[:attribute_name])
367
+ check_already_exists_in_model(dest[:model_name], dest[:attribute_name])
368
+
369
+ @errors << "Links #{source} and #{dest} must point to each other." unless source[:model_name] == dest[:attribute_name] && source[:attribute_name] == dest[:model_name]
370
+ end
371
+ end
372
+ end
373
+
374
+ class RenameAttributeActionValidator < AttributeActionValidatorBase
375
+ def validate
376
+ validate_action
377
+ validate_model_exists(action.model_name)
378
+ validate_proposed_name
379
+ end
380
+
381
+ def validate_action
382
+ check_key("action #{action}", action, :model_name)
383
+ check_key("action #{action}", action, :attribute_name)
384
+ check_key("action #{action}", action, :new_attribute_name)
385
+ end
386
+
387
+ def validate_proposed_name
388
+ check_does_not_exist_in_model(action.model_name, action.attribute_name)
389
+ check_valid_name_with_numbers('New attribute', action.new_attribute_name)
390
+ check_already_exists_in_model(action.model_name, action.new_attribute_name)
391
+ end
392
+ end
393
+
394
+ class UpdateAttributeActionValidator < AttributeActionValidatorBase
395
+ def initialize(action, project_models)
396
+ super
397
+ @attribute = action_to_attribute(action)
398
+ end
399
+
400
+ def validate
401
+ validate_attribute_data
402
+ validate_model_exists(action.model_name)
403
+ end
404
+
405
+ def validate_attribute_data
406
+ check_does_not_exist_in_model(action.model_name, action.attribute_name)
407
+ end
408
+ end
409
+
410
+ class AttributeActionsValidator < ValidatorBase
411
+ attr_reader :actions, :project_models
412
+
413
+ def initialize(actions, project_models)
414
+ super()
415
+ @actions = actions
416
+ @project_models = project_models
417
+ end
418
+
419
+ def project_model_names
420
+ project_models.all.map(&:template).map(&:name)
421
+ end
422
+
423
+ def validate
424
+ validate_actions
425
+ end
426
+
427
+ def validate_actions
428
+ actions.each do |action|
429
+ clazz = Object.const_get(clazz_name(action.action_name))
430
+ validator = clazz.new(action, project_models)
431
+ validator.validate
432
+
433
+ @errors += validator.errors unless validator.valid?
434
+ end
435
+ end
436
+
437
+ def camelize(action_name)
438
+ action_name.split('_').map(&:capitalize).join('')
439
+ end
440
+
441
+ def clazz_name(action_name)
442
+ "Etna::Clients::Magma::#{camelize(action_name)}ActionValidator"
443
+ end
444
+ end
445
+ end
446
+ end
447
+ end