asciidoctor-kroki 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e516cef89499dde5cea46152f3926f6b91c8f81c9e8ff57884dac15d1877fd1c
4
- data.tar.gz: 919d391cf9ca8ad118ead138a8338434b7ffa6d6e15e6678a65a084aadbedd15
3
+ metadata.gz: f7ac5f1586d5aa5a1cf3f3031b0bcc1839e40681913f30722703011e133b6a72
4
+ data.tar.gz: f10b7edc9d3e1092c35b07028b25255c6c2ae0921795d161dc542bdecc0b814f
5
5
  SHA512:
6
- metadata.gz: f96a43e66d73647ff2593e74ef695526fa52c6a9cf453c22d34d11bcc035bf8e5bb4256d43eab35f5e8290b32f5f02c0df064a975b7c3164efd83e8d96c320f3
7
- data.tar.gz: 1536eb019a5b9fe3bd939ee64b6bd26252dd5096185f919c15536b6405f436baee91ca852a5519c2414b9ceee2215fad1228b28d8995a1da85ac583e7d5b0c86
6
+ metadata.gz: 613ae500380df383cc2b05720795de55933d5813ebb10f70f9252454727fac61f05d7f9c581641c28be8ed1e9fd42b6dcb9bb86283de455f92e4c41e0e00a78e
7
+ data.tar.gz: 25e547670dea5f121373ddc402b3740d33a6c1c31a006f3c2dd66dd5a753f4daa9d61aa7b6919543d7ec5c1b641b590a59c95b8de1b9419c6ffba2be7747a938
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  pkg/
2
+ .asciidoctor/kroki
@@ -8,7 +8,10 @@ Metrics/MethodLength:
8
8
  Max: 50
9
9
 
10
10
  Metrics/CyclomaticComplexity:
11
- Max : 10
11
+ Max: 10
12
+
13
+ Metrics/PerceivedComplexity:
14
+ Max: 10
12
15
 
13
16
  Metrics/AbcSize:
14
- Max: 25
17
+ Max: 30
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- asciidoctor-kroki (0.1.2)
4
+ asciidoctor-kroki (0.2.0)
5
5
  asciidoctor (~> 2.0)
6
6
  asciidoctor-pdf (= 1.5.3)
7
7
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'asciidoctor-kroki'
5
- s.version = '0.1.2'
5
+ s.version = '0.2.0'
6
6
  s.summary = 'Asciidoctor extension to convert diagrams to images using Kroki'
7
7
  s.description = 'An extension for Asciidoctor to convert diagrams to images using https://kroki.io'
8
8
 
@@ -4,8 +4,7 @@ require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'
4
4
  require_relative 'asciidoctor_kroki/extension'
5
5
 
6
6
  Asciidoctor::Extensions.register do
7
- names = %w[plantuml ditaa graphviz blockdiag seqdiag actdiag nwdiag packetdiag rackdiag c4plantuml erd mermaid nomnoml svgbob umlet vega vegalite wavedrom]
8
- names.each do |name|
7
+ ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES.each do |name|
9
8
  block_macro ::AsciidoctorExtensions::KrokiBlockMacroProcessor, name
10
9
  block ::AsciidoctorExtensions::KrokiBlockProcessor, name
11
10
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal'
4
- require 'stringio'
5
- require 'zlib'
6
4
 
7
5
  # Asciidoctor extensions
8
6
  #
@@ -29,9 +27,11 @@ module AsciidoctorExtensions
29
27
  class KrokiBlockMacroProcessor < Asciidoctor::Extensions::BlockMacroProcessor
30
28
  use_dsl
31
29
 
30
+ name_positional_attributes 'format'
31
+
32
32
  def process(parent, target, attrs)
33
33
  diagram_type = @name
34
- target = parent.apply_subs(target, ['attributes'])
34
+ target = parent.apply_subs(target, [:attributes])
35
35
  diagram_text = read(target)
36
36
  KrokiProcessor.process(self, parent, attrs, diagram_type, diagram_text)
37
37
  end
@@ -46,9 +46,38 @@ module AsciidoctorExtensions
46
46
  end
47
47
  end
48
48
 
49
+ # Kroki API
50
+ #
51
+ module Kroki
52
+ SUPPORTED_DIAGRAM_NAMES = %w[
53
+ plantuml
54
+ ditaa
55
+ graphviz
56
+ blockdiag
57
+ seqdiag
58
+ actdiag
59
+ nwdiag
60
+ packetdiag
61
+ rackdiag
62
+ c4plantuml
63
+ erd
64
+ mermaid
65
+ nomnoml
66
+ svgbob
67
+ umlet
68
+ vega
69
+ vegalite
70
+ wavedrom
71
+ bytefield
72
+ bpmn
73
+ ].freeze
74
+ end
75
+
49
76
  # Internal processor
