Class: WscSdk::Model

Inherits:
Object
  • Object
show all
Includes:
ApiResponse, Loggable
Defined in:
lib/wsc_sdk/model.rb

Overview

Base class for defining model schemas and managing inbound/outbound model data.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Loggable

#logger, #logger=

Constructor Details

#initialize(endpoint, attributes = {}) ⇒ Model

Create a new model instance.

Parameters:

  • endpoint (WscSdk::Endpoint)

    The endpoint instance that generated the model.

  • attributes (Hash) (defaults to: {})

    A hash of attribute values to assign to the model.



66
67
68
69
70
71
72
73
74
75
# File 'lib/wsc_sdk/model.rb', line 66

def initialize(endpoint, attributes={})
  @endpoint     = endpoint
  @changes      = []
  @partial_data = false
  @data_mode    = :access
  initialize_attributes
  ingest_attributes(attributes, write_to_read_only: true, mark_clean: true)
  @errors       = nil
  @dirty        = false
end

Instance Attribute Details

#attributesObject

A hash of attributes and values for the model

NOTE: The attributes accessor directly accesses the attributes for a model. This is okay for reading data out of a model, but data assignment should be done using the getter (model.attribute) and setter (model.attribute=value) methods that are built into the model. Otherwise you will be bypassing the structures necessary to validate the data, which may generate unwanted outcomes.



32
33
34
# File 'lib/wsc_sdk/model.rb', line 32

def attributes
  @attributes
end

#changesObject (readonly)

An array of fields that have changed since the model was last saved to the API.



48
49
50
# File 'lib/wsc_sdk/model.rb', line 48

def changes
  @changes
end

#data_modeObject (readonly)

Determines what mode the data is being ingested/accesed in.



56
57
58
# File 'lib/wsc_sdk/model.rb', line 56

def data_mode
  @data_mode
end

#endpointObject (readonly)

The endpoint that generated the model.



43
44
45
# File 'lib/wsc_sdk/model.rb', line 43

def endpoint
  @endpoint
end

#errorsObject (readonly)

A hash of fields and messages that indicates validation errors for the model. Use `model.valid?` if the response is false, this hash will contain the information necessary to fix the concerns.



39
40
41
# File 'lib/wsc_sdk/model.rb', line 39

def errors
  @errors
end

#partial_dataObject (readonly)

Determines if the model contains a partial or complete dataset.



52
53
54
# File 'lib/wsc_sdk/model.rb', line 52

def partial_data
  @partial_data
end

Class Method Details

.attribute(name, type, options = {}) ⇒ Object

Defines an attribute in the model schema and establishes the appropriate getters/setters according to its access settings.



477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# File 'lib/wsc_sdk/model.rb', line 477

def self.attribute(name, type, options={})
  name            = name.to_sym
  accessor_name   = options.fetch(:as, name).to_sym
  type            = type.to_sym
  current_schema  = self.schema
  attr            = current_schema.add_attribute(name, type, options)

  define_singleton_method :schema do
    current_schema
  end

  define_method accessor_name do
    raise ::NoMethodError.new("the '#{accessor_name}' attribute is not currently readable") unless schema[name].read_access?(self)
    current_value = @attributes[name.to_sym]
    request_remaining_data if (@partial_data and current_value.nil?)
    @attributes[name.to_sym]
  end

  set_sym = "#{accessor_name.to_s}=".to_sym
  define_method(set_sym) do |value|
    not_writable = ((@data_mode == :access) and (!schema[name].write_access?(self)))
    if not_writable
      logger.warn("Attempting to write to non-writable attribute: #{accessor_name}")
      return
    end
    valid_type = self.class.schema.valid_type?(name, value)
    if valid_type
      self.attributes[name] = self.class.schema.transform_value(name, self, value)
      @errors               = nil
      @changes              << name
    else
      logger.warn("The attribute '#{accessor_name.to_s}' expected a(n) #{type} value but got '#{value}' [#{value.class.name}] instead!")
    end
  end

end

.model_name_plural(wrapper) ⇒ Object

Builds the `plural_name` static method that will return the name of the model in plural form.



462
463
464
465
466
# File 'lib/wsc_sdk/model.rb', line 462

def self.model_name_plural(wrapper)
  define_singleton_method :plural_name do
    wrapper
  end
end

.model_name_singular(wrapper) ⇒ Object

Builds the `singular_name` static method that will return the name of the model in singular form.



453
454
455
456
457
# File 'lib/wsc_sdk/model.rb', line 453

def self.model_name_singular(wrapper)
  define_singleton_method :singular_name do
    wrapper
  end
end

.primary_key(attribute_name) ⇒ Object

Rebuilds the `primary_key_attribute` method in the class to identify an attribute other than the default as the primary key

