cistern 0.10.2 → 0.11.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: 654c51d23fb889c16d378ffd6b0517dd8a9bea01
4
- data.tar.gz: bb865084d2e05d0f07647ca6cfc193aae4ba277a
3
+ metadata.gz: e00bac7d299cc0636b1ac4b5ca42958de6f96f41
4
+ data.tar.gz: 0f83891e9e0bc7f9cdcbdb8ad9c0280dfb969ae3
5
5
  SHA512:
6
- metadata.gz: 2686e1a240fc0bbca101c446c09a5bd06eed8dd76e15c62243be044afa0316f84d8b5eadefcc8b22b092e3024d155da155a0db176776ab75ca3d4a3391641fa6
7
- data.tar.gz: 9743c3688790d9b67ba0c24528dced11f0559126eb821103f1eb5d56c13fd1cdb7494b6f0e0d4956fddd3415291f99aac199f1356f733c9d6251494ae08dfe77
6
+ metadata.gz: d8e0baa62cd55e6f8cafc7ef6401d1c8a6f8a8bcfafd4fb19cb850ae6e06ba76eacdc55912c6780eddc53729b162db855db6d8986ae7ac25463937eac96c7fb3
7
+ data.tar.gz: 4ec77508143ddad1788aff2159711eaa49301bafb765eecdfda0c498d02c0e4969449646eb6990daf1b6e7f899374043dd176869261867f4105302238242a1f5
data/README.md CHANGED
@@ -15,154 +15,177 @@ This represents the remote service that you are wrapping. If the service name is
15
15
 
16
16
  Requests are enumerated using the `request` method and required immediately via the relative path specified via `request_path`.
17
17
 
18
- class Foo::Client < Cistern::Service
19
- request_path "my-foo/requests"
18
+ ```ruby
19
+ class Foo::Client < Cistern::Service
20
+ request_path "my-foo/requests"
20
21
 
21
- request :get_bar # require my-foo/requests/get_bar.rb
22
- request :get_bars # require my-foo/requests/get_bars.rb
22
+ request :get_bar # require my-foo/requests/get_bar.rb
23
+ request :get_bars # require my-foo/requests/get_bars.rb
23
24
 
24
- class Real
25
- def request(url)
26
- Net::HTTP.get(url)
27
- end
28
- end
25
+ class Real
26
+ def request(url)
27
+ Net::HTTP.get(url)
29
28
  end
29
+ end
30
+ end
31
+ ```
30
32
 
31
33
 
32
34
  <!--todo move to a request section-->
33
35
  A request is method defined within the context of service and mode (Real or Mock). Defining requests within the service mock class is optional.
34
36
 
35
- # my-foo/requests/get_bar.rb
36
- class Foo::Client
37
- class Real
38
- def get_bar(bar_id)
39
- request("http://example.org/bar/#{bar_id}")
40
- end
41
- end # Real
42
-
43
- # optional, but encouraged
44
- class Mock
45
- def get_bars
46
- # do some mock things
47
- end
48
- end # Mock
49
- end # Foo::client
37
+ ```ruby
38
+ # my-foo/requests/get_bar.rb
39
+ class Foo::Client
40
+ class Real
41
+ def get_bar(bar_id)
42
+ request("http://example.org/bar/#{bar_id}")
43
+ end
44
+ end # Real
45
+
46
+ # optional, but encouraged
47
+ class Mock
48
+ def get_bars
49
+ # do some mock things
50
+ end
51
+ end # Mock
52
+ end # Foo::client
53
+ ```
50
54
 
51
55
  All declared requests can be listed via `Cistern::Service#requests`.
52
56
 
53
- Foo::Client.requests # => [:get_bar, :get_bars]
57
+ ```ruby
58
+ Foo::Client.requests # => [:get_bar, :get_bars]
59
+ ```
54
60
 
55
61
  #### Models and Collections
56
62
 
57
63
  Models and collections have declaration semantics similar to requests. Models and collections are enumerated via `model` and `collection` respectively.
58
64
 
59
- class Foo::Client < Cistern::Service
60
- model_path "my-foo/models"
65
+ ```ruby
66
+ class Foo::Client < Cistern::Service
67
+ model_path "my-foo/models"
61
68
 
62
- model :bar # require my-foo/models/bar.rb
63
- collection :bars # require my-foo/models/bars.rb
64
- end
69
+ model :bar # require my-foo/models/bar.rb
70
+ collection :bars # require my-foo/models/bars.rb
71
+ end
72
+ ```
65
73
 
66
74
  #### Initialization
67
75
 
68
76
  Service initialization parameters are enumerated by `requires` and `recognizes`. `recognizes` parameters are optional.