50
77
  #
51
78
  class KrokiProcessor
79
+ TEXT_FORMATS = %w[txt atxt utxt].freeze
80
+
52
81
  class << self
53
82
  def process(processor, parent, attrs, diagram_type, diagram_text)
54
83
  doc = parent.document
@@ -64,11 +93,18 @@ module AsciidoctorExtensions
64
93
  role = attrs['role']
65
94
  format = get_format(doc, attrs, diagram_type)
66
95
  attrs['role'] = get_role(format, role)
67
- attrs['alt'] = get_alt(attrs)
68
- attrs['target'] = create_image_src(doc, diagram_type, format, diagram_text)
69
96
  attrs['format'] = format
70
- block = processor.create_image_block(parent, attrs)
71
- block.title = title
97
+ kroki_diagram = KrokiDiagram.new(diagram_type, format, diagram_text)
98
+ kroki_client = KrokiClient.new(server_url(doc), http_method(doc), KrokiHttpClient)
99
+ if TEXT_FORMATS.include?(format)
100
+ text_content = kroki_client.text_content(kroki_diagram)
101
+ block = processor.create_block(parent, 'literal', text_content, attrs)
102
+ else
103
+ attrs['alt'] = get_alt(attrs)
104
+ attrs['target'] = create_image_src(doc, kroki_diagram, kroki_client)
105
+ block = processor.create_image_block(parent, attrs)
106
+ end
107
+ block.title = title if title
72
108
  block.assign_caption(caption, 'figure')
73
109
  block
74
110
  end
@@ -119,13 +155,168 @@ module AsciidoctorExtensions
119
155
  format
120
156
  end
121
157
 
122
- def create_image_src(doc, type, format, text)
123
- data = Base64.urlsafe_encode64(Zlib::Deflate.deflate(text, 9))
124
- "#{server_url(doc)}/#{type}/#{format}/#{data}"
158
+ def create_image_src(doc, kroki_diagram, kroki_client)
159
+ if doc.attr('kroki-fetch-diagram')
160
+ kroki_diagram.save(output_dir_path(doc), kroki_client)
161
+ else
162
+ kroki_diagram.get_diagram_uri(server_url(doc))
163
+ end
125
164
  end
126
165
 
127
166
  def server_url(doc)
