jsonapi-resources 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +68 -1
- data/Rakefile +3 -7
- data/lib/jsonapi-resources.rb +1 -0
- data/lib/jsonapi/configuration.rb +29 -4
- data/lib/jsonapi/error_codes.rb +6 -2
- data/lib/jsonapi/exceptions.rb +92 -19
- data/lib/jsonapi/operation.rb +0 -18
- data/lib/jsonapi/paginator.rb +98 -0
- data/lib/jsonapi/request.rb +257 -182
- data/lib/jsonapi/resource.rb +58 -47
- data/lib/jsonapi/resource_controller.rb +85 -29
- data/lib/jsonapi/resource_serializer.rb +88 -33
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/routing_ext.rb +38 -12
- data/test/controllers/controller_test.rb +761 -455
- data/test/fixtures/active_record.rb +90 -18
- data/test/integration/requests/request_test.rb +183 -25
- data/test/integration/routes/routes_test.rb +0 -5
- data/test/test_helper.rb +31 -7
- data/test/unit/operation/operations_processor_test.rb +28 -1
- data/test/unit/resource/resource_test.rb +4 -0
- data/test/unit/serializer/serializer_test.rb +882 -377
- metadata +3 -2
    
        data/lib/jsonapi/resource.rb
    CHANGED
    
    | @@ -72,12 +72,6 @@ module JSONAPI | |
| 72 72 | 
             
                  end
         | 
| 73 73 | 
             
                end
         | 
| 74 74 |  | 
| 75 | 
            -
                def create_has_one_link(association_type, association_key_value)
         | 
| 76 | 
            -
                  change :create_has_one_link do
         | 
| 77 | 
            -
                    _create_has_one_link(association_type, association_key_value)
         | 
| 78 | 
            -
                  end
         | 
| 79 | 
            -
                end
         | 
| 80 | 
            -
             | 
| 81 75 | 
             
                def replace_has_one_link(association_type, association_key_value)
         | 
| 82 76 | 
             
                  change :replace_has_one_link do
         | 
| 83 77 | 
             
                    _replace_has_one_link(association_type, association_key_value)
         | 
| @@ -148,19 +142,6 @@ module JSONAPI | |
| 148 142 | 
             
                  @save_needed = true
         | 
| 149 143 | 
             
                end
         | 
| 150 144 |  | 
| 151 | 
            -
                def _create_has_one_link(association_type, association_key_value)
         | 
| 152 | 
            -
                  association = self.class._associations[association_type]
         | 
| 153 | 
            -
             | 
| 154 | 
            -
                  # ToDo: Add option to skip relations that already exist instead of returning an error?
         | 
| 155 | 
            -
                  relation = @model.send("#{association.foreign_key}")
         | 
| 156 | 
            -
                  if relation.nil?
         | 
| 157 | 
            -
                    send("#{association.foreign_key}=", association_key_value)
         | 
| 158 | 
            -
                  else
         | 
| 159 | 
            -
                    raise JSONAPI::Exceptions::HasOneRelationExists.new
         | 
| 160 | 
            -
                  end
         | 
| 161 | 
            -
                  @save_needed = true
         | 
| 162 | 
            -
                end
         | 
| 163 | 
            -
             | 
| 164 145 | 
             
                def _replace_has_one_link(association_type, association_key_value)
         | 
| 165 146 | 
             
                  association = self.class._associations[association_type]
         | 
| 166 147 |  | 
| @@ -215,6 +196,8 @@ module JSONAPI | |
| 215 196 | 
             
                    type = base.name.demodulize.sub(/Resource$/, '').underscore
         | 
| 216 197 | 
             
                    base._type = type.pluralize.to_sym
         | 
| 217 198 |  | 
| 199 | 
            +
                    base.attribute :id, format: :id
         | 
| 200 | 
            +
             | 
| 218 201 | 
             
                    check_reserved_resource_name(base._type, base.name)
         | 
| 219 202 |  | 
| 220 203 | 
             
                    # If eager loading is on this is how all the resource types are setup
         | 
| @@ -223,7 +206,7 @@ module JSONAPI | |
| 223 206 | 
             
                    @@resource_types[base._type] ||= base.name.demodulize
         | 
| 224 207 | 
             
                  end
         | 
| 225 208 |  | 
| 226 | 
            -
                  attr_accessor :_attributes, :_associations, :_allowed_filters , :_type
         | 
| 209 | 
            +
                  attr_accessor :_attributes, :_associations, :_allowed_filters , :_type, :_paginator
         | 
| 227 210 |  | 
| 228 211 | 
             
                  def create(context)
         | 
| 229 212 | 
             
                    self.new(self.create_model, context)
         | 
