Class: Etna::Clients::Magma::ModelsCsv

Inherits:
Object
  • Object
show all
Defined in:
lib/etna/clients/magma/formatting/models_csv.rb

Defined Under Namespace

Classes: ModelsChangeset

Constant Summary collapse

COPY_OPTIONS_SENTINEL =
'$copy_from_original$'
COLUMNS =
[
    :comments,
    :model_name, :identifier, :parent_model_name, :parent_link_type,
    :attribute_name,
    :new_attribute_name,
    :attribute_type,
    :link_model_name,
    :description,
    :display_name,
    :format_hint,
    :restricted,
    :read_only,
    :options,
    :match,
    :attribute_group,
    :hidden,
    :unique,
    :matrix_constant,
]
COLUMN_AS_BOOLEAN =
-> (s) { ['true', 't', 'y', 'yes'].include?(s.downcase) }
COLUMNS_TO_ATTRIBUTES =
{
    attribute_name: :attribute_name,
    attribute_type: [:attribute_type, -> (s) { AttributeType.new(s) }],
    link_model_name: :link_model_name,
    description: :desc,
    display_name: :display_name,
    format_hint: :format_hint,
    restricted: [:restricted, COLUMN_AS_BOOLEAN],
    read_only: [:read_only, COLUMN_AS_BOOLEAN],
    options: [:validation, -> (s) { {"type" => "Array", "value" => s.split(',').map(&:strip)} }],
    match: [:validation, -> (s) { {"type" => "Regexp", "value" => Regexp.new(s).source} }],
    attribute_group: :attribute_group,
    hidden: [:hidden, COLUMN_AS_BOOLEAN],
    unique: [:unique, COLUMN_AS_BOOLEAN],
}

Class Method Summary collapse

Class Method Details

.apply_csv_row(changeset = ModelsChangeset.new, row = {}, &err_block) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 46

def self.apply_csv_row(changeset = ModelsChangeset.new, row = {}, &err_block)
  changeset.tap do
    models = changeset.models

    if (matrix_constant = self.get_col_or_nil(row, :matrix_constant))
      if matrix_constant.start_with?(COPY_OPTIONS_SENTINEL)
        matrix_constant = matrix_constant.slice((COPY_OPTIONS_SENTINEL.length)..-1)
        changeset.last_matrix_constant = matrix_constant
      else
        (changeset.matrix_constants[changeset.last_matrix_constant] ||= []) << matrix_constant
      end
    end

    template = if (model_name = self.get_col_or_nil(row, :model_name))
      changeset.last_model_key = model_name
      models.build_model(model_name).build_template.tap { |t| t.name = model_name }
    else
      last_model = changeset.last_model_key
      if last_model.nil?
        nil
      else
        models.model(last_model).build_template
      end
    end

    if (identifier = self.get_col_or_nil(row, :identifier))
      if template.nil?
        yield "Found identifier #{identifier} but no model_name had been given!"
        next
      end
      template.identifier = identifier
    end

    if (parent_model_name = self.get_col_or_nil(row, :parent_model_name))
      self.process_parent_model_name(template: template, parent_model_name: parent_model_name, models: models, &err_block)
    end

    if (parent_link_type = self.get_col_or_nil(row, :parent_link_type))
      self.process_parent_link_type(template: template, parent_link_type: parent_link_type, models: models, &err_block)
    end

    if (attribute_name = self.get_col_or_nil(row, :attribute_name))
      self.process_attribute(template: template, attribute_name: attribute_name, models: models, row: row, &err_block)
      if (new_attribute_name = self.get_col_or_nil(row, :new_attribute_name))
        self.process_new_attribute_name(template: template, changeset: changeset, new_attribute_name: new_attribute_name, attribute_name: attribute_name, &err_block)
      end
    end
  end
end

.each_attribute_row(models = Models.new, model = Model.new, attribute = Attribute.new, matrix_constants = {}) {|row_from_columns( attribute_name: attribute.name, attribute_type: attribute.attribute_type, link_model_name: attribute.link_model_name, reciprocal_link_type: models.find_reciprocal(model: model, attribute: attribute)&.attribute_type, description: attribute.desc, display_name: attribute.display_name, match: attribute.match, format_hint: attribute.format_hint, restricted: attribute.restricted, read_only: attribute.read_only, options: options, attribute_group: attribute.attribute_group, hidden: attribute.hidden, unique: attribute.unique, )| ... } ⇒ Object

Yields:

  • (row_from_columns( attribute_name: attribute.name, attribute_type: attribute.attribute_type, link_model_name: attribute.link_model_name, reciprocal_link_type: models.find_reciprocal(model: model, attribute: attribute)&.attribute_type, description: attribute.desc, display_name: attribute.display_name, match: attribute.match, format_hint: attribute.format_hint, restricted: attribute.restricted, read_only: attribute.read_only, options: options, attribute_group: attribute.attribute_group, hidden: attribute.hidden, unique: attribute.unique, ))