69
77
 
70
- class Foo::Client < Cistern::Service
71
- requires :hmac_id, :hmac_secret
72
- recognizes :url
73
- end
78
+ ```ruby
79
+ class Foo::Client < Cistern::Service
80
+ requires :hmac_id, :hmac_secret
81
+ recognizes :url
82
+ end
74
83
 
75
- # Acceptable
76
- Foo::Client.new(hmac_id: "1", hmac_secret: "2") # Foo::Client::Real
77
- Foo::Client.new(hmac_id: "1", hmac_secret: "2", url: "http://example.org") # Foo::Client::Real
84
+ # Acceptable
85
+ Foo::Client.new(hmac_id: "1", hmac_secret: "2") # Foo::Client::Real
86
+ Foo::Client.new(hmac_id: "1", hmac_secret: "2", url: "http://example.org") # Foo::Client::Real
78
87
 
79
- # ArgumentError
80
- Foo::Client.new(hmac_id: "1", url: "http://example.org")
81
- Foo::Client.new(hmac_id: "1")
88
+ # ArgumentError
89
+ Foo::Client.new(hmac_id: "1", url: "http://example.org")
90
+ Foo::Client.new(hmac_id: "1")
91
+ ```
82
92
 
83
93
 
84
94
  ### Mocking
85
95
 
86
96
  Cistern strongly encourages you to generate mock support for service. Mocking can be enabled using `mock!`.
87
97
 
88
- Foo::Client.mocking? # falsey
89
- real = Foo::Client.new # Foo::Client::Real
90
- Foo::Client.mock!
91
- Foo::Client.mocking? # true
92
- fake = Foo::Client.new # Foo::Client::Mock
93
- Foo::Client.unmock!
94
- Foo::Client.mocking? # false
95
- real.is_a?(Foo::Client::Real) # true
96
- fake.is_a?(Foo::Client::Mock) # true
98
+ ```ruby
99
+ Foo::Client.mocking? # falsey
100
+ real = Foo::Client.new # Foo::Client::Real
101
+ Foo::Client.mock!
102
+ Foo::Client.mocking? # true
103
+ fake = Foo::Client.new # Foo::Client::Mock
104
+ Foo::Client.unmock!
105
+ Foo::Client.mocking? # false
106
+ real.is_a?(Foo::Client::Real) # true
107
+ fake.is_a?(Foo::Client::Mock) # true
108
+ ```
97
109
 
98
110
  #### Data
99
111
 
100
112
  A uniform interface for mock data is mixed into the `Mock` class by default.
101
113
 
102
- Foo::Client.mock!
103
- client = Foo::Client.new # Foo::Client::Mock
104
- client.data # Cistern::Data::Hash
105
- client.data["bars"] += ["x"] # ["x"]
114
+ ```ruby
115
+ Foo::Client.mock!
116
+ client = Foo::Client.new # Foo::Client::Mock
117
+ client.data # Cistern::Data::Hash
118
+ client.data["bars"] += ["x"] # ["x"]
119
+ ```
106
120
 
107
121
  Mock data is class-level by default
108
122
 
109
- Foo::Client::Mock.data["bars"] # ["x"]
123
+ ```ruby
124
+ Foo::Client::Mock.data["bars"] # ["x"]
125
+ ```
110
126
 
111
127
  `reset!` dimisses the `data` object.
112
128
 
113
- client.data.object_id # 70199868585600
114
- client.reset!
115
- client.data["bars"] # []
116
- client.data.object_id # 70199868566840
129
+ ```ruby
130
+ client.data.object_id # 70199868585600
131
+ client.reset!
132
+ client.data["bars"] # []
133
+ client.data.object_id # 70199868566840
134
+ ```
117
135
 
118
136
  `clear` removes existing keys and values but keeps the same object.
119
137
 
120
- client.data["bars"] += ["y"] # ["y"]
121
- client.data.object_id # 70199868378300
122
- client.clear
123
- client.data["bars"] # []
138
+ ```ruby
139
+ client.data["bars"] += ["y"] # ["y"]
140
+ client.data.object_id # 70199868378300
141
+ client.clear
142
+ client.data["bars"] # []
124
143
 
125
- client.data.object_id # 70199868566840
144
+ client.data.object_id # 70199868566840
145
+ ```
126
146
 
127
147
  * `store` and `[]=` write
128
148
  * `fetch` and `[]` read
129
149
 
130
150
  You can make the service bypass Cistern's mock data structures by simply creating a `self.data` function in your service `Mock` declaration.
131
151
 