128
- doc.attr('kroki-server-url') || 'https://kroki.io'
167
+ doc.attr('kroki-server-url', 'https://kroki.io')
168
+ end
169
+
170
+ def http_method(doc)
171
+ doc.attr('kroki-http-method', 'adaptive').downcase
172
+ end
173
+
174
+ def output_dir_path(doc)
175
+ images_output_dir = doc.attr('imagesoutdir')
176
+ out_dir = doc.attr('outdir')
177
+ to_dir = doc.attr('to_dir')
178
+ base_dir = doc.base_dir
179
+ images_dir = doc.attr('imagesdir', '')
180
+ if images_output_dir
181
+ images_output_dir
182
+ elsif out_dir
183
+ File.join(out_dir, images_dir)
184
+ elsif to_dir
185
+ File.join(to_dir, images_dir)
186
+ else
187
+ File.join(base_dir, images_dir)
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ # Kroki diagram
194
+ #
195
+ class KrokiDiagram
196
+ require 'fileutils'
197
+ require 'zlib'
198
+ require 'digest'
199
+
200
+ attr_reader :type
201
+ attr_reader :text
202
+ attr_reader :format
203
+
204
+ def initialize(type, format, text)
205
+ @text = text
206
+ @type = type
207
+ @format = format
208
+ end
209
+
210
+ def get_diagram_uri(server_url)
211
+ _join_uri_segments(server_url, @type, @format, encode)
212
+ end
213
+
214
+ def encode
215
+ Base64.urlsafe_encode64(Zlib::Deflate.deflate(@text, 9))
216
+ end
217
+
218
+ def save(output_dir_path, kroki_client)
219
+ diagram_url = get_diagram_uri(kroki_client.server_url)
220
+ diagram_name = "diag-#{Digest::SHA256.hexdigest diagram_url}.#{@format}"
221
+ file_path = File.join(output_dir_path, diagram_name)
222
+ encoding = if @format == 'txt' || @format == 'atxt' || @format == 'utxt'
223
+ 'utf8'
224
+ elsif @format == 'svg'
225
+ 'binary'
226
+ else
227
+ 'binary'
228
+ end
229
+ # file is either (already) on the file system or we should read it from Kroki
230
+ contents = File.exist?(file_path) ? File.open(file_path, &:read) : kroki_client.get_image(self, encoding)
231
+ FileUtils.mkdir_p(output_dir_path)
232
+ if encoding == 'binary'
233
+ File.binwrite(file_path, contents)
234
+ else
235
+ File.write(file_path, contents)
236
+ end
237
+ diagram_name
238
+ end
239
+
240
+ private
241
+
242
+ def _join_uri_segments(base, *uris)
243
+ segments = []
244
+ # remove trailing slashes
245
+ segments.push(base.gsub(%r{[/]+$}, ''))
246
+ segments.concat(uris.map do |uri|
247
+ # remove leading and trailing slashes
248
+ uri.to_s
249
+ .gsub(%r{^[/]+}, '')
250
+ .gsub(%r{[/]+$}, '')
251
+ end)
252
+ segments.join('/')
253
+ end
254
+ end
255
+
256
+ # Kroki client
257
+ #
258
+ class KrokiClient
259
+ attr_reader :server_url
260
+ attr_reader :method
261
+
262
+ SUPPORTED_HTTP_METHODS = %w[get post adaptive].freeze
263
+
264
+ def initialize(server_url, http_method, http_client)
265
+ @server_url = server_url
266
+ @max_uri_length = 4096
267
+ @http_client = http_client
268
+ method = (http_method || 'adaptive').downcase
269
+ if SUPPORTED_HTTP_METHODS.include?(method)
270
+ @method = method
271
+ else
272
+ puts "Invalid value '#{method}' for kroki-http-method attribute. The value must be either: 'get', 'post' or 'adaptive'. Proceeding using: 'adaptive'."
273
+ @method = 'adaptive'
274
+ end
275
+ end
276
+
277
+ def text_content(kroki_diagram)
278
+ get_image(kroki_diagram, 'utf-8')
279
+ end
280
+
281
+ def get_image(kroki_diagram, encoding)
282
+ type = kroki_diagram.type
283
+ format = kroki_diagram.format
284
+ text = kroki_diagram.text
285
+ if @method == 'adaptive' || @method == 'get'
286
+ uri = kroki_diagram.get_diagram_uri(server_url)
287
+ if uri.length > @max_uri_length
288
+ # The request URI is longer than 4096.
289
+ if @method == 'get'
290
+ # The request might be rejected by the server with a 414 Request-URI Too Large.
291
+ # Consider using the attribute kroki-http-method with the value 'adaptive'.
292
+ @http_client.get(uri, encoding)
293
+ else
294
+ @http_client.post("#{@server_url}/#{type}/#{format}", text, encoding)
295
+ end
296
+ else
297
+ @http_client.get(uri, encoding)
298
+ end
299
+ else
300
+ @http_client.post("#{@server_url}/#{type}/#{format}", text, encoding)
301
+ end
302
+ end
303
+ end
304
+
305
+ # Kroki HTTP client
306
+ #
307
+ class KrokiHttpClient
308
+ require 'net/http'
309
+ require 'uri'
310
+ require 'json'
311
+
312
+ class << self
313
+ def get(uri, _)
314
+ ::OpenURI.open_uri(uri, 'r', &:read)
315
+ end
316
+
317
+ def post(uri, data, _)
318
+ res = ::Net::HTTP.request_post(uri, data)
319
+ res.body
129
320
  end
130
321
  end