| @@ -251,6 +234,7 @@ module JSONAPI | |
| 251 234 | 
             
                  def attribute(attr, options = {})
         | 
| 252 235 | 
             
                    check_reserved_attribute_name(attr)
         | 
| 253 236 |  | 
| 237 | 
            +
                    @_attributes ||= {}
         | 
| 254 238 | 
             
                    @_attributes[attr] = options
         | 
| 255 239 | 
             
                    define_method attr do
         | 
| 256 240 | 
             
                      @model.send(attr)
         | 
| @@ -298,7 +282,7 @@ module JSONAPI | |
| 298 282 |  | 
| 299 283 | 
             
                  # Override in your resource to filter the updateable keys
         | 
| 300 284 | 
             
                  def updateable_fields(context = nil)
         | 
| 301 | 
            -
                    _updateable_associations | _attributes.keys
         | 
| 285 | 
            +
                    _updateable_associations | _attributes.keys - [_primary_key]
         | 
| 302 286 | 
             
                  end
         | 
| 303 287 |  | 
| 304 288 | 
             
                  # Override in your resource to filter the createable keys
         | 
| @@ -315,22 +299,27 @@ module JSONAPI | |
| 315 299 | 
             
                    _associations.keys | _attributes.keys
         | 
| 316 300 | 
             
                  end
         | 
| 317 301 |  | 
| 318 | 
            -
                  def  | 
| 319 | 
            -
                     | 
| 302 | 
            +
                  def apply_pagination(records, paginator)
         | 
| 303 | 
            +
                    if paginator
         | 
| 304 | 
            +
                      records = paginator.apply(records)
         | 
| 305 | 
            +
                    end
         | 
| 306 | 
            +
                    records
         | 
| 320 307 | 
             
                  end
         | 
| 321 308 |  | 
| 322 | 
            -
                   | 
| 323 | 
            -
             | 
| 324 | 
            -
             | 
| 325 | 
            -
                    sort_params = options.fetch(:sort_params) { [] }
         | 
| 326 | 
            -
                    includes = []
         | 
| 309 | 
            +
                  def apply_sort(records, order_options)
         | 
| 310 | 
            +
                    records.order(order_options)
         | 
| 311 | 
            +
                  end
         | 
| 327 312 |  | 
| 328 | 
            -
             | 
| 313 | 
            +
                  def apply_filter(records, filter, value)
         | 
| 314 | 
            +
                    records.where(filter => value)
         | 
| 315 | 
            +
                  end
         | 
| 329 316 |  | 
| 317 | 
            +
                  def apply_filters(records, filters)
         | 
| 318 | 
            +
                    required_includes = []
         | 
| 330 319 | 
             
                    filters.each do |filter, value|
         | 
| 331 320 | 
             
                      if _associations.include?(filter)
         | 
| 332 321 | 
             
                        if _associations[filter].is_a?(JSONAPI::Association::HasMany)
         | 
| 333 | 
            -
                           | 
| 322 | 
            +
                          required_includes.push(filter)
         | 
| 334 323 | 
             
                          records = apply_filter(records, "#{filter}.#{_associations[filter].primary_key}", value)
         | 
| 335 324 | 
             
                        else
         | 
| 336 325 | 
             
                          records = apply_filter(records, "#{_associations[filter].foreign_key}", value)
         | 
| @@ -339,10 +328,22 @@ module JSONAPI | |
| 339 328 | 
             
                        records = apply_filter(records, filter, value)
         | 
| 340 329 | 
             
                      end
         | 
| 341 330 | 
             
                    end
         | 
| 331 | 
            +
                    records.includes(required_includes)
         | 
| 332 | 
            +
                  end
         | 
| 333 | 
            +
             | 
| 334 | 
            +
                  # Override this method if you have more complex requirements than this basic find method provides
         | 
| 335 | 
            +
                  def find(filters, options = {})
         | 
| 336 | 
            +
                    context = options[:context]
         | 
| 337 | 
            +
                    sort_criteria = options.fetch(:sort_criteria) { [] }
         | 
| 342 338 |  | 
| 343 339 | 
             
                    resources = []
         | 
| 344 | 
            -
             | 
| 345 | 
            -
                    records | 
| 340 | 
            +
             | 
| 341 | 
            +
                    records = records(options)
         | 
| 342 | 
            +
                    records = apply_filters(records, filters)
         | 
| 343 | 
            +
                    records = apply_sort(records, construct_order_options(sort_criteria))
         | 
| 344 | 
            +
                    records = apply_pagination(records, options[:paginator])
         | 
| 345 | 
            +
             | 
| 346 | 
            +
                    records.each do |model|
         | 
