lucid_works 0.1.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/.autotest ADDED
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .idea
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.2@lucidworks
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in lucid_works.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'ruby-debug19'
8
+ gem 'rspec'
9
+ gem 'autotest'
10
+ gem "autotest-fsevent"
11
+ end
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ desc "Create RDoc documentation"
5
+ task :doc do
6
+ system 'rdoc'
7
+ end
@@ -0,0 +1,26 @@
1
+ module LucidWorks
2
+ end
3
+
4
+ require 'active_model'
5
+ require 'active_support/core_ext/module/attr_accessor_with_default'
6
+ require 'active_support/core_ext/hash/indifferent_access'
7
+ require 'active_support/inflector'
8
+ require 'restclient'
9
+ require 'json'
10
+
11
+ require 'lucid_works/patch_restclient'
12
+
13
+ require 'lucid_works/exceptions'
14
+ require 'lucid_works/associations'
15
+ require 'lucid_works/server'
16
+ require 'lucid_works/base'
17
+
18
+ require 'lucid_works/collection'
19
+ require 'lucid_works/collection/info'
20
+ require 'lucid_works/collection/index'
21
+ require 'lucid_works/collection/settings'
22
+ require 'lucid_works/datasource'
23
+ require 'lucid_works/datasource/index'
24
+ require 'lucid_works/datasource/status'
25
+ require 'lucid_works/datasource/history'
26
+ require 'lucid_works/datasource/schedule'
@@ -0,0 +1,118 @@
1
+ module LucidWorks
2
+
3
+ module Associations
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+
8
+ # Specifies a singleton child resource.
9
+ #
10
+ # In the parent resource creates methods:
11
+ # child - load and cache the child. Subsequent calls will access the cached value.
12
+ # child! - load and cache the child, ignoring existing cached value if present.
13
+ #
14
+ # === Options
15
+ #
16
+ # The declaration can also include an options hash to specialize the behavior of the association.
17
+ #
18
+ # Options are:
19
+ # [:class_name]
20
+ # Specify the class name of the association. Use it only if you want to us a child class name different
21
+ # from the association name, e.g.
22
+ # has_one :info, :class_name => :collection_info # use CollectionInfo class
23
+ # has_one :foo, :class_name => :'foo/bar' # use Foo::Bar class
24
+ #
25
+ def has_one(*arguments)
26
+ options = arguments.last.is_a?(Hash) ? arguments.pop : {}
27
+ arguments.each do |resource|
28
+ define_has_one resource, options
29
+ end
30
+ end
31
+
32
+ #
33
+ # Specifies a child resource.
34
+ #
35
+ # e.g. for Blog has_many posts
36
+ #
37
+ # In the parent resources creates methods:
38
+ #
39
+ # posts
40
+ # post(id)
41
+ #
42
+ def has_many(resources, options = {})
43
+ resource = resources.to_s.singularize
44
+ resource_class_name = (options[:class_name] || resource).to_s.classify
45
+
46
+ class_eval <<-EOF, __FILE__, __LINE__ + 1
47
+ def #{resources}(options={})
48
+ @#{resources} || #{resources}!
49
+ end
50
+
51
+ def #{resources}!(options={})
52
+ @#{resources} = #{resource_class_name}.all(options.merge :parent => self)
53
+ end
54
+
55
+ def #{resource}(id, options={})
56
+ #{resource_class_name}.find(id, options.merge(:parent => self))
57
+ end
58
+
59
+ def create_#{resource}(options = {})
60
+ #{resource_class_name}.create(options.merge :parent => self)
61
+ end
62
+
63
+ def build_#{resource}(options = {})
64
+ #{resource_class_name}.new(options.merge :parent => self)
65
+ end
66
+ EOF
67
+ end
68
+
69
+ # Specified a parent resource.
70
+ # In the child resource creates:
71
+ # e.g. for Post belongs_to Blog
72
+ # Class methods:
73
+ # parent_class => Blog
74
+ #
75
+ # Instance methods:
76
+ # blog
77
+ #
78
+ def belongs_to(parent_resource, options = {})
79
+ parent_resource_class_name = (options[:class_name] || parent_resource).to_s.classify
80
+
81
+ class_eval <<-EOF, __FILE__, __LINE__ + 1
82
+ def self.parent_class # def self.parent_class
83
+ #{parent_resource_class_name} # Blog
84
+ end # end
85
+
86
+ def #{parent_resource} # def blog
87
+ parent # parent
88
+ end # end
89
+
90
+ private
91
+
92
+ def self.belongs_to_association_name
93
+ :'#{parent_resource}'
94
+ end
95
+ EOF
96
+ end
97
+
98
+ private
99
+
100
+ def define_has_one(resource, options={})
101
+ resource_class_name = (options[:class_name] || resource).to_s.camelize
102
+ class_eval <<-EOF, __FILE__, __LINE__ + 1
103
+ def #{resource} # def resource
104
+ @#{resource} || #{resource}! # @resource || resource!
105
+ end # end
106
+
107
+ def #{resource}! # def resource!
108
+ @#{resource} = #{resource_class_name}.find(:parent => self) # @resource = Resource.find(:parent => self)
109
+ end # end
110
+
111
+ def build_#{resource}(options = {})
112
+ #{resource_class_name}.new(options.merge :parent => self)
113
+ end
114
+ EOF
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,274 @@
1
+ module LucidWorks
2
+
3
+ # LucidWorks::Base is our REST ORM foundation.
4
+ #
5
+ # The motivaiton for developing it was:
6
+ # * ActiveResource makes a lot of assumtions about how REST APIs should work that we had to keep patching it for
7
+ # (e.g. parameters must always be nested inside :resouce => {}).
8
+ #
9
+ # * ActiveResource was missing some features we needed:
10
+ # - send new id with POST
11
+ # - associations
12
+ # - multipart POST
13
+ #
14
+ # * The straw that broke the camel's back was the need to talk to the same type of REST API on multiple servers
15
+ # simultaneously, which was so orthogonal to the design of ActiveResource that patching it to support this would
16
+ # require a major re-write.
17
+ #
18
+ # So, I decided to marry ActiveModel and RestClient.
19
+ #
20
+ # Much of the internal structure here (method names etc) will resemble ActiveResource as I have spent much time
21
+ # wandering through that code.
22
+
23
+ class Base
24
+ include ActiveModel::Validations
25
+ include ActiveModel::Conversion
26
+ extend ActiveModel::Naming
27
+
28
+ include Associations
29
+
30
+ attr_accessor :parent # :nodoc:
31
+ attr_writer :id # :nodoc:
32
+ attr_writer :persisted # :nodoc:
33
+ attr_accessor :raw_response # :nodoc:
34
+ attr_accessor :response_data # :nodoc:
35
+
36
+ class << self
37
+ attr_accessor_with_default :primary_key, :id
38
+ attr_accessor :collection_name
39
+ attr_accessor_with_default :singleton, false
40
+
41
+ def create(*arguments)
42
+ new(*arguments).tap { |model| model.save }
43
+ end
44
+
45
+ # Retrieve one or more models from the server.
46
+ #
47
+ # Find may be called in the following ways:
48
+ #
49
+ # Retrieve an entire collection:
50
+ # find(:all, options)
51
+ #
52
+ # Retrieve a single model
53
+ # find(id, options)
54
+ # find(:one, id, options)
55
+ #
56
+ # Retrieve a singleton model
57
+ # find(options)
58
+ # find(:singleton, options)
59
+ #
60
+ # == Options
61
+ #
62
+ # :parent - mandatory, another LucidWorks::Base instance or a LucidWorks::Server instance.
63
+ #
64
+ def find(*arguments)
65
+ unless arguments.first.is_a?(Symbol)
66
+ # We weren't called with a symbol, figure out what kind of find this is and re-call
67
+ if singleton
68
+ raise ArgumentError.new("wrong number of arguments (#{arguments.size} for 1)") unless arguments.size == 1
69
+ return find(:singleton, *arguments)
70
+ else
71
+ raise ArgumentError.new("wrong number of arguments (#{arguments.size} for 2)") unless arguments.size == 2
72
+ return find(:one, *arguments)
73
+ end
74
+ end
75
+
76
+ kind_of_find = arguments.shift
77
+
78
+ if kind_of_find == :one
79
+ id = arguments.shift
80
+ options = arguments.shift
81
+ raise ArgumentError.new("find(:one) requires 3 arguments") if arguments.count > 0
82
+ else
83
+ options = arguments.first
84
+ end
85
+
86
+ parent = extract_parent_from_options(options)
87
+ includes = options.delete(:include)
88
+
89
+ url = case kind_of_find
90
+ when :all; collection_url(parent)
91
+ when :one; "#{parent.uri}/#{collection_name}/#{id}"
92
+ when :singleton; "#{parent.uri}/#{singleton_name}"
93
+ end
94
+
95
+ raw_response = RestClient.get(url)
96
+ data = JSON.parse(raw_response)
97
+
98
+ results =
99
+ if kind_of_find == :all
100
+ data.collect do |collection_attributes|
101
+ new(collection_attributes.merge(:parent => parent, :persisted => true))
102
+ end
103
+ else
104
+ attributes = data.is_a?(Hash) ? data : {}
105
+ attributes.merge!(:parent => parent, :persisted => true)
106
+ object = new(attributes)
107
+ object.raw_response = raw_response
108
+ object.response_data = data
109
+ object
110
+ end
111
+
112
+ # Process :include options
113
+ # Pedestrian version first: step through all results and pull in submodel
114
+ if includes
115
+ [results].flatten.each do |model|
116
+ [includes].flatten.each do |include|
117
+ model.send(include)
118
+ end
119
+ end
120
+ end
121
+
122
+ results
123
+ end
124
+
125
+ def all(options)
126
+ find(:all, options)
127
+ end
128
+
129
+ def first(options)
130
+ find(:all, options).first
131
+ end
132
+
133
+ def last(options)
134
+ find(:all, options).last
135
+ end
136
+
137
+ def collection_name # :nodoc:
138
+ @collection_name || name.underscore.gsub(/^.*\//, '').pluralize
139
+ end
140
+
141
+ def collection_url(parent) # :nodoc:
142
+ "#{parent.uri}/#{collection_name}"
143
+ end
144
+
145
+ def singleton_name # :nodoc:
146
+ name.underscore.gsub(/^.*\//, '')
147
+ end
148
+
149
+ def extract_parent_from_options(options) # :nodoc:
150
+ # When this resource belongs_to another, allow the other's name as a key instead of :parent
151
+ if respond_to?(:belongs_to_association_name) && options.has_key?(belongs_to_association_name)
152
+ parent = options.delete(belongs_to_association_name)
153
+ else
154
+ parent = options.delete(:parent)
155
+ end
156
+ raise ArgumentError.new("parent is a required option") unless parent
157
+ unless parent.is_a?(Base) || parent.is_a?(Server)
158
+ raise ArgumentError.new("parent must be a LucidWorks::Server or LucidWorks::Base")
159
+ end
160
+ parent
161
+ end
162
+
163
+ end # class methods
164
+
165
+ def initialize(options)
166
+ raise ArgumentError.new("new requires a Hash") unless options.is_a?(Hash)
167
+ @parent = self.class.extract_parent_from_options(options)
168
+ @persisted = options.delete(:persisted) || singleton? || false
169
+ @attributes = options.with_indifferent_access
170
+ end
171
+
172
+ def save
173
+ if valid?
174
+ begin
175
+ if persisted?
176
+ response = RestClient.put(member_url, encode, :content_type => :json)
177
+ else
178
+ response = RestClient.post(collection_url, encode, :content_type => :json)
179
+ @persisted = true
180
+ end
181
+ load_attributes_from_json_string(response)
182
+ true
183
+ rescue RestClient::UnprocessableEntity, RestClient::Conflict => e
184
+ attach_errors_to_model(e.response)
185
+ false
186
+ end
187
+ end
188
+ end
189
+
190
+ def destroy(options={})
191
+ RestClient.delete(member_url, options)
192
+ end
193
+
194
+ def id # :nodoc:
195
+ @attributes[self.class.primary_key]
196
+ end
197
+
198
+ def id=(value) # :nodoc:
199
+ @attributes[self.class.primary_key] = value
200
+ end
201
+
202
+ def persisted?
203
+ @persisted
204
+ end
205
+
206
+ def method_missing(method_sym, *arguments) # :nodoc:
207
+ return super if method_sym == :to_ary
208
+ if method_sym.to_s =~ /^(\w+)=$/
209
+ return @attributes[$1] = arguments.first
210
+ elsif method_sym.to_s =~ /^(\w+)\?$/
211
+ attr = $1
212
+ predicate = true
213
+ else
214
+ attr = method_sym
215
+ predicate = false
216
+ end
217
+ raise "Unknown attribute: '#{attr}'" unless @attributes.has_key?(attr)
218
+ predicate ? !!@attributes[attr] : @attributes[attr]
219
+ end
220
+
221
+ def read_attribute_for_validation(key) # :nodoc:
222
+ @attributes[key]
223
+ end
224
+
225
+ def collection_url # :nodoc:
226
+ self.class.collection_url(parent)
227
+ end
228
+
229
+ def member_url # :nodoc:
230
+ if singleton?
231
+ "#{parent.uri}/#{self.class.singleton_name}"
232
+ else
233
+ "#{parent.uri}/#{collection_name}/#{self.id}"
234
+ end
235
+ end
236
+
237
+ alias :uri :member_url
238
+
239
+ def inspect
240
+ "<#{self.class.name} " + @attributes.map { |k,v| "#{k}=#{v.inspect}" }.join(" ") + ">"
241
+ end
242
+
243
+ private
244
+
245
+ def singleton? # :nodoc:
246
+ self.class.singleton
247
+ end
248
+
249
+ def collection_name # :nodoc:
250
+ self.class.collection_name
251
+ end
252
+
253
+ def encode # :nodoc:
254
+ @attributes.reject { |k,v| k.to_s == 'id'}.to_json
255
+ end
256
+
257
+ def load_attributes_from_json_string(response) # :nodoc:
258
+ data = JSON.parse(response) rescue {}
259
+ data.each do |k,v|
260
+ @attributes[k] = v
261
+ end
262
+ end
263
+
264
+ def attach_errors_to_model(response) # :nodoc:
265
+ data = JSON.parse(response) rescue nil
266
+ if data.is_a?(Hash) && data['errors']
267
+ data['errors'].each do |error|
268
+ key = error['key'].blank? ? 'base' : error['key']
269
+ self.errors.add(key, error['message'])
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end