lucid_works 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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