lucid_works 0.1.1

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