Skip to content

Commit

Permalink
Merge pull request #30 from avvo/associations
Browse files Browse the repository at this point in the history
Associations
  • Loading branch information
gaorlov committed Jul 21, 2016
2 parents e342cc6 + c3e5860 commit 1f9cd51
Show file tree
Hide file tree
Showing 28 changed files with 1,168 additions and 6 deletions.
3 changes: 0 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,3 @@ DEPENDENCIES
rake
simplecov
webmock (> 0)

BUNDLED WITH
1.11.2
2 changes: 2 additions & 0 deletions lib/json_api_resource.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require 'json_api_client'

module JsonApiResource
autoload :Associatable, 'json_api_resource/associatable'
autoload :Associations, 'json_api_resource/associations'
autoload :Cacheable, 'json_api_resource/cacheable'
autoload :Clientable, 'json_api_resource/clientable'
autoload :Connections, 'json_api_resource/connections'
Expand Down
60 changes: 60 additions & 0 deletions lib/json_api_resource/associatable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
module JsonApiResource
module Associatable
extend ActiveSupport::Concern

included do

class_attribute :_associations
self._associations = {}

attr_accessor :_cached_associations

class << self
def belongs_to( name, opts = {} )
process Associations::BelongsTo.new( self, name, opts )
end

def has_one( name, opts = {} )
process Associations::HasOne.new( self, name, opts )
end

def has_many( name, opts = {} )
if opts[:prefetched_ids]
process Associations::HasManyPrefetched.new( self, name, opts )
else
process Associations::HasMany.new( self, name, opts )
end
end

private

def process(association)
add_association association
methodize association
end

def add_association(association)
self._associations = _associations.merge association.name => association
end

def methodize( association )
define_method association.name do
self._cached_associations ||= {}
unless self._cached_associations.has_key? association.name
if association.callable?(self)
result = association.klass.send( association.action, association.query(self) )
result = association.post_process result
else
result = association.nil_default
end

self._cached_associations[association.name] = result
end
self._cached_associations[association.name]
end
end
end
end
end
end

17 changes: 17 additions & 0 deletions lib/json_api_resource/associations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module JsonApiResource
module Associations
autoload :Base, 'json_api_resource/associations/base'
autoload :BelongsTo, 'json_api_resource/associations/belongs_to'
autoload :HasManyPrefetched, 'json_api_resource/associations/has_many_prefetched'
autoload :HasOne, 'json_api_resource/associations/has_one'
autoload :HasMany, 'json_api_resource/associations/has_many'
autoload :Preloader, 'json_api_resource/associations/preloader'
autoload :Preloaders, 'json_api_resource/associations/preloaders'


BELONGS_TO = :belongs_to
HAS_ONE = :has_one
HAS_MANY = :has_many
HAS_MANY_PREFETCHED = :has_many_prefetched
end
end
79 changes: 79 additions & 0 deletions lib/json_api_resource/associations/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
module JsonApiResource
module Associations
class Base
include ::JsonApiResource::Errors
attr_accessor :name, :action, :key, :root

def initialize(associated_class, name, opts = {})
self.name = name.to_sym
self.root = associated_class
@opts = opts.merge( skip_pagination: true )

self.action = @opts.delete :action do default_action end
self.key = @opts.delete :foreign_key do server_key end

self.key = self.key.try :to_sym
validate_options
end

def type
raise NotImplementedError
end

def query( root_insatnce )
raise NotImplementedError
end

def callable?( root_insatnce )
raise NotImplementedError
end

def default_nil
raise NotImplementedError
end

# klass has to be lazy initted for circular dependencies
def klass
@klass ||= @opts.delete :class do derived_class end
end

def post_process( value )
value
end

def opts
@opts.except *ASSOCIATION_OPTS
end

protected

def server_key
raise NotImplementedError
end

def default_action
raise NotImplementedError
end

def derived_class
module_string = self.root.to_s.split("::")[0 ... -1].join("::")
class_string = name.to_s.singularize.camelize

# we don't necessarily want to add :: to classes, in case they have a relative path or something
class_string = [module_string, class_string].select{|s| s.present? }.join "::"
class_string.constantize
end

ASSOCIATION_OPTS = [:class, :action, :foreign_key, :prefetched_ids]

RESERVED_KEYWORDS = [:attributes, :_associations, :_cached_associations, :schema, :client]

def validate_options
raise_unless action.present?, "Invalid action: #{self.root}.#{name}"
raise_unless key.present?, "Invalid foreign_key for #{self.root}.#{name}"

