goodread 0.1.3 → 0.2.1

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
- SHA1:
3
- metadata.gz: 6088e5eebb70dc235fab80821df7e26454bc1058
4
- data.tar.gz: e9a7db495590ffeccd4eff3e2800d835b42df7f1
2
+ SHA256:
3
+ metadata.gz: 2dd0cac02f1796fe0b626e32cc6cc6f4c85424f59457fde0db95a70e8cb8b26a
4
+ data.tar.gz: 55e79251fd7b7dc19aa6f7534b21778697ee53036f0d66283d641b5db894352b
5
5
  SHA512:
6
- metadata.gz: c481de27e24bb80d857aa1e6c6e4c1c251b734bdf5b563aaf0b262e769779318ff507da10255ccdd9a8e03112255671a5155df15c0898db29b75d64b161c81c9
7
- data.tar.gz: 6ef8ff9e48104e65c299fe9656de15b3cb152ee38a57ba985a2cc0667bb32c1fa047196bc9533aabb7e097ada3ef5fccbd5c161433f14e934beed6d5fe880f2a
6
+ metadata.gz: 30d152a2f6868a1a50a2c9620680e0c3bba6b0294d0783b4e1e5a30d29a996693bc2c91c849ef3f720a56f76289b0adf687c8f9fc9b6aba64e17be76cf119e23
7
+ data.tar.gz: 73c5aa278a4480f2599ad6038b46689d5589286b5d8c09343c3dfaf26b56f888d8d27f3e0edd4f34920adb5239ae489ffb4aaa661f8631c9bf7028806130c871
data/.gitignore CHANGED
@@ -52,3 +52,4 @@ build-iPhoneSimulator/
52
52
  # Extra
53
53
  Gemfile.lock
54
54
  .rspec_status
55
+ goodread.yml
data/goodread.gemspec CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "goodread"
7
- spec.version = "0.1.3"
7
+ spec.version = "0.2.1"
8
8
  spec.authors = ["Evgeny Karev\n"]
9
9
  spec.email = ["eskarev@gmail.com"]
10
10
 
data/lib/cli.rb CHANGED
@@ -2,415 +2,45 @@ require 'json'
2
2
  require 'yaml'
3
3
  require 'emoji'
4
4
  require 'colorize'
5
+ require_relative 'document'
6
+ require_relative 'helpers'
5
7
 
6
8
 
