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 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