132
- class Foo::Client < Cistern::Service
133
- class Mock
134
- def self.data
135
- @data ||= {}
136
- end
137
- end
152
+ ```ruby
153
+ class Foo::Client < Cistern::Service
154
+ class Mock
155
+ def self.data
156
+ @data ||= {}
138
157
  end
158
+ end
159
+ end
160
+ ```
139
161
 
140
162
 
141
163
  #### Requests
142
164
 
143
165
  Mock requests should be defined within the contextual `Mock` module and interact with the `data` object directly.
144
166
 
145
- # lib/foo/requests/create_bar.rb
146
- class Foo::Client
147
- class Mock
148
- def create_bar(options={})
149
- id = Foo.random_hex(6)
167
+ ```ruby
168
+ # lib/foo/requests/create_bar.rb
169
+ class Foo::Client
170
+ class Mock
171
+ def create_bar(options={})
172
+ id = Foo.random_hex(6)
150
173
 
151
- bar = {
152
- "id" => id
153
- }.merge(options)
174
+ bar = {
175
+ "id" => id
176
+ }.merge(options)
154
177
 
155
- self.data[:bars][id] = bar
156
-
157
- response(
158
- :body => {"bar" => bar},
159
- :status => 201,
160
- :path => '/bar',
161
- )
162
- end
163
- end # Mock
164
- end # Foo::Client
178
+ self.data[:bars][id] = bar
165
179
 
180
+ response(
181
+ :body => {"bar" => bar},
182
+ :status => 201,
183
+ :path => '/bar',
184
+ )
185
+ end
186
+ end # Mock
187
+ end # Foo::Client
188
+ ```
166
189
 
167
190
  #### Storage
168
191
 
@@ -174,12 +197,14 @@ Currently supported storage backends are:
174
197
 
175
198
  Backends can be switched by using `store_in`.
176
199
 
