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 +4 -4
- data/README.md +231 -173
- data/lib/cistern/attributes.rb +35 -1
- data/lib/cistern/formatter.rb +1 -2
- data/lib/cistern/version.rb +1 -1
- data/spec/dirty_spec.rb +38 -0
- data/spec/model_spec.rb +0 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e00bac7d299cc0636b1ac4b5ca42958de6f96f41
|
4
|
+
data.tar.gz: 0f83891e9e0bc7f9cdcbdb8ad9c0280dfb969ae3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
19
|
-
|
18
|
+
```ruby
|
19
|
+
class Foo::Client < Cistern::Service
|
20
|
+
request_path "my-foo/requests"
|
20
21
|
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
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
|
-
|
60
|
-
|
65
|
+
```ruby
|
66
|
+
class Foo::Client < Cistern::Service
|
67
|
+
model_path "my-foo/models"
|
61
68
|
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
78
|
+
```ruby
|
79
|
+
class Foo::Client < Cistern::Service
|
80
|
+
requires :hmac_id, :hmac_secret
|
81
|
+
recognizes :url
|
82
|
+
end
|
74
83
|
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
123
|
+
```ruby
|
124
|
+
Foo::Client::Mock.data["bars"] # ["x"]
|
125
|
+
```
|
110
126
|
|
111
127
|
`reset!` dimisses the `data` object.
|
112
128
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
138
|
+
```ruby
|
139
|
+
client.data["bars"] += ["y"] # ["y"]
|
140
|
+
client.data.object_id # 70199868378300
|
141
|
+
client.clear
|
142
|
+
client.data["bars"] # []
|
124
143
|
|
125
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
174
|
+
bar = {
|
175
|
+
"id" => id
|
176
|
+
}.merge(options)
|
154
177
|
|
155
|
-
|
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
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
192
|
-
|
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
|
-
|
195
|
-
attribute :keypair_id, aliases: "keypair", squash: "id"
|
196
|
-
attribute :private_ips, type: :array
|
252
|
+
#### Dirty
|
197
253
|
|
198
|
-
|
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
|
-
|
206
|
-
|
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
|
-
|
216
|
-
|
217
|
-
else
|
218
|
-
requires :identity
|
260
|
+
```ruby
|
261
|
+
bar = Foo::Client::Bar.new(id: 1, flavor: "x") # => <#Foo::Client::Bar>
|
219
262
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
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
|
-
|
283
|
+
```ruby
|
284
|
+
class Foo::Client::Bars < Cistern::Collection
|
230
285
|
|
231
|
-
|
286
|
+
model Foo::Client::Bar
|
232
287
|
|
233
|
-
|
234
|
-
|
288
|
+
def all(params = {})
|
289
|
+
response = connection.get_bars(params)
|
235
290
|
|
236
|
-
|
291
|
+
data = response.body
|
237
292
|
|
238
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
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
|
-
|
249
|
-
|
303
|
+
connection.requests.new(connection.discover_bar(params).body["request"])
|
304
|
+
end
|
250
305
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
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
|
|
data/lib/cistern/attributes.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/cistern/formatter.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
module Cistern::Formatter
|
2
2
|
autoload :AwesomePrint, 'cistern/formatter/awesome_print'
|
3
|
-
autoload :
|
4
|
-
autoload :Formatador, 'cistern/formatter/formatador'
|
3
|
+
autoload :Formatador, 'cistern/formatter/formatador'
|
5
4
|
|
6
5
|
def self.default
|
7
6
|
if defined?(::AwesomePrint)
|
data/lib/cistern/version.rb
CHANGED
data/spec/dirty_spec.rb
ADDED
@@ -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
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.
|
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
|