cistern 0.10.2 → 0.11.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: 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