288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 288

def self.each_attribute_row(models = Models.new, model = Model.new, attribute = Attribute.new, matrix_constants = {}, &block)
  if attribute.attribute_type == AttributeType::IDENTIFIER
    # Identifiers for models whose parent link type ends up being a table are non configurable, so we don't
    # want to include them in the CSV.
    if models.find_reciprocal(model: model, link_attribute_name: model.template.parent)&.attribute_type == AttributeType::TABLE
      return
    end
  else
    return unless AttributeValidator.valid_add_row_attribute_types.include?(attribute.attribute_type)
  end

  options = attribute.options&.join(', ')
  if attribute.attribute_type == AttributeType::MATRIX
    # Matrix attribute validations are massive, and functional.  For now, we don't support showing and editing
    # them inline with this spreadsheet.  In the future, I think we should possibly introduce the concept of
    # CONSTANTS or Matrix Types that are managed separately.
    options = options || ''
    digest = Digest::MD5.hexdigest(options)
    matrix_constants[digest] ||= COLUMNS_TO_ATTRIBUTES[:options][1].call(options)["value"]

    options = COPY_OPTIONS_SENTINEL + digest
  end

  yield row_from_columns(
      attribute_name: attribute.name,
      attribute_type: attribute.attribute_type,
      link_model_name: attribute.link_model_name,
      reciprocal_link_type: models.find_reciprocal(model: model, attribute: attribute)&.attribute_type,
      description: attribute.desc,
      display_name: attribute.display_name,
      match: attribute.match,
      format_hint: attribute.format_hint,
      restricted: attribute.restricted,
      read_only: attribute.read_only,
      options: options,
      attribute_group: attribute.attribute_group,
      hidden: attribute.hidden,
      unique: attribute.unique,
  )
end

.each_csv_row(models = Models.new, model_keys = models.model_keys.sort) {|COLUMNS.map(&:to_s)| ... } ⇒ Object

Yields:



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 219

def self.each_csv_row(models = Models.new, model_keys = models.model_keys.sort, &block)
  yield COLUMNS.map(&:to_s)

  self.ensure_parents(models, model_keys, &block)
  matrix_constants = {}
  model_keys.each { |model_name| self.each_model_row(models, model_name, matrix_constants, &block) }

  matrix_constants.each do |digest, options|
    yield row_from_columns
    yield row_from_columns(matrix_constant: COPY_OPTIONS_SENTINEL + digest)
    options.each do |option|
      yield row_from_columns(matrix_constant: option)
    end
  end
end

.each_model_row(models, model_name, matrix_constants, &block) ⇒ Object



279
280
281
282
283
284
285
286
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 279

def self.each_model_row(models, model_name, matrix_constants, &block)
  return unless (model = models.model(model_name))

  self.each_model_trunk_row(models, model_name, &block)
  model.template.attributes.all.each do |attribute|
    self.each_attribute_row(models, model, attribute, matrix_constants, &block)
  end
end

.each_model_trunk_row(models, model_name) {|row_from_columns| ... } ⇒ Object

Yields:



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 256

def self.each_model_trunk_row(models, model_name, &block)
  return unless (model = models.model(model_name))

  # Empty link for better visual separation
  yield row_from_columns
  yield row_from_columns(model_name: model_name)

  unless model.template.parent.nil?
    parent_model = models.model(model.template.parent)
    reciprocal = models.find_reciprocal(model: model, link_model: parent_model)

    yield row_from_columns(
        identifier: model.template.identifier,
        parent_model_name: model.template.parent,
        parent_link_type: reciprocal.attribute_type.to_s
    )
  else
    yield row_from_columns(
        identifier: model.template.identifier,
    )
  end
end

.ensure_parents(models, model_keys, &block) ⇒ Object



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 235

def self.ensure_parents(models, model_keys, &block)
  q = model_keys.dup
  seen = Set.new

  until q.empty?
    model_key = q.shift
    next if model_key.nil?
    next if seen.include?(model_key)
    seen.add(model_key)

    # For models that are only part of the trunk, but not of the tree of model_keys,
    # we still need their basic information (identifier / parent) for validation and for
    # potentially creating the required tree dependencies to connect it to a remote tree.
    unless model_keys.include?(model_key)
      self.each_model_trunk_row(models, model_key, &block)
    end

    q.push(*models.model(model_key).template.all_linked_model_names)
  end
end

.get_col_or_nil(row, col) ⇒ Object



213
214
215
216
217
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 213

def self.get_col_or_nil(row, col)
  c = row[col]&.chomp
  return nil if c&.empty?
  c
end

.prettify(name) ⇒ Object



209
210
211
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 209

def self.prettify(name)
  name.split('_').map(&:capitalize).join(' ')
end

.process_attribute(template:, attribute_name:, models:, row:, &err_block) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 164

