apiculture 0.1.2 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.travis.yml +5 -7
- data/apiculture.gemspec +3 -3
- data/gemfiles/Gemfile.rack-1.x +1 -1
- data/gemfiles/Gemfile.rack-2.x +1 -1
- data/lib/apiculture.rb +2 -1
- data/lib/apiculture/app.rb +1 -1
- data/lib/apiculture/app_documentation.rb +4 -0
- data/lib/apiculture/app_documentation_tpl.mustache +2 -2
- data/lib/apiculture/method_documentation.rb +10 -5
- data/lib/apiculture/openapi_documentation.rb +224 -0
- data/lib/apiculture/version.rb +1 -1
- data/spec/apiculture/app_documentation_spec.rb +34 -26
- data/spec/apiculture/method_documentation_spec.rb +5 -5
- data/spec/apiculture/openapi_documentation_spec.rb +197 -0
- metadata +16 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 21e9fada9f8ba88c058f19789d597059bb30b055fe92c10e27953c5df6296758
|
4
|
+
data.tar.gz: 7b3c02a5587b6976a2a1a7b11656834824e32887b9dde3922542687fcbe63c71
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0818c0fd6c04efa47f0cabcd0927c13516bd293d7486fea8750b7325d260208b3134432ebf43fbc9d7f0321a8ff7b8e5ea07e6f679e59b1df62e0f5f105f1b7
|
7
|
+
data.tar.gz: 853ca12aefe6005ae08432d29f902f6a1584252db87663c08c96a00f47d7b771108e2485f39e7400615e8f54243f68f6abf2e8406a963def5e3821c9ee62a6fc
|
data/.travis.yml
CHANGED
@@ -2,12 +2,10 @@ gemfile:
|
|
2
2
|
- gemfiles/Gemfile.rack-1.x
|
3
3
|
- gemfiles/Gemfile.rack-2.x
|
4
4
|
rvm:
|
5
|
-
- 2.3
|
6
|
-
- 2.4
|
7
|
-
- 2.5
|
8
|
-
- 2.6
|
5
|
+
- 2.3
|
6
|
+
- 2.4
|
7
|
+
- 2.5
|
8
|
+
- 2.6
|
9
|
+
- 2.7
|
9
10
|
sudo: false
|
10
11
|
cache: bundler
|
11
|
-
matrix:
|
12
|
-
allow_failures:
|
13
|
-
- rvm: 2.6.0-preview1
|
data/apiculture.gemspec
CHANGED
@@ -36,13 +36,13 @@ Gem::Specification.new do |s|
|
|
36
36
|
s.add_runtime_dependency 'mustermann', '~> 1'
|
37
37
|
s.add_runtime_dependency 'builder', '~> 3'
|
38
38
|
s.add_runtime_dependency 'rdiscount', '~> 2'
|
39
|
-
s.add_runtime_dependency 'github-markup', '~>
|
39
|
+
s.add_runtime_dependency 'github-markup', '~> 3'
|
40
40
|
s.add_runtime_dependency 'mustache', '~> 1'
|
41
41
|
|
42
42
|
s.add_development_dependency 'rack-test'
|
43
|
-
s.add_development_dependency "rspec", "~> 3
|
43
|
+
s.add_development_dependency "rspec", "~> 3"
|
44
44
|
s.add_development_dependency "rdoc", "~> 6.0"
|
45
|
-
s.add_development_dependency "rake"
|
45
|
+
s.add_development_dependency "rake"
|
46
46
|
s.add_development_dependency "bundler", "~> 1.0"
|
47
47
|
s.add_development_dependency "simplecov", ">= 0"
|
48
48
|
end
|
data/gemfiles/Gemfile.rack-1.x
CHANGED
data/gemfiles/Gemfile.rack-2.x
CHANGED
data/lib/apiculture.rb
CHANGED
@@ -9,6 +9,7 @@ module Apiculture
|
|
9
9
|
require_relative 'apiculture/markdown_segment'
|
10
10
|
require_relative 'apiculture/timestamp_promise'
|
11
11
|
require_relative 'apiculture/app_documentation'
|
12
|
+
require_relative 'apiculture/openapi_documentation'
|
12
13
|
|
13
14
|
def self.extended(in_class)
|
14
15
|
in_class.send(:include, SinatraInstanceMethods)
|
@@ -205,7 +206,7 @@ module Apiculture
|
|
205
206
|
def api_documentation
|
206
207
|
AppDocumentation.new(self, @apiculture_mounted_at.to_s, @apiculture_actions_and_docs || [])
|
207
208
|
end
|
208
|
-
|
209
|
+
|
209
210
|
# Define an API method. Under the hood will call the related methods in Sinatra
|
210
211
|
# to define the route.
|
211
212
|
def api_method(http_verb, path, options={}, &blk)
|
data/lib/apiculture/app.rb
CHANGED
@@ -102,7 +102,7 @@ class Apiculture::App
|
|
102
102
|
end
|
103
103
|
|
104
104
|
def perform_action_block(&blk)
|
105
|
-
#
|
105
|
+
# Executes the action in a Sinatra-like fashion - passing the route parameter values as
|
106
106
|
# arguments to the given block/callable. This is where in the future we should ditch
|
107
107
|
# the Sinatra calling conventions - Sinatra mandates that the action accept the route parameters
|
108
108
|
# as arguments and grab all the useful stuff from instance methods like `params` etc. whereas
|
@@ -25,6 +25,10 @@ class Apiculture::AppDocumentation
|
|
25
25
|
# Generates a Markdown string that contains the entire API documentation
|
26
26
|
def to_markdown
|
27
27
|
(['## %s' % @app_title] + to_markdown_slices).join("\n\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_openapi
|
31
|
+
OpenApiDocumentation::Base.new(@app_title, @mountpoint, @chunks)
|
28
32
|
end
|
29
33
|
|
30
34
|
# Generates an HTML fragment string that can be included into another HTML document
|
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'builder'
|
2
|
+
require 'rdiscount'
|
3
|
+
|
2
4
|
# Generates Markdown/HTML documentation about a single API action.
|
3
5
|
#
|
4
6
|
# Formats route parameters and request/QS parameters as a neat HTML
|
@@ -26,12 +28,15 @@ class Apiculture::MethodDocumentation
|
|
26
28
|
|
27
29
|
# Compose an HTML string by converting the result of +to_markdown+
|
28
30
|
def to_html_fragment
|
29
|
-
|
30
|
-
RDiscount.new(to_markdown).to_html
|
31
|
+
markdown_string_to_html(to_markdown)
|
31
32
|
end
|
32
33
|
|
33
34
|
private
|
34
35
|
|
36
|
+
def markdown_string_to_html(str)
|
37
|
+
RDiscount.new(str.to_s).to_html
|
38
|
+
end
|
39
|
+
|
35
40
|
class StringBuf #:nodoc:
|
36
41
|
def initialize; @blocks = []; end
|
37
42
|
def <<(block); @blocks << block.to_s; self; end
|
@@ -59,7 +64,7 @@ class Apiculture::MethodDocumentation
|
|
59
64
|
@definition.route_parameters.each do | param |
|
60
65
|
html.tr do
|
61
66
|
html.td { html.tt(':%s' % param.name) }
|
62
|
-
html.td(param.description)
|
67
|
+
html.td { html << markdown_string_to_html(param.description) }
|
63
68
|
end
|
64
69
|
end
|
65
70
|
end
|
@@ -98,7 +103,7 @@ class Apiculture::MethodDocumentation
|
|
98
103
|
@definition.responses.each do | resp |
|
99
104
|
html.tr do
|
100
105
|
html.td { html.b(resp.http_status_code) }
|
101
|
-
html.td resp.description
|
106
|
+
html.td { html << markdown_string_to_html(resp.description) }
|
102
107
|
html.td { html.pre { html.code(body_example(resp)) }}
|
103
108
|
end
|
104
109
|
end
|
@@ -139,7 +144,7 @@ class Apiculture::MethodDocumentation
|
|
139
144
|
html.td { html.tt(param.name.to_s) }
|
140
145
|
html.td(param.required ? 'Yes' : 'No')
|
141
146
|
html.td(param.matchable.inspect)
|
142
|
-
html.td(param.description
|
147
|
+
html.td { html << markdown_string_to_html(param.description) }
|
143
148
|
end
|
144
149
|
end
|
145
150
|
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'yaml'
|
3
|
+
require 'base64'
|
4
|
+
module OpenApiDocumentation
|
5
|
+
class Base
|
6
|
+
def initialize(app, prefix, chunks)
|
7
|
+
@app, @prefix = app, prefix
|
8
|
+
@paths = chunks.select { |chunk| chunk.respond_to?(:http_verb) }
|
9
|
+
@data = {
|
10
|
+
openapi: '3.0.0',
|
11
|
+
info: {
|
12
|
+
title: @app.to_s,
|
13
|
+
version: '0.0.1',
|
14
|
+
description: @app.to_s + " " + chunks.select { |chunk| chunk.respond_to?(:to_markdown) }.map(&:to_markdown).join("\n")
|
15
|
+
},
|
16
|
+
tags: []
|
17
|
+
}
|
18
|
+
@data[:paths] = build_paths
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_yaml
|
22
|
+
JSON.load(@data.to_json).to_yaml # trickery to get string based yaml
|
23
|
+
end
|
24
|
+
|
25
|
+
def paths
|
26
|
+
@data[:paths]
|
27
|
+
end
|
28
|
+
|
29
|
+
def spec
|
30
|
+
@data
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def build_paths
|
36
|
+
paths = {}
|
37
|
+
@paths.each do |path|
|
38
|
+
path = Path.new(path, @prefix, @app)
|
39
|
+
paths = merge_paths(paths, path)
|
40
|
+
end
|
41
|
+
paths
|
42
|
+
end
|
43
|
+
|
44
|
+
# We don't have deep_merge here so this is the poor man's alternative
|
45
|
+
def merge_paths(paths, path)
|
46
|
+
if paths.key?(path.name)
|
47
|
+
paths[path.name].merge!(path.build[path.name])
|
48
|
+
else
|
49
|
+
paths.merge!(path.build)
|
50
|
+
end
|
51
|
+
paths
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
class Path
|
57
|
+
VERBS_WITHOUT_BODY = %w(get head delete options)
|
58
|
+
|
59
|
+
def initialize(path, prefix, app)
|
60
|
+
@path, @prefix, @app = path, prefix, app
|
61
|
+
end
|
62
|
+
|
63
|
+
def build
|
64
|
+
request_body = build_request_body unless VERBS_WITHOUT_BODY.include?(@path.http_verb)
|
65
|
+
{
|
66
|
+
name =>
|
67
|
+
{
|
68
|
+
@path.http_verb.to_sym => {
|
69
|
+
summary: @path.description,
|
70
|
+
description: @path.description,
|
71
|
+
tags: [ @app.to_s ],
|
72
|
+
parameters: build_parameters,
|
73
|
+
requestBody: request_body,
|
74
|
+
responses: build_responses,
|
75
|
+
operationId: operation_id
|
76
|
+
}.delete_if { |_k, v| v.nil? || v.empty? }
|
77
|
+
}
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def name
|
82
|
+
full_path = @path.path.to_s
|
83
|
+
@path.route_parameters.each do |parameter|
|
84
|
+
# This is a bit confusing but naming is a little different between
|
85
|
+
# apiculture and openapi
|
86
|
+
full_path.gsub!(":#{parameter.name}", "\{#{parameter.name}\}")
|
87
|
+
end
|
88
|
+
Util.clean_path("#{@prefix}#{full_path}")
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def operation_id
|
94
|
+
# base64 encoding to make sure these ids are safe to use in an url
|
95
|
+
Base64.urlsafe_encode64("#{@path.http_verb}#{@prefix}#{@path.path}")
|
96
|
+
end
|
97
|
+
|
98
|
+
def build_parameters
|
99
|
+
if VERBS_WITHOUT_BODY.include?(@path.http_verb)
|
100
|
+
build_route_parameters + build_query_parameters
|
101
|
+
else
|
102
|
+
build_route_parameters
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def build_route_parameters
|
107
|
+
route_params = @path.route_parameters.map do |parameter|
|
108
|
+
{
|
109
|
+
name: parameter.name,
|
110
|
+
description: parameter.description,
|
111
|
+
required: true,
|
112
|
+
in: :path,
|
113
|
+
schema: {
|
114
|
+
type: Util.map_type(parameter.matchable),
|
115
|
+
example: Util.map_example(parameter.matchable)
|
116
|
+
}
|
117
|
+
}
|
118
|
+
end
|
119
|
+
route_params
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_query_parameters
|
123
|
+
params = @path.parameters.map do |parameter|
|
124
|
+
{
|
125
|
+
name: parameter.name,
|
126
|
+
description: parameter.description,
|
127
|
+
required: true,
|
128
|
+
in: :query,
|
129
|
+
schema: {
|
130
|
+
type: Util.map_type(parameter.matchable),
|
131
|
+
example: parameter.matchable
|
132
|
+
}
|
133
|
+
}
|
134
|
+
end
|
135
|
+
params
|
136
|
+
end
|
137
|
+
|
138
|
+
def build_request_body
|
139
|
+
return nil if VERBS_WITHOUT_BODY.include?(@path.http_verb)
|
140
|
+
|
141
|
+
body_params = Hash[ @path.parameters.collect do |parameter|
|
142
|
+
[parameter.name, {
|
143
|
+
type: Util.map_type(parameter.matchable),
|
144
|
+
description: parameter.description
|
145
|
+
}]
|
146
|
+
end ]
|
147
|
+
|
148
|
+
return nil if body_params.count == 0
|
149
|
+
|
150
|
+
schema = {
|
151
|
+
type: :object,
|
152
|
+
properties: body_params
|
153
|
+
}
|
154
|
+
|
155
|
+
schema[:required] = @path.parameters.select(&:required).map(&:name) if @path.parameters.select(&:required).map(&:name).count > 0
|
156
|
+
{
|
157
|
+
content: {
|
158
|
+
"application/json": {
|
159
|
+
schema: schema
|
160
|
+
}
|
161
|
+
}
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
def build_responses
|
166
|
+
responses = Hash[@path.responses.collect do |response|
|
167
|
+
_response = {
|
168
|
+
description: response.description
|
169
|
+
}
|
170
|
+
|
171
|
+
unless response.jsonable_object_example.nil? || response.jsonable_object_example.empty?
|
172
|
+
_response[:content] = {
|
173
|
+
'application/json': {
|
174
|
+
schema:
|
175
|
+
{ type: 'object',
|
176
|
+
properties: Util.response_to_schema(response.jsonable_object_example) }
|
177
|
+
}
|
178
|
+
}
|
179
|
+
end
|
180
|
+
|
181
|
+
[response.http_status_code.to_s, _response]
|
182
|
+
end ]
|
183
|
+
responses
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
class Util
|
188
|
+
TYPES = {
|
189
|
+
String => 'string',
|
190
|
+
Integer => 'integer',
|
191
|
+
TrueClass => 'boolean'
|
192
|
+
}.freeze
|
193
|
+
|
194
|
+
EXAMPLES = {
|
195
|
+
String => 'string',
|
196
|
+
Integer => 1234,
|
197
|
+
TrueClass => true
|
198
|
+
}.freeze
|
199
|
+
|
200
|
+
def self.response_to_schema(response)
|
201
|
+
schema = {}
|
202
|
+
return nil if response.nil? || response.empty? || response.class == String
|
203
|
+
response.each do |key, value|
|
204
|
+
schema[key] = {
|
205
|
+
type: 'string',
|
206
|
+
example: value.to_s
|
207
|
+
}
|
208
|
+
end
|
209
|
+
schema
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.map_type(type)
|
213
|
+
TYPES.fetch(type, 'string')
|
214
|
+
end
|
215
|
+
|
216
|
+
def self.map_example(type)
|
217
|
+
EXAMPLES.fetch(type, 'string')
|
218
|
+
end
|
219
|
+
|
220
|
+
def self.clean_path(path)
|
221
|
+
path.gsub(/\/\?\*\?$/, '')
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
data/lib/apiculture/version.rb
CHANGED
@@ -1,28 +1,35 @@
|
|
1
1
|
require_relative '../spec_helper'
|
2
2
|
|
3
3
|
describe "Apiculture.api_documentation" do
|
4
|
-
let(:app)
|
4
|
+
let(:app) do
|
5
5
|
Class.new(Apiculture::App) do
|
6
6
|
extend Apiculture
|
7
|
-
|
7
|
+
|
8
8
|
markdown_string 'This API is very important. Because it has to do with pancakes.'
|
9
|
-
|
9
|
+
|
10
10
|
documentation_build_time!
|
11
|
-
|
11
|
+
|
12
12
|
desc 'Order a pancake'
|
13
|
-
required_param :diameter, "Diameter of the pancake", Integer
|
13
|
+
required_param :diameter, "Diameter of the pancake. The pancake will be **bold**", Integer
|
14
14
|
param :topping, 'Type of topping', String
|
15
|
-
|
15
|
+
pancake_response_info = <<~EOS
|
16
|
+
When the pancake has been baked successfully
|
17
|
+
The pancake will have the following properties:
|
18
|
+
|
19
|
+
* It is going to be round
|
20
|
+
* It is going to be delicious
|
21
|
+
EOS
|
22
|
+
responds_with 200, pancake_response_info, { id: 'abdef..c21' }
|
16
23
|
api_method :post, '/pancakes' do
|
17
24
|
end
|
18
|
-
|
25
|
+
|
19
26
|
desc 'Check the pancake status'
|
20
27
|
route_param :id, 'Pancake ID to check status on'
|
21
|
-
responds_with 200, 'When the pancake is found', {status: 'Baking'}
|
22
|
-
responds_with 404, 'When no such pancake exists', {status: 'No such pancake'}
|
28
|
+
responds_with 200, 'When the pancake is found', { status: 'Baking' }
|
29
|
+
responds_with 404, 'When no such pancake exists', { status: 'No such pancake' }
|
23
30
|
api_method :get, '/pancake/:id' do
|
24
31
|
end
|
25
|
-
|
32
|
+
|
26
33
|
desc 'Throw away the pancake'
|
27
34
|
route_param :id, 'Pancake ID to delete'
|
28
35
|
api_method :delete, '/pancake/:id' do
|
@@ -33,21 +40,21 @@ describe "Apiculture.api_documentation" do
|
|
33
40
|
api_method :get, '/pancake/with/:topping_id' do |topping_id|
|
34
41
|
end
|
35
42
|
end
|
36
|
-
|
37
|
-
|
43
|
+
end
|
44
|
+
|
38
45
|
it 'generates app documentation as HTML without the body element' do
|
39
46
|
docco = app.api_documentation
|
40
47
|
generated_html = docco.to_html_fragment
|
41
|
-
|
48
|
+
|
42
49
|
expect(generated_html).not_to include('<body')
|
43
50
|
expect(generated_html).to include('Pancake ID to check status on')
|
44
51
|
expect(generated_html).to include('Pancake ID to delete')
|
45
52
|
end
|
46
|
-
|
53
|
+
|
47
54
|
it 'generates app documentation in HTML' do
|
48
55
|
docco = app.api_documentation
|
49
56
|
generated_html = docco.to_html
|
50
|
-
|
57
|
+
|
51
58
|
if ENV['SHOW_TEST_DOC']
|
52
59
|
File.open('t.html', 'w') do |f|
|
53
60
|
f.write(generated_html)
|
@@ -55,21 +62,22 @@ describe "Apiculture.api_documentation" do
|
|
55
62
|
`open #{f.path}`
|
56
63
|
end
|
57
64
|
end
|
58
|
-
|
65
|
+
|
59
66
|
expect(generated_html).to include('<body')
|
60
67
|
expect(generated_html).to include('Pancake ID to check status on')
|
61
|
-
expect(generated_html).to include('When the pancake
|
68
|
+
expect(generated_html).to include('When the pancake has been baked successfully')
|
62
69
|
expect(generated_html).to include('"id": "abdef..c21"')
|
70
|
+
expect(generated_html).to end_with("\n")
|
63
71
|
end
|
64
|
-
|
72
|
+
|
65
73
|
it 'generates app documentation in Markdown' do
|
66
74
|
docco = app.api_documentation
|
67
75
|
generated_markdown = docco.to_markdown
|
68
|
-
|
76
|
+
|
69
77
|
expect(generated_markdown).not_to include('<body')
|
70
78
|
expect(generated_markdown).to include('## POST /pancakes')
|
71
79
|
end
|
72
|
-
|
80
|
+
|
73
81
|
it 'generates app documentation honoring the mount point' do
|
74
82
|
overridden = Class.new(Apiculture::App) do
|
75
83
|
extend Apiculture
|
@@ -77,11 +85,11 @@ describe "Apiculture.api_documentation" do
|
|
77
85
|
api_method :get, '/pancakes' do
|
78
86
|
end
|
79
87
|
end
|
80
|
-
|
88
|
+
|
81
89
|
generated_markdown = overridden.api_documentation.to_markdown
|
82
90
|
expect(generated_markdown).to include('## GET /api/v2/pancakes')
|
83
91
|
end
|
84
|
-
|
92
|
+
|
85
93
|
it 'generates app documentation injecting the inline Markdown strings' do
|
86
94
|
app_class = Class.new(Apiculture::App) do
|
87
95
|
extend Apiculture
|
@@ -91,16 +99,16 @@ describe "Apiculture.api_documentation" do
|
|
91
99
|
markdown_string '# This describes even more important stuff'
|
92
100
|
markdown_string 'This is a paragraph'
|
93
101
|
end
|
94
|
-
|
102
|
+
|
95
103
|
generated_html = app_class.api_documentation.to_html
|
96
104
|
expect(generated_html).to include('<h2>GET /pancakes</h2>')
|
97
105
|
expect(generated_html).to include('<h1>This describes even more important stuff')
|
98
106
|
expect(generated_html).to include('<h1>This describes important stuff')
|
99
107
|
expect(generated_html).to include('<p>This is a paragraph')
|
100
108
|
end
|
101
|
-
|
109
|
+
|
102
110
|
context 'with a file containing Markdown that has to be spliced into the docs' do
|
103
|
-
before(:each) { File.open('./TEST.md', 'w') {|f| f << "# This is an important header"} }
|
111
|
+
before(:each) { File.open('./TEST.md', 'w') { |f| f << "# This is an important header" } }
|
104
112
|
after(:each) { File.unlink('./TEST.md') }
|
105
113
|
it 'splices the contents of the file using markdown_file' do
|
106
114
|
app_class = Class.new(Apiculture::App) do
|
@@ -109,7 +117,7 @@ describe "Apiculture.api_documentation" do
|
|
109
117
|
api_method :get, '/pancakes' do
|
110
118
|
end
|
111
119
|
end
|
112
|
-
|
120
|
+
|
113
121
|
generated_html = app_class.api_documentation.to_html
|
114
122
|
expect(generated_html).to include('<h2>GET /pancakes</h2>')
|
115
123
|
expect(generated_html).to include('<h1>This is an important header')
|
@@ -7,7 +7,7 @@ describe Apiculture::MethodDocumentation do
|
|
7
7
|
|
8
8
|
definition.description = "This action bakes pancakes"
|
9
9
|
definition.parameters << Apiculture::Parameter.new(:name, 'Pancake name', true, String, :to_s)
|
10
|
-
definition.parameters << Apiculture::Parameter.new(:thickness, 'Pancake
|
10
|
+
definition.parameters << Apiculture::Parameter.new(:thickness, 'Pancake **thick**ness', false, Float, :to_f)
|
11
11
|
definition.parameters << Apiculture::Parameter.new(:diameter, 'Pancake diameter', false, Integer, :to_i)
|
12
12
|
|
13
13
|
definition.route_parameters << Apiculture::RouteParameter.new(:pan_id, 'ID of the pancake frying pan')
|
@@ -28,9 +28,9 @@ describe Apiculture::MethodDocumentation do
|
|
28
28
|
expect(generated_html).to include('<h3>URL parameters</h3>')
|
29
29
|
expect(generated_html).to include('ID of the pancake frying pan')
|
30
30
|
expect(generated_html).to include('<h3>Request parameters</h3>')
|
31
|
-
expect(generated_html).to include('<
|
32
|
-
expect(generated_html).to include('<
|
33
|
-
expect(generated_html).to include('<
|
31
|
+
expect(generated_html).to include('<p>Pancake name</p>')
|
32
|
+
expect(generated_html).to include('<p>Pancake has been baked</p>')
|
33
|
+
expect(generated_html).to include('<p>Frying pan too cold</p>')
|
34
34
|
end
|
35
35
|
|
36
36
|
it 'generates HTML from an ActionDefinition without route params' do
|
@@ -38,7 +38,7 @@ describe Apiculture::MethodDocumentation do
|
|
38
38
|
|
39
39
|
definition.description = "This action bakes pancakes"
|
40
40
|
definition.parameters << Apiculture::Parameter.new(:name, 'Pancake name', true, String, :to_s)
|
41
|
-
definition.parameters << Apiculture::Parameter.new(:thickness, 'Pancake
|
41
|
+
definition.parameters << Apiculture::Parameter.new(:thickness, 'Pancake **thick**ness', false, Float, :to_f)
|
42
42
|
definition.parameters << Apiculture::Parameter.new(:diameter, 'Pancake diameter', false, Integer, :to_i)
|
43
43
|
|
44
44
|
definition.http_verb = 'get'
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe 'Apiculture.api_documentation' do
|
4
|
+
let!(:test_class) do
|
5
|
+
class PancakeApi < Apiculture::App
|
6
|
+
extend Apiculture
|
7
|
+
|
8
|
+
documentation_build_time!
|
9
|
+
|
10
|
+
desc 'Order a pancake'
|
11
|
+
required_param :diameter, 'Diameter of the pancake. The pancake will be **bold**', Integer
|
12
|
+
param :topping, 'Type of topping', String
|
13
|
+
pancake_response_info = <<~EOS
|
14
|
+
When the pancake has been baked successfully
|
15
|
+
The pancake will have the following properties:
|
16
|
+
|
17
|
+
* It is going to be round
|
18
|
+
* It is going to be delicious
|
19
|
+
EOS
|
20
|
+
responds_with 200, pancake_response_info, { id: 'abdef..c21' }
|
21
|
+
api_method :post, '/pancakes' do
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Check the pancake status'
|
25
|
+
route_param :id, 'Pancake ID to check status on'
|
26
|
+
responds_with 200, 'When the pancake is found', { status: 'Baking' }
|
27
|
+
responds_with 404, 'When no such pancake exists', { status: 'No such pancake' }
|
28
|
+
api_method :get, '/pancake/:id' do
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'Throw away the pancake'
|
32
|
+
route_param :id, 'Pancake ID to delete'
|
33
|
+
api_method :delete, '/pancake/:id' do
|
34
|
+
end
|
35
|
+
|
36
|
+
desc 'Pancake ingredients are in the URL'
|
37
|
+
route_param :topping_id, 'Pancake topping ID', Integer, cast: :to_i
|
38
|
+
api_method :get, '/pancake/with/:topping_id' do |topping_id|
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
let(:documentation) { PancakeApi.api_documentation }
|
44
|
+
let(:open_api_map) { documentation.to_openapi.spec }
|
45
|
+
|
46
|
+
describe '.to_openapi' do
|
47
|
+
it 'will have openapi version' do
|
48
|
+
expect(open_api_map).to include(openapi: '3.0.0')
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'info' do
|
52
|
+
let(:info) { open_api_map.fetch(:info) }
|
53
|
+
|
54
|
+
it 'will not to be empty' do
|
55
|
+
expect(info).not_to be_empty
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'will have title' do
|
59
|
+
expect(info).to include(title: 'PancakeApi')
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'will have version' do
|
63
|
+
expect(info).to include(version: '0.0.1')
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'will have description' do
|
67
|
+
expect(info).to include(:description)
|
68
|
+
expect(info[:description]).to include('PancakeApi Documentation built on')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'paths' do
|
73
|
+
let(:paths) { open_api_map.fetch(:paths) }
|
74
|
+
|
75
|
+
it 'will have paths' do
|
76
|
+
expect(paths).not_to be_empty
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'will have 4 paths' do
|
80
|
+
expect(paths.size).to eq(3)
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'POST /pancakes' do
|
84
|
+
let(:post_pancakes) { paths.dig('/pancakes', :post) }
|
85
|
+
|
86
|
+
it 'will have route' do
|
87
|
+
expect(post_pancakes).not_to be_empty
|
88
|
+
end
|
89
|
+
|
90
|
+
describe 'request body content' do
|
91
|
+
let(:request_content) { post_pancakes.dig(:requestBody, :content) }
|
92
|
+
it 'will have correct JSON content type' do
|
93
|
+
expect(request_content).to have_key(:'application/json')
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'schema' do
|
97
|
+
let(:schema) { request_content.dig(:'application/json', :schema) }
|
98
|
+
it 'will have type' do
|
99
|
+
expect(schema).to include(type: :object)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'will have required parameters' do
|
103
|
+
expect(schema).to include(required: [:diameter])
|
104
|
+
end
|
105
|
+
|
106
|
+
describe 'properties' do
|
107
|
+
let(:properties) { schema.fetch(:properties) }
|
108
|
+
it 'will have all properties' do
|
109
|
+
expect(properties).to have_key :diameter
|
110
|
+
expect(properties).to have_key :topping
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'GET /pancake/:id' do
|
118
|
+
let(:get_pancake_by_id_path) { paths.dig('/pancake/{id}', :get) }
|
119
|
+
it 'will have route' do
|
120
|
+
expect(get_pancake_by_id_path).not_to be_empty
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'will have summary' do
|
124
|
+
expect(get_pancake_by_id_path).to include(summary: 'Check the pancake status')
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'will have a description' do
|
128
|
+
expect(get_pancake_by_id_path).to include(description: 'Check the pancake status')
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'will have operationId' do
|
132
|
+
expect(get_pancake_by_id_path).to have_key :operationId
|
133
|
+
end
|
134
|
+
|
135
|
+
describe 'parameters' do
|
136
|
+
let(:parameter) { get_pancake_by_id_path.fetch(:parameters).first }
|
137
|
+
|
138
|
+
it 'will contain parameter' do
|
139
|
+
expect(parameter).not_to be_empty
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'will have required propertie' do
|
143
|
+
expect(parameter).to include(required: true)
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'will indicate a path parameter' do
|
147
|
+
expect(parameter).to include(in: :path)
|
148
|
+
end
|
149
|
+
|
150
|
+
describe 'schema' do
|
151
|
+
let(:schema) { parameter.fetch(:schema) }
|
152
|
+
it 'will have object type' do
|
153
|
+
expect(schema).to include(type: 'string')
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'will have an example' do
|
157
|
+
expect(schema).to include(example: 'string')
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe 'responses' do
|
163
|
+
let(:responses) { get_pancake_by_id_path.fetch(:responses) }
|
164
|
+
|
165
|
+
it 'will contain all responses by response code' do
|
166
|
+
expect(responses).to have_key('200')
|
167
|
+
expect(responses).to have_key('404')
|
168
|
+
end
|
169
|
+
|
170
|
+
describe 'response 200' do
|
171
|
+
let(:response_200) { responses.fetch('200') }
|
172
|
+
|
173
|
+
it 'will have description' do
|
174
|
+
expect(response_200).to include(description: 'When the pancake is found')
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'will have correct content type' do
|
178
|
+
expect(response_200.fetch(:content)).to have_key(:'application/json')
|
179
|
+
end
|
180
|
+
|
181
|
+
describe 'schema' do
|
182
|
+
let(:schema) { response_200.dig(:content, :'application/json', :schema) }
|
183
|
+
|
184
|
+
it 'will have object type' do
|
185
|
+
expect(schema).to include(type: 'object')
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'will have properties' do
|
189
|
+
expect(schema).to have_key :properties
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: apiculture
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
- WeTransfer
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2021-06-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mustermann
|
@@ -59,14 +59,14 @@ dependencies:
|
|
59
59
|
requirements:
|
60
60
|
- - "~>"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
62
|
+
version: '3'
|
63
63
|
type: :runtime
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
67
|
- - "~>"
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '
|
69
|
+
version: '3'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: mustache
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -101,20 +101,14 @@ dependencies:
|
|
101
101
|
requirements:
|
102
102
|
- - "~>"
|
103
103
|
- !ruby/object:Gem::Version
|
104
|
-
version: '3
|
105
|
-
- - "<"
|
106
|
-
- !ruby/object:Gem::Version
|
107
|
-
version: '3.2'
|
104
|
+
version: '3'
|
108
105
|
type: :development
|
109
106
|
prerelease: false
|
110
107
|
version_requirements: !ruby/object:Gem::Requirement
|
111
108
|
requirements:
|
112
109
|
- - "~>"
|
113
110
|
- !ruby/object:Gem::Version
|
114
|
-
version: '3
|
115
|
-
- - "<"
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '3.2'
|
111
|
+
version: '3'
|
118
112
|
- !ruby/object:Gem::Dependency
|
119
113
|
name: rdoc
|
120
114
|
requirement: !ruby/object:Gem::Requirement
|
@@ -133,16 +127,16 @@ dependencies:
|
|
133
127
|
name: rake
|
134
128
|
requirement: !ruby/object:Gem::Requirement
|
135
129
|
requirements:
|
136
|
-
- - "
|
130
|
+
- - ">="
|
137
131
|
- !ruby/object:Gem::Version
|
138
|
-
version: '
|
132
|
+
version: '0'
|
139
133
|
type: :development
|
140
134
|
prerelease: false
|
141
135
|
version_requirements: !ruby/object:Gem::Requirement
|
142
136
|
requirements:
|
143
|
-
- - "
|
137
|
+
- - ">="
|
144
138
|
- !ruby/object:Gem::Version
|
145
|
-
version: '
|
139
|
+
version: '0'
|
146
140
|
- !ruby/object:Gem::Dependency
|
147
141
|
name: bundler
|
148
142
|
requirement: !ruby/object:Gem::Requirement
|
@@ -197,12 +191,14 @@ files:
|
|
197
191
|
- lib/apiculture/indifferent_hash.rb
|
198
192
|
- lib/apiculture/markdown_segment.rb
|
199
193
|
- lib/apiculture/method_documentation.rb
|
194
|
+
- lib/apiculture/openapi_documentation.rb
|
200
195
|
- lib/apiculture/sinatra_instance_methods.rb
|
201
196
|
- lib/apiculture/timestamp_promise.rb
|
202
197
|
- lib/apiculture/version.rb
|
203
198
|
- spec/apiculture/action_spec.rb
|
204
199
|
- spec/apiculture/app_documentation_spec.rb
|
205
200
|
- spec/apiculture/method_documentation_spec.rb
|
201
|
+
- spec/apiculture/openapi_documentation_spec.rb
|
206
202
|
- spec/apiculture_spec.rb
|
207
203
|
- spec/spec_helper.rb
|
208
204
|
homepage: https://github.com/WeTransfer/apiculture
|
@@ -210,7 +206,7 @@ licenses:
|
|
210
206
|
- MIT
|
211
207
|
metadata:
|
212
208
|
allowed_push_host: https://rubygems.org
|
213
|
-
post_install_message:
|
209
|
+
post_install_message:
|
214
210
|
rdoc_options: []
|
215
211
|
require_paths:
|
216
212
|
- lib
|
@@ -225,9 +221,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
225
221
|
- !ruby/object:Gem::Version
|
226
222
|
version: '0'
|
227
223
|
requirements: []
|
228
|
-
|
229
|
-
|
230
|
-
signing_key:
|
224
|
+
rubygems_version: 3.0.9
|
225
|
+
signing_key:
|
231
226
|
specification_version: 4
|
232
227
|
summary: Sweet API sauce on top of Rack
|
233
228
|
test_files: []
|