7
- # Helpers
8
-
9
- def parse_specs(path)
10
-
11
- # Paths
12
- paths = []
13
- if !path
14
- paths = Dir.glob('package.*')
15
- if paths.empty?
16
- path = 'packspec'
17
- end
18
- end
19
- if File.file?(path)
20
- paths = [path]
21
- elsif File.directory?(path)
22
- for path in Dir.glob("#{path}/*.*")
23
- paths.push(path)
24
- end
25
- end
26
-
27
- # Specs
28
- specs = []
29
- for path in paths
30
- spec = parse_spec(path)
31
- if spec
32
- specs.push(spec)
33
- end
34
- end
35
-
36
- return specs
37
-
38
- end
39
-
40
-
41
- def parse_spec(path)
42
-
43
- # Documents
44
- documents = []
45
- if !path.end_with?('.yml')
46
- return nil
47
- end
48
- contents = File.read(path)
49
- YAML.load_stream(contents) do |document|
50
- documents.push(document)
51
- end
52
-
53
- # Package
54
- feature = parse_feature(documents[0][0])
55
- if feature['skip']
56
- return nil
57
- end
58
- package = feature['comment']
59
-
60
- # Features
61
- skip = false
62
- features = []
63
- for feature in documents[0]
64
- feature = parse_feature(feature)
65
- features.push(feature)
66
- if feature['comment']
67
- skip = feature['skip']
68
- end
69
- feature['skip'] = skip || feature['skip']
70
- end
71
-
72
- # Scope
73
- scope = {}
74
- scope['$import'] = BuiltinFunctions.new().public_method(:builtin_import)
75
- if documents.length > 1 && documents[1]['rb']
76
- eval(documents[1]['rb'])
77
- hook_scope = Functions.new()
78
- for name in hook_scope.public_methods
79
- # TODO: filter ruby builtin methods
80
- scope["$#{name}"] = hook_scope.public_method(name)
81
- end
82
- end
83
-
84
- # Stats
85
- stats = {'features' => 0, 'comments' => 0, 'skipped' => 0, 'tests' => 0}
86
- for feature in features
87
- stats['features'] += 1
88
- if feature['comment']
89
- stats['comments'] += 1
90
- else
91
- stats['tests'] += 1
92
- if feature['skip']
93
- stats['skipped'] += 1
94
- end
95
- end
96
- end
97
-
98
- return {
99
- 'package' => package,
100
- 'features' => features,
101
- 'scope' => scope,
102
- 'stats' => stats,
103
- }
104
-
105
- end
106
-
107
-
108
- def parse_feature(feature)
109
-
110
- # General
111
- if feature.is_a?(String)
112
- match = /^(?:\((.*)\))?(\w.*)$/.match(feature)
113
- skip, comment = match[1], match[2]
114
- if !!skip
115
- skip = !skip.split(':').include?('rb')
116
- end
117
- return {'assign' => nil, 'comment' => comment, 'skip' => skip}
118
- end
119
- left, right = Array(feature.each_pair)[0]
120
-
121
- # Left side
122
- call = false
123
- match = /^(?:\((.*)\))?(?:([^=]*)=)?([^=].*)?$/.match(left)
124
- skip, assign, property = match[1], match[2], match[3]
125
- if !!skip
126
- skip = !skip.split(':').include?('rb')
127
- end
128
- if !assign && !property
129
- raise Exception.new('Non-valid feature')
130
- end
131
- if !!property
132
- call = true
133
- if property.end_with?('==')
134
- property = property[0..-3]
135
- call = false
136
- end
137
- end
138
-
139
- # Right side
140
- args = []
141
- kwargs = {}
142
- result = right
143
- if !!call
144
- result = nil
145
- for item in right
146
- if item.is_a?(Hash) && item.length == 1
147
- item_left, item_right = Array(item.each_pair)[0]
148
- if item_left == '=='
149
- result = item_right
150
- next
151
- end
152
- if item_left.end_with?('=')
153
- kwargs[item_left[0..-2]] = item_right
154
- next
155
- end
156
- end
157
- args.push(item)
158
- end
159
- end
160
-
161
- # Text repr
162
- text = property
163
- if !!assign
164
- text = "#{assign} = #{property || JSON.generate(result)}"
165
- end
166
- if !!call
167
- items = []
168
- for item in args
169
- items.push(JSON.generate(item))
170
- end
171
- for name, item in kwargs.each_pair
172
- items.push("#{name}=#{JSON.generate(item)}")
173
- end
174
- text = "#{text}(#{items.join(', ')})"
175
- end
176
- if !!result && !assign
177
- text = "#{text} == #{result == 'ERROR' ? result : JSON.generate(result)}"
178
- end
179
- text = text.gsub(/{"([^{}]*?)": null}/, '\1')
180
-
181
- return {
182
- 'comment' => nil,
183
- 'skip' => skip,
184
- 'call' => call,
185
- 'assign' => assign,
186
- 'property' => property,
187
- 'args' => args,
188
- 'kwargs' => kwargs,
189
- 'result' => result,
190
- 'text' => text,
191
- }
192
-
193
- end
194
-
195
-
196
- def test_specs(specs)
197
-
198
- # Message
199
- message = "\n # Ruby\n".bold
200
- puts(message)
201
-
202
- # Test specs
203
- success = true
204
- for spec in specs
205
- spec_success = test_spec(spec)
206
- success = success && spec_success
207
- end
208
-
209
- return success
210
-
211
- end
212
-
213
-
214
- def test_spec(spec)
215
-
216
- # Message
217
- message = Emoji.find_by_alias('heavy_minus_sign').raw * 3 + "\n\n"
218
- puts(message)
219
-
220
- # Test spec
221
- passed = 0
222
- for feature in spec['features']
223
- result = test_feature(feature, spec['scope'])
224
- if result
225
- passed += 1
226
- end
227
- end
228
- success = (passed == spec['stats']['features'])
229
-
230
- # Message
231
- color = 'green'
232
- message = ("\n " + Emoji.find_by_alias('heavy_check_mark').raw + ' ').green.bold
233
- if !success
234
- color = 'red'
235
- message = ("\n " + Emoji.find_by_alias('x').raw + ' ').red.bold
236
- end
237
- message += "#{spec['package']}: #{passed - spec['stats']['comments'] - spec['stats']['skipped']}/#{spec['stats']['tests'] - spec['stats']['skipped']}\n".colorize(color).bold
238
- puts(message)
239
-
240
- return success
241
-
242
- end
243
-
244
-
245
- def test_feature(feature, scope)
246
-
247
- # Comment
248
- if !!feature['comment']
249
- message = "\n # #{feature['comment']}\n".bold
250
- puts(message)
251
- return true
252
- end
253
-
254
- # Skip
255
- if !!feature['skip']
256
- message = " #{Emoji.find_by_alias('heavy_minus_sign').raw} ".yellow
257
- message += feature['text']
258
- puts(message)
259
- return true
260
- end
261
-
262
- # Dereference
263
- # TODO: deepcopy feature
264
- if !!feature['call']
265
- feature['args'] = dereference_value(feature['args'], scope)
266
- feature['kwargs'] = dereference_value(feature['kwargs'], scope)
267
- end
268
- feature['result'] = dereference_value(feature['result'], scope)
269
-
270
- # Execute
271
- exception = nil
272
- result = feature['result']
273
- if !!feature['property']
274
- begin
275
- property = scope
276
- for name in feature['property'].split('.')
277
- property = get_property(property, name)
278
- end
279
- if !!feature['call']
280
- args = feature['args'].dup
281
- if !feature['kwargs'].empty?
282
- args.push(Hash[feature['kwargs'].map{|k, v| [k.to_sym, v]}])
283
- end
284
- if property.respond_to?('new')
285
- result = property.new(*args)
286
- else
287
- result = property.call(*args)
288
- end
289
- else
290
- result = property
291
- if result.is_a?(Method)
292
- result = result.call()
293
- end
294
- end
295
- rescue Exception => exc
296
- exception = exc
297
- result = 'ERROR'
298
- end
299
- end
300
-
301
- # Assign
302
- if !!feature['assign']
303
- owner = scope
304
- names = feature['assign'].split('.')
305
- for name in names[0..-2]
306
- owner = get_property(owner, name)
307
- end
308
- # TODO: ensure constants are immutable
309
- set_property(owner, names[-1], result)
310
- end
9
+ # Main program
311
10
 
