lucid_works 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -227,23 +227,6 @@ Then
227
227
 
228
228
  thing.build_whatnot -> an unsaved Whatnot
229
229
 
230
- ==== Has_singleton associations
231
-
232
- The has_singleton association is used to associate a resource with another intransient singleton resource, i.e. one that always exists and calling destroy does not remove it.
233
-
234
- class Thing < LucidWorks::Base
235
- has_one :whatnot
236
- end
237
-
238
- class Whatnot < LucidWorks::Base
239
- self.singleton = true
240
- belongs_to :thing
241
- end
242
-
243
- Then
244
-
245
- thing.whatnot -> an unsaved Whatnot
246
-
247
230
  === Belongs_to associations
248
231
 
249
232
  Te belongs to association augments the model with methods to access its parent. Given:
@@ -257,6 +240,8 @@ Then:
257
240
 
258
241
  whatnot.thing -> A Thing
259
242
 
243
+ For more information on association see LucidWorks::Associations::ClassMethods
244
+
260
245
  === Schema
261
246
 
262
247
  A class may have a schema defined as follows:
@@ -267,11 +252,11 @@ A class may have a schema defined as follows:
267
252
  attribute :bool1, :boolean
268
253
  attribute :integer1, :integer
269
254
  attributes :string2, :string3, :string4
270
- attributes :bool2, :bool3, :type => :boolean
271
- attributes :int2, :int3, :type => :integer
255
+ attributes :bool2, :bool3, :type => :boolean
256
+ attributes :int2, :int3, :type => :integer
272
257
  attribute :string_with_values, :values => ['one', 'two']
273
258
  attribute :dontsendme, :omit_during_update => true
274
- attribute :sendnull, :string, :nil_when_blank => true
259
+ attribute :sendnull, :string, :nil_when_blank => true
275
260
  end
276
261
  end
277
262
 
@@ -0,0 +1,70 @@
1
+ module LucidWorks
2
+ module Associations
3
+ class HasMany < Proxy #:nodoc:
4
+
5
+ def build(attributes={})
6
+ @target_class.new(attributes.merge({:parent => @owner}))
7
+ # Don't cache it - it's only a single target
8
+ end
9
+
10
+ def create(attributes={})
11
+ target = build(attributes)
12
+ target.save
13
+ target
14
+ end
15
+
16
+ def create!(attributes={})
17
+ target = build(attributes)
18
+ if target.save
19
+ target
20
+ else
21
+ raise target.errors.full_messages
22
+ end
23
+ end
24
+
25
+ def remember_find_options(options)
26
+ @find_options = options
27
+ end
28
+
29
+ # Explicit find call does not use the remembered options, or cache the result
30
+ def find(id_or_find_type, options={})
31
+ @target_class.send(:find, id_or_find_type, options.merge(:parent => @owner))
32
+ end
33
+
34
+ private
35
+
36
+ # Caching rules
37
+ # It's easy to cache a has_one resource as we never search for different things.
38
+ # With a has_many the user can either use .others and get the entire collection
39
+ # or .others.find(id) and get just one. What do we cache?
40
+ #
41
+ # Interestingly for us the cost difference is marginal between the two forms of
42
+ # the call. Should we just grab all of them and return the one they want?
43
+ # Probably not. Not every elegant and there is some cost to creating models
44
+ # on our end.
45
+ #
46
+ # Okay for now we will just not cache .find()
47
+ #
48
+ # The largest challenge is this sequence:
49
+ #
50
+ # a = others.find(1) #
51
+ # others.find(1).name
52
+ # b = others.first # requires retrieving all
53
+
54
+
55
+ def load_target(options={})
56
+ if @target.nil? || options[:force]
57
+ if @options[:has_content] === false
58
+ return true # make method_missing happy so it will operate on NilClass
59
+ else
60
+ opts = (@find_options || {}).merge(:parent => @owner)
61
+ @target = @target_class.send(:find, :all, opts)
62
+ @find_options = {}
63
+ end
64
+ else
65
+ end
66
+ @target
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,38 @@
1
+ module LucidWorks
2
+ module Associations
3
+ class HasOne < Proxy #:nodoc:
4
+
5
+ def build(attributes={})
6
+ @target = @target_class.new(attributes.merge({:parent => @owner}))
7
+ end
8
+
9
+ def create(attributes={})
10
+ build(attributes)
11
+ @target.save
12
+ @target
13
+ end
14
+
15
+ def create!(attributes={})
16
+ build(attributes)
17
+ if @target.save
18
+ @target
19
+ else
20
+ raise @target.errors.full_messages
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def load_target(options={})
27
+ if @target.nil? || options[:force]
28
+ if @options[:has_content] === false
29
+ return true # make method_missing happy so it will operate on NilClass
30
+ else
31
+ @target = @target_class.send(:find, :singleton, :parent => @owner)
32
+ end
33
+ end
34
+ @target
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,51 @@
1
+ module LucidWorks
2
+ module Associations
3
+
4
+ # This is the root class of our association proxies:
5
+ #
6
+ # Associations
7
+ # Proxy
8
+ # HasOne
9
+ # HasMany
10
+
11
+ class Proxy #:nodoc:
12
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|caller|^object_id$)/ }
13
+
14
+ def initialize(owner, target_class, options={})
15
+ @owner, @target_class, @options = owner, target_class, options
16
+ @target = nil
17
+ end
18
+
19
+ # Sets the target of this proxy to <tt>\target</tt>.
20
+ def target=(target)
21
+ @target = target
22
+ end
23
+
24
+ def loaded?
25
+ !!@target
26
+ end
27
+
28
+ # Can targets be retrieved for all owners if this type, in one step using owner/all/target
29
+ def supports_all?
30
+ @options[:supports_all]
31
+ end
32
+
33
+ def reload!
34
+ load_target(:force => true)
35
+ end
36
+
37
+ private
38
+
39
+ # Forwards any missing method call to the \target.
40
+ def method_missing(method, *args, &block)
41
+ if load_target
42
+ if @target.respond_to?(method)
43
+ @target.send(method, *args, &block)
44
+ else
45
+ super
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,37 +1,58 @@
1
1
  module LucidWorks