131
322
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec_helper'
4
+ require 'asciidoctor'
5
+ require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki'
6
+
7
+ describe ::AsciidoctorExtensions::KrokiClient do
8
+ it 'should use adaptive method when http method is invalid' do
9
+ kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient
10
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('http://localhost:8000', 'patch', kroki_http_client)
11
+ expect(kroki_client.method).to eq('adaptive')
12
+ end
13
+ it 'should use post method when http method is post' do
14
+ kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient
15
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('http://localhost:8000', 'POST', kroki_http_client)
16
+ expect(kroki_client.method).to eq('post')
17
+ end
18
+ it 'should use get method when http method is get' do
19
+ kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient
20
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('http://localhost:8000', 'get', kroki_http_client)
21
+ expect(kroki_client.method).to eq('get')
22
+ end
23
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec_helper'
4
+ require 'asciidoctor'
5
+ require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki'
6
+
7
+ describe ::AsciidoctorExtensions::KrokiDiagram do
8
+ it 'should compute a diagram URI' do
9
+ kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('vegalite', 'png', '{}')
10
+ diagram_uri = kroki_diagram.get_diagram_uri('http://localhost:8000')
11
+ expect(diagram_uri).to eq('http://localhost:8000/vegalite/png/eNqrrgUAAXUA-Q==')
12
+ end
13
+ it 'should compute a diagram URI with a trailing slashes' do
14
+ kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('vegalite', 'png', '{}')
15
+ diagram_uri = kroki_diagram.get_diagram_uri('https://my.domain.org/kroki/')
16
+ expect(diagram_uri).to eq('https://my.domain.org/kroki/vegalite/png/eNqrrgUAAXUA-Q==')
17
+ end
18
+ it 'should compute a diagram URI with trailing slashes' do
19
+ kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('vegalite', 'png', '{}')
20
+ diagram_uri = kroki_diagram.get_diagram_uri('https://my-server/kroki//')
21
+ expect(diagram_uri).to eq('https://my-server/kroki/vegalite/png/eNqrrgUAAXUA-Q==')
22
+ end
23
+ it 'should encode a diagram text definition' do
24
+ kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('plantuml', 'txt', ' alice -> bob: hello')
25
+ diagram_definition_encoded = kroki_diagram.encode
26
+ expect(diagram_definition_encoded).to eq('eNpTSMzJTE5V0LVTSMpPslLISM3JyQcAQAwGaw==')
27
+ end
28
+ it 'should fetch a diagram from Kroki and save it to disk' do
29
+ kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('plantuml', 'txt', ' alice -> bob: hello')
30
+ kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient
31
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('https://kroki.io', 'get', kroki_http_client)
32
+ output_dir_path = "#{__dir__}/../.asciidoctor/kroki"
33
+ diagram_name = kroki_diagram.save(output_dir_path, kroki_client)
34
+ diagram_path = File.join(output_dir_path, diagram_name)
35
+ expect(File.exist?(diagram_path)).to be_truthy, "expected diagram to be saved at #{diagram_path}"
36
+ content = <<-TXT.chomp
37
+ ,-----. ,---.
38
+ |alice| |bob|
39
+ `--+--' `-+-'
40
+ | hello |
41
+ |-------------->|
42
+ ,--+--. ,-+-.
43
+ |alice| |bob|
44
+ `-----' `---'
45
+ TXT
46
+ expect(File.read(diagram_path).split("\n").map(&:rstrip).join("\n")).to eq(content)
47
+ end
48
+ it 'should fetch a diagram from Kroki with the same definition only once' do
49
+ kroki_diagram = ::AsciidoctorExtensions::KrokiDiagram.new('plantuml', 'png', ' guillaume -> dan: hello')
50
+ kroki_http_client = ::AsciidoctorExtensions::KrokiHttpClient
51
+ kroki_client = ::AsciidoctorExtensions::KrokiClient.new('https://kroki.io', 'get', kroki_http_client)
52
+ output_dir_path = "#{__dir__}/../.asciidoctor/kroki"
53
+ # make sure that we are doing only one GET request
54
+ expect(kroki_http_client).to receive(:get).once
55
+ diagram_name = kroki_diagram.save(output_dir_path, kroki_client)
56
+ diagram_path = File.join(output_dir_path, diagram_name)
57
+ expect(File.exist?(diagram_path)).to be_truthy, "expected diagram to be saved at #{diagram_path}"
58
+ # calling again... should read the file from disk (and not do a GET request)
59
+ kroki_diagram.save(output_dir_path, kroki_client)
60
+ end
61
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rspec_helper'
3
4
  require 'asciidoctor'
4
5
  require_relative '../lib/asciidoctor/extensions/asciidoctor_kroki'
5
6
 
@@ -47,5 +48,47 @@ describe ::AsciidoctorExtensions::KrokiBlockProcessor do
47
48
  </div>
48
49
  </div>)
49
50
  end
