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.
Files changed (63) hide show
  1. data/.rspec +1 -1
  2. data/CHANGELOG.md +17 -0
  3. data/Gemfile +1 -1
  4. data/lib/morpheus.rb +3 -23
  5. data/lib/morpheus/base.rb +11 -11
  6. data/lib/morpheus/client.rb +10 -0
  7. data/lib/morpheus/client/associations.rb +6 -6
  8. data/lib/morpheus/client/cached_request_formatter.rb +20 -0
  9. data/lib/morpheus/client/log_subscriber.rb +2 -28
  10. data/lib/morpheus/client/railtie.rb +4 -4
  11. data/lib/morpheus/client/request_formatter.rb +50 -0
  12. data/lib/morpheus/client/uncached_request_formatter.rb +40 -0
  13. data/lib/morpheus/configuration.rb +30 -9
  14. data/lib/morpheus/mixins.rb +16 -0
  15. data/lib/morpheus/mixins/associations.rb +39 -37
  16. data/lib/morpheus/mixins/associations/association.rb +112 -0
  17. data/lib/morpheus/mixins/associations/belongs_to_association.rb +47 -0
  18. data/lib/morpheus/mixins/associations/has_many_association.rb +72 -0
  19. data/lib/morpheus/mixins/associations/has_one_association.rb +48 -0
  20. data/lib/morpheus/mixins/attributes.rb +97 -95
  21. data/lib/morpheus/mixins/conversion.rb +16 -14
  22. data/lib/morpheus/mixins/filtering.rb +13 -11
  23. data/lib/morpheus/mixins/finders.rb +47 -45
  24. data/lib/morpheus/mixins/introspection.rb +15 -13
  25. data/lib/morpheus/mixins/persistence.rb +32 -30
  26. data/lib/morpheus/mixins/reflections.rb +17 -15
  27. data/lib/morpheus/mixins/request_handling.rb +27 -25
  28. data/lib/morpheus/mixins/response_parsing.rb +10 -8
  29. data/lib/morpheus/mixins/url_support.rb +27 -25
  30. data/lib/morpheus/reflection.rb +5 -2
  31. data/lib/morpheus/type_caster.rb +1 -1
  32. data/lib/morpheus/version.rb +1 -1
  33. data/morpheus.gemspec +2 -2
  34. data/spec/dummy/app/resources/book.rb +1 -1
  35. data/spec/morpheus/base_spec.rb +35 -35
  36. data/spec/morpheus/client/cached_request_formatter_spec.rb +28 -0
  37. data/spec/morpheus/client/log_subscriber_spec.rb +53 -10
  38. data/spec/morpheus/client/request_formatter_spec.rb +5 -0
  39. data/spec/morpheus/client/uncached_request_formatter_spec.rb +29 -0
  40. data/spec/morpheus/configuration_spec.rb +49 -11
  41. data/spec/morpheus/{associations → mixins/associations}/association_spec.rb +1 -1
  42. data/spec/morpheus/{associations → mixins/associations}/belongs_to_association_spec.rb +1 -1
  43. data/spec/morpheus/{associations → mixins/associations}/has_many_association_spec.rb +1 -1
  44. data/spec/morpheus/{associations → mixins/associations}/has_one_association_spec.rb +1 -1
  45. data/spec/morpheus/mixins/associations_spec.rb +1 -1
  46. data/spec/morpheus/mixins/attributes_spec.rb +27 -6
  47. data/spec/morpheus/mixins/conversion_spec.rb +1 -1
  48. data/spec/morpheus/mixins/filtering_spec.rb +2 -2
  49. data/spec/morpheus/mixins/finders_spec.rb +1 -1
  50. data/spec/morpheus/mixins/introspection_spec.rb +1 -1
  51. data/spec/morpheus/mixins/persistence_spec.rb +1 -1
  52. data/spec/morpheus/mixins/reflections_spec.rb +1 -1
  53. data/spec/morpheus/mixins/request_handling_spec.rb +2 -2
  54. data/spec/morpheus/mixins/response_parsing_spec.rb +2 -2
  55. data/spec/morpheus/mixins/url_support_spec.rb +2 -2
  56. data/spec/morpheus/response_parser_spec.rb +5 -1
  57. data/spec/regressions/sorting_resources_spec.rb +119 -0
  58. data/spec/spec_helper.rb +3 -2
  59. metadata +159 -87
  60. data/lib/morpheus/associations/association.rb +0 -110
  61. data/lib/morpheus/associations/belongs_to_association.rb +0 -45
  62. data/lib/morpheus/associations/has_many_association.rb +0 -70
  63. 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