tap 0.10.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. data/History +12 -0
  2. data/MIT-LICENSE +0 -2
  3. data/README +23 -32
  4. data/bin/rap +116 -0
  5. data/bin/tap +6 -9
  6. data/cgi/run.rb +67 -0
  7. data/cmd/console.rb +1 -1
  8. data/cmd/destroy.rb +4 -4
  9. data/cmd/generate.rb +4 -4
  10. data/cmd/manifest.rb +61 -53
  11. data/cmd/run.rb +8 -75
  12. data/doc/Class Reference +130 -121
  13. data/doc/Command Reference +76 -124
  14. data/doc/Syntax Reference +290 -0
  15. data/doc/Tutorial +305 -237
  16. data/lib/tap/app.rb +140 -467
  17. data/lib/tap/constants.rb +2 -2
  18. data/lib/tap/declarations.rb +211 -0
  19. data/lib/tap/env.rb +171 -193
  20. data/lib/tap/exe.rb +100 -21
  21. data/lib/tap/file_task.rb +3 -3
  22. data/lib/tap/generator/base.rb +1 -1
  23. data/lib/tap/generator/destroy.rb +10 -10
  24. data/lib/tap/generator/generate.rb +29 -18
  25. data/lib/tap/generator/generators/command/command_generator.rb +2 -2
  26. data/lib/tap/generator/generators/command/templates/command.erb +2 -2
  27. data/lib/tap/generator/generators/config/config_generator.rb +3 -3
  28. data/lib/tap/generator/generators/config/templates/doc.erb +1 -1
  29. data/lib/tap/generator/generators/file_task/file_task_generator.rb +1 -1
  30. data/lib/tap/generator/generators/file_task/templates/task.erb +1 -1
  31. data/lib/tap/generator/generators/file_task/templates/test.erb +1 -1
  32. data/lib/tap/generator/generators/generator/generator_generator.rb +27 -0
  33. data/lib/tap/generator/generators/generator/templates/task.erb +27 -0
  34. data/lib/tap/generator/generators/root/root_generator.rb +13 -13
  35. data/lib/tap/generator/generators/root/templates/README +0 -0
  36. data/lib/tap/generator/generators/root/templates/Rakefile +2 -2
  37. data/lib/tap/generator/generators/root/templates/gemspec +4 -5
  38. data/lib/tap/generator/generators/root/templates/tapfile +11 -8
  39. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +1 -1
  40. data/lib/tap/generator/generators/task/task_generator.rb +1 -3
  41. data/lib/tap/generator/generators/task/templates/test.erb +1 -3
  42. data/lib/tap/patches/optparse/summarize.rb +62 -0
  43. data/lib/tap/root.rb +41 -29
  44. data/lib/tap/support/aggregator.rb +16 -3
  45. data/lib/tap/support/assignments.rb +10 -9
  46. data/lib/tap/support/audit.rb +58 -64
  47. data/lib/tap/support/class_configuration.rb +33 -44
  48. data/lib/tap/support/combinator.rb +125 -0
  49. data/lib/tap/support/configurable.rb +13 -14
  50. data/lib/tap/support/configurable_class.rb +21 -43
  51. data/lib/tap/support/configuration.rb +55 -9
  52. data/lib/tap/support/constant.rb +87 -13
  53. data/lib/tap/support/constant_manifest.rb +116 -0
  54. data/lib/tap/support/dependencies.rb +54 -0
  55. data/lib/tap/support/dependency.rb +44 -0
  56. data/lib/tap/support/executable.rb +247 -32
  57. data/lib/tap/support/executable_queue.rb +1 -1
  58. data/lib/tap/support/gems/rake.rb +29 -8
  59. data/lib/tap/support/gems.rb +10 -30
  60. data/lib/tap/support/instance_configuration.rb +29 -3
  61. data/lib/tap/support/intern.rb +46 -0
  62. data/lib/tap/support/join.rb +143 -0
  63. data/lib/tap/support/joins/fork.rb +19 -0
  64. data/lib/tap/support/joins/merge.rb +22 -0
  65. data/lib/tap/support/joins/sequence.rb +21 -0
  66. data/lib/tap/support/joins/switch.rb +25 -0
  67. data/lib/tap/support/joins/sync_merge.rb +63 -0
  68. data/lib/tap/support/joins.rb +15 -0
  69. data/lib/tap/support/lazy_attributes.rb +17 -2
  70. data/lib/tap/support/lazydoc/comment.rb +503 -0
  71. data/lib/tap/support/lazydoc/config.rb +17 -0
  72. data/lib/tap/support/lazydoc/definition.rb +36 -0
  73. data/lib/tap/support/lazydoc/document.rb +152 -0
  74. data/lib/tap/support/lazydoc/method.rb +24 -0
  75. data/lib/tap/support/lazydoc.rb +269 -343
  76. data/lib/tap/support/manifest.rb +121 -103
  77. data/lib/tap/support/minimap.rb +90 -0
  78. data/lib/tap/support/node.rb +56 -0
  79. data/lib/tap/support/parser.rb +436 -0
  80. data/lib/tap/support/schema.rb +359 -0
  81. data/lib/tap/support/shell_utils.rb +3 -5
  82. data/lib/tap/support/string_ext.rb +60 -0
  83. data/lib/tap/support/tdoc.rb +7 -2
  84. data/lib/tap/support/templater.rb +30 -16
  85. data/lib/tap/support/validation.rb +77 -8
  86. data/lib/tap/task.rb +431 -143
  87. data/lib/tap/tasks/dump.rb +15 -10
  88. data/lib/tap/tasks/load.rb +112 -0
  89. data/lib/tap/tasks/rake.rb +4 -41
  90. data/lib/tap/test/assertions.rb +38 -0
  91. data/lib/tap/test/env_vars.rb +1 -1
  92. data/lib/tap/test/extensions.rb +79 -0
  93. data/lib/tap/test/file_test.rb +420 -0
  94. data/lib/tap/test/file_test_class.rb +12 -0
  95. data/lib/tap/test/regexp_escape.rb +87 -0
  96. data/lib/tap/test/script_test.rb +46 -0
  97. data/lib/tap/test/script_tester.rb +115 -0
  98. data/lib/tap/test/subset_test.rb +260 -0
  99. data/lib/tap/test/subset_test_class.rb +99 -0
  100. data/lib/tap/test/{tap_methods.rb → tap_test.rb} +45 -43
  101. data/lib/tap/test/utils.rb +231 -0
  102. data/lib/tap/test.rb +53 -26
  103. data/lib/tap.rb +3 -20
  104. metadata +50 -27
  105. data/lib/tap/generator/generators/root/templates/test/tapfile_test.rb +0 -15
  106. data/lib/tap/patches/rake/rake_test_loader.rb +0 -8
  107. data/lib/tap/patches/rake/testtask.rb +0 -57
  108. data/lib/tap/patches/ruby19/backtrace_filter.rb +0 -51
  109. data/lib/tap/patches/ruby19/parsedate.rb +0 -16
  110. data/lib/tap/support/batchable.rb +0 -47
  111. data/lib/tap/support/batchable_class.rb +0 -107
  112. data/lib/tap/support/command_line.rb +0 -98
  113. data/lib/tap/support/comment.rb +0 -270
  114. data/lib/tap/support/constant_utils.rb +0 -127
  115. data/lib/tap/support/declarations.rb +0 -111
  116. data/lib/tap/support/framework.rb +0 -83
  117. data/lib/tap/support/framework_class.rb +0 -180
  118. data/lib/tap/support/run_error.rb +0 -39
  119. data/lib/tap/support/summary.rb +0 -30
  120. data/lib/tap/test/file_methods.rb +0 -377
  121. data/lib/tap/test/script_methods/script_test.rb +0 -98
  122. data/lib/tap/test/script_methods.rb +0 -107
  123. data/lib/tap/test/subset_methods.rb +0 -420
  124. data/lib/tap/workflow.rb +0 -200