Parameters:

  • attribute_name (Symbol)

    The name of the attribute to use as the primary key.



444
445
446
447
448
# File 'lib/wsc_sdk/model.rb', line 444

def self.primary_key(attribute_name)
  define_method :primary_key_attribute do
    return attribute_name.to_sym
  end
end

.schemaObject

Defines the default schema structure.



470
471
472
# File 'lib/wsc_sdk/model.rb', line 470

def self.schema
  Schema.new
end

Instance Method Details

#add_error(attribute, message) ⇒ Object

Adds an error message to the errors hash.

Parameters:

  • attribute (Symbol)

    The name of the attribute to add the error to.

  • message (String)

    The message to add to the error



294
295
296
297
# File 'lib/wsc_sdk/model.rb', line 294

def add_error(attribute, message)
  @errors[attribute] ||= []
  @errors[attribute] << message
end

#build_payloadHash

Build the request payload.

Returns:

  • (Hash)

    The properly structure request data.



408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/wsc_sdk/model.rb', line 408

def build_payload
  return nil unless valid?

  model_wrapper = self.class.singular_name.to_sym
  payload       = {}
  wrapper       = payload[model_wrapper] = {}

  self.schema.each do |name, attribute|
    if attribute.write_access?(self)
      value     = attribute.value_or_default(self)
      required  = attribute.required?(self)

      if required or !value.nil?
        wrapper[name] = value
      end
    end
  end

  payload
end

#build_payload!Hash

Build the request payload, or generate an exception if the data is not valid.

Returns:

  • (Hash)

    The properly structure request data.

Raises:

  • (ArgumenError)

    If the model has invalid data.



398
399
400
401
# File 'lib/wsc_sdk/model.rb', line 398

def build_payload!
  valid!
  return build_payload
end

#clean!Object

Cleans out the changes array, which means that the data is no longer marked as dirty.



211
212
213
# File 'lib/wsc_sdk/model.rb', line 211

def clean!
  @changes = []
end

#clear_primary_keyObject

Clear the primary key value.

This can be used to create a completely new instance of a model's data



117
118
119
# File 'lib/wsc_sdk/model.rb', line 117

def clear_primary_key
  @attributes[self.primary_key_attribute] = nil
end

#deleteWscSdk::Model, Nil

Delete the model data from the api.

Returns:

  • (WscSdk::Model)

    If the created/updated model is valid and returns properly from the endpoint.

  • (Nil)

    If the created/updated model is invalid.



348
349
350
351
352
353
354
355
356
357
# File 'lib/wsc_sdk/model.rb', line 348

def delete
  return WscSdk::Errors.model_does_not_exist(self.endpoint) if new_model?
  result = endpoint.delete(self)
  if result.success?
    clean!
    clear_primary_key
    @partial_data = false
  end
  return result
end

#dirty?Boolean

Determines if the data is dirty or not based on whether changes have been marked in the changes array.

Returns:

  • (Boolean)


218
219
220
# File 'lib/wsc_sdk/model.rb', line 218

def dirty?
  @changes.length > 0
end

#has_attribute?(attribute) ⇒ Boolean

Determines if an attribute names exists in the schema.

Parameters:

  • attribute (Symbol)

    The name of the attribute to determine the existence of.

Returns:

  • (Boolean)


204
205
206
# File 'lib/wsc_sdk/model.rb', line 204

def has_attribute?(attribute)
  attributes.keys.include?(attribute)
end

#ingest_attribute(attribute, value, options = {}) ⇒ Object

Load an attribute value into the attributes hash and ensure that it meets the requirements of the schema.

Parameters:

  • attribute (Symbol)

    The name of the attribute to ingest

  • value (Any)

    The value to ingest into the attribute.

  • options (Hash) (defaults to: {})

    A hash of options

Options Hash (options):

  • :write_to_read_only (Boolean)

    Allow data to be written to read only fields. This option should only be used by the model class, and not by users.



184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/wsc_sdk/model.rb', line 184

def ingest_attribute(attribute, value, options={})
  return unless has_attribute?(attribute)

  write_to_read_only  = options.fetch(:write_to_read_only, false)
  attr                = self.class.schema[attribute]
  @data_mode          = write_to_read_only ? :ingest : :access
  _temp_partial_data  = @partial_data
  @partial_data       = false
  set_sym             = "#{attr.attribute_name.to_s}=".to_sym

  self.send(set_sym, value)

  @partial_data       = _temp_partial_data
end

#ingest_attributes(attributes, options = {}) ⇒ Object

Load data into the attributes hash and ensure that it meets the requirements of the schema.

Parameters:

  • attributes (Hash)

    The data to load into the attributes hash.

  • options (Hash) (defaults to: {})

    A hash of options

