Class: JsonapiCompliable::Sideload

Inherits:
Object
  • Object
show all
Defined in:
lib/jsonapi_compliable/sideload.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, type: nil, resource: nil, polymorphic: false, primary_key: :id, foreign_key: nil, parent: nil) ⇒ Sideload

NB - the adapter's #sideloading_module is mixed in on instantiation

An anonymous Resource will be assigned when none provided.



32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/jsonapi_compliable/sideload.rb', line 32

def initialize(name, type: nil, resource: nil, polymorphic: false, primary_key: :id, foreign_key: nil, parent: nil)
  @name               = name
  @resource_class     = (resource || Class.new(Resource))
  @sideloads          = {}
  @polymorphic        = !!polymorphic
  @polymorphic_groups = {} if polymorphic?
  @parent             = parent
  @primary_key        = primary_key
  @foreign_key        = foreign_key
  @type               = type

  extend @resource_class.config[:adapter].sideloading_module
end

Instance Attribute Details

#assign_procProc (readonly)

The configured 'assign' block

Returns:

  • (Proc)

    the current value of assign_proc



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def assign_proc
  @assign_proc
end

#foreign_keySymbol (readonly)

The attribute used to match objects - need not be a true database foreign key.

Returns:

  • (Symbol)

    the current value of foreign_key



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def foreign_key
  @foreign_key
end

#grouping_fieldSymbol (readonly)

The configured 'group_by' symbol

Returns:

  • (Symbol)

    the current value of grouping_field



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def grouping_field
  @grouping_field
end

#nameSymbol (readonly)

The name of the sideload

Returns:

  • (Symbol)

    the current value of name



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def name
  @name
end

#parentObject (readonly)

Returns the value of attribute parent



14
15
16
# File 'lib/jsonapi_compliable/sideload.rb', line 14

def parent
  @parent
end

#polymorphicBoolean (readonly)

Is this a polymorphic sideload?

Returns:

  • (Boolean)

    the current value of polymorphic



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def polymorphic
  @polymorphic
end

#polymorphic_groupsHash (readonly)

The subgroups, when polymorphic

Returns:

  • (Hash)

    the current value of polymorphic_groups



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def polymorphic_groups
  @polymorphic_groups
end

#primary_keySymbol (readonly)

The attribute used to match objects - need not be a true database primary key.

Returns:

  • (Symbol)

    the current value of primary_key



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def primary_key
  @primary_key
end

#resource_classClass (readonly)

The corresponding Resource class

Returns:

  • (Class)

    the current value of resource_class



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def resource_class
  @resource_class
end

#scope_procProc (readonly)

The configured 'scope' block

Returns:

  • (Proc)

    the current value of scope_proc



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def scope_proc
  @scope_proc
end

#sideloadsHash (readonly)

The associated sibling sideloads

Returns:

  • (Hash)

    the current value of sideloads



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def sideloads
  @sideloads
end

#typeSymbol (readonly)

One of :has_many, :belongs_to, etc

Returns:

  • (Symbol)

    the current value of type



13
14
15
# File 'lib/jsonapi_compliable/sideload.rb', line 13

def type
  @type
end

Instance Method Details

#allow_sideload(name, opts = {}, &blk) ⇒ Object

Configure a relationship between Resource objects

You probably want to extract this logic into an adapter rather than using directly

Examples:

Default ActiveRecord

# What happens 'under the hood'
class CommentResource < ApplicationResource
  # ... code ...
  allow_sideload :post, resource: PostResource do
    scope do |comments|
      Post.where(id: comments.map(&:post_id))
    end

    assign do |comments, posts|
      comments.each do |comment|
        relevant_post = posts.find { |p| p.id == comment.post_id }
        comment.post = relevant_post
      end
    end
  end
end

# Rather than writing that code directly, go through the adapter:
class CommentResource < ApplicationResource
  # ... code ...
  use_adapter JsonapiCompliable::Adapters::ActiveRecord

  belongs_to :post,
    scope: -> { Post.all },
    resource: PostResource,
    foreign_key: :post_id
end

Returns:

  • void

See Also:



270
271
272
273
274
275
276
277
278
279
# File 'lib/jsonapi_compliable/sideload.rb', line 270

def allow_sideload(name, opts = {}, &blk)
  sideload = Sideload.new(name, opts)
  sideload.instance_eval(&blk) if blk

  if polymorphic?
    @polymorphic_groups[name] = sideload
  else
    @sideloads[name] = sideload
  end
end

#assign {|parents, children| ... } ⇒ Object

The proc used to assign the resolved parents and children.

You probably want to wrap this logic in an Adapter, instead of specifying in your resource directly.

Examples:

Default ActiveRecord

class PostResource < ApplicationResource
  # ... code ...
  allow_sideload :comments, resource: CommentResource do
    # ... code ...
    assign do |posts, comments|
      posts.each do |post|
        relevant_comments = comments.select { |c| c.post_id == post.id }
        post.comments = relevant_comments
      end
    end
  end
end

ActiveRecord via Adapter

class PostResource < ApplicationResource
  # ... code ...
  has_many :comments,
    scope: -> { Comment.all },
    resource: CommentResource,
    foreign_key: :post_id
end

Yield Parameters:

  • parents
    • The resolved parent records

  • children
    • The resolved child records

See Also:



168
169
170
# File 'lib/jsonapi_compliable/sideload.rb', line 168

def assign(&blk)
  @assign_proc = blk
end

#associate(parent, child) ⇒ Object

Configure how to associate parent and child records.

Examples:

Basic attr_accessor

