core/app/models/mno_enterprise/base_resource.rb
# TODO: spec the ActiveRecord behaviour
# - processing of remote errors
# - response parsing (using data: [] format)
# - save methods
module MnoEnterprise
class BaseResource
include Her::Model
include HerExtension::Validations::RemoteUniquenessValidation
include GlobalID::Identification
include_root_in_json :data
use_api MnoEnterprise.mnoe_api_v1
# TODO: spec that changed_attributes is empty
# after a KLASS.all / KLASS.where etc..
after_find { |res| res.instance_variable_set(:@changed_attributes, {}) }
# Attributes common to all classes
attributes :id, :created_at, :updated_at
# Class query methods
class << self
# Delegate the following methods to `scoped`
# Clear relation params for each class level query
[:all, :where, :create, :find, :first_or_create, :first_or_initialize, :limit, :skip, :order_by, :sort_by, :order, :sort].each do |method|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(*params)
Her::Model::Relation.new(self).send(#{method.to_sym.inspect}, *params)
end
RUBY
end
# ActiveRecord Compatibility for Her
def first(n = 1)
return [] unless n > 0
q = self.order_by('id.asc').limit(n)
n == 1 ? q.to_a.first : q.to_a
end
# ActiveRecord Compatibility for Her
def last(n = 1)
return [] unless n > 0
q = self.order_by('id.desc').limit(n)
n == 1 ? q.to_a.first : q.to_a
end
# Find first record using a hash of attributes
def find_by(hash)
self.where(hash).limit(1).first
end
# ActiveRecord Compatibility for Her
def exists?(hash)
find_by(hash).present?
end
# ActiveRecord Compatibility for Her
# Returns the class descending directly from MnoEnterprise::BaseResource, or
# an abstract class, if any, in the inheritance hierarchy.
#
# If A extends MnoEnterprise::BaseResource, A.base_class will return A. If B descends from A
# through some arbitrarily deep hierarchy, B.base_class will return A.
#
# If B < A and C < B and if A is an abstract_class then both B.base_class
# and C.base_class would return B as the answer since A is an abstract_class.
def base_class
unless self < BaseResource
raise Error, "#{name} doesn't belong in a hierarchy descending from BaseResource"
end
if superclass == BaseResource || superclass.abstract_class?
self
else
superclass.base_class
end
end
end
#======================================================================
# Instance methods
#======================================================================
# Returns a cache key that can be used to identify this record.
#
# Product.new.cache_key # => "products/new"
# Product.find(5).cache_key # => "products/5" (updated_at not available)
# Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
#
# You can also pass a list of named timestamps, and the newest in the list will be
# used to generate the key:
#
# Person.find(5).cache_key(:updated_at, :last_reviewed_at)
#
# Notes: copied from ActiveRecord
def cache_key(*timestamp_names)
case
when new?
"#{model_name.cache_key}/new"
when timestamp_names.any?
timestamp = max_updated_column_timestamp(timestamp_names)
timestamp = timestamp.utc.to_s(:nsec)
"#{model_name.cache_key}/#{id}-#{timestamp}"
when timestamp = max_updated_column_timestamp
timestamp = timestamp.utc.to_s(:nsec)
"#{model_name.cache_key}/#{id}-#{timestamp}"
else
"#{model_name.cache_key}/#{id}"
end
end
def max_updated_column_timestamp(timestamp_names = [:updated_at])
timestamp_names
.map { |attr| self[attr] }
.compact
.map(&:to_time)
.max
end
# Clear the record association cache
def clear_association_cache
self.class.associations[:has_many].each do |assoc|
instance_variable_set(:"@_her_association_#{assoc[:name]}", nil)
attributes.delete(assoc[:name].to_s)
end
end
# ActiveRecord Compatibility for Her
def read_attribute(attr_name)
get_attribute(attr_name)
end
# ActiveRecord Compatibility for Her
def write_attribute(attr_name, value)
assign_attributes(attr_name => value)
end
alias []= write_attribute
# ActiveRecord Compatibility for Her
def save(options={})
if perform_validations(options)
ret = super()
process_response_errors
ret
else
false
end
end
# ActiveRecord Compatibility for Her
def save!(options={})
if perform_validations(options)
ret = super()
process_response_errors
raise_record_invalid if self.errors.any?
ret
else
raise_record_invalid
end
end
# ActiveRecord Compatibility for Her
def reload(options = nil)
@attributes.update(self.class.find(self.id).attributes)
self.run_callbacks :find
self
end
# ActiveRecord Compatibility for Her
def update(attributes)
assign_attributes(attributes)
save
end
# Reset the ActiveModel hash containing all attribute changes
# Useful when initializing a existing resource using a hash fetched
# via http call (e.g.: MnoEnterprise::User.authenticate)
def clear_attribute_changes!
self.instance_variable_set(:@changed_attributes, {})
end
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
# is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
#
# Note that new records are different from any other record by definition, unless the
# other record is the receiver itself. Besides, if you fetch existing records with
# +select+ and leave the ID out, you're on your own, this predicate will return false.
#
# Note also that destroying a record preserves its ID in the model instance, so deleted
# models are still comparable.
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
!id.nil? &&
comparison_object.id == id
end
alias :eql? :==
protected
# Process errors from the servers and add them to the
# model
# Servers are returned using the jsonapi format
# E.g.:
# errors: [
# {
# :id=>"f720ca10-b104-0132-dbc0-600308937d74",
# :href=>"http://maestrano.github.io/enterprise/#users-users-list-post",
# :status=>"400",
# :code=>"name-can-t-be-blank",
# :title=>"Name can't be blank",
# :detail=>"Name can't be blank"
# :attribute => "name"
# :value => "can't be blank"
# }
# ]
def process_response_errors
if self.response_errors && self.response_errors.any?
self.response_errors.each do |error|
key = error[:attribute] && !error[:attribute].empty? ? error[:attribute] : :base
val = error[:value] && !error[:value].empty? ? error[:value] : error[:title]
self.errors[key.to_sym] << val
end
end
end
# ActiveRecord Compatibility for Her
def raise_record_invalid
raise(Her::Errors::ResourceInvalid.new(self))
end
# ActiveRecord Compatibility for Her
def perform_validations(options={}) # :nodoc:
# errors.blank? to avoid the unexpected case when errors is nil...
# -> THIS IS A TEMPORARY UGLY FIX
options[:validate] == false || self.errors.nil? || valid?(options[:context])
end
end
end