etna 0.1.15 → 0.1.21

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