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 +5 -5
- data/.gitignore +1 -0
- data/goodread.gemspec +1 -1
- data/lib/cli.rb +30 -400
- data/lib/document.rb +266 -0
- data/lib/helpers.rb +85 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2dd0cac02f1796fe0b626e32cc6cc6f4c85424f59457fde0db95a70e8cb8b26a
|
4
|
+
data.tar.gz: 55e79251fd7b7dc19aa6f7534b21778697ee53036f0d66283d641b5db894352b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 30d152a2f6868a1a50a2c9620680e0c3bba6b0294d0783b4e1e5a30d29a996693bc2c91c849ef3f720a56f76289b0adf687c8f9fc9b6aba64e17be76cf119e23
|
7
|
+
data.tar.gz: 73c5aa278a4480f2599ad6038b46689d5589286b5d8c09343c3dfaf26b56f888d8d27f3e0edd4f34920adb5239ae489ffb4aaa661f8631c9bf7028806130c871
|
data/.gitignore
CHANGED
data/goodread.gemspec
CHANGED
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
|
-
#
|
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
|
-
|
313
|
-
|
314
|
-
|
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
|
-
|
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
|
-
|
344
|
-
|
345
|
-
|
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
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
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
|
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-
|
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.
|
126
|
+
rubygems_version: 2.7.1
|
125
127
|
signing_key:
|
126
128
|
specification_version: 4
|
127
129
|
summary: goodread
|