51
+ it 'should create SVG diagram in imagesdir if kroki-fetch-diagram is set' do
52
+ input = <<~'ADOC'
53
+ :imagesdir: .asciidoctor/kroki
54
+
55
+ plantuml::spec/fixtures/alice.puml[svg,role=sequence]
56
+ ADOC
57
+ output = Asciidoctor.convert(input, attributes: { 'kroki-fetch-diagram' => '' }, standalone: false)
58
+ (expect output).to eql %(<div class="imageblock sequence kroki-format-svg kroki">
59
+ <div class="content">
60
+ <img src=".asciidoctor/kroki/diag-f6acdc206506b6ca7badd3fe722f252af992871426e580c8361ff4d47c2c7d9b.svg" alt="Diagram">
61
+ </div>
62
+ </div>)
63
+ end
64
+ it 'should create PNG diagram in imagesdir if kroki-fetch-diagram is set' do
65
+ input = <<~'ADOC'
66
+ :imagesdir: .asciidoctor/kroki
67
+
68
+ plantuml::spec/fixtures/alice.puml[png,role=sequence]
69
+ ADOC
70
+ output = Asciidoctor.convert(input, attributes: { 'kroki-fetch-diagram' => '' }, standalone: false)
71
+ (expect output).to eql %(<div class="imageblock sequence kroki-format-png kroki">
72
+ <div class="content">
73
+ <img src=".asciidoctor/kroki/diag-d4f314b2d4e75cc08aa4f8c2c944f7bf78321895d8ec5f665b42476d4e67e610.png" alt="Diagram">
74
+ </div>
75
+ </div>)
76
+ end
77
+ end
78
+ end
79
+
80
+ describe ::AsciidoctorExtensions::Kroki do
81
+ it 'should return the list of supported diagrams' do
82
+ diagram_names = ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES
83
+ expect(diagram_names).to include('vegalite', 'plantuml', 'bytefield', 'bpmn')
84
+ end
85
+ it 'should register the extension for the list of supported diagrams' do
86
+ doc = Asciidoctor::Document.new
87
+ registry = Asciidoctor::Extensions::Registry.new
88
+ registry.activate doc
89
+ ::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES.each do |name|
90
+ expect(registry.find_block_extension(name)).to_not be_nil, "expected block extension named '#{name}' to be registered"
91
+ expect(registry.find_block_macro_extension(name)).to_not be_nil, "expected block macro extension named '#{name}' to be registered "
92
+ end
50
93
  end
51
94
  end
@@ -0,0 +1 @@
1
+ alice -> bob: hello
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.configure do |config|
4
+ config.before(:suite) do
5
+ FileUtils.rm(Dir.glob("#{__dir__}/../.asciidoctor/kroki/diag-*"))
6
+ end
7
+ config.after(:suite) do
8
+ FileUtils.rm(Dir.glob("#{__dir__}/../.asciidoctor/kroki/diag-*")) unless ENV['DEBUG']
9
+ end
10
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciidoctor-kroki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guillaume Grossetie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-06 00:00:00.000000000 Z
11
+ date: 2020-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor
@@ -87,6 +87,7 @@ executables: []
87
87
  extensions: []
88
88
  extra_rdoc_files: []
89
89
  files:
90
+ - ".asciidoctor/kroki/.gitkeep"
90
91
  - ".gitignore"
91
92
  - ".rubocop.yml"
92
93
  - ".ruby-version"
@@ -98,9 +99,13 @@ files:
98
99
  - lib/asciidoctor/extensions/asciidoctor_kroki.rb
99
100
  - lib/asciidoctor/extensions/asciidoctor_kroki/extension.rb
100
101
  - spec/.rubocop.yml
102
+ - spec/asciidoctor_kroki_client_spec.rb
103
+ - spec/asciidoctor_kroki_diagram_spec.rb
101
104
  - spec/asciidoctor_kroki_spec.rb
105
+ - spec/fixtures/alice.puml
102
106
  - spec/fixtures/config.puml
103
107
  - spec/require_spec.rb
108
+ - spec/rspec_helper.rb
104
109
  - tasks/bundler.rake
105
110
  - tasks/lint.rake
106
111
  - tasks/rspec.rake
@@ -131,9 +136,13 @@ specification_version: 4
131
136
  summary: Asciidoctor extension to convert diagrams to images using Kroki
132
137
  test_files:
133
138
  - spec/.rubocop.yml
139
+ - spec/asciidoctor_kroki_client_spec.rb
140
+ - spec/asciidoctor_kroki_diagram_spec.rb
134
141
  - spec/asciidoctor_kroki_spec.rb
142
+ - spec/fixtures/alice.puml
135
143
  - spec/fixtures/config.puml
136
144
  - spec/require_spec.rb
145
+ - spec/rspec_helper.rb
137
146
  - tasks/bundler.rake
138
147
  - tasks/lint.rake
139
148
  - tasks/rspec.rake