312
- # Compare
313
- if feature['result'] != nil
314
- success = result == feature['result']
11
+ # Parse
12
+ paths = []
13
+ edit = false
14
+ sync = false
15
+ exit_first = false
16
+ for arg in ARGV
17
+ if ['-e', '--edit'].include?(arg)
18
+ edit = true
19
+ elsif ['-s', '--sync'].include?(arg)
20
+ sync = true
21
+ elsif ['-x', '--exit-first'].include?(arg)
22
+ exit_first = true
315
23
  else
316
- success = result != 'ERROR'
24
+ paths.push(arg)
317
25
  end
318
- if success
319
- message = " #{Emoji.find_by_alias('heavy_check_mark').raw} ".green
320
- message += feature['text']
321
- puts(message)
322
- else
323
- begin
324
- result_text = JSON.generate(result)
325
- rescue Exception
326
- result_text = result.to_s
327
- end
328
- message = " #{Emoji.find_by_alias('x').raw} ".red
329
- message += "#{feature['text']}\n"
330
- if exception
331
- message += "Exception: #{exception}".red.bold
332
- else
333
- message += "Assertion: #{result_text} != #{JSON.generate(feature['result'])}".red.bold
334
- end
335
- puts(message)
336
- end
337
-
338
- return success
339
-
340
26
  end
341
27
 
28
+ # Prepare
29
+ config = read_config()
30
+ documents = DocumentList.new(paths, config)
342
31
 
343
- class BuiltinFunctions
344
- def builtin_import(package)
345
- attributes = {}
346
- require(package)
347
- for item in ObjectSpace.each_object
348
- if package == String(item).downcase
349
- begin
350
- scope = Kernel.const_get(item)
351
- rescue Exception
352
- next
353
- end
354
- for name in scope.constants
355
- attributes[String(name)] = scope.const_get(name)
356
- end
357
- end
358
- end
359
- return attributes
360
- end
361
- end
32
+ # Edit
33
+ if edit
34
+ documents.edit()
362
35
 
