lib/jsonapi/resource.rb in jsonapi-resources-0.3.3 vs lib/jsonapi/resource.rb in jsonapi-resources-0.4.0
- old
+ new
@@ -1,7 +1,5 @@
-require 'jsonapi/configuration'
-require 'jsonapi/association'
require 'jsonapi/callbacks'
module JSONAPI
class Resource
include Callbacks
@@ -35,23 +33,30 @@
def is_new?
id.nil?
end
def change(callback)
+ completed = false
+
if @changing
run_callbacks callback do
- yield
+ completed = (yield == :completed)
end
else
run_callbacks is_new? ? :create : :update do
@changing = true
run_callbacks callback do
- yield
- save if @save_needed || is_new?
+ completed = (yield == :completed)
end
+
+ if @save_needed || is_new?
+ completed = (save == :completed)
+ end
end
end
+
+ return completed ? :completed : :accepted
end
def remove
run_callbacks :remove do
_remove
@@ -98,11 +103,11 @@
def fetchable_fields
self.class.fields
end
# Override this on a resource to customize how the associated records
- # are fetched for a model. Particularly helpful for authoriztion.
+ # are fetched for a model. Particularly helpful for authorization.
def records_for(association_name, options = {})
model.send association_name
end
private
@@ -110,19 +115,46 @@
run_callbacks :save do
_save
end
end
+ # Override this on a resource to return a different result code. Any
+ # value other than :completed will result in operations returning
+ # `:accepted`
+ #
+ # For example to return `:accepted` if your model does not immediately
+ # save resources to the database you could override `_save` as follows:
+ #
+ # ```
+ # def _save
+ # super
+ # return :accepted
+ # end
+ # ```
def _save
- @model.save!
- @save_needed = false
- rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
- raise JSONAPI::Exceptions::ValidationErrors.new(e.record.errors.messages)
+ unless @model.valid?
+ raise JSONAPI::Exceptions::ValidationErrors.new(@model.errors.messages)
+ end
+
+ if defined? @model.save
+ saved = @model.save
+ unless saved
+ raise JSONAPI::Exceptions::SaveFailed.new
+ end
+ else
+ saved = true
+ end
+
+ @save_needed = !saved
+
+ return :completed
end
def _remove
@model.destroy
+
+ return :completed
end
def _create_has_many_links(association_type, association_key_values)
association = self.class._associations[association_type]
@@ -135,37 +167,47 @@
@model.send(association.type) << related_resource.model
else
raise JSONAPI::Exceptions::HasManyRelationExists.new(association_key_value)
end
end
+
+ return :completed
end
def _replace_has_many_links(association_type, association_key_values)
association = self.class._associations[association_type]
send("#{association.foreign_key}=", association_key_values)
@save_needed = true
+
+ return :completed
end
def _replace_has_one_link(association_type, association_key_value)
association = self.class._associations[association_type]
send("#{association.foreign_key}=", association_key_value)
@save_needed = true
+
+ return :completed
end
def _remove_has_many_link(association_type, key)
association = self.class._associations[association_type]
@model.send(association.type).delete(key)
+
+ return :completed
end
def _remove_has_one_link(association_type)
association = self.class._associations[association_type]
send("#{association.foreign_key}=", nil)
@save_needed = true
+
+ return :completed
end
def _replace_fields(field_data)
field_data[:attributes].each do |attribute, value|
begin
@@ -187,10 +229,12 @@
end if field_data[:has_one]
field_data[:has_many].each do |association_type, values|
replace_has_many_links(association_type, values)
end if field_data[:has_many]
+
+ return :completed
end
class << self
def inherited(base)
base._attributes = (_attributes || {}).dup
@@ -240,10 +284,14 @@
end
def attribute(attr, options = {})
check_reserved_attribute_name(attr)
+ if (attr.to_sym == :id) && (options[:format].nil?)
+ ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
+ end
+
@_attributes ||= {}
@_attributes[attr] = options
define_method attr do
@model.send(attr)
end unless method_defined?(attr)
@@ -268,94 +316,148 @@
def model_name(model)
@_model_name = model.to_sym
end
def filters(*attrs)
- @_allowed_filters.merge(attrs)
+ @_allowed_filters.merge!(attrs.inject( Hash.new ) { |h, attr| h[attr] = {}; h })
end
- def filter(attr)
- @_allowed_filters.add(attr.to_sym)
+ def filter(attr, *args)
+ @_allowed_filters[attr.to_sym] = args.extract_options!
end
def primary_key(key)
@_primary_key = key.to_sym
end
- # Override in your resource to filter the updateable keys
- def updateable_fields(context = nil)
- _updateable_associations | _attributes.keys - [_primary_key]
+ # TODO: remove this after the createable_fields and updateable_fields are phased out
+ # :nocov:
+ def method_missing(method, *args)
+ if method.to_s.match /createable_fields/
+ ActiveSupport::Deprecation.warn("`createable_fields` is deprecated, please use `creatable_fields` instead")
+ self.creatable_fields(*args)
+ elsif method.to_s.match /updateable_fields/
+ ActiveSupport::Deprecation.warn("`updateable_fields` is deprecated, please use `updatable_fields` instead")
+ self.updatable_fields(*args)
+ else
+ super
+ end
end
+ # :nocov:
- # Override in your resource to filter the createable keys
- def createable_fields(context = nil)
- _updateable_associations | _attributes.keys
+ # Override in your resource to filter the updatable keys
+ def updatable_fields(context = nil)
+ _updatable_associations | _attributes.keys - [:id]
end
+ # Override in your resource to filter the creatable keys
+ def creatable_fields(context = nil)
+ _updatable_associations | _attributes.keys
+ end
+
# Override in your resource to filter the sortable keys
def sortable_fields(context = nil)
_attributes.keys
end
def fields
_associations.keys | _attributes.keys
end
- def apply_pagination(records, paginator)
+ def apply_includes(records, directives)
+ records = records.includes(*directives.model_includes) if directives
+ records
+ end
+
+ def apply_pagination(records, paginator, order_options)
if paginator
- records = paginator.apply(records)
+ records = paginator.apply(records, order_options)
end
records
end
def apply_sort(records, order_options)
- records.order(order_options)
+ if order_options.any?
+ records.order(order_options)
+ else
+ records
+ end
end
- def apply_filter(records, filter, value)
+ def apply_filter(records, filter, value, options = {})
records.where(filter => value)
end
- def apply_filters(records, filters)
+ def apply_filters(records, filters, options = {})
required_includes = []
- filters.each do |filter, value|
- if _associations.include?(filter)
- if _associations[filter].is_a?(JSONAPI::Association::HasMany)
- required_includes.push(filter)
- records = apply_filter(records, "#{filter}.#{_associations[filter].primary_key}", value)
+
+ if filters
+ filters.each do |filter, value|
+ if _associations.include?(filter)
+ if _associations[filter].is_a?(JSONAPI::Association::HasMany)
+ required_includes.push(filter)
+ records = apply_filter(records, "#{filter}.#{_associations[filter].primary_key}", value, options)
+ else
+ records = apply_filter(records, "#{_associations[filter].foreign_key}", value, options)
+ end
else
- records = apply_filter(records, "#{_associations[filter].foreign_key}", value)
+ records = apply_filter(records, filter, value, options)
end
- else
- records = apply_filter(records, filter, value)
end
end
- records.includes(required_includes)
+
+ if required_includes.any?
+ records.includes(required_includes)
+ elsif records.respond_to? :to_ary
+ records
+ else
+ records.all
+ end
end
+ def filter_records(filters, options)
+ include_directives = options[:include_directives]
+
+ records = records(options)
+ records = apply_includes(records, include_directives)
+ apply_filters(records, filters, options)
+ end
+
+ def sort_records(records, order_options)
+ apply_sort(records, order_options)
+ end
+
+ def find_count(filters, options = {})
+ filter_records(filters, options).count
+ end
+
# Override this method if you have more complex requirements than this basic find method provides
def find(filters, options = {})
context = options[:context]
- sort_criteria = options.fetch(:sort_criteria) { [] }
- resources = []
+ records = filter_records(filters, options)
- records = records(options)
- records = apply_filters(records, filters)
- records = apply_sort(records, construct_order_options(sort_criteria))
- records = apply_pagination(records, options[:paginator])
+ sort_criteria = options.fetch(:sort_criteria) { [] }
+ order_options = construct_order_options(sort_criteria)
+ records = sort_records(records, order_options)
+ records = apply_pagination(records, options[:paginator], order_options)
+
+ resources = []
records.each do |model|
resources.push self.new(model, context)
end
return resources
end
def find_by_key(key, options = {})
context = options[:context]
- model = records(options).where({_primary_key => key}).first
+ include_directives = options[:include_directives]
+ records = records(options)
+ records = apply_includes(records, include_directives)
+ model = records.where({_primary_key => key}).first
if model.nil?
raise JSONAPI::Exceptions::RecordNotFound.new(key)
end
self.new(model, context)
end
@@ -392,11 +494,11 @@
# override to allow for key processing and checking
def verify_key(key, context = nil)
key && Integer(key)
rescue
- raise JSONAPI::Exceptions::InvalidFieldValue.new(_primary_key, key)
+ raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
end
# override to allow for key processing and checking
def verify_keys(keys, context = nil)
return keys.collect do |key|
@@ -417,17 +519,12 @@
# quasi private class methods
def _attribute_options(attr)
default_attribute_options.merge(@_attributes[attr])
end
- def _updateable_associations
- associations = []
-
- @_associations.each do |key, association|
- associations.push(key)
- end
- associations
+ def _updatable_associations
+ @_associations.map { |key, association| key }
end
def _has_association?(type)
type = type.to_s
@_associations.has_key?(type.singularize.to_sym) || @_associations.has_key?(type.pluralize.to_sym)
@@ -449,17 +546,17 @@
def _as_parent_key
@_as_parent_key ||= "#{_type.to_s.singularize}_#{_primary_key}"
end
def _allowed_filters
- !@_allowed_filters.nil? ? @_allowed_filters : Set.new([_primary_key])
+ !@_allowed_filters.nil? ? @_allowed_filters : { :id => {} }
end
def _resource_name_from_type(type)
class_name = @@resource_types[type]
if class_name.nil?
- class_name = type.to_s.singularize.camelize + 'Resource'
+ class_name = "#{type.to_s.singularize}_resource".camelize
@@resource_types[type] = class_name
end
return class_name
end
@@ -474,20 +571,23 @@
def _model_class
@model ||= _model_name.to_s.safe_constantize
end
def _allowed_filter?(filter)
- _allowed_filters.include?(filter)
+ !_allowed_filters[filter].nil?
end
def module_path
@module_path ||= self.name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').downcase : ''
end
def construct_order_options(sort_params)
+ return {} unless sort_params
+
sort_params.each_with_object({}) { |sort, order_hash|
- order_hash[sort[:field]] = sort[:direction]
+ field = sort[:field] == 'id' ? _primary_key : sort[:field]
+ order_hash[field] = sort[:direction]
}
end
private
def check_reserved_resource_name(type, name)
@@ -552,25 +652,28 @@
define_method attr do |options = {}|
type_name = self.class._associations[attr].type.to_s
resource_class = Resource.resource_for(self.class.module_path + type_name)
filters = options.fetch(:filters, {})
sort_criteria = options.fetch(:sort_criteria, {})
- paginator = options.fetch(:paginator, nil)
+ paginator = options[:paginator]
resources = []
+
if resource_class
records = public_send(associated_records_method_name)
- records = self.class.apply_filters(records, filters)
- records = self.class.apply_sort(records, self.class.construct_order_options(sort_criteria))
- records = self.class.apply_pagination(records, paginator)
+ records = resource_class.apply_filters(records, filters, options)
+ order_options = self.class.construct_order_options(sort_criteria)
+ records = resource_class.apply_sort(records, order_options)
+ records = resource_class.apply_pagination(records, paginator, order_options)
records.each do |record|
resources.push resource_class.new(record, @context)
end
end
return resources
end unless method_defined?(attr)
end
end
end
end
+
end
end