etna 0.1.14 → 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/bin/etna +18 -0
- data/etna.completion +1001 -0
- data/etna_app.completion +133 -0
- data/ext/completions/extconf.rb +20 -0
- data/lib/commands.rb +395 -0
- data/lib/etna.rb +7 -0
- data/lib/etna/application.rb +46 -22
- data/lib/etna/client.rb +82 -48
- data/lib/etna/clients.rb +4 -0
- data/lib/etna/clients/enum.rb +9 -0
- data/lib/etna/clients/janus.rb +2 -0
- data/lib/etna/clients/janus/client.rb +73 -0
- data/lib/etna/clients/janus/models.rb +78 -0
- data/lib/etna/clients/magma.rb +4 -0
- data/lib/etna/clients/magma/client.rb +80 -0
- data/lib/etna/clients/magma/formatting.rb +1 -0
- data/lib/etna/clients/magma/formatting/models_csv.rb +354 -0
- data/lib/etna/clients/magma/models.rb +630 -0
- data/lib/etna/clients/magma/workflows.rb +10 -0
- data/lib/etna/clients/magma/workflows/add_project_models_workflow.rb +67 -0
- data/lib/etna/clients/magma/workflows/attribute_actions_from_json_workflow.rb +62 -0
- data/lib/etna/clients/magma/workflows/create_project_workflow.rb +123 -0
- data/lib/etna/clients/magma/workflows/crud_workflow.rb +85 -0
- data/lib/etna/clients/magma/workflows/ensure_containing_record_workflow.rb +44 -0
- data/lib/etna/clients/magma/workflows/file_attributes_blank_workflow.rb +68 -0
- data/lib/etna/clients/magma/workflows/file_linking_workflow.rb +115 -0
- data/lib/etna/clients/magma/workflows/json_converters.rb +81 -0
- data/lib/etna/clients/magma/workflows/json_validators.rb +452 -0
- data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +306 -0
- data/lib/etna/clients/magma/workflows/record_synchronization_workflow.rb +63 -0
- data/lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb +246 -0
- data/lib/etna/clients/metis.rb +3 -0
- data/lib/etna/clients/metis/client.rb +239 -0
- data/lib/etna/clients/metis/models.rb +313 -0
- data/lib/etna/clients/metis/workflows.rb +2 -0
- data/lib/etna/clients/metis/workflows/metis_download_workflow.rb +37 -0
- data/lib/etna/clients/metis/workflows/metis_upload_workflow.rb +137 -0
- data/lib/etna/clients/polyphemus.rb +3 -0
- data/lib/etna/clients/polyphemus/client.rb +33 -0
- data/lib/etna/clients/polyphemus/models.rb +68 -0
- data/lib/etna/clients/polyphemus/workflows.rb +1 -0
- data/lib/etna/clients/polyphemus/workflows/set_configuration_workflow.rb +47 -0
- data/lib/etna/command.rb +243 -5
- data/lib/etna/controller.rb +4 -0
- data/lib/etna/csvs.rb +159 -0
- data/lib/etna/directed_graph.rb +56 -0
- data/lib/etna/environment_scoped.rb +19 -0
- data/lib/etna/errors.rb +6 -0
- data/lib/etna/generate_autocompletion_script.rb +131 -0
- data/lib/etna/json_serializable_struct.rb +37 -0
- data/lib/etna/logger.rb +24 -2
- data/lib/etna/multipart_serializable_nested_hash.rb +50 -0
- data/lib/etna/route.rb +1 -1
- data/lib/etna/server.rb +3 -0
- data/lib/etna/spec/vcr.rb +99 -0
- data/lib/etna/templates/attribute_actions_template.json +43 -0
- data/lib/etna/test_auth.rb +3 -1
- data/lib/etna/user.rb +11 -1
- data/lib/helpers.rb +90 -0
- 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
|