Class: WellRested::Base

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Serializers::JSON, ActiveModel::Validations, Utils, Utils
Defined in:
lib/well_rested/base.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils

#objects_to_attributes

Methods included from GenericUtils

#class_exists?, #get_class

Constructor Details

#initialize(attrs = {}) ⇒ Base

Returns a new instance of Base.



84
85
86
87
88
# File 'lib/well_rested/base.rb', line 84

def initialize(attrs = {})
  raise "Attrs must be hash" unless attrs.is_a? Hash

  self.load(attrs, false) 
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_sym, *args, &block) ⇒ Object

Respond to getter and setter methods for attributes.



276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/well_rested/base.rb', line 276

def method_missing(method_sym, *args, &block)
  method = method_sym.to_s
  # Is this an attribute getter?
  if args.empty? and attributes.include?(method)
    attributes[method]
  # Is it an attribute setter?
  elsif args.length == 1 and method[method.length-1..method.length-1] == '=' and attributes.include?(attr_name = method[0..method.length-2])
    attributes[attr_name] = args.first
  else
    super
  end
end

Instance Attribute Details

#attributesObject (readonly)

Returns the value of attribute attributes.



37
38
39
# File 'lib/well_rested/base.rb', line 37

def attributes
  @attributes
end

#new_recordObject

Returns the value of attribute new_record.



38
39
40
# File 'lib/well_rested/base.rb', line 38

def new_record
  @new_record
end

Class Method Details

.define_schema(*args) ⇒ Object

Define the schema for this resource.

Either takes an array, or a list of arguments which we treat as an array. Each element of the array should be either a symbol or a hash. If it’s a symbol, we create an attribute using the symol as the name and with a null default value. If it’s a hash, we use the keys as attribute names.

 - Any values that are hashes, we use to specify further options (currently, the only option is :default).
 - Any value that is not a hash is treated as a default. 
e.g.
define_schema :x, :y, :z                            # x, y, and z all default to nil
define_schema :id, :name => 'John'                  # id defaults to nil, name defaults to 'John'
define_schema :id, :name => { :default => 'John' }  # same as above


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
# File 'lib/well_rested/base.rb', line 53

def self.define_schema(*args)
  return schema if args.empty?

  attrs = args.first.is_a?(Array) ? args.first : args
  self.schema = {}.with_indifferent_access
  attrs.each do |attr|
    if attr.is_a?(Symbol)
      self.schema[attr] = { :default => nil }
    elsif attr.is_a?(Hash)
      attr.each do |k,v|
        if v.is_a?(Hash)
          self.schema[k] = v
        else
          self.schema[k] = { :default => v }
        end
      end
    end
  end

=begin
  # Possible alternative to using method_missing:
  # define getter/setter methods for attributes.
  @attributes.keys.each do |attr_name|
    define_method(attr_name) { @attributes[attr_name] }
    define_method("#{attr_name}=") { |val| @attributes[attr_name] = val }
  end
=end

  self.schema
end

.descendant_mapObject

Create a map of all descendants of Base to lookup classes from names when converting hashes to objects.



102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/well_rested/base.rb', line 102

def self.descendant_map
  return @descendant_map if @descendant_map
  @descendant_map = {}.with_indifferent_access
  self.descendants.each do |des|
    unless des.name.blank?
      sep_index = des.name.rindex('::')
      short_name = sep_index ? des.name[sep_index+2..-1] : des.name
      @descendant_map[short_name] = des
    end
  end
  @descendant_map
end

.descendantsObject



97
98
99
# File 'lib/well_rested/base.rb', line 97

def self.descendants
  ObjectSpace.each_object(::Class).select { |klass| klass < self }
end

.fill_path(params) ⇒ Object



209
210
211
# File 'lib/well_rested/base.rb', line 209

def self.fill_path(params)
  API.fill_path(self.path, params)
end

.find_resource_class(class_name) ⇒ Object

When we are loading a resource from an API call, we will use this method to instantiate classes based on attribute names.



171
172
173
174
175
176
# File 'lib/well_rested/base.rb', line 171

def self.find_resource_class(class_name)
  klass = Utils.get_class(class_name)
  #puts "**** descendant map: #{Base.descendant_map.inspect}"
  return klass if klass.respond_to?(:new_from_api)
  Base.descendant_map[class_name]
end

.hash_to_objects(hash, from_api = false) ⇒ Object

Convert a hash received from the API into an object or array of objects. e.g. Base.hash_to_objects(=> {‘name’ => ‘Test’ }) => @attributes={“name”=>“Test”}



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/well_rested/base.rb', line 180

def self.hash_to_objects(hash, from_api = false)
  hash.each do |k,v|
    if v.kind_of?(Hash)
      class_name = k.camelize
      klass = self.find_resource_class(class_name)
      if klass
        hash[k] = from_api ? klass.new_from_api(v) : klass.new(v)
      end
    elsif v.kind_of?(Array)
      class_name = k.to_s.singularize.camelize
      #puts "**** class_name=#{class_name}"
      klass = find_resource_class(class_name)
      if klass
        #puts "**** class exists, instantiation"
        hash[k] = v.map do |o| 
          if o.kind_of?(Hash) 
            from_api ? klass.new_from_api(o) : klass.new(o) 
          else
            o
          end
        end
      else
        #puts "**** class does not exist"
      end
    end
  end
  hash
end

