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.
- checksums.yaml +4 -4
- data/bin/etna +63 -0
- data/etna.completion +926 -0
- data/etna_app.completion +133 -0
- data/ext/completions/extconf.rb +20 -0
- data/lib/commands.rb +368 -0
- data/lib/etna.rb +6 -0
- data/lib/etna/application.rb +38 -20
- data/lib/etna/client.rb +60 -29
- 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 +2 -0
- data/lib/etna/clients/magma/client.rb +24 -9
- 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 +323 -9
- 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 +1 -0
- data/lib/etna/clients/metis/client.rb +207 -5
- data/lib/etna/clients/metis/models.rb +174 -3
- 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 +235 -5
- data/lib/etna/controller.rb +4 -0
- data/lib/etna/environment_scoped.rb +19 -0
- data/lib/etna/generate_autocompletion_script.rb +130 -0
- data/lib/etna/json_serializable_struct.rb +6 -3
- data/lib/etna/logger.rb +0 -3
- data/lib/etna/multipart_serializable_nested_hash.rb +6 -1
- data/lib/etna/route.rb +1 -1
- data/lib/etna/spec/vcr.rb +98 -0
- data/lib/etna/templates/attribute_actions_template.json +43 -0
- data/lib/etna/test_auth.rb +3 -1
- data/lib/etna/user.rb +4 -0
- data/lib/helpers.rb +81 -0
- metadata +47 -7
@@ -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
|
@@ -1,13 +1,15 @@
|
|
1
|
+
require 'ostruct'
|
1
2
|
require_relative '../../json_serializable_struct'
|
2
3
|
require_relative '../../multipart_serializable_nested_hash'
|
3
4
|
require_relative '../../directed_graph'
|
5
|
+
require_relative '../enum'
|
4
6
|
|
5
7
|
# TODO: In the near future, I'd like to transition to specifying apis via SWAGGER and generating model stubs from the
|
6
8
|
# common definitions. For nowe I've written them out by hand here.
|
7
9
|
module Etna
|
8
10
|
module Clients
|
9
11
|
class Magma
|
10
|
-
class RetrievalRequest < Struct.new(:model_name, :attribute_names, :record_names, :project_name, keyword_init: true)
|
12
|
+
class RetrievalRequest < Struct.new(:model_name, :attribute_names, :record_names, :project_name, :page, :page_size, :order, :filter, keyword_init: true)
|
11
13
|
include JsonSerializableStruct
|
12
14
|
|
13
15
|
def initialize(**params)
|
@@ -27,11 +29,20 @@ module Etna
|
|
27
29
|
super({revisions: {}}.update(params))
|
28
30
|
end
|
29
31
|
|
30
|
-
def update_revision(model_name, record_name,
|
32
|
+
def update_revision(model_name, record_name, attrs)
|
31
33
|
revision = revisions[model_name] ||= {}
|
32
34
|
record = revision[record_name] ||= {}
|
33
35
|
record.update(attrs)
|
34
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
|
35
46
|
end
|
36
47
|
|
37
48
|
class UpdateModelRequest < Struct.new(:project_name, :actions, keyword_init: true)
|
@@ -42,15 +53,88 @@ module Etna
|
|
42
53
|
end
|
43
54
|
|
44
55
|
def add_action(action)
|
45
|
-
|
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))
|
46
65
|
end
|
47
66
|
end
|
48
67
|
|
49
|
-
class AddAttributeAction < Struct.new(:action_name, :model_name, :attribute_name, :type, :description, :display_name, :format_hint, :hidden, :index, :link_model_name, :read_only, :restricted, :unique, :validation, keyword_init: true)
|
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)
|
50
69
|
include JsonSerializableStruct
|
70
|
+
|
51
71
|
def initialize(**args)
|
52
72
|
super({action_name: 'add_attribute'}.update(args))
|
53
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
|
54
138
|
end
|
55
139
|
|
56
140
|
class AttributeValidation < Struct.new(:type, :value, :begin, :end, keyword_init: true)
|
@@ -58,6 +142,7 @@ module Etna
|
|
58
142
|
end
|
59
143
|
|
60
144
|
class AttributeValidationType < String
|
145
|
+
include Enum
|
61
146
|
REGEXP = AttributeValidationType.new("Regexp")
|
62
147
|
ARRAY = AttributeValidationType.new("Array")
|
63
148
|
RANGE = AttributeValidationType.new("Range")
|
@@ -101,6 +186,18 @@ module Etna
|
|
101
186
|
class UpdateResponse < RetrievalResponse
|
102
187
|
end
|
103
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
|
+
|
104
201
|
class Models
|
105
202
|
attr_reader :raw
|
106
203
|
|
@@ -112,14 +209,42 @@ module Etna
|
|
112
209
|
raw.keys
|
113
210
|
end
|
114
211
|
|
212
|
+
def build_model(model_key)
|
213
|
+
Model.new(raw[model_key] ||= {})
|
214
|
+
end
|
215
|
+
|
115
216
|
def model(model_key)
|
217
|
+
return nil unless raw.include?(model_key)
|
116
218
|
Model.new(raw[model_key])
|
117
219
|
end
|
118
220
|
|
119
|
-
def
|
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)
|
120
245
|
graph = ::DirectedGraph.new
|
121
246
|
|
122
|
-
model_keys.each do |model_name|
|
247
|
+
model_keys.sort.each do |model_name|
|
123
248
|
graph.add_connection(model(model_name).template.parent, model_name)
|
124
249
|
|
125
250
|
if include_casual_links
|
@@ -154,6 +279,10 @@ module Etna
|
|
154
279
|
@raw = raw
|
155
280
|
end
|
156
281
|
|
282
|
+
def build_template
|
283
|
+
Template.new(raw['template'] ||= {})
|
284
|
+
end
|
285
|
+
|
157
286
|
def documents
|
158
287
|
Documents.new(raw['documents'])
|
159
288
|
end
|
@@ -161,6 +290,14 @@ module Etna
|
|
161
290
|
def template
|
162
291
|
Template.new(raw['template'])
|
163
292
|
end
|
293
|
+
|
294
|
+
def name
|
295
|
+
@raw.dig('template', 'name')
|
296
|
+
end
|
297
|
+
|
298
|
+
def count
|
299
|
+
raw['count'] || 0
|
300
|
+
end
|
164
301
|
end
|
165
302
|
|
166
303
|
class Documents
|
@@ -174,7 +311,12 @@ module Etna
|
|
174
311
|
raw.keys
|
175
312
|
end
|
176
313
|
|
314
|
+
def +(other)
|
315
|
+
Documents.new({}.update(raw).update(other.raw))
|
316
|
+
end
|
317
|
+
|
177
318
|
def document(document_key)
|
319
|
+
return nil unless raw.include?(document_key)
|
178
320
|
raw[document_key]
|
179
321
|
end
|
180
322
|
end
|
@@ -190,16 +332,45 @@ module Etna
|
|
190
332
|
raw['name'] || ""
|
191
333
|
end
|
192
334
|
|
335
|
+
def name=(val)
|
336
|
+
raw['name'] = val.to_s
|
337
|
+
end
|
338
|
+
|
193
339
|
def identifier
|
194
340
|
raw['identifier'] || ""
|
195
341
|
end
|
196
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
|
+
|
197
355
|
def parent
|
198
356
|
raw['parent']
|
199
357
|
end
|
200
358
|
|
359
|
+
def parent=(val)
|
360
|
+
raw['parent'] = val
|
361
|
+
end
|
362
|
+
|
201
363
|
def attributes
|
202
|
-
Attributes.new(raw['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
|
203
374
|
end
|
204
375
|
end
|
205
376
|
|
@@ -215,8 +386,17 @@ module Etna
|
|
215
386
|
end
|
216
387
|
|
217
388
|
def attribute(attribute_key)
|
389
|
+
return nil unless raw.include?(attribute_key)
|
218
390
|
Attribute.new(raw[attribute_key])
|
219
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
|
220
400
|
end
|
221
401
|
|
222
402
|
class Attribute
|
@@ -226,24 +406,151 @@ module Etna
|
|
226
406
|
@raw = raw
|
227
407
|
end
|
228
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
|
+
|
229
419
|
def name
|
230
420
|
@raw['name'] || ""
|
231
421
|
end
|
232
422
|
|
423
|
+
def name=(val)
|
424
|
+
@raw['name'] = val
|
425
|
+
end
|
426
|
+
|
233
427
|
def attribute_name
|
234
428
|
@raw['attribute_name'] || ""
|
235
429
|
end
|
236
430
|
|
431
|
+
def attribute_name=(val)
|
432
|
+
@raw['attribute_name'] = val
|
433
|
+
end
|
434
|
+
|
237
435
|
def attribute_type
|
238
436
|
@raw['attribute_type'] && AttributeType.new(@raw['attribute_type'])
|
239
437
|
end
|
240
438
|
|
439
|
+
def attribute_type=(val)
|
440
|
+
val = val.to_s if val
|
441
|
+
@raw['attribute_type'] = val
|
442
|
+
end
|
443
|
+
|
241
444
|
def link_model_name
|
242
|
-
raw['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
|
243
549
|
end
|
244
550
|
end
|
245
551
|
|
246
552
|
class AttributeType < String
|
553
|
+
include Enum
|
247
554
|
STRING = AttributeType.new("string")
|
248
555
|
DATE_TIME = AttributeType.new("date_time")
|
249
556
|
BOOLEAN = AttributeType.new("boolean")
|
@@ -260,6 +567,13 @@ module Etna
|
|
260
567
|
PARENT = AttributeType.new("parent")
|
261
568
|
TABLE = AttributeType.new("table")
|
262
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
|
263
577
|
end
|
264
578
|
end
|
265
|
-
end
|
579
|
+
end
|