36
+ # Sync
37
+ elsif sync
38
+ documents.sync()
363
39
 
364
- def dereference_value(value, scope)
365
- if value.is_a?(Hash) && value.length == 1 && Array(value.each_value)[0] == nil
366
- result = scope
367
- for name in Array(value.each_key)[0].split('.')
368
- result = get_property(result, name)
369
- end
370
- value = result
371
- elsif value.is_a?(Hash)
372
- for key, item in value
373
- value[key] = dereference_value(item, scope)
374
- end
375
- elsif value.is_a?(Array)
376
- for item, index in value.each_with_index
377
- value[index] = dereference_value(item, scope)
378
- end
40
+ # Test
41
+ else
42
+ success = documents.test(exit_first:exit_first)
43
+ if not success
44
+ exit(1)
379
45
  end
380
- return value
381
- end
382
-
383
-
384
- def get_property(owner, name)
385
- if owner.is_a?(Method)
386
- owner = owner.call()
387
- end
388
- if owner.class == Hash
389
- return owner[name]
390
- elsif owner.class == Array
391
- return owner[name.to_i]
392
- end
393
- return owner.method(name)
394
- end
395
-
396
-
397
- def set_property(owner, name, value)
398
- if owner.class == Hash
399
- owner[name] = value
400
- return
401
- elsif owner.class == Array
402
- owner[name.to_i] = value
403
- return
404
- end
405
- return owner.const_set(name, value)
406
- end
407
-
408
-
409
- # Main program
410
-
411
- path = ARGV[0] || nil
412
- specs = parse_specs(path)
413
- success = test_specs(specs)
414
- if !success
415
- exit(1)
416
46
  end
