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 +4 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +29 -0
- data/README.rdoc +72 -0
- data/Rakefile +19 -0
- data/lib/reactive_resource/association/belongs_to_association.rb +96 -0
- data/lib/reactive_resource/association/has_many_association.rb +62 -0
- data/lib/reactive_resource/association/has_one_association.rb +62 -0
- data/lib/reactive_resource/association.rb +7 -0
- data/lib/reactive_resource/base.rb +219 -0
- data/lib/reactive_resource/extensions/relative_const_get.rb +29 -0
- data/lib/reactive_resource/extensions.rb +3 -0
- data/lib/reactive_resource/version.rb +4 -0
- data/lib/reactive_resource.rb +5 -0
- data/reactive_resource.gemspec +25 -0
- data/test/test_helper.rb +15 -0
- data/test/test_objects.rb +60 -0
- data/test/unit/base_test.rb +169 -0
- metadata +128 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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,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
|
data/test/test_helper.rb
ADDED
@@ -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
|