doc_repo 0.1.1 → 1.0.0.pre.beta.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.
@@ -1,55 +1,110 @@
1
- require 'support/using_env'
1
+ # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe DocRepo::Configuration do
4
4
 
5
- context "without environment variables configured" do
5
+ shared_examples "has setting" do |setting_name|
6
+ it "allows configuring `#{setting_name}`" do
7
+ a_config = DocRepo::Configuration.new
8
+ expect {
9
+ a_config.public_send "#{setting_name}=", "Any Value"
10
+ }.to change(a_config, setting_name).to "Any Value"
11
+ end
12
+ end
13
+
14
+ context "new configuration" do
6
15
  subject(:default_config) { DocRepo::Configuration.new }
7
16
 
8
- around do |example|
9
- env = {
10
- 'DOC_REPO_ORG' => nil,
11
- 'DOC_REPO_REPONAME' => nil,
12
- 'DOC_REPO_BRANCH' => nil,
13
- }
14
- using_env(env, &example)
17
+ it "yields itself when given a block" do
18
+ a_config = DocRepo::Configuration.new { |c| c.repo = "Any Repo" }
19
+ expect(a_config.repo).to eq "Any Repo"
15
20
  end
16
21
 
17
- it "has no org" do
18
- expect(default_config.org).to be nil
22
+ it "defaults to the 'master' branch" do
23
+ expect(default_config.branch).to eq 'master'
19
24
  end
20
25
 
21
- it "has no repo" do
22
- expect(default_config.repo).to be nil
26
+ it "defaults to empty cache options" do
27
+ expect(default_config.cache_options).to eq({})
23
28
  end
24
29
 
25
- it "use the 'master' branch" do
26
- expect(default_config.branch).to eq 'master'
30
+ it "defaults to a null cache store" do
31
+ expect(default_config.cache_store).to be DocRepo::NullCache.instance
32
+ end
33
+
34
+ it "defaults to markdown and HTML as documentation formats" do
35
+ expect(default_config.doc_formats).to match_array %w[
36
+ .md
37
+ .markdown
38
+ .htm
39
+ .html
40
+ ]
27
41
  end
28
- end
29
42
 
30
- context "with environment variables configured" do
31
- subject(:default_env_config) { DocRepo::Configuration.new }
43
+ it "defaults to a 'docs' root path" do
44
+ expect(default_config.doc_root).to eq 'docs'
45
+ end
32
46
 
33
- around do |example|
34
- env = {
35
- 'DOC_REPO_ORG' => 'the-silence',
36
- 'DOC_REPO_REPONAME' => 'falls',
37
- 'DOC_REPO_BRANCH' => 'ask_the_question',
38
- }
39
- using_env(env, &example)
47
+ it "defaults to '.md' for the fallback extension" do
48
+ expect(default_config.fallback_ext).to eq '.md'
40
49
  end
41
50
 
42
- it "uses DOC_REPO_ORG for the default org" do
43
- expect(default_env_config.org).to eq 'the-silence'
51
+ it "has no org set" do
52
+ expect(default_config.org).to be nil
44
53
  end
45
54
 
46
- it "uses DOC_REPO_REPONAME for the default repo" do
47
- expect(default_env_config.repo).to eq 'falls'
55
+ it "has no repo set" do
56
+ expect(default_config.repo).to be nil
48
57
  end
49
58
 
50
- it "uses DOC_REPO_BRANCH for the default branch" do
51
- expect(default_env_config.branch).to eq 'ask_the_question'
59
+ it "converting to a hash includes all settings" do
60
+ expect(default_config.to_h.keys).to match_array %i[
61
+ branch
62
+ cache_options
63
+ cache_store
64
+ doc_formats
65
+ doc_root
66
+ fallback_ext
67
+ org
68
+ repo
69
+ ]
52
70
  end
53
71
  end
54
72
 