2
2
 
3
+ #
4
+ # See LucidWorks::Associations::ClassMethods
5
+ #
3
6
  module Associations
4
7
  extend ActiveSupport::Concern
5
8
 
6
9
  module ClassMethods
7
10
 
11
+ def associations #:nodoc:
12
+ @associations ||= {}
13
+ end
14
+
8
15
  # Specifies a singleton child resource.
9
16
  #
10
- # In the parent resource creates methods:
11
- # if option :has_content is true (default)
12
- # child - load and cache the child. Subsequent calls will access the cached value.
13
- # child! - load and cache the child, ignoring existing cached value if present.
14
- # build_child - create a new, unsaved resource
15
- # if option :has_content is false
16
- # child - create a new, unsaved resource
17
+ # has_one :resource, <options>
18
+ #
19
+ # In the parent resource, creates methods:
20
+ #
21
+ # .resource
22
+ # .resource!
23
+ # .resource.build(attributes={})
24
+ # .resource.create(attributes={})
25
+ # .resource.create!(attributes={})
26
+ # .build_resource(attributes={})
27
+ # .create_resource(attributes={})
28
+ #
29
+ # _.resource_ will not actually load the resource until used i.e. resource._anything_ is called.
30
+ # After being loaded, the child is cached. Use _.resource!_ or _resource.reload!_ to relaod it.
31
+ #
32
+ # _.resource!_ will load the resource immediately, or reload it if it was already loaded.
33
+ #
34
+ # _.build_ creates a new model but does not save it. Parent is preset to the calling class.
35
+ #
36
+ # _.create_ builds the model, saves it, then returns it.
37
+ # You need to check persisted? and errors to see if the save succeeded.
38
+ #
39
+ # _.build_resource_ and _.create_resource_ are aliases for _.resource.build_ and _.resource.create_ respectively.
40
+ #
17
41
  # === Options
18
42
  #
19
43
  # The declaration can also include an options hash to specialize the behavior of the association.
20
44
  #
21
45
  # Options are:
22
- # [:class_name]
23
- # Specify the class name of the association. Use it only if you want to us a child class name different
46
+ # [:class_name => "name"]
47
+ # Specify the class name of the association. Use it if you want to us a child class name different
24
48
  # from the association name, e.g.
25
- # has_one :info, :class_name => :collection_info # use CollectionInfo class
26
- # has_one :foo, :class_name => :'foo/bar' # use Foo::Bar class
27
- # [:has_content]
28
- # Changes the behavior of the .<resource> association method:
29
- # If set to true (default), indicates that this resource may be retrieved using a GET, and
30
- # the .<resource> method will retrieve it.
31
- # If set to false, this resource may not be retrieved using a GET, and the .<resource> method
32
- # will instead build and return new, unsaved, model. This is useful for pseudo-resources that only
33
- # provide actions, not data.
34
- #
49
+ # has_one :foo # Use class Foo
50
+ # has_one :foo, :class_name => :'things/foo_bar' # use class Things::FooBar
51
+ # [:has_content => false]
52
+ # If set to false, indicated that this resource may not be retrieved using a GET,
53
+ # and will cause the _.resource_ method to return nil until the resource is built or created.
54
+ # This is useful for pseudo-resources that only provide actions, not data.
55
+
35
56
  def has_one(*arguments)
36
57
  options = arguments.last.is_a?(Hash) ? arguments.pop : {}
37
58
  arguments.each do |resource|
@@ -39,7 +60,7 @@ module LucidWorks
39
60
  end
40
61
  end
41
62
 
42
- # Specifies a child resource.
63
+ # Specifies a collection child resource.
43
64
  #
44
65
  # e.g. for Blog has_many posts
45
66
  #
@@ -86,57 +107,55 @@ module LucidWorks
86
107
 
87
108
  private
88
109
 
89
- def define_has_one(resource, options={})
90
- resource_class_name = (options[:class_name] || resource).to_s.camelize
110
+ def define_has_one(resource, options={}) #:nodoc:
111
+ resource_class_name = (options.delete(:class_name) || resource).to_s.camelize
112
+ associations[resource.to_sym] = {:type => :has_one, :class_name => "#{resource_class_name}"}.merge(options)
91
113
 
92
114
  class_eval <<-EOF, __FILE__, __LINE__ + 1
93
- def #{resource} # def child
94
- @#{resource} || #{resource}! # @child || child!
95
- end # end
96
- EOF
115
+ def #{resource}
116
+ @#{resource}_association ||= HasOne.new(self, #{resource_class_name}, #{options.to_s})
117
+ end
97
118
 
98
- if options[:has_content] == false
99
- class_eval <<-EOF, __FILE__, __LINE__ + 1
100
- def #{resource}! # def child!
101
- @#{resource} = #{resource_class_name}.new(:parent => self) # @child = Child.new(options.merge :parent => self)
102
- end # end
103
- EOF
104
- else
105
- class_eval <<-EOF, __FILE__, __LINE__ + 1
106
- def #{resource}! # def child!
107
- @#{resource} = #{resource_class_name}.find(:parent => self) # @child = Child.find(:parent => self)
108
- end # end
109
-
110
- def build_#{resource}(options = {})
111
- #{resource_class_name}.new(options.merge :parent => self)
112
- end
113
- EOF
114
- end
119
+ def #{resource}!
120
+ #{resource}.reload!
121
+ end
122
+
123
+ def build_#{resource}(options = {})
124
+ #{resource}.build(options)
125
+ end
126
+
127
+ def create_#{resource}(options = {})
128
+ #{resource}.create(options)
129
+ end
130
+ EOF
115
131
  end
116
132
 
117
- def define_has_many(resources, options = {})
133
+ def define_has_many(resources, options={}) #:nodoc:
118
134
  resource = resources.to_s.singularize
