rrrmatey 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.md +16 -0
- data/README.md +249 -0
- data/RELEASE_NOTES.md +26 -0
- data/Rakefile +39 -0
- data/lib/rrrmatey.rb +8 -0
- data/lib/rrrmatey/crud_controller.rb +97 -0
- data/lib/rrrmatey/discrete_result.rb +31 -0
- data/lib/rrrmatey/errors.rb +11 -0
- data/lib/rrrmatey/retryable.rb +28 -0
- data/lib/rrrmatey/string_model/connection_methods.rb +47 -0
- data/lib/rrrmatey/string_model/consumer_adapter_methods.rb +9 -0
- data/lib/rrrmatey/string_model/delete_methods.rb +12 -0
- data/lib/rrrmatey/string_model/field_definition_methods.rb +101 -0
- data/lib/rrrmatey/string_model/find_methods.rb +85 -0
- data/lib/rrrmatey/string_model/index_methods.rb +90 -0
- data/lib/rrrmatey/string_model/namespaced_key_methods.rb +21 -0
- data/lib/rrrmatey/string_model/string_model.rb +92 -0
- data/lib/rrrmatey/type_coercion.rb +61 -0
- data/lib/rrrmatey/version.rb +3 -0
- data/spec/rrrmatey/crud_controller/crud_controller_spec.rb +258 -0
- data/spec/rrrmatey/crud_controller/model_methods_spec.rb +26 -0
- data/spec/rrrmatey/discrete_result_spec.rb +104 -0
- data/spec/rrrmatey/retryable_spec.rb +95 -0
- data/spec/rrrmatey/string_model/connection_methods_spec.rb +64 -0
- data/spec/rrrmatey/string_model/consumer_adapter_methods_spec.rb +43 -0
- data/spec/rrrmatey/string_model/delete_methods_spec.rb +36 -0
- data/spec/rrrmatey/string_model/field_definition_methods_spec.rb +51 -0
- data/spec/rrrmatey/string_model/find_methods_spec.rb +147 -0
- data/spec/rrrmatey/string_model/index_methods_spec.rb +231 -0
- data/spec/rrrmatey/string_model/namespaced_key_methods_spec.rb +34 -0
- data/spec/rrrmatey/string_model/string_model_spec.rb +208 -0
- data/spec/spec_helper.rb +16 -0
- metadata +148 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
module RRRMatey
|
2
|
+
module StringModel
|
3
|
+
require 'json'
|
4
|
+
require 'xmlsimple'
|
5
|
+
require_relative 'connection_methods'
|
6
|
+
require_relative 'namespaced_key_methods'
|
7
|
+
require_relative 'field_definition_methods'
|
8
|
+
require_relative 'index_methods'
|
9
|
+
require_relative 'find_methods'
|
10
|
+
require_relative 'delete_methods'
|
11
|
+
require_relative 'consumer_adapter_methods'
|
12
|
+
|
13
|
+
def self.included(base)
|
14
|
+
base.extend ConnectionMethods
|
15
|
+
base.extend NamespacedKeyMethods
|
16
|
+
base.extend FieldDefinitionMethods
|
17
|
+
base.extend IndexMethods
|
18
|
+
base.extend FindMethods
|
19
|
+
base.extend DeleteMethods
|
20
|
+
base.extend ConsumerAdapterMethods
|
21
|
+
end
|
22
|
+
|
23
|
+
self.extend ConnectionMethods
|
24
|
+
|
25
|
+
attr_accessor :content_type, :id
|
26
|
+
def content_type()
|
27
|
+
@content_type || 'application/json'
|
28
|
+
end
|
29
|
+
|
30
|
+
def save()
|
31
|
+
raise UnsupportedTypeError.new(content_type) if content_type == 'application/unknown'
|
32
|
+
raise InvalidModelError if id.blank?
|
33
|
+
raise InvalidModelError if has_valid? && !valid?
|
34
|
+
h = to_hash()
|
35
|
+
s = hash_to_typed_string(h)
|
36
|
+
unless self.class.cache_proxy.nil?
|
37
|
+
self.class.cache_proxy.with { |r|
|
38
|
+
r.set(self.class.namespaced_key(id), s)
|
39
|
+
}
|
40
|
+
end
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete()
|
45
|
+
self.class.delete(id)
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_json(opts = {})
|
49
|
+
to_consumer_hash.to_json
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_xml(opts = {})
|
53
|
+
to_consumer_hash.to_xml(:root => self.class.item_name)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def to_consumer_hash
|
59
|
+
h = {
|
60
|
+
'id' => id
|
61
|
+
}
|
62
|
+
self.class.consumer_fields.each do |f|
|
63
|
+
h[f] = send(f.to_sym)
|
64
|
+
end
|
65
|
+
h
|
66
|
+
end
|
67
|
+
|
68
|
+
def has_valid?
|
69
|
+
@has_valid ||= respond_to?(:valid?)
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_hash()
|
73
|
+
h = {}
|
74
|
+
unless self.class.fields.nil?
|
75
|
+
self.class.fields.each {|f| h[f.to_s] = send(f) }
|
76
|
+
end
|
77
|
+
h
|
78
|
+
end
|
79
|
+
|
80
|
+
def hash_to_typed_string(h)
|
81
|
+
case content_type
|
82
|
+
when 'application/json'
|
83
|
+
{ self.class.item_name => h }.to_json
|
84
|
+
when 'application/xml'
|
85
|
+
h.to_xml(:root => self.class.item_name, :skip_instruct => true,
|
86
|
+
:indent => 0)
|
87
|
+
else
|
88
|
+
raise UnknownContentTypeError
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class String
|
2
|
+
def to_fixnum_to_date
|
3
|
+
to_i.to_date
|
4
|
+
end
|
5
|
+
|
6
|
+
unless respond_to?(:underscore)
|
7
|
+
def underscore
|
8
|
+
self.gsub(/::/, '/').
|
9
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
10
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
11
|
+
tr("- ", "_").
|
12
|
+
downcase
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
unless respond_to?(:pluralize)
|
17
|
+
def pluralize
|
18
|
+
self + "s"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
unless respond_to?(:constantize)
|
23
|
+
def constantize
|
24
|
+
Module.const_get(self)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Hash
|
30
|
+
unless respond_to?(:to_xml)
|
31
|
+
def to_xml(opts = {})
|
32
|
+
root_name = opts[:root] || 'root'
|
33
|
+
self.keys.each { |k| self[k] = 'null' if self[k].nil? }
|
34
|
+
XmlSimple.xml_out(self, :root_name => root_name)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
unless self.class.respond_to?(:from_xml)
|
39
|
+
def self.from_xml(s, opts={})
|
40
|
+
XmlSimple.xml_in(s, :force_array => false)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Fixnum
|
46
|
+
def to_date
|
47
|
+
Time.at(self).to_datetime
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class DateTime
|
52
|
+
def seconds_since_epoch
|
53
|
+
to_time.to_i
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Date
|
58
|
+
def seconds_since_epoch
|
59
|
+
to_date.to_time.to_i
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Model
|
4
|
+
attr_accessor :id, :content_type
|
5
|
+
attr_accessor :name
|
6
|
+
attr_accessor :name_s
|
7
|
+
|
8
|
+
def initialize(opts = {})
|
9
|
+
opts.each {|k,v| send("#{k}=".to_sym, v) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.list(offset = 0, limit = 20)
|
13
|
+
results = offset.upto(offset + limit - 1).map do |i|
|
14
|
+
factory_by_i(i)
|
15
|
+
end
|
16
|
+
RRRMatey::DiscreteResult.new(:results => results,
|
17
|
+
:length => 420,
|
18
|
+
:offset => offset,
|
19
|
+
:discrete_length => limit)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.get(id)
|
23
|
+
return if id == 'dne'
|
24
|
+
factory_by_id(id, "enam")
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.delete(id)
|
28
|
+
return 0 if id.nil?
|
29
|
+
1
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def self.factory_by_i(i)
|
35
|
+
factory_by_id("di#{i}", "#eman#{i}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.factory_by_id(id, name)
|
39
|
+
Model.new(:id => id, :name => name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class ModelsController
|
44
|
+
include RRRMatey::CrudController
|
45
|
+
|
46
|
+
attr_accessor :params
|
47
|
+
|
48
|
+
# override respond_ methods to make testable
|
49
|
+
def respond_bad_request
|
50
|
+
{ :status => 400, :content_type => 'application/json', :body => nil }
|
51
|
+
end
|
52
|
+
|
53
|
+
def respond_not_found
|
54
|
+
{ :status => 404, :content_type => 'application/json', :body => nil }
|
55
|
+
end
|
56
|
+
|
57
|
+
def respond_internal_server_error(e)
|
58
|
+
{ :status => 400, :content_type => 'application/json', :body => { :message => e.message } }
|
59
|
+
end
|
60
|
+
|
61
|
+
def respond_ok(item)
|
62
|
+
{ :status => 200, :content_type => 'application/json', :body => item }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe RRRMatey::CrudController do
|
67
|
+
let(:kontroller_modyule) { RRRMatey::CrudController }
|
68
|
+
let(:kontroller_klass) { ModelsController }
|
69
|
+
let(:kontroller) { kontroller_klass.new }
|
70
|
+
|
71
|
+
describe '#included' do
|
72
|
+
it 'included extends ModelMethods' do
|
73
|
+
expect(extends_all_instance_methods(kontroller_klass, RRRMatey::CrudController::ModelMethods)).to eq(true)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#index' do
|
78
|
+
let(:params) { { :offset => 40, :limit => 2 } }
|
79
|
+
let(:kontroller) {
|
80
|
+
k = kontroller_klass.new
|
81
|
+
k.params = params
|
82
|
+
k
|
83
|
+
}
|
84
|
+
let(:response) {
|
85
|
+
kontroller.index()
|
86
|
+
}
|
87
|
+
let(:status) { response[:status] }
|
88
|
+
let(:content_type) { response[:content_type] }
|
89
|
+
let(:body) { response[:body] }
|
90
|
+
|
91
|
+
it 'responds with an :ok status' do
|
92
|
+
expect(status).to eq(200)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'responds with a json content_type' do
|
96
|
+
expect(content_type).to eq('application/json')
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'responds with the specifid offset' do
|
100
|
+
expect(body.offset).to eq(40)
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'responds with the specifid limit' do
|
104
|
+
expect(body.discrete_length).to eq(2)
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'responds with the underlying length' do
|
108
|
+
expect(body.length).to eq(420)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'lists the first page of model results' do
|
112
|
+
expect(body.results.length).to eq(2)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe '#show' do
|
117
|
+
context 'item found in store' do
|
118
|
+
let(:params) { { :id => 'di2' } }
|
119
|
+
let(:kontroller) {
|
120
|
+
k = kontroller_klass.new
|
121
|
+
k.params = params
|
122
|
+
k
|
123
|
+
}
|
124
|
+
let(:response) {
|
125
|
+
kontroller.show()
|
126
|
+
}
|
127
|
+
let(:status) { response[:status] }
|
128
|
+
let(:content_type) { response[:content_type] }
|
129
|
+
let(:body) { response[:body] }
|
130
|
+
|
131
|
+
it 'responds with an :ok status' do
|
132
|
+
expect(status).to eq(200)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'responds with a json content_type' do
|
136
|
+
expect(content_type).to eq('application/json')
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'responds with a model item' do
|
140
|
+
expect(body.class.name).to eq('Model')
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context 'item not found in store' do
|
145
|
+
let(:params) { { :id => 'dne' } }
|
146
|
+
let(:kontroller) {
|
147
|
+
k = kontroller_klass.new
|
148
|
+
k.params = params
|
149
|
+
k
|
150
|
+
}
|
151
|
+
let(:response) {
|
152
|
+
kontroller.show()
|
153
|
+
}
|
154
|
+
let(:status) { response[:status] }
|
155
|
+
let(:content_type) { response[:content_type] }
|
156
|
+
let(:body) { response[:body] }
|
157
|
+
|
158
|
+
it 'responds with an :ok status' do
|
159
|
+
expect(status).to eq(404)
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'responds with a json content_type' do
|
163
|
+
expect(content_type).to eq('application/json')
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'responds with a nil body' do
|
167
|
+
expect(body).to be(nil)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
context 'id not specified' do
|
172
|
+
let(:params) { { } }
|
173
|
+
let(:kontroller) {
|
174
|
+
k = kontroller_klass.new
|
175
|
+
k.params = params
|
176
|
+
k
|
177
|
+
}
|
178
|
+
let(:response) {
|
179
|
+
kontroller.show()
|
180
|
+
}
|
181
|
+
let(:status) { response[:status] }
|
182
|
+
let(:content_type) { response[:content_type] }
|
183
|
+
let(:body) { response[:body] }
|
184
|
+
|
185
|
+
it 'responds with an :ok status' do
|
186
|
+
expect(status).to eq(400)
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'responds with a json content_type' do
|
190
|
+
expect(content_type).to eq('application/json')
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'responds with a model item' do
|
194
|
+
expect(body).to be(nil)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
describe '#destroy' do
|
201
|
+
context 'id specified' do
|
202
|
+
let(:params) { { :id => 'di2' } }
|
203
|
+
let(:kontroller) {
|
204
|
+
k = kontroller_klass.new
|
205
|
+
k.params = params
|
206
|
+
k
|
207
|
+
}
|
208
|
+
let(:response) {
|
209
|
+
kontroller.destroy()
|
210
|
+
}
|
211
|
+
let(:status) { response[:status] }
|
212
|
+
let(:content_type) { response[:content_type] }
|
213
|
+
let(:body) { response[:body] }
|
214
|
+
|
215
|
+
it 'responds with an :ok status' do
|
216
|
+
expect(status).to eq(200)
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'responds with a json content_type' do
|
220
|
+
expect(content_type).to eq('application/json')
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'responds with an empty body' do
|
224
|
+
expect(body).to be(nil)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
context 'id not specified' do
|
229
|
+
let(:params) { { } }
|
230
|
+
let(:kontroller) {
|
231
|
+
k = kontroller_klass.new
|
232
|
+
k.params = params
|
233
|
+
k
|
234
|
+
}
|
235
|
+
let(:response) {
|
236
|
+
kontroller.destroy()
|
237
|
+
}
|
238
|
+
let(:status) { response[:status] }
|
239
|
+
let(:content_type) { response[:content_type] }
|
240
|
+
let(:body) { response[:body] }
|
241
|
+
|
242
|
+
it 'responds with an :ok status' do
|
243
|
+
expect(status).to eq(400)
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'responds with a json content_type' do
|
247
|
+
expect(content_type).to eq('application/json')
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'responds with a model item' do
|
251
|
+
expect(body).to be(nil)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
describe '#update' do
|
257
|
+
end
|
258
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class OtherModel
|
4
|
+
end
|
5
|
+
|
6
|
+
class Model
|
7
|
+
end
|
8
|
+
|
9
|
+
class ModelsController
|
10
|
+
extend RRRMatey::CrudController::ModelMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
describe RRRMatey::CrudController::ModelMethods do
|
14
|
+
let(:kontroller_klass) { ModelsController }
|
15
|
+
|
16
|
+
describe '#model' do
|
17
|
+
it 'defaults to controller name derived model' do
|
18
|
+
expect(kontroller_klass.model).to eq(Model)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'allows specification' do
|
22
|
+
kontroller_klass.model(OtherModel)
|
23
|
+
expect(kontroller_klass.model).to eq(OtherModel)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class DiscreteModel
|
4
|
+
attr_accessor :id, :name
|
5
|
+
|
6
|
+
def initialize(opts = {})
|
7
|
+
@id = opts[:id]
|
8
|
+
@name = opts[:name]
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_consumer_hash
|
12
|
+
{ 'id' => id, :name => name }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe RRRMatey::DiscreteResult do
|
17
|
+
context 'empty results' do
|
18
|
+
let(:results) { nil }
|
19
|
+
let(:discrete_result) {
|
20
|
+
RRRMatey::DiscreteResult.new(:results => results,
|
21
|
+
:length => 42,
|
22
|
+
:offset => 40,
|
23
|
+
:discrete_length => 10)
|
24
|
+
}
|
25
|
+
|
26
|
+
describe '#initialize' do
|
27
|
+
it 'sets results' do
|
28
|
+
expect(discrete_result.results).to eq([])
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'sets length' do
|
32
|
+
expect(discrete_result.length).to eq(42)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'sets offset' do
|
36
|
+
expect(discrete_result.offset).to eq(40)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'sets discrete_length' do
|
40
|
+
expect(discrete_result.discrete_length).to eq(10)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#to_json' do
|
45
|
+
it 'yields json array of results' do
|
46
|
+
expect(discrete_result.to_json).to eq('{"length":42,"offset":40,"limit":10,"results":[]}')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#to_xml' do
|
51
|
+
it 'yields xml array of results' do
|
52
|
+
expect(discrete_result.to_xml.gsub(/\n/, '').
|
53
|
+
gsub(/\>\s+\</, '><')).
|
54
|
+
to eq('<root length="42" offset="40" limit="10"></root>')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'hydrated results' do
|
61
|
+
let(:results) { [
|
62
|
+
DiscreteModel.new(:id => 'di1', :name => 'name1'),
|
63
|
+
DiscreteModel.new(:id => 'di2', :name => 'name2')
|
64
|
+
]}
|
65
|
+
let(:discrete_result) {
|
66
|
+
RRRMatey::DiscreteResult.new(:results => results,
|
67
|
+
:length => 42,
|
68
|
+
:offset => 40,
|
69
|
+
:discrete_length => 10)
|
70
|
+
}
|
71
|
+
|
72
|
+
describe '#initialize' do
|
73
|
+
it 'sets results' do
|
74
|
+
expect(discrete_result.results).to eq(results)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'sets length' do
|
78
|
+
expect(discrete_result.length).to eq(42)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'sets offset' do
|
82
|
+
expect(discrete_result.offset).to eq(40)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'sets discrete_length' do
|
86
|
+
expect(discrete_result.discrete_length).to eq(10)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#to_json' do
|
91
|
+
it 'yields json array of results' do
|
92
|
+
expect(discrete_result.to_json).to eq('{"length":42,"offset":40,"limit":10,"results":[{"id":"di1","name":"name1"},{"id":"di2","name":"name2"}]}')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#to_xml' do
|
97
|
+
it 'yields xml array of results' do
|
98
|
+
expect(discrete_result.to_xml.gsub(/\n/, '').
|
99
|
+
gsub(/\>\s+\</, '><')).
|
100
|
+
to eq('<root length="42" offset="40" limit="10"><results id="di1" name="name1" /><results id="di2" name="name2" /></root>')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|