73
+ include_examples "has setting", :branch
74
+ include_examples "has setting", :cache_options
75
+ include_examples "has setting", :cache_store
76
+ include_examples "has setting", :doc_formats
77
+ include_examples "has setting", :doc_root
78
+ include_examples "has setting", :fallback_ext
79
+ include_examples "has setting", :org
80
+ include_examples "has setting", :repo
81
+
82
+ it "converting to a hash maps all settings to configured values" do
83
+ a_config = DocRepo::Configuration.new
84
+ a_config.branch = "Any Branch"
85
+ a_config.cache_options = "Any Cache Options"
86
+ a_config.cache_store = "Any Cache Store"
87
+ a_config.doc_formats = %w[ .any .formats ]
88
+ a_config.doc_root = "Any Doc Root"
89
+ a_config.fallback_ext = ".anything"
90
+ a_config.org = "Any Org"
91
+ a_config.repo = "Any Repo"
92
+ expect(a_config.to_h).to eq(
93
+ branch: "Any Branch",
94
+ cache_options: "Any Cache Options",
95
+ cache_store: "Any Cache Store",
96
+ doc_formats: %w[ .any .formats ],
97
+ doc_root: "Any Doc Root",
98
+ fallback_ext: ".anything",
99
+ org: "Any Org",
100
+ repo: "Any Repo",
101
+ )
102
+ end
103
+
104
+ it "doesn't allow creating new settings" do
105
+ expect {
106
+ DocRepo::Configuration.add_setting :new_setting
107
+ }.to raise_error NoMethodError
108
+ end
109
+
55
110
  end
