etna 0.1.15 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +4 -0
- data/lib/helpers.rb +90 -0
- metadata +70 -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,354 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require_relative '../../../csvs'
|
3
|
+
|
4
|
+
module Etna
|
5
|
+
module Clients
|
6
|
+
class Magma
|
7
|
+
module ModelsCsv
|
8
|
+
module Prettify
|
9
|
+
def prettify(name)
|
10
|
+
name.split('_').map(&:capitalize).join(' ')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Exporter < Etna::CsvExporter
|
15
|
+
include Prettify
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
super([
|
19
|
+
:comments,
|
20
|
+
:model_name, :identifier, :parent_model_name, :parent_link_type,
|
21
|
+
:attribute_name,
|
22
|
+
:new_attribute_name,
|
23
|
+
:attribute_type,
|
24
|
+
:link_model_name,
|
25
|
+
:description,
|
26
|
+
:display_name,
|
27
|
+
:format_hint,
|
28
|
+
:restricted,
|
29
|
+
:read_only,
|
30
|
+
:options,
|
31
|
+
:match,
|
32
|
+
:attribute_group,
|
33
|
+
:hidden,
|
34
|
+
:unique,
|
35
|
+
:matrix_constant,
|
36
|
+
])
|
37
|
+
end
|
38
|
+
|
39
|
+
def write_models(models, model_keys = models.model_keys.sort, output_io: nil, filename: nil)
|
40
|
+
with_row_writeable(filename: filename, output_io: output_io) do |row_writeable|
|
41
|
+
write_model_rows(models, model_keys, row_writeable)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_model_rows(models, model_keys, row_writeable)
|
46
|
+
ensure_parents(models, model_keys, row_writeable)
|
47
|
+
|
48
|
+
matrix_constants = {}
|
49
|
+
model_keys.each { |model_name| each_model_row(models, model_name, matrix_constants, row_writeable) }
|
50
|
+
|
51
|
+
matrix_constants.each do |digest, options|
|
52
|
+
row_writeable.write
|
53
|
+
row_writeable.write(matrix_constant: COPY_OPTIONS_SENTINEL + digest)
|
54
|
+
options.each do |option|
|
55
|
+
row_writeable.write(matrix_constant: option)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def ensure_parents(models, model_keys, row_writeable)
|
61
|
+
q = model_keys.dup
|
62
|
+
seen = Set.new
|
63
|
+
|
64
|
+
until q.empty?
|
65
|
+
model_key = q.shift
|
66
|
+
next if model_key.nil?
|
67
|
+
next if seen.include?(model_key)
|
68
|
+
seen.add(model_key)
|
69
|
+
|
70
|
+
# For models that are only part of the trunk, but not of the tree of model_keys,
|
71
|
+
# we still need their basic information (identifier / parent) for validation and for
|
72
|
+
# potentially creating the required tree dependencies to connect it to a remote tree.
|
73
|
+
unless model_keys.include?(model_key)
|
74
|
+
each_model_trunk_row(models, model_key, row_writeable)
|
75
|
+
end
|
76
|
+
|
77
|
+
q.push(*models.model(model_key).template.all_linked_model_names)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def each_model_trunk_row(models, model_name, row_writeable)
|
82
|
+
return unless (model = models.model(model_name))
|
83
|
+
|
84
|
+
# Empty link for better visual separation
|
85
|
+
row_writeable.write
|
86
|
+
row_writeable.write(model_name: model_name)
|
87
|
+
|
88
|
+
if !model.template.parent.nil?
|
89
|
+
parent_model = models.model(model.template.parent)
|
90
|
+
reciprocal = models.find_reciprocal(model: model, link_model: parent_model)
|
91
|
+
|
92
|
+
row_writeable.write(
|
93
|
+
identifier: model.template.identifier,
|
94
|
+
parent_model_name: model.template.parent,
|
95
|
+
parent_link_type: reciprocal.attribute_type.to_s
|
96
|
+
)
|
97
|
+
else
|
98
|
+
row_writeable.write(
|
99
|
+
identifier: model.template.identifier,
|
100
|
+
)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def each_model_row(models, model_name, matrix_constants, row_writeable)
|
105
|
+
return unless (model = models.model(model_name))
|
106
|
+
|
107
|
+
each_model_trunk_row(models, model_name, row_writeable)
|
108
|
+
model.template.attributes.all.each do |attribute|
|
109
|
+
each_attribute_row(models, model, attribute, matrix_constants, row_writeable)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def each_attribute_row(models, model, attribute, matrix_constants, row_writeable)
|
114
|
+
if attribute.attribute_type == Etna::Clients::Magma::AttributeType::IDENTIFIER
|
115
|
+
# Identifiers for models whose parent link type ends up being a table are non configurable, so we don't
|
116
|
+
# want to include them in the CSV.
|
117
|
+
if models.find_reciprocal(model: model, link_attribute_name: model.template.parent)&.attribute_type == Etna::Clients::Magma::AttributeType::TABLE
|
118
|
+
return
|
119
|
+
end
|
120
|
+
else
|
121
|
+
return unless Etna::Clients::Magma::AttributeValidator.valid_add_row_attribute_types.include?(attribute.attribute_type)
|
122
|
+
end
|
123
|
+
|
124
|
+
options = attribute.options&.join(', ')
|
125
|
+
if attribute.attribute_type == Etna::Clients::Magma::AttributeType::MATRIX
|
126
|
+
# Matrix attribute validations are massive, and functional. For now, we don't support showing and editing
|
127
|
+
# them inline with this spreadsheet. In the future, I think we should possibly introduce the concept of
|
128
|
+
# CONSTANTS or Matrix Types that are managed separately.
|
129
|
+
options = options || ''
|
130
|
+
digest = Digest::MD5.hexdigest(options)
|
131
|
+
matrix_constants[digest] ||= options.split(',').map(&:strip)
|
132
|
+
|
133
|
+
options = COPY_OPTIONS_SENTINEL + digest
|
134
|
+
end
|
135
|
+
|
136
|
+
row_writeable.write(
|
137
|
+
attribute_name: attribute.name,
|
138
|
+
attribute_type: attribute.attribute_type,
|
139
|
+
link_model_name: attribute.link_model_name,
|
140
|
+
reciprocal_link_type: models.find_reciprocal(model: model, attribute: attribute)&.attribute_type,
|
141
|
+
description: attribute.desc,
|
142
|
+
display_name: attribute.display_name,
|
143
|
+
match: attribute.match,
|
144
|
+
format_hint: attribute.format_hint,
|
145
|
+
restricted: attribute.restricted,
|
146
|
+
read_only: attribute.read_only,
|
147
|
+
options: options,
|
148
|
+
attribute_group: attribute.attribute_group,
|
149
|
+
hidden: attribute.hidden,
|
150
|
+
unique: attribute.unique,
|
151
|
+
)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class ModelsChangeset < Struct.new(:models, :renames, :matrix_constants, keyword_init: true)
|
156
|
+
def initialize(*args)
|
157
|
+
super
|
158
|
+
|
159
|
+
self.models ||= Models.new
|
160
|
+
self.renames ||= {}
|
161
|
+
self.matrix_constants ||= {}
|
162
|
+
end
|
163
|
+
|
164
|
+
def build_renames(model_name)
|
165
|
+
renames[model_name] ||= {}
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
COPY_OPTIONS_SENTINEL = '$copy_from_original$'
|
170
|
+
|
171
|
+
class Importer < Etna::CsvImporter
|
172
|
+
include Prettify
|
173
|
+
|
174
|
+
# Columns of the row, _after format_row is called_, that should be applied to an attribute.
|
175
|
+
# This should line up with the attribute names _on the model itself_.
|
176
|
+
ATTRIBUTE_ROW_ENTRIES = [
|
177
|
+
:attribute_type,
|
178
|
+
:link_model_name, :desc,
|
179
|
+
:display_name, :format_hint,
|
180
|
+
:restricted, :read_only,
|
181
|
+
:validation, :attribute_group,
|
182
|
+
:hidden, :unique,
|
183
|
+
]
|
184
|
+
|
185
|
+
def initialize
|
186
|
+
super(&method(:format_row))
|
187
|
+
end
|
188
|
+
|
189
|
+
def format_row(row)
|
190
|
+
replace_row_column(row, :attribute_type) { |s| AttributeType.new(s) }
|
191
|
+
replace_row_column(row, :desc) { row.delete(:description) }
|
192
|
+
replace_row_column(row, :restricted, &COLUMN_AS_BOOLEAN)
|
193
|
+
replace_row_column(row, :read_only, &COLUMN_AS_BOOLEAN)
|
194
|
+
replace_row_column(row, :options) { |s| {"type" => "Array", "value" => s.split(',').map(&:strip)} }
|
195
|
+
replace_row_column(row, :match) { |s| {"type" => "Regexp", "value" => Regexp.new(s).source} }
|
196
|
+
replace_row_column(row, :validation) { row[:options] || row[:match] }
|
197
|
+
replace_row_column(row, :hidden, &COLUMN_AS_BOOLEAN)
|
198
|
+
replace_row_column(row, :unique, &COLUMN_AS_BOOLEAN)
|
199
|
+
end
|
200
|
+
|
201
|
+
def process_row(row_processor, changeset)
|
202
|
+
models = changeset.models
|
203
|
+
|
204
|
+
process_matrix_constants(changeset, row_processor)
|
205
|
+
process_model_config(models, row_processor)
|
206
|
+
process_parent_config(models, row_processor)
|
207
|
+
process_attribute_properties(changeset, row_processor)
|
208
|
+
end
|
209
|
+
|
210
|
+
def prepare_changeset(filename: nil, input_io: nil, &validation_err_block)
|
211
|
+
ModelsChangeset.new.tap do |changeset|
|
212
|
+
context = {}
|
213
|
+
each_csv_row(filename: filename, input_io: input_io) do |row, lineno|
|
214
|
+
p = NestedRowProcessor.new(row, lineno, context)
|
215
|
+
process_row(p, changeset)
|
216
|
+
end
|
217
|
+
|
218
|
+
# After configuring attributes, set defaults for certain attribute properties.
|
219
|
+
changeset.models.all.each do |model|
|
220
|
+
model.template.attributes.all.each do |attribute|
|
221
|
+
attribute.set_field_defaults!
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
rescue ImportError => e
|
226
|
+
validation_err_block.call(e.message)
|
227
|
+
end
|
228
|
+
|
229
|
+
|
230
|
+
private
|
231
|
+
|
232
|
+
def process_model_config(models, row_processor)
|
233
|
+
row_processor.process(:model_name) do |model_name|
|
234
|
+
models.build_model(model_name).build_template.tap { |t| t.name = model_name }
|
235
|
+
end
|
236
|
+
row_processor.process(:identifier, :model_name) do |identifier, template|
|
237
|
+
template.identifier = identifier
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def process_parent_config(models, row_processor)
|
242
|
+
row_processor.process(:parent_model_name, :model_name) do |parent_model_name, template|
|
243
|
+
if template.parent && !template.parent.empty? && template.parent != parent_model_name
|
244
|
+
raise ImportError.new("Model #{template.name} was provided multiple parent_model_names: #{template.parent} and #{parent_model_name}")
|
245
|
+
end
|
246
|
+
|
247
|
+
template.parent = parent_model_name
|
248
|
+
|
249
|
+
template.build_attributes.build_attribute(template.parent).tap do |parent_att|
|
250
|
+
parent_att.name = parent_att.attribute_name = parent_model_name
|
251
|
+
parent_att.attribute_type = Etna::Clients::Magma::AttributeType::PARENT
|
252
|
+
parent_att.link_model_name = parent_model_name
|
253
|
+
parent_att.desc = prettify(parent_model_name)
|
254
|
+
parent_att.display_name = prettify(parent_model_name)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
row_processor.process(:parent_link_type, :model_name, :parent_model_name) do |parent_link_type, template|
|
259
|
+
reciprocal = models.find_reciprocal(model_name: template.name, link_attribute_name: template.parent)
|
260
|
+
if reciprocal && reciprocal.attribute_type.to_s != parent_link_type
|
261
|
+
raise ImportError.new("Model #{template.name} was provided multiple parent_link_types: #{reciprocal.attribute_type} and #{parent_link_type}")
|
262
|
+
end
|
263
|
+
|
264
|
+
if reciprocal && reciprocal.attribute_name != template.name
|
265
|
+
raise ImportError.new("Model #{template.name} is linked to #{template.parent}, but the reciprocal link is misnamed as '#{reciprocal.attribute_name}'.")
|
266
|
+
end
|
267
|
+
|
268
|
+
models.build_model(template.parent).tap do |parent_model|
|
269
|
+
parent_model_attribute_by_model_name = parent_model.build_template.build_attributes.attribute(template.name)
|
270
|
+
if parent_model_attribute_by_model_name && !reciprocal
|
271
|
+
raise ImportError.new("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}.")
|
272
|
+
end
|
273
|
+
|
274
|
+
parent_model.build_template.build_attributes.build_attribute(template.name).tap do |attr|
|
275
|
+
attr.attribute_name = attr.name = template.name
|
276
|
+
attr.attribute_type = parent_link_type
|
277
|
+
attr.link_model_name = template.name
|
278
|
+
attr.desc = prettify(template.name)
|
279
|
+
attr.display_name = prettify(template.name)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def process_matrix_constants(changeset, row_processor)
|
286
|
+
row_processor.process(:matrix_constant) do |matrix_constant|
|
287
|
+
# Lines that start with the sentinel are a distinct 'group'
|
288
|
+
if matrix_constant.start_with?(COPY_OPTIONS_SENTINEL)
|
289
|
+
matrix_constant = matrix_constant.slice((COPY_OPTIONS_SENTINEL.length)..-1)
|
290
|
+
changeset.matrix_constants[matrix_constant] = []
|
291
|
+
else
|
292
|
+
# Until the sentinel is seen again, all further entries belong to the set.
|
293
|
+
changeset.matrix_constants[changeset.matrix_constants.keys.last] << matrix_constant
|
294
|
+
end
|
295
|
+
|
296
|
+
matrix_constant
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def process_attribute_properties(changeset, row_processor)
|
301
|
+
models = changeset.models
|
302
|
+
|
303
|
+
row_processor.process(:attribute_name, :model_name) do |attribute_name, template|
|
304
|
+
attributes = template.build_attributes
|
305
|
+
|
306
|
+
existing_attribute = attributes.attribute(attribute_name)
|
307
|
+
if existing_attribute
|
308
|
+
if existing_attribute.attribute_type != Etna::Clients::Magma::AttributeType::COLLECTION
|
309
|
+
raise ImportError.new("Attribute #{attribute_name} of model #{template.name} has duplicate definitions!")
|
310
|
+
end
|
311
|
+
|
312
|
+
# Clear its definition; implicit attributes are overwritten by explicit definitions if they exist.
|
313
|
+
attributes.raw.delete(attribute_name)
|
314
|
+
end
|
315
|
+
|
316
|
+
attributes.build_attribute(attribute_name).tap { |att| att.name = att.attribute_name = attribute_name }
|
317
|
+
end
|
318
|
+
|
319
|
+
row_processor.process(:new_attribute_name, :attribute_name, :model_name) do |new_attribute_name, att, template|
|
320
|
+
renames = changeset.build_renames(template.name)
|
321
|
+
if renames.include?(att.name) && renames[att.name] != new_attribute_name
|
322
|
+
raise ImportError.new("Found multiple new_attribute_name values for #{template.name}'s #{att.name}': #{new_attribute_name} or #{renames[att.name]}?")
|
323
|
+
end
|
324
|
+
|
325
|
+
renames[att.attribute_name] = new_attribute_name
|
326
|
+
end
|
327
|
+
|
328
|
+
ATTRIBUTE_ROW_ENTRIES.each do |prop_name|
|
329
|
+
row_processor.process(prop_name, :model_name, :attribute_name) do |value, template, att|
|
330
|
+
if att.raw.include?(prop_name.to_s)
|
331
|
+
raise ImportError.new("Value for #{prop_name} on attribute #{att.attribute_name} has duplicate definitions!")
|
332
|
+
end
|
333
|
+
|
334
|
+
att.send(:"#{prop_name}=", value)
|
335
|
+
|
336
|
+
if att.attribute_type && att.link_model_name
|
337
|
+
if att.attribute_type == Etna::Clients::Magma::AttributeType::LINK && models.find_reciprocal(model_name: template.name, attribute: att).nil?
|
338
|
+
models.build_model(att.link_model_name).build_template.build_attributes.build_attribute(template.name).tap do |rec_att|
|
339
|
+
rec_att.attribute_name = rec_att.name = template.name
|
340
|
+
rec_att.display_name = prettify(template.name)
|
341
|
+
rec_att.desc = prettify(template.name)
|
342
|
+
rec_att.attribute_type = Etna::Clients::Magma::AttributeType::COLLECTION
|
343
|
+
rec_att.link_model_name = template.name
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
@@ -0,0 +1,630 @@
|
|
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 dictionary
|
372
|
+
Dictionary.new(raw['dictionary'] ||= {})
|
373
|
+
end
|
374
|
+
|
375
|
+
def build_dictionary
|
376
|
+
Dictionary.new(raw['dictionary'] ||= {})
|
377
|
+
end
|
378
|
+
|
379
|
+
def all_linked_model_names
|
380
|
+
models = [ self.parent, ] + build_attributes.all.map { |v| v.link_model_name }
|
381
|
+
models.select { |m| !m.nil? }.uniq
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
class Dictionary
|
386
|
+
attr_reader :raw
|
387
|
+
|
388
|
+
def initialize(raw = {})
|
389
|
+
@raw = raw
|
390
|
+
end
|
391
|
+
|
392
|
+
def dictionary_keys
|
393
|
+
raw.keys
|
394
|
+
end
|
395
|
+
|
396
|
+
def dictionary_model
|
397
|
+
raw['dictionary_model']
|
398
|
+
end
|
399
|
+
|
400
|
+
def dictionary_model=(val)
|
401
|
+
@raw['dictionary_model'] = val
|
402
|
+
end
|
403
|
+
|
404
|
+
def model_name
|
405
|
+
raw['model_name']
|
406
|
+
end
|
407
|
+
|
408
|
+
def model_name=(val)
|
409
|
+
@raw['model_name'] = val
|
410
|
+
end
|
411
|
+
|
412
|
+
def attributes
|
413
|
+
raw['attributes']
|
414
|
+
end
|
415
|
+
|
416
|
+
def attributes=(val)
|
417
|
+
@raw['attributes'] = val
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
class Attributes
|
422
|
+
attr_reader :raw
|
423
|
+
|
424
|
+
def initialize(raw = {})
|
425
|
+
@raw = raw
|
426
|
+
end
|
427
|
+
|
428
|
+
def attribute_keys
|
429
|
+
raw.keys
|
430
|
+
end
|
431
|
+
|
432
|
+
def attribute(attribute_key)
|
433
|
+
return nil unless raw.include?(attribute_key)
|
434
|
+
Attribute.new(raw[attribute_key])
|
435
|
+
end
|
436
|
+
|
437
|
+
def build_attribute(key)
|
438
|
+
Attribute.new(raw[key] ||= {})
|
439
|
+
end
|
440
|
+
|
441
|
+
def all
|
442
|
+
raw.values.map { |r| Attribute.new(r) }
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
class Attribute
|
447
|
+
attr_reader :raw
|
448
|
+
|
449
|
+
def initialize(raw = {})
|
450
|
+
@raw = raw
|
451
|
+
end
|
452
|
+
|
453
|
+
# Sets certain attribute fields which are implicit, even when not set, to match server behavior.
|
454
|
+
def set_field_defaults!
|
455
|
+
@raw.replace({
|
456
|
+
'hidden' => false,
|
457
|
+
'read_only' => false,
|
458
|
+
'restricted' => false,
|
459
|
+
'validation' => nil,
|
460
|
+
}.merge(@raw))
|
461
|
+
end
|
462
|
+
|
463
|
+
def name
|
464
|
+
@raw['name'] || ""
|
465
|
+
end
|
466
|
+
|
467
|
+
def name=(val)
|
468
|
+
@raw['name'] = val
|
469
|
+
end
|
470
|
+
|
471
|
+
def attribute_name
|
472
|
+
@raw['attribute_name'] || ""
|
473
|
+
end
|
474
|
+
|
475
|
+
def attribute_name=(val)
|
476
|
+
@raw['attribute_name'] = val
|
477
|
+
end
|
478
|
+
|
479
|
+
def attribute_type
|
480
|
+
@raw['attribute_type'] && AttributeType.new(@raw['attribute_type'])
|
481
|
+
end
|
482
|
+
|
483
|
+
def is_project_name_reference?(model_name)
|
484
|
+
return true if model_name == 'project' && attribute_type == AttributeType::IDENTIFIER
|
485
|
+
return true if link_model_name == 'project'
|
486
|
+
false
|
487
|
+
end
|
488
|
+
|
489
|
+
def attribute_type=(val)
|
490
|
+
val = val.to_s if val
|
491
|
+
@raw['attribute_type'] = val
|
492
|
+
end
|
493
|
+
|
494
|
+
def link_model_name
|
495
|
+
@raw['link_model_name']
|
496
|
+
end
|
497
|
+
|
498
|
+
def link_model_name=(val)
|
499
|
+
@raw['link_model_name'] = val
|
500
|
+
end
|
501
|
+
|
502
|
+
def unique
|
503
|
+
raw['unique']
|
504
|
+
end
|
505
|
+
|
506
|
+
def unique=(val)
|
507
|
+
raw['unique'] = val
|
508
|
+
end
|
509
|
+
|
510
|
+
def desc
|
511
|
+
raw['desc']
|
512
|
+
end
|
513
|
+
|
514
|
+
def desc=(val)
|
515
|
+
@raw['desc'] = val
|
516
|
+
end
|
517
|
+
|
518
|
+
def display_name
|
519
|
+
raw['display_name']
|
520
|
+
end
|
521
|
+
|
522
|
+
def display_name=(val)
|
523
|
+
raw['display_name'] = val
|
524
|
+
end
|
525
|
+
|
526
|
+
def match
|
527
|
+
raw['match']
|
528
|
+
end
|
529
|
+
|
530
|
+
def restricted
|
531
|
+
raw['restricted']
|
532
|
+
end
|
533
|
+
|
534
|
+
def restricted=(val)
|
535
|
+
raw['restricted'] = val
|
536
|
+
end
|
537
|
+
|
538
|
+
def format_hint
|
539
|
+
raw['format_hint']
|
540
|
+
end
|
541
|
+
|
542
|
+
def format_hint=(val)
|
543
|
+
raw['format_hint'] = val
|
544
|
+
end
|
545
|
+
|
546
|
+
def read_only
|
547
|
+
raw['read_only']
|
548
|
+
end
|
549
|
+
|
550
|
+
def read_only=(val)
|
551
|
+
raw['read_only'] = val
|
552
|
+
end
|
553
|
+
|
554
|
+
def attribute_group
|
555
|
+
raw['attribute_group']
|
556
|
+
end
|
557
|
+
|
558
|
+
def attribute_group=(val)
|
559
|
+
raw['attribute_group'] = val
|
560
|
+
end
|
561
|
+
|
562
|
+
def hidden
|
563
|
+
raw['hidden']
|
564
|
+
end
|
565
|
+
|
566
|
+
def hidden=(val)
|
567
|
+
raw['hidden'] = val
|
568
|
+
end
|
569
|
+
|
570
|
+
def validation
|
571
|
+
raw['validation']
|
572
|
+
end
|
573
|
+
|
574
|
+
def validation=(val)
|
575
|
+
raw['validation'] = val
|
576
|
+
end
|
577
|
+
|
578
|
+
def options
|
579
|
+
raw['options']
|
580
|
+
end
|
581
|
+
|
582
|
+
# NOTE! The Attribute class returns description as desc, where as actions take it in as description.
|
583
|
+
# There are shortcut methods that try to handle this on the action class side of things. Ideally we would
|
584
|
+
# make this more consistent in the near future.
|
585
|
+
COPYABLE_ATTRIBUTE_ATTRIBUTES = [
|
586
|
+
:attribute_name, :attribute_type, :desc, :display_name, :format_hint,
|
587
|
+
:hidden, :link_model_name, :read_only, :attribute_group, :unique, :validation,
|
588
|
+
:restricted
|
589
|
+
]
|
590
|
+
|
591
|
+
EDITABLE_ATTRIBUTE_ATTRIBUTES = UpdateAttributeAction.members & COPYABLE_ATTRIBUTE_ATTRIBUTES
|
592
|
+
|
593
|
+
def self.copy(source, dest, attributes: COPYABLE_ATTRIBUTE_ATTRIBUTES)
|
594
|
+
attributes.each do |attr_name|
|
595
|
+
next unless dest.respond_to?(:"#{attr_name}=")
|
596
|
+
source_v = source.send(attr_name)
|
597
|
+
dest.send(:"#{attr_name}=", source_v)
|
598
|
+
end
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
class AttributeType < String
|
603
|
+
include Enum
|
604
|
+
STRING = AttributeType.new("string")
|
605
|
+
DATE_TIME = AttributeType.new("date_time")
|
606
|
+
BOOLEAN = AttributeType.new("boolean")
|
607
|
+
CHILD = AttributeType.new("child")
|
608
|
+
COLLECTION = AttributeType.new("collection")
|
609
|
+
FILE = AttributeType.new("file")
|
610
|
+
FILE_COLLECTION = AttributeType.new("file_collection")
|
611
|
+
FLOAT = AttributeType.new("float")
|
612
|
+
IDENTIFIER = AttributeType.new("identifier")
|
613
|
+
IMAGE = AttributeType.new("image")
|
614
|
+
INTEGER = AttributeType.new("integer")
|
615
|
+
LINK = AttributeType.new("link")
|
616
|
+
MATCH = AttributeType.new("match")
|
617
|
+
MATRIX = AttributeType.new("matrix")
|
618
|
+
PARENT = AttributeType.new("parent")
|
619
|
+
TABLE = AttributeType.new("table")
|
620
|
+
end
|
621
|
+
|
622
|
+
class ParentLinkType < String
|
623
|
+
include Enum
|
624
|
+
CHILD = ParentLinkType.new("child")
|
625
|
+
COLLECTION = ParentLinkType.new("collection")
|
626
|
+
TABLE = ParentLinkType.new("table")
|
627
|
+
end
|
628
|
+
end
|
629
|
+
end
|
630
|
+
end
|