Options Hash (options):

  • :mark_clean (Boolean)

    Mark the data as clean after ingesting it.

  • :write_to_read_only (Boolean)

    Allow data to be written to read only fields. This option should only be used by the model class, and not by users.



154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/wsc_sdk/model.rb', line 154

def ingest_attributes(attributes, options={})
  write_to_read_only  = options.fetch(:write_to_read_only,  false)
  mark_clean          = options.fetch(:mark_clean,          false)
  @partial_data       = options.fetch(:partial_data,        false)
  attributes          = (attributes || {}).deep_symbolize_keys
  root_key            = self.class.singular_name.to_sym
  attributes          = attributes.has_key?(root_key) ? attributes[root_key] : attributes

  attributes.each do |attribute, value|
    ingest_attribute(attribute, value, options)
  end
  clean! if mark_clean
end

#initialize_attributesObject

Build the attributes hash from the schema definitions.

This is a non-destructive process, so if the attributes are already established and have values, then nothing will happen to them.



126
127
128
129
130
131
132
133
134
135
136
# File 'lib/wsc_sdk/model.rb', line 126

def initialize_attributes()
  @attributes       ||= {}
  merged_attributes = {}
  defaults          = self.class.schema.defaults(self)

  # Merge the defaults in where attributes don't have a value defined.
  defaults.each do |name, value|
    merged_attributes[name] = attributes[name] || value
  end
  @attributes = merged_attributes
end

#new_model?Boolean

Determine if the model data is new or not.

Returns:

  • (Boolean)

    An indication if the model is new.



247
248
249
# File 'lib/wsc_sdk/model.rb', line 247

def new_model?
  self.primary_key.nil? or self.primary_key.empty?
end

#primary_keyAny

Get the value of the primary key attribute.

Returns:

  • (Any)

    The value of the primary key



99
100
101
# File 'lib/wsc_sdk/model.rb', line 99

def primary_key
  @attributes[self.primary_key_attribute]
end

#primary_key_attributeSymbol

Get the name of the attribute that represents the primary key of the model.

Returns:

  • (Symbol)

    The name of the primary key field.



109
110
111
# File 'lib/wsc_sdk/model.rb', line 109

def primary_key_attribute
  :id
end

#refreshWscSdk::Model

Refresh the data in the model from the API.

Returns:

  • (WscSdk::Model)

    A copy of the model if the refresh was successful. An error model if the refresh was a failure.



305
306
307
308
309
310
311
# File 'lib/wsc_sdk/model.rb', line 305

def refresh
  if new_model?
    return WscSdk::Errors.model_does_not_exist(self.endpoint)
  else
    return self.endpoint.refresh(self)
  end
end

#saveWscSdk::Model, Nil

Save the data in the model to the api.

If the model is new, then the endpoint create method will be called, if the model is not new, then the endpoint update method will be called.

Returns:

  • (WscSdk::Model)

    If the created/updated model is valid and returns properly from the endpoint.

  • (Nil)

    If the created/updated model is invalid.



325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/wsc_sdk/model.rb', line 325

def save
  result = nil
  if new_model?
    result = endpoint.create(self)
    @attributes[self.primary_key_attribute] = result.primary_key if result.success?
  elsif dirty?
    result = endpoint.update(self)
  else
    result = self
  end
  clean! if result.success?
  result
end

#schemaWscSdk::Schema

Get the current schema definition

Returns:



90
91
92
# File 'lib/wsc_sdk/model.rb', line 90

def schema
  self.class.schema
end

#success?Boolean

Return the success status of the API call that generated this model.

Returns:

  • (Boolean)

    An indication of the status of the API call.



364
365
366
# File 'lib/wsc_sdk/model.rb', line 364

def success?
  return true
end

#valid!Object

Determine if the model data is valid and raise an exception if its not.

Raises:

  • (ArgumentError)

    If the model has invalid data.



256
257
258
# File 'lib/wsc_sdk/model.rb', line 256

def valid!
  raise ArgumentError.new("Invalid attribute values: #{@errors.map{ |f, m| "#{f}: #{m}"}.join(" | ")}") unless valid?
end

#valid?Boolean

Determine if the model data is valid.

Returns:

  • (Boolean)

    An indication if the model data is valid.



265
266
267
268
# File 'lib/wsc_sdk/model.rb', line 265

def valid?
  validate
  (errors.keys.length == 0)
end

#validateObject

Validate the model data.

This will populate the `.errors` hash if data is invalid.



274
275
276
277
278
279
280
281
282
283
284
# File 'lib/wsc_sdk/model.rb', line 274

def validate
  @errors = {}
  self.class.schema.each do |name, attribute|
    value = attribute.value_or_default(self)
    if attribute.required?(self) and value.nil?
      add_error(name, "is required")
    end

    attribute.valid?(self) unless value.nil?
  end
end