@@ -0,0 +1,442 @@
1
+ # frozen_string_literal: true
2
+ require 'ostruct'
3
+
4
+ RSpec.describe DocRepo::Doc do
5
+
6
+ context "a standard document", "with cachable content" do
7
+ subject(:a_document) {
8
+ DocRepo::Doc.new("/any/uri", any_http_ok_response)
9
+ }
10
+
11
+ let(:any_http_ok_response) {
12
+ instance_double("Net::HTTPOK", code: "200", body: "Any Content Body").tap { |dbl|
13
+ allow(dbl).to receive(:[]) { |key| response_cache_headers[key] }
14
+ }
15
+ }
16
+
17
+ let(:response_cache_headers) {
18
+ # Make string keys mutable to allow testing mutations
19
+ {
20
+ "Last-Modified" => String.new("Sat, 01 Jul 2017 18:18:33 GMT"),
21
+ "Content-Type" => String.new("text/plain"),
22
+ "Cache-Control" => String.new("max-age=300, private, must-revalidate"),
23
+ }
24
+ }
25
+
26
+ it "is not an error" do
27
+ expect(a_document).not_to be_an_error
28
+ end
29
+
30
+ it "is not missing" do
31
+ expect(a_document).not_to be_not_found
32
+ end
33
+
34
+ it "is not a redirect" do
35
+ expect(a_document).not_to be_a_redirect
36
+ end
37
+
38
+ it "is successful" do
39
+ expect(a_document).to be_a_success
40
+ end
41
+
42
+ it "has a numeric status code" do
43
+ expect(a_document.code).to eq 200
44
+ end
45
+
46
+ it "has a URI" do
47
+ expect(a_document.uri).to eq "/any/uri"
48
+ end
49
+
50
+ it "sets the last modified time from the cache headers" do
51
+ modified_time = Time.gm(2017, 7, 1, 18, 18, 33)
52
+ expect(a_document.last_modified).to eq(modified_time).and be_frozen
53
+ expect(response_cache_headers["Last-Modified"]).not_to be_frozen
54
+ end
55
+
56
+ it "sets the content to the response body" do
57
+ expect(a_document.content).to eq "Any Content Body"
58
+ end
59
+
60
+ it "sets the content type according to the associated header" do
61
+ expect(a_document.content_type).to eq "text/plain"
62
+ end
63
+
64
+ it "uses the URI as the cache key" do
65
+ expect(a_document.cache_key).to eq "/any/uri"
66
+ end
67
+
68
+ it "sets the cache version based on the raw content" do
69
+ content_version = Digest::SHA1.hexdigest("Any Content Body")
70
+ expect(a_document.cache_version).to eq(content_version).and be_frozen
71
+ end
72
+
73
+ it "has a convenience reader for a versioned cache key" do
74
+ content_version = Digest::SHA1.hexdigest("Any Content Body")
75
+ expect(a_document.cache_key_with_version).to eq "/any/uri-#{content_version}"
76
+ end
77
+
78
+ it "allows access to the cache control settings" do
79
+ expect(a_document.cache_control).to eq "max-age=300, private, must-revalidate"
80
+ end
81
+ end
82
+
83
+ context "a standard document", "with missing headers" do
84
+ subject(:uncachable_document) {
85
+ DocRepo::Doc.new("/any/uri", headerless_http_ok_response)
86
+ }
87
+
88
+ let(:headerless_http_ok_response) {
89
+ # Use either `:[] => nil` or `"[]" => nil` as `[]: nil` is invalid Ruby
90
+ instance_double(
91
+ "Net::HTTPOK",
92
+ "code" => "200",
93
+ "body" => nil,
94
+ "[]" => nil,
95
+ )
96
+ }
97
+
98
+ EMPTY_STRING_SHA1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
99
+
100
+ it "may not have a last modified timestamp", :aggregate_failures do
101
+ expect(uncachable_document.last_modified).to be nil
102
+
103
+ allow(headerless_http_ok_response).to receive(:[]) { |key|
104
+ "Last-Modified" == key ? "0" : nil
105
+ }
106
+ invalid_time = DocRepo::Doc.new("/any/uri", headerless_http_ok_response)
107
+ expect(invalid_time.last_modified).to be nil
108
+ end
109
+
110
+ it "may not have any content type" do
111
+ expect(uncachable_document.content_type).to be nil
112
+ end
113
+
114
+ it "always has a cache version" do
115
+ expect(uncachable_document.cache_version).to eq EMPTY_STRING_SHA1
116
+ end
117
+
118
+ it "has a convenience reader for a versioned cache key" do
119
+ expect(uncachable_document.cache_key_with_version).to eq "/any/uri-#{EMPTY_STRING_SHA1}"
120
+ end
121
+
122
+ it "may not have any cache control settings" do
123
+ expect(uncachable_document.cache_control).to be nil
124
+ end
125
+ end
126
+
127
+ describe "converting markdown content to HTML" do
128
+ it "handles empty content" do
129
+ empty_content = OpenStruct.new(body: nil)
130
+ empty_doc = DocRepo::Doc.new("/any/uri", empty_content)
131
+ expect(empty_doc.to_html).to eq ""
132
+ end
133
+
134
+ it "does not parse emphasis inside of words" do
135
+ em_content = OpenStruct.new(body: <<~MARKDOWN)
136
+ _this is emphasis_
137
+
138
+ this_is_not
139
+ MARKDOWN
140
+ em_doc = DocRepo::Doc.new("/any/uri", em_content)
141
+ expect(em_doc.to_html).to eq <<~HTML
142
+ <p><em>this is emphasis</em></p>
143
+
144
+ <p>this_is_not</p>
145
+ HTML
146
+ end
147
+
148
+ it "parse tables" do
149
+ tables = OpenStruct.new(body: <<~MARKDOWN)
150
+ | Heading 1 | Heading 2 |
151
+ |-----------|-----------|
152
+ | Content A | Content B |
153
+ | Content C | Content D |
154
+
155
+ Heading 3 | Heading 4
156
+ -----------|-----------
157
+ Content E | Content F
158
+ Content G | Content H
159
+
160
+ | Left align | Right align | Center align |
161
+ |:-----------|------------:|:------------:|
162
+ | left | right | center |
163
+ | aligned | aligned | aligned |
164
+ MARKDOWN
165
+ table_doc = DocRepo::Doc.new("/any/uri", tables)
166
+ expect(table_doc.to_html).to eq <<~HTML
167
+ <table><thead>
168
+ <tr>
169
+ <th>Heading 1</th>
170
+ <th>Heading 2</th>
171
+ </tr>
172
+ </thead><tbody>
173
+ <tr>
174
+ <td>Content A</td>
175
+ <td>Content B</td>
176
+ </tr>
177
+ <tr>
178
+ <td>Content C</td>
179
+ <td>Content D</td>
180
+ </tr>
181
+ </tbody></table>
182
+
183
+ <table><thead>
184
+ <tr>
185
+ <th>Heading 3</th>
186
+ <th>Heading 4</th>
187
+ </tr>
188
+ </thead><tbody>
189
+ <tr>
190
+ <td>Content E</td>
191
+ <td>Content F</td>
192
+ </tr>
193
+ <tr>
194
+ <td>Content G</td>
195
+ <td>Content H</td>
196
+ </tr>
197
+ </tbody></table>
198
+
199
+ <table><thead>
200
+ <tr>
201
+ <th style="text-align: left">Left align</th>
202
+ <th style="text-align: right">Right align</th>
203
+ <th style="text-align: center">Center align</th>
204
+ </tr>
205
+ </thead><tbody>
206
+ <tr>
207
+ <td style="text-align: left">left</td>
208
+ <td style="text-align: right">right</td>
209
+ <td style="text-align: center">center</td>
210
+ </tr>
211
+ <tr>
212
+ <td style="text-align: left">aligned</td>
213
+ <td style="text-align: right">aligned</td>
214
+ <td style="text-align: center">aligned</td>
215
+ </tr>
216
+ </tbody></table>
217
+ HTML
218
+ end
219
+
220
+ it "supports fenced code blocks" do
221
+ fenced_code = OpenStruct.new(body: <<~MARKDOWN)
222
+ Plain Code Block:
223
+
224
+ ```
225
+ plain = code
226
+ block { do_something }
227
+ ```
228
+
229
+ Language Formatted Block:
230
+
231
+ ```ruby
232
+ str = "tagged code"
233
+ # with coments
234
+ block { do_something }
235
+ ```
236
+
237
+ Alternative Block Delimiter:
238
+
239
+ ~~~js
240
+ alternative = { style: "javascript" }
241
+ ~~~
242
+
243
+ End
244
+ MARKDOWN
245
+ code_doc = DocRepo::Doc.new("/any/uri", fenced_code)
246
+ expect(code_doc.to_html).to eq <<~HTML
247
+ <p>Plain Code Block:</p>
248
+ <div class="highlight"><pre><code class="language-" data-lang="">plain = code
249
+ block { do_something }
250
+ </code></pre></div>
251
+ <p>Language Formatted Block:</p>
252
+ <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">str</span> <span class="o">=</span> <span class="s2">"tagged code"</span>
253
+ <span class="c1"># with coments</span>
254
+ <span class="n">block</span> <span class="p">{</span> <span class="n">do_something</span> <span class="p">}</span>
255
+ </code></pre></div>
256
+ <p>Alternative Block Delimiter:</p>
257
+ <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="nx">alternative</span> <span class="o">=</span> <span class="p">{</span> <span class="na">style</span><span class="p">:</span> <span class="s2">"javascript"</span> <span class="p">}</span>
258
+ </code></pre></div>
259
+ <p>End</p>
260
+ HTML
261
+ end
262
+
263
+ it "autolinks URLs" do
264
+ embedded_urls = OpenStruct.new(body: <<~MARKDOWN)
265
+ Many people use www.google.com to search. But they really should use
266
+ https://www.duckduckgo.com if they want some security and privacy.
267
+
268
+ Is it still common to use http://example.com? What about just example.com
269
+
270
+ And no one uses ftp://example.com any more.
271
+ MARKDOWN
272
+ linked_doc = DocRepo::Doc.new("/any/uri", embedded_urls)
273
+ expect(linked_doc.to_html).to eq <<~HTML
274
+ <p>Many people use <a href="http://www.google.com">www.google.com</a> to search. But they really should use
275
+ <a href="https://www.duckduckgo.com">https://www.duckduckgo.com</a> if they want some security and privacy.</p>
276
+
277
+ <p>Is it still common to use <a href="http://example.com">http://example.com</a>? What about just example.com</p>
278
+
279
+ <p>And no one uses <a href="ftp://example.com">ftp://example.com</a> any more.</p>
280
+ HTML
281
+ end
282
+
283
+ it "autolinks email addresses" do
284
+ embedded_email = OpenStruct.new(body: <<~MARKDOWN)
285
+ If you need some help just contant support@example.com.
286
+ MARKDOWN
287
+ mail_doc = DocRepo::Doc.new("/any/uri", embedded_email)
288
+ expect(mail_doc.to_html).to eq <<~HTML
289
+ <p>If you need some help just contant <a href="mailto:support@example.com">support@example.com</a>.</p>
290
+ HTML
291
+ end
292
+
293
+ it "supports styling strikethrough content" do
294
+ styled_content = OpenStruct.new(body: <<~MARKDOWN)
295
+ this is ~~good~~ bad
296
+ MARKDOWN
297
+ styled_doc = DocRepo::Doc.new("/any/uri", styled_content)
298
+ expect(styled_doc.to_html).to eq <<~HTML
299
+ <p>this is <del>good</del> bad</p>
300
+ HTML
301
+ end
302
+
303
+ it "supports styling superscript content" do
304
+ styled_content = OpenStruct.new(body: <<~MARKDOWN)
305
+ this is the 2^(nd) time
306
+ MARKDOWN
307
+ styled_doc = DocRepo::Doc.new("/any/uri", styled_content)
308
+ expect(styled_doc.to_html).to eq <<~HTML
309
+ <p>this is the 2<sup>nd</sup> time</p>
310
+ HTML
311
+ end
312
+
313
+ it "adds HTML anchors to headers" do
314
+ styled_content = OpenStruct.new(body: <<~MARKDOWN)
315
+ # Heading 1
316
+
317
+ Content A
318
+
319
+ ## Sub-Heading 2
320
+
321
+ Content B
322
+ MARKDOWN
323
+ styled_doc = DocRepo::Doc.new("/any/uri", styled_content)
324
+ expect(styled_doc.to_html).to eq <<~HTML
325
+ <h1 id="heading-1">Heading 1</h1>
326
+
327
+ <p>Content A</p>
328
+
329
+ <h2 id="sub-heading-2">Sub-Heading 2</h2>
330
+
331
+ <p>Content B</p>
332
+ HTML
333
+ end
334
+
335
+ it "leaves markdown compliant HTML unchanged but may add whitespace" do
336
+ html_content = OpenStruct.new(body: <<~HTML)
337
+ <h1 id="heading-1">Heading 1</h1>
338
+ <p>Content A</p>
339
+ <p>this is <del>good</del> bad</p>
340
+ <p>If you need some help just contant <a href="mailto:support@example.com">support@example.com</a>.</p>
341
+ <p>Plain Code Block:</p>
342
+ <div class="highlight"><pre><code class="language-" data-lang="">plain = code
343
+ block { do_something }
344
+ </code></pre></div>
345
+ <p>Language Formatted Block:</p>
346
+ <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">str</span> <span class="o">=</span> <span class="s2">"tagged code"</span>
347
+ <span class="c1"># with coments</span>
348
+ <span class="n">block</span> <span class="p">{</span> <span class="n">do_something</span> <span class="p">}</span>
349
+ </code></pre></div>
350
+ <p>Alternative Block Delimiter:</p>
351
+ <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="nx">alternative</span> <span class="o">=</span> <span class="p">{</span> <span class="na">style</span><span class="p">:</span> <span class="s2">"javascript"</span> <span class="p">}</span>
352
+ </code></pre></div>
353
+ <h2 id="sub-heading-2">Sub-Heading 2</h2>
354
+ <p>this is the 2<sup>nd</sup> time</p>
355
+ <h3>Sub-Sub-Heading 3</h3>
356
+ <p>Many people use <a href="http://www.google.com">www.google.com</a> to search. But they really should use
357
+ <a href="https://www.duckduckgo.com">https://www.duckduckgo.com</a> if they want some security and privacy.</p>
358
+ <p>And no one uses <a href="ftp://example.com">ftp://example.com</a> any more.</p>
359
+ <table><thead>
360
+ <tr>
361
+ <th style="text-align: left">Left align</th>
362
+ <th style="text-align: right">Right align</th>
363
+ <th style="text-align: center">Center align</th>
364
+ </tr>
365
+ </thead><tbody>
366
+ <tr>
367
+ <td style="text-align: left">left</td>
368
+ <td style="text-align: right">right</td>
369
+ <td style="text-align: center">center</td>
370
+ </tr>
371
+ <tr>
372
+ <td style="text-align: left">aligned</td>
373
+ <td style="text-align: right">aligned</td>
374
+ <td style="text-align: center">aligned</td>
375
+ </tr>
376
+ </tbody></table>
377
+ <p>Final Content</p>
378
+ HTML
379
+ html_doc = DocRepo::Doc.new("/any/uri", html_content)
380
+ expect(html_doc.to_html).to eq <<~HTML
381
+ <h1 id="heading-1">Heading 1</h1>
382
+
383
+ <p>Content A</p>
384
+
385
+ <p>this is <del>good</del> bad</p>
386
+
387
+ <p>If you need some help just contant <a href="mailto:support@example.com">support@example.com</a>.</p>
388
+
389
+ <p>Plain Code Block:</p>
390
+
391
+ <div class="highlight"><pre><code class="language-" data-lang="">plain = code
392
+ block { do_something }
393
+ </code></pre></div>
394
+
395
+ <p>Language Formatted Block:</p>
396
+
397
+ <div class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">str</span> <span class="o">=</span> <span class="s2">"tagged code"</span>
398
+ <span class="c1"># with coments</span>
399
+ <span class="n">block</span> <span class="p">{</span> <span class="n">do_something</span> <span class="p">}</span>
400
+ </code></pre></div>
401
+
402
+ <p>Alternative Block Delimiter:</p>
403
+
404
+ <div class="highlight"><pre><code class="language-js" data-lang="js"><span class="nx">alternative</span> <span class="o">=</span> <span class="p">{</span> <span class="na">style</span><span class="p">:</span> <span class="s2">"javascript"</span> <span class="p">}</span>
405
+ </code></pre></div>
406
+
407
+ <h2 id="sub-heading-2">Sub-Heading 2</h2>
408
+
409
+ <p>this is the 2<sup>nd</sup> time</p>
410
+
411
+ <h3>Sub-Sub-Heading 3</h3>
412
+
413
+ <p>Many people use <a href="http://www.google.com">www.google.com</a> to search. But they really should use
414
+ <a href="https://www.duckduckgo.com">https://www.duckduckgo.com</a> if they want some security and privacy.</p>
415
+
416
+ <p>And no one uses <a href="ftp://example.com">ftp://example.com</a> any more.</p>
417
+
418
+ <table><thead>
419
+ <tr>
420
+ <th style="text-align: left">Left align</th>
421
+ <th style="text-align: right">Right align</th>
422
+ <th style="text-align: center">Center align</th>
423
+ </tr>
424
+ </thead><tbody>
425
+ <tr>
426
+ <td style="text-align: left">left</td>
427
+ <td style="text-align: right">right</td>
428
+ <td style="text-align: center">center</td>
429
+ </tr>
430
+ <tr>
431
+ <td style="text-align: left">aligned</td>
432
+ <td style="text-align: right">aligned</td>
433
+ <td style="text-align: center">aligned</td>
434
+ </tr>
435
+ </tbody></table>
436
+
437
+ <p>Final Content</p>
438
+ HTML
439
+ end
440
+ end
441
+
442
+ end