.new_from_api(attrs) ⇒ Object

Convenience method for creating an object and calling load_from_api The API should call this method when creating representations of objects that are already persisted.

By default, attributes loaded from the API have new_record set to true. This has implications for Rails form handling. (Rails uses POST for records that it thinks are new, but PUT for records that it thinks are already persisted.)



120
121
122
123
124
# File 'lib/well_rested/base.rb', line 120

def self.new_from_api(attrs)
  obj = self.new
  obj.load_from_api(attrs)
  return obj
end

Instance Method Details

#==(other) ⇒ Object

Equality is defined as having the same attributes.



271
272
273
# File 'lib/well_rested/base.rb', line 271

def ==(other)
  other.respond_to?(:attributes) ? (self.attributes == other.attributes) : false
end

#attributes_for_apiObject

Return the attributes that we want to send to the server when this resource is saved. If a schema is defined, only return elements defined in the schema. Override this for special attribute-handling.



216
217
218
219
220
221
222
# File 'lib/well_rested/base.rb', line 216

def attributes_for_api
  # by default, filter out nil elements
  hash = objects_to_attributes(@attributes.reject { |k,v| v.nil? }.with_indifferent_access)
  # and anything not included in the schema
  hash.reject! { |k,v| !schema.include?(k) } unless schema.nil?
  hash
end

#convert_attributes_to_objectsObject

Convert attribute hashes that represent objects into objects



159
160
161
# File 'lib/well_rested/base.rb', line 159

def convert_attributes_to_objects
  self.class.hash_to_objects(attributes, self.class)
end

#handle_errors(received_errors) ⇒ Object

This method is called by API when a hash including ‘errors’ is returned along with an HTTP error code.



164
165
166
167
168
# File 'lib/well_rested/base.rb', line 164

def handle_errors(received_errors)
  received_errors.each do |err|
    self.errors.add :base, err
  end
end

#idObject

Define an actual method for ID. This is important in Ruby 1.8 where the object_id method is also aliased to id.



91
92
93
# File 'lib/well_rested/base.rb', line 91

def id
  attributes[:id]
end

#load(attrs_to_load, from_api = false) ⇒ Object

Load this resource from attributes. If these attributes were received from the API, true should be passed for from_api. This will ensure any object-specific loading behavior is respected. example:

res = Resource.new
res.load(:name => 'New')


131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/well_rested/base.rb', line 131

def load(attrs_to_load, from_api = false)
  raise "Attrs is not a hash: #{attrs_to_load.inspect}" unless attrs_to_load.kind_of? Hash

  #puts "*** Warning: loading a resource without a schema (#{self.class})!" if schema.nil?
  #raise "Tried to load attributes for a resource with no schema (#{self.class})!" if schema.nil?
  
  # By default we mark a record as new if it doesn't come from the API and it doesn't have an ID attribute.
  self.new_record = !from_api and !attrs_to_load.include?('id')

  new_attrs = {}.with_indifferent_access

  # Take default values from schema, but allow arbitrary args to be loaded.
  # We will address the security issue by filtering in attributes_for_api.
  schema.each { |key, opts| new_attrs[key] = opts[:default] } unless schema.blank?
  new_attrs.merge!(attrs_to_load)

  @attributes = self.class.hash_to_objects(new_attrs, from_api).with_indifferent_access

  return self
end

#load_from_api(attrs) ⇒ Object

Load attributes from the API. This method exists to be overridden so that attributes created manually can be handled differently from those loaded from the API.



154
155
156
# File 'lib/well_rested/base.rb', line 154

def load_from_api(attrs)
  load(attrs, true)
end

#new?Boolean

Alias of new_record? Apparently used by Rails sometimes.

Returns:

  • (Boolean)


254
255
256
# File 'lib/well_rested/base.rb', line 254

def new?
  self.new_record
end

#new_record?Boolean

The following 3 methods were copied from active_record/persistence.rb Returns true if this object hasn’t been saved yet – that is, a record for the object doesn’t exist in the data store yet; otherwise, returns false.

Returns:

  • (Boolean)


249
250
251
# File 'lib/well_rested/base.rb', line 249

def new_record?
  self.new_record
end

#path_parametersObject

API should use these to generate the path. Override this to control how path variables get inserted.



226
227
228
# File 'lib/well_rested/base.rb', line 226

def path_parameters
  objects_to_attributes(@attributes.reject { |k,v| v.nil? }.with_indifferent_access)
end

#persisted?Boolean

Returns if the record is persisted, i.e. it’s not a new record and it was not destroyed.

Returns:

  • (Boolean)


265
266
267
268
# File 'lib/well_rested/base.rb', line 265

def persisted?
#  !(new_record? || destroyed?)
  !new_record?
end

#read_attribute_for_validation(key) ⇒ Object

Run active_model validations on @attributes hash.



231
232
233
# File 'lib/well_rested/base.rb', line 231

def read_attribute_for_validation(key)
  @attributes[key]
end

#to_keyObject

Return a key for rails to use for… not sure exaclty what. Should be an array, or nil.



242
243
244
# File 'lib/well_rested/base.rb', line 242

def to_key
  self.id.nil? ? nil : [self.id] 
end

#to_paramObject

Return a string form of this object for rails to use in routes.



236
237
238
# File 'lib/well_rested/base.rb', line 236

def to_param
  self.id.nil? ? nil : self.id.to_s
end