etna 0.1.13 → 0.1.19
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 +926 -0
- data/etna_app.completion +133 -0
- data/ext/completions/extconf.rb +20 -0
- data/lib/commands.rb +360 -0
- data/lib/etna.rb +6 -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 +345 -0
- data/lib/etna/clients/magma/models.rb +579 -0
- data/lib/etna/clients/magma/workflows.rb +10 -0
- data/lib/etna/clients/magma/workflows/add_project_models_workflow.rb +78 -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 +117 -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 +447 -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 +178 -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/directed_graph.rb +56 -0
- data/lib/etna/environment_scoped.rb +19 -0
- data/lib/etna/generate_autocompletion_script.rb +131 -0
- data/lib/etna/hmac.rb +1 -0
- data/lib/etna/json_serializable_struct.rb +37 -0
- data/lib/etna/logger.rb +15 -1
- data/lib/etna/multipart_serializable_nested_hash.rb +50 -0
- data/lib/etna/parse_body.rb +1 -1
- data/lib/etna/route.rb +1 -1
- data/lib/etna/server.rb +3 -0
- data/lib/etna/spec/vcr.rb +98 -0
- data/lib/etna/templates/attribute_actions_template.json +43 -0
- data/lib/etna/test_auth.rb +4 -2
- data/lib/etna/user.rb +11 -1
- data/lib/helpers.rb +87 -0
- metadata +69 -5
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'net/http/persistent'
|
2
|
+
require 'net/http/post/multipart'
|
3
|
+
require 'singleton'
|
4
|
+
require_relative '../../client'
|
5
|
+
require_relative './models'
|
6
|
+
|
7
|
+
module Etna
|
8
|
+
module Clients
|
9
|
+
class Magma
|
10
|
+
attr_reader :host, :token, :ignore_ssl
|
11
|
+
def initialize(host:, token:, persistent: true, ignore_ssl: false)
|
12
|
+
raise 'Magma client configuration is missing host.' unless host
|
13
|
+
raise 'Magma client configuration is missing token.' unless token
|
14
|
+
@etna_client = ::Etna::Client.new(
|
15
|
+
host,
|
16
|
+
token,
|
17
|
+
routes_available: false,
|
18
|
+
persistent: persistent,
|
19
|
+
ignore_ssl: ignore_ssl)
|
20
|
+
@host = host
|
21
|
+
@token = token
|
22
|
+
@ignore_ssl = ignore_ssl
|
23
|
+
end
|
24
|
+
|
25
|
+
# This endpoint returns models and records by name:
|
26
|
+
# e.g. params:
|
27
|
+
# {
|
28
|
+
# model_name: "model_one", # or "all"
|
29
|
+
# record_names: [ "rn1", "rn2" ], # or "all",
|
30
|
+
# attribute_names: "all"
|
31
|
+
# }
|
32
|
+
def retrieve(retrieval_request = RetrievalRequest.new)
|
33
|
+
json = nil
|
34
|
+
@etna_client.post('/retrieve', retrieval_request) do |res|
|
35
|
+
json = JSON.parse(res.body)
|
36
|
+
end
|
37
|
+
|
38
|
+
RetrievalResponse.new(json)
|
39
|
+
end
|
40
|
+
|
41
|
+
# This 'query' end point is used to fetch data by graph query
|
42
|
+
# See question.rb for more detail
|
43
|
+
def query(query_request = QueryRequest.new)
|
44
|
+
json = nil
|
45
|
+
@etna_client.post('/query', query_request) do |res|
|
46
|
+
json = JSON.parse(res.body)
|
47
|
+
end
|
48
|
+
|
49
|
+
QueryResponse.new(json)
|
50
|
+
end
|
51
|
+
|
52
|
+
def update(update_request = UpdateRequest.new)
|
53
|
+
json = nil
|
54
|
+
@etna_client.multipart_post('/update', update_request.encode_multipart_content) do |res|
|
55
|
+
json = JSON.parse(res.body)
|
56
|
+
end
|
57
|
+
|
58
|
+
UpdateResponse.new(json)
|
59
|
+
end
|
60
|
+
|
61
|
+
def update_json(update_request = UpdateRequest.new)
|
62
|
+
json = nil
|
63
|
+
@etna_client.post('/update', update_request) do |res|
|
64
|
+
json = JSON.parse(res.body)
|
65
|
+
end
|
66
|
+
|
67
|
+
UpdateResponse.new(json)
|
68
|
+
end
|
69
|
+
|
70
|
+
def update_model(update_model_request = UpdateModelRequest.new)
|
71
|
+
json = nil
|
72
|
+
@etna_client.post('/update_model', update_model_request) do |res|
|
73
|
+
json = JSON.parse(res.body)
|
74
|
+
end
|
75
|
+
|
76
|
+
UpdateModelResponse.new(json)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'formatting/models_csv'
|
@@ -0,0 +1,345 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Etna
|
4
|
+
module Clients
|
5
|
+
class Magma
|
6
|
+
class ModelsCsv
|
7
|
+
COPY_OPTIONS_SENTINEL = '$copy_from_original$'
|
8
|
+
COLUMNS = [
|
9
|
+
:comments,
|
10
|
+
:model_name, :identifier, :parent_model_name, :parent_link_type,
|
11
|
+
:attribute_name,
|
12
|
+
:new_attribute_name,
|
13
|
+
:attribute_type,
|
14
|
+
:link_model_name,
|
15
|
+
:description,
|
16
|
+
:display_name,
|
17
|
+
:format_hint,
|
18
|
+
:restricted,
|
19
|
+
:read_only,
|
20
|
+
:options,
|
21
|
+
:match,
|
22
|
+
:attribute_group,
|
23
|
+
:hidden,
|
24
|
+
:unique,
|
25
|
+
:matrix_constant,
|
26
|
+
]
|
27
|
+
|
28
|
+
COLUMN_AS_BOOLEAN = -> (s) { ['true', 't', 'y', 'yes'].include?(s.downcase) }
|
29
|
+
|
30
|
+
COLUMNS_TO_ATTRIBUTES = {
|
31
|
+
attribute_name: :attribute_name,
|
32
|
+
attribute_type: [:attribute_type, -> (s) { AttributeType.new(s) }],
|
33
|
+
link_model_name: :link_model_name,
|
34
|
+
description: :desc,
|
35
|
+
display_name: :display_name,
|
36
|
+
format_hint: :format_hint,
|
37
|
+
restricted: [:restricted, COLUMN_AS_BOOLEAN],
|
38
|
+
read_only: [:read_only, COLUMN_AS_BOOLEAN],
|
39
|
+
options: [:validation, -> (s) { {"type" => "Array", "value" => s.split(',').map(&:strip)} }],
|
40
|
+
match: [:validation, -> (s) { {"type" => "Regexp", "value" => Regexp.new(s).source} }],
|
41
|
+
attribute_group: :attribute_group,
|
42
|
+
hidden: [:hidden, COLUMN_AS_BOOLEAN],
|
43
|
+
unique: [:unique, COLUMN_AS_BOOLEAN],
|
44
|
+
}
|
45
|
+
|
46
|
+
def self.apply_csv_row(changeset = ModelsChangeset.new, row = {}, &err_block)
|
47
|
+
changeset.tap do
|
48
|
+
models = changeset.models
|
49
|
+
|
50
|
+
if (matrix_constant = self.get_col_or_nil(row, :matrix_constant))
|
51
|
+
if matrix_constant.start_with?(COPY_OPTIONS_SENTINEL)
|
52
|
+
matrix_constant = matrix_constant.slice((COPY_OPTIONS_SENTINEL.length)..-1)
|
53
|
+
changeset.last_matrix_constant = matrix_constant
|
54
|
+
else
|
55
|
+
(changeset.matrix_constants[changeset.last_matrix_constant] ||= []) << matrix_constant
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
template = if (model_name = self.get_col_or_nil(row, :model_name))
|
60
|
+
changeset.last_model_key = model_name
|
61
|
+
models.build_model(model_name).build_template.tap { |t| t.name = model_name }
|
62
|
+
else
|
63
|
+
last_model = changeset.last_model_key
|
64
|
+
if last_model.nil?
|
65
|
+
nil
|
66
|
+
else
|
67
|
+
models.model(last_model).build_template
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if (identifier = self.get_col_or_nil(row, :identifier))
|
72
|
+
if template.nil?
|
73
|
+
yield "Found identifier #{identifier} but no model_name had been given!"
|
74
|
+
next
|
75
|
+
end
|
76
|
+
template.identifier = identifier
|
77
|
+
end
|
78
|
+
|
79
|
+
if (parent_model_name = self.get_col_or_nil(row, :parent_model_name))
|
80
|
+
self.process_parent_model_name(template: template, parent_model_name: parent_model_name, models: models, &err_block)
|
81
|
+
end
|
82
|
+
|
83
|
+
if (parent_link_type = self.get_col_or_nil(row, :parent_link_type))
|
84
|
+
self.process_parent_link_type(template: template, parent_link_type: parent_link_type, models: models, &err_block)
|
85
|
+
end
|
86
|
+
|
87
|
+
if (attribute_name = self.get_col_or_nil(row, :attribute_name))
|
88
|
+
self.process_attribute(template: template, attribute_name: attribute_name, models: models, row: row, &err_block)
|
89
|
+
if (new_attribute_name = self.get_col_or_nil(row, :new_attribute_name))
|
90
|
+
self.process_new_attribute_name(template: template, changeset: changeset, new_attribute_name: new_attribute_name, attribute_name: attribute_name, &err_block)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.process_new_attribute_name(template:, changeset:, new_attribute_name:, attribute_name:, &err_block)
|
97
|
+
renames = changeset.build_renames(template.name)
|
98
|
+
if renames.include?(attribute_name) && renames[attribute_name] != new_attribute_name
|
99
|
+
if block_given?
|
100
|
+
yield "Found multiple new_attribute_name values for #{template.name}'s #{attribute_name}': #{new_attribute_name} or #{renames[attribute_name]}?"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
renames[attribute_name] = new_attribute_name
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.process_parent_model_name(template:, parent_model_name:, models:, &err_block)
|
108
|
+
if template.nil?
|
109
|
+
yield "Found parent_model_name #{parent_model_name} but no model_name had been given!" if block_given?
|
110
|
+
return
|
111
|
+
end
|
112
|
+
|
113
|
+
if template.parent && !template.parent.empty? && template.parent != parent_model_name
|
114
|
+
yield "Model #{template.name} was provided multiple parent_model_names: #{template.parent} and #{parent_model_name}" if block_given?
|
115
|
+
end
|
116
|
+
|
117
|
+
template.parent = parent_model_name
|
118
|
+
|
119
|
+
template.build_attributes.build_attribute(template.parent).tap do |parent_att|
|
120
|
+
parent_att.name = parent_att.attribute_name = parent_model_name
|
121
|
+
parent_att.attribute_type = AttributeType::PARENT
|
122
|
+
parent_att.link_model_name = parent_model_name
|
123
|
+
parent_att.desc = self.prettify(parent_model_name)
|
124
|
+
parent_att.display_name = self.prettify(parent_model_name)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.process_parent_link_type(template:, parent_link_type:, models:, &err_block)
|
129
|
+
if template.nil?
|
130
|
+
yield "Found parent_link_type #{parent_link_type} but no model_name had been given!" if block_given?
|
131
|
+
return
|
132
|
+
end
|
133
|
+
|
134
|
+
if template.parent.nil?
|
135
|
+
yield "Found parent_link_type #{parent_link_type} but no parent_model_name had been given!" if block_given?
|
136
|
+
return
|
137
|
+
end
|
138
|
+
|
139
|
+
reciprocal = models.find_reciprocal(model_name: template.name, link_attribute_name: template.parent)
|
140
|
+
if reciprocal && reciprocal.attribute_type.to_s != parent_link_type
|
141
|
+
yield "Model #{template.name} was provided multiple parent_link_types: #{reciprocal.attribute_type} and #{parent_link_type}" if block_given?
|
142
|
+
end
|
143
|
+
|
144
|
+
if reciprocal && reciprocal.attribute_name != template.name
|
145
|
+
yield "Model #{template.name} is linked to #{template.parent}, but the reciprocal link is misnamed as '#{reciprocal.attribute_name}'." if block_given?
|
146
|
+
end
|
147
|
+
|
148
|
+
models.build_model(template.parent).tap do |parent_model|
|
149
|
+
parent_model_attribute_by_model_name = parent_model.build_template.build_attributes.attribute(template.name)
|
150
|
+
if parent_model_attribute_by_model_name && !reciprocal
|
151
|
+
yield "Model #{template.parent} is linked as a parent to #{template.name}, but it already has an attribute named #{template.name} #{parent_model_attribute_by_model_name.raw}." if block_given?
|
152
|
+
end
|
153
|
+
|
154
|
+
parent_model.build_template.build_attributes.build_attribute(template.name).tap do |attr|
|
155
|
+
attr.attribute_name = attr.name = template.name
|
156
|
+
attr.attribute_type = parent_link_type
|
157
|
+
attr.link_model_name = template.name
|
158
|
+
attr.desc = self.prettify(template.name)
|
159
|
+
attr.display_name = self.prettify(template.name)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.process_attribute(template:, attribute_name:, models:, row:, &err_block)
|
165
|
+
if template.nil?
|
166
|
+
yield "Found attribute #{attribute_name} but no model_name had been given!" if block_given?
|
167
|
+
return
|
168
|
+
end
|
169
|
+
|
170
|
+
attributes = template.build_attributes
|
171
|
+
existing_attribute = attributes.attribute(attribute_name)
|
172
|
+
if existing_attribute
|
173
|
+
if existing_attribute.attribute_type != AttributeType::COLLECTION
|
174
|
+
yield "Attribute #{attribute_name} of model #{template.name} has duplicate definitions!" if block_given?
|
175
|
+
end
|
176
|
+
|
177
|
+
attributes.raw.delete(attribute_name)
|
178
|
+
end
|
179
|
+
|
180
|
+
attributes.build_attribute(attribute_name).tap do |att|
|
181
|
+
COLUMNS_TO_ATTRIBUTES.each do |column, processor|
|
182
|
+
next unless (value = self.get_col_or_nil(row, column))
|
183
|
+
if processor.is_a?(Array)
|
184
|
+
processor, f = processor
|
185
|
+
value = f.call(value)
|
186
|
+
end
|
187
|
+
|
188
|
+
if !att.send(processor).nil? && !att.send(processor).empty?
|
189
|
+
yield "Value for #{processor} on attribute #{attribute_name} has duplicate definitions!" if block_given?
|
190
|
+
end
|
191
|
+
|
192
|
+
att.send(:"#{processor}=", value)
|
193
|
+
end
|
194
|
+
|
195
|
+
if att.attribute_type == AttributeType::LINK && models.find_reciprocal(model_name: template.name, attribute: att).nil?
|
196
|
+
models.build_model(att.link_model_name).build_template.build_attributes.build_attribute(template.name).tap do |rec_att|
|
197
|
+
rec_att.attribute_name = template.name
|
198
|
+
rec_att.display_name = self.prettify(template.name)
|
199
|
+
rec_att.desc = self.prettify(template.name)
|
200
|
+
rec_att.attribute_type = AttributeType::COLLECTION
|
201
|
+
rec_att.link_model_name = template.name
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
att.set_field_defaults!
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def self.prettify(name)
|
210
|
+
name.split('_').map(&:capitalize).join(' ')
|
211
|
+
end
|
212
|
+
|
213
|
+
def self.get_col_or_nil(row, col)
|
214
|
+
c = row[col]&.chomp
|
215
|
+
return nil if c&.empty?
|
216
|
+
c
|
217
|
+
end
|
218
|
+
|
219
|
+
def self.each_csv_row(models = Models.new, model_keys = models.model_keys.sort, &block)
|
220
|
+
yield COLUMNS.map(&:to_s)
|
221
|
+
|
222
|
+
self.ensure_parents(models, model_keys, &block)
|
223
|
+
matrix_constants = {}
|
224
|
+
model_keys.each { |model_name| self.each_model_row(models, model_name, matrix_constants, &block) }
|
225
|
+
|
226
|
+
matrix_constants.each do |digest, options|
|
227
|
+
yield row_from_columns
|
228
|
+
yield row_from_columns(matrix_constant: COPY_OPTIONS_SENTINEL + digest)
|
229
|
+
options.each do |option|
|
230
|
+
yield row_from_columns(matrix_constant: option)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def self.ensure_parents(models, model_keys, &block)
|
236
|
+
q = model_keys.dup
|
237
|
+
seen = Set.new
|
238
|
+
|
239
|
+
until q.empty?
|
240
|
+
model_key = q.shift
|
241
|
+
next if model_key.nil?
|
242
|
+
next if seen.include?(model_key)
|
243
|
+
seen.add(model_key)
|
244
|
+
|
245
|
+
# For models that are only part of the trunk, but not of the tree of model_keys,
|
246
|
+
# we still need their basic information (identifier / parent) for validation and for
|
247
|
+
# potentially creating the required tree dependencies to connect it to a remote tree.
|
248
|
+
unless model_keys.include?(model_key)
|
249
|
+
self.each_model_trunk_row(models, model_key, &block)
|
250
|
+
end
|
251
|
+
|
252
|
+
q.push(*models.model(model_key).template.all_linked_model_names)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def self.each_model_trunk_row(models, model_name, &block)
|
257
|
+
return unless (model = models.model(model_name))
|
258
|
+
|
259
|
+
# Empty link for better visual separation
|
260
|
+
yield row_from_columns
|
261
|
+
yield row_from_columns(model_name: model_name)
|
262
|
+
|
263
|
+
unless model.template.parent.nil?
|
264
|
+
parent_model = models.model(model.template.parent)
|
265
|
+
reciprocal = models.find_reciprocal(model: model, link_model: parent_model)
|
266
|
+
|
267
|
+
yield row_from_columns(
|
268
|
+
identifier: model.template.identifier,
|
269
|
+
parent_model_name: model.template.parent,
|
270
|
+
parent_link_type: reciprocal.attribute_type.to_s
|
271
|
+
)
|
272
|
+
else
|
273
|
+
yield row_from_columns(
|
274
|
+
identifier: model.template.identifier,
|
275
|
+
)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def self.each_model_row(models, model_name, matrix_constants, &block)
|
280
|
+
return unless (model = models.model(model_name))
|
281
|
+
|
282
|
+
self.each_model_trunk_row(models, model_name, &block)
|
283
|
+
model.template.attributes.all.each do |attribute|
|
284
|
+
self.each_attribute_row(models, model, attribute, matrix_constants, &block)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def self.each_attribute_row(models = Models.new, model = Model.new, attribute = Attribute.new, matrix_constants = {}, &block)
|
289
|
+
if attribute.attribute_type == AttributeType::IDENTIFIER
|
290
|
+
# Identifiers for models whose parent link type ends up being a table are non configurable, so we don't
|
291
|
+
# want to include them in the CSV.
|
292
|
+
if models.find_reciprocal(model: model, link_attribute_name: model.template.parent)&.attribute_type == AttributeType::TABLE
|
293
|
+
return
|
294
|
+
end
|
295
|
+
else
|
296
|
+
return unless AttributeValidator.valid_add_row_attribute_types.include?(attribute.attribute_type)
|
297
|
+
end
|
298
|
+
|
299
|
+
options = attribute.options&.join(', ')
|
300
|
+
if attribute.attribute_type == AttributeType::MATRIX
|
301
|
+
# Matrix attribute validations are massive, and functional. For now, we don't support showing and editing
|
302
|
+
# them inline with this spreadsheet. In the future, I think we should possibly introduce the concept of
|
303
|
+
# CONSTANTS or Matrix Types that are managed separately.
|
304
|
+
options = options || ''
|
305
|
+
digest = Digest::MD5.hexdigest(options)
|
306
|
+
matrix_constants[digest] ||= COLUMNS_TO_ATTRIBUTES[:options][1].call(options)["value"]
|
307
|
+
|
308
|
+
options = COPY_OPTIONS_SENTINEL + digest
|
309
|
+
end
|
310
|
+
|
311
|
+
yield row_from_columns(
|
312
|
+
attribute_name: attribute.name,
|
313
|
+
attribute_type: attribute.attribute_type,
|
314
|
+
link_model_name: attribute.link_model_name,
|
315
|
+
reciprocal_link_type: models.find_reciprocal(model: model, attribute: attribute)&.attribute_type,
|
316
|
+
description: attribute.desc,
|
317
|
+
display_name: attribute.display_name,
|
318
|
+
match: attribute.match,
|
319
|
+
format_hint: attribute.format_hint,
|
320
|
+
restricted: attribute.restricted,
|
321
|
+
read_only: attribute.read_only,
|
322
|
+
options: options,
|
323
|
+
attribute_group: attribute.attribute_group,
|
324
|
+
hidden: attribute.hidden,
|
325
|
+
unique: attribute.unique,
|
326
|
+
)
|
327
|
+
end
|
328
|
+
|
329
|
+
def self.row_from_columns(**columns)
|
330
|
+
COLUMNS.map { |c| (columns[c] || '').to_s }
|
331
|
+
end
|
332
|
+
|
333
|
+
class ModelsChangeset < Struct.new(:models, :renames, :matrix_constants, :last_matrix_constant, :last_model_key, keyword_init: true)
|
334
|
+
def initialize(*args)
|
335
|
+
super
|
336
|
+
|
337
|
+
self.models ||= Models.new
|
338
|
+
self.renames ||= {}
|
339
|
+
self.matrix_constants ||= {}
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
@@ -0,0 +1,579 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require_relative '../../json_serializable_struct'
|
3
|
+
require_relative '../../multipart_serializable_nested_hash'
|
4
|
+
require_relative '../../directed_graph'
|
5
|
+
require_relative '../enum'
|
6
|
+
|
7
|
+
# TODO: In the near future, I'd like to transition to specifying apis via SWAGGER and generating model stubs from the
|
8
|
+
# common definitions. For nowe I've written them out by hand here.
|
9
|
+
module Etna
|
10
|
+
module Clients
|
11
|
+
class Magma
|
12
|
+
class RetrievalRequest < Struct.new(:model_name, :attribute_names, :record_names, :project_name, :page, :page_size, :order, :filter, keyword_init: true)
|
13
|
+
include JsonSerializableStruct
|
14
|
+
|
15
|
+
def initialize(**params)
|
16
|
+
super({model_name: 'all', attribute_names: 'all', record_names: []}.update(params))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class QueryRequest < Struct.new(:query, :project_name, keyword_init: true)
|
21
|
+
include JsonSerializableStruct
|
22
|
+
end
|
23
|
+
|
24
|
+
class UpdateRequest < Struct.new(:revisions, :project_name, keyword_init: true)
|
25
|
+
include JsonSerializableStruct
|
26
|
+
include MultipartSerializableNestedHash
|
27
|
+
|
28
|
+
def initialize(**params)
|
29
|
+
super({revisions: {}}.update(params))
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_revision(model_name, record_name, attrs)
|
33
|
+
revision = revisions[model_name] ||= {}
|
34
|
+
record = revision[record_name] ||= {}
|
35
|
+
record.update(attrs)
|
36
|
+
end
|
37
|
+
|
38
|
+
def append_table(parent_model_name, parent_record_name, model_name, attrs, attribute_name = model_name)
|
39
|
+
parent_revision = update_revision(parent_model_name, parent_record_name, {})
|
40
|
+
table = parent_revision[attribute_name] ||= []
|
41
|
+
id = "::#{model_name}#{(revisions[model_name] || {}).length + 1}"
|
42
|
+
table << id
|
43
|
+
update_revision(model_name, id, attrs)
|
44
|
+
id
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class UpdateModelRequest < Struct.new(:project_name, :actions, keyword_init: true)
|
49
|
+
include JsonSerializableStruct
|
50
|
+
|
51
|
+
def initialize(**params)
|
52
|
+
super({actions: []}.update(params))
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_action(action)
|
56
|
+
actions << action
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class AddModelAction < Struct.new(:action_name, :model_name, :parent_model_name, :parent_link_type, :identifier, keyword_init: true)
|
61
|
+
include JsonSerializableStruct
|
62
|
+
|
63
|
+
def initialize(**args)
|
64
|
+
super({action_name: 'add_model'}.update(args))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class AddAttributeAction < Struct.new(:action_name, :model_name, :attribute_name, :type, :description, :display_name, :format_hint, :hidden, :index, :link_model_name, :read_only, :attribute_group, :restricted, :unique, :validation, keyword_init: true)
|
69
|
+
include JsonSerializableStruct
|
70
|
+
|
71
|
+
def initialize(**args)
|
72
|
+
super({action_name: 'add_attribute'}.update(args))
|
73
|
+
end
|
74
|
+
|
75
|
+
def attribute_type=(val)
|
76
|
+
self.type = val
|
77
|
+
end
|
78
|
+
|
79
|
+
def attribute_type
|
80
|
+
self.type
|
81
|
+
end
|
82
|
+
|
83
|
+
def desc=(val)
|
84
|
+
self.description = val
|
85
|
+
end
|
86
|
+
|
87
|
+
def desc
|
88
|
+
self.description
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class AddLinkAction < Struct.new(:action_name, :links, keyword_init: true)
|
93
|
+
include JsonSerializableStruct
|
94
|
+
|
95
|
+
def initialize(**args)
|
96
|
+
super({action_name: 'add_link', links: []}.update(args))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class AddLinkDefinition < Struct.new(:type, :model_name, :attribute_name, keyword_init: true)
|
101
|
+
include JsonSerializableStruct
|
102
|
+
end
|
103
|
+
|
104
|
+
class AddProjectAction < Struct.new(:action_name, keyword_init: true)
|
105
|
+
include JsonSerializableStruct
|
106
|
+
|
107
|
+
def initialize(**args)
|
108
|
+
super({action_name: 'add_project'}.update(args))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class UpdateAttributeAction < Struct.new(:action_name, :model_name, :attribute_name, :description, :display_name, :format_hint, :hidden, :index, :link_model_name, :read_only, :attribute_group, :restricted, :unique, :validation, keyword_init: true)
|
113
|
+
include JsonSerializableStruct
|
114
|
+
|
115
|
+
def initialize(**args)
|
116
|
+
super({action_name: 'update_attribute'}.update(args))
|
117
|
+
end
|
118
|
+
|
119
|
+
def desc=(val)
|
120
|
+
self.description = val
|
121
|
+
end
|
122
|
+
|
123
|
+
def desc
|
124
|
+
self.description
|
125
|
+
end
|
126
|
+
|
127
|
+
def as_json
|
128
|
+
super(keep_nils: true)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class RenameAttributeAction < Struct.new(:action_name, :model_name, :attribute_name, :new_attribute_name, keyword_init: true)
|
133
|
+
include JsonSerializableStruct
|
134
|
+
|
135
|
+
def initialize(**args)
|
136
|
+
super({action_name: 'rename_attribute'}.update(args))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class AttributeValidation < Struct.new(:type, :value, :begin, :end, keyword_init: true)
|
141
|
+
include JsonSerializableStruct
|
142
|
+
end
|
143
|
+
|
144
|
+
class AttributeValidationType < String
|
145
|
+
include Enum
|
146
|
+
REGEXP = AttributeValidationType.new("Regexp")
|
147
|
+
ARRAY = AttributeValidationType.new("Array")
|
148
|
+
RANGE = AttributeValidationType.new("Range")
|
149
|
+
end
|
150
|
+
|
151
|
+
class RetrievalResponse
|
152
|
+
attr_reader :raw
|
153
|
+
|
154
|
+
def initialize(raw = {})
|
155
|
+
@raw = raw
|
156
|
+
end
|
157
|
+
|
158
|
+
def models
|
159
|
+
Models.new(raw['models'])
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
class UpdateModelResponse < RetrievalResponse
|
164
|
+
end
|
165
|
+
|
166
|
+
class QueryResponse
|
167
|
+
attr_reader :raw
|
168
|
+
|
169
|
+
def initialize(raw = {})
|
170
|
+
@raw = raw
|
171
|
+
end
|
172
|
+
|
173
|
+
def answer
|
174
|
+
raw['answer']
|
175
|
+
end
|
176
|
+
|
177
|
+
def format
|
178
|
+
raw['format']
|
179
|
+
end
|
180
|
+
|
181
|
+
def type
|
182
|
+
raw['type']
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
class UpdateResponse < RetrievalResponse
|
187
|
+
end
|
188
|
+
|
189
|
+
class Project
|
190
|
+
attr_reader :raw
|
191
|
+
|
192
|
+
def initialize(raw = {})
|
193
|
+
@raw = raw
|
194
|
+
end
|
195
|
+
|
196
|
+
def models
|
197
|
+
Models.new(raw['models'])
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
class Models
|
202
|
+
attr_reader :raw
|
203
|
+
|
204
|
+
def initialize(raw = {})
|
205
|
+
@raw = raw
|
206
|
+
end
|
207
|
+
|
208
|
+
def model_keys
|
209
|
+
raw.keys
|
210
|
+
end
|
211
|
+
|
212
|
+
def build_model(model_key)
|
213
|
+
Model.new(raw[model_key] ||= {})
|
214
|
+
end
|
215
|
+
|
216
|
+
def model(model_key)
|
217
|
+
return nil unless raw.include?(model_key)
|
218
|
+
Model.new(raw[model_key])
|
219
|
+
end
|
220
|
+
|
221
|
+
def all
|
222
|
+
raw.values.map { |r| Model.new(r) }
|
223
|
+
end
|
224
|
+
|
225
|
+
def +(other)
|
226
|
+
raw_update = {}
|
227
|
+
raw_update[other.name] = other.raw
|
228
|
+
Models.new({}.update(raw).update(raw_update))
|
229
|
+
end
|
230
|
+
|
231
|
+
# Can look up reciprocal links by many means. At minimum, a source model or model name must be provided,
|
232
|
+
# and either a link_attribute_name, attribute, or link_model.
|
233
|
+
def find_reciprocal(
|
234
|
+
model_name: nil,
|
235
|
+
model: self.model(model_name),
|
236
|
+
link_attribute_name: nil,
|
237
|
+
attribute: model&.template&.attributes&.attribute(link_attribute_name),
|
238
|
+
link_model: self.model(attribute&.link_model_name)
|
239
|
+
)
|
240
|
+
return nil if model.nil? || model.name.nil?
|
241
|
+
link_model&.template&.attributes&.all&.find { |a| a.link_model_name == model.name }
|
242
|
+
end
|
243
|
+
|
244
|
+
def to_directed_graph(include_casual_links = false)
|
245
|
+
graph = ::DirectedGraph.new
|
246
|
+
|
247
|
+
model_keys.sort.each do |model_name|
|
248
|
+
graph.add_connection(model(model_name).template.parent, model_name)
|
249
|
+
|
250
|
+
if include_casual_links
|
251
|
+
attributes = model(model_name).template.attributes
|
252
|
+
attributes.attribute_keys.each do |attribute_name|
|
253
|
+
attribute = attributes.attribute(attribute_name)
|
254
|
+
|
255
|
+
linked_model_name = attribute.link_model_name
|
256
|
+
if linked_model_name
|
257
|
+
if attribute.attribute_type == AttributeType::PARENT
|
258
|
+
graph.add_connection(linked_model_name, model_name)
|
259
|
+
elsif attribute.attribute_type == AttributeType::COLLECTION
|
260
|
+
graph.add_connection(model_name, linked_model_name)
|
261
|
+
elsif attribute.attribute_type == AttributeType::CHILD
|
262
|
+
graph.add_connection(model_name, linked_model_name)
|
263
|
+
elsif attribute.attribute_type == AttributeType::LINK
|
264
|
+
graph.add_connection(model_name, linked_model_name)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
graph
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
class Model
|
276
|
+
attr_reader :raw
|
277
|
+
|
278
|
+
def initialize(raw = {})
|
279
|
+
@raw = raw
|
280
|
+
end
|
281
|
+
|
282
|
+
def build_template
|
283
|
+
Template.new(raw['template'] ||= {})
|
284
|
+
end
|
285
|
+
|
286
|
+
def documents
|
287
|
+
Documents.new(raw['documents'])
|
288
|
+
end
|
289
|
+
|
290
|
+
def template
|
291
|
+
Template.new(raw['template'])
|
292
|
+
end
|
293
|
+
|
294
|
+
def name
|
295
|
+
@raw.dig('template', 'name')
|
296
|
+
end
|
297
|
+
|
298
|
+
def count
|
299
|
+
raw['count'] || 0
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
class Documents
|
304
|
+
attr_reader :raw
|
305
|
+
|
306
|
+
def initialize(raw = {})
|
307
|
+
@raw = raw
|
308
|
+
end
|
309
|
+
|
310
|
+
def document_keys
|
311
|
+
raw.keys
|
312
|
+
end
|
313
|
+
|
314
|
+
def +(other)
|
315
|
+
Documents.new({}.update(raw).update(other.raw))
|
316
|
+
end
|
317
|
+
|
318
|
+
def document(document_key)
|
319
|
+
return nil unless raw.include?(document_key)
|
320
|
+
raw[document_key]
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
class Template
|
325
|
+
attr_reader :raw
|
326
|
+
|
327
|
+
def initialize(raw = {})
|
328
|
+
@raw = raw
|
329
|
+
end
|
330
|
+
|
331
|
+
def name
|
332
|
+
raw['name'] || ""
|
333
|
+
end
|
334
|
+
|
335
|
+
def name=(val)
|
336
|
+
raw['name'] = val.to_s
|
337
|
+
end
|
338
|
+
|
339
|
+
def identifier
|
340
|
+
raw['identifier'] || ""
|
341
|
+
end
|
342
|
+
|
343
|
+
def version
|
344
|
+
raw['version'] || 0
|
345
|
+
end
|
346
|
+
|
347
|
+
def version=(val)
|
348
|
+
raw['version'] = val
|
349
|
+
end
|
350
|
+
|
351
|
+
def identifier=(val)
|
352
|
+
raw['identifier'] = val
|
353
|
+
end
|
354
|
+
|
355
|
+
def parent
|
356
|
+
raw['parent']
|
357
|
+
end
|
358
|
+
|
359
|
+
def parent=(val)
|
360
|
+
raw['parent'] = val
|
361
|
+
end
|
362
|
+
|
363
|
+
def attributes
|
364
|
+
Attributes.new(raw['attributes'] ||= {})
|
365
|
+
end
|
366
|
+
|
367
|
+
def build_attributes
|
368
|
+
Attributes.new(raw['attributes'] ||= {})
|
369
|
+
end
|
370
|
+
|
371
|
+
def all_linked_model_names
|
372
|
+
models = [ self.parent, ] + build_attributes.all.map { |v| v.link_model_name }
|
373
|
+
models.select { |m| !m.nil? }.uniq
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
class Attributes
|
378
|
+
attr_reader :raw
|
379
|
+
|
380
|
+
def initialize(raw = {})
|
381
|
+
@raw = raw
|
382
|
+
end
|
383
|
+
|
384
|
+
def attribute_keys
|
385
|
+
raw.keys
|
386
|
+
end
|
387
|
+
|
388
|
+
def attribute(attribute_key)
|
389
|
+
return nil unless raw.include?(attribute_key)
|
390
|
+
Attribute.new(raw[attribute_key])
|
391
|
+
end
|
392
|
+
|
393
|
+
def build_attribute(key)
|
394
|
+
Attribute.new(raw[key] ||= {})
|
395
|
+
end
|
396
|
+
|
397
|
+
def all
|
398
|
+
raw.values.map { |r| Attribute.new(r) }
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
class Attribute
|
403
|
+
attr_reader :raw
|
404
|
+
|
405
|
+
def initialize(raw = {})
|
406
|
+
@raw = raw
|
407
|
+
end
|
408
|
+
|
409
|
+
# Sets certain attribute fields which are implicit, even when not set, to match server behavior.
|
410
|
+
def set_field_defaults!
|
411
|
+
@raw.replace({
|
412
|
+
'hidden' => false,
|
413
|
+
'read_only' => false,
|
414
|
+
'restricted' => false,
|
415
|
+
'validation' => nil,
|
416
|
+
}.merge(@raw))
|
417
|
+
end
|
418
|
+
|
419
|
+
def name
|
420
|
+
@raw['name'] || ""
|
421
|
+
end
|
422
|
+
|
423
|
+
def name=(val)
|
424
|
+
@raw['name'] = val
|
425
|
+
end
|
426
|
+
|
427
|
+
def attribute_name
|
428
|
+
@raw['attribute_name'] || ""
|
429
|
+
end
|
430
|
+
|
431
|
+
def attribute_name=(val)
|
432
|
+
@raw['attribute_name'] = val
|
433
|
+
end
|
434
|
+
|
435
|
+
def attribute_type
|
436
|
+
@raw['attribute_type'] && AttributeType.new(@raw['attribute_type'])
|
437
|
+
end
|
438
|
+
|
439
|
+
def attribute_type=(val)
|
440
|
+
val = val.to_s if val
|
441
|
+
@raw['attribute_type'] = val
|
442
|
+
end
|
443
|
+
|
444
|
+
def link_model_name
|
445
|
+
@raw['link_model_name']
|
446
|
+
end
|
447
|
+
|
448
|
+
def link_model_name=(val)
|
449
|
+
@raw['link_model_name'] = val
|
450
|
+
end
|
451
|
+
|
452
|
+
def unique
|
453
|
+
raw['unique']
|
454
|
+
end
|
455
|
+
|
456
|
+
def unique=(val)
|
457
|
+
raw['unique'] = val
|
458
|
+
end
|
459
|
+
|
460
|
+
def desc
|
461
|
+
raw['desc']
|
462
|
+
end
|
463
|
+
|
464
|
+
def desc=(val)
|
465
|
+
@raw['desc'] = val
|
466
|
+
end
|
467
|
+
|
468
|
+
def display_name
|
469
|
+
raw['display_name']
|
470
|
+
end
|
471
|
+
|
472
|
+
def display_name=(val)
|
473
|
+
raw['display_name'] = val
|
474
|
+
end
|
475
|
+
|
476
|
+
def match
|
477
|
+
raw['match']
|
478
|
+
end
|
479
|
+
|
480
|
+
def restricted
|
481
|
+
raw['restricted']
|
482
|
+
end
|
483
|
+
|
484
|
+
def restricted=(val)
|
485
|
+
raw['restricted'] = val
|
486
|
+
end
|
487
|
+
|
488
|
+
def format_hint
|
489
|
+
raw['format_hint']
|
490
|
+
end
|
491
|
+
|
492
|
+
def format_hint=(val)
|
493
|
+
raw['format_hint'] = val
|
494
|
+
end
|
495
|
+
|
496
|
+
def read_only
|
497
|
+
raw['read_only']
|
498
|
+
end
|
499
|
+
|
500
|
+
def read_only=(val)
|
501
|
+
raw['read_only'] = val
|
502
|
+
end
|
503
|
+
|
504
|
+
def attribute_group
|
505
|
+
raw['attribute_group']
|
506
|
+
end
|
507
|
+
|
508
|
+
def attribute_group=(val)
|
509
|
+
raw['attribute_group'] = val
|
510
|
+
end
|
511
|
+
|
512
|
+
def hidden
|
513
|
+
raw['hidden']
|
514
|
+
end
|
515
|
+
|
516
|
+
def hidden=(val)
|
517
|
+
raw['hidden'] = val
|
518
|
+
end
|
519
|
+
|
520
|
+
def validation
|
521
|
+
raw['validation']
|
522
|
+
end
|
523
|
+
|
524
|
+
def validation=(val)
|
525
|
+
raw['validation'] = val
|
526
|
+
end
|
527
|
+
|
528
|
+
def options
|
529
|
+
raw['options']
|
530
|
+
end
|
531
|
+
|
532
|
+
# NOTE! The Attribute class returns description as desc, where as actions take it in as description.
|
533
|
+
# There are shortcut methods that try to handle this on the action class side of things. Ideally we would
|
534
|
+
# make this more consistent in the near future.
|
535
|
+
COPYABLE_ATTRIBUTE_ATTRIBUTES = [
|
536
|
+
:attribute_name, :attribute_type, :desc, :display_name, :format_hint,
|
537
|
+
:hidden, :link_model_name, :read_only, :attribute_group, :unique, :validation,
|
538
|
+
:restricted
|
539
|
+
]
|
540
|
+
|
541
|
+
EDITABLE_ATTRIBUTE_ATTRIBUTES = UpdateAttributeAction.members & COPYABLE_ATTRIBUTE_ATTRIBUTES
|
542
|
+
|
543
|
+
def self.copy(source, dest, attributes: COPYABLE_ATTRIBUTE_ATTRIBUTES)
|
544
|
+
attributes.each do |attr_name|
|
545
|
+
next unless dest.respond_to?(:"#{attr_name}=")
|
546
|
+
source_v = source.send(attr_name)
|
547
|
+
dest.send(:"#{attr_name}=", source_v)
|
548
|
+
end
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
class AttributeType < String
|
553
|
+
include Enum
|
554
|
+
STRING = AttributeType.new("string")
|
555
|
+
DATE_TIME = AttributeType.new("date_time")
|
556
|
+
BOOLEAN = AttributeType.new("boolean")
|
557
|
+
CHILD = AttributeType.new("child")
|
558
|
+
COLLECTION = AttributeType.new("collection")
|
559
|
+
FILE = AttributeType.new("file")
|
560
|
+
FLOAT = AttributeType.new("float")
|
561
|
+
IDENTIFIER = AttributeType.new("identifier")
|
562
|
+
IMAGE = AttributeType.new("image")
|
563
|
+
INTEGER = AttributeType.new("integer")
|
564
|
+
LINK = AttributeType.new("link")
|
565
|
+
MATCH = AttributeType.new("match")
|
566
|
+
MATRIX = AttributeType.new("matrix")
|
567
|
+
PARENT = AttributeType.new("parent")
|
568
|
+
TABLE = AttributeType.new("table")
|
569
|
+
end
|
570
|
+
|
571
|
+
class ParentLinkType < String
|
572
|
+
include Enum
|
573
|
+
CHILD = ParentLinkType.new("child")
|
574
|
+
COLLECTION = ParentLinkType.new("collection")
|
575
|
+
TABLE = ParentLinkType.new("table")
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
579
|
+
end
|