| 346 347 | 
             
                      resources.push self.new(model, context)
         | 
| 347 348 | 
             
                    end
         | 
| 348 349 |  | 
| @@ -480,6 +481,14 @@ module JSONAPI | |
| 480 481 | 
             
                    return class_name
         | 
| 481 482 | 
             
                  end
         | 
| 482 483 |  | 
| 484 | 
            +
                  def _paginator
         | 
| 485 | 
            +
                    @_paginator ||= JSONAPI.configuration.default_paginator
         | 
| 486 | 
            +
                  end
         | 
| 487 | 
            +
             | 
| 488 | 
            +
                  def paginator(paginator)
         | 
| 489 | 
            +
                    @_paginator = paginator
         | 
| 490 | 
            +
                  end
         | 
| 491 | 
            +
             | 
| 483 492 | 
             
                  # :nocov:
         | 
| 484 493 | 
             
                  if RUBY_VERSION >= '2.0'
         | 
| 485 494 | 
             
                    def _model_class
         | 
| @@ -500,8 +509,13 @@ module JSONAPI | |
| 500 509 | 
             
                    @module_path ||= self.name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').downcase : ''
         | 
| 501 510 | 
             
                  end
         | 
| 502 511 |  | 
| 503 | 
            -
                   | 
| 512 | 
            +
                  def construct_order_options(sort_params)
         | 
| 513 | 
            +
                    sort_params.each_with_object({}) { |sort, order_hash|
         | 
| 514 | 
            +
                      order_hash[sort[:field]] = sort[:direction]
         | 
| 515 | 
            +
                    }
         | 
| 516 | 
            +
                  end
         | 
| 504 517 |  | 
| 518 | 
            +
                  private
         | 
| 505 519 | 
             
                  def check_reserved_resource_name(type, name)
         | 
| 506 520 | 
             
                    if [:ids, :types, :hrefs, :links].include?(type)
         | 
| 507 521 | 
             
                      warn "[NAME COLLISION] `#{name}` is a reserved resource name."
         | 
| @@ -551,14 +565,21 @@ module JSONAPI | |
| 551 565 | 
             
                          end
         | 
| 552 566 | 
             
                        end unless method_defined?(attr)
         | 
| 553 567 | 
             
                      elsif @_associations[attr].is_a?(JSONAPI::Association::HasMany)
         | 
| 554 | 
            -
                        define_method attr do
         | 
| 568 | 
            +
                        define_method attr do |options = {}|
         | 
| 555 569 | 
             
                          type_name = self.class._associations[attr].type.to_s
         | 
| 556 570 | 
             
                          resource_class = self.class.resource_for(self.class.module_path + type_name)
         | 
| 571 | 
            +
                          filters = options.fetch(:filters, {})
         | 
| 572 | 
            +
                          sort_criteria =  options.fetch(:sort_criteria, {})
         | 
| 573 | 
            +
                          paginator = options.fetch(:paginator, nil)
         | 
| 574 | 
            +
             | 
| 557 575 | 
             
                          resources = []
         | 
| 558 576 | 
             
                          if resource_class
         | 
| 559 | 
            -
                             | 
| 560 | 
            -
                             | 
| 561 | 
            -
             | 
| 577 | 
            +
                            records = @model.send attr
         | 
| 578 | 
            +
                            records = self.class.apply_filters(records, filters)
         | 
| 579 | 
            +
                            records = self.class.apply_sort(records, self.class.construct_order_options(sort_criteria))
         | 
| 580 | 
            +
                            records = self.class.apply_pagination(records, paginator)
         | 
| 581 | 
            +
                            records.each do |record|
         | 
| 582 | 
            +
                              resources.push resource_class.new(record, @context)
         | 
| 562 583 | 
             
                            end
         | 
| 563 584 | 
             
                          end
         | 
| 564 585 | 
             
                          return resources
         | 
| @@ -566,16 +587,6 @@ module JSONAPI | |
| 566 587 | 
             
                      end
         | 
| 567 588 | 
             
                    end
         | 
| 568 589 | 
             
                  end
         | 
| 569 | 
            -
             | 
| 570 | 
            -
                  def construct_order_options(sort_params)
         | 
| 571 | 
            -
                    sort_params.each_with_object({}) { |sort_key, order_hash|
         | 
| 572 | 
            -
                      if sort_key.starts_with?('-')
         | 
| 573 | 
            -
                        order_hash[sort_key.slice(1..-1)] = :desc
         | 
| 574 | 
            -
                      else
         | 
| 575 | 
            -
                        order_hash[sort_key] = :asc
         | 
| 576 | 
            -
                      end
         | 
| 577 | 
            -
                    }
         | 
| 578 | 
            -
                  end
         | 