177
- # use redis with defaults
178
- Patient::Mock.store_in(:redis)
179
- # use redis with a specific client
180
- Patient::Mock.store_in(:redis, client: Redis::Namespace.new("cistern", redis: Redis.new(host: "10.1.0.1"))
181
- # use a hash
182
- Patient::Mock.store_in(:hash)
200
+ ```ruby
201
+ # use redis with defaults
202
+ Patient::Mock.store_in(:redis)
203
+ # use redis with a specific client
204
+ Patient::Mock.store_in(:redis, client: Redis::Namespace.new("cistern", redis: Redis.new(host: "10.1.0.1"))
205
+ # use a hash
206
+ Patient::Mock.store_in(:hash)
207
+ ```
183
208
 
184
209
  ### Model
185
210
 
@@ -188,108 +213,141 @@ Backends can be switched by using `store_in`.
188
213
 
189
214
  Example
190
215
 
191
- class Foo::Client::Bar < Cistern::Model
192
- identity :id
216
+ ```ruby
217
+ class Foo::Client::Bar < Cistern::Model
218
+ identity :id
219
+
220
+ attribute :flavor
221
+ attribute :keypair_id, aliases: "keypair", squash: "id"
222
+ attribute :private_ips, type: :array
223
+
224
+ def destroy
225
+ params = {
226
+ "id" => self.identity
227
+ }
228
+ self.connection.destroy_bar(params).body["request"]
229
+ end
230
+
231
+ def save
232
+ requires :keypair_id
233
+
234
+ params = {
235
+ "keypair" => self.keypair_id,
236
+ "bar" => {
237
+ "flavor" => self.flavor,
238
+ },
239
+ }
240
+
241
+ if new_record?
242
+ merge_attributes(connection.create_bar(params).body["bar"])
243
+ else
244
+ requires :identity
245
+
246
+ merge_attributes(connection.update_bar(params).body["bar"])
247
+ end
248
+ end
249
+ end
250
+ ```
193
251
 
194
- attribute :flavor
195
- attribute :keypair_id, aliases: "keypair", squash: "id"
196
- attribute :private_ips, type: :array
252
+ #### Dirty
197
253
 
198
- def destroy
199
- params = {
200
- "id" => self.identity
201
- }
202
- self.connection.destroy_bar(params).body["request"]
203
- end
254
+ Dirty attributes are tracked and cleared when `merge_attributes` is called.
204
255
 
205
- def save
206
- requires :keypair_id
256
+ * `changed` returns a Hash of changed attributes mapped to there initial value and current value
257
+ * `dirty_attributes` returns Hash of changed attributes with there current value. This should be used in the model `save` function.
207
258
 
208
- params = {
209
- "keypair" => self.keypair_id,
210
- "bar" => {
211
- "flavor" => self.flavor,
212
- },
213
- }
214
259
 
215
- if new_record?
216
- merge_attributes(connection.create_bar(params).body["bar"])
217
- else
218
- requires :identity
260
+ ```ruby
261
+ bar = Foo::Client::Bar.new(id: 1, flavor: "x") # => <#Foo::Client::Bar>
219
262
 
220
- merge_attributes(connection.update_bar(params).body["bar"])
221
- end
222
- end
223
- end
263
+ bar.dirty? # => false
264
+ bar.changed # => {}
265
+ bar.dirty_attributes # => {}
266
+
267
+ bar.flavor = "y"
268
+
269
+ bar.dirty? # => true
270
+ bar.changed # => {flavor: ["x", "y"]}
271
+ bar.dirty_attributes # => {flavor: "y"}
272
+
273
+ bar.save
274
+ bar.dirty? # => false
275
+ bar.changed # => {}
276
+ bar.dirty_attributes # => {}
277
+ ```
224
278
 
225
279
  ### Collection
226
280
 
227
281
  `model` tells Cistern which class is contained within the collection. `Cistern::Collection` inherits from `Array` and lazy loads where applicable.
228
282
 
229
- class Foo::Client::Bars < Cistern::Collection
283
+ ```ruby
284
+ class Foo::Client::Bars < Cistern::Collection
230
285
 
231
- model Foo::Client::Bar
286
+ model Foo::Client::Bar
232
287
 
233
- def all(params = {})
234
- response = connection.get_bars(params)
288
+ def all(params = {})
289
+ response = connection.get_bars(params)
235
290
 
236
- data = response.body
291
+ data = response.body
237
292
 
238
- self.load(data["bars"]) # store bar records in collection
239
- self.merge_attributes(data) # store any other attributes of the response on the collection
240
- end
293
+ self.load(data["bars"]) # store bar records in collection
294
+ self.merge_attributes(data) # store any other attributes of the response on the collection
295
+ end
241
296
 
242
- def discover(provisioned_id, options={})
243
- params = {
244
- "provisioned_id" => provisioned_id,
245
- }
246
- params.merge!("location" => options[:location]) if options.key?(:location)
297
+ def discover(provisioned_id, options={})
298
+ params = {
299
+ "provisioned_id" => provisioned_id,
300
+ }
301
+ params.merge!("location" => options[:location]) if options.key?(:location)
247
302
 
248
- connection.requests.new(connection.discover_bar(params).body["request"])
249
- end
303
+ connection.requests.new(connection.discover_bar(params).body["request"])
304
+ end
250
305
 
251
- def get(id)
252
- if data = connection.get_bar("id" => id).body["bar"]
253
- new(data)
254
- else
255
- nil
256
- end
257
- end
306
+ def get(id)
307
+ if data = connection.get_bar("id" => id).body["bar"]
308
+ new(data)
309
+ else
310
+ nil
258
311
  end
312
+ end
313
+ end
314
+ ```
259
315
 
260
316
  ### Request
261
317
 
262
- module Foo
263
- class Client
264
- class Real
265
- def create_bar(options={})
266
- request(
267
- :body => {"bar" => options},
268
- :method => :post,
269
- :path => '/bar'
270
- )
271
- end
272
- end # Real
273
-
274
- class Mock
275
- def create_bar(options={})
276
- id = Foo.random_hex(6)
277
-
278
- bar = {
279
- "id" => id
280
- }.merge(options)
281
-
282
- self.data[:bars][id]= bar
283
-
284
- response(
285
- :body => {"bar" => bar},
286
- :status => 201,
287
- :path => '/bar',
288
- )
289
- end
290
- end # Mock
291
- end # Client
292
- end # Foo
318
+ ```ruby
319
+ module Foo
320
+ class Client
321
+ class Real
322
+ def create_bar(options={})
323
+ request(
324
+ :body => {"bar" => options},
325
+ :method => :post,
326
+ :path => '/bar'
327
+ )
328
+ end
329
+ end # Real
330
+
331
+ class Mock
332
+ def create_bar(options={})
333
+ id = Foo.random_hex(6)
334
+
335
+ bar = {
336
+ "id" => id
337
+ }.merge(options)
338
+
339
+ self.data[:bars][id]= bar
340
+
341
+ response(
342
+ :body => {"bar" => bar},
343
+ :status => 201,
344
+ :path => '/bar',
345
+ )
346
+ end
347
+ end # Mock
348
+ end # Client
349
+ end # Foo
350
+ ```
293
351
 
294
352
  ## Examples
295
353
 
@@ -123,13 +123,26 @@ module Cistern::Attributes
123
123
 
124
124
  def write_attribute(name, value)
125
125
  options = self.class.attributes[name] || {}
126
+
126
127
  transform = Cistern::Attributes.transforms[options[:squash] ? :squash : :none] ||
127
128
  Cistern::Attributes.default_transform
129
+
128
130
  parser = Cistern::Attributes.parsers[options[:type]] ||
129
131
  options[:parser] ||
130
132
  Cistern::Attributes.default_parser
133
+
131
134
  transformed = transform.call(name, value, options)
132
- attributes[name.to_s.to_sym]= parser.call(transformed, options)
135
+
136
+ new_value = parser.call(transformed, options)
137
+ attribute = name.to_s.to_sym
138
+
139
+ previous_value = attributes[attribute]
140
+
141
+ attributes[attribute] = new_value
142
+
143
+ changed!(attribute, previous_value, new_value)
144
+
145
+ new_value
133
146
  end
134
147
 
135
148
  def attributes
@@ -174,6 +187,7 @@ module Cistern::Attributes
174
187
  end
175
188
  end
176
189
  end
190
+ changed.clear
177
191
  self
178
192
  end
179
193
 
@@ -198,10 +212,30 @@ module Cistern::Attributes
198
212
  end
199
213
  end
200
214
 
215
+ def dirty?
216
+ changed.any?
217
+ end
218
+
219
+ def dirty_attributes
220
+ changed.inject({}) { |r,(k,(_,v))| r.merge(k => v) }
221
+ end
222
+
223
+ def changed
224
+ @changes ||= {}
225
+ end
226
+
201
227
  protected
202
228
 
203
229
  def missing_attributes(args)
204
230
  ([:connection] | args).select{|arg| send("#{arg}").nil?}
205
231
  end
232
+
233
+ def changed!(attribute, from, to)
234
+ changed[attribute] = if existing = changed[attribute]
235
+ [existing.first, to]
236
+ else
237
+ [from, to]
238
+ end
239
+ end
206
240
  end
207
241
  end
@@ -1,7 +1,6 @@
1
1
  module Cistern::Formatter
2
2
  autoload :AwesomePrint, 'cistern/formatter/awesome_print'
3
- autoload :Default, 'cistern/formatter/default'
4
- autoload :Formatador, 'cistern/formatter/formatador'
3
+ autoload :Formatador, 'cistern/formatter/formatador'
5
4
 
6
5
  def self.default
7
6
  if defined?(::AwesomePrint)
@@ -1,3 +1,3 @@
1
1
  module Cistern
2
- VERSION = "0.10.2"
2
+ VERSION = "0.11.0"
3
3
  end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Cistern::Model#dirty" do
4
+ class DirtySpec < Cistern::Model
5
+ identity :id
6
+
7
+ attribute :name
8
+ attribute :properties, type: :array
9
+
10
+ def save
11
+ merge_attributes(attributes)
12
+ end
13
+ end
14
+
15
+ it "should mark a existing record as dirty" do
16
+ model = DirtySpec.new(id: 1, name: "steve")
17
+ expect(model.changed).to be_empty
18
+
19
+ expect {
20
+ model.properties = [1]
21
+ }.to change { model.dirty? }.to(true)
22
+
23
+ expect(model.changed).to eq(properties: [nil, [1]])
24
+ expect(model.dirty_attributes).to eq(properties: [1])
25
+
26
+ expect {
27
+ model.properties = [2]
28
+ }.to change { model.changed }.to(properties: [nil, [2]])
29
+ expect(model.dirty_attributes).to eq(properties: [2])
30
+
31
+ expect {
32
+ model.save
33
+ }.to change { model.dirty? }.to(false)
34
+
35
+ expect(model.changed).to eq({})
36
+ expect(model.dirty_attributes).to eq({})
37
+ end
38
+ end
data/spec/model_spec.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe "Cistern::Model" do
4
-
5
4
  describe "#update" do
6
5
  class UpdateSpec < Cistern::Model
7
6
  identity :id
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cistern
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.2
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Lane
@@ -45,6 +45,7 @@ files:
45
45
  - lib/cistern/version.rb
46
46
  - lib/cistern/wait_for.rb
47
47
  - spec/collection_spec.rb
48
+ - spec/dirty_spec.rb
48
49
  - spec/formatter_spec.rb
49
50
  - spec/hash_spec.rb
50
51
  - spec/mock_data_spec.rb
@@ -78,6 +79,7 @@ specification_version: 4
78
79
  summary: API client framework
79
80
  test_files:
80
81
  - spec/collection_spec.rb
82
+ - spec/dirty_spec.rb
81
83
  - spec/formatter_spec.rb
82
84
  - spec/hash_spec.rb
83
85
  - spec/mock_data_spec.rb