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 +1 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +11 -0
- data/Rakefile +7 -0
- data/lib/lucid_works.rb +26 -0
- data/lib/lucid_works/associations.rb +118 -0
- data/lib/lucid_works/base.rb +274 -0
- data/lib/lucid_works/collection.rb +14 -0
- data/lib/lucid_works/collection/index.rb +9 -0
- data/lib/lucid_works/collection/info.rb +9 -0
- data/lib/lucid_works/collection/settings.rb +9 -0
- data/lib/lucid_works/datasource.rb +40 -0
- data/lib/lucid_works/datasource/history.rb +17 -0
- data/lib/lucid_works/datasource/index.rb +9 -0
- data/lib/lucid_works/datasource/schedule.rb +9 -0
- data/lib/lucid_works/datasource/status.rb +9 -0
- data/lib/lucid_works/exceptions.rb +6 -0
- data/lib/lucid_works/patch_restclient.rb +29 -0
- data/lib/lucid_works/server.rb +23 -0
- data/lib/lucid_works/version.rb +3 -0
- data/lucid_works.gemspec +26 -0
- data/spec/lib/lucid_works/associations_spec.rb +130 -0
- data/spec/lib/lucid_works/base_spec.rb +399 -0
- data/spec/lib/lucid_works/collection_spec.rb +231 -0
- data/spec/lib/lucid_works/datasource_spec.rb +280 -0
- data/spec/lib/lucid_works/server_spec.rb +39 -0
- data/spec/spec_helper.rb +47 -0
- metadata +133 -0
data/.autotest
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Autotest.add_discovery { "rspec2" }
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.2@lucidworks
|
data/Gemfile
ADDED
data/Rakefile
ADDED
data/lib/lucid_works.rb
ADDED
@@ -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
|