morpheus 0.4.0 → 0.5.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.
- 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
|