119
- resource_class_name = (options[:class_name] || resource).to_s.classify
135
+ resource_class_name = (options.delete(:class_name) || resource).to_s.classify
136
+ associations[resource.to_sym] = {:type => :has_many, :class_name => "#{resource_class_name}"}.merge(options)
120
137
 
121
138
  class_eval <<-EOF, __FILE__, __LINE__ + 1
122
139
  def #{resources}(options={})
123
- @#{resources} || #{resources}!(options)
140
+ @#{resources}_association ||= HasMany.new(self, #{resource_class_name}, #{options.to_s})
141
+ @#{resources}_association.remember_find_options(options) unless options.empty?
142
+ @#{resources}_association
124
143
  end
125
144
 
126
145
  def #{resources}!(options={})
127
- @#{resources} = #{resource_class_name}.all(options.merge :parent => self)
146
+ #{resources}(options).reload!
128
147
  end
129
148
 
130
- def #{resource}(id, options={})
131
- #{resource_class_name}.find(id, options.merge(:parent => self))
149
+ def build_#{resource}(options = {})
150
+ #{resources}.build(options)
132
151
  end
133
152
 
134
153
  def create_#{resource}(options = {})
135
- #{resource_class_name}.create(options.merge :parent => self)
154
+ #{resources}.create(options)
136
155
  end
137
156
 
138
- def build_#{resource}(options = {})
139
- #{resource_class_name}.new(options.merge :parent => self)
157
+ def #{resource}(id, options={})
158
+ #{resources}.find(id, options)
140
159
  end
141
160
  EOF
142
161
  end
@@ -141,8 +141,8 @@ module LucidWorks
141
141
 
142
142
  results =
143
143
  if kind_of_find == :all
144
- data.collect do |collection_attributes|
145
- new(collection_attributes.merge(:parent => parent, :persisted => true))
144
+ data.collect do |model_attributes|
145
+ new(model_attributes.merge(:parent => parent, :persisted => true))
146
146
  end
147
147
  else
148
148
  attributes = data.is_a?(Hash) ? data : {}
@@ -154,11 +154,56 @@ module LucidWorks
154
154
  end
155
155
 
156
156
  # Process :include options
157
- # Pedestrian version first: step through all results and pull in submodel
158
- if includes
159
- [results].flatten.each do |model|
160
- [includes].flatten.each do |include|
161
- model.send(include)
157
+ #
158
+ # In the section, to reduce confusion we will use the terms owner/target instead of
159
+ # parent/child to describe the association.
160
+ #
161
+ # If we are doing a find(:all) and this owner resource supports retrieval of all targets
162
+ # in one request with owners/all/targets, get them and attach them to the original models'
163
+ # association proxys.
164
+ #
165
+ results_array = [results].flatten # allow us to process a single result or array with the same code
166
+ if includes && !results_array.empty?
167
+ includes = [includes].flatten
168
+ includes.each do |association_name|
169
+ association_info = associations[association_name.to_s.singularize.to_sym]
170
+ target_class = class_eval(association_info[:class_name]) # get scoping right
171
+ target_name = association_info[:class_name].underscore
172
+
173
+ if kind_of_find == :all && association_info[:supports_all]
174
+ all_targets_path = "#{collection_url(parent)}/all/#{target_name}"
175
+ raw_response = ActiveSupport::Notifications.instrument("lucid_works.request") do |payload|
176
+ payload[:method] = :get
177
+ payload[:uri] = all_targets_path
178
+ payload[:response] = RestClient.get(all_targets_path)
179
+ end
180
+ if association_info[:type] == :has_one
181
+ all_targets_attributes = JSON.parse raw_response
182
+ all_targets_attributes.each do |target_attributes|
183
+ owner_id = target_attributes['id']
184
+ owner = results_array.detect { |result| result.id == owner_id }
185
+ if owner
186
+ target = target_class.new(target_attributes.merge(:parent => owner, :persisted => true))
187
+ association_proxy = owner.send(association_name)
188
+ association_proxy.target = target
189
+ end
190
+ end
191
+ elsif association_info[:type] == :has_many
192
+ # [{"history":[...history_models...],"id":372},{"history":[...history_models...],"id":371}]
193
+ JSON.parse(raw_response).each do |group_of_targets|
194
+ owner_id = group_of_targets['id']
195
+ owner = results_array.detect { |result| result.id == owner_id }
196
+ targets = group_of_targets[target_name].collect do |target_attrs|
197
+ target_class.new(target_attrs.merge(:parent => owner, :persisted => true))
198
+ end
199
+ association_proxy = owner.send(association_name)
200
+ association_proxy.target = targets
201
+ end
202
+ end
203
+ else # kind_of_find != :all || !supports_all
204
+ results_array.each do |result|
205
+ result.send("#{association_name}!")
206
+ end
162
207
  end
163
208
  end
164
209
  end
@@ -249,6 +294,7 @@ module LucidWorks
249
294
  def initialize(options)
250
295
  raise ArgumentError.new("new requires a Hash") unless options.is_a?(Hash)
251
296
  @parent = self.class.extract_parent_from_options(options)
297
+ @associations = {}
252
298
  @persisted = options.delete(:persisted) || singleton? || false
253
299
  @attributes = {}.with_indifferent_access
254
300
  load_attributes(options)
@@ -19,7 +19,7 @@ module LucidWorks
19
19
  end
20
20
 
21
21
  def empty!
22
- index.destroy(:params => {:key => 'iaccepttherisk'})
22
+ build_index.destroy(:params => {:key => 'iaccepttherisk'})
23
23
  end
24
24
 
25
25
  # Setup the Collection with an RSolr object that it can use to search.
@@ -2,8 +2,9 @@ module LucidWorks
2
2
 
3
3
  class Datasource < Base
4
4
  belongs_to :collection
5
- has_many :histories, :class_name => :history
6
- has_one :status, :schedule, :crawldata
5
+ has_many :histories, :class_name => :history, :supports_all => true
6
+ has_one :status, :supports_all => true
7
+ has_one :schedule, :crawldata
7
8
  has_one :index, :job, :has_content => false