| 579 590 | 
             
                end
         | 
| 580 591 | 
             
              end
         | 
| 581 592 | 
             
            end
         | 
| @@ -18,32 +18,40 @@ module JSONAPI | |
| 18 18 | 
             
                after_filter :setup_response
         | 
| 19 19 |  | 
| 20 20 | 
             
                def index
         | 
| 21 | 
            -
                   | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 21 | 
            +
                  serializer = JSONAPI::ResourceSerializer.new(resource_klass,
         | 
| 22 | 
            +
                                                               include: @request.include,
         | 
| 23 | 
            +
                                                               fields: @request.fields,
         | 
| 24 | 
            +
                                                               base_url: base_url,
         | 
| 25 | 
            +
                                                               key_formatter: key_formatter,
         | 
| 26 | 
            +
                                                               route_formatter: route_formatter)
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  resource_records = resource_klass.find(resource_klass.verify_filters(@request.filters, context),
         | 
| 29 | 
            +
                                                         context: context,
         | 
| 30 | 
            +
                                                         sort_criteria: @request.sort_criteria,
         | 
| 31 | 
            +
                                                         paginator: @request.paginator)
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  render json: serializer.serialize_to_hash(resource_records)
         | 
| 28 34 | 
             
                rescue => e
         | 
| 29 35 | 
             
                  handle_exceptions(e)
         | 
| 30 36 | 
             
                end
         | 
| 31 37 |  | 
| 32 38 | 
             
                def show
         | 
| 39 | 
            +
                  serializer = JSONAPI::ResourceSerializer.new(resource_klass,
         | 
| 40 | 
            +
                                                               include: @request.include,
         | 
| 41 | 
            +
                                                               fields: @request.fields,
         | 
| 42 | 
            +
                                                               base_url: base_url,
         | 
| 43 | 
            +
                                                               key_formatter: key_formatter,
         | 
| 44 | 
            +
                                                               route_formatter: route_formatter)
         | 
| 45 | 
            +
             | 
| 33 46 | 
             
                  keys = parse_key_array(params[resource_klass._primary_key])
         | 
| 34 47 |  | 
| 35 | 
            -
                   | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
                  render json:  | 
| 42 | 
            -
                      resources,
         | 
| 43 | 
            -
                      include: @request.include,
         | 
| 44 | 
            -
                      fields: @request.fields,
         | 
| 45 | 
            -
                      attribute_formatters: attribute_formatters,
         | 
| 46 | 
            -
                      key_formatter: key_formatter)
         | 
| 48 | 
            +
                  resource_records = if keys.length > 1
         | 
| 49 | 
            +
                                       resource_klass.find_by_keys(keys, context: context)
         | 
| 50 | 
            +
                                     else
         | 
| 51 | 
            +
                                       resource_klass.find_by_key(keys[0], context: context)
         | 
| 52 | 
            +
                                     end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  render json: serializer.serialize_to_hash(resource_records)
         | 
| 47 55 | 
             
                rescue => e
         | 
| 48 56 | 
             
                  handle_exceptions(e)
         | 
| 49 57 | 
             
                end
         | 
| @@ -56,7 +64,14 @@ module JSONAPI | |
| 56 64 | 
             
                  parent_resource = resource_klass.find_by_key(parent_key, context: context)
         | 
| 57 65 |  | 
| 58 66 | 
             
                  association = resource_klass._association(association_type)
         | 
| 59 | 
            -
             | 
| 67 | 
            +
             | 
| 68 | 
            +
                  serializer = JSONAPI::ResourceSerializer.new(resource_klass,
         | 
| 69 | 
            +
                                                               fields: @request.fields,
         | 
| 70 | 
            +
                                                               base_url: base_url,
         | 
| 71 | 
            +
                                                               key_formatter: key_formatter,
         | 
| 72 | 
            +
                                                               route_formatter: route_formatter)
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  render json: serializer.serialize_to_links_hash(parent_resource, association)
         | 
| 60 75 | 
             
                rescue => e
         | 
| 61 76 | 
             
                  # :nocov:
         | 
| 62 77 | 
             
                  handle_exceptions(e)
         | 
| @@ -87,6 +102,41 @@ module JSONAPI | |
| 87 102 | 
             
                  process_request_operations
         | 
| 88 103 | 
             
                end
         | 
| 89 104 |  | 
| 105 | 
            +
                def get_related_resource
         | 
| 106 | 
            +
                  association_type = params[:association]
         | 