raise_if RESERVED_KEYWORDS.include?(name), "'#{name}' is a reserved keyword for #{self.root}"
end
end
end
end
29 changes: 29 additions & 0 deletions lib/json_api_resource/associations/belongs_to.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module JsonApiResource
module Associations
class BelongsTo < Base
def default_action
:find
end

def server_key
"#{name}_id"
end

def query( root_instance )
root_instance.send key
end

def callable?( root_instance )
root_instance.send(key).present?
end

def nil_default
nil
end

def type
JsonApiResource::Associations::BELONGS_TO
end
end
end
end
26 changes: 26 additions & 0 deletions lib/json_api_resource/associations/has_many.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module JsonApiResource
module Associations
class HasMany < Base
def default_action
:where
end

def server_key
class_name = self.root.to_s.demodulize.underscore
"#{class_name}_id"
end

def callable?( root_instance )
true
end

def query( root_instance )
{ key => root_instance.id }.merge(opts)
end

def type
JsonApiResource::Associations::HAS_MANY
end
end
end
end
34 changes: 34 additions & 0 deletions lib/json_api_resource/associations/has_many_prefetched.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module JsonApiResource
module Associations
class HasManyPrefetched < Base
def default_action
:where
end

def server_key
@opts[:prefetched_ids]
end

def query( root_instance )
{ id: root_instance.send(key) }.merge(opts)
end

def callable?( root_instance )
root_instance.send(key).present?
end

def nil_default
[]
end

def type
JsonApiResource::Associations::HAS_MANY_PREFETCHED
end

def validate_options
raise_unless key == server_key, "#{root}.#{name} cannot specify both prefetched_ids and a foreign key"
super
end
end
end
end
30 changes: 30 additions & 0 deletions lib/json_api_resource/associations/has_one.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module JsonApiResource
module Associations
class HasOne < Base
def post_process( value )
Array(value).first
end

def default_action
:where
end

def server_key
class_name = self.root.to_s.demodulize.underscore
"#{class_name}_id"
end

def query( root_instance )
{ key => root_instance.id }.merge(opts)
end

def callable?( root_instance )
true
end

def type
JsonApiResource::Associations::HAS_ONE
end
end
end
end
58 changes: 58 additions & 0 deletions lib/json_api_resource/associations/preloader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module JsonApiResource
module Associations
class Preloader
include JsonApiResource::Errors

class << self
def preload ( objects, preloads )
objects = Array(objects)
preloads = Array(preloads)

preloads.each do |preload|

association = association_for objects, preload

preloader = preloader_for association

preloader.preload( objects )

end
end

private

def association_for( objects, preload )
# let's assert the objects are of a single class
verify_object_homogenity!(objects)

obj_class = objects.first.class
association = obj_class._associations[preload]

raise_if association.nil?, "'#{preload}' is not a valid association on #{obj_class}"

association
end

def preloader_for( association )
preloader_class = PREOLOADERS_FOR_ASSOCIATIONS[association.type]
preloader_class.new association
end

def verify_object_homogenity!( objects )
obj_class = objects.first.class

objects.each do |obj|
raise_unless obj.is_a?(obj_class), "JsonApiResource::Associations::Preloader.preload called with a heterogenious array of objects."
end
end

PREOLOADERS_FOR_ASSOCIATIONS = {
JsonApiResource::Associations::BELONGS_TO => JsonApiResource::Associations::Preloaders::BelongsToPreloader,
JsonApiResource::Associations::HAS_ONE => JsonApiResource::Associations::Preloaders::HasOnePreloader,
JsonApiResource::Associations::HAS_MANY => JsonApiResource::Associations::Preloaders::HasManyPreloader,
JsonApiResource::Associations::HAS_MANY_PREFETCHED => JsonApiResource::Associations::Preloaders::HasManyPrefetchedPreloader,
}
end
end
end
end
12 changes: 12 additions & 0 deletions lib/json_api_resource/associations/preloaders.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module JsonApiResource
module Associations
module Preloaders
autoload :Base, 'json_api_resource/associations/preloaders/base'
autoload :BelongsToPreloader, 'json_api_resource/associations/preloaders/belongs_to_preloader'
autoload :Distributors, 'json_api_resource/associations/preloaders/distributors'
autoload :HasManyPrefetchedPreloader, 'json_api_resource/associations/preloaders/has_many_prefetched_preloader'
autoload :HasManyPreloader, 'json_api_resource/associations/preloaders/has_many_preloader'
autoload :HasOnePreloader, 'json_api_resource/associations/preloaders/has_one_preloader'
end
end
end
Loading

0 comments on commit 1f9cd51

Please sign in to comment.