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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a4ab124cf563b30d3d4a15c1b8b1ada69a30b434
4
- data.tar.gz: e52206d15a0da143a293cfacd1ed105f13dcda2b
3
+ metadata.gz: 8a2797ca8edb773a77fe40604024aeca97df9880
4
+ data.tar.gz: 4bfd89a74eef61fdd0cca096a01465235a40009f
5
5
  SHA512:
6
- metadata.gz: 189206346dc0a43b654527bf72d31fba3accb7327b0355ede31a713e3b94a27a05d92e153e84489cd79afb78d99d5d0060b1f4dac44754f5daad58956e42b0c8
7
- data.tar.gz: 3440beaa800a967faadca537eee39661294ff10ba681a1d7805733e16714627990d00d4bee942b60f58870873b451bc70577cc1fd7827bcac2098a77f07113f9
6
+ metadata.gz: bab52f2a7c09673e54e943da2ada9cacdf8684ca0b3b419c415a8fbc031f5090a9c378ba366adeee9d662770a744975b6732b6715dbc03baccbc0bde93d3fa23
7
+ data.tar.gz: 805ab1b15511ada7e048f0ebdea1f7e1ca8ab15bfdd3cf7fca417c8662eeef7e4cfd5851381491cc432fcb21f9ece6bd033bd2ea150634195307578b2ec1ab33
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  .rspec
19
+ gemfiles/*.lock
@@ -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=
@@ -0,0 +1,3 @@
1
+ appraise "ruby lt 2.0" do
2
+ gem "json", '~> 1.8'
3
+ end
@@ -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.2.7...HEAD)
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
@@ -3,6 +3,8 @@ source "https://rubygems.org"
3
3
  # Specify your gem"s dependencies in cistern.gemspec
4
4
  gemspec
5
5
 
6
+ gem "appraisal"
7
+
6
8
  group :test do
7
9
  gem "guard-rspec", "~> 4.2", require: false
8
10
  gem "guard-bundler", "~> 2.0", require: false
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
- ### Custom Architecture
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
- By default a service's `Request`, `Collection`, and `Model` are all classes. In cistern `~> 3.0`, the default will be modules.
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
- You can modify your client's architecture to be forwards compatible by using `Cistern::Client.with`
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 Foo::Client
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 Foo::GetBar
28
- include Foo::Request
31
+ class Blog::GetPost
32
+ include Blog::Request
29
33
 
30
34
  def real
31
- "bar"
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
- If `Request` is to reserved for a model, then the `Request` component name can be remapped to `Prayer`
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
- For example:
45
+ Service initialization parameters are enumerated by `requires` and `recognizes`. Parameters defined using `recognizes` are optional.
41
46
 
42
47
  ```ruby
43
- class Foo::Client
44
- include Cistern::Client.with(request: "Prayer")
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
- allows a model named `Request` to exist
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
- class Foo::Request < Foo::Model
52
- identity :jovi
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
- class Foo::GetBar < Foo::Prayer
60
- def real
61
- cistern.request.get("/wing")
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
- ### Service
94
+ Cistern strongly encourages you to generate mock support for your service. Mocking can be enabled using `mock!`.
68
95
 
69
- This represents the remote service that you are wrapping. If the service name is `foo` then a good name is `Foo::Client`.
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
- Service initialization parameters are enumerated by `requires` and `recognizes`. Parameters defined using `recognizes` are optional.
108
+ ### Working with data
72
109
 
73
- ```ruby
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
- requires :hmac_id, :hmac_secret
78
- recognizes :url
79
- end
112
+ **#stringify_keys**
80
113
 
81
- # Acceptable
82
- Foo::Client.new(hmac_id: "1", hmac_secret: "2") # Foo::Client::Real
83
- Foo::Client.new(hmac_id: "1", hmac_secret: "2", url: "http://example.org") # Foo::Client::Real
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
- # ArgumentError
86
- Foo::Client.new(hmac_id: "1", url: "http://example.org")
87
- Foo::Client.new(hmac_id: "1")
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
- Cistern will define for you two classes, `Mock` and `Real`.
130
+ **#except**
91
131
 
92
- ### Mocking
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
- Cistern strongly encourages you to generate mock support for your service. Mocking can be enabled using `mock!`.
139
+
140
+ **#except!**
95
141
 
96
142
  ```ruby
97
- Foo::Client.mocking? # falsey
98
- real = Foo::Client.new # Foo::Client::Real
99
- Foo::Client.mock!
100
- Foo::Client.mocking? # true
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 `Foo::Client` instance.
154
+ * `cistern` represents the associated `Blog` instance.
113
155
 
114
156
  ```ruby
115
- class Foo::Client::GetBar < Foo::Client::Request
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
- Foo::Client.new.get_bar # "i'm real"
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 Foo::Client::GetBars < Foo::Client::Request
134
- cistern_method :get_all_the_bars
175
+ class Blog::GetPosts < Blog::Request
176
+ cistern_method :get_all_the_posts
135
177
 
136
178
  def real(params)
137
- "all the bars"
179
+ "all the posts"
138
180
  end
139
181
  end
140
182
 
141
- Foo::Client.new.respond_to?(:get_bars) # false
142
- Foo::Client.new.get_all_the_bars # "all the bars"
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
- Foo::Client.requests # => [Foo::Client::GetBars, Foo::Client::GetBar]
190
+ Blog.requests # => [Blog::GetPosts, Blog::GetPost]
149
191
  ```
150
192
 
151
193
  ### Models
152
194
 
153
- * `cistern` represents the associated `Foo::Client` instance.
154
- * `collection` represents the related collection (if applicable)
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
- Attributes are designed to be a flexible way of parsing service request responses.
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
- `attribute :flavor` makes `Foo::Client::Bar.new.respond_to?(:flavor)`
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 Foo::Client::Bar < Foo::Client::Model
175
- identity :id
267
+ class Blog::Post < Blog::Model
268
+ identity :id, type: :integer
176
269
 
177
- attribute :flavor
178
- attribute :keypair_id, aliases: "keypair", squash: "id"
179
- attribute :private_ips, type: :array
270
+ attribute :body
271
+ attribute :author_id, aliases: "author", squash: "id"
272
+ attribute :deleted_at, type: :time
180
273
 
181
274
  def destroy
182
- params = {
183
- "id" => self.identity
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 :keypair_id
281
+ requires :author_id
190
282
 
191
- params = {
192
- "keypair" => self.keypair_id,
193
- "bar" => {
194
- "flavor" => self.flavor,
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
- if new_record?
199
- merge_attributes(cistern.create_bar(params).body["bar"])
200
- else
201
- requires :identity
289
+ merge_attributes(response.body['post'])
290
+ end
291
+ end
292
+ ```
202
293
 
203
- merge_attributes(cistern.update_bar(params).body["bar"])
204
- end
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 is contained within the collection.
212
- * `cistern` is the associated `Foo::Client` instance
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 Foo::Client::Bars < Foo::Client::Collection
375
+ class Blog::Posts < Blog::Collection
218
376
 
219
377
  attribute :count, type: :integer
220
378
 
221
- model Foo::Client::Bar
379
+ model Blog::Post
222
380
 
223
381
  def all(params = {})
224
- response = cistern.get_bars(params)
382
+ response = cistern.get_posts(params)
225
383
 
226
384
  data = response.body
227
385
 
228
- self.load(data["bars"]) # store bar records in collection
229
- self.merge_attributes(data) # store any other attributes of the response on the collection
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(provisioned_id, options={})
390
+ def discover(author_id, options={})
233
391
  params = {
234
- "provisioned_id" => provisioned_id,
392
+ "author_id" => author_id,
235
393
  }
236
- params.merge!("location" => options[:location]) if options.key?(:location)
394
+ params.merge!("topic" => options[:topic]) if options.key?(:topic)
237
395
 
238
- cistern.requests.new(cistern.discover_bar(params).body["request"])
396
+ cistern.blogs.new(cistern.discover_blog(params).body["blog"])
239
397
  end
240
398
 
241
399
  def get(id)
242
- if data = cistern.get_bar("id" => id).body["bar"]
243
- new(data)
244
- else
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
- Foo::Client.mock!
257
- client = Foo::Client.new # Foo::Client::Mock
412
+ Blog.mock!
413
+ client = Blog.new # Blog::Mock
258
414
  client.data # Cistern::Data::Hash
259
- client.data["bars"] += ["x"] # ["x"]
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
- Foo::Client::Mock.data["bars"] # ["x"]
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["bars"] # []
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["bars"] += ["y"] # ["y"]
281
- client.data.object_id # 70199868378300
436
+ client.data["posts"] += ["y"] # ["y"]
437
+ client.data.object_id # 70199868378300
282
438
  client.clear
283
- client.data["bars"] # []
284
- client.data.object_id # 70199868378300
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 Foo::Client
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
- bar = Foo::Client::Bar.new(id: 1, flavor: "x") # => <#Foo::Client::Bar>
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
- bar.dirty? # => false
336
- bar.changed # => {}
337
- bar.dirty_attributes # => {}
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
- bar.flavor = "y"
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
- bar.dirty? # => true
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
- bar.save
346
- bar.dirty? # => false
347
- bar.changed # => {}
348
- bar.dirty_attributes # => {}
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