| 107 | 
            +
                  source_resource = @request.source_klass.find_by_key(@request.source_id, context: context)
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  serializer = JSONAPI::ResourceSerializer.new(@request.source_klass,
         | 
| 110 | 
            +
                                                               include: @request.include,
         | 
| 111 | 
            +
                                                               fields: @request.fields,
         | 
| 112 | 
            +
                                                               base_url: base_url,
         | 
| 113 | 
            +
                                                               key_formatter: key_formatter,
         | 
| 114 | 
            +
                                                               route_formatter: route_formatter)
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                  render json: serializer.serialize_to_hash(source_resource.send(association_type))
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def get_related_resources
         | 
| 120 | 
            +
                  association_type = params[:association]
         | 
| 121 | 
            +
                  source_resource = @request.source_klass.find_by_key(@request.source_id, context: context)
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  related_resources = source_resource.send(association_type,
         | 
| 124 | 
            +
                                                           {
         | 
| 125 | 
            +
                                                             filters:  @request.source_klass.verify_filters(@request.filters, context),
         | 
| 126 | 
            +
                                                             sort_criteria: @request.sort_criteria,
         | 
| 127 | 
            +
                                                             paginator: @request.paginator
         | 
| 128 | 
            +
                                                           })
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                  serializer = JSONAPI::ResourceSerializer.new(@request.source_klass,
         | 
| 131 | 
            +
                                                               include: @request.include,
         | 
| 132 | 
            +
                                                               fields: @request.fields,
         | 
| 133 | 
            +
                                                               base_url: base_url,
         | 
| 134 | 
            +
                                                               key_formatter: key_formatter,
         | 
| 135 | 
            +
                                                               route_formatter: route_formatter)
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                  render json: serializer.serialize_to_hash(related_resources)
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 90 140 | 
             
                # Override this to use another operations processor
         | 
| 91 141 | 
             
                def create_operations_processor
         | 
| 92 142 | 
             
                  JSONAPI::ActiveRecordOperationsProcessor.new
         | 
| @@ -105,6 +155,10 @@ module JSONAPI | |
| 105 155 | 
             
                end
         | 
| 106 156 | 
             
                # :nocov:
         | 
| 107 157 |  | 
| 158 | 
            +
                def base_url
         | 
| 159 | 
            +
                  @base_url ||= request.protocol + request.host_with_port
         | 
| 160 | 
            +
                end
         | 
| 161 | 
            +
             | 
| 108 162 | 
             
                def resource_klass_name
         | 
| 109 163 | 
             
                  @resource_klass_name ||= "#{self.class.name.sub(/Controller$/, '').singularize}Resource"
         | 
| 110 164 | 
             
                end
         | 
| @@ -149,6 +203,7 @@ module JSONAPI | |
| 149 203 |  | 
| 150 204 | 
             
                # Control by setting in an initializer:
         | 
| 151 205 | 
             
                #     JSONAPI.configuration.json_key_format = :camelized_key
         | 
| 206 | 
            +
                #     JSONAPI.configuration.route = :camelized_route
         | 
| 152 207 | 
             
                #
         | 
| 153 208 | 
             
                # Override if you want to set a per controller key format.
         | 
| 154 209 | 
             
                # Must return a class derived from KeyFormatter.
         | 
| @@ -156,9 +211,8 @@ module JSONAPI | |
| 156 211 | 
             
                  JSONAPI.configuration.key_formatter
         | 
| 157 212 | 
             
                end
         | 
| 158 213 |  | 
| 159 | 
            -
                 | 
| 160 | 
            -
             | 
| 161 | 
            -
                  {}
         | 
| 214 | 
            +
                def route_formatter
         | 
| 215 | 
            +
                  JSONAPI.configuration.route_formatter
         | 
| 162 216 | 
             
                end
         | 
| 163 217 |  | 
| 164 218 | 
             
                def render_errors(errors)
         | 
| @@ -185,13 +239,15 @@ module JSONAPI | |
| 185 239 | 
             
                    render status: errors[0].status, json: {errors: errors}
         | 
| 186 240 | 
             
                  else
         | 
| 187 241 | 
             
                    if results.length > 0 && resources.length > 0
         | 
| 242 | 
            +
                      serializer = JSONAPI::ResourceSerializer.new(resource_klass,
         | 
| 243 | 
            +
                                                                   include: @request.include,
         | 
| 244 | 
            +
                                                                   fields: @request.fields,
         | 
| 245 | 
            +
                                                                   base_url: base_url,
         | 
| 246 | 
            +
                                                                   key_formatter: key_formatter,
         | 
| 247 | 
            +
                                                                   route_formatter: route_formatter)
         | 
| 248 | 
            +
             | 
| 188 249 | 
             
                      render status: results[0].code,
         | 
| 189 | 
            -
                             json:  | 
| 190 | 
            -
                               resources.length > 1 ? resources : resources[0],
         | 
