documentary 0.1.0
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 +7 -0
- data/.gitignore +8 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +35 -0
- data/README.md +152 -0
- data/Rakefile +12 -0
- data/bin/documentary +5 -0
- data/documentary.gemspec +20 -0
- data/lib/default_layout.erb +5 -0
- data/lib/documentary/cli.rb +39 -0
- data/lib/documentary/docblock.rb +9 -0
- data/lib/documentary/docblock_collection.rb +33 -0
- data/lib/documentary/parser.rb +61 -0
- data/lib/documentary/version.rb +3 -0
- data/lib/documentary/view/helpers.rb +106 -0
- data/lib/documentary.rb +49 -0
- data/test/docblock_collection_test.rb +24 -0
- data/test/docblock_test.rb +21 -0
- data/test/fixtures/invalid_block.txt +5 -0
- data/test/fixtures/kitchen_sink.txt +60 -0
- data/test/fixtures/title_block.txt +30 -0
- data/test/integration_helper.rb +37 -0
- data/test/integration_test.rb +80 -0
- data/test/parser_test.rb +34 -0
- data/test/test_helper.rb +38 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3ead34348f2423c01e41ed552a763454622c704a
|
4
|
+
data.tar.gz: 30fe4374be10722e56c6d9f24f1ed532fb51752d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 405f93a4c0352eefbc7c9f98cfaecd597a813c5413bff0803abf7f6d7b04e4d5a5a7e17ad0a57d6d564bf2c845d72a8301cc3d6b2cefee60f566e7cf4bc1c186
|
7
|
+
data.tar.gz: 649f53fa435e6aae7e299057340545d2488e89302ac0b233a70709414c06f347d2982d71e34cdb3620c4e53cde41c0b6ea3ea26dfacb84a9acbf0c6ea1ced143
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
documentary (0.1.0)
|
5
|
+
activesupport
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activesupport (4.2.0)
|
11
|
+
i18n (~> 0.7)
|
12
|
+
json (~> 1.7, >= 1.7.7)
|
13
|
+
minitest (~> 5.1)
|
14
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
15
|
+
tzinfo (~> 1.1)
|
16
|
+
byebug (9.0.5)
|
17
|
+
i18n (0.7.0)
|
18
|
+
json (1.8.2)
|
19
|
+
minitest (5.4.3)
|
20
|
+
rake (10.3.2)
|
21
|
+
thread_safe (0.3.4)
|
22
|
+
tzinfo (1.2.2)
|
23
|
+
thread_safe (~> 0.1)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
byebug
|
30
|
+
documentary!
|
31
|
+
minitest (~> 5.4.3)
|
32
|
+
rake (~> 10.3.2)
|
33
|
+
|
34
|
+
BUNDLED WITH
|
35
|
+
1.12.5
|
data/README.md
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# Documentary
|
2
|
+
|
3
|
+
A simple tool that will allow you to generate API runnable documentation quickly.
|
4
|
+
|
5
|
+
> If you want them to RTFM, make a better FM.
|
6
|
+
>
|
7
|
+
> -- <cite>Kathy Sierra</cite>
|
8
|
+
|
9
|
+
## Todos
|
10
|
+
|
11
|
+
* [X] Build the parser
|
12
|
+
* [X] Integration Test
|
13
|
+
* [X] Title Blocks
|
14
|
+
* [X] Ordering
|
15
|
+
* [X] Resource Blocks
|
16
|
+
* [X] Endpoint Blocks
|
17
|
+
* [X] Introduce Generation config
|
18
|
+
* [X] Introduce view helpers
|
19
|
+
* [ ] CLI design
|
20
|
+
* [ ] TOC for documentation
|
21
|
+
* [ ] Start being more strict on specification for MD generation?
|
22
|
+
* [ ] Create testing suite
|
23
|
+
|
24
|
+
## Specification
|
25
|
+
|
26
|
+
Documentary works using good old fashioned docblocks. I believe documentation for APIs should live with the code they document. Creating a documentary docblock is as simple as
|
27
|
+
|
28
|
+
```
|
29
|
+
# --- documentary
|
30
|
+
# Docblock content in here
|
31
|
+
# --- end
|
32
|
+
```
|
33
|
+
|
34
|
+
The contents of a documentary docblock is YAML which will generate markdown documentation.
|
35
|
+
|
36
|
+
All docblocks take an order attribute. Without it documentary will build teh documentation as it finds it in the file hierarchy.
|
37
|
+
|
38
|
+
### Title Blocks
|
39
|
+
|
40
|
+
Title blocks are to provide important supplamentary information about the API. They are formed as follows:
|
41
|
+
|
42
|
+
```
|
43
|
+
# --- documentary
|
44
|
+
# type: title_block
|
45
|
+
# title: The Heading for the Title Block
|
46
|
+
# content: >
|
47
|
+
# Here is the example content, using the YAML escaping character '>'.
|
48
|
+
# --- end
|
49
|
+
```
|
50
|
+
|
51
|
+
### Resources
|
52
|
+
|
53
|
+
Resource blocks let you describe the domain of your API. They are formed as follows:
|
54
|
+
|
55
|
+
```
|
56
|
+
# --- documentary
|
57
|
+
# type: resource
|
58
|
+
# title: The Resource Name
|
59
|
+
# description: >
|
60
|
+
# A description of the resource would go in here
|
61
|
+
# attributes:
|
62
|
+
# - string_attribute:
|
63
|
+
# required: true
|
64
|
+
# type: string
|
65
|
+
# --- end
|
66
|
+
```
|
67
|
+
|
68
|
+
Attributes are formed using the attribute name as the key and specifing if they are required (validation present) and the type to be passed.
|
69
|
+
|
70
|
+
### Endpoints
|
71
|
+
|
72
|
+
Endpoint blocks specify the public endpoints of your API. They are formed as follows:
|
73
|
+
|
74
|
+
```
|
75
|
+
# --- documentary
|
76
|
+
# type: endpoint
|
77
|
+
# title: List users
|
78
|
+
# notes: >
|
79
|
+
# This enpoint will list all the users.
|
80
|
+
# verb: GET
|
81
|
+
# endpoint: '/users'
|
82
|
+
# example_response:
|
83
|
+
# page: 1
|
84
|
+
# total_pages: 1
|
85
|
+
# count: 1
|
86
|
+
# users:
|
87
|
+
# - id: 1
|
88
|
+
# name: Testy McTesterson
|
89
|
+
# email: test@email.com
|
90
|
+
# --- end
|
91
|
+
```
|
92
|
+
|
93
|
+
Of course not all enpoints are this simple, for `GET` requests especially you may wish to pass in extra parameters for things like pagination of filtering. This is accomplished using the `params` key. Parameters take on the structure of the param name, whether it is required and any notes (example below). Better still you can provide an example request using the `example_request` key.
|
94
|
+
|
95
|
+
```
|
96
|
+
# --- documentary
|
97
|
+
# type: endpoint
|
98
|
+
# title: List users
|
99
|
+
# verb: GET
|
100
|
+
# endpoint: '/users'
|
101
|
+
# params:
|
102
|
+
# - page:
|
103
|
+
# required: false
|
104
|
+
# notes: >
|
105
|
+
# The page desired from the set
|
106
|
+
# - count:
|
107
|
+
# required: false
|
108
|
+
# notes: >
|
109
|
+
# The number of results to return
|
110
|
+
# - filter:
|
111
|
+
# required: false
|
112
|
+
# notes: >
|
113
|
+
# The filter for a specific user to find for example: `filter=Testy`
|
114
|
+
# example_request: >
|
115
|
+
# /users?filter=Testy&page=1&count=3
|
116
|
+
# example_response:
|
117
|
+
# page: 1
|
118
|
+
# total_pages: 1
|
119
|
+
# count: 1
|
120
|
+
# users:
|
121
|
+
# - id: 1
|
122
|
+
# name: Testy McTesterson
|
123
|
+
# email: test@email.com
|
124
|
+
# --- end
|
125
|
+
```
|
126
|
+
|
127
|
+
There is a common practice on create and update (`POST` and `PUT`/`PATCH`) to return an empty response body and provide the `location` (the cannonical URL) in the response header. If no `example_response` is provided documentary will simply not generate documentation for it. We do suggest for clarity you mention this in the endpoint notes.
|
128
|
+
|
129
|
+
```
|
130
|
+
# --- documentary
|
131
|
+
# type: endpoint
|
132
|
+
# title: Create a user
|
133
|
+
# notes: >
|
134
|
+
# The body of this response will be empty.
|
135
|
+
# information:
|
136
|
+
# authenticated: true
|
137
|
+
# response_formats: JSON, XML
|
138
|
+
# verb: POST
|
139
|
+
# endpoint: '/users'
|
140
|
+
# params:
|
141
|
+
# - name:
|
142
|
+
# required: true
|
143
|
+
# notes: >
|
144
|
+
# The name of the user
|
145
|
+
# - email:
|
146
|
+
# required: true
|
147
|
+
# notes: >
|
148
|
+
# The email of the user
|
149
|
+
# example_request: >
|
150
|
+
# /users?name=Testy%20McTesterson&email=test%40email.com
|
151
|
+
# --- end
|
152
|
+
```
|
data/Rakefile
ADDED
data/bin/documentary
ADDED
data/documentary.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
basedir = File.expand_path(File.dirname(__FILE__))
|
2
|
+
require "#{basedir}/lib/documentary/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'documentary'
|
6
|
+
s.version = Documentary::VERSION
|
7
|
+
s.summary = 'Documentary'
|
8
|
+
s.description = 'Simple, useable, runnable API documentation'
|
9
|
+
s.authors = ['Dave Kennedy']
|
10
|
+
s.email = 'david@bangline.co.uk'
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
s.require_path = 'lib'
|
13
|
+
s.executables = ['documentary']
|
14
|
+
s.homepage = 'http://rubygems.org/gems/documentary'
|
15
|
+
s.license = 'MIT'
|
16
|
+
|
17
|
+
s.add_dependency 'activesupport'
|
18
|
+
s.add_development_dependency 'minitest', '~> 5.4.3'
|
19
|
+
s.add_development_dependency 'rake', '~> 10.3.2'
|
20
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'documentary'
|
3
|
+
|
4
|
+
module Documentary
|
5
|
+
class Cli
|
6
|
+
def self.run(args, out=STDOUT)
|
7
|
+
file_glob = "./**/*.rb"
|
8
|
+
project_name = File.basename(Dir.getwd).split.each {|w| w.capitalize! }.join(' ')
|
9
|
+
output = 'api.md'
|
10
|
+
|
11
|
+
OptionParser.new do |opts|
|
12
|
+
opts.on("-v", "--version", "Print version number") do
|
13
|
+
require "documentary/version"
|
14
|
+
out << "Documentary #{Documentary::VERSION}\n"
|
15
|
+
exit
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on("-d", "--directory glob", "Set directory to search for docblocks") do |glob|
|
19
|
+
file_glob = glob
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on("-p", "--project project", "Set project name") do |project|
|
23
|
+
project_name = project
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on("-o", "--output output", "Set the output file") do |op|
|
27
|
+
output = op
|
28
|
+
end
|
29
|
+
end.parse!
|
30
|
+
|
31
|
+
files = Dir.glob(file_glob)
|
32
|
+
config = {
|
33
|
+
project: project_name,
|
34
|
+
op: output
|
35
|
+
}
|
36
|
+
Documentary::Generator.new(files, config).generate
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Documentary
|
2
|
+
class DocblockCollection
|
3
|
+
require 'forwardable'
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
def_delegators :@collection, :<<, :concat
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@collection = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def title_blocks
|
13
|
+
fetch_subset :title_block
|
14
|
+
end
|
15
|
+
|
16
|
+
def resources
|
17
|
+
fetch_subset :resource
|
18
|
+
end
|
19
|
+
|
20
|
+
def endpoints
|
21
|
+
fetch_subset :endpoint
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :collection
|
27
|
+
|
28
|
+
def fetch_subset(type)
|
29
|
+
collection.select { |docblock| docblock.type == type }.sort_by { |block| block.order }
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Documentary
|
2
|
+
class Parser
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
def initialize(path)
|
6
|
+
@path = path
|
7
|
+
@docblocks = []
|
8
|
+
@line_number = 0
|
9
|
+
begin
|
10
|
+
parse_file
|
11
|
+
rescue Psych::SyntaxError
|
12
|
+
raise Documentary::InvalidDocblock.new form_exception_message
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :docblocks
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :path
|
21
|
+
|
22
|
+
def parse_file
|
23
|
+
File.open(path, 'r') do |file_content|
|
24
|
+
in_docblock = false
|
25
|
+
block_body = ""
|
26
|
+
comment_delimeter = ""
|
27
|
+
file_content.readlines.each do |line|
|
28
|
+
line.rstrip!
|
29
|
+
@line_number += 1
|
30
|
+
if in_docblock && line.match(/---( |)end/)
|
31
|
+
in_docblock = false
|
32
|
+
yml = YAML.load(block_body)
|
33
|
+
self.docblocks << Docblock.new(yml)
|
34
|
+
block_body = ""
|
35
|
+
end
|
36
|
+
|
37
|
+
if line.match(/---( |)documentary/)
|
38
|
+
comment_delimeter = line.match(/(.+)---/)[1]
|
39
|
+
@current_docblock_start = @line_number
|
40
|
+
in_docblock = true
|
41
|
+
end
|
42
|
+
|
43
|
+
if in_docblock
|
44
|
+
if line.match(/---( |)documentary/)
|
45
|
+
block_body << "---\n"
|
46
|
+
elsif line == comment_delimeter.rstrip
|
47
|
+
block_body << "\n"
|
48
|
+
else
|
49
|
+
regex = /^#{comment_delimeter}/
|
50
|
+
block_body << "#{line.gsub(regex, '')}\n"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def form_exception_message
|
58
|
+
"Invalid docblock YAML in file #{path}:#{@current_docblock_start}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Documentary
|
2
|
+
module View
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
def title_blocks
|
6
|
+
io = StringIO.new
|
7
|
+
docblocks.title_blocks.each do |title_block|
|
8
|
+
io.puts "## #{title_block.title}"
|
9
|
+
io.puts new_line
|
10
|
+
io.puts title_block.content
|
11
|
+
io.puts new_line
|
12
|
+
end
|
13
|
+
io.string
|
14
|
+
end
|
15
|
+
|
16
|
+
def resource_blocks
|
17
|
+
io = StringIO.new
|
18
|
+
io.puts '## Resources'
|
19
|
+
io.puts new_line
|
20
|
+
docblocks.resources.each do |resource|
|
21
|
+
io.puts "### #{resource.title}"
|
22
|
+
io.puts new_line
|
23
|
+
io.puts resource.description
|
24
|
+
io.puts new_line
|
25
|
+
resource_attributes(resource, io)
|
26
|
+
end
|
27
|
+
io.puts new_line
|
28
|
+
io.string
|
29
|
+
end
|
30
|
+
|
31
|
+
def endpoint_blocks
|
32
|
+
io = StringIO.new
|
33
|
+
io.puts '## Endpoints'
|
34
|
+
io.puts new_line
|
35
|
+
docblocks.endpoints.each do |endpoint|
|
36
|
+
io.puts "### #{endpoint.title}"
|
37
|
+
io.puts new_line
|
38
|
+
io.puts endpoint.notes if endpoint.notes
|
39
|
+
io.puts new_line
|
40
|
+
io.puts '#### Enpoint URL'
|
41
|
+
io.puts new_line
|
42
|
+
io.puts start_code_block
|
43
|
+
io.puts "#{endpoint.verb} #{endpoint.endpoint}"
|
44
|
+
io.puts end_code_block
|
45
|
+
if endpoint.information
|
46
|
+
io.puts '#### Endpoint Information'
|
47
|
+
io.puts new_line
|
48
|
+
endpoint.information.each do |key, value|
|
49
|
+
io.puts "* **#{key.titleize}**: #{value}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
if endpoint.params
|
53
|
+
io.puts new_line
|
54
|
+
io.puts '#### Parameters'
|
55
|
+
io.puts new_line
|
56
|
+
io.puts 'Name | Required | Description'
|
57
|
+
io.puts '-------- | -------- | -----------'
|
58
|
+
endpoint.params.each do |param|
|
59
|
+
io.puts "#{param.keys.first} | #{param['required']} | #{param['notes'].strip}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
if endpoint.example_request
|
63
|
+
io.puts new_line
|
64
|
+
io.puts '#### Example Request'
|
65
|
+
io.puts new_line
|
66
|
+
io.puts start_code_block
|
67
|
+
io.puts "#{endpoint.verb} #{endpoint.example_request.strip}"
|
68
|
+
io.puts end_code_block
|
69
|
+
end
|
70
|
+
if endpoint.example_response
|
71
|
+
io.puts new_line
|
72
|
+
io.puts '#### Example Response'
|
73
|
+
io.puts new_line
|
74
|
+
io.puts start_code_block
|
75
|
+
io.puts JSON.pretty_generate(endpoint.example_response)
|
76
|
+
io.puts end_code_block
|
77
|
+
end
|
78
|
+
end
|
79
|
+
io.string
|
80
|
+
end
|
81
|
+
|
82
|
+
def code_block
|
83
|
+
'```'
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def resource_attributes(resource, io)
|
89
|
+
io.puts '#### Attributes'
|
90
|
+
io.puts new_line
|
91
|
+
io.puts 'Name | Type | Required'
|
92
|
+
io.puts '------------- | ------------- | -----------------'
|
93
|
+
resource.attributes.each do |attribute|
|
94
|
+
io.puts "#{attribute.keys.first} | #{attribute['type'] } | #{attribute['required'] }"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def new_line
|
99
|
+
"\n"
|
100
|
+
end
|
101
|
+
|
102
|
+
alias_method :start_code_block, :code_block
|
103
|
+
alias_method :end_code_block, :code_block
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
data/lib/documentary.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Documentary
|
2
|
+
require 'ostruct'
|
3
|
+
require 'documentary/version'
|
4
|
+
require 'documentary/docblock'
|
5
|
+
require 'documentary/docblock_collection'
|
6
|
+
require 'documentary/parser'
|
7
|
+
require 'documentary/view/helpers'
|
8
|
+
|
9
|
+
class InvalidDocblock < StandardError; end
|
10
|
+
|
11
|
+
class Generator
|
12
|
+
require 'erb'
|
13
|
+
require 'json'
|
14
|
+
require 'active_support/inflector'
|
15
|
+
|
16
|
+
include View
|
17
|
+
|
18
|
+
def initialize(file_tree, config={})
|
19
|
+
@docblocks = DocblockCollection.new
|
20
|
+
@file_tree = file_tree
|
21
|
+
@config = config
|
22
|
+
end
|
23
|
+
|
24
|
+
def generate
|
25
|
+
file_tree.each do |path|
|
26
|
+
parsed_file = Parser.new(path)
|
27
|
+
docblocks.concat parsed_file.docblocks
|
28
|
+
template = File.expand_path('../default_layout.erb', __FILE__)
|
29
|
+
erb = ERB.new(File.new(template).read, nil, '<>')
|
30
|
+
File.open(output_file, 'w+') do |file|
|
31
|
+
file.write erb.result(binding)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :file_tree, :config
|
39
|
+
attr_accessor :docblocks
|
40
|
+
|
41
|
+
def project_name
|
42
|
+
config[:project]
|
43
|
+
end
|
44
|
+
|
45
|
+
def output_file
|
46
|
+
config[:op]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Documentary::DocblockCollectionTest < MiniTest::Test
|
4
|
+
|
5
|
+
test 'dockblocks are split by type' do
|
6
|
+
title_block = Documentary::Docblock.new(type: 'title_block')
|
7
|
+
resource_block = Documentary::Docblock.new(type: 'resource')
|
8
|
+
endpoint_block = Documentary::Docblock.new(type: 'endpoint')
|
9
|
+
collection = Documentary::DocblockCollection.new
|
10
|
+
collection << title_block << resource_block << endpoint_block
|
11
|
+
assert_equal title_block, collection.title_blocks.first
|
12
|
+
assert_equal resource_block, collection.resources.first
|
13
|
+
assert_equal endpoint_block, collection.endpoints.first
|
14
|
+
end
|
15
|
+
|
16
|
+
test 'docblocks are sorted by order' do
|
17
|
+
title_block1 = Documentary::Docblock.new(type: 'title_block')
|
18
|
+
title_block2 = Documentary::Docblock.new(type: 'title_block', order: 99)
|
19
|
+
collection = Documentary::DocblockCollection.new
|
20
|
+
collection << title_block1 << title_block2
|
21
|
+
assert_equal collection.title_blocks, [title_block2, title_block1]
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Documentary::DocblockTest < MiniTest::Test
|
4
|
+
|
5
|
+
test 'type is stored as a symbol' do
|
6
|
+
docblock = Documentary::Docblock.new({type: 'something'})
|
7
|
+
assert_equal :something, docblock.type
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'the order is defaulted if not given' do
|
11
|
+
docblock = Documentary::Docblock.new({type: 'something'})
|
12
|
+
assert_equal 9999, docblock.order
|
13
|
+
end
|
14
|
+
|
15
|
+
test 'the order is set if given' do
|
16
|
+
docblock = Documentary::Docblock.new({type: 'something', order: 1})
|
17
|
+
assert_equal 1, docblock.order
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# --- documentary
|
2
|
+
# type: title_block
|
3
|
+
# title: Salutations
|
4
|
+
# content: |
|
5
|
+
# Hello world
|
6
|
+
# --- end
|
7
|
+
|
8
|
+
// --- documentary
|
9
|
+
// type: title_block
|
10
|
+
// title: Salutations Again
|
11
|
+
// content: |
|
12
|
+
// Hello world Again
|
13
|
+
// --- end
|
14
|
+
|
15
|
+
# --- documentary
|
16
|
+
# type: resource
|
17
|
+
# title: A Resource
|
18
|
+
# description: This is a resource
|
19
|
+
# attributes:
|
20
|
+
# - email:
|
21
|
+
# required: true
|
22
|
+
# type: string
|
23
|
+
# - another:
|
24
|
+
# required: no
|
25
|
+
# type: boolean
|
26
|
+
# --- end
|
27
|
+
|
28
|
+
# --- documentary
|
29
|
+
# type: endpoint
|
30
|
+
# title: List endpoint
|
31
|
+
# notes: Some notes about the endpoint
|
32
|
+
# information:
|
33
|
+
# authenticated: true
|
34
|
+
# response_formats: JSON
|
35
|
+
# verb: GET
|
36
|
+
# endpoint: '/some/path'
|
37
|
+
# params:
|
38
|
+
# - page:
|
39
|
+
# required: false
|
40
|
+
# notes: >
|
41
|
+
# The page desired from the set
|
42
|
+
# - count:
|
43
|
+
# required: false
|
44
|
+
# notes: >
|
45
|
+
# The number of results to return
|
46
|
+
# - filter:
|
47
|
+
# required: false
|
48
|
+
# notes: >
|
49
|
+
# The filter for a specific user to find for example: `filter=Testy`
|
50
|
+
# example_request: >
|
51
|
+
# /some/path?filter=Testy&page=1&count=3
|
52
|
+
# example_response:
|
53
|
+
# page: 1
|
54
|
+
# total_pages: 1
|
55
|
+
# count: 1
|
56
|
+
# users:
|
57
|
+
# - id: 1
|
58
|
+
# name: Testy McTesterson
|
59
|
+
# email: test@email.com
|
60
|
+
# --- end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# --- documentary
|
2
|
+
# type: title_block
|
3
|
+
# title: Hashes
|
4
|
+
# content: >
|
5
|
+
# This is the title block. With added [markdown](http://daringfireball.net/projects/markdown/syntax)
|
6
|
+
# --- end
|
7
|
+
|
8
|
+
Some other text that should be here
|
9
|
+
|
10
|
+
# this is not documentary
|
11
|
+
# nor this. end of!
|
12
|
+
|
13
|
+
We can have wierdly indented comments like the one below!
|
14
|
+
|
15
|
+
// --- documentary
|
16
|
+
// type: title_block
|
17
|
+
// title: Slashes
|
18
|
+
// content: >
|
19
|
+
// Dont care about comment chars
|
20
|
+
// --- end
|
21
|
+
|
22
|
+
# --- documentary
|
23
|
+
# type: title_block
|
24
|
+
# title: Multiple Lines
|
25
|
+
# content: |
|
26
|
+
# This is the title block.
|
27
|
+
#
|
28
|
+
# With multiple
|
29
|
+
# Significant lines
|
30
|
+
# --- end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Minitest::Test
|
2
|
+
def generated_docs_path
|
3
|
+
File.expand_path('../../api.md', __FILE__)
|
4
|
+
end
|
5
|
+
|
6
|
+
def generated_docs
|
7
|
+
@docs ||= File.read generated_docs_path
|
8
|
+
end
|
9
|
+
|
10
|
+
def assert_has_title_block_header(title)
|
11
|
+
assert_includes generated_docs, "## #{title}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def assert_content_for_title_block(title, content)
|
15
|
+
assert_match /## #{title}\n\n#{content}/, generated_docs
|
16
|
+
end
|
17
|
+
|
18
|
+
def assert_has_resource_title
|
19
|
+
assert_includes generated_docs, "## Resources"
|
20
|
+
end
|
21
|
+
|
22
|
+
def assert_has_enpoints_title
|
23
|
+
assert_includes generated_docs, "## Endpoints"
|
24
|
+
end
|
25
|
+
|
26
|
+
def assert_has_third_level_heading(heading)
|
27
|
+
assert_includes generated_docs, "### #{heading}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def assert_content_for_resource(resource, content)
|
31
|
+
assert_match /### #{resource}\n\n#{content}/, generated_docs
|
32
|
+
end
|
33
|
+
|
34
|
+
def assert_table_entry_for_resource(resource, row_content)
|
35
|
+
assert_match /### #{resource}[.+]#{row_content[:name]} | #{row_content[:required]} | #{row_content[:type]}/, generated_docs
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'integration_helper'
|
3
|
+
|
4
|
+
class Documentary::IntegrationTest < MiniTest::Test
|
5
|
+
|
6
|
+
def config
|
7
|
+
{
|
8
|
+
project: 'Test Project',
|
9
|
+
op: 'api.md'
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
setup do
|
14
|
+
Documentary::Generator.new([fetch_fixture('kitchen_sink.txt')], config).generate
|
15
|
+
end
|
16
|
+
|
17
|
+
teardown do
|
18
|
+
File.unlink generated_docs_path
|
19
|
+
end
|
20
|
+
|
21
|
+
test 'the document title is generated correctly' do
|
22
|
+
assert_includes generated_docs, "# #{config[:project]}"
|
23
|
+
end
|
24
|
+
|
25
|
+
test 'the title blocks are correctly generated' do
|
26
|
+
assert_has_title_block_header 'Salutations'
|
27
|
+
end
|
28
|
+
|
29
|
+
test 'the content of the title block is correctly generated' do
|
30
|
+
assert_content_for_title_block 'Salutations', "Hello world"
|
31
|
+
end
|
32
|
+
|
33
|
+
test 'resources are generated' do
|
34
|
+
assert_has_resource_title
|
35
|
+
end
|
36
|
+
|
37
|
+
test 'named resources are correctly generated' do
|
38
|
+
assert_has_third_level_heading 'A Resource'
|
39
|
+
end
|
40
|
+
|
41
|
+
test 'named resources descriptions are correctly generated' do
|
42
|
+
assert_content_for_resource 'A Resource', 'This is a resource'
|
43
|
+
end
|
44
|
+
|
45
|
+
test 'named resource has table of attributes' do
|
46
|
+
assert_table_entry_for_resource 'A Resource', {name: 'email', required: 'true', type: 'string'}
|
47
|
+
end
|
48
|
+
|
49
|
+
test 'endpoints are generated' do
|
50
|
+
assert_has_enpoints_title
|
51
|
+
end
|
52
|
+
|
53
|
+
test 'named enpoints are correctly generated' do
|
54
|
+
assert_includes generated_docs, '### List endpoint'
|
55
|
+
end
|
56
|
+
|
57
|
+
test 'enpoint url is generated' do
|
58
|
+
assert_includes generated_docs, "```\nGET /some/path\n```"
|
59
|
+
end
|
60
|
+
|
61
|
+
test 'endpoint information is correctly generated' do
|
62
|
+
assert_includes generated_docs, '* **Authenticated**: true'
|
63
|
+
assert_includes generated_docs, '* **Response Formats**: JSON'
|
64
|
+
end
|
65
|
+
|
66
|
+
test 'enpoint params are correctly generated' do
|
67
|
+
assert_includes generated_docs, 'page | false | The page desired from the set'
|
68
|
+
assert_includes generated_docs, 'count | false | The number of results to return'
|
69
|
+
assert_includes generated_docs, 'filter | false | The filter for a specific user to find for example: `filter=Testy`'
|
70
|
+
end
|
71
|
+
|
72
|
+
test 'enpoint example request is generated' do
|
73
|
+
assert_includes generated_docs, "```\nGET /some/path?filter=Testy&page=1&count=3\n```"
|
74
|
+
end
|
75
|
+
|
76
|
+
test 'enpoint example response is generated' do
|
77
|
+
assert_includes generated_docs, "#### Example Response\n\n```\n"
|
78
|
+
assert_includes generated_docs, "\"page\": 1,\n"
|
79
|
+
end
|
80
|
+
end
|
data/test/parser_test.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Documentary::ParserTest < MiniTest::Test
|
4
|
+
|
5
|
+
test 'that docblocks will be collected from a file' do
|
6
|
+
file_with_title_block = fetch_fixture('title_block.txt')
|
7
|
+
parser = Documentary::Parser.new(file_with_title_block)
|
8
|
+
assert_equal 3, parser.docblocks.count
|
9
|
+
assert_kind_of Documentary::Docblock, parser.docblocks.first
|
10
|
+
end
|
11
|
+
|
12
|
+
test 'invalid docblocks will raise invalid docblock error' do
|
13
|
+
file_with_title_block = fetch_fixture('invalid_block.txt')
|
14
|
+
assert_raises Documentary::InvalidDocblock do
|
15
|
+
Documentary::Parser.new(file_with_title_block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
test 'invalid docblock errors will have a helpful message' do
|
20
|
+
file_with_title_block = fetch_fixture('invalid_block.txt')
|
21
|
+
begin
|
22
|
+
Documentary::Parser.new(file_with_title_block)
|
23
|
+
rescue => e
|
24
|
+
assert_includes e.message, 'Invalid docblock YAML in file'
|
25
|
+
assert_includes e.message, '/invalid_block.txt:1'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
test 'other exceptions bubble up' do
|
30
|
+
assert_raises Errno::ENOENT do
|
31
|
+
Documentary::Parser.new('not_there.txt')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'documentary'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
|
4
|
+
# Plagiarized and bastardized from Tilt, they like tests like me. :raised_hands:
|
5
|
+
# https://github.com/rtomayko/tilt/blob/master/test/test_helper.rb
|
6
|
+
class Minitest::Test
|
7
|
+
def self.setup(&block)
|
8
|
+
define_method :setup do
|
9
|
+
super(&block)
|
10
|
+
instance_eval(&block)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.teardown(&block)
|
15
|
+
define_method :teardown do
|
16
|
+
instance_eval(&block)
|
17
|
+
super(&block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.test(name, &block)
|
22
|
+
define_method(test_name(name), &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def fetch_fixture(name)
|
26
|
+
File.expand_path("../fixtures/#{name}", __FILE__)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def self.test_name(name)
|
32
|
+
"test_#{sanitize_name(name).gsub(/\s+/,'_')}".to_sym
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.sanitize_name(name)
|
36
|
+
name.gsub(/\W+/, ' ').strip
|
37
|
+
end
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: documentary
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dave Kennedy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-06-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 5.4.3
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 5.4.3
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 10.3.2
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 10.3.2
|
55
|
+
description: Simple, useable, runnable API documentation
|
56
|
+
email: david@bangline.co.uk
|
57
|
+
executables:
|
58
|
+
- documentary
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- Gemfile
|
64
|
+
- Gemfile.lock
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- bin/documentary
|
68
|
+
- documentary.gemspec
|
69
|
+
- lib/default_layout.erb
|
70
|
+
- lib/documentary.rb
|
71
|
+
- lib/documentary/cli.rb
|
72
|
+
- lib/documentary/docblock.rb
|
73
|
+
- lib/documentary/docblock_collection.rb
|
74
|
+
- lib/documentary/parser.rb
|
75
|
+
- lib/documentary/version.rb
|
76
|
+
- lib/documentary/view/helpers.rb
|
77
|
+
- test/docblock_collection_test.rb
|
78
|
+
- test/docblock_test.rb
|
79
|
+
- test/fixtures/invalid_block.txt
|
80
|
+
- test/fixtures/kitchen_sink.txt
|
81
|
+
- test/fixtures/title_block.txt
|
82
|
+
- test/integration_helper.rb
|
83
|
+
- test/integration_test.rb
|
84
|
+
- test/parser_test.rb
|
85
|
+
- test/test_helper.rb
|
86
|
+
homepage: http://rubygems.org/gems/documentary
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.4.5.1
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Documentary
|
110
|
+
test_files: []
|