data/lib/document.rb ADDED
@@ -0,0 +1,266 @@
1
+ require 'net/http'
2
+ require_relative 'helpers'
3
+
4
+
5
+ # Module API
6
+
7
+ class DocumentList
8
+
9
+ # Public
10
+
11
+ def initialize(paths, config)
12
+ @documents = []
13
+ if paths.empty?
14
+ for item in config['documents']
15
+ paths.push(item['main'])
16
+ end
17
+ end
18
+ for path in !paths.empty? ? paths : ['README.md']
19
+ main_path = path
20
+ edit_path = nil
21
+ sync_path = nil
22
+ for item in config['documents']
23
+ if path == item['main']
24
+ edit_path = item.fetch('edit', nil)
25
+ sync_path = item.fetch('sync', nil)
26
+ break
27
+ end
28
+ end
29
+ document = Document.new(main_path, edit_path:edit_path, sync_path:sync_path)
30
+ @documents.push(document)
31
+ end
32
+ end
33
+
34
+ def edit()
35
+ for document in @documents
36
+ document.edit()
37
+ end
38
+ end
39
+
40
+ def sync()
41
+ success = true
42
+ for document in @documents
43
+ valid = document.test(sync:true)
44
+ success = success && valid
45
+ if valid
46
+ document.sync()
47
+ end
48
+ end
49
+ return success
50
+ end
51
+
52
+ def test(exit_first:false)
53
+ success = true
54
+ for document, index in @documents.each_with_index
55
+ number = index + 1
56
+ valid = document.test(exit_first:exit_first)
57
+ success = success && valid
58
+ print_message(nil, (number < @documents.length ? 'separator' : 'blank'))
59
+ end
60
+ return success
61
+ end
62
+
63
+ end
64
+
65
+
66
+ class Document
67
+
68
+ # Public
69
+
70
+ def initialize(main_path, edit_path:nil, sync_path:nil)
71
+ @main_path = main_path
72
+ @edit_path = edit_path
73
+ @sync_path = sync_path
74
+ end
75
+
76
+ def edit()
77
+
78
+ # No edit path
79
+ if !@edit_path
80
+ return
81
+ end
82
+
83
+ # Check synced
84
+ if @main_path != @edit_path
85
+ main_contents = _load_document(@main_path)
86
+ sync_contents = _load_document(@sync_path)
87
+ if main_contents != sync_contents
88
+ raise Exception.new("Document '#{@edit_path}' is out of sync")
89
+ end
90
+ end
91
+
92
+ # Remote document
93
+ # TODO: supress commands output
94
+ if !@edit_path.start_with?('http')
95
+ Kernel.system('editor', @edit_path)
96
+
97
+ # Local document
98
+ else
99
+ Kernel.system('xdg-open', @edit_path)
100
+ end
101
+
102
+ end
103
+
104
+ def sync()
105
+
106
+ # No sync path
107
+ if !@sync_path
108
+ return
109
+ end
110
+
111
+ # Save remote to local
112
+ contents = Net::HTTP.get(URI(@sync_path))
113
+ File.write(@main_path, contents)
114
+
115
+ end
116
+
117
+ def test(sync:false, return_report:false, exit_first:false)
118
+
119
+ # No test path
120
+ path = sync ? @sync_path : @main_path
121
+ if !path
122
+ return true
123
+ end
124
+
125
+ # Test document
126
+ contents = _load_document(path)
127
+ elements = _parse_document(contents)
128
+ report = _validate_document(elements, exit_first:exit_first)
129
+
130
+ return return_report ? report : report['valid']
131
+ end
132
+
133
+ end
134
+
135
+
136
+ # Internal
137
+
138
+ def _load_document(path)
139
+
140
+ # Remote document
141
+ if path.start_with?('http')
142
+ return Net::HTTP.get(URI(path))
143
+
144
+ # Local document
145
+ else
146
+ return File.read(path)
147
+ end
148
+
149
+ end
150
+
151
+
152
+ def _parse_document(contents)
153
+ elements = []
154
+ codeblock = ''
155
+ capture = false
156
+
157
+ # Parse file lines
158
+ for line in contents.strip().split("\n")
159
+
160
+ # Heading
161
+ if line.start_with?('#')
162
+ heading = line.strip().tr('#', '')
163
+ level = line.length - line.tr('#', '').length
164
+ if (!elements.empty? &&
165
+ elements[-1]['type'] == 'heading' &&
166
+ elements[-1]['level'] == level)
167
+ next
168
+ end
169
+ elements.push({
170
+ 'type' => 'heading',
171
+ 'value' => heading,
172
+ 'level' => level,
173
+ })
174
+ end
175
+
176
+ # Codeblock
177
+ if line.start_with?('```ruby')
178
+ if line.include?('goodread')
179
+ capture = true
180
+ end
181
+ codeblock = ''
182
+ next
183
+ end
184
+ if line.start_with?('```')
185
+ if capture
186
+ elements.push({
187
+ 'type' => 'codeblock',
188
+ 'value' => codeblock,
189
+ })
190
+ end
191
+ capture = false
192
+ end
193
+ if capture && !line.empty?
194
+ codeblock += line + "\n"
195
+ next
196
+ end
197
+
198
+ end
199
+
200
+ return elements
201
+ end
202
+
203
+
204
+ def _validate_document(elements, exit_first:false)
205
+ scope = binding()
206
+ passed = 0
207
+ failed = 0
208
+ skipped = 0
209
+ title = nil
210
+ exception = nil
211
+
212
+ # Test elements
213
+ for element in elements
214
+
215
+ # Heading
216
+ if element['type'] == 'heading'
217
+ print_message(element['value'], 'heading', level:element['level'])
218
+ if title == nil
219
+ title = element['value']
220
+ print_message(nil, 'separator')
221
+ end
222
+
223
+ # Codeblock
224
+ elsif element['type'] == 'codeblock'
225
+ exception_line = 1000 # infinity
226
+ begin
227
+ eval(instrument_codeblock(element['value']), scope)
228
+ rescue Exception => exc
229
+ exception = exc
230
+ # TODO: get a real exception line
231
+ exception_line = 1
232
+ end
233
+ lines = element['value'].strip().split("\n")
234
+ for line, index in lines.each_with_index
235
+ line_number = index + 1
236
+ if line_number < exception_line
237
+ print_message(line, 'success')
238
+ passed += 1
239
+ elsif line_number == exception_line
240
+ print_message(line, 'failure', exception:exception)
241
+ if exit_first
242
+ print_message(scope, 'scope')
243
+ raise exception
244
+ end
245
+ failed += 1
246
+ elsif line_number > exception_line
247
+ print_message(line, 'skipped')
248
+ skipped += 1
249
+ end
250
+ end
251
+ end
252
+
253
+ end
254
+
255
+ # Print summary
256
+ if title != nil
257
+ print_message(title, 'summary', passed:passed, failed:failed, skipped:skipped)
258
+ end
259
+
260
+ return {
261
+ 'valid' => exception == nil,
262
+ 'passed' => passed,
263
+ 'failed' => failed,
264
+ 'skipped' => skipped,
265
+ }
266
+ end
data/lib/helpers.rb ADDED
@@ -0,0 +1,85 @@
1
+ require 'yaml'
2
+ require 'emoji'
3
+ require 'colorize'
4
+ $state = {'last_message_type' => nil}
5
+
6
+
7
+ # Module API
8
+
9
+ def read_config()
10
+ config = {'documents' => ['README.md']}
11
+ if File.file?('goodread.yml')
12
+ config = YAML.load(File.read('goodread.yml'))
13
+ for document, index in config['documents'].each_with_index
14
+ if document.is_a?(Hash)
15
+ if !document.include?('main')
16
+ raise Exception.new('Document requires "main" property')
17
+ end
18
+ end
19
+ if document.is_a?(String)
20
+ config['documents'][index] = {'main' => document}
21
+ end
22
+ end
23
+ end
24
+ return config
25
+ end
26
+
27
+
28
+ def instrument_codeblock(codeblock)
29
+ lines = []
30
+ for line in codeblock.strip().split("\n")
31
+ if line.include?(' # ')
32
+ left, right = line.split(' # ')
33
+ left = left.strip()
34
+ right = right.strip()
35
+ if left && right
36
+ message = "#{left} != #{right}"
37
+ line = "raise '#{message}' unless #{left} == #{right}"
38
+ end
39
+ end
40
+ lines.push(line)
41
+ end
42
+ return lines.join("\n")
43
+ end
44
+
45
+
46
+ def print_message(message, type, level: nil, exception: nil, passed: nil, failed: nil, skipped: nil)
47
+ text = ''
48
+ if type == 'blank'
49
+ return puts('')
50
+ elsif type == 'separator'
51
+ text = Emoji.find_by_alias('heavy_minus_sign').raw * 3
52
+ elsif type == 'heading'
53
+ text = " #{'#' * (level || 1)}" + message.bold
54
+ elsif type == 'success'
55
+ text = " #{Emoji.find_by_alias('heavy_check_mark').raw} ".green + message
56
+ elsif type == 'failure'
57
+ text = " #{Emoji.find_by_alias('x').raw} ".red + message + "\n"
58
+ text += "Exception: #{exception}".red.bold
59
+ elsif type == 'scope'
60
+ text += "---\n\n"
61
+ text += "Scope (current execution scope):\n"
62
+ text += "#{message}\n"
63
+ text += "\n---\n"
64
+ elsif type == 'skipped'
65
+ text = " #{Emoji.find_by_alias('heavy_minus_sign').raw} ".yellow + message
66
+ elsif type == 'summary'
67
+ color = :green
68
+ text = (' ' + Emoji.find_by_alias('heavy_check_mark').raw + ' ').green.bold
69
+ if (failed + skipped) > 0
70
+ color = :red
71
+ text = ("\n " + Emoji.find_by_alias('x').raw + ' ').red.bold
72
+ end
73
+ text += "#{message}: #{passed}/#{passed + failed + skipped}".colorize(color).bold
74
+ end
75
+ if ['success', 'failure', 'skipped'].include?(type)
76
+ type = 'test'
77
+ end
78
+ if text
79
+ if $state['last_message_type'] != type
80
+ text = "\n" + text
81
+ end
82
+ puts(text)
83
+ $state['last_message_type'] = type
84
+ end
85
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: goodread
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - |
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-08-14 00:00:00.000000000 Z
12
+ date: 2017-11-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -101,6 +101,8 @@ files:
101
101
  - bin/setup
102
102
  - goodread.gemspec
103
103
  - lib/cli.rb
104
+ - lib/document.rb
105
+ - lib/helpers.rb
104
106
  homepage: https://github.com/goodread/goodread-rb
105
107
  licenses:
106
108
  - MIT
@@ -121,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
123
  version: '0'
122
124
  requirements: []
123
125
  rubyforge_project:
124
- rubygems_version: 2.6.11
126
+ rubygems_version: 2.7.1
125
127
  signing_key:
126
128
  specification_version: 4
127
129
  summary: goodread