| 191 | 
            -
                               include: @request.include,
         | 
| 192 | 
            -
                               fields: @request.fields,
         | 
| 193 | 
            -
                               attribute_formatters: attribute_formatters,
         | 
| 194 | 
            -
                               key_formatter: key_formatter)
         | 
| 250 | 
            +
                             json: serializer.serialize_to_hash(resources.length > 1 ? resources : resources[0])
         | 
| 195 251 | 
             
                    else
         | 
| 196 252 | 
             
                      render status: results[0].code, json: nil
         | 
| 197 253 | 
             
                    end
         | 
| @@ -1,12 +1,7 @@ | |
| 1 1 | 
             
            module JSONAPI
         | 
| 2 2 | 
             
              class ResourceSerializer
         | 
| 3 3 |  | 
| 4 | 
            -
                 | 
| 5 | 
            -
                  @primary_resource_klass = primary_resource_klass
         | 
| 6 | 
            -
                  @primary_class_name = @primary_resource_klass._type
         | 
| 7 | 
            -
                end
         | 
| 8 | 
            -
             | 
| 9 | 
            -
                # Converts a single resource, or an array of resources to a hash, conforming to the JSONAPI structure
         | 
| 4 | 
            +
                # Options can include
         | 
| 10 5 | 
             
                # include:
         | 
| 11 6 | 
             
                #     Purpose: determines which objects will be side loaded with the source objects in a linked section
         | 
| 12 7 | 
             
                #     Example: ['comments','author','comments.tags','author.posts']
         | 
| @@ -14,26 +9,33 @@ module JSONAPI | |
| 14 9 | 
             
                #     Purpose: determines which fields are serialized for a resource type. This encompasses both attributes and
         | 
| 15 10 | 
             
                #              association ids in the links section for a resource. Fields are global for a resource type.
         | 
| 16 11 | 
             
                #     Example: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
         | 
| 17 | 
            -
                 | 
| 18 | 
            -
             | 
| 12 | 
            +
                # key_formatter: KeyFormatter class to override the default configuration
         | 
| 13 | 
            +
                # base_url: a string to prepend to generated resource links
         | 
| 19 14 |  | 
| 20 | 
            -
             | 
| 21 | 
            -
                   | 
| 15 | 
            +
                def initialize(primary_resource_klass, options = {})
         | 
| 16 | 
            +
                  @primary_resource_klass = primary_resource_klass
         | 
| 17 | 
            +
                  @primary_class_name = @primary_resource_klass._type
         | 
| 22 18 |  | 
| 19 | 
            +
                  @fields =  options.fetch(:fields, {})
         | 
| 20 | 
            +
                  @include = options.fetch(:include, [])
         | 
| 23 21 | 
             
                  @key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
         | 
| 22 | 
            +
                  @route_formatter = options.fetch(:route_formatter, JSONAPI.configuration.route_formatter)
         | 
| 23 | 
            +
                  @base_url = options.fetch(:base_url, '')
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                # Converts a single resource, or an array of resources to a hash, conforming to the JSONAPI structure
         | 
| 27 | 
            +
                def serialize_to_hash(source)
         | 
| 28 | 
            +
                  is_resource_collection = source.respond_to?(:to_ary)
         | 
| 24 29 |  | 
| 25 30 | 
             
                  @linked_objects = {}
         | 
| 26 31 |  | 
| 27 | 
            -
                  requested_associations = parse_includes(include)
         | 
| 32 | 
            +
                  requested_associations = parse_includes(@include)
         | 
| 28 33 |  | 
| 29 34 | 
             
                  process_primary(source, requested_associations)
         | 
| 30 35 |  | 
| 31 | 
            -
                   | 
| 36 | 
            +
                  linked_objects = []
         | 
| 32 37 | 
             
                  primary_objects = []
         | 
| 33 | 
            -
                  @linked_objects. | 
| 34 | 
            -
                    class_name = class_name.to_sym
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                    linked_objects = []
         | 
| 38 | 
            +
                  @linked_objects.each_value do |objects|
         | 
| 37 39 | 
             
                    objects.each_value do |object|
         | 
| 38 40 | 
             
                      if object[:primary]
         | 
| 39 41 | 
             
                        primary_objects.push(object[:object_hash])
         | 
| @@ -41,20 +43,20 @@ module JSONAPI | |
| 41 43 | 
             
                        linked_objects.push(object[:object_hash])
         | 
| 42 44 | 
             
                      end
         | 
| 43 45 | 
             
                    end
         | 
| 44 | 
            -
                    linked_hash[format_key(class_name)] = linked_objects unless linked_objects.empty?
         | 
| 45 46 | 
             
                  end
         | 
