reactive_resource 0.0.1

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/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ doc/*
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in reactive_resource.gemspec
4
+ gemspec
5
+ gem 'rake'
data/Gemfile.lock ADDED
@@ -0,0 +1,29 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ reactive_resource (0.0.1)
5
+ activeresource (~> 2.3.10)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ activeresource (2.3.10)
11
+ activesupport (= 2.3.10)
12
+ activesupport (2.3.10)
13
+ addressable (2.2.2)
14
+ crack (0.1.8)
15
+ rake (0.8.7)
16
+ shoulda (2.11.3)
17
+ webmock (1.6.2)
18
+ addressable (>= 2.2.2)
19
+ crack (>= 0.1.7)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ activeresource (~> 2.3.10)
26
+ rake
27
+ reactive_resource!
28
+ shoulda (~> 2.11.3)
29
+ webmock (~> 1.6.1)
data/README.rdoc ADDED
@@ -0,0 +1,72 @@
1
+ = Reactive Resource
2
+
3
+ Reactive Resource is a collection of extensions extracted from
4
+ ActiveResource wrappers around various APIs.
5
+
6
+ == Usage
7
+
8
+ After installing the gem, your ActiveResource models should inherit
9
+ from <tt>ReactiveResource::Base</tt> instead of
10
+ <tt>ActiveResource::Base</tt>.
11
+
12
+ == Associations
13
+
14
+ The most useful thing Reactive Resource adds to ActiveResource is read
15
+ support for associations. This allows you to specify relationships between objects:
16
+
17
+ class ReactiveResource::Lawyer < ReactiveResource::Base
18
+ has_many :addresses
19
+ end
20
+
21
+ class ReactiveResource::Address < ReactiveResource::Base
22
+ belongs_to :lawyer
23
+ has_many :phones
24
+ end
25
+
26
+ class ReactiveResource::Phone < ReactiveResource::Base
27
+ belongs_to :address
28
+ end
29
+
30
+ and allows you to make calls like:
31
+
32
+ ReactiveResource::Lawyer.find(1).addresses.first.phones
33
+
34
+ This also takes care of URL generation for the associated objects, so
35
+ the above command will hit <tt>/lawyers/1.json</tt>, then
36
+ <tt>/lawyers/1/addresses.json</tt>, then
37
+ <tt>/lawyers/1/addresses/:id/phones.json</tt>.
38
+
39
+ Currently, Reactive Resource only supports 'read-style'
40
+ associations. It does not yet support things like +build_association+
41
+ and <tt>association=</tt>.
42
+
43
+ == Other additions
44
+
45
+ === Nested routes
46
+
47
+ One thing ActiveResource was lacking was good support for generating
48
+ nested paths for child resources. Reactive Resource uses the
49
+ +belongs_to+ declarations to generate paths like
50
+ <tt>/lawyers/1/addresses.json</tt>, without having to specify all the paths
51
+ in each class.
52
+
53
+ === Support for singleton resources
54
+
55
+ For singleton resources, ActiveResource would still use the plural and
56
+ generate paths like "/lawyers/1/headshots.json". I didn't like this, so you
57
+ can now mark a resource as a singleton resource:
58
+
59
+ class ReactiveResource::Headshot < ReactiveResource::Base
60
+ singleton
61
+ end
62
+
63
+ and the paths will be generated correctly. This is based on the patch
64
+ at https://rails.lighthouseapp.com/projects/8994/tickets/4348-supporting-singleton-resources-in-activeresource
65
+
66
+ === Support for setting URL options after creation
67
+
68
+ In ActiveResource, if you had nested URLs specified in the resource
69
+ path in your class definition, you had to set those params at the
70
+ object's creation time. Reactive Resource allows you to set those
71
+ parameters at any time after object creation.
72
+
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'bundler'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ task :default => :test
7
+ task :build => :test
8
+
9
+ Rake::TestTask.new do |t|
10
+ t.libs << "test"
11
+ t.test_files = FileList['test/**/*_test.rb']
12
+ t.verbose = true
13
+ end
14
+
15
+ Rake::RDocTask.new do |rd|
16
+ rd.main = "README.rdoc"
17
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
18
+ rd.rdoc_dir = 'doc'
19
+ end
@@ -0,0 +1,96 @@
1
+ module ReactiveResource
2
+ module Association
3
+ # Represents and resolves a belongs_to association.
4
+ class BelongsToAssociation
5
+
6
+ # The class this association is attached to
7
+ attr_reader :klass
8
+
9
+ # The attribute name this association represents
10
+ attr_reader :attribute
11
+
12
+ # additional options passed in when the association was created
13
+ attr_reader :options
14
+
15
+ # Returns the class name of the target of the association. Based
16
+ # off of +attribute+ unless +class_name+ was passed in the
17
+ # +options+ hash.
18
+ def associated_class
19
+ if options[:class_name]
20
+ options[:class_name].constantize
21
+ else
22
+ klass.relative_const_get(attribute.to_s.camelize)
23
+ end
24
+ end
25
+
26
+ # A flattened list of attributes from the entire association
27
+ # +belongs_to+ hierarchy, including this association's attribute.
28
+ def associated_attributes
29
+ attributes = [attribute]
30
+ if associated_class
31
+ attributes += associated_class.belongs_to_associations.map(&:attribute)
32
+ end
33
+ attributes.uniq
34
+ end
35
+
36
+ # Called when this assocation is referenced. Finds and returns
37
+ # the target of this association.
38
+ def resolve_relationship(object)
39
+ parent_params = object.prefix_options.dup
40
+ parent_params.delete("#{attribute}_id".intern)
41
+ associated_class.find(object.send("#{attribute}_id"), :params => parent_params)
42
+ end
43
+
44
+ # Adds methods for belongs_to associations, to make dealing with
45
+ # these objects a bit more straightforward. If the attribute name
46
+ # is +lawyer+, it will add:
47
+ #
48
+ # [lawyer] returns the actual lawyer object (after doing a web request)
49
+ # [lawyer_id] returns the lawyer id
50
+ # [lawyer_id=] sets the lawyer id
51
+ def add_helper_methods(klass, attribute)
52
+ association = self
53
+
54
+ klass.class_eval do
55
+ # address.lawyer_id
56
+ define_method("#{attribute}_id") do
57
+ prefix_options["#{attribute}_id".intern]
58
+ end
59
+
60
+ # address.lawyer_id = 3
61
+ define_method("#{attribute}_id=") do |value|
62
+ prefix_options["#{attribute}_id".intern] = value
63
+ end
64
+
65
+ # address.lawyer
66
+ define_method(attribute) do
67
+ # if the parent has its own belongs_to associations, we need
68
+ # to add those to the 'find' call. So, let's grab all of
69
+ # these associations, turn them into a hash of :attr_name =>
70
+ # attr_id, and fire off the find.
71
+
72
+ unless instance_variable_get("@#{attribute}")
73
+ object = association.resolve_relationship(self)
74
+ instance_variable_set("@#{attribute}", object)
75
+ end
76
+ instance_variable_get("@#{attribute}")
77
+ end
78
+ end
79
+
80
+ # Recurse through the parent object.
81
+ associated_class.belongs_to_associations.each do |parent_attribute|
82
+ parent_attribute.add_helper_methods(klass, parent_attribute.attribute)
83
+ end
84
+ end
85
+
86
+ # Create a new belongs_to association.
87
+ def initialize(klass, attribute, options)
88
+ @klass = klass
89
+ @attribute = attribute
90
+ @options = options
91
+
92
+ add_helper_methods(klass, attribute)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,62 @@
1
+ module ReactiveResource
2
+ module Association
3
+ # Represents and resolves a has_many association.
4
+ class HasManyAssociation
5
+
6
+ # The class this association is attached to
7
+ attr_reader :klass
8
+
9
+ # The attribute name this association represents
10
+ attr_reader :attribute
11
+
12
+ # additional options passed in when the association was created
13
+ attr_reader :options
14
+
15
+ # Returns the class name of the target of the association. Based
16
+ # off of +attribute+ unless +class_name+ was passed in the
17
+ # +options+ hash.
18
+ def associated_class
19
+ if options[:class_name]
20
+ options[:class_name].constantize
21
+ else
22
+ klass.relative_const_get(attribute.to_s.singularize.camelize)
23
+ end
24
+ end
25
+
26
+ # Called when this assocation is referenced. Finds and returns
27
+ # the targets of this association.
28
+ def resolve_relationship(object)
29
+ id_attribute = "#{klass.name.split("::").last.underscore}_id"
30
+ associated_class.find(:all, :params => object.prefix_options.merge(id_attribute => object.id))
31
+ end
32
+
33
+ # Adds methods for has_many associations, to make dealing with
34
+ # these objects a bit more straightforward. If the attribute name
35
+ # is +lawyers+, it will add:
36
+ #
37
+ # [lawyers] returns the associated lawyers
38
+ def add_helper_methods(klass, attribute)
39
+ association = self
40
+ klass.class_eval do
41
+ # lawyer.addresses
42
+ define_method(attribute) do
43
+ unless instance_variable_get("@#{attribute}")
44
+ object = association.resolve_relationship(self)
45
+ instance_variable_set("@#{attribute}", object)
46
+ end
47
+ instance_variable_get("@#{attribute}")
48
+ end
49
+ end
50
+ end
51
+
52
+ # Create a new has_many association.
53
+ def initialize(klass, attribute, options)
54
+ @klass = klass
55
+ @attribute = attribute
56
+ @options = options
57
+
58
+ add_helper_methods(klass, attribute)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,62 @@
1
+ module ReactiveResource
2
+ module Association
3
+ # Represents and resolves a has_one association
4
+ class HasOneAssociation
5
+
6
+ # The class this association is attached to
7
+ attr_reader :klass
8
+
9
+ # The attribute name this association represents
10
+ attr_reader :attribute
11
+
12
+ # additional options passed in when the association was created
13
+ attr_reader :options
14
+
15
+ # Returns the class name of the target of the association. Based
16
+ # off of +attribute+ unless +class_name+ was passed in the
17
+ # +options+ hash.
18
+ def associated_class
19
+ if options[:class_name]
20
+ options[:class_name].constantize
21
+ else
22
+ klass.relative_const_get(attribute.to_s.camelize)
23
+ end
24
+ end
25
+
26
+ # Called when this assocation is referenced. Finds and returns
27
+ # the target of this association.
28
+ def resolve_relationship(object)
29
+ id_attribute = "#{klass.name.split("::").last.underscore}_id"
30
+ associated_class.find(:one, :params => object.prefix_options.merge(id_attribute => object.id))
31
+ end
32
+
33
+ # Adds methods for has_one associations, to make dealing with
34
+ # these objects a bit more straightforward. If the attribute name
35
+ # is +headshot+, it will add:
36
+ #
37
+ # [headshot] returns the associated headshot
38
+ def add_helper_methods(klass, attribute)
39
+ association = self
40
+ klass.class_eval do
41
+ # lawyer.headshot
42
+ define_method(attribute) do
43
+ unless instance_variable_get("@#{attribute}")
44
+ object = association.resolve_relationship(self)
45
+ instance_variable_set("@#{attribute}", object)
46
+ end
47
+ instance_variable_get("@#{attribute}")
48
+ end
49
+ end
50
+ end
51
+
52
+ # Create a new has_one association.
53
+ def initialize(klass, attribute, options)
54
+ @klass = klass
55
+ @attribute = attribute
56
+ @options = options
57
+
58
+ add_helper_methods(klass, attribute)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,7 @@
1
+ module ReactiveResource
2
+ module Association # :nodoc:
3
+ autoload :BelongsToAssociation, 'reactive_resource/association/belongs_to_association'
4
+ autoload :HasManyAssociation, 'reactive_resource/association/has_many_association'
5
+ autoload :HasOneAssociation, 'reactive_resource/association/has_one_association'
6
+ end
7
+ end
@@ -0,0 +1,219 @@
1
+ require 'active_resource'
2
+
3
+ module ReactiveResource
4
+
5
+ # The class that all ReactiveResourse resources should inherit
6
+ # from. This class fixes and patches over a lot of the broken stuff
7
+ # in Active Resource, and smoothes out the differences between the
8
+ # client-side Rails REST stuff and the server-side Rails REST stuff.
9
+ # It also adds support for ActiveRecord-like associations.
10
+ class Base < ActiveResource::Base
11
+ extend Extensions::RelativeConstGet
12
+ # Call this method to transform a resource into a 'singleton'
13
+ # resource. This will fix the paths Active Resource generates for
14
+ # singleton resources. See
15
+ # https://rails.lighthouseapp.com/projects/8994/tickets/4348-supporting-singleton-resources-in-activeresource
16
+ # for more info.
17
+ def self.singleton
18
+ write_inheritable_attribute(:singleton, true)
19
+ end
20
+
21
+ # +true+ if this resource is a singleton resource, +false+
22
+ # otherwise
23
+ def self.singleton?
24
+ read_inheritable_attribute(:singleton)
25
+ end
26
+
27
+ # Active Resource's find_one does nothing if you don't pass a
28
+ # +:from+ parameter. This doesn't make sense if you're dealing
29
+ # with a singleton resource, so if we don't get anything back from
30
+ # +find_one+, try hitting the element path directly
31
+ def self.find_one(options)
32
+ found_object = super(options)
33
+ if !found_object && singleton?
34
+ prefix_options, query_options = split_options(options[:params])
35
+ path = element_path(nil, prefix_options, query_options)
36
+ found_object = instantiate_record(connection.get(path, headers), prefix_options)
37
+ end
38
+ found_object
39
+ end
40
+
41
+ # Override ActiveResource's +collection_name+ to support singular
42
+ # names for singleton resources.
43
+ def self.collection_name
44
+ if singleton?
45
+ element_name
46
+ else
47
+ super
48
+ end
49
+ end
50
+
51
+ # Returns the extension based on the format ('.json', for
52
+ # example), or the empty string if +format+ doesn't specify an
53
+ # extension
54
+ def self.extension
55
+ format.extension.blank? ? "" : ".#{format.extension}"
56
+ end
57
+
58
+ # This method differs from its parent by adding association_prefix
59
+ # into the generated url. This is needed to support belongs_to
60
+ # associations.
61
+ def self.collection_path(prefix_options = {}, query_options = nil)
62
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
63
+ "#{prefix(prefix_options)}#{association_prefix(prefix_options)}#{collection_name}#{extension}#{query_string(query_options)}"
64
+ end
65
+
66
+ # Same as collection_path, except with an extra +method_name+ on
67
+ # the end to support custom methods
68
+ def self.custom_method_collection_url(method_name, options = {})
69
+ prefix_options, query_options = split_options(options)
70
+ "#{prefix(prefix_options)}#{association_prefix(prefix_options)}#{collection_name}/#{method_name}#{extension}#{query_string(query_options)}"
71
+ end
72
+
73
+ # Same as collection_path, except it adds the ID to the end of the
74
+ # path (unless it's a singleton resource)
75
+ def self.element_path(id, prefix_options = {}, query_options = nil)
76
+ prefix_options, query_options = split_options(prefix_options) if query_options.nil?
77
+ element_path = "#{prefix(prefix_options)}#{association_prefix(prefix_options)}#{collection_name}"
78
+
79
+ # singleton resources don't have an ID
80
+ if id || !singleton?
81
+ element_path += "/#{id}"
82
+ end
83
+ element_path += "#{extension}#{query_string(query_options)}"
84
+ element_path
85
+ end
86
+
87
+ # It's kind of redundant to have the server return the foreign
88
+ # keys corresponding to the belongs_to associations (since they'll
89
+ # be in the URL anyway), so we'll try to inject them based on the
90
+ # attributes of the object we just used.
91
+ def load(attributes)
92
+ self.class.belongs_to_with_parents.each do |belongs_to_param|
93
+ attributes["#{belongs_to_param}_id".intern] ||= prefix_options["#{belongs_to_param}_id".intern]
94
+ end
95
+ super(attributes)
96
+ end
97
+
98
+ # Add all of the belongs_to attributes as prefix parameters. This is
99
+ # necessary to support nested url generation on our polymorphic
100
+ # associations, because we need some way of getting the attributes
101
+ # at the point where we need to generate the url, and only the
102
+ # prefix options are available for both finds and creates.
103
+ def self.prefix_parameters
104
+ if !@prefix_parameters
105
+ @prefix_parameters = super
106
+
107
+ @prefix_parameters.merge(belongs_to_with_parents.map {|p| "#{p}_id".to_sym})
108
+ end
109
+ @prefix_parameters
110
+ end
111
+
112
+ # Generates the URL prefix that the belongs_to parameters and
113
+ # associations refer to. For example, a license with params
114
+ # :lawyer_id => 2 will return 'lawyers/2/' and a phone with params
115
+ # :address_id => 2, :lawyer_id => 3 will return
116
+ # 'lawyers/3/addresses/2/'.
117
+ def self.association_prefix(options)
118
+ options = options.dup
119
+ association_prefix = ''
120
+ parent_prefix = ''
121
+
122
+ if belongs_to_associations
123
+ # Recurse to add the parent resource hierarchy. For Phone, for
124
+ # instance, this will add the '/lawyers/:id' part of the URL,
125
+ # which it knows about from the Address class.
126
+ parents.each do |parent|
127
+ parent_prefix = parent.association_prefix(options) if parent_prefix.blank?
128
+ end
129
+
130
+ belongs_to_associations.each do |association|
131
+ if association_prefix.blank? && param_value = options.delete("#{association.attribute}_id".intern) # only take the first one
132
+ association_prefix = "#{association.associated_class.collection_name}/#{param_value}/"
133
+ end
134
+ end
135
+ end
136
+ parent_prefix + association_prefix
137
+ end
138
+
139
+ class << self
140
+ # Holds all the associations that have been declared for this class
141
+ attr_accessor :associations
142
+ end
143
+ self.associations = []
144
+
145
+ # Add a has_one relationship to another class. +options+ is a hash
146
+ # of extra parameters:
147
+ #
148
+ # [:class_name] Override the class name of the target of the
149
+ # association. By default, this is based on the
150
+ # attribute name.
151
+ def self.has_one(attribute, options = {})
152
+ self.associations << Association::HasOneAssociation.new(self, attribute, options)
153
+ end
154
+
155
+ # Add a has_many relationship to another class. +options+ is a hash
156
+ # of extra parameters:
157
+ #
158
+ # [:class_name] Override the class name of the target of the
159
+ # association. By default, this is based on the
160
+ # attribute name.
161
+ def self.has_many(attribute, options = {})
162
+ self.associations << Association::HasManyAssociation.new(self, attribute, options)
163
+ end
164
+
165
+ # Add a parent-child relationship between +attribute+ and this
166
+ # class. This allows parameters like +attribute_id+ to contribute
167
+ # to generating nested urls. +options+ is a hash of extra
168
+ # parameters:
169
+ #
170
+ # [:class_name] Override the class name of the target of the
171
+ # association. By default, this is based on the
172
+ # attribute name.
173
+ def self.belongs_to(attribute, options = {})
174
+ self.associations << Association::BelongsToAssociation.new(self, attribute, options)
175
+ end
176
+
177
+ # Returns all of the belongs_to associations this class has.
178
+ def self.belongs_to_associations
179
+ self.associations.select {|assoc| assoc.kind_of?(Association::BelongsToAssociation) }
180
+ end
181
+
182
+ # Fix up the +klass+ attribute of all of our associations to point
183
+ # to the new child class instead of the parent class, so class
184
+ # lookup works as expected
185
+ def self.inherited(child)
186
+ super(child)
187
+ child.associations = []
188
+ associations.each do |association|
189
+ begin
190
+ child.associations << association.class.new(child, association.attribute, association.options)
191
+ rescue NameError
192
+ # assume that they'll fix the association later by manually specifying :class_name in the belongs_to
193
+ end
194
+ end
195
+ end
196
+
197
+ # belongs_to in ReactiveResource works a little differently than
198
+ # ActiveRecord. Because we have to deal with full class hierachies
199
+ # in order to generate the full URL (as mentioned in
200
+ # association_prefix), we have to treat the belongs_to
201
+ # associations on objects that this object belongs_to as if they
202
+ # exist on this object itself. This method merges in all of this
203
+ # class' associated classes' belongs_to associations, so we can
204
+ # handle deeply nested routes. So, for instance, if we have phone
205
+ # \=> address => lawyer, phone will look for address' belongs_to
206
+ # associations and merge them in. This allows us to have both
207
+ # lawyer_id and address_id at url generation time.
208
+ def self.belongs_to_with_parents
209
+ belongs_to_associations.map(&:associated_attributes).flatten.uniq
210
+ end
211
+
212
+ # All the classes that this class references in its +belongs_to+,
213
+ # along with their parents, and so on.
214
+ def self.parents
215
+ @parents ||= belongs_to_associations.map(&:associated_class)
216
+ end
217
+
218
+ end
219
+ end
@@ -0,0 +1,29 @@
1
+ module Extensions
2
+ # Supplies RelativeConstGet#relative_const_get, which is like +const_get+, except it
3
+ # attempts to resolve using the current class's module, rather than
4
+ # the class's scope itself.
5
+ module RelativeConstGet
6
+
7
+ # Finds the constant with name +name+, relative to the calling
8
+ # module. For instance, <tt>A::B.const_get_relative("C")</tt> will
9
+ # search for A::C, then ::C. This is heavily inspired by
10
+ # +find_resource_in_modules+ in active_resource.
11
+ def relative_const_get(name)
12
+ module_names = self.name.split("::")
13
+ if module_names.length > 1
14
+ receiver = Object
15
+ namespaces = module_names[0, module_names.size-1].map do |module_name|
16
+ receiver = receiver.const_get(module_name)
17
+ end
18
+ const_args = RUBY_VERSION < "1.9" ? [name] : [name, false]
19
+ if namespace = namespaces.reverse.detect { |ns| ns.const_defined?(*const_args) }
20
+ return namespace.const_get(*const_args)
21
+ else
22
+ raise NameError, "Couldn't find a class named #{name}"
23
+ end
24
+ else
25
+ const_get(name)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Extensions # :nodoc:
2
+ autoload :RelativeConstGet, 'reactive_resource/extensions/relative_const_get'
3
+ end
@@ -0,0 +1,4 @@
1
+ module ReactiveResource
2
+ # The current version of ReactiveResource
3
+ VERSION = "0.0.1"
4
+ end
@@ -0,0 +1,5 @@
1
+ module ReactiveResource # :nodoc:
2
+ autoload :Base, 'reactive_resource/base'
3
+ autoload :Association, 'reactive_resource/association'
4
+ autoload :Extensions, 'reactive_resource/extensions'
5
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "reactive_resource/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "reactive_resource"
7
+ s.version = ReactiveResource::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Justin Weiss"]
10
+ s.email = ["justin@uberweiss.org"]
11
+ s.homepage = ""
12
+ s.summary = %q{ActiveRecord-like associations for ActiveResource}
13
+ s.description = %q{ActiveRecord-like associations for ActiveResource}
14
+
15
+ s.rubyforge_project = "reactive_resource"
16
+
17
+ s.add_dependency "activeresource", '~> 2.3.10'
18
+ s.add_development_dependency "shoulda", '~> 2.11.3'
19
+ s.add_development_dependency "webmock", '~> 1.6.1'
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ s.require_paths = ["lib"]
25
+ end
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'reactive_resource'
4
+ require 'test_objects'
5
+
6
+ require 'shoulda'
7
+
8
+ require 'webmock/test_unit'
9
+ class Test::Unit::TestCase
10
+ include WebMock::API
11
+ end
12
+
13
+ WebMock.disable_net_connect!
14
+
15
+ ReactiveResource::Base.site = 'https://api.avvo.com/1'
@@ -0,0 +1,60 @@
1
+ class ReactiveResource::Base
2
+ self.site = "https://api.avvo.com/"
3
+ self.prefix = "/api/1/"
4
+ self.format = :json
5
+ end
6
+
7
+ class ReactiveResource::Lawyer < ReactiveResource::Base
8
+ has_one :headshot
9
+ has_many :addresses
10
+ end
11
+
12
+ # half the complications in here come from the fact that we have
13
+ # resources shared with one of two different types of parents
14
+ class ReactiveResource::Doctor < ReactiveResource::Base
15
+ has_one :headshot
16
+ has_many :addresses
17
+ end
18
+
19
+ class ReactiveResource::Headshot < ReactiveResource::Base
20
+ singleton
21
+ belongs_to :lawyer
22
+ belongs_to :doctor
23
+ end
24
+
25
+ class ReactiveResource::Address < ReactiveResource::Base
26
+ belongs_to :lawyer
27
+ belongs_to :doctor
28
+ has_many :phones
29
+ end
30
+
31
+ module ChildResource
32
+
33
+ class Address < ReactiveResource::Address
34
+ belongs_to :lawyer, :class_name => "ReactiveResource::Lawyer"
35
+ belongs_to :doctor, :class_name => "ReactiveResource::Doctor"
36
+ end
37
+
38
+ class Phone < ReactiveResource::Address
39
+ end
40
+ end
41
+
42
+ class ReactiveResource::Phone < ReactiveResource::Base
43
+ belongs_to :address
44
+ end
45
+
46
+ module ActiveResource
47
+ module Formats
48
+ module NoFormatFormat
49
+ extend ActiveResource::Formats::JsonFormat
50
+ extend self
51
+ def extension
52
+ ''
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ class ReactiveResource::NoExtension < ReactiveResource::Base
59
+ self.format = :no_format
60
+ end
@@ -0,0 +1,169 @@
1
+ require 'test_helper'
2
+
3
+ class ReactiveResource::BaseTest < Test::Unit::TestCase
4
+
5
+ context "A resource that inherits from another resource" do
6
+ setup do
7
+ @object = ChildResource::Address.new
8
+ end
9
+
10
+ should "inherit its parents' associations" do
11
+ assert_contains @object.class.associations.map(&:attribute), :phones
12
+ end
13
+
14
+ should "prefer to resolve to another child class" do
15
+ assert_equal ChildResource::Phone, @object.class.associations.detect {|assoc| assoc.attribute == :phones }.associated_class
16
+ end
17
+
18
+ should "follow relationships where :class_name is specified" do
19
+ assert_equal ReactiveResource::Lawyer, @object.class.associations.detect {|assoc| assoc.attribute == :lawyer }.associated_class
20
+ end
21
+ end
22
+
23
+ context "A resource that inherits from ReactiveResource::Base" do
24
+
25
+ setup do
26
+ @object = ReactiveResource::Lawyer.new
27
+ end
28
+
29
+ should "hit the Avvo API with the correct URL when saved" do
30
+ stub_request(:post, "https://api.avvo.com/api/1/lawyers.json")
31
+ @object.save
32
+ assert_requested(:post, "https://api.avvo.com/api/1/lawyers.json")
33
+ end
34
+
35
+ should "hit the Avvo API with the correct URL when retrieved" do
36
+ stub_request(:get, "https://api.avvo.com/api/1/lawyers/1.json").to_return(:body => {:id => '1'}.to_json)
37
+ ReactiveResource::Lawyer.find(1)
38
+ assert_requested(:get, "https://api.avvo.com/api/1/lawyers/1.json")
39
+ end
40
+
41
+ context "with a has_many relationship to another object" do
42
+ should "hit the associated object's URL with the correct parameters when requested" do
43
+ stub_request(:get, "https://api.avvo.com/api/1/lawyers/1/addresses.json")
44
+ @object.id = 1
45
+ @object.addresses
46
+ assert_requested(:get, "https://api.avvo.com/api/1/lawyers/1/addresses.json")
47
+ end
48
+ end
49
+
50
+ context "with a has_one relationship to another object" do
51
+ should "hit the associated object's URL with the correct parameters when requested" do
52
+ stub_request(:get, "https://api.avvo.com/api/1/lawyers/1/headshot.json").to_return(:body => {:headshot_url => "blah"}.to_json)
53
+ @object.id = 1
54
+ @object.headshot
55
+ assert_requested(:get, "https://api.avvo.com/api/1/lawyers/1/headshot.json")
56
+ end
57
+ end
58
+
59
+ context "with a belongs_to association and correct parameters" do
60
+ setup do
61
+ @object = ReactiveResource::Address.new(:lawyer_id => 2)
62
+ end
63
+
64
+ should "hit the Avvo API with the correct URL when saved" do
65
+ stub_request(:post, "https://api.avvo.com/api/1/lawyers/2/addresses.json")
66
+ @object.save
67
+ assert_requested(:post, "https://api.avvo.com/api/1/lawyers/2/addresses.json")
68
+ end
69
+
70
+ should "hit the Avvo API with the correct URL when retrieved" do
71
+ stub_request(:get, "https://api.avvo.com/api/1/lawyers/2/addresses/3.json").to_return(:body => {:id => '3'}.to_json)
72
+ ReactiveResource::Address.find(3, :params => {:lawyer_id => 2})
73
+ assert_requested(:get, "https://api.avvo.com/api/1/lawyers/2/addresses/3.json")
74
+ end
75
+
76
+ should "hit the Avvo API with the correct URL when updated" do
77
+ stub_request(:get, "https://api.avvo.com/api/1/lawyers/2/addresses/3.json").to_return(:body => {:id => '3'}.to_json)
78
+ license = ReactiveResource::Address.find(3, :params => {:lawyer_id => 2})
79
+ assert_requested(:get, "https://api.avvo.com/api/1/lawyers/2/addresses/3.json")
80
+ stub_request(:put, "https://api.avvo.com/api/1/lawyers/2/addresses/3.json")
81
+ license.save
82
+ assert_requested(:put, "https://api.avvo.com/api/1/lawyers/2/addresses/3.json")
83
+ end
84
+
85
+ should "set the prefix parameters correctly when saved" do
86
+ stub_request(:post, "https://api.avvo.com/api/1/lawyers/2/addresses.json").to_return(:body => {:id => '2'}.to_json)
87
+ @object.save
88
+ assert_requested(:post, "https://api.avvo.com/api/1/lawyers/2/addresses.json")
89
+
90
+ assert_equal "/api/1/lawyers/2/addresses/2.json", @object.send(:element_path)
91
+ end
92
+
93
+ end
94
+
95
+ context "with a belongs_to hierarchy and correct parameters" do
96
+
97
+ setup do
98
+ @object = ReactiveResource::Phone.new(:doctor_id => 2, :address_id => 3)
99
+ end
100
+
101
+ should "allow setting the prefix options after creation" do
102
+ @object = ReactiveResource::Phone.new
103
+ @object.doctor_id = 2
104
+ @object.address_id = 3
105
+ stub_request(:post, "https://api.avvo.com/api/1/doctors/2/addresses/3/phones.json")
106
+ @object.save
107
+ assert_requested(:post, "https://api.avvo.com/api/1/doctors/2/addresses/3/phones.json")
108
+ end
109
+
110
+ should "allow following +belongs_to+ associations" do
111
+ @object = ReactiveResource::Phone.new
112
+ @object.doctor_id = 2
113
+ @object.address_id = 3
114
+ assert_equal 3, @object.address_id
115
+ stub_request(:get, "https://api.avvo.com/api/1/doctors/2/addresses/3.json").to_return(:body => {:id => '3'}.to_json)
116
+ @object.address
117
+ @object.address
118
+ assert_requested(:get, "https://api.avvo.com/api/1/doctors/2/addresses/3.json", :times => 1)
119
+ end
120
+
121
+ should "hit the Avvo API with the correct URL when saved" do
122
+ stub_request(:post, "https://api.avvo.com/api/1/doctors/2/addresses/3/phones.json")
123
+ @object.save
124
+ assert_requested(:post, "https://api.avvo.com/api/1/doctors/2/addresses/3/phones.json")
125
+ end
126
+
127
+ should "hit the Avvo API with the correct URL when updated" do
128
+ stub_request(:get, "https://api.avvo.com/api/1/doctors/2/addresses/3/phones/4.json").to_return(:body => {:id => '4'}.to_json)
129
+ phone = ReactiveResource::Phone.find(4, :params => {:doctor_id => 2, :address_id => 3})
130
+ assert_requested(:get, "https://api.avvo.com/api/1/doctors/2/addresses/3/phones/4.json")
131
+
132
+ stub_request(:put, "https://api.avvo.com/api/1/doctors/2/addresses/3/phones/4.json")
133
+ phone.save
134
+ assert_requested(:put, "https://api.avvo.com/api/1/doctors/2/addresses/3/phones/4.json")
135
+ end
136
+
137
+ should "set the prefix parameters correctly when saved" do
138
+ stub_request(:post, "https://api.avvo.com/api/1/doctors/2/addresses/3/phones.json").to_return(:body => {:phone_type_id => 2, :id => 4, :phone_number=>"206-728-0588"}.to_json)
139
+ @object.save
140
+ assert_requested(:post, "https://api.avvo.com/api/1/doctors/2/addresses/3/phones.json")
141
+
142
+ assert_equal "/api/1/doctors/2/addresses/3/phones/4.json", @object.send(:element_path)
143
+ end
144
+
145
+ should "hit the Avvo API with the correct URL when retrieved" do
146
+ stub_request(:get, "https://api.avvo.com/api/1/doctors/2/addresses/3/phones/4.json").to_return(:body => {:id => '4'}.to_json)
147
+ ReactiveResource::Phone.find(4, :params => {:doctor_id => 2, :address_id => 3})
148
+ assert_requested(:get, "https://api.avvo.com/api/1/doctors/2/addresses/3/phones/4.json")
149
+ end
150
+ end
151
+
152
+ end
153
+
154
+ context "A resource without an extension" do
155
+ should "hit the correct urls" do
156
+ stub_request(:get, "https://api.avvo.com/api/1/no_extensions")
157
+ @object = ReactiveResource::NoExtension.find(:all)
158
+ assert_requested(:get, "https://api.avvo.com/api/1/no_extensions")
159
+
160
+ stub_request(:put, "https://api.avvo.com/api/1/no_extensions/1")
161
+ @object = ReactiveResource::NoExtension.new(:id => 1).save
162
+ assert_requested(:put, "https://api.avvo.com/api/1/no_extensions/1")
163
+
164
+ stub_request(:get, "https://api.avvo.com/api/1/no_extensions/test")
165
+ @object = ReactiveResource::NoExtension.get(:test)
166
+ assert_requested(:get, "https://api.avvo.com/api/1/no_extensions/test")
167
+ end
168
+ end
169
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: reactive_resource
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Justin Weiss
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-01-14 00:00:00 -08:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activeresource
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 2
30
+ - 3
31
+ - 10
32
+ version: 2.3.10
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: shoulda
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 2
45
+ - 11
46
+ - 3
47
+ version: 2.11.3
48
+ type: :development
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: webmock
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ~>
57
+ - !ruby/object:Gem::Version
58
+ segments:
59
+ - 1
60
+ - 6
61
+ - 1
62
+ version: 1.6.1
63
+ type: :development
64
+ version_requirements: *id003
65
+ description: ActiveRecord-like associations for ActiveResource
66
+ email:
67
+ - justin@uberweiss.org
68
+ executables: []
69
+
70
+ extensions: []
71
+
72
+ extra_rdoc_files: []
73
+
74
+ files:
75
+ - .gitignore
76
+ - Gemfile
77
+ - Gemfile.lock
78
+ - README.rdoc
79
+ - Rakefile
80
+ - lib/reactive_resource.rb
81
+ - lib/reactive_resource/association.rb
82
+ - lib/reactive_resource/association/belongs_to_association.rb
83
+ - lib/reactive_resource/association/has_many_association.rb
84
+ - lib/reactive_resource/association/has_one_association.rb
85
+ - lib/reactive_resource/base.rb
86
+ - lib/reactive_resource/extensions.rb
87
+ - lib/reactive_resource/extensions/relative_const_get.rb
88
+ - lib/reactive_resource/version.rb
89
+ - reactive_resource.gemspec
90
+ - test/test_helper.rb
91
+ - test/test_objects.rb
92
+ - test/unit/base_test.rb
93
+ has_rdoc: true
94
+ homepage: ""
95
+ licenses: []
96
+
97
+ post_install_message:
98
+ rdoc_options: []
99
+
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ segments:
108
+ - 0
109
+ version: "0"
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ segments:
116
+ - 0
117
+ version: "0"
118
+ requirements: []
119
+
120
+ rubyforge_project: reactive_resource
121
+ rubygems_version: 1.3.7
122
+ signing_key:
123
+ specification_version: 3
124
+ summary: ActiveRecord-like associations for ActiveResource
125
+ test_files:
126
+ - test/test_helper.rb
127
+ - test/test_objects.rb
128
+ - test/unit/base_test.rb