@@ -0,0 +1,503 @@
1
+ require 'strscan'
2
+
3
+ module Tap
4
+ module Support
5
+ module Lazydoc
6
+ # Comment represents a code comment parsed by Lazydoc. Comments consist
7
+ # of a subject and content.
8
+ #
9
+ # sample_comment = %Q{
10
+ # # this is the content
11
+ # #
12
+ # # content may stretch across
13
+ # # multiple lines
14
+ # this is the subject
15
+ # }
16
+ #
17
+ # Normally the subject is the first non-comment line following the content,
18
+ # although in some cases the subject will be manually set to something else
19
+ # (as in a Lazydoc constant attribute). The content is an array of comment
20
+ # fragments organized by line:
21
+ #
22
+ # c = Comment.parse(sample_comment)
23
+ # c.subject # => "this is the subject"
24
+ # c.content
25
+ # # => [
26
+ # # ["this is the content"],
27
+ # # [""],
28
+ # # ["content may stretch across", "multiple lines"]]
29
+ #
30
+ # Comments may be initialized to the subject line and then resolved later:
31
+ #
32
+ # doc = %Q{
33
+ # module Sample
34
+ # # this is the content of the comment
35
+ # # for method_one
36
+ # def method_one
37
+ # end
38
+ #
39
+ # # this is the content of the comment
40
+ # # for method_two
41
+ # def method_two
42
+ # end
43
+ # end}
44
+ #
45
+ # c1 = Comment.new(4).resolve(doc)
46
+ # c1.subject # => " def method_one"
47
+ # c1.content # => [["this is the content of the comment", "for method_one"]]
48
+ #
49
+ # c2 = Comment.new(9).resolve(doc)
50
+ # c2.subject # => " def method_two"
51
+ # c2.content # => [["this is the content of the comment", "for method_two"]]
52
+ #
53
+ # A Regexp (or Proc) may be used in place of a line number; during resolve,
54
+ # the lines will be scanned and the first matching line will be used.
55
+ #
56
+ # c3 = Comment.new(/def method_two/).resolve(doc)
57
+ # c3.subject # => " def method_two"
58
+ # c3.content # => [["this is the content of the comment", "for method_two"]]
59
+ #
60
+ class Comment
61
+
62
+ class << self
63
+
64
+ # Parses the input string into a comment. Takes a string or a
65
+ # StringScanner and returns the comment.
66
+ #
67
+ # comment_string = %Q{
68
+ # # comments spanning multiple
69
+ # # lines are collected
70
+ # #
71
+ # # while indented lines
72
+ # # are preserved individually
73
+ # #
74
+ # this is the subject line
75
+ #
76
+ # # this line is not parsed
77
+ # }
78
+ #
79
+ # c = Comment.parse(comment_string)
80
+ # c.content
81
+ # # => [
82
+ # # ['comments spanning multiple', 'lines are collected'],
83
+ # # [''],
84
+ # # [' while indented lines'],
85
+ # # [' are preserved individually'],
86
+ # # [''],
87
+ # # []]
88
+ # c.subject # => "this is the subject line"
89
+ #
90
+ # Parsing may be manually ended by providing a block; parse yields
91
+ # each line fragment to the block and stops parsing when the block
92
+ # returns true. Note that no subject will be parsed under these
93
+ # circumstances.
94
+ #
95
+ # c = Comment.parse(comment_string) {|frag| frag.strip.empty? }
96
+ # c.content
97
+ # # => [
98
+ # # ['comments spanning multiple', 'lines are collected']]
99
+ # c.subject # => nil
100
+ #
101
+ # Subject parsing may also be suppressed by setting parse_subject
102
+ # to false.
103
+ def parse(str, parse_subject=true) # :yields: fragment
104
+ scanner = case str
105
+ when StringScanner then str
106
+ when String then StringScanner.new(str)
107
+ else raise TypeError, "can't convert #{str.class} into StringScanner or String"
108
+ end
109
+
110
+ comment = Comment.new
111
+ while scanner.scan(/\r?\n?[ \t]*#[ \t]?(([ \t]*).*?)\r?$/)
112
+ fragment = scanner[1]
113
+ indent = scanner[2]
114
+
115
+ # collect continuous description line
116
+ # fragments and join into a single line
117
+ if block_given? && yield(fragment)
118
+ # break on comment if the description end is reached
119
+ parse_subject = false
120
+ break
121
+ else
122
+ categorize(fragment, indent) {|f| comment.push(f) }
123
+ end
124
+ end
125
+
126
+ if parse_subject
127
+ scanner.skip(/\s+/)
128
+ unless scanner.peek(1) == '#'
129
+ comment.subject = scanner.scan(/.+?$/)
130
+ comment.subject.strip! unless comment.subject == nil
131
+ end
132
+ end
133
+
134
+ comment
135
+ end
136
+
137
+ # Scan determines if and how to add a line fragment to a comment and
138
+ # yields the appropriate fragments to the block. Returns true if
139
+ # fragments are yielded and false otherwise.
140
+ #
141
+ # Content may be built from an array of lines using scan like so:
142
+ #
143
+ # lines = [
144
+ # "# comments spanning multiple",
145
+ # "# lines are collected",
146
+ # "#",
147
+ # "# while indented lines",
148
+ # "# are preserved individually",
149
+ # "# ",
150
+ # "not a comment line",
151
+ # "# skipped since the loop breaks",
152
+ # "# at the first non-comment line"]
153
+ #
154
+ # c = Comment.new
155
+ # lines.each do |line|
156
+ # break unless Comment.scan(line) do |fragment|
157
+ # c.push(fragment)
158
+ # end
159
+ # end
160
+ #
161
+ # c.content
162
+ # # => [
163
+ # # ['comments spanning multiple', 'lines are collected'],
164
+ # # [''],
165
+ # # [' while indented lines'],
166
+ # # [' are preserved individually'],
167
+ # # [''],
168
+ # # []]
169
+ #
170
+ def scan(line) # :yields: fragment
171
+ return false unless line =~ /^[ \t]*#[ \t]?(([ \t]*).*?)\r?$/
172
+ categorize($1, $2) do |fragment|
173
+ yield(fragment)
174
+ end
175
+ true
176
+ end
177
+
178
+ # Splits a line of text along whitespace breaks into fragments of cols
179
+ # width. Tabs in the line will be expanded into tabsize spaces;
180
+ # fragments are rstripped of whitespace.
181
+ #
182
+ # Comment.wrap("some line that will wrap", 10) # => ["some line", "that will", "wrap"]
183
+ # Comment.wrap(" line that will wrap ", 10) # => [" line", "that will", "wrap"]
184
+ # Comment.wrap(" ", 10) # => []
185
+ #
186
+ # The wrapping algorithm is slightly modified from:
187
+ # http://blog.macromates.com/2006/wrapping-text-with-regular-expressions/
188
+ def wrap(line, cols=80, tabsize=2)
189
+ line = line.gsub(/\t/, " " * tabsize) unless tabsize == nil
190
+ line.gsub(/(.{1,#{cols}})( +|$\r?\n?)|(.{1,#{cols}})/, "\\1\\3\n").split(/\s*?\n/)
191
+ end
192
+
193
+ private
194
+
195
+ # utility method used by scan to categorize and yield
196
+ # the appropriate objects to add the fragment to a
197
+ # comment
198
+ def categorize(fragment, indent) # :nodoc:
199
+ case
200
+ when fragment == indent
201
+ # empty comment line
202
+ yield [""]
203
+ yield []
204
+ when indent.empty?
205
+ # continuation line
206
+ yield fragment.rstrip
207
+ else
208
+ # indented line
209
+ yield [fragment.rstrip]
210
+ yield []
211
+ end
212
+ end
213
+ end
214
+
215
+ # An array of comment fragments organized into lines
216
+ attr_reader :content
217
+
218
+ # The subject of the comment (normally set to the next
219
+ # non-comment line after the content ends; ie the line
220
+ # that would receive the comment in RDoc documentation)
221
+ attr_accessor :subject
222
+
223
+ # Returns the line number for the subject line, if known.
224
+ # Although normally an integer, line_number may be
225
+ # set to a Regexp or Proc to dynamically determine
226
+ # the subject line during resolve
227
+ attr_accessor :line_number
228
+
229
+ def initialize(line_number=nil)
230
+ @content = []
231
+ @subject = nil
232
+ @line_number = line_number
233
+ end
234
+
235
+ # Alias for subject
236
+ def value
237
+ subject
238
+ end
239
+
240
+ # Alias for subject=
241
+ def value=(value)
242
+ self.subject = value
243
+ end
244
+
245
+ # Pushes the fragment onto the last line array of content. If the
246
+ # fragment is an array itself then it will be pushed onto content
247
+ # as a new line.
248
+ #
249
+ # c = Comment.new
250
+ # c.push "some line"
251
+ # c.push "fragments"
252
+ # c.push ["a", "whole", "new line"]
253
+ #
254
+ # c.content
255
+ # # => [
256
+ # # ["some line", "fragments"],
257
+ # # ["a", "whole", "new line"]]
258
+ #
259
+ def push(fragment)
260
+ content << [] if content.empty?
261
+
262
+ case fragment
263
+ when Array
264
+ if content[-1].empty?
265
+ content[-1] = fragment
266
+ else
267
+ content.push fragment
268
+ end
269
+ else
270
+ content[-1].push fragment
271
+ end
272
+ end
273
+
274
+ # Alias for push.
275
+ def <<(fragment)
276
+ push(fragment)
277
+ end
278
+
279
+ # Scans the comment line using Comment.scan and pushes the appropriate
280
+ # fragments onto self. Used to build a content by scanning down a set
281
+ # of lines.
282
+ #
283
+ # lines = [
284
+ # "# comment spanning multiple",
285
+ # "# lines",
286
+ # "#",
287
+ # "# indented line one",
288
+ # "# indented line two",
289
+ # "# ",
290
+ # "not a comment line"]
291
+ #
292
+ # c = Comment.new
293
+ # lines.each {|line| c.append(line) }
294
+ #
295
+ # c.content
296
+ # # => [
297
+ # # ['comment spanning multiple', 'lines'],
298
+ # # [''],
299
+ # # [' indented line one'],
300
+ # # [' indented line two'],
301
+ # # [''],
302
+ # # []]
303
+ #
304
+ def append(line)
305
+ Comment.scan(line) {|f| push(f) }
306
+ end
307
+
308
+ # Unshifts the fragment to the first line array of content. If the
309
+ # fragment is an array itself then it will be unshifted onto content
310
+ # as a new line.
311
+ #
312
+ # c = Comment.new
313
+ # c.unshift "some line"
314
+ # c.unshift "fragments"
315
+ # c.unshift ["a", "whole", "new line"]
316
+ #
317
+ # c.content
318
+ # # => [
319
+ # # ["a", "whole", "new line"],
320
+ # # ["fragments", "some line"]]
321
+ #
322
+ def unshift(fragment)
323
+ content << [] if content.empty?
324
+
325
+ case fragment
326
+ when Array
327
+ if content[0].empty?
328
+ content[0] = fragment
329
+ else
330
+ content.unshift fragment
331
+ end
332
+ else
333
+ content[0].unshift fragment
334
+ end
335
+ end
336
+
337
+ # Scans the comment line using Comment.scan and unshifts the appropriate
338
+ # fragments onto self. Used to build a content by scanning up a set of
339
+ # lines.
340
+ #
341
+ # lines = [
342
+ # "# comment spanning multiple",
343
+ # "# lines",
344
+ # "#",
345
+ # "# indented line one",
346
+ # "# indented line two",
347
+ # "# ",
348
+ # "not a comment line"]
349
+ #
350
+ # c = Comment.new
351
+ # lines.reverse_each {|line| c.prepend(line) }
352
+ #
353
+ # c.content
354
+ # # => [
355
+ # # ['comment spanning multiple', 'lines'],
356
+ # # [''],
357
+ # # [' indented line one'],
358
+ # # [' indented line two'],
359
+ # # ['']]
360
+ #
361
+ def prepend(line)
362
+ Comment.scan(line) {|f| unshift(f) }
363
+ end
364
+
365
+ # Builds the subject and content of self using lines; resolve sets
366
+ # the subject to the line at line_number, and parses content up
367
+ # from there. Any previously set subject and content is overridden.
368
+ # Returns self.
369
+ #
370
+ # document = %Q{
371
+ # module Sample
372
+ # # this is the content of the comment
373
+ # # for method_one
374
+ # def method_one
375
+ # end
376
+ #
377
+ # # this is the content of the comment
378
+ # # for method_two
379
+ # def method_two
380
+ # end
381
+ # end}
382
+ #
383
+ # c = Comment.new 4
384
+ # c.resolve(document)
385
+ # c.subject # => " def method_one"
386
+ # c.content # => [["this is the content of the comment", "for method_one"]]
387
+ #
388
+ # Lines may be an array or a string; string inputs are split into an
389
+ # array along newline boundaries.
390
+ #
391
+ # === dynamic line numbers
392
+ # The line_number used by resolve may be determined dynamically from
393
+ # lines by setting line_number to a Regexp and Proc. In the case
394
+ # of a Regexp, the first line matching the regexp is used:
395
+ #
396
+ # c = Comment.new(/def method/)
397
+ # c.resolve(document)
398
+ # c.line_number = 4
399
+ # c.subject # => " def method_one"
400
+ # c.content # => [["this is the content of the comment", "for method_one"]]
401
+ #
402
+ # Procs are called with lines and are expected to return the
403
+ # actual line number.
404
+ #
405
+ # c = Comment.new lambda {|lines| 9 }
406
+ # c.resolve(document)
407
+ # c.line_number = 9
408
+ # c.subject # => " def method_two"
409
+ # c.content # => [["this is the content of the comment", "for method_two"]]
410
+ #
411
+ # As shown in the examples, in both cases the dynamically determined
412
+ # line_number overwrites the Regexp or Proc.
413
+ def resolve(lines)
414
+ lines = lines.split(/\r?\n/) if lines.kind_of?(String)
415
+
416
+ # resolve late-evaluation line numbers
417
+ n = case line_number
418
+ when Regexp then match_index(line_number, lines)
419
+ when Proc then line_number.call(lines)
420
+ else line_number
421
+ end
422
+
423
+ # quietly exit if a line number was not found
424
+ return self unless n.kind_of?(Integer)
425
+
426
+ unless n < lines.length
427
+ raise RangeError, "line_number outside of lines: #{line_number} (#{lines.length})"
428
+ end
429
+
430
+ self.line_number = n
431
+ self.subject = lines[n]
432
+ self.content.clear
433
+
434
+ # remove whitespace lines
435
+ n -= 1
436
+ n -= 1 while n >=0 && lines[n].strip.empty?
437
+
438
+ # put together the comment
439
+ while n >= 0
440
+ break unless prepend(lines[n])
441
+ n -= 1
442
+ end
443
+
444
+ self
445
+ end
446
+
447
+ # Removes leading and trailing lines from content that are
448
+ # empty ([]) or whitespace (['']). Returns self.
449
+ def trim
450
+ content.shift while !content.empty? && (content[0].empty? || content[0].join.strip.empty?)
451
+ content.pop while !content.empty? && (content[-1].empty? || content[-1].join.strip.empty?)
452
+ self
453
+ end
454
+
455
+ # True if all lines in content are empty.
456
+ def empty?
457
+ !content.find {|line| !line.empty?}
458
+ end
459
+
460
+ # Returns content as a string where line fragments are joined by
461
+ # fragment_sep and lines are joined by line_sep.
462
+ def to_s(fragment_sep=" ", line_sep="\n", strip=true)
463
+ lines = content.collect {|line| line.join(fragment_sep)}
464
+
465
+ # strip leading an trailing whitespace lines
466
+ if strip
467
+ lines.shift while !lines.empty? && lines[0].empty?
468
+ lines.pop while !lines.empty? && lines[-1].empty?
469
+ end
470
+
471
+ line_sep ? lines.join(line_sep) : lines
472
+ end
473
+
474
+ # Like to_s, but wraps the content to the specified number of cols
475
+ # and expands tabs to tabsize spaces.
476
+ def wrap(cols=80, tabsize=2, line_sep="\n", fragment_sep=" ", strip=true)
477
+ lines = Comment.wrap(to_s(fragment_sep, "\n", strip), cols, tabsize)
478
+ line_sep ? lines.join(line_sep) : lines
479
+ end
480
+
481
+ # Returns true if another is a Comment with the same
482
+ # line_number, subject, and content as self
483
+ def ==(another)
484
+ another.kind_of?(Comment) &&
485
+ self.line_number == another.line_number &&
486
+ self.subject == another.subject &&
487
+ self.content == another.content
488
+ end
489
+
490
+ private
491
+
492
+ # utility method used to by resolve to find the index
493
+ # of a line matching a regexp line_number.
494
+ def match_index(regexp, lines) # :nodoc:
495
+ lines.each_with_index do |line, index|
496
+ return index if line =~ regexp
497
+ end
498
+ nil
499
+ end
500
+ end
501
+ end
502
+ end
503
+ end
@@ -0,0 +1,17 @@
1
+ module Tap
2
+ module Support
3
+ module Lazydoc
4
+ class Config < Comment
5
+ def empty?
6
+ to_str.empty?
7
+ end
8
+
9
+ def to_str
10
+ # currently removes the :no_default: document modifier
11
+ # which is used during generation of TDoc
12
+ subject.to_s =~ /#\s*(:no_default:)?\s*(.*)$/ ? $2.strip : ""
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ module Tap
2
+ module Support
3
+ module Lazydoc
4
+ class Definition < Comment
5
+ attr_accessor :subclass
6
+
7
+ def configurations(fragment_sep=" ", line_sep="\n", strip=true)
8
+ lines = []
9
+ subclass.configurations.each do |receiver, key, config|
10
+ desc = config.desc
11
+ case desc
12
+ when Definition
13
+ lines << "# #{desc.subclass}"
14
+ lines.concat desc.original_to_s(fragment_sep, nil, strip).collect {|line| "# #{line}"}
15
+ lines << "#{key}:"
16
+ lines.concat desc.configurations(fragment_sep).collect {|line| " #{line}"}
17
+ else
18
+ lines << "# #{desc}"
19
+ lines << "#{key}: #{config.default}"
20
+ lines << ""
21
+ end
22
+ end
23
+
24
+ lines
25
+ end
26
+
27
+ alias original_to_s to_s
28
+
29
+ def to_s(fragment_sep=" ", line_sep="\n", strip=true)
30
+ lines = [original_to_s(fragment_sep, line_sep, strip)] + configurations(fragment_sep)
31
+ line_sep ? lines.join(line_sep) : lines
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,152 @@
1
+ require 'tap/support/lazydoc/comment'
2
+
3
+ module Tap
4
+ module Support
5
+ module Lazydoc
6
+
7
+ # A Document tracks constant attributes and code comments for a particular
8
+ # source file. Documents may be assigned a default_const_name to be used
9
+ # when a constant attribute does not specify a constant.
10
+ #
11
+ # # KeyWithConst::key value a
12
+ # # ::key value b
13
+ #
14
+ # doc = Document.new(__FILE__, 'DefaultConst')
15
+ # doc.resolve
16
+ # doc['KeyWithConst']['key'].value # => 'value a'
17
+ # doc['DefaultConst']['key'].value # => 'value b'
18
+ #
19
+ class Document
20
+
21
+ # The source file for self, used during resolve
22
+ attr_reader :source_file
23
+
24
+ # An array of Comment objects identifying lines
25
+ # resolved or to-be-resolved
26
+ attr_reader :comments
27
+
28
+ # A hash of [const_name, attributes] pairs tracking the constant
29
+ # attributes resolved or to-be-resolved for self. Attributes
30
+ # are hashes of [key, comment] pairs.
31
+ attr_reader :const_attrs
32
+
33
+ # The default constant name used when no constant name
34
+ # is specified for a constant attribute
35
+ attr_reader :default_const_name
36
+
37
+ # Flag indicating whether or not self has been resolved
38
+ attr_accessor :resolved
39
+
40
+ def initialize(source_file=nil, default_const_name='')
41
+ self.source_file = source_file
42
+ @default_const_name = default_const_name
43
+ @comments = []
44
+ @const_attrs = {}
45
+ @resolved = false
46
+ self.reset
47
+ end
48
+
49
+ # Resets self by clearing const_attrs, comments, and setting
50
+ # resolved to false. Generally NOT recommended as this
51
+ # clears any work you've done registering lines; to simply
52
+ # allow resolve to re-scan a document, manually set
53
+ # resolved to false.
54
+ def reset
55
+ @const_attrs.clear
56
+ @comments.clear
57
+ @resolved = false
58
+ self
59
+ end
60
+
61
+ # Sets the source file for self. Expands the source file path if necessary.
62
+ def source_file=(source_file)
63
+ @source_file = source_file == nil ? nil : File.expand_path(source_file)
64
+ end
65
+
66
+ # Sets the default_const_name for self. Any const_attrs assigned to
67
+ # the previous default will be removed and merged with those already
68
+ # assigned to the new default.
69
+ def default_const_name=(const_name)
70
+ self[const_name].merge!(const_attrs.delete(@default_const_name) || {})
71
+ @default_const_name = const_name
72
+ end
73
+
74
+ # Returns the attributes for the specified const_name.
75
+ def [](const_name)
76
+ const_attrs[const_name] ||= {}
77
+ end
78
+
79
+ # Returns an array of the const_names in self with at
80
+ # least one attribute.
81
+ def const_names
82
+ names = []
83
+ const_attrs.each_pair do |const_name, attrs|
84
+ names << const_name unless attrs.empty?
85
+ end
86
+ names
87
+ end
88
+
89
+ # Register the specified line number to self. Register
90
+ # may take an integer or a regexp for late-evaluation.
91
+ # See Comment#resolve for more details.
92
+ #
93
+ # Returns a comment_class instance corresponding to the line.
94
+ def register(line_number, comment_class=Comment)
95
+ comment = comments.find {|c| c.class == comment_class && c.line_number == line_number }
96
+
97
+ if comment == nil
98
+ comment = comment_class.new(line_number)
99
+ comments << comment
100
+ end
101
+
102
+ comment
103
+ end
104
+
105
+ # Registers a regexp matching methods by the specified
106
+ # name.
107
+ def register_method(method, comment_class=Comment)
108
+ register(/^\s*def\s+#{method}(\W|$)/, comment_class)
109
+ end
110
+
111
+ # Scans str for constant attributes and adds them to to self. Code
112
+ # comments are also resolved against str. If no str is specified,
113
+ # the contents of source_file are used instead.
114
+ #
115
+ # Resolve does nothing if resolved == true. Returns true if str
116
+ # was resolved, or false otherwise.
117
+ def resolve(str=nil)
118
+ return(false) if resolved
119
+
120
+ str = File.read(source_file) if str == nil
121
+ Lazydoc.parse(str) do |const_name, key, comment|
122
+ const_name = default_const_name if const_name.empty?
123
+ self[const_name][key] = comment
124
+ end
125
+
126
+ unless comments.empty?
127
+ lines = str.split(/\r?\n/)
128
+ comments.each do |comment|
129
+ comment.resolve(lines)
130
+ end
131
+ end
132
+
133
+ @resolved = true
134
+ end
135
+
136
+ def to_hash
137
+ const_hash = {}
138
+ const_attrs.each_pair do |const_name, attributes|
139
+ next if attributes.empty?
140
+
141
+ attr_hash = {}
142
+ attributes.each_pair do |key, comment|
143
+ attr_hash[key] = (block_given? ? yield(comment) : comment)
144
+ end
145
+ const_hash[const_name] = attr_hash
146
+ end
147
+ const_hash
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end