| 46 47 |  | 
| 47 | 
            -
                   | 
| 48 | 
            -
                    primary_hash = {format_key(@primary_class_name) => primary_objects}
         | 
| 49 | 
            -
                  else
         | 
| 50 | 
            -
                    primary_hash = {format_key(@primary_class_name) => primary_objects[0]}
         | 
| 51 | 
            -
                  end
         | 
| 48 | 
            +
                  primary_hash = {data: is_resource_collection ? primary_objects : primary_objects[0]}
         | 
| 52 49 |  | 
| 53 | 
            -
                  if  | 
| 54 | 
            -
                    primary_hash | 
| 50 | 
            +
                  if linked_objects.size > 0
         | 
| 51 | 
            +
                    primary_hash[:linked] = linked_objects
         | 
| 55 52 | 
             
                  else
         | 
| 56 53 | 
             
                    primary_hash
         | 
| 57 54 | 
             
                  end
         | 
| 55 | 
            +
                  primary_hash
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def serialize_to_links_hash(source, requested_association)
         | 
| 59 | 
            +
                  {data: link_object(source, requested_association, true)}
         | 
| 58 60 | 
             
                end
         | 
| 59 61 |  | 
| 60 62 | 
             
                private
         | 
| @@ -111,6 +113,10 @@ module JSONAPI | |
| 111 113 | 
             
                def object_hash(source, requested_associations)
         | 
| 112 114 | 
             
                  obj_hash = attribute_hash(source)
         | 
| 113 115 | 
             
                  links = links_hash(source, requested_associations)
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                  # ToDo: Do we format these required keys
         | 
| 118 | 
            +
                  obj_hash[format_key('type')] = format_value(source.class._type.to_s, :default, source)
         | 
| 119 | 
            +
                  obj_hash[format_key('id')] ||= format_value(source.id, :id, source)
         | 
| 114 120 | 
             
                  obj_hash.merge!({links: links}) unless links.empty?
         | 
| 115 121 | 
             
                  return obj_hash
         | 
| 116 122 | 
             
                end
         | 
| @@ -152,30 +158,33 @@ module JSONAPI | |
| 152 158 | 
             
                  field_set = Set.new(fields)
         | 
| 153 159 |  | 
| 154 160 | 
             
                  included_associations = source.fetchable_fields & associations.keys
         | 
| 155 | 
            -
                  associations.each_with_object({}) do |(name, association), hash|
         | 
| 156 | 
            -
                    if included_associations.include? name
         | 
| 157 161 |  | 
| 158 | 
            -
             | 
| 159 | 
            -
             | 
| 160 | 
            -
                      end
         | 
| 162 | 
            +
                  links = {}
         | 
| 163 | 
            +
                  links[:self] = self_href(source)
         | 
| 161 164 |  | 
| 165 | 
            +
                  associations.each_with_object(links) do |(name, association), hash|
         | 
| 166 | 
            +
                    if included_associations.include? name
         | 
| 162 167 | 
             
                      ia = requested_associations.is_a?(Hash) ? requested_associations[name] : nil
         | 
| 163 168 |  | 
| 164 | 
            -
                       | 
| 169 | 
            +
                      include_linkage = ia && ia[:include]
         | 
| 165 170 | 
             
                      include_linked_children = ia && ia[:include_children]
         | 
| 166 171 |  | 
| 172 | 
            +
                      if field_set.include?(name)
         | 
| 173 | 
            +
                        hash[format_key(name)] = link_object(source, association, include_linkage)
         | 
| 174 | 
            +
                      end
         | 
| 175 | 
            +
             | 
| 167 176 | 
             
                      type = association.type
         | 
| 168 177 |  | 
| 169 178 | 
             
                      # If the object has been serialized once it will be in the related objects list,
         | 
| 170 179 | 
             
                      # but it's possible all children won't have been captured. So we must still go
         | 
| 171 180 | 
             
                      # through the associations.
         | 
| 172 | 
            -
                      if  | 
| 181 | 
            +
                      if include_linkage || include_linked_children
         | 
| 173 182 | 
             
                        if association.is_a?(JSONAPI::Association::HasOne)
         | 
| 174 183 | 
             
                          resource = source.send(name)
         | 
| 175 184 | 
             
                          if resource
         | 
| 176 185 | 
             
                            id = resource.id
         | 
| 177 186 | 
             
                            associations_only = already_serialized?(type, id)
         | 
| 178 | 
            -
                            if  | 
| 187 | 
            +
                            if include_linkage && !associations_only
         | 
| 179 188 | 
             
                              add_linked_object(type, id, object_hash(resource, ia[:include_related]))
         | 
| 180 189 | 
             
                            elsif include_linked_children || associations_only
         | 