def self.process_attribute(template:, attribute_name:, models:, row:, &err_block)
  if template.nil?
    yield "Found attribute #{attribute_name} but no model_name had been given!" if block_given?
    return
  end

  attributes = template.build_attributes
  existing_attribute = attributes.attribute(attribute_name)
  if existing_attribute
    if existing_attribute.attribute_type != AttributeType::COLLECTION
      yield "Attribute #{attribute_name} of model #{template.name} has duplicate definitions!" if block_given?
    end

    attributes.raw.delete(attribute_name)
  end

  attributes.build_attribute(attribute_name).tap do |att|
    COLUMNS_TO_ATTRIBUTES.each do |column, processor|
      next unless (value = self.get_col_or_nil(row, column))
      if processor.is_a?(Array)
        processor, f = processor
        value = f.call(value)
      end

      if !att.send(processor).nil? && !att.send(processor).empty?
        yield "Value for #{processor} on attribute #{attribute_name} has duplicate definitions!" if block_given?
      end

      att.send(:"#{processor}=", value)
    end

    if att.attribute_type == AttributeType::LINK && models.find_reciprocal(model_name: template.name, attribute: att).nil?
      models.build_model(att.link_model_name).build_template.build_attributes.build_attribute(template.name).tap do |rec_att|
        rec_att.attribute_name = template.name
        rec_att.display_name = self.prettify(template.name)
        rec_att.desc = self.prettify(template.name)
        rec_att.attribute_type = AttributeType::COLLECTION
        rec_att.link_model_name = template.name
      end
    end

    att.set_field_defaults!
  end
end

.process_new_attribute_name(template:, changeset:, new_attribute_name:, attribute_name:, &err_block) ⇒ Object



96
97
98
99
100
101
102
103
104
105
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 96

def self.process_new_attribute_name(template:, changeset:, new_attribute_name:, attribute_name:, &err_block)
  renames = changeset.build_renames(template.name)
  if renames.include?(attribute_name) && renames[attribute_name] != new_attribute_name
    if block_given?
      yield "Found multiple new_attribute_name values for #{template.name}'s #{attribute_name}': #{new_attribute_name} or #{renames[attribute_name]}?"
    end
  end

  renames[attribute_name] = new_attribute_name
end


128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 128

def self.process_parent_link_type(template:, parent_link_type:, models:, &err_block)
  if template.nil?
    yield "Found parent_link_type #{parent_link_type} but no model_name had been given!" if block_given?
    return
  end

  if template.parent.nil?
    yield "Found parent_link_type #{parent_link_type} but no parent_model_name had been given!" if block_given?
    return
  end

  reciprocal = models.find_reciprocal(model_name: template.name, link_attribute_name: template.parent)
  if reciprocal && reciprocal.attribute_type.to_s != parent_link_type
    yield "Model #{template.name} was provided multiple parent_link_types: #{reciprocal.attribute_type} and #{parent_link_type}" if block_given?
  end

  if reciprocal && reciprocal.attribute_name != template.name
    yield "Model #{template.name} is linked to #{template.parent}, but the reciprocal link is misnamed as '#{reciprocal.attribute_name}'." if block_given?
  end

  models.build_model(template.parent).tap do |parent_model|
    parent_model_attribute_by_model_name = parent_model.build_template.build_attributes.attribute(template.name)
    if parent_model_attribute_by_model_name && !reciprocal
      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?
    end

    parent_model.build_template.build_attributes.build_attribute(template.name).tap do |attr|
      attr.attribute_name = attr.name = template.name
      attr.attribute_type = parent_link_type
      attr.link_model_name = template.name
      attr.desc = self.prettify(template.name)
      attr.display_name = self.prettify(template.name)
    end
  end
end

.process_parent_model_name(template:, parent_model_name:, models:, &err_block) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 107

def self.process_parent_model_name(template:, parent_model_name:, models:, &err_block)
  if template.nil?
    yield "Found parent_model_name #{parent_model_name} but no model_name had been given!" if block_given?
    return
  end

  if template.parent && !template.parent.empty? && template.parent != parent_model_name
    yield "Model #{template.name} was provided multiple parent_model_names: #{template.parent} and #{parent_model_name}" if block_given?
  end

  template.parent = parent_model_name

  template.build_attributes.build_attribute(template.parent).tap do |parent_att|
    parent_att.name = parent_att.attribute_name = parent_model_name
    parent_att.attribute_type = AttributeType::PARENT
    parent_att.link_model_name = parent_model_name
    parent_att.desc = self.prettify(parent_model_name)
    parent_att.display_name = self.prettify(parent_model_name)
  end
end

.row_from_columns(**columns) ⇒ Object



329
330
331
# File 'lib/etna/clients/magma/formatting/models_csv.rb', line 329

def self.row_from_columns(**columns)
  COLUMNS.map { |c| (columns[c] || '').to_s }
end