8
9
 
9
10
  schema do
@@ -62,7 +63,7 @@ module LucidWorks
62
63
 
63
64
 
64
65
  def empty!
65
- index.destroy
66
+ build_index.destroy
66
67
  end
67
68
 
68
69
  def editable?
@@ -81,11 +82,11 @@ module LucidWorks
81
82
  end
82
83
 
83
84
  def start_crawl!
84
- job.save
85
+ build_job.save
85
86
  end
86
87
 
87
88
  def stop_crawl!
88
- job.destroy
89
+ build_job.destroy
89
90
  end
90
91
 
91
92
  def t_type
@@ -1,5 +1,5 @@
1
- module RestClient
2
- class Request
1
+ module RestClient #:nodoc:
2
+ class Request #:nodoc:
3
3
 
4
4
  # Extract the query parameters for get request and append them to the url
5
5
  def process_get_params url, headers
@@ -1,6 +1,6 @@
1
1
  require 'time'
2
2
 
3
- class Time
3
+ class Time #:nodoc:
4
4
  class << self
5
5
  # Ruby's 'Time.is08601 can't handle timezone offsets expressed as +nnnn.
6
6
  # It prefers +nn:nn. Morph the former to the latter.
@@ -1,3 +1,3 @@
1
1
  module LucidWorks
2
- VERSION = "0.5.3"
2
+ VERSION = "0.6.0"
3
3
  end
data/lib/lucid_works.rb CHANGED
@@ -23,6 +23,9 @@ require 'lucid_works/patch_time'
23
23
 
24
24
  require 'lucid_works/exceptions'
25
25
  require 'lucid_works/associations'
26
+ require 'lucid_works/associations/proxy'
27
+ require 'lucid_works/associations/has_one'
28
+ require 'lucid_works/associations/has_many'
26
29
  require 'lucid_works/server'
27
30
  require 'lucid_works/base'
28
31
  require 'lucid_works/schema'
