requisite 0.1.0 → 0.2.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/CHANGES.md +5 -0
- data/lib/requisite/api_model.rb +1 -1
- data/lib/requisite/boundary_object.rb +5 -1
- data/lib/requisite/version.rb +1 -1
- data/test/requisite/api_model_test.rb +89 -85
- data/test/requisite/api_user_test.rb +40 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fdf15247190df240348bbf154bac1485d5e93b6a
|
4
|
+
data.tar.gz: 32d1748aff1b074f8b5f9b9639249697176c7c58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 569dae2fbad28fae97b4c5013f34290f24702494eddbfb560f3279f07387e3df679d74bd1a8ee3c52c5531e193d240008a12454c8f70d20304fe58f064de8a5d
|
7
|
+
data.tar.gz: c66308ee9e74ec995613b5c7d35202c76e645725d77d72bbbc406e0380cc43c78d34b3519adbec9da9c82d45e7662d558671c2676a03058ce2d627a34a1a6786
|
data/CHANGES.md
ADDED
data/lib/requisite/api_model.rb
CHANGED
@@ -31,7 +31,7 @@ module Requisite
|
|
31
31
|
def to_hash
|
32
32
|
preprocess_model
|
33
33
|
{}.tap do |result|
|
34
|
-
self.class.
|
34
|
+
self.class.attribute_keys_with_inheritance.each do |meth|
|
35
35
|
value = self.send(meth)
|
36
36
|
result.merge!({meth => value}) unless value.nil?
|
37
37
|
end
|
@@ -36,7 +36,11 @@ module Requisite
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def attribute_keys
|
39
|
-
@attribute_keys
|
39
|
+
@attribute_keys || []
|
40
|
+
end
|
41
|
+
|
42
|
+
def attribute_keys_with_inheritance
|
43
|
+
superclass.respond_to?(:attribute_keys_with_inheritance) ? superclass.attribute_keys_with_inheritance.concat(attribute_keys) : attribute_keys || []
|
40
44
|
end
|
41
45
|
end
|
42
46
|
|
data/lib/requisite/version.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
require 'test_helper'
|
2
|
+
# require_relative '../dummy_api_model'
|
2
3
|
|
3
4
|
module Requisite
|
5
|
+
class DummyApiModel < Requisite::ApiModel
|
6
|
+
end
|
7
|
+
|
4
8
|
describe ApiModel do
|
5
9
|
it 'creates methods from serialized_attributes block' do
|
6
|
-
|
7
|
-
response =
|
10
|
+
DummyApiModel.serialized_attributes { attribute :a; attribute :b }
|
11
|
+
response = DummyApiModel.new
|
8
12
|
def response.a; 'A'; end
|
9
13
|
def response.b; 2; end
|
10
14
|
response.to_hash.must_equal( :a => 'A', :b => 2 )
|
@@ -13,119 +17,119 @@ module Requisite
|
|
13
17
|
end
|
14
18
|
|
15
19
|
it 'attribute provides a default implementation of calling a hash model' do
|
16
|
-
|
20
|
+
DummyApiModel.serialized_attributes { attribute :c }
|
17
21
|
mock = {:c => 'C'}
|
18
|
-
response =
|
22
|
+
response = DummyApiModel.new(mock)
|
19
23
|
response.to_hash.must_equal( :c => 'C' )
|
20
24
|
end
|
21
25
|
|
22
26
|
let(:params_hash) { {:c => 'C', :num => 12} }
|
23
27
|
|
24
28
|
it 'attribute provides a default implementation of calling a model' do
|
25
|
-
|
26
|
-
response =
|
29
|
+
DummyApiModel.serialized_attributes { attribute :c }
|
30
|
+
response = DummyApiModel.new(params_hash)
|
27
31
|
response.to_hash.must_equal(:c => 'C')
|
28
32
|
end
|
29
33
|
|
30
34
|
it 'attribute can work with a default' do
|
31
|
-
|
32
|
-
response =
|
35
|
+
DummyApiModel.serialized_attributes { attribute :c, default: 'see' }
|
36
|
+
response = DummyApiModel.new
|
33
37
|
response.to_hash.must_equal(:c => 'see')
|
34
38
|
end
|
35
39
|
|
36
40
|
it 'ignores default if value given' do
|
37
|
-
|
38
|
-
response =
|
41
|
+
DummyApiModel.serialized_attributes { attribute :num, default: 0 }
|
42
|
+
response = DummyApiModel.new(params_hash)
|
39
43
|
response.to_hash.must_equal(:num => 12)
|
40
44
|
end
|
41
45
|
|
42
46
|
it 'attribute can be set to stringify fields' do
|
43
|
-
|
44
|
-
response =
|
47
|
+
DummyApiModel.serialized_attributes { attribute :num, stringify: true }
|
48
|
+
response = DummyApiModel.new(params_hash)
|
45
49
|
response.to_hash.must_equal(:num => '12')
|
46
50
|
end
|
47
51
|
|
48
52
|
it 'attribute can be set to rename fields' do
|
49
|
-
|
50
|
-
response =
|
53
|
+
DummyApiModel.serialized_attributes { attribute :my_num, rename: :num }
|
54
|
+
response = DummyApiModel.new(params_hash)
|
51
55
|
response.to_hash.must_equal(:my_num => 12)
|
52
56
|
end
|
53
57
|
|
54
58
|
it 'attribute can assert type of a field' do
|
55
|
-
|
56
|
-
response =
|
59
|
+
DummyApiModel.serialized_attributes { attribute :num, type: String }
|
60
|
+
response = DummyApiModel.new(params_hash)
|
57
61
|
proc { response.to_hash }.must_raise(BadTypeError)
|
58
62
|
end
|
59
63
|
|
60
64
|
it 'with_type! helper raises on mismatched type' do
|
61
|
-
model =
|
65
|
+
model = DummyApiModel.new()
|
62
66
|
proc { model.with_type!(String) { 1 + 2 }}.must_raise(Requisite::BadTypeError)
|
63
67
|
end
|
64
68
|
|
65
69
|
it 'first_attribute_from_model helper finds first matching attriubute' do
|
66
|
-
model =
|
70
|
+
model = DummyApiModel.new(:oh => 12, :a => nil, :b => 'B', :c => 'C')
|
67
71
|
model.first_attribute_from_model(:a, :b, :c).must_equal('B')
|
68
72
|
end
|
69
73
|
|
70
74
|
it 'attribute can assert type of a boolean field' do
|
71
|
-
|
72
|
-
response =
|
75
|
+
DummyApiModel.serialized_attributes { attribute :truthy_val, type: Requisite::Boolean }
|
76
|
+
response = DummyApiModel.new(:truthy_val => false)
|
73
77
|
response.to_hash.must_equal(:truthy_val => false)
|
74
78
|
end
|
75
79
|
|
76
80
|
it 'attribute does not include values of nil' do
|
77
|
-
|
78
|
-
response =
|
81
|
+
DummyApiModel.serialized_attributes { attribute :num, type: String }
|
82
|
+
response = DummyApiModel.new({:num => nil})
|
79
83
|
response.to_hash.must_equal({})
|
80
84
|
end
|
81
85
|
|
82
86
|
it 'attribute can be stringified and renamed with default fields' do
|
83
|
-
|
84
|
-
response =
|
87
|
+
DummyApiModel.serialized_attributes { attribute :my_num, rename: :num, stringify: true, default: 22 }
|
88
|
+
response = DummyApiModel.new
|
85
89
|
response.to_hash.must_equal(:my_num => '22')
|
86
90
|
end
|
87
91
|
|
88
92
|
it 'attribute can be stringified after type check' do
|
89
|
-
|
90
|
-
response =
|
93
|
+
DummyApiModel.serialized_attributes { attribute :num, stringify: true, type: Fixnum }
|
94
|
+
response = DummyApiModel.new(params_hash)
|
91
95
|
response.to_hash.must_equal(:num => '12')
|
92
96
|
end
|
93
97
|
|
94
98
|
it 'attribute type checks after rename' do
|
95
|
-
|
96
|
-
response =
|
99
|
+
DummyApiModel.serialized_attributes { attribute :my_num, rename: :num, type: String }
|
100
|
+
response = DummyApiModel.new(params_hash)
|
97
101
|
proc { response.to_hash }.must_raise(BadTypeError)
|
98
102
|
end
|
99
103
|
|
100
104
|
it 'attribute can be stringified, renamed, defaulted and have type checking on a field' do
|
101
|
-
|
102
|
-
response =
|
105
|
+
DummyApiModel.serialized_attributes { attribute :my_num, rename: :num, stringify: true, default: 22, type: String }
|
106
|
+
response = DummyApiModel.new
|
103
107
|
proc { response.to_hash }.must_raise(BadTypeError)
|
104
108
|
end
|
105
109
|
|
106
110
|
let(:invalid_params_hash) { {:d => nil} }
|
107
111
|
|
108
112
|
it "attribute! raises an error if not found on model" do
|
109
|
-
|
110
|
-
response =
|
113
|
+
DummyApiModel.serialized_attributes { attribute! :d }
|
114
|
+
response = DummyApiModel.new(invalid_params_hash)
|
111
115
|
proc { response.to_hash }.must_raise(NotImplementedError, "'d' not found on model")
|
112
116
|
end
|
113
117
|
|
114
118
|
it 'attribute! can be set to stringify fields' do
|
115
|
-
|
116
|
-
response =
|
119
|
+
DummyApiModel.serialized_attributes { attribute! :num, stringify: true }
|
120
|
+
response = DummyApiModel.new(params_hash)
|
117
121
|
response.to_hash.must_equal(:num => '12')
|
118
122
|
end
|
119
123
|
|
120
124
|
it 'attribute! can be set to rename fields' do
|
121
|
-
|
122
|
-
response =
|
125
|
+
DummyApiModel.serialized_attributes { attribute! :my_num, rename: :num }
|
126
|
+
response = DummyApiModel.new(params_hash)
|
123
127
|
response.to_hash.must_equal(:my_num => 12)
|
124
128
|
end
|
125
129
|
|
126
130
|
it 'sets the model from a hash' do
|
127
|
-
|
128
|
-
response =
|
131
|
+
DummyApiModel.serialized_attributes { }
|
132
|
+
response = DummyApiModel.new(params_hash)
|
129
133
|
response.model.must_equal(params_hash)
|
130
134
|
end
|
131
135
|
|
@@ -133,33 +137,33 @@ module Requisite
|
|
133
137
|
mc = MockClass.new
|
134
138
|
mc.a = 'a'
|
135
139
|
mc.b = 2
|
136
|
-
|
137
|
-
response =
|
140
|
+
DummyApiModel.serialized_attributes { attribute :a }
|
141
|
+
response = DummyApiModel.new(mc)
|
138
142
|
response.model.must_equal(mc)
|
139
143
|
response.to_hash.must_equal(:a => 'a')
|
140
144
|
end
|
141
145
|
|
142
146
|
it 'has alias a for attribute' do
|
143
|
-
|
144
|
-
response =
|
147
|
+
DummyApiModel.serialized_attributes { a :num }
|
148
|
+
response = DummyApiModel.new(params_hash)
|
145
149
|
response.to_hash.must_equal(:num => 12)
|
146
150
|
end
|
147
151
|
|
148
152
|
it 'has alias a! for attribute!' do
|
149
|
-
|
150
|
-
response =
|
153
|
+
DummyApiModel.serialized_attributes { a! :num }
|
154
|
+
response = DummyApiModel.new(params_hash)
|
151
155
|
response.to_hash.must_equal(:num => 12)
|
152
156
|
end
|
153
157
|
|
154
158
|
it 'can convert to json' do
|
155
|
-
|
156
|
-
response =
|
159
|
+
DummyApiModel.serialized_attributes { a! :num }
|
160
|
+
response = DummyApiModel.new(params_hash)
|
157
161
|
response.to_json.must_equal("{\"num\":12}")
|
158
162
|
end
|
159
163
|
|
160
164
|
it 'drops non-listed parameters' do
|
161
|
-
|
162
|
-
response =
|
165
|
+
DummyApiModel.serialized_attributes { attribute :num }
|
166
|
+
response = DummyApiModel.new({num: 12, other: 'value'})
|
163
167
|
response.to_hash.must_equal(:num => 12)
|
164
168
|
end
|
165
169
|
|
@@ -167,77 +171,77 @@ module Requisite
|
|
167
171
|
|
168
172
|
describe 'with typed arrays' do
|
169
173
|
it 'allows arrays of one type' do
|
170
|
-
|
171
|
-
response =
|
174
|
+
DummyApiModel.serialized_attributes { attribute :ids, typed_array: Fixnum }
|
175
|
+
response = DummyApiModel.new({ids: [1, 2, 3]})
|
172
176
|
response.to_hash.must_equal(:ids => [1, 2, 3])
|
173
177
|
end
|
174
178
|
|
175
179
|
it 'raises errors when array has a wrongly typed value' do
|
176
|
-
|
177
|
-
response =
|
180
|
+
DummyApiModel.serialized_attributes { attribute :ids, typed_array: Requisite::Boolean }
|
181
|
+
response = DummyApiModel.new({ids: [true, 'value', false]})
|
178
182
|
Proc.new {response.to_hash}.must_raise(BadTypeError)
|
179
183
|
end
|
180
184
|
end
|
181
185
|
|
182
186
|
describe 'with typed nested hashes' do
|
183
187
|
it 'drops non listed parameters in nested hashes' do
|
184
|
-
|
185
|
-
response =
|
188
|
+
DummyApiModel.serialized_attributes { attribute :data, typed_hash: { num: Numeric, bool: Requisite::Boolean } }
|
189
|
+
response = DummyApiModel.new({data: { num: 12, value: 'x', bool: true }})
|
186
190
|
response.to_hash.must_equal(:data => { :num => 12, :bool => true })
|
187
191
|
end
|
188
192
|
|
189
193
|
it 'can stringify nested hashes' do
|
190
|
-
|
191
|
-
response =
|
194
|
+
DummyApiModel.serialized_attributes { attribute :data, typed_hash: { num: Numeric }, stringify: true }
|
195
|
+
response = DummyApiModel.new({data: { num: 12, value: 'x' }})
|
192
196
|
response.to_hash.must_equal(:data => "{:num=>12}")
|
193
197
|
end
|
194
198
|
|
195
199
|
it 'raises an error when nested hash values of the wrong type' do
|
196
|
-
|
197
|
-
Proc.new {
|
200
|
+
DummyApiModel.serialized_attributes { attribute :data, typed_hash: { num: Numeric } }
|
201
|
+
Proc.new {DummyApiModel.new({data: { num: '12'}}).to_hash}.must_raise(BadTypeError)
|
198
202
|
end
|
199
203
|
|
200
204
|
it 'can rename param and work with nested hashes' do
|
201
|
-
|
202
|
-
response =
|
205
|
+
DummyApiModel.serialized_attributes { attribute :my_data, typed_hash: { num: Numeric }, rename: :data }
|
206
|
+
response = DummyApiModel.new({data: { num: 12, value: 'x' }})
|
203
207
|
response.to_hash.must_equal(:my_data => { :num => 12 })
|
204
208
|
end
|
205
209
|
|
206
210
|
it 'can set a default value for a nested hash' do
|
207
|
-
|
208
|
-
response =
|
211
|
+
DummyApiModel.serialized_attributes { attribute :data, typed_hash: { num: Numeric }, default: { num: 4 } }
|
212
|
+
response = DummyApiModel.new({data: { value: 'x' }})
|
209
213
|
response.to_hash.must_equal(:data => { :num => 4 })
|
210
214
|
end
|
211
215
|
|
212
216
|
it 'drops non listed fields with attribute!' do
|
213
|
-
|
214
|
-
response =
|
217
|
+
DummyApiModel.serialized_attributes { attribute! :data, typed_hash: { num: Numeric } }
|
218
|
+
response = DummyApiModel.new({data: { num: 12, value: 'x' }})
|
215
219
|
response.to_hash.must_equal(:data => { :num => 12 })
|
216
220
|
end
|
217
221
|
|
218
222
|
it 'attribute! does not raise an error with missing values in hash' do
|
219
|
-
|
220
|
-
response =
|
223
|
+
DummyApiModel.serialized_attributes { attribute! :data, typed_hash: { num: Numeric } }
|
224
|
+
response = DummyApiModel.new({data: { value: 'x' }})
|
221
225
|
response.to_hash.must_equal(:data => { })
|
222
226
|
end
|
223
227
|
end
|
224
228
|
|
225
229
|
describe 'with scalar only nested hashes' do
|
226
230
|
it 'should parse scalar hashes permitting anything scalar' do
|
227
|
-
|
228
|
-
response =
|
231
|
+
DummyApiModel.serialized_attributes { attribute :data, scalar_hash: true }
|
232
|
+
response = DummyApiModel.new({data: { num: 12, value: 'x', :truthy => false }})
|
229
233
|
response.to_hash.must_equal(:data => { :num => 12, :value => 'x', :truthy => false })
|
230
234
|
end
|
231
235
|
|
232
236
|
it 'should parse a renamed scalar hash' do
|
233
|
-
|
234
|
-
response =
|
237
|
+
DummyApiModel.serialized_attributes { attribute :my_data, scalar_hash: true, rename: :data }
|
238
|
+
response = DummyApiModel.new({data: { num: 12, value: 'x' }})
|
235
239
|
response.to_hash.must_equal(:my_data => { :num => 12, :value => 'x' })
|
236
240
|
end
|
237
241
|
|
238
242
|
it 'should stringify a scalar hash' do
|
239
|
-
|
240
|
-
response =
|
243
|
+
DummyApiModel.serialized_attributes { attribute :data, scalar_hash: true, stringify: true }
|
244
|
+
response = DummyApiModel.new({data: { num: 12, value: 'x' }})
|
241
245
|
response.to_hash.must_equal(:data => "{:num=>12, :value=>\"x\"}")
|
242
246
|
end
|
243
247
|
|
@@ -245,41 +249,41 @@ module Requisite
|
|
245
249
|
mc = MockClass.new
|
246
250
|
mc.a = 'a'
|
247
251
|
mc.b = { num: 12, value: 'x' }
|
248
|
-
|
249
|
-
response =
|
252
|
+
DummyApiModel.serialized_attributes { attribute :b, scalar_hash: true }
|
253
|
+
response = DummyApiModel.new(mc)
|
250
254
|
response.to_hash.must_equal(:b => { :num => 12, :value => 'x' })
|
251
255
|
end
|
252
256
|
|
253
257
|
it 'should fail to parse scalar hashes when non scalar values present' do
|
254
|
-
|
255
|
-
Proc.new {
|
256
|
-
Proc.new {
|
258
|
+
DummyApiModel.serialized_attributes { attribute :data, scalar_hash: true }
|
259
|
+
Proc.new { DummyApiModel.new({data: { num: 12, value: { nested: 'value' } }}).to_hash}.must_raise(BadTypeError)
|
260
|
+
Proc.new { DummyApiModel.new({data: { num: 12, value: ['array value'] }}).to_hash}.must_raise(BadTypeError)
|
257
261
|
end
|
258
262
|
|
259
263
|
it 'should fail to parse scalar hashes permitting anything scalar with object' do
|
260
264
|
mc = MockClass.new
|
261
265
|
mc.a = 'a'
|
262
266
|
mc.b = { value: { nested: 'value' } }
|
263
|
-
|
264
|
-
response =
|
267
|
+
DummyApiModel.serialized_attributes { attribute :b, scalar_hash: true }
|
268
|
+
response = DummyApiModel.new(mc)
|
265
269
|
Proc.new { response.to_hash }.must_raise(BadTypeError)
|
266
270
|
end
|
267
271
|
|
268
272
|
it 'can set a default value for a scalar hash' do
|
269
|
-
|
270
|
-
response =
|
273
|
+
DummyApiModel.serialized_attributes { attribute :data, scalar_hash: true, default: { num: 9, value: 'y' } }
|
274
|
+
response = DummyApiModel.new({data: { }})
|
271
275
|
response.to_hash.must_equal(:data => { :num => 9, :value => 'y' })
|
272
276
|
end
|
273
277
|
|
274
278
|
it 'doesnt raise with attribute! when an empty hash passed' do
|
275
|
-
|
276
|
-
response =
|
279
|
+
DummyApiModel.serialized_attributes { attribute! :data, scalar_hash: true }
|
280
|
+
response = DummyApiModel.new({data: {}})
|
277
281
|
response.to_hash.must_equal(:data => {})
|
278
282
|
end
|
279
283
|
|
280
284
|
it 'raises with attribute! when nil is passed' do
|
281
|
-
|
282
|
-
response =
|
285
|
+
DummyApiModel.serialized_attributes { attribute! :data, scalar_hash: true }
|
286
|
+
response = DummyApiModel.new({data: nil})
|
283
287
|
Proc.new {response.to_hash}.must_raise(NotImplementedError)
|
284
288
|
end
|
285
289
|
end
|
@@ -32,6 +32,13 @@ class ApiUser < Requisite::ApiModel
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
+
class InheritedApiUser < ApiUser
|
36
|
+
serialized_attributes do
|
37
|
+
attribute :new_attribute, type: String
|
38
|
+
attribute :custom_data
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
35
42
|
module Requisite
|
36
43
|
describe ApiUser do
|
37
44
|
it 'accepts a user' do
|
@@ -71,5 +78,38 @@ module Requisite
|
|
71
78
|
user = ApiUser.new(user_request_params)
|
72
79
|
proc { user.to_hash }.must_raise(Requisite::BadTypeError)
|
73
80
|
end
|
81
|
+
|
82
|
+
it "can resolve an inherited set of attributes" do
|
83
|
+
user_request_params = {
|
84
|
+
:user_id => 'abcdef',
|
85
|
+
:name => 'Bob',
|
86
|
+
:created => 1414173164,
|
87
|
+
:new_session => true,
|
88
|
+
:custom_attributes => {
|
89
|
+
:is_cool => true,
|
90
|
+
:logins => 77
|
91
|
+
},
|
92
|
+
:custom_data => {
|
93
|
+
:different => true
|
94
|
+
},
|
95
|
+
:new_attribute => 'hi',
|
96
|
+
:junk => 'data'
|
97
|
+
}
|
98
|
+
user = InheritedApiUser.new(user_request_params)
|
99
|
+
user.new_attribute.must_equal 'hi'
|
100
|
+
user.name.must_equal 'Bob'
|
101
|
+
user.custom_data.must_equal({ :different => true })
|
102
|
+
InheritedApiUser.attribute_keys.must_equal([:new_attribute, :custom_data])
|
103
|
+
user.to_hash.must_equal({
|
104
|
+
:user_id => 'abcdef',
|
105
|
+
:name => 'Bob',
|
106
|
+
:created_at => 1414173164,
|
107
|
+
:new_session => true,
|
108
|
+
:custom_data => {
|
109
|
+
:different => true
|
110
|
+
},
|
111
|
+
:new_attribute => 'hi'
|
112
|
+
})
|
113
|
+
end
|
74
114
|
end
|
75
115
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: requisite
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James Osler
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-11-
|
11
|
+
date: 2014-11-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -33,6 +33,7 @@ extensions: []
|
|
33
33
|
extra_rdoc_files: []
|
34
34
|
files:
|
35
35
|
- .gitignore
|
36
|
+
- CHANGES.md
|
36
37
|
- Gemfile
|
37
38
|
- LICENCE.txt
|
38
39
|
- README.md
|