morpheus 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -1
- data/CHANGELOG.md +17 -0
- data/Gemfile +1 -1
- data/lib/morpheus.rb +3 -23
- data/lib/morpheus/base.rb +11 -11
- data/lib/morpheus/client.rb +10 -0
- data/lib/morpheus/client/associations.rb +6 -6
- data/lib/morpheus/client/cached_request_formatter.rb +20 -0
- data/lib/morpheus/client/log_subscriber.rb +2 -28
- data/lib/morpheus/client/railtie.rb +4 -4
- data/lib/morpheus/client/request_formatter.rb +50 -0
- data/lib/morpheus/client/uncached_request_formatter.rb +40 -0
- data/lib/morpheus/configuration.rb +30 -9
- data/lib/morpheus/mixins.rb +16 -0
- data/lib/morpheus/mixins/associations.rb +39 -37
- data/lib/morpheus/mixins/associations/association.rb +112 -0
- data/lib/morpheus/mixins/associations/belongs_to_association.rb +47 -0
- data/lib/morpheus/mixins/associations/has_many_association.rb +72 -0
- data/lib/morpheus/mixins/associations/has_one_association.rb +48 -0
- data/lib/morpheus/mixins/attributes.rb +97 -95
- data/lib/morpheus/mixins/conversion.rb +16 -14
- data/lib/morpheus/mixins/filtering.rb +13 -11
- data/lib/morpheus/mixins/finders.rb +47 -45
- data/lib/morpheus/mixins/introspection.rb +15 -13
- data/lib/morpheus/mixins/persistence.rb +32 -30
- data/lib/morpheus/mixins/reflections.rb +17 -15
- data/lib/morpheus/mixins/request_handling.rb +27 -25
- data/lib/morpheus/mixins/response_parsing.rb +10 -8
- data/lib/morpheus/mixins/url_support.rb +27 -25
- data/lib/morpheus/reflection.rb +5 -2
- data/lib/morpheus/type_caster.rb +1 -1
- data/lib/morpheus/version.rb +1 -1
- data/morpheus.gemspec +2 -2
- data/spec/dummy/app/resources/book.rb +1 -1
- data/spec/morpheus/base_spec.rb +35 -35
- data/spec/morpheus/client/cached_request_formatter_spec.rb +28 -0
- data/spec/morpheus/client/log_subscriber_spec.rb +53 -10
- data/spec/morpheus/client/request_formatter_spec.rb +5 -0
- data/spec/morpheus/client/uncached_request_formatter_spec.rb +29 -0
- data/spec/morpheus/configuration_spec.rb +49 -11
- data/spec/morpheus/{associations → mixins/associations}/association_spec.rb +1 -1
- data/spec/morpheus/{associations → mixins/associations}/belongs_to_association_spec.rb +1 -1
- data/spec/morpheus/{associations → mixins/associations}/has_many_association_spec.rb +1 -1
- data/spec/morpheus/{associations → mixins/associations}/has_one_association_spec.rb +1 -1
- data/spec/morpheus/mixins/associations_spec.rb +1 -1
- data/spec/morpheus/mixins/attributes_spec.rb +27 -6
- data/spec/morpheus/mixins/conversion_spec.rb +1 -1
- data/spec/morpheus/mixins/filtering_spec.rb +2 -2
- data/spec/morpheus/mixins/finders_spec.rb +1 -1
- data/spec/morpheus/mixins/introspection_spec.rb +1 -1
- data/spec/morpheus/mixins/persistence_spec.rb +1 -1
- data/spec/morpheus/mixins/reflections_spec.rb +1 -1
- data/spec/morpheus/mixins/request_handling_spec.rb +2 -2
- data/spec/morpheus/mixins/response_parsing_spec.rb +2 -2
- data/spec/morpheus/mixins/url_support_spec.rb +2 -2
- data/spec/morpheus/response_parser_spec.rb +5 -1
- data/spec/regressions/sorting_resources_spec.rb +119 -0
- data/spec/spec_helper.rb +3 -2
- metadata +159 -87
- data/lib/morpheus/associations/association.rb +0 -110
- data/lib/morpheus/associations/belongs_to_association.rb +0 -45
- data/lib/morpheus/associations/has_many_association.rb +0 -70
- data/lib/morpheus/associations/has_one_association.rb +0 -46
@@ -1,110 +0,0 @@
|
|
1
|
-
# The Association class serves as the basis for associating resources.
|
2
|
-
# Resources have an association DSL that follows that of ActiveRecord:
|
3
|
-
#
|
4
|
-
# has_many :automobiles
|
5
|
-
#
|
6
|
-
# has_one :car
|
7
|
-
#
|
8
|
-
# belongs_to :car_club
|
9
|
-
#
|
10
|
-
# This class acts as a proxy that will allow for lazy evaluation of an
|
11
|
-
# association. The proxy waits to make the request for the object until
|
12
|
-
# a method on the association is called. When this happens, because the
|
13
|
-
# method does not exist on the proxy object, method_missing will be
|
14
|
-
# called, the target object will be loaded, and then the method will be
|
15
|
-
# called on the loaded target.
|
16
|
-
module Morpheus
|
17
|
-
module Associations
|
18
|
-
class Association < ActiveSupport::BasicObject
|
19
|
-
|
20
|
-
# Associations can be loaded with several options.
|
21
|
-
def initialize(owner, association, settings = {})
|
22
|
-
# @owner stores the class that the association exists on.
|
23
|
-
@owner = owner
|
24
|
-
|
25
|
-
# @association stores the associated class, as named in the association
|
26
|
-
# method (i.e. :automobile, :car, :car_club)
|
27
|
-
@association = association
|
28
|
-
|
29
|
-
# @target stores the loaded object. It is not typically accessed directly,
|
30
|
-
# but instead should be accessed through the loaded_target method.
|
31
|
-
@target = settings[:target]
|
32
|
-
|
33
|
-
@filters = settings[:filters] || []
|
34
|
-
|
35
|
-
@includes = []
|
36
|
-
|
37
|
-
# @options holds the chosen options for the association. Several of these
|
38
|
-
# options are set in the subclass' initializer.
|
39
|
-
@options = settings[:options] || {}
|
40
|
-
|
41
|
-
# In some cases, the association name will not match that of the class
|
42
|
-
# that should be instantiated when it is invoked. Here, we can specify
|
43
|
-
# that this association uses a specified class as its target. When the
|
44
|
-
# request is made for the association, this class will be used to
|
45
|
-
# instantiate this object or collection.
|
46
|
-
@options[:class_name] = settings[:options][:class_name] || @association.to_s.classify
|
47
|
-
end
|
48
|
-
|
49
|
-
# The proxy implements a few methods that need to be delegated to the target
|
50
|
-
# so that they will work as expected.
|
51
|
-
def id
|
52
|
-
loaded_target.id
|
53
|
-
end
|
54
|
-
|
55
|
-
def nil?
|
56
|
-
loaded_target.nil?
|
57
|
-
end
|
58
|
-
|
59
|
-
def to_param
|
60
|
-
loaded_target.to_param
|
61
|
-
end
|
62
|
-
|
63
|
-
def try(method, *args, &block)
|
64
|
-
loaded_target.try(method, *args, &block)
|
65
|
-
end
|
66
|
-
|
67
|
-
def includes(*associations)
|
68
|
-
associations.each do |association|
|
69
|
-
@includes << association unless @includes.include?(association)
|
70
|
-
end
|
71
|
-
self
|
72
|
-
end
|
73
|
-
|
74
|
-
# This is left to be implemented by the subclasses as it will operate
|
75
|
-
# differently in each case.
|
76
|
-
def load_target!
|
77
|
-
end
|
78
|
-
|
79
|
-
private
|
80
|
-
|
81
|
-
# The loaded_target method holds a cached version of the loaded target.
|
82
|
-
# This method is used to access the proxied object. Since a request will
|
83
|
-
# be made when a method is invoked on the object, and this can happen
|
84
|
-
# very often, we are caching the target here, so that only a single
|
85
|
-
# request will be made.
|
86
|
-
def loaded_target
|
87
|
-
@target ||= load_target!
|
88
|
-
if ::Array === @target && !@filters.empty?
|
89
|
-
@filters.uniq.inject(@target.dup) do |target, filter|
|
90
|
-
filter.call(target)
|
91
|
-
end
|
92
|
-
else
|
93
|
-
@target
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
# The method_missing hook will be called when methods that do not exist
|
98
|
-
# on the proxy object are invoked. This is the point at which the proxied
|
99
|
-
# object is loaded, if it has not been loaded already.
|
100
|
-
def method_missing(m, *args, &block)
|
101
|
-
if filter = @association_class.find_filter(m)
|
102
|
-
with_filter(filter)
|
103
|
-
else
|
104
|
-
loaded_target.send(m, *args, &block)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
@@ -1,45 +0,0 @@
|
|
1
|
-
module Morpheus
|
2
|
-
module Associations
|
3
|
-
class BelongsToAssociation < Association
|
4
|
-
|
5
|
-
# The initializer calls out to the superclass' initializer, then will set the
|
6
|
-
# options particular to itself.
|
7
|
-
def initialize(owner, association, settings = {})
|
8
|
-
super
|
9
|
-
|
10
|
-
# The foreign key is used to generate the url for a request. The default uses
|
11
|
-
# the association name with an '_id' suffix as the generated key. For example,
|
12
|
-
# belongs_to :school, will have the foreign key :school_id.
|
13
|
-
@options[:foreign_key] ||= "#{@association.to_s}_id".to_sym
|
14
|
-
|
15
|
-
# The primary key defaults to :id.
|
16
|
-
@options[:primary_key] ||= :id
|
17
|
-
|
18
|
-
# Associations can be marked as polymorphic. These associations will use
|
19
|
-
# the returned type to instantiate the associated object.
|
20
|
-
@options[:polymorphic] = settings[:options][:polymorphic] || false
|
21
|
-
|
22
|
-
# @association_class stores the class of the association, constantized
|
23
|
-
# from the named association (i.e. Automobile, Car, CarClub)
|
24
|
-
@association_class = @options[:class_name].constantize
|
25
|
-
end
|
26
|
-
|
27
|
-
def with_filter(filter)
|
28
|
-
BelongsToAssociation.new(@owner, @association, :target => @target, :filters => @filters.dup.push(filter), :options => @options)
|
29
|
-
end
|
30
|
-
|
31
|
-
# When loading the target, the association will only be loaded if the foreign_key
|
32
|
-
# has been set. Additionally, the class used to find the record will be inferred
|
33
|
-
# by calling the method which is the name of the association with a '_type' suffix.
|
34
|
-
# Alternatively, the class name can be set by using the :class_name option.
|
35
|
-
def load_target!
|
36
|
-
if association_id = @owner.send(@options[:foreign_key])
|
37
|
-
polymorphic_class = @options[:polymorphic] ? @owner.send("#{@association}_type".to_sym).constantize : @options[:class_name].constantize
|
38
|
-
attributes = [UrlBuilder.belongs_to(polymorphic_class, association_id), nil, { :id => association_id }]
|
39
|
-
polymorphic_class.find(association_id)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
@@ -1,70 +0,0 @@
|
|
1
|
-
module Morpheus
|
2
|
-
module Associations
|
3
|
-
class HasManyAssociation < Association
|
4
|
-
|
5
|
-
# The initializer calls out to the superclass' initializer and then
|
6
|
-
# sets the options particular to itself.
|
7
|
-
def initialize(owner, association, settings = {})
|
8
|
-
super
|
9
|
-
|
10
|
-
# The foreign key is used to generate the url for the association
|
11
|
-
# request when the association is transformed into a relation.
|
12
|
-
# The default is to use the class of the owner object with an '_id'
|
13
|
-
# suffix.
|
14
|
-
@options[:foreign_key] ||= "#{@owner.class.to_s.underscore}_id".to_sym
|
15
|
-
|
16
|
-
# The primary key is used in the generated url for the target. It
|
17
|
-
# defaults to :id.
|
18
|
-
@options[:primary_key] ||= :id
|
19
|
-
|
20
|
-
# @association_class stores the class of the association, constantized
|
21
|
-
# from the named association (i.e. Automobile, Car, CarClub)
|
22
|
-
@association_class = @options[:class_name].constantize
|
23
|
-
end
|
24
|
-
|
25
|
-
def with_filter(filter)
|
26
|
-
HasManyAssociation.new(@owner, @association, :target => @target, :filters => @filters.dup.push(filter), :options => @options)
|
27
|
-
end
|
28
|
-
|
29
|
-
# When loading the target, the primary key is first checked. If the
|
30
|
-
# key is nil, then an empty array is returned. Otherwise, the target
|
31
|
-
# is requested at the generated url. For a has_many :meetings
|
32
|
-
# association on a class called Course, the generated url might look
|
33
|
-
# like this: /meetings?course_id=1, where the 1 is the primary key.
|
34
|
-
def load_target!
|
35
|
-
if primary_key = @owner.send(@options[:primary_key])
|
36
|
-
Relation.new(@association.to_s.classify.constantize).where(@options[:foreign_key] => primary_key).all
|
37
|
-
else
|
38
|
-
[]
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
# The where, limit, and page methods delegate to a Relation object.
|
43
|
-
# The association generates a relation, and then calls the very
|
44
|
-
# same where, limit, or page method on that relation object.
|
45
|
-
def where(query_attributes)
|
46
|
-
transform_association_into_relation.where(query_attributes)
|
47
|
-
end
|
48
|
-
|
49
|
-
def limit(amount)
|
50
|
-
transform_association_into_relation.limit(amount)
|
51
|
-
end
|
52
|
-
|
53
|
-
def page(page_number)
|
54
|
-
transform_association_into_relation.page(page_number)
|
55
|
-
end
|
56
|
-
|
57
|
-
def transform_association_into_relation
|
58
|
-
Relation.new(@association.to_s.classify.constantize).where(@options[:foreign_key] => @owner.send(@options[:primary_key]))
|
59
|
-
end
|
60
|
-
private :transform_association_into_relation
|
61
|
-
|
62
|
-
# The append operator is used to append new resources to the association.
|
63
|
-
def <<(one_of_many)
|
64
|
-
@target ||= []
|
65
|
-
@target << one_of_many
|
66
|
-
end
|
67
|
-
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
@@ -1,46 +0,0 @@
|
|
1
|
-
module Morpheus
|
2
|
-
module Associations
|
3
|
-
class HasOneAssociation < Association
|
4
|
-
|
5
|
-
# The initializer calls out to the superclass' initializer and then
|
6
|
-
# sets the options particular to itself.
|
7
|
-
def initialize(owner, association, settings = {})
|
8
|
-
super
|
9
|
-
|
10
|
-
# The foreign key is used to generate the url for the association
|
11
|
-
# request when the association is transformed into a relation.
|
12
|
-
# The default is to use the class of the owner object with an '_id'
|
13
|
-
# suffix.
|
14
|
-
@options[:foreign_key] ||= "#{@owner.class.to_s.underscore}_id".to_sym
|
15
|
-
|
16
|
-
# The primary key is used in the generated url for the target. It
|
17
|
-
# defaults to :id.
|
18
|
-
@options[:primary_key] ||= :id
|
19
|
-
|
20
|
-
# @association_class stores the class of the association, constantized
|
21
|
-
# from the named association (i.e. Automobile, Car, CarClub)
|
22
|
-
if @options[:class_name]
|
23
|
-
@association_class = @options[:class_name].constantize
|
24
|
-
else
|
25
|
-
@association_class = @association
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def with_filter(filter)
|
30
|
-
HasOneAssociation.new(@owner, @association, :target => @target, :filters => @filters.dup.push(filter), :options => @options)
|
31
|
-
end
|
32
|
-
|
33
|
-
# When loading the target, the primary key is first checked. If the
|
34
|
-
# key is nil, then an nil is returned. Otherwise, the target
|
35
|
-
# is requested at the generated url. For a has_one :meeting
|
36
|
-
# association on a class called Course, the generated url might look
|
37
|
-
# like this: /meetings?course_id=1, where the 1 is the primary key.
|
38
|
-
def load_target!
|
39
|
-
if primary_key = @owner.send(@options[:primary_key])
|
40
|
-
Relation.new(@association_class.to_s.classify.constantize).where(@options[:foreign_key] => primary_key).limit(1).first
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|