cistern 1.0.1.pre6 → 2.0.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.
- checksums.yaml +7 -0
- data/.gemrelease +3 -0
- data/README.md +139 -201
- data/lib/cistern/attributes.rb +31 -16
- data/lib/cistern/collection.rb +8 -6
- data/lib/cistern/model.rb +15 -0
- data/lib/cistern/request.rb +5 -8
- data/lib/cistern/service.rb +38 -22
- data/lib/cistern/version.rb +1 -1
- data/spec/collection_spec.rb +28 -12
- data/spec/hash_spec.rb +14 -0
- data/spec/model_spec.rb +24 -0
- data/spec/request_spec.rb +6 -6
- metadata +12 -12
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 14abaac482ac21a28c8397bc14d3f122f8a8f7b6
|
4
|
+
data.tar.gz: 1bd8bbc0c5f2c0a11596b5e420042d7a468c2ea6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a30d8da5aac9ea437a76903c3183bfc0ae88ce295080e8703644eece9fa8c81656ef9df60e73d102384a7d9db9ea2e94bb96d39e30802b0e4238fc3d5647bcf0
|
7
|
+
data.tar.gz: 5bb8cf1014201e231ed9f51456369e410a3802b084f322c1f1d051b24124bdb20ba1048841a71c96b9952e89dd67e8628dd8b9533dc45481af9089ee5079d890
|
data/.gemrelease
ADDED
data/README.md
CHANGED
@@ -11,68 +11,6 @@ Cistern helps you consistenly build your API clients and faciliates building moc
|
|
11
11
|
|
12
12
|
This represents the remote service that you are wrapping. If the service name is 'foo' then a good name is 'Foo::Client'.
|
13
13
|
|
14
|
-
#### Requests
|
15
|
-
|
16
|
-
Requests are enumerated using the `request` method and required immediately via the relative path specified via `request_path`.
|
17
|
-
|
18
|
-
```ruby
|
19
|
-
class Foo::Client < Cistern::Service
|
20
|
-
request_path "my-foo/requests"
|
21
|
-
|
22
|
-
request :get_bar # require my-foo/requests/get_bar.rb
|
23
|
-
request :get_bars # require my-foo/requests/get_bars.rb
|
24
|
-
|
25
|
-
class Real
|
26
|
-
def request(url)
|
27
|
-
Net::HTTP.get(url)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
```
|
32
|
-
|
33
|
-
|
34
|
-
<!--todo move to a request section-->
|
35
|
-
A request is method defined within the context of service and mode (Real or Mock). Defining requests within the service mock class is optional.
|
36
|
-
|
37
|
-
```ruby
|
38
|
-
# my-foo/requests/get_bar.rb
|
39
|
-
class Foo::Client
|
40
|
-
class Real
|
41
|
-
def get_bar(bar_id)
|
42
|
-
request("http://example.org/bar/#{bar_id}")
|
43
|
-
end
|
44
|
-
end # Real
|
45
|
-
|
46
|
-
# optional, but encouraged
|
47
|
-
class Mock
|
48
|
-
def get_bars
|
49
|
-
# do some mock things
|
50
|
-
end
|
51
|
-
end # Mock
|
52
|
-
end # Foo::client
|
53
|
-
```
|
54
|
-
|
55
|
-
All declared requests can be listed via `Cistern::Service#requests`.
|
56
|
-
|
57
|
-
```ruby
|
58
|
-
Foo::Client.requests # => [:get_bar, :get_bars]
|
59
|
-
```
|
60
|
-
|
61
|
-
#### Models and Collections
|
62
|
-
|
63
|
-
Models and collections have declaration semantics similar to requests. Models and collections are enumerated via `model` and `collection` respectively.
|
64
|
-
|
65
|
-
```ruby
|
66
|
-
class Foo::Client < Cistern::Service
|
67
|
-
model_path "my-foo/models"
|
68
|
-
|
69
|
-
model :bar # require my-foo/models/bar.rb
|
70
|
-
collection :bars # require my-foo/models/bars.rb
|
71
|
-
end
|
72
|
-
```
|
73
|
-
|
74
|
-
#### Initialization
|
75
|
-
|
76
14
|
Service initialization parameters are enumerated by `requires` and `recognizes`. `recognizes` parameters are optional.
|
77
15
|
|
78
16
|
```ruby
|
@@ -90,6 +28,7 @@ Foo::Client.new(hmac_id: "1", url: "http://example.org")
|
|
90
28
|
Foo::Client.new(hmac_id: "1")
|
91
29
|
```
|
92
30
|
|
31
|
+
Cistern will define for you two classes, `Mock` and `Real`.
|
93
32
|
|
94
33
|
### Mocking
|
95
34
|
|
@@ -107,114 +46,72 @@ real.is_a?(Foo::Client::Real) # true
|
|
107
46
|
fake.is_a?(Foo::Client::Mock) # true
|
108
47
|
```
|
109
48
|
|
110
|
-
|
111
|
-
|
112
|
-
A uniform interface for mock data is mixed into the `Mock` class by default.
|
49
|
+
### Requests
|
113
50
|
|
114
|
-
|
115
|
-
Foo::Client.mock!
|
116
|
-
client = Foo::Client.new # Foo::Client::Mock
|
117
|
-
client.data # Cistern::Data::Hash
|
118
|
-
client.data["bars"] += ["x"] # ["x"]
|
119
|
-
```
|
51
|
+
Requests are defined by subclassing `#{service}::Request`.
|
120
52
|
|
121
|
-
|
53
|
+
* `service` represents the associated `Foo::Client` instance.
|
122
54
|
|
123
55
|
```ruby
|
124
|
-
Foo::Client::
|
125
|
-
|
56
|
+
class Foo::Client::GetBar < Foo::Client::Request
|
57
|
+
def real(params)
|
58
|
+
# make a real request
|
59
|
+
"i'm real"
|
60
|
+
end
|
126
61
|
|
127
|
-
|
62
|
+
def mock(params)
|
63
|
+
# return a fake response
|
64
|
+
"imposter!"
|
65
|
+
end
|
66
|
+
end
|
128
67
|
|
129
|
-
|
130
|
-
client.data.object_id # 70199868585600
|
131
|
-
client.reset!
|
132
|
-
client.data["bars"] # []
|
133
|
-
client.data.object_id # 70199868566840
|
68
|
+
Foo::Client.new.get_bar # "i'm real"
|
134
69
|
```
|
135
70
|
|
136
|
-
`
|
71
|
+
The `#service_method` function allows you to specific the name of generated method.
|
137
72
|
|
138
73
|
```ruby
|
139
|
-
|
140
|
-
|
141
|
-
client.clear
|
142
|
-
client.data["bars"] # []
|
143
|
-
|
144
|
-
client.data.object_id # 70199868566840
|
145
|
-
```
|
146
|
-
|
147
|
-
* `store` and `[]=` write
|
148
|
-
* `fetch` and `[]` read
|
74
|
+
class Foo::Client::GetBars < Foo::Client::Request
|
75
|
+
service_method :get_all_the_bars
|
149
76
|
|
150
|
-
|
151
|
-
|
152
|
-
```ruby
|
153
|
-
class Foo::Client < Cistern::Service
|
154
|
-
class Mock
|
155
|
-
def self.data
|
156
|
-
@data ||= {}
|
157
|
-
end
|
77
|
+
def real(params)
|
78
|
+
"all the bars"
|
158
79
|
end
|
159
80
|
end
|
160
|
-
```
|
161
|
-
|
162
81
|
|
163
|
-
|
82
|
+
Foo::Client.new.respond_to?(:get_bars) # false
|
83
|
+
Foo::Client.new.get_all_the_bars # "all the bars"
|
164
84
|
|
165
|
-
|
85
|
+
All declared requests can be listed via `Cistern::Service#requests`.
|
166
86
|
|
167
87
|
```ruby
|
168
|
-
#
|
169
|
-
class Foo::Client
|
170
|
-
class Mock
|
171
|
-
def create_bar(options={})
|
172
|
-
id = Foo.random_hex(6)
|
173
|
-
|
174
|
-
bar = {
|
175
|
-
"id" => id
|
176
|
-
}.merge(options)
|
177
|
-
|
178
|
-
self.data[:bars][id] = bar
|
179
|
-
|
180
|
-
response(
|
181
|
-
:body => {"bar" => bar},
|
182
|
-
:status => 201,
|
183
|
-
:path => '/bar',
|
184
|
-
)
|
185
|
-
end
|
186
|
-
end # Mock
|
187
|
-
end # Foo::Client
|
88
|
+
Foo::Client.requests # => [Foo::Client::GetBars, Foo::Client::GetBar]
|
188
89
|
```
|
189
90
|
|
190
|
-
|
191
|
-
|
192
|
-
Currently supported storage backends are:
|
91
|
+
### Models
|
193
92
|
|
194
|
-
*
|
195
|
-
*
|
93
|
+
* `service` represents the associated `Foo::Client` instance.
|
94
|
+
* `collection` represents the related collection (if applicable)
|
95
|
+
* `new_record?` checks if `identity` is present
|
96
|
+
* `requires(*requirements)` throws `ArgumentError` if an attribute matching a requirement isn't set
|
97
|
+
* `merge_attributes(attributes)` sets attributes for the current model instance
|
196
98
|
|
99
|
+
#### Attributes
|
197
100
|
|
198
|
-
|
101
|
+
Attributes are designed to be a flexible way of parsing service request responses.
|
199
102
|
|
200
|
-
|
201
|
-
# use redis with defaults
|
202
|
-
Patient::Mock.store_in(:redis)
|
203
|
-
# use redis with a specific client
|
204
|
-
Patient::Mock.store_in(:redis, client: Redis::Namespace.new("cistern", redis: Redis.new(host: "10.1.0.1"))
|
205
|
-
# use a hash
|
206
|
-
Patient::Mock.store_in(:hash)
|
207
|
-
```
|
103
|
+
`identity` is special but not required.
|
208
104
|
|
209
|
-
|
105
|
+
`attribute :flavor` makes `Foo::Client::Bar.new.respond_to?(:flavor)`
|
210
106
|
|
211
|
-
* `
|
212
|
-
* `
|
107
|
+
* `:aliases` or `:alias` allows a attribute key to be different then a response key. `attribute :keypair_id, alias: "keypair"` with `merge_attributes("keypair" => 1)` sets `keypair_id` to `1`
|
108
|
+
* `:type` automatically casts the attribute do the specified type. `attribute :private_ips, type: :array` with `merge_attributes("private_ips" => 2)` sets `private_ips` to `[2]`
|
109
|
+
* `:squash` traverses nested hashes for a key. `attribute :keypair_id, aliases: "keypair", squash: "id"` with `merge_attributes("keypair" => {"id" => 3})` sets `keypair_id` to `3`
|
213
110
|
|
214
111
|
Example
|
215
112
|
|
216
113
|
```ruby
|
217
|
-
class Foo::Client::Bar <
|
114
|
+
class Foo::Client::Bar < Foo::Client::Model
|
218
115
|
identity :id
|
219
116
|
|
220
117
|
attribute :flavor
|
@@ -249,40 +146,18 @@ class Foo::Client::Bar < Cistern::Model
|
|
249
146
|
end
|
250
147
|
```
|
251
148
|
|
252
|
-
#### Dirty
|
253
|
-
|
254
|
-
Dirty attributes are tracked and cleared when `merge_attributes` is called.
|
255
|
-
|
256
|
-
* `changed` returns a Hash of changed attributes mapped to there initial value and current value
|
257
|
-
* `dirty_attributes` returns Hash of changed attributes with there current value. This should be used in the model `save` function.
|
258
|
-
|
259
|
-
|
260
|
-
```ruby
|
261
|
-
bar = Foo::Client::Bar.new(id: 1, flavor: "x") # => <#Foo::Client::Bar>
|
262
|
-
|
263
|
-
bar.dirty? # => false
|
264
|
-
bar.changed # => {}
|
265
|
-
bar.dirty_attributes # => {}
|
266
|
-
|
267
|
-
bar.flavor = "y"
|
268
|
-
|
269
|
-
bar.dirty? # => true
|
270
|
-
bar.changed # => {flavor: ["x", "y"]}
|
271
|
-
bar.dirty_attributes # => {flavor: "y"}
|
272
|
-
|
273
|
-
bar.save
|
274
|
-
bar.dirty? # => false
|
275
|
-
bar.changed # => {}
|
276
|
-
bar.dirty_attributes # => {}
|
277
|
-
```
|
278
|
-
|
279
149
|
### Collection
|
280
150
|
|
281
|
-
`model` tells Cistern which class is contained within the collection.
|
151
|
+
* `model` tells Cistern which class is contained within the collection.
|
152
|
+
* `service` is the associated `Foo::Client` instance
|
153
|
+
* `attribute` specifications on collections are allowed. use `merge_attributes`
|
154
|
+
* `load` consumes an Array of data and constructs matching `model` instances
|
282
155
|
|
283
156
|
```ruby
|
284
157
|
class Foo::Client::Bars < Cistern::Collection
|
285
158
|
|
159
|
+
attribute :count, type: :integer
|
160
|
+
|
286
161
|
model Foo::Client::Bar
|
287
162
|
|
288
163
|
def all(params = {})
|
@@ -313,40 +188,103 @@ class Foo::Client::Bars < Cistern::Collection
|
|
313
188
|
end
|
314
189
|
```
|
315
190
|
|
316
|
-
|
191
|
+
#### Data
|
192
|
+
|
193
|
+
A uniform interface for mock data is mixed into the `Mock` class by default.
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
Foo::Client.mock!
|
197
|
+
client = Foo::Client.new # Foo::Client::Mock
|
198
|
+
client.data # Cistern::Data::Hash
|
199
|
+
client.data["bars"] += ["x"] # ["x"]
|
200
|
+
```
|
201
|
+
|
202
|
+
Mock data is class-level by default
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
Foo::Client::Mock.data["bars"] # ["x"]
|
206
|
+
```
|
207
|
+
|
208
|
+
`reset!` dimisses the `data` object.
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
client.data.object_id # 70199868585600
|
212
|
+
client.reset!
|
213
|
+
client.data["bars"] # []
|
214
|
+
client.data.object_id # 70199868566840
|
215
|
+
```
|
216
|
+
|
217
|
+
`clear` removes existing keys and values but keeps the same object.
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
client.data["bars"] += ["y"] # ["y"]
|
221
|
+
client.data.object_id # 70199868378300
|
222
|
+
client.clear
|
223
|
+
client.data["bars"] # []
|
224
|
+
|
225
|
+
client.data.object_id # 70199868566840
|
226
|
+
```
|
227
|
+
|
228
|
+
* `store` and `[]=` write
|
229
|
+
* `fetch` and `[]` read
|
230
|
+
|
231
|
+
You can make the service bypass Cistern's mock data structures by simply creating a `self.data` function in your service `Mock` declaration.
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
class Foo::Client < Cistern::Service
|
235
|
+
class Mock
|
236
|
+
def self.data
|
237
|
+
@data ||= {}
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
```
|
242
|
+
|
243
|
+
#### Storage
|
244
|
+
|
245
|
+
Currently supported storage backends are:
|
246
|
+
|
247
|
+
* `:hash` : `Cistern::Data::Hash` (default)
|
248
|
+
* `:redis` : `Cistern::Data::Redis`
|
249
|
+
|
250
|
+
|
251
|
+
Backends can be switched by using `store_in`.
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
# use redis with defaults
|
255
|
+
Patient::Mock.store_in(:redis)
|
256
|
+
# use redis with a specific client
|
257
|
+
Patient::Mock.store_in(:redis, client: Redis::Namespace.new("cistern", redis: Redis.new(host: "10.1.0.1"))
|
258
|
+
# use a hash
|
259
|
+
Patient::Mock.store_in(:hash)
|
260
|
+
```
|
261
|
+
|
262
|
+
|
263
|
+
#### Dirty
|
264
|
+
|
265
|
+
Dirty attributes are tracked and cleared when `merge_attributes` is called.
|
266
|
+
|
267
|
+
* `changed` returns a Hash of changed attributes mapped to there initial value and current value
|
268
|
+
* `dirty_attributes` returns Hash of changed attributes with there current value. This should be used in the model `save` function.
|
269
|
+
|
317
270
|
|
318
271
|
```ruby
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
bar = {
|
336
|
-
"id" => id
|
337
|
-
}.merge(options)
|
338
|
-
|
339
|
-
self.data[:bars][id]= bar
|
340
|
-
|
341
|
-
response(
|
342
|
-
:body => {"bar" => bar},
|
343
|
-
:status => 201,
|
344
|
-
:path => '/bar',
|
345
|
-
)
|
346
|
-
end
|
347
|
-
end # Mock
|
348
|
-
end # Client
|
349
|
-
end # Foo
|
272
|
+
bar = Foo::Client::Bar.new(id: 1, flavor: "x") # => <#Foo::Client::Bar>
|
273
|
+
|
274
|
+
bar.dirty? # => false
|
275
|
+
bar.changed # => {}
|
276
|
+
bar.dirty_attributes # => {}
|
277
|
+
|
278
|
+
bar.flavor = "y"
|
279
|
+
|
280
|
+
bar.dirty? # => true
|
281
|
+
bar.changed # => {flavor: ["x", "y"]}
|
282
|
+
bar.dirty_attributes # => {flavor: "y"}
|
283
|
+
|
284
|
+
bar.save
|
285
|
+
bar.dirty? # => false
|
286
|
+
bar.changed # => {}
|
287
|
+
bar.dirty_attributes # => {}
|
350
288
|
```
|
351
289
|
|
352
290
|
## Examples
|
data/lib/cistern/attributes.rb
CHANGED
@@ -26,10 +26,12 @@ module Cistern::Attributes
|
|
26
26
|
|
27
27
|
travel.call(v, squash.dup)
|
28
28
|
elsif v.is_a?(::Hash)
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
v[
|
29
|
+
squash_s = squash.to_s
|
30
|
+
|
31
|
+
if v.key?(key = squash_s.to_sym)
|
32
|
+
v[key]
|
33
|
+
elsif v.has_key?(squash_s)
|
34
|
+
v[squash_s]
|
33
35
|
else
|
34
36
|
v
|
35
37
|
end
|
@@ -92,7 +94,7 @@ module Cistern::Attributes
|
|
92
94
|
self.attributes[name] = options
|
93
95
|
end
|
94
96
|
|
95
|
-
options[:aliases] = Array(options[:aliases]).map { |a| a.to_s.to_sym }
|
97
|
+
options[:aliases] = Array(options[:aliases] || options[:alias]).map { |a| a.to_s.to_sym }
|
96
98
|
|
97
99
|
options[:aliases].each do |new_alias|
|
98
100
|
aliases[new_alias] << name.to_s.to_sym
|
@@ -173,26 +175,39 @@ module Cistern::Attributes
|
|
173
175
|
end
|
174
176
|
|
175
177
|
def merge_attributes(new_attributes = {})
|
178
|
+
protected_methods = (Cistern::Model.instance_methods - [:service, :identity, :collection])
|
179
|
+
ignored_attributes = self.class.ignored_attributes
|
180
|
+
class_attributes = self.class.attributes
|
181
|
+
class_aliases = self.class.aliases
|
182
|
+
|
176
183
|
new_attributes.each do |_key, value|
|
177
|
-
|
184
|
+
string_key = _key.kind_of?(String) ? _key : _key.to_s
|
185
|
+
symbol_key = case _key
|
186
|
+
when String
|
187
|
+
_key.to_sym
|
188
|
+
when Symbol
|
189
|
+
_key
|
190
|
+
else
|
191
|
+
string_key.to_sym
|
192
|
+
end
|
193
|
+
|
178
194
|
# find nested paths
|
179
|
-
value.is_a?(::Hash) &&
|
180
|
-
if
|
181
|
-
send("#{name}=", {
|
195
|
+
value.is_a?(::Hash) && class_attributes.each do |name, options|
|
196
|
+
if options[:squash] && options[:squash].first == string_key
|
197
|
+
send("#{name}=", {symbol_key => value})
|
182
198
|
end
|
183
199
|
end
|
184
200
|
|
185
|
-
unless
|
186
|
-
if
|
187
|
-
|
201
|
+
unless ignored_attributes.include?(symbol_key)
|
202
|
+
if class_aliases.has_key?(symbol_key)
|
203
|
+
class_aliases[symbol_key].each do |aliased_key|
|
188
204
|
send("#{aliased_key}=", value)
|
189
205
|
end
|
190
206
|
end
|
191
207
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
send("#{key}=", value)
|
208
|
+
assignment_method = "#{string_key}="
|
209
|
+
if !protected_methods.include?(symbol_key) && self.respond_to?(assignment_method, true)
|
210
|
+
send(assignment_method, value)
|
196
211
|
end
|
197
212
|
end
|
198
213
|
end
|
data/lib/cistern/collection.rb
CHANGED
@@ -6,11 +6,9 @@ module Cistern::Collection
|
|
6
6
|
:keep_if, :pop, :shift, :delete_at, :compact
|
7
7
|
].to_set # :nodoc:
|
8
8
|
|
9
|
-
def self.service_collection(service, klass)
|
10
|
-
plural_name = service.collection_method(klass)
|
11
|
-
|
9
|
+
def self.service_collection(service, klass, name)
|
12
10
|
service.const_get(:Collections).module_eval <<-EOS, __FILE__, __LINE__
|
13
|
-
def #{
|
11
|
+
def #{name}(attributes={})
|
14
12
|
#{klass.name}.new(attributes.merge(service: self))
|
15
13
|
end
|
16
14
|
EOS
|
@@ -20,7 +18,11 @@ module Cistern::Collection
|
|
20
18
|
|
21
19
|
module ClassMethods
|
22
20
|
def model(new_model=nil)
|
23
|
-
@
|
21
|
+
@_model ||= new_model
|
22
|
+
end
|
23
|
+
|
24
|
+
def service_method(name=nil)
|
25
|
+
@_service_method ||= name
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
@@ -69,7 +71,7 @@ module Cistern::Collection
|
|
69
71
|
end
|
70
72
|
|
71
73
|
def model
|
72
|
-
self.class.
|
74
|
+
self.class.model
|
73
75
|
end
|
74
76
|
|
75
77
|
def new(attributes = {})
|
data/lib/cistern/model.rb
CHANGED
@@ -4,6 +4,21 @@ module Cistern::Model
|
|
4
4
|
def self.included(klass)
|
5
5
|
klass.send(:extend, Cistern::Attributes::ClassMethods)
|
6
6
|
klass.send(:include, Cistern::Attributes::InstanceMethods)
|
7
|
+
klass.send(:extend, Cistern::Model::ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.service_model(service, klass, name)
|
11
|
+
service.const_get(:Collections).module_eval <<-EOS, __FILE__, __LINE__
|
12
|
+
def #{name}(attributes={})
|
13
|
+
#{klass.name}.new(attributes.merge(service: self))
|
14
|
+
end
|
15
|
+
EOS
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def service_method(name=nil)
|
20
|
+
@_service_method ||= name
|
21
|
+
end
|
7
22
|
end
|
8
23
|
|
9
24
|
attr_accessor :collection, :service
|
data/lib/cistern/request.rb
CHANGED
@@ -1,20 +1,16 @@
|
|
1
1
|
module Cistern::Request
|
2
|
-
def self.service_request(service, klass)
|
3
|
-
request = klass.request_name || Cistern::String.camelize(Cistern::String.demodulize(klass.name))
|
4
|
-
|
2
|
+
def self.service_request(service, klass, name)
|
5
3
|
service::Mock.module_eval <<-EOS, __FILE__, __LINE__
|
6
|
-
def #{
|
4
|
+
def #{name}(*args)
|
7
5
|
#{klass}.new(self)._mock(*args)
|
8
6
|
end
|
9
7
|
EOS
|
10
8
|
|
11
9
|
service::Real.module_eval <<-EOS, __FILE__, __LINE__
|
12
|
-
def #{
|
10
|
+
def #{name}(*args)
|
13
11
|
#{klass}.new(self)._real(*args)
|
14
12
|
end
|
15
13
|
EOS
|
16
|
-
|
17
|
-
request
|
18
14
|
end
|
19
15
|
|
20
16
|
attr_reader :service
|
@@ -24,7 +20,8 @@ module Cistern::Request
|
|
24
20
|
end
|
25
21
|
|
26
22
|
module ClassMethods
|
27
|
-
def
|
23
|
+
def service_method(name=nil)
|
24
|
+
@_service_method ||= name
|
28
25
|
end
|
29
26
|
end
|
30
27
|
end
|
data/lib/cistern/service.rb
CHANGED
@@ -9,10 +9,6 @@ class Cistern::Service
|
|
9
9
|
service.collections
|
10
10
|
end
|
11
11
|
|
12
|
-
def mocked_requests
|
13
|
-
service.mocked_requests
|
14
|
-
end
|
15
|
-
|
16
12
|
def requests
|
17
13
|
service.requests
|
18
14
|
end
|
@@ -46,6 +42,10 @@ class Cistern::Service
|
|
46
42
|
class Model
|
47
43
|
include Cistern::Model
|
48
44
|
|
45
|
+
def self.inherited(klass)
|
46
|
+
service.models << klass
|
47
|
+
end
|
48
|
+
|
49
49
|
def self.service
|
50
50
|
#{klass.name}
|
51
51
|
end
|
@@ -59,7 +59,7 @@ class Cistern::Service
|
|
59
59
|
klass.send(:extend, Cistern::Collection::ClassMethods)
|
60
60
|
klass.send(:include, Cistern::Attributes::InstanceMethods)
|
61
61
|
|
62
|
-
|
62
|
+
service.collections << klass
|
63
63
|
end
|
64
64
|
|
65
65
|
def self.service
|
@@ -73,9 +73,7 @@ class Cistern::Service
|
|
73
73
|
def self.inherited(klass)
|
74
74
|
klass.extend(Cistern::Request::ClassMethods)
|
75
75
|
|
76
|
-
|
77
|
-
|
78
|
-
service.requests[name.to_sym] = klass
|
76
|
+
service.requests << klass
|
79
77
|
end
|
80
78
|
|
81
79
|
def self.service
|
@@ -109,26 +107,20 @@ class Cistern::Service
|
|
109
107
|
@collections ||= []
|
110
108
|
end
|
111
109
|
|
112
|
-
def collection_method(klass)
|
113
|
-
relative_demodulized = klass.name.gsub("#{self.name}::", "").gsub("::", "")
|
114
|
-
|
115
|
-
Cistern::String.underscore(relative_demodulized)
|
116
|
-
end
|
117
|
-
|
118
110
|
def models
|
119
|
-
@
|
111
|
+
@_models ||= []
|
120
112
|
end
|
121
113
|
|
122
114
|
def recognized_arguments
|
123
|
-
@
|
115
|
+
@_recognized_arguments ||= []
|
124
116
|
end
|
125
117
|
|
126
118
|
def required_arguments
|
127
|
-
@
|
119
|
+
@_required_arguments ||= []
|
128
120
|
end
|
129
121
|
|
130
122
|
def requests
|
131
|
-
@
|
123
|
+
@_requests ||= []
|
132
124
|
end
|
133
125
|
|
134
126
|
def requires(*args)
|
@@ -139,10 +131,6 @@ class Cistern::Service
|
|
139
131
|
self.recognized_arguments.concat(args)
|
140
132
|
end
|
141
133
|
|
142
|
-
def mocked_requests
|
143
|
-
@mocked_requests ||= {}
|
144
|
-
end
|
145
|
-
|
146
134
|
def validate_options(options={})
|
147
135
|
required_options = Cistern::Hash.slice(options, *required_arguments)
|
148
136
|
|
@@ -159,7 +147,35 @@ class Cistern::Service
|
|
159
147
|
end
|
160
148
|
end
|
161
149
|
|
150
|
+
def setup
|
151
|
+
return true if @_setup
|
152
|
+
|
153
|
+
requests.each do |klass|
|
154
|
+
name = klass.service_method ||
|
155
|
+
Cistern::String.camelize(Cistern::String.demodulize(klass.name))
|
156
|
+
|
157
|
+
Cistern::Request.service_request(self, klass, name)
|
158
|
+
end
|
159
|
+
|
160
|
+
collections.each do |klass|
|
161
|
+
name = klass.service_method ||
|
162
|
+
Cistern::String.underscore(klass.name.gsub("#{self.name}::", "").gsub("::", ""))
|
163
|
+
|
164
|
+
Cistern::Collection.service_collection(self, klass, name)
|
165
|
+
end
|
166
|
+
|
167
|
+
models.each do |klass|
|
168
|
+
name = klass.service_method ||
|
169
|
+
Cistern::String.underscore(klass.name.gsub("#{self.name}::", "").gsub("::", ""))
|
170
|
+
|
171
|
+
Cistern::Model.service_model(self, klass, name)
|
172
|
+
end
|
173
|
+
|
174
|
+
@_setup = true
|
175
|
+
end
|
176
|
+
|
162
177
|
def new(options={})
|
178
|
+
setup
|
163
179
|
validate_options(options)
|
164
180
|
|
165
181
|
self.const_get(self.mocking? ? :Mock : :Real).new(options)
|
data/lib/cistern/version.rb
CHANGED
data/spec/collection_spec.rb
CHANGED
@@ -1,51 +1,67 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe "Cistern::Collection" do
|
4
|
-
class
|
4
|
+
class SampleService < Cistern::Service
|
5
|
+
end
|
6
|
+
|
7
|
+
class Drug < SampleService::Model
|
5
8
|
identity :id
|
6
9
|
attribute :name
|
7
10
|
end
|
8
11
|
|
9
|
-
class
|
10
|
-
model
|
12
|
+
class Drugs < SampleService::Collection
|
13
|
+
model Drug
|
11
14
|
|
12
15
|
def all
|
13
16
|
self.load([{id: 1}, {id: 3, name: "tom"}, {id: 2}])
|
14
17
|
end
|
15
18
|
end
|
16
19
|
|
20
|
+
class Tacs < SampleService::Collection
|
21
|
+
service_method :toes
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should generate a default collection method" do
|
25
|
+
expect(SampleService.new.drugs).not_to be_empty
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should allow for a specific collection name" do
|
29
|
+
expect(SampleService.new).to respond_to(:toes)
|
30
|
+
expect(SampleService.new).not_to respond_to(:tacs)
|
31
|
+
end
|
32
|
+
|
17
33
|
it "should give to_s" do
|
18
|
-
collection =
|
34
|
+
collection = Drugs.new
|
19
35
|
expect(collection.to_s).not_to eq "[]"
|
20
36
|
expect(collection.to_s.gsub(/:[^>]*/,'')).to eq(collection.all.to_s.gsub(/:[^>]*/,''))
|
21
37
|
end
|
22
38
|
|
23
39
|
it "should give size and count" do
|
24
|
-
expect(
|
25
|
-
expect(
|
40
|
+
expect(Drugs.new.size).to eq(3)
|
41
|
+
expect(Drugs.new.count).to eq(3)
|
26
42
|
end
|
27
43
|
|
28
44
|
it "should give first" do
|
29
|
-
expect(
|
45
|
+
expect(Drugs.new.first).to eq(Drug.new(id: 1))
|
30
46
|
end
|
31
47
|
|
32
48
|
it "should give last" do
|
33
|
-
expect(
|
49
|
+
expect(Drugs.new.last).to eq(Drug.new(id: 2))
|
34
50
|
end
|
35
51
|
|
36
52
|
it "should reject" do
|
37
|
-
expect(
|
53
|
+
expect(Drugs.new.reject{|m| m.id == 2}).to eq([Drug.new(id: 1), Drug.new(id: 3)])
|
38
54
|
end
|
39
55
|
|
40
56
|
it "should select" do
|
41
|
-
expect(
|
57
|
+
expect(Drugs.new.select{|m| m.id == 2}).to eq([Drug.new(id: 2)])
|
42
58
|
end
|
43
59
|
|
44
60
|
it "should slice" do
|
45
|
-
expect(
|
61
|
+
expect(Drugs.new.slice(0,2)).to eq([Drug.new(id: 1), Drug.new(id: 3, name: "tom")])
|
46
62
|
end
|
47
63
|
|
48
64
|
it "should ==" do
|
49
|
-
|
65
|
+
Drugs.new.all == Drugs.new.all
|
50
66
|
end
|
51
67
|
end
|
data/spec/hash_spec.rb
CHANGED
@@ -15,6 +15,20 @@ describe "Cistern::Hash" do
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
+
describe "#except" do
|
19
|
+
let(:input) do
|
20
|
+
{ one: "one", two: "two", three: "three" }
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns a new hash without the specified keys" do
|
24
|
+
expect(Cistern::Hash.except(input, :one, :two)).to eq({three: "three"})
|
25
|
+
end
|
26
|
+
|
27
|
+
it "skips keys that aren't in the original hash" do
|
28
|
+
expect(Cistern::Hash.except(input, :four)).to eq(input)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
18
32
|
describe "#stringify_keys" do
|
19
33
|
let(:input) do
|
20
34
|
{ one: "one", two: "two" }
|
data/spec/model_spec.rb
CHANGED
@@ -20,6 +20,30 @@ describe "Cistern::Model" do
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
+
it "should set singular resource service method" do
|
24
|
+
class ModelService < Cistern::Service
|
25
|
+
end
|
26
|
+
|
27
|
+
class ModelService::Jimbob < ModelService::Model
|
28
|
+
end
|
29
|
+
|
30
|
+
expect(ModelService.new).to respond_to(:jimbob)
|
31
|
+
expect(ModelService.new.jimbob).to be_a(ModelService::Jimbob)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should set specific singular resource service method" do
|
35
|
+
class SpecificModelService < Cistern::Service
|
36
|
+
end
|
37
|
+
|
38
|
+
class SpecificModelService::Jimbob < SpecificModelService::Model
|
39
|
+
service_method :john_boy
|
40
|
+
end
|
41
|
+
|
42
|
+
expect(SpecificModelService.new).not_to respond_to(:jimbob)
|
43
|
+
expect(SpecificModelService.new).to respond_to(:john_boy)
|
44
|
+
expect(SpecificModelService.new.john_boy).to be_a(SpecificModelService::Jimbob)
|
45
|
+
end
|
46
|
+
|
23
47
|
it "should duplicate a model" do
|
24
48
|
class DupSpec < Sample::Model
|
25
49
|
identity :id
|
data/spec/request_spec.rb
CHANGED
@@ -15,8 +15,8 @@ describe "Cistern::Request" do
|
|
15
15
|
|
16
16
|
# @todo Sample::Service.request
|
17
17
|
class ListSamples < SampleService::Request
|
18
|
-
|
19
|
-
|
18
|
+
service_method :list_all_samples
|
19
|
+
|
20
20
|
def real(*args)
|
21
21
|
service.service_args + args + ["real"]
|
22
22
|
end
|
@@ -27,11 +27,11 @@ describe "Cistern::Request" do
|
|
27
27
|
end
|
28
28
|
|
29
29
|
it "should execute a new-style request" do
|
30
|
-
expect(SampleService.new.
|
31
|
-
expect(SampleService::Real.new.
|
32
|
-
expect(SampleService::Mock.new.
|
30
|
+
expect(SampleService.new.list_all_samples("sample1")).to eq([{}, "sample1", "real"])
|
31
|
+
expect(SampleService::Real.new.list_all_samples("sample2")).to eq(["sample2", "real"])
|
32
|
+
expect(SampleService::Mock.new.list_all_samples("sample3")).to eq(["sample3", "mock"])
|
33
33
|
|
34
34
|
# service access
|
35
|
-
expect(SampleService.new(:key => "value").
|
35
|
+
expect(SampleService.new(:key => "value").list_all_samples("stat")).to eq([{:key => "value"}, "stat", "real"])
|
36
36
|
end
|
37
37
|
end
|
metadata
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cistern
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
5
|
-
prerelease: 6
|
4
|
+
version: 2.0.1
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Josh Lane
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2015-
|
11
|
+
date: 2015-03-05 00:00:00.000000000 Z
|
13
12
|
dependencies: []
|
14
13
|
description: API client framework extracted from Fog
|
15
14
|
email:
|
@@ -18,8 +17,9 @@ executables: []
|
|
18
17
|
extensions: []
|
19
18
|
extra_rdoc_files: []
|
20
19
|
files:
|
21
|
-
- .
|
22
|
-
- .
|
20
|
+
- ".gemrelease"
|
21
|
+
- ".gitignore"
|
22
|
+
- ".travis.yml"
|
23
23
|
- Gemfile
|
24
24
|
- Guardfile
|
25
25
|
- LICENSE.txt
|
@@ -62,27 +62,26 @@ files:
|
|
62
62
|
homepage: http://joshualane.com/cistern
|
63
63
|
licenses:
|
64
64
|
- MIT
|
65
|
+
metadata: {}
|
65
66
|
post_install_message:
|
66
67
|
rdoc_options: []
|
67
68
|
require_paths:
|
68
69
|
- lib
|
69
70
|
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
-
none: false
|
71
71
|
requirements:
|
72
|
-
- -
|
72
|
+
- - ">="
|
73
73
|
- !ruby/object:Gem::Version
|
74
74
|
version: '0'
|
75
75
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
-
none: false
|
77
76
|
requirements:
|
78
|
-
- -
|
77
|
+
- - ">="
|
79
78
|
- !ruby/object:Gem::Version
|
80
|
-
version:
|
79
|
+
version: '0'
|
81
80
|
requirements: []
|
82
81
|
rubyforge_project:
|
83
|
-
rubygems_version:
|
82
|
+
rubygems_version: 2.2.2
|
84
83
|
signing_key:
|
85
|
-
specification_version:
|
84
|
+
specification_version: 4
|
86
85
|
summary: API client framework
|
87
86
|
test_files:
|
88
87
|
- spec/collection_spec.rb
|
@@ -97,3 +96,4 @@ test_files:
|
|
97
96
|
- spec/spec_helper.rb
|
98
97
|
- spec/support/service.rb
|
99
98
|
- spec/wait_for_spec.rb
|
99
|
+
has_rdoc:
|