@@ -0,0 +1,167 @@
1
+ require 'spec_helper'
2
+
3
+ describe LucidWorks::Associations::HasMany do
4
+ before :all do
5
+ @fake_server_uri = "http://12.0.0.1:99999"
6
+ @server = LucidWorks::Server.new(@fake_server_uri)
7
+
8
+ class ::Target < LucidWorks::Base
9
+ attr_accessor :foo
10
+ end
11
+ class ::Owner < LucidWorks::Base
12
+ has_many :targets
13
+ end
14
+ end
15
+
16
+ before :each do
17
+ @owner = Owner.new(:parent => @server)
18
+ end
19
+
20
+ it "should not load the targets if just referenced" do
21
+ Target.should_not_receive(:find)
22
+ @owner.targets
23
+ end
24
+
25
+ it "should load the target with find(:parent => Owner) when target is accessed" do
26
+ Target.should_receive(:find).with(:all, :parent => @owner)
27
+ @owner.targets.first
28
+ end
29
+
30
+ it "should load the target with find(:parent => Owner) and remembered options when target is accessed" do
31
+ Target.should_receive(:find).with(:all, :parent => @owner, :include => :foo)
32
+ @owner.targets(:include => :foo)
33
+ @owner.targets.first
34
+ end
35
+
36
+ it "should cache the targets and not reload them on every access" do
37
+ mock_targets = [double('target1'), double('target2')]
38
+ Target.should_receive(:find).with(:all, :parent => @owner).once.and_return(mock_targets)
39
+ @owner.targets.first
40
+ @owner.targets.first
41
+ end
42
+
43
+ it "should delegate methods to the target" do
44
+ mock_targets = [double('target1'), double('target2')]
45
+ Target.stub(:find) { mock_targets }
46
+ mock_targets.should_receive(:first)
47
+ @owner.targets.first
48
+ end
49
+
50
+ describe "#loaded?" do
51
+ it "should return true if the target has been loaded, false otherwise" do
52
+ mock_targets = [double('target1'), double('target2')]
53
+ Target.stub(:find) { mock_targets }
54
+
55
+ @owner.targets.loaded?.should be_false
56
+ @owner.targets.first
57
+ @owner.targets.loaded?.should be_true
58
+ end
59
+ end
60
+
61
+ describe "#reload!" do
62
+ it "should force a reload of the target" do
63
+ mock_targets = [double('target1'), double('target2')]
64
+ Target.should_receive(:find).with(:all, :parent => @owner).twice.and_return(mock_targets)
65
+ @owner.targets.first
66
+ @owner.targets.reload!
67
+ end
68
+ end
69
+
70
+ describe "#find" do
71
+ it "should call Target.find with the provided options (not remembered options)" do
72
+ @owner.targets(:include => :foo)
73
+ Target.should_receive(:find).with(123, :parent => @owner)
74
+ @owner.targets.find(123)
75
+ end
76
+
77
+ it "should pass the find options to Target.find() when actually accessed" do
78
+ mock_target = double('target')
79
+ Target.should_receive(:find).with(123, :parent => @owner).and_return(mock_target)
80
+ @owner.targets.find(123).class
81
+ end
82
+ end
83
+
84
+ describe "#build" do
85
+ it "should new up a target and initialize it with the supplied attributes" do
86
+ target = @owner.targets.build(:foo => 'bar')
87
+ target.should_not be_persisted
88
+ target.foo.should == 'bar'
89
+ end
90
+
91
+ it "should initialize the new target's parent" do
92
+ target = @owner.targets.build
93
+ target.parent.should == @owner
94
+ end
95
+ end
96
+
97
+ describe "#create" do
98
+ it "should new up a target and initialize it with the supplied attributes, then attempt to save it" do
99
+ mock_target = double('target')
100
+ Target.should_receive(:new).with(:foo => :bar, :parent => @owner).and_return(mock_target)
101
+ mock_target.should_receive(:save).and_return(:save_result)
102
+ target = @owner.targets.create(:foo => :bar)
103
+ target.should == mock_target
104
+ end
105
+
106
+ it "should initialize the new target's parent" do
107
+ target = @owner.targets.build
108
+ target.parent.should == @owner
109
+ end
110
+ end
111
+
112
+ describe "#create!" do
113
+ before do
114
+ @mock_target = double('target')
115
+ Target.should_receive(:new).with(:foo => :bar, :parent => @owner).and_return(@mock_target)
116
+ end
117
+
118
+ context "when save succeeds" do
119
+ before do
120
+ @mock_target.should_receive(:save).and_return(true)
121
+ end
122
+
123
+ it "should return the target" do
124
+ target = @owner.targets.create!(:foo => :bar)
125
+ target.should == @mock_target
126
+ end
127
+ end
128
+
129
+ context "when save fails" do
130
+ before do
131
+ @mock_target.should_receive(:save).and_return(false)
132
+ end
133
+
134
+ it "should raise an error" do
135
+ lambda {
136
+ @owner.targets.create!(:foo => :bar)
137
+ }.should raise_error
138
+ end
139
+ end
140
+ end
141
+
142
+ context "with options :supports_all => true" do
143
+ before :all do
144
+ class ::AllableTarget < LucidWorks::Base
145
+ end
146
+ class ::Owner
147
+ has_many :allable_targets, :supports_all => true
148
+ end
149
+ end
150
+
151
+ describe "Owner.find(:all, :include => :targets)" do
152
+ it "should use owwner/all/targets to retrieve the targets" do
153
+ owners_json = '[{"id":1},{"id":2}]'
154
+ target_json = '[{"id":1,"allable_target":[{"id":1,"fruit":"apple"},{"id":1,"fruit":"pear"}]},' +
155
+ '{"id":2,"allable_target":[{"id":2,"fruit":"orange"}]}]'
156
+ RestClient.should_receive(:get).with("#{@fake_server_uri}/api/owners").
157
+ once.ordered.and_return(owners_json)
158
+ RestClient.should_receive(:get).with("#{@fake_server_uri}/api/owners/all/allable_target").
159
+ once.ordered.and_return(target_json)
160
+ owners = Owner.find(:all, :parent => @server, :include => :allable_targets)
161
+ owners.first.allable_targets.first.fruit.should == 'apple'
162
+ owners.first.allable_targets.last.fruit.should == 'pear'
163
+ owners.last.allable_targets.first.fruit.should == 'orange'
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,177 @@
1
+ require 'spec_helper'
2
+
3
+ describe LucidWorks::Associations::HasOne do
4
+ before :all do
5
+ @fake_server_uri = "http://12.0.0.1:99999"
6
+ @server = LucidWorks::Server.new(@fake_server_uri)
7
+
8
+ class ::Target < LucidWorks::Base
9
+ attr_accessor :foo
10
+ end
11
+ class ::Owner < LucidWorks::Base
12
+ has_one :target
13
+ end
14
+ end
15
+
16
+ before :each do
17
+ @owner = Owner.new(:parent => @server)
18
+ end
19
+
20
+ it "should not load the target if just referenced" do
21
+ Target.should_not_receive(:find)
22
+ @owner.target
23
+ end
24
+
25
+ it "should load the target with find(:parent => Owner) when target is accessed" do
26
+ Target.should_receive(:find).with(:singleton, :parent => @owner)
27
+ @owner.target.class
28
+ end
29
+
30
+ it "should cache the target and not reload it on every access" do
31
+ mock_target = double('target')
32
+ Target.should_receive(:find).with(:singleton, :parent => @owner).once.and_return(mock_target)
33
+ @owner.target.class
34
+ @owner.target.class
35
+ end
36
+
37
+ it "should delegate methods to the target" do
38
+ mock_target = double('target')
39
+ Target.stub(:find) { mock_target }
40
+ mock_target.should_receive(:foo)
41
+ @owner.target.foo
42
+ end
43
+
44
+ describe "#loaded?" do
45
+ it "should return true if the target has been loaded, false otherwise" do
46
+ mock_target = double('target')
47
+ Target.stub(:find) { mock_target }
48
+
49
+ @owner.target.loaded?.should be_false
50
+ @owner.target.class
51
+ @owner.target.loaded?.should be_true
52
+ end
53
+ end
54
+
55
+ describe "#reload!" do
56
+ it "should force a reload of the target" do
57
+ mock_target = double('target')
58
+ Target.should_receive(:find).with(:singleton, :parent => @owner).twice.and_return(mock_target)
59
+ @owner.target.class
60
+ @owner.target.reload!
61
+ end
62
+ end
63
+
64
+ describe "#build" do
65
+ it "should new up a target and initialize it with the supplied attributes" do
66
+ target = @owner.target.build(:foo => 'bar')
67
+ target.should_not be_persisted
68
+ target.foo.should == 'bar'
69
+ end
70
+
71
+ it "should initialize the new target's parent" do
72
+ target = @owner.target.build
73
+ target.parent.should == @owner
74
+ end
75
+ end
76
+
77
+ describe "#create" do
78
+ it "should new up a target and initialize it with the supplied attributes, then attempt to save it" do
79
+ mock_target = double('target')
80
+ Target.should_receive(:new).with(:foo => :bar, :parent => @owner).and_return(mock_target)
81
+ mock_target.should_receive(:save).and_return(:save_result)
82
+ target = @owner.target.create(:foo => :bar)
83
+ target.should == mock_target
84
+ end
85
+
86
+ it "should initialize the new target's parent" do
87
+ target = @owner.target.build
88
+ target.parent.should == @owner
89
+ end
90
+ end
91
+
92
+ describe "#create!" do
93
+ before do
94
+ @mock_target = double('target')
95
+ Target.should_receive(:new).with(:foo => :bar, :parent => @owner).and_return(@mock_target)
96
+ end
97
+
98
+ context "when save succeeds" do
99
+ before do
100
+ @mock_target.should_receive(:save).and_return(true)
101
+ end
102
+
103
+ it "should return the target" do
104
+ target = @owner.target.create!(:foo => :bar)
105
+ target.should == @mock_target
106
+ end
107
+ end
108
+
109
+ context "when save fails" do
110
+ before do
111
+ @mock_target.should_receive(:save).and_return(false)
112
+ end
113
+
114
+ it "should raise an error" do
115
+ lambda {
116
+ @owner.target.create!(:foo => :bar)
117
+ }.should raise_error
118
+ end
119
+ end
120
+ end
121
+
122
+ context "with option has_content => false" do
123
+ before :all do
124
+ class ::TargetWithoutContent < LucidWorks::Base
125
+ attr_accessor :foo
126
+ end
127
+ class ::Owner
128
+ has_one :target_without_content, :has_content => false
129
+ end
130
+ end
131
+
132
+ before :each do
133
+ @owner = Owner.new(:parent => @server)
134
+ end
135
+
136
+ context "before any build or create" do
137
+ describe "attribute accesses" do
138
+ it "should not attempt to retrieve the target, and let the caller interact with NilClass" do
139
+ TargetWithoutContent.should_not_receive(:find)
140
+ @owner.target_without_content.should be_a(NilClass)
141
+ end
142
+ end
143
+ end
144
+
145
+ context "after build" do
146
+ it "should return the target" do
147
+ target = @owner.target_without_content.build(:foo => 'bar')
148
+ target.should be_a(TargetWithoutContent)
149
+ target.foo.should == 'bar'
150
+ end
151
+ end
152
+ end
153
+
154
+ context "with options :supports_all => true" do
155
+ before :all do
156
+ class ::TargetWithAll < LucidWorks::Base
157
+ end
158
+ class ::Owner
159
+ has_one :target_with_all, :supports_all => true
160
+ end
161
+ end
162
+
163
+ describe "Owner.find(:all, :include => :target)" do
164
+ it "should use owwner/all/target to retrieve the targets" do
165
+ owners_json = '[{"id":1},{"id":2}]'
166
+ target_json = '[{"id":1,"fruit":"apple"},{"id":2,"fruit":"orange"}]'
167
+ RestClient.should_receive(:get).with("#{@fake_server_uri}/api/owners").
168
+ once.ordered.and_return(owners_json)
169
+ RestClient.should_receive(:get).with("#{@fake_server_uri}/api/owners/all/target_with_all").
170
+ once.ordered.and_return(target_json)
171
+ owners = Owner.find(:all, :parent => @server, :include => :target_with_all)
172
+ owners.first.target_with_all.fruit.should == 'apple'
173
+ owners.last.target_with_all.fruit.should == 'orange'
174
+ end
175
+ end
176
+ end
177
+ end
@@ -4,128 +4,108 @@ describe LucidWorks::Associations do
4
4
  before :all do