| 181 190 | 
             
                              links_hash(resource, ia[:include_related])
         | 
| @@ -186,7 +195,7 @@ module JSONAPI | |
| 186 195 | 
             
                          resources.each do |resource|
         | 
| 187 196 | 
             
                            id = resource.id
         | 
| 188 197 | 
             
                            associations_only = already_serialized?(type, id)
         | 
| 189 | 
            -
                            if  | 
| 198 | 
            +
                            if include_linkage && !associations_only
         | 
| 190 199 | 
             
                              add_linked_object(type, id, object_hash(resource, ia[:include_related]))
         | 
| 191 200 | 
             
                            elsif include_linked_children || associations_only
         | 
| 192 201 | 
             
                              links_hash(resource, ia[:include_related])
         | 
| @@ -198,11 +207,57 @@ module JSONAPI | |
| 198 207 | 
             
                  end
         | 
| 199 208 | 
             
                end
         | 
| 200 209 |  | 
| 210 | 
            +
                def formatted_module_path(source)
         | 
| 211 | 
            +
                  source.class.name =~ /::[^:]+\Z/ ? (@route_formatter.format($`).freeze.gsub('::', '/') + '/').downcase : ''
         | 
| 212 | 
            +
                end
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                def self_href(source)
         | 
| 215 | 
            +
                  "#{@base_url}/#{formatted_module_path(source)}#{@route_formatter.format(source.class._type.to_s)}/#{source.id}"
         | 
| 216 | 
            +
                end
         | 
| 217 | 
            +
             | 
| 201 218 | 
             
                def already_serialized?(type, id)
         | 
| 202 219 | 
             
                  type = format_key(type)
         | 
| 203 220 | 
             
                  return @linked_objects.key?(type) && @linked_objects[type].key?(id)
         | 
| 204 221 | 
             
                end
         | 
| 205 222 |  | 
| 223 | 
            +
                def format_route(route)
         | 
| 224 | 
            +
                  @route_formatter.format(route.to_s)
         | 
| 225 | 
            +
                end
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                def link_object_has_one(source, association)
         | 
| 228 | 
            +
                  route = association.name
         | 
| 229 | 
            +
             | 
| 230 | 
            +
                  link_object_hash = {}
         | 
| 231 | 
            +
                  link_object_hash[:self] = "#{self_href(source)}/links/#{format_route(route)}"
         | 
| 232 | 
            +
                  link_object_hash[:resource] = "#{self_href(source)}/#{format_route(route)}"
         | 
| 233 | 
            +
                  # ToDo: Get correct formatting figured out
         | 
| 234 | 
            +
                  link_object_hash[:type] = format_route(association.type)
         | 
| 235 | 
            +
                  link_object_hash[:id] = foreign_key_value(source, association)
         | 
| 236 | 
            +
                  link_object_hash
         | 
| 237 | 
            +
                end
         | 
| 238 | 
            +
             | 
| 239 | 
            +
                def link_object_has_many(source, association, include_linkage)
         | 
| 240 | 
            +
                  route = association.name
         | 
| 241 | 
            +
             | 
| 242 | 
            +
                  link_object_hash = {}
         | 
| 243 | 
            +
                  link_object_hash[:self] = "#{self_href(source)}/links/#{format_route(route)}"
         | 
| 244 | 
            +
                  link_object_hash[:resource] = "#{self_href(source)}/#{format_route(route)}"
         | 
| 245 | 
            +
                  if include_linkage
         | 
| 246 | 
            +
                    # ToDo: Get correct formatting figured out
         | 
| 247 | 
            +
                    link_object_hash[:type] = format_route(association.type)
         | 
| 248 | 
            +
                    link_object_hash[:ids] = foreign_key_value(source, association)
         | 
| 249 | 
            +
                  end
         | 
| 250 | 
            +
                  link_object_hash
         | 
| 251 | 
            +
                end
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                def link_object(source, association, include_linkage = false)
         | 
| 254 | 
            +
                  if association.is_a?(JSONAPI::Association::HasOne)
         | 
| 255 | 
            +
                    link_object_has_one(source, association)
         | 
| 256 | 
            +
                  elsif association.is_a?(JSONAPI::Association::HasMany)
         | 
| 257 | 
            +
                    link_object_has_many(source, association, include_linkage)
         | 
| 258 | 
            +
                  end
         | 
| 259 | 
            +
                end
         | 
| 260 | 
            +
             | 
| 206 261 | 
             
                # Extracts the foreign key value for an association.
         | 
| 207 262 | 
             
                def foreign_key_value(source, association)
         | 
| 208 263 | 
             
                  foreign_key = association.foreign_key
         |