def associate(parent, child)
  if type == :has_many
    parent.send(:#{name}").push(child)
  else
    child.send(:#{name}=", parent)
  end
end

See Also:



185
186
187
188
189
190
191
# File 'lib/jsonapi_compliable/sideload.rb', line 185

def associate(parent, child)
  association_name = @parent ? @parent.name : name
  resource_class.config[:adapter].associate parent,
    child,
    association_name,
    type
end

#group_by(grouping_field) ⇒ Object

Define an attribute that groups the parent records. For instance, with an ActiveRecord polymorphic belongs_to there will be a parent_id and parent_type. We would want to group on parent_type:

allow_sideload :organization, polymorphic: true do
  # group parent_type, parent here is 'organization'
  group_by :organization_type
end

See Also:



203
204
205
# File 'lib/jsonapi_compliable/sideload.rb', line 203

def group_by(grouping_field)
  @grouping_field = grouping_field
end

#polymorphic?Boolean

Is this sideload polymorphic?

Polymorphic sideloads group the parent objects in some fashion, so different 'types' can be resolved differently. Let's say an Office has a polymorphic Organization, which can be either a Business or Government:

allow_sideload :organization, :polymorphic: true do
  group_by :organization_type

  allow_sideload 'Business', resource: BusinessResource do
    # ... code ...
  end

  allow_sideload 'Governemnt', resource: GovernmentResource do
    # ... code ...
  end
end

You probably want to extract this code into an Adapter. For instance, with ActiveRecord:

polymorphic_belongs_to :organization,
  group_by: :organization_type,
  groups: {
    'Business' => {
      scope: -> { Business.all },
      resource: BusinessResource,
      foreign_key: :organization_id
    },
    'Government' => {
      scope: -> { Government.all },
      resource: GovernmentResource,
      foreign_key: :organization_id
    }
  }

Returns:

  • (Boolean)

    is this sideload polymorphic?

See Also:



91
92
93
# File 'lib/jsonapi_compliable/sideload.rb', line 91

def polymorphic?
  @polymorphic == true
end

#polymorphic_child_for_type(type) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



333
334
335
336
337
# File 'lib/jsonapi_compliable/sideload.rb', line 333

def polymorphic_child_for_type(type)
  polymorphic_groups.values.find do |v|
    v.resource_class.config[:type] == type
  end
end

#resolve(parents, query, namespace = nil) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Resolve the sideload.

  • Uses the 'scope' proc to build a 'base scope'

  • Chains additional criteria onto that 'base scope'

  • Resolves that scope (see Scope#resolve)

  • Assigns the resulting child objects to their corresponding parents

Parameters:

  • parents (Object)

    The resolved parent models

  • query (Query)

    The Query instance

  • namespace (Symbol) (defaults to: nil)

    The current namespace (see Resource#with_context)

See Also:



222
223
224
225
226
227
228
229
230
# File 'lib/jsonapi_compliable/sideload.rb', line 222

def resolve(parents, query, namespace = nil)
  namespace ||= name

  if polymorphic?
    resolve_polymorphic(parents, query)
  else
    resolve_basic(parents, query, namespace)
  end
end

#resourceResource

Returns an instance of #resource_class

Returns:

  • (Resource)

    an instance of #resource_class

See Also:



48
49
50
# File 'lib/jsonapi_compliable/sideload.rb', line 48

def resource
  @resource ||= resource_class.new
end

#scope {|parents| ... } ⇒ Object

Build a scope that will be used to fetch the related records This scope will be further chained with filtering/sorting/etc

You probably want to wrap this logic in an Adapter, instead of specifying in your resource directly.

Examples:

Default ActiveRecord

class PostResource < ApplicationResource
  # ... code ...
  allow_sideload :comments, resource: CommentResource do
    scope do |posts|
      Comment.where(post_id: posts.map(&:id))
    end
    # ... code ...
  end
end

Custom Scope

# In this example, our base scope is a Hash
scope do |posts|
  { post_ids: posts.map(&:id) }
end

ActiveRecord via Adapter

class PostResource < ApplicationResource
  # ... code ...
  has_many :comments,
    scope: -> { Comment.all },
    resource: CommentResource,
    foreign_key: :post_id
end

Yield Parameters:

  • parents
    • The resolved parent records

See Also:



131
132
133
# File 'lib/jsonapi_compliable/sideload.rb', line 131

def scope(&blk)
  @scope_proc = blk
end

#sideload(name) ⇒ Object

Fetch a Sideload object by its name

Parameters:

  • name (Symbol)

    The name of the corresponding sideload

Returns:

  • the corresponding Sideload object

See Also:

  • +allow_sideload


285
286
287
# File 'lib/jsonapi_compliable/sideload.rb', line 285

def sideload(name)
  @sideloads[name]
end

#to_hash(processed = []) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Looks at all nested sideload, and all nested sideloads for the corresponding Resources, and returns an Include Directive hash

For instance, this configuration:

class BarResource < ApplicationResource
  allow_sideload :baz do
  end
end

class PostResource < ApplicationResource
  allow_sideload :foo do
    allow_sideload :bar, resource: BarResource do
    end
  end
end

post_resource.sideloading.to_hash would return

{ base: { foo: { bar: { baz: {} } } } }

Returns:

  • (Hash)

    The nested include hash



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/jsonapi_compliable/sideload.rb', line 312

def to_hash(processed = [])
  return { name => {} } if processed.include?(self)
  processed << self

  result = { name => {} }.tap do |hash|
    @sideloads.each_pair do |key, sideload|
      hash[name][key] = sideload.to_hash(processed)[key] || {}

      if sideload.polymorphic?
        sideload.polymorphic_groups.each_pair do |type, sl|
          hash[name][key].merge!(nested_sideload_hash(sl, processed))
        end
      else
        hash[name][key].merge!(nested_sideload_hash(sideload, processed))
      end
    end
  end
  result
end