5
5
  @fake_server_uri = "http://fakehost.com:8888"
6
6
  @server = LucidWorks::Server.new(@fake_server_uri)
7
-
8
- class ::Blog < LucidWorks::Base
9
- has_many :posts
10
- has_one :homepage
11
- has_one :launch_party, :has_content => false
12
- end
13
- @blog = ::Blog.new(:parent => @server)
14
-
15
- class ::Post < LucidWorks::Base
16
- belongs_to :blog
17
- end
18
-
19
- class ::Homepage < LucidWorks::Base
20
- self.singleton = true
21
- belongs_to :blog
22
- end
23
-
24
- class ::LaunchParty < LucidWorks::Base
25
- self.singleton = true
26
- belongs_to :blog
27
- end
28
7
  end
29
8
 
30
9
  describe ".has_one" do
31
- context "without content" do
32
- describe "#<child>" do
33
- it "should call child! the first time then return the cached value thereafter" do
34
- mock_launch_party = double('launch_party')
35
- LaunchParty.should_receive(:new).once.and_return(mock_launch_party)
36
-
37
- @blog.launch_party.should == mock_launch_party
38
- @blog.launch_party.should == mock_launch_party
39
- end
10
+ before :all do
11
+ class ::Blog < LucidWorks::Base
12
+ has_one :homepage
13
+ has_one :calendar, :bogus_option => :bogus_value
14
+ has_one :sitemap, :class_name => 'blog_site_map', :bogus_option => :bogus_value
40
15
  end
16
+ class ::Homepage < LucidWorks::Base ; end
17
+ class ::Calendar < LucidWorks::Base ; end
18
+ class ::BlogSiteMap < LucidWorks::Base ; end
41
19
 
42
- describe "#<child!>" do
43
- it "should build a new model, and not call REST API to retrieve" do
44
- LaunchParty.should_not_receive(:find)
45
-
46
- launch_party = @blog.launch_party!
20
+ @blog = ::Blog.new(:parent => @server)
21
+ end
47
22
 
48
- launch_party.should be_a(LaunchParty)
49
- launch_party.should be_persisted # All singletons are always persisted
50
- end
23
+ describe "#resource" do
24
+ it "should create a HasOne association proxy for the specified class" do
25
+ LucidWorks::Associations::HasOne.should_receive(:new).
26
+ with(@blog, Homepage, {})
27
+ @blog.homepage
51
28
  end
52
29
  end
53
30
 
54
- context "with content" do
55
- describe "#<child>!" do
56
- it "should call Child.find" do
57
- Homepage.should_receive(:find).with(:parent => @blog)
58
- @blog.homepage!
59
- end
31
+ describe "#build_resource" do
32
+ it "should call .resource.build" do
33
+ mock_association = double("has_one association")
34
+ @blog.should_receive(:homepage).and_return(mock_association)
35
+ mock_association.should_receive(:build).with(:foo => :bar)
36
+ @blog.build_homepage(:foo => :bar)
60
37
  end
38
+ end
61
39
 
62
- describe "#<child>" do
63
- it "should call child! the first time then return the cached value thereafter" do
64
- mock_homepage = double('homepage')
65
- Homepage.should_receive(:find).once.and_return(mock_homepage)
66
- @blog.homepage.should == mock_homepage
67
- @blog.homepage.should == mock_homepage
68
- end
40
+ describe "#create_resource" do
41
+ it "should call .resource.create" do
42
+ mock_association = double("has_one association")
43
+ @blog.should_receive(:homepage).and_return(mock_association)
44
+ mock_association.should_receive(:create).with(:foo => :bar)
45
+ @blog.create_homepage(:foo => :bar)
69
46
  end
