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