cistern 2.3.0 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +5 -1
- data/Appraisals +3 -0
- data/CHANGELOG.md +17 -1
- data/Gemfile +2 -0
- data/README.md +319 -127
- data/gemfiles/ruby_lt_2.0.gemfile +24 -0
- data/lib/cistern.rb +1 -0
- data/lib/cistern/attributes.rb +101 -110
- data/lib/cistern/collection.rb +2 -0
- data/lib/cistern/formatter/awesome_print.rb +1 -1
- data/lib/cistern/hash.rb +19 -5
- data/lib/cistern/hash_support.rb +6 -0
- data/lib/cistern/model.rb +1 -0
- data/lib/cistern/request.rb +2 -0
- data/lib/cistern/singular.rb +12 -22
- data/lib/cistern/version.rb +1 -1
- data/spec/attributes_spec.rb +1 -1
- data/spec/coverage_spec.rb +1 -1
- data/spec/hash_spec.rb +35 -0
- data/spec/model_spec.rb +14 -0
- data/spec/singular_spec.rb +25 -17
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a2797ca8edb773a77fe40604024aeca97df9880
|
4
|
+
data.tar.gz: 4bfd89a74eef61fdd0cca096a01465235a40009f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bab52f2a7c09673e54e943da2ada9cacdf8684ca0b3b419c415a8fbc031f5090a9c378ba366adeee9d662770a744975b6732b6715dbc03baccbc0bde93d3fa23
|
7
|
+
data.tar.gz: 805ab1b15511ada7e048f0ebdea1f7e1ca8ab15bfdd3cf7fca417c8662eeef7e4cfd5851381491cc432fcb21f9ece6bd033bd2ea150634195307578b2ec1ab33
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -2,7 +2,6 @@ language: ruby
|
|
2
2
|
rvm:
|
3
3
|
- 2.3.0
|
4
4
|
- 2.2
|
5
|
-
- 1.9
|
6
5
|
bundler_args: "--without development"
|
7
6
|
before_install:
|
8
7
|
- gem install bundler -v "~> 1.10"
|
@@ -12,6 +11,11 @@ notifications:
|
|
12
11
|
sudo: false
|
13
12
|
services:
|
14
13
|
- redis-server
|
14
|
+
matrix:
|
15
|
+
include:
|
16
|
+
- rvm: 1.9
|
17
|
+
gemfile: gemfiles/ruby_lt_2.0.gemfile
|
18
|
+
|
15
19
|
env:
|
16
20
|
matrix:
|
17
21
|
secure: eiDhDgp8jBYKZVOqe891g4StnFsqRXcQwlDSnXthRSuWNMd6oFyFOo/s9aXd9lNFkaTBnSAFrUvywH1t2+H3j+cPMn/91W2s2Ldc+SxVxjrY3mWyr6NIudro/rdK7nIfIcWJFtm0teSXg/1nRPZt0qlXc4bZmvwvN3T8MvdgI2I=
|
data/Appraisals
ADDED
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,23 @@
|
|
2
2
|
|
3
3
|
## [Unreleased](https://github.com/lanej/cistern/tree/HEAD)
|
4
4
|
|
5
|
-
[Full Changelog](https://github.com/lanej/cistern/compare/v2.
|
5
|
+
[Full Changelog](https://github.com/lanej/cistern/compare/v2.3.0...HEAD)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- refactor\(singular\): a collection-less model [\#61](https://github.com/lanej/cistern/pull/61) ([lanej](https://github.com/lanej))
|
10
|
+
|
11
|
+
**Merged pull requests:**
|
12
|
+
|
13
|
+
- test\(ci\): use `appraisal` for gemfile splitting [\#63](https://github.com/lanej/cistern/pull/63) ([lanej](https://github.com/lanej))
|
14
|
+
- modernize README [\#62](https://github.com/lanej/cistern/pull/62) ([lanej](https://github.com/lanej))
|
15
|
+
- feature\(hash\): refactor implementation, mixin helpers [\#60](https://github.com/lanej/cistern/pull/60) ([lanej](https://github.com/lanej))
|
16
|
+
- refactor\(attributes\): overhaul internals [\#59](https://github.com/lanej/cistern/pull/59) ([lanej](https://github.com/lanej))
|
17
|
+
- fix\(attributes\): allow string types to be nil [\#58](https://github.com/lanej/cistern/pull/58) ([lanej](https://github.com/lanej))
|
18
|
+
- Tweaks for Readme [\#56](https://github.com/lanej/cistern/pull/56) ([jaw6](https://github.com/jaw6))
|
19
|
+
|
20
|
+
## [v2.3.0](https://github.com/lanej/cistern/tree/v2.3.0) (2016-05-17)
|
21
|
+
[Full Changelog](https://github.com/lanej/cistern/compare/v2.2.7...v2.3.0)
|
6
22
|
|
7
23
|
**Implemented enhancements:**
|
8
24
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -9,14 +9,18 @@ Cistern helps you consistently build your API clients and faciliates building mo
|
|
9
9
|
|
10
10
|
## Usage
|
11
11
|
|
12
|
-
###
|
12
|
+
### Notice: Cistern 3.0
|
13
|
+
|
14
|
+
Cistern 3.0 will change the way Cistern interacts with your `Request`, `Collection` and `Model` classes.
|
13
15
|
|
14
|
-
|
16
|
+
Prior to 3.0, your `Request`, `Collection` and `Model` classes would have inherited from `<service>::Client::Request`, `<service>::Client::Collection` and `<service>::Client::Model` classes, respectively.
|
15
17
|
|
16
|
-
|
18
|
+
In cistern `~> 3.0`, the default will be for `Request`, `Collection` and `Model` classes to instead include their respective `<service>::Client` modules.
|
19
|
+
|
20
|
+
If you want to be forwards-compatible today, you can configure your client by using `Cistern::Client.with`
|
17
21
|
|
18
22
|
```ruby
|
19
|
-
class
|
23
|
+
class Blog
|
20
24
|
include Cistern::Client.with(interface: :module)
|
21
25
|
end
|
22
26
|
```
|
@@ -24,95 +28,133 @@ end
|
|
24
28
|
Now request classes would look like:
|
25
29
|
|
26
30
|
```ruby
|
27
|
-
class
|
28
|
-
include
|
31
|
+
class Blog::GetPost
|
32
|
+
include Blog::Request
|
29
33
|
|
30
34
|
def real
|
31
|
-
"
|
35
|
+
"post"
|
32
36
|
end
|
33
37
|
end
|
34
38
|
```
|
35
39
|
|
36
|
-
Other options include `:collection`, `:request`, and `:model`. This options define the name of module or class interface for the service component.
|
37
40
|
|
38
|
-
|
41
|
+
### Service
|
42
|
+
|
43
|
+
This represents the remote service that you are wrapping. If the service name is `blog` then a good name is `Blog`.
|
39
44
|
|
40
|
-
|
45
|
+
Service initialization parameters are enumerated by `requires` and `recognizes`. Parameters defined using `recognizes` are optional.
|
41
46
|
|
42
47
|
```ruby
|
43
|
-
|
44
|
-
|
48
|
+
# lib/blog.rb
|
49
|
+
class Blog
|
50
|
+
include Cistern::Client
|
51
|
+
|
52
|
+
requires :hmac_id, :hmac_secret
|
53
|
+
recognizes :url
|
45
54
|
end
|
55
|
+
|
56
|
+
# Acceptable
|
57
|
+
Blog.new(hmac_id: "1", hmac_secret: "2") # Blog::Real
|
58
|
+
Blog.new(hmac_id: "1", hmac_secret: "2", url: "http://example.org") # Blog::Real
|
59
|
+
|
60
|
+
# ArgumentError
|
61
|
+
Blog.new(hmac_id: "1", url: "http://example.org")
|
62
|
+
Blog.new(hmac_id: "1")
|
46
63
|
```
|
47
64
|
|
48
|
-
|
65
|
+
Cistern will define for you two classes, `Mock` and `Real`. Create the corresponding files and initialzers for your
|
66
|
+
new service.
|
49
67
|
|
50
68
|
```ruby
|
51
|
-
|
52
|
-
|
69
|
+
# lib/blog/real.rb
|
70
|
+
class Blog::Real
|
71
|
+
attr_reader :url, :connection
|
72
|
+
|
73
|
+
def initialize(attributes)
|
74
|
+
@hmac_id, @hmac_secret = attributes.values_at(:hmac_id, :hmac_secret)
|
75
|
+
@url = attributes[:url] || 'http://blog.example.org'
|
76
|
+
@connection = Faraday.new(url)
|
77
|
+
end
|
53
78
|
end
|
54
79
|
```
|
55
80
|
|
56
|
-
while living on a `Prayer`
|
57
|
-
|
58
81
|
```ruby
|
59
|
-
|
60
|
-
|
61
|
-
|
82
|
+
# lib/blog/mock.rb
|
83
|
+
class Blog::Mock
|
84
|
+
attr_reader :url
|
85
|
+
|
86
|
+
def initialize(attributes)
|
87
|
+
@url = attributes[:url]
|
62
88
|
end
|
63
89
|
end
|
64
90
|
```
|
65
91
|
|
92
|
+
### Mocking
|
66
93
|
|
67
|
-
|
94
|
+
Cistern strongly encourages you to generate mock support for your service. Mocking can be enabled using `mock!`.
|
68
95
|
|
69
|
-
|
96
|
+
```ruby
|
97
|
+
Blog.mocking? # falsey
|
98
|
+
real = Blog.new # Blog::Real
|
99
|
+
Blog.mock!
|
100
|
+
Blog.mocking? # true
|
101
|
+
fake = Blog.new # Blog::Mock
|
102
|
+
Blog.unmock!
|
103
|
+
Blog.mocking? # false
|
104
|
+
real.is_a?(Blog::Real) # true
|
105
|
+
fake.is_a?(Blog::Mock) # true
|
106
|
+
```
|
70
107
|
|
71
|
-
|
108
|
+
### Working with data
|
72
109
|
|
73
|
-
|
74
|
-
class Foo::Client
|
75
|
-
include Cistern::Client
|
110
|
+
`Cistern::Hash` contains many useful functions for working with data normalization and transformation.
|
76
111
|
|
77
|
-
|
78
|
-
recognizes :url
|
79
|
-
end
|
112
|
+
**#stringify_keys**
|
80
113
|
|
81
|
-
|
82
|
-
|
83
|
-
|
114
|
+
```ruby
|
115
|
+
# anywhere
|
116
|
+
Cistern::Hash.stringify_keys({a: 1, b: 2}) #=> {'a' => 1, 'b' => 2}
|
117
|
+
# within a Resource
|
118
|
+
hash_stringify_keys({a: 1, b: 2}) #=> {'a' => 1, 'b' => 2}
|
119
|
+
```
|
84
120
|
|
85
|
-
|
86
|
-
|
87
|
-
|
121
|
+
**#slice**
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
# anywhere
|
125
|
+
Cistern::Hash.slice({a: 1, b: 2, c: 3}, :a, :c) #=> {a: 1, c: 3}
|
126
|
+
# within a Resource
|
127
|
+
hash_slice({a: 1, b: 2, c: 3}, :a, :c) #=> {a: 1, c: 3}
|
88
128
|
```
|
89
129
|
|
90
|
-
|
130
|
+
**#except**
|
91
131
|
|
92
|
-
|
132
|
+
```ruby
|
133
|
+
# anywhere
|
134
|
+
Cistern::Hash.except({a: 1, b: 2}, :a) #=> {b: 2}
|
135
|
+
# within a Resource
|
136
|
+
hash_except({a: 1, b: 2}, :a) #=> {b: 2}
|
137
|
+
```
|
93
138
|
|
94
|
-
|
139
|
+
|
140
|
+
**#except!**
|
95
141
|
|
96
142
|
```ruby
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
fake = Foo::Client.new # Foo::Client::Mock
|
102
|
-
Foo::Client.unmock!
|
103
|
-
Foo::Client.mocking? # false
|
104
|
-
real.is_a?(Foo::Client::Real) # true
|
105
|
-
fake.is_a?(Foo::Client::Mock) # true
|
143
|
+
# same as #except but modify specified Hash in-place
|
144
|
+
Cistern::Hash.except!({:a => 1, :b => 2}, :a) #=> {:b => 2}
|
145
|
+
# within a Resource
|
146
|
+
hash_except!({:a => 1, :b => 2}, :a) #=> {:b => 2}
|
106
147
|
```
|
107
148
|
|
149
|
+
|
108
150
|
### Requests
|
109
151
|
|
110
152
|
Requests are defined by subclassing `#{service}::Request`.
|
111
153
|
|
112
|
-
* `cistern` represents the associated `
|
154
|
+
* `cistern` represents the associated `Blog` instance.
|
113
155
|
|
114
156
|
```ruby
|
115
|
-
class
|
157
|
+
class Blog::GetPost < Blog::Request
|
116
158
|
def real(params)
|
117
159
|
# make a real request
|
118
160
|
"i'm real"
|
@@ -124,126 +166,240 @@ class Foo::Client::GetBar < Foo::Client::Request
|
|
124
166
|
end
|
125
167
|
end
|
126
168
|
|
127
|
-
|
169
|
+
Blog.new.get_post # "i'm real"
|
128
170
|
```
|
129
171
|
|
130
172
|
The `#cistern_method` function allows you to specify the name of the generated method.
|
131
173
|
|
132
174
|
```ruby
|
133
|
-
class
|
134
|
-
cistern_method :
|
175
|
+
class Blog::GetPosts < Blog::Request
|
176
|
+
cistern_method :get_all_the_posts
|
135
177
|
|
136
178
|
def real(params)
|
137
|
-
"all the
|
179
|
+
"all the posts"
|
138
180
|
end
|
139
181
|
end
|
140
182
|
|
141
|
-
|
142
|
-
|
183
|
+
Blog.new.respond_to?(:get_posts) # false
|
184
|
+
Blog.new.get_all_the_posts # "all the posts"
|
143
185
|
```
|
144
186
|
|
145
187
|
All declared requests can be listed via `Cistern::Client#requests`.
|
146
188
|
|
147
189
|
```ruby
|
148
|
-
|
190
|
+
Blog.requests # => [Blog::GetPosts, Blog::GetPost]
|
149
191
|
```
|
150
192
|
|
151
193
|
### Models
|
152
194
|
|
153
|
-
* `cistern` represents the associated `
|
154
|
-
* `collection` represents the related collection
|
195
|
+
* `cistern` represents the associated `Blog::Real` or `Blog::Mock` instance.
|
196
|
+
* `collection` represents the related collection.
|
155
197
|
* `new_record?` checks if `identity` is present
|
156
198
|
* `requires(*requirements)` throws `ArgumentError` if an attribute matching a requirement isn't set
|
199
|
+
* `requires_one(*requirements)` throws `ArgumentError` if no attribute matching requirement is set
|
157
200
|
* `merge_attributes(attributes)` sets attributes for the current model instance
|
201
|
+
* `dirty_attributes` represents attributes changed since the last `merge_attributes`. This is useful for using `update`
|
158
202
|
|
159
203
|
#### Attributes
|
160
204
|
|
161
|
-
|
205
|
+
Cistern attributes are designed to make your model flexible and developer friendly.
|
206
|
+
|
207
|
+
* `attribute :post_id` adds an accessor to the model.
|
208
|
+
```ruby
|
209
|
+
attribute :post_id
|
210
|
+
|
211
|
+
model.post_id #=> nil
|
212
|
+
model.post_id = 1 #=> 1
|
213
|
+
model.post_id #=> 1
|
214
|
+
model.attributes #=> {'post_id' => 1 }
|
215
|
+
model.dirty_attributes #=> {'post_id' => 1 }
|
216
|
+
```
|
217
|
+
* `identity` represents the name of the model's unique identifier. As this is not always available, it is not required.
|
218
|
+
```ruby
|
219
|
+
identity :name
|
220
|
+
```
|
221
|
+
|
222
|
+
creates an attribute called `name` that is aliased to identity.
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
model.name = 'michelle'
|
226
|
+
|
227
|
+
model.identity #=> 'michelle'
|
228
|
+
model.name #=> 'michelle'
|
229
|
+
model.attributes #=> { 'name' => 'michelle' }
|
230
|
+
```
|
231
|
+
* `:aliases` or `:alias` allows a attribute key to be different then a response key.
|
232
|
+
```ruby
|
233
|
+
attribute :post_id, alias: "post"
|
234
|
+
```
|
235
|
+
|
236
|
+
allows
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
model.merge_attributes("post" => 1)
|
240
|
+
model.post_id #=> 1
|
241
|
+
```
|
242
|
+
* `:type` automatically casts the attribute do the specified type.
|
243
|
+
```ruby
|
244
|
+
attribute :private_ips, type: :array
|
245
|
+
|
246
|
+
model.merge_attributes("private_ips" => 2)
|
247
|
+
model.private_ips #=> [2]
|
248
|
+
```
|
249
|
+
* `:squash` traverses nested hashes for a key.
|
250
|
+
```ruby
|
251
|
+
attribute :post_id, aliases: "post", squash: "id"
|
252
|
+
|
253
|
+
model.merge_attributes("post" => {"id" => 3})
|
254
|
+
model.post_id #=> 3
|
255
|
+
```
|
256
|
+
|
257
|
+
#### Persistence
|
258
|
+
|
259
|
+
* `save` is used to persist the model into the remote service. `save` is responsible for determining if the operation is an update to an existing resource or a new resource.
|
260
|
+
* `reload` is used to grab the latest data and merge it into the model. `reload` uses `collection.get(identity)` by default.
|
261
|
+
* `update(attrs)` is a `merge_attributes` and a `save`. When calling `update`, `dirty_attributes` can be used to persist only what has changed locally.
|
162
262
|
|
163
|
-
`identity` is special but not required.
|
164
263
|
|
165
|
-
|
166
|
-
|
167
|
-
* `: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`
|
168
|
-
* `: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]`
|
169
|
-
* `: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`
|
170
|
-
|
171
|
-
Example
|
264
|
+
For example:
|
172
265
|
|
173
266
|
```ruby
|
174
|
-
class
|
175
|
-
identity :id
|
267
|
+
class Blog::Post < Blog::Model
|
268
|
+
identity :id, type: :integer
|
176
269
|
|
177
|
-
attribute :
|
178
|
-
attribute :
|
179
|
-
attribute :
|
270
|
+
attribute :body
|
271
|
+
attribute :author_id, aliases: "author", squash: "id"
|
272
|
+
attribute :deleted_at, type: :time
|
180
273
|
|
181
274
|
def destroy
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
self.cistern.destroy_bar(params).body["request"]
|
275
|
+
requires :identity
|
276
|
+
|
277
|
+
data = cistern.destroy_post(params).body['post']
|
186
278
|
end
|
187
279
|
|
188
280
|
def save
|
189
|
-
requires :
|
281
|
+
requires :author_id
|
190
282
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
}
|
283
|
+
response = if new_record?
|
284
|
+
cistern.create_post(attributes)
|
285
|
+
else
|
286
|
+
cistern.update_post(dirty_attributes)
|
287
|
+
end
|
197
288
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
289
|
+
merge_attributes(response.body['post'])
|
290
|
+
end
|
291
|
+
end
|
292
|
+
```
|
202
293
|
|
203
|
-
|
204
|
-
|
294
|
+
Usage:
|
295
|
+
|
296
|
+
**create**
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
blog.posts.create(author_id: 1, body: 'text')
|
300
|
+
```
|
301
|
+
|
302
|
+
is equal to
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
post = blog.posts.new(author_id: 1, body: 'text')
|
306
|
+
post.save
|
307
|
+
```
|
308
|
+
|
309
|
+
**update**
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
post = blog.posts.get(1)
|
313
|
+
post.update(author_id: 1) #=> calls #save with #dirty_attributes == { 'author_id' => 1 }
|
314
|
+
post.author_id #=> 1
|
315
|
+
```
|
316
|
+
|
317
|
+
### Singular
|
318
|
+
|
319
|
+
Singular resources do not have an associated collection and the model contains the `get` and`save` methods.
|
320
|
+
|
321
|
+
For instance:
|
322
|
+
|
323
|
+
```ruby
|
324
|
+
class Blog::PostData
|
325
|
+
include Blog::Singular
|
326
|
+
|
327
|
+
attribute :post_id, type: :integer
|
328
|
+
attribute :upvotes, type: :integer
|
329
|
+
attribute :views, type: :integer
|
330
|
+
attribute :rating, type: :float
|
331
|
+
|
332
|
+
def get
|
333
|
+
response = cistern.get_post_data(post_id)
|
334
|
+
merge_attributes(response.body['data'])
|
335
|
+
end
|
336
|
+
|
337
|
+
def save
|
338
|
+
response = cistern.update_post_data(post_id, dirty_attributes)
|
339
|
+
merge_attributes(response.data['data'])
|
205
340
|
end
|
206
341
|
end
|
207
342
|
```
|
208
343
|
|
344
|
+
Singular resources often hang off of other models or collections.
|
345
|
+
|
346
|
+
```ruby
|
347
|
+
class Blog::Post
|
348
|
+
include Cistern::Model
|
349
|
+
|
350
|
+
identity :id, type: :integer
|
351
|
+
|
352
|
+
def data
|
353
|
+
cistern.post_data(post_id: identity).load
|
354
|
+
end
|
355
|
+
end
|
356
|
+
```
|
357
|
+
|
358
|
+
They are special cases of Models and have similar interfaces.
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
post.data.views #=> nil
|
362
|
+
post.data.update(views: 3)
|
363
|
+
post.data.views #=> 3
|
364
|
+
```
|
365
|
+
|
366
|
+
|
209
367
|
### Collection
|
210
368
|
|
211
|
-
* `model` tells Cistern which class
|
212
|
-
* `cistern` is the associated `
|
369
|
+
* `model` tells Cistern which resource class this collection represents.
|
370
|
+
* `cistern` is the associated `Blog::Real` or `Blog::Mock` instance
|
213
371
|
* `attribute` specifications on collections are allowed. use `merge_attributes`
|
214
372
|
* `load` consumes an Array of data and constructs matching `model` instances
|
215
373
|
|
216
374
|
```ruby
|
217
|
-
class
|
375
|
+
class Blog::Posts < Blog::Collection
|
218
376
|
|
219
377
|
attribute :count, type: :integer
|
220
378
|
|
221
|
-
model
|
379
|
+
model Blog::Post
|
222
380
|
|
223
381
|
def all(params = {})
|
224
|
-
response = cistern.
|
382
|
+
response = cistern.get_posts(params)
|
225
383
|
|
226
384
|
data = response.body
|
227
385
|
|
228
|
-
|
229
|
-
|
386
|
+
load(data["posts"]) # store post records in collection
|
387
|
+
merge_attributes(data) # store any other attributes of the response on the collection
|
230
388
|
end
|
231
389
|
|
232
|
-
def discover(
|
390
|
+
def discover(author_id, options={})
|
233
391
|
params = {
|
234
|
-
"
|
392
|
+
"author_id" => author_id,
|
235
393
|
}
|
236
|
-
params.merge!("
|
394
|
+
params.merge!("topic" => options[:topic]) if options.key?(:topic)
|
237
395
|
|
238
|
-
cistern.
|
396
|
+
cistern.blogs.new(cistern.discover_blog(params).body["blog"])
|
239
397
|
end
|
240
398
|
|
241
399
|
def get(id)
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
nil
|
246
|
-
end
|
400
|
+
data = cistern.get_post(id).body["post"]
|
401
|
+
|
402
|
+
new(data) if data
|
247
403
|
end
|
248
404
|
end
|
249
405
|
```
|
@@ -253,16 +409,16 @@ end
|
|
253
409
|
A uniform interface for mock data is mixed into the `Mock` class by default.
|
254
410
|
|
255
411
|
```ruby
|
256
|
-
|
257
|
-
client =
|
412
|
+
Blog.mock!
|
413
|
+
client = Blog.new # Blog::Mock
|
258
414
|
client.data # Cistern::Data::Hash
|
259
|
-
client.data["
|
415
|
+
client.data["posts"] += ["x"] # ["x"]
|
260
416
|
```
|
261
417
|
|
262
418
|
Mock data is class-level by default
|
263
419
|
|
264
420
|
```ruby
|
265
|
-
|
421
|
+
Blog::Mock.data["posts"] # ["x"]
|
266
422
|
```
|
267
423
|
|
268
424
|
`reset!` dimisses the `data` object.
|
@@ -270,18 +426,18 @@ Foo::Client::Mock.data["bars"] # ["x"]
|
|
270
426
|
```ruby
|
271
427
|
client.data.object_id # 70199868585600
|
272
428
|
client.reset!
|
273
|
-
client.data["
|
429
|
+
client.data["posts"] # []
|
274
430
|
client.data.object_id # 70199868566840
|
275
431
|
```
|
276
432
|
|
277
433
|
`clear` removes existing keys and values but keeps the same object.
|
278
434
|
|
279
435
|
```ruby
|
280
|
-
client.data["
|
281
|
-
client.data.object_id
|
436
|
+
client.data["posts"] += ["y"] # ["y"]
|
437
|
+
client.data.object_id # 70199868378300
|
282
438
|
client.clear
|
283
|
-
client.data["
|
284
|
-
client.data.object_id
|
439
|
+
client.data["posts"] # []
|
440
|
+
client.data.object_id # 70199868378300
|
285
441
|
```
|
286
442
|
|
287
443
|
* `store` and `[]=` write
|
@@ -290,7 +446,7 @@ client.data.object_id # 70199868378300
|
|
290
446
|
You can make the service bypass Cistern's mock data structures by simply creating a `self.data` function in your service `Mock` declaration.
|
291
447
|
|
292
448
|
```ruby
|
293
|
-
class
|
449
|
+
class Blog
|
294
450
|
include Cistern::Client
|
295
451
|
|
296
452
|
class Mock
|
@@ -330,22 +486,58 @@ Dirty attributes are tracked and cleared when `merge_attributes` is called.
|
|
330
486
|
|
331
487
|
|
332
488
|
```ruby
|
333
|
-
|
489
|
+
post = Blog::Post.new(id: 1, flavor: "x") # => <#Blog::Post>
|
490
|
+
|
491
|
+
post.dirty? # => false
|
492
|
+
post.changed # => {}
|
493
|
+
post.dirty_attributes # => {}
|
494
|
+
|
495
|
+
post.flavor = "y"
|
334
496
|
|
335
|
-
|
336
|
-
|
337
|
-
|
497
|
+
post.dirty? # => true
|
498
|
+
post.changed # => {flavor: ["x", "y"]}
|
499
|
+
post.dirty_attributes # => {flavor: "y"}
|
500
|
+
|
501
|
+
post.save
|
502
|
+
post.dirty? # => false
|
503
|
+
post.changed # => {}
|
504
|
+
post.dirty_attributes # => {}
|
505
|
+
```
|
506
|
+
|
507
|
+
### Custom Architecture
|
338
508
|
|
339
|
-
|
509
|
+
When configuring your client, you can use `:collection`, `:request`, and `:model` options to define the name of module or class interface for the service component.
|
340
510
|
|
341
|
-
|
342
|
-
bar.changed # => {flavor: ["x", "y"]}
|
343
|
-
bar.dirty_attributes # => {flavor: "y"}
|
511
|
+
For example: if you'd `Request` is to be used for a model, then the `Request` component name can be remapped to `Demand`
|
344
512
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
513
|
+
For example:
|
514
|
+
|
515
|
+
```ruby
|
516
|
+
class Blog
|
517
|
+
include Cistern::Client.with(interface: :modules, request: "Demand")
|
518
|
+
end
|
519
|
+
```
|
520
|
+
|
521
|
+
allows a model named `Request` to exist
|
522
|
+
|
523
|
+
```ruby
|
524
|
+
class Blog::Request
|
525
|
+
include Blog::Model
|
526
|
+
|
527
|
+
identity :jovi
|
528
|
+
end
|
529
|
+
```
|
530
|
+
|
531
|
+
while living on a `Demand`
|
532
|
+
|
533
|
+
```ruby
|
534
|
+
class Blog::GetPost
|
535
|
+
include Blog::Demand
|
536
|
+
|
537
|
+
def real
|
538
|
+
cistern.request.get("/wing")
|
539
|
+
end
|
540
|
+
end
|
349
541
|
```
|
350
542
|
|
351
543
|
## Examples
|