47
+ end
70
48
 
71
- describe "#build_<child>" do
72
- it "should create a new child with persisted = true" do
73
- homepage = @blog.build_homepage
74
- homepage.should be_a(Homepage)
75
- homepage.should be_persisted
76
- end
77
- end
49
+ it "should pass thru options to the proxy" do
50
+ LucidWorks::Associations::HasOne.should_receive(:new).
51
+ with(@blog, Calendar, :bogus_option => :bogus_value)
52
+ @blog.calendar
78
53
  end
79
- end
80
54
 
81
- describe ".has_singleton" do
82
- describe "#<child>" do
83
- it "should create a new child with persisted = true" do
84
- end
55
+ it "should respect the :class_name option" do
56
+ LucidWorks::Associations::HasOne.should_receive(:new).
57
+ with(@blog, BlogSiteMap, :bogus_option => :bogus_value)
58
+ @blog.sitemap
85
59
  end
86
60
  end
87
61
 
88
62
  describe ".has_many" do
89
- describe "#<children>!" do
90
- it "should call Child.all" do
91
- Post.should_receive(:all).with(:parent => @blog)
92
- @blog.posts!
63
+ before :all do
64
+ class ::Blog < LucidWorks::Base
65
+ has_many :posts
93
66
  end
94
- end
95
67
 
96
- describe "#<children>" do
97
- it "should call children! the first time then return the cached value thereafter" do
98
- mock_posts = double('some posts')
99
- Post.should_receive(:find).once.and_return(mock_posts)
100
- @blog.posts.should == mock_posts
101
- @blog.posts.should == mock_posts
68
+ class ::Post < LucidWorks::Base
69
+ belongs_to :blog
102
70
  end
103
71
  end
104
72
 
105
- describe "#<child>" do
106
- it "should call Child.find" do
107
- Post.should_receive(:find).with('child_id', :parent => @blog)
108
- @blog.post('child_id')
109
- end
73
+ before :each do
74
+ @blog = ::Blog.new(:parent => @server)
110
75
  end
111
76
 
112
- describe "create_<child>" do
113
- it "should call Child.create" do
114
- Post.should_receive(:create).with(:name => 'child_name', :parent => @blog)
115
- @blog.create_post(:name => 'child_name')
77
+ describe "#resources" do
78
+ it "should create a HasMany association proxy for the specified class" do
79
+ mock_proxy = double('hasmany proxy', :find => nil)
80
+ LucidWorks::Associations::HasMany.should_receive(:new).
81
+ with(@blog, Post, {}).
82
+ and_return(mock_proxy)
83
+ @blog.posts
116
84
  end
117
85
  end
118
86
 
119
- describe "#build_<child>" do
120
- it "should create a new child with persisted = true" do
121
- post = @blog.build_post
122
- post.should be_a(Post)
123
- post.should_not be_persisted
87
+ describe "#resource" do
88
+ it "should call Resource.find" do
89
+ mock_proxy = double('hasmany proxy', :first => nil)
90
+ Post.should_receive(:find).with('child_id', :parent => @blog).and_return(mock_proxy)
91
+ @blog.post('child_id').first
124
92
  end
125
93
  end
126
94
  end
127
95
 
128
96
  describe ".belongs_to" do
97
+ before :all do
98
+ class ::Blog < LucidWorks::Base
99
+ has_many :posts # Same as above - keep
100
+ end
101
+ class ::Post < LucidWorks::Base
102
+ belongs_to :blog
103
+ end
104
+ end
105
+
106
+ before :each do
107
+ @blog = ::Blog.new(:parent => @server)
108
+ end
129
109
 
130
110
  describe ".belongs_to_association_name (private)" do
131
111
  it "should return the name of the association" do
@@ -16,6 +16,7 @@ describe LucidWorks::Base do
16
16
 
17
17
  class Widget < LucidWorks::Base
18
18
  has_one :singleton_widget
19
+ has_one :singleton_widget_that_supports_all, :supports_all => true
19
20
  end
20
21
 
21
22
  class SingletonWidget < LucidWorks::Base
@@ -226,18 +227,36 @@ describe LucidWorks::Base do
226
227
 
227
228
  describe ":include option" do
228
229
  context "with a single argument" do
229
- it "should retrieve the :included submodel" do
230
- RestClient.should_receive(:get).with("#{@fake_server_uri}/widgets").and_return(WIDGETS_JSON)
231
- mock_singleton1 = double('singleton widget1')
232
- mock_singleton2 = double('singleton widget2')
233
- mock_singleton3 = double('singleton widget3')
234
- SingletonWidget.should_receive(:find).and_return(mock_singleton1, mock_singleton2, mock_singleton3)
230
+ context "for a subresource that does not support /all" do
231
+ it "should retrieve the :included submodel by retrieving them individually" do
232
+ RestClient.should_receive(:get).with("#{@fake_server_uri}/widgets").and_return(WIDGETS_JSON)
233
+ mock_singleton1 = double('singleton widget1')
234
+ mock_singleton2 = double('singleton widget2')
235
+ mock_singleton3 = double('singleton widget3')
236
+ SingletonWidget.should_receive(:find).and_return(mock_singleton1, mock_singleton2, mock_singleton3)
237
+
238
+ widgets = Widget.find(:all, :include => :singleton_widget, :parent => @server)
239
+
240
+ widgets[0].singleton_widget.should == mock_singleton1
241
+ widgets[1].singleton_widget.should == mock_singleton2
242
+ widgets[2].singleton_widget.should == mock_singleton3
243
+ end
244
+ end
235
245
 
