cistern 2.3.0 → 2.4.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 +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
|