reactive_resource 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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