236
- widgets = Widget.find(:all, :include => :singleton_widget, :parent => @server)
246
+ context "for a subresource that supports /all" do
247
+ it "should retrieve the :included submodel by using /submodel/all" do
248
+ RestClient.should_receive(:get).with("#{@fake_server_uri}/widgets").and_return(WIDGETS_JSON)
249
+ mock_singleton1 = double('singleton widget1')
250
+ mock_singleton2 = double('singleton widget2')
251
+ mock_singleton3 = double('singleton widget3')
252
+ SingletonWidget.should_receive(:find).and_return(mock_singleton1, mock_singleton2, mock_singleton3)
237
253
 
238
- widgets[0].singleton_widget.should == mock_singleton1
239
- widgets[1].singleton_widget.should == mock_singleton2
240
- widgets[2].singleton_widget.should == mock_singleton3
254
+ widgets = Widget.find(:all, :include => :singleton_widget, :parent => @server)
255
+
256
+ widgets[0].singleton_widget.should == mock_singleton1
257
+ widgets[1].singleton_widget.should == mock_singleton2
258
+ widgets[2].singleton_widget.should == mock_singleton3
259
+ end
241
260
  end
242
261
  end
243
262
 
@@ -253,9 +272,9 @@ describe LucidWorks::Base do
253
272
  mock_singleton1 = double('singleton widget1')
254
273
  mock_singleton2 = double('singleton widget2')
255
274
  mock_singleton3 = double('singleton widget3')
256
- mock_other_widgets_1 = double('other widgets 1')
257
- mock_other_widgets_2 = double('other widgets 2')
258
- mock_other_widgets_3 = double('other widgets 3')
275
+ mock_other_widgets_1 = double('other widgets 1', :supports_all? => false)
276
+ mock_other_widgets_2 = double('other widgets 2', :supports_all? => false)
277
+ mock_other_widgets_3 = double('other widgets 3', :supports_all? => false)
259
278
  SingletonWidget.should_receive(:find).and_return(mock_singleton1, mock_singleton2, mock_singleton3)
260
279
  OtherWidget.should_receive(:find).and_return(mock_other_widgets_1, mock_other_widgets_2, mock_other_widgets_3)
261
280
 
@@ -250,7 +250,7 @@ describe LucidWorks::Collection do
250
250
 
251
251
  describe "#empty!" do
252
252
  before do
253
- @collection = @server.collections.first
253
+ @collection = @server.collections!.first
254
254
  end
255
255
 
256
256
  it "should DELETE /api/collections/<collection_id>/index" do
@@ -222,7 +222,7 @@ describe LucidWorks::Datasource do
222
222
  end
223
223
  end
224
224
 
225
- describe "#index" do
225
+ describe "#build_index" do
226
226
  it "should return a new LucidWorks::Datasource::Index for this datasource" do
227
227
  @datasource = LucidWorks::Datasource.create(
228
228
  :collection => @collection,
@@ -234,7 +234,7 @@ describe LucidWorks::Datasource do
234
234
  )
235
235
  @datasource.should be_valid
236
236
 
237
- index = @datasource.index
237
+ index = @datasource.build_index
238
238
  index.should be_a(LucidWorks::Datasource::Index)
239
239
  index.should be_persisted # special case - singletons are always considered persisted
240
240
  end
@@ -253,9 +253,9 @@ describe LucidWorks::Datasource do
253
253
  @datasource.should be_valid
254
254
  end
255
255
 
256
- describe "#job" do
256
+ describe "#build_job" do
257
257
  it "should return a new LucidWorks::Datasource::Job for this datasource" do
258
- job = @datasource.job
258
+ job = @datasource.build_job
259
259
  job.should be_a(LucidWorks::Datasource::Job)
260
260
  job.should be_persisted # special case - singletons are always considered persisted
261
261
  end
@@ -29,9 +29,12 @@ describe LucidWorks::Server do
29
29
  end
30
30
 
31
31
  describe "#collections" do
32
-
33
- it "should call Collection.get and pass the server" do
34
- LucidWorks::Collection.should_receive(:all).with(:parent => @server)
32
+ it "should create a HasMany association proxy for the specified class" do
33
+ mock_proxy = double('has_many proxy', :find => nil)
34
+ LucidWorks::Associations::HasMany.should_receive(:new).
35
+ with(@server, LucidWorks::Collection, {}).
36
+ and_return(mock_proxy)
37
+
35
38
  @server.collections
36
39
  end
37
40
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: lucid_works
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.5.3
5
+ version: 0.6.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Sam Pierson
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-04-15 00:00:00 -07:00
13
+ date: 2011-04-25 00:00:00 -07:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -101,6 +101,9 @@ files:
101
101
  - config/locales/en.yml
102
102
  - lib/lucid_works.rb
103
103
  - lib/lucid_works/associations.rb
104
+ - lib/lucid_works/associations/has_many.rb
105
+ - lib/lucid_works/associations/has_one.rb
106
+ - lib/lucid_works/associations/proxy.rb
104
107
  - lib/lucid_works/base.rb
105
108
  - lib/lucid_works/collection.rb
106
109
  - lib/lucid_works/collection/activity.rb
@@ -131,6 +134,8 @@ files:
131
134
  - lib/lucid_works/utils.rb
132
135
  - lib/lucid_works/version.rb
133
136
  - lucid_works.gemspec
137
+ - spec/lib/lucid_works/associations/has_many_spec.rb
138
+ - spec/lib/lucid_works/associations/has_one_spec.rb
134
139
  - spec/lib/lucid_works/associations_spec.rb
135
140
  - spec/lib/lucid_works/base_spec.rb
136
141
  - spec/lib/lucid_works/collection/activity/history_spec.rb
@@ -176,6 +181,8 @@ signing_key:
176
181
  specification_version: 3
177
182
  summary: Ruby wrapper for the LucidWorks REST API
178
183
  test_files:
184
+ - spec/lib/lucid_works/associations/has_many_spec.rb
185
+ - spec/lib/lucid_works/associations/has_one_spec.rb
179
186
  - spec/lib/lucid_works/associations_spec.rb
180
187
  - spec/lib/lucid_works/base_spec.rb
181
188
  - spec/lib/lucid_works/collection/activity/history_spec.rb