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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +16 -0
  3. data/README.md +249 -0
  4. data/RELEASE_NOTES.md +26 -0
  5. data/Rakefile +39 -0
  6. data/lib/rrrmatey.rb +8 -0
  7. data/lib/rrrmatey/crud_controller.rb +97 -0
  8. data/lib/rrrmatey/discrete_result.rb +31 -0
  9. data/lib/rrrmatey/errors.rb +11 -0
  10. data/lib/rrrmatey/retryable.rb +28 -0
  11. data/lib/rrrmatey/string_model/connection_methods.rb +47 -0
  12. data/lib/rrrmatey/string_model/consumer_adapter_methods.rb +9 -0
  13. data/lib/rrrmatey/string_model/delete_methods.rb +12 -0
  14. data/lib/rrrmatey/string_model/field_definition_methods.rb +101 -0
  15. data/lib/rrrmatey/string_model/find_methods.rb +85 -0
  16. data/lib/rrrmatey/string_model/index_methods.rb +90 -0
  17. data/lib/rrrmatey/string_model/namespaced_key_methods.rb +21 -0
  18. data/lib/rrrmatey/string_model/string_model.rb +92 -0
  19. data/lib/rrrmatey/type_coercion.rb +61 -0
  20. data/lib/rrrmatey/version.rb +3 -0
  21. data/spec/rrrmatey/crud_controller/crud_controller_spec.rb +258 -0
  22. data/spec/rrrmatey/crud_controller/model_methods_spec.rb +26 -0
  23. data/spec/rrrmatey/discrete_result_spec.rb +104 -0
  24. data/spec/rrrmatey/retryable_spec.rb +95 -0
  25. data/spec/rrrmatey/string_model/connection_methods_spec.rb +64 -0
  26. data/spec/rrrmatey/string_model/consumer_adapter_methods_spec.rb +43 -0
  27. data/spec/rrrmatey/string_model/delete_methods_spec.rb +36 -0
  28. data/spec/rrrmatey/string_model/field_definition_methods_spec.rb +51 -0
  29. data/spec/rrrmatey/string_model/find_methods_spec.rb +147 -0
  30. data/spec/rrrmatey/string_model/index_methods_spec.rb +231 -0
  31. data/spec/rrrmatey/string_model/namespaced_key_methods_spec.rb +34 -0
  32. data/spec/rrrmatey/string_model/string_model_spec.rb +208 -0
  33. data/spec/spec_helper.rb +16 -0
  34. 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,3 @@
1
+ module RRRMatey
2
+ VERSION = '0.1.0'
3
+ 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