apiculture 0.1.6 → 0.2.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 +4 -4
- data/.gitignore +3 -5
- data/.travis.yml +2 -0
- data/CHANGELOG.md +3 -0
- data/apiculture.gemspec +19 -20
- data/gemfiles/Gemfile.rack-1.x +2 -3
- data/gemfiles/Gemfile.rack-2.x +2 -3
- data/lib/apiculture/app.rb +4 -4
- data/lib/apiculture/app_documentation.rb +4 -0
- data/lib/apiculture/openapi_documentation.rb +224 -0
- data/lib/apiculture/sinatra_instance_methods.rb +5 -5
- data/lib/apiculture/version.rb +1 -1
- data/lib/apiculture.rb +41 -40
- metadata +42 -45
- data/spec/apiculture/action_spec.rb +0 -45
- data/spec/apiculture/app_documentation_spec.rb +0 -126
- data/spec/apiculture/method_documentation_spec.rb +0 -102
- data/spec/apiculture_spec.rb +0 -461
- data/spec/spec_helper.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d78cc86595f72f0ec4ea08adaa5b545798eb54653aafedb47d3e6923351b3b76
|
4
|
+
data.tar.gz: 532aa3794d65dfd962d4d5984a29678e026b5da43efd1f5ad5dbaea4d4ee6ff1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b351b52e4a10c1c7ee2b7d4f7b5c8f63d4ed8b28f65348ebf63ec0f064ccb55b9bfd6cad6f21d49c34867436dfc50036e67f4d8fdb206f3525581346ee7db3a8
|
7
|
+
data.tar.gz: 9a0d97711023461335d4237f44e077daf2b1669c9509bb53dcd79571fb8a157188fb06b9d93db28ab9df69f170d152c50746ce8c27f73eb2af7781963d8e26bf
|
data/.gitignore
CHANGED
@@ -1,7 +1,3 @@
|
|
1
|
-
# rcov generated
|
2
|
-
coverage
|
3
|
-
coverage.data
|
4
|
-
|
5
1
|
# rdoc generated
|
6
2
|
rdoc
|
7
3
|
|
@@ -16,7 +12,7 @@ Gemfile.lock
|
|
16
12
|
# jeweler generated
|
17
13
|
pkg
|
18
14
|
|
19
|
-
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
|
15
|
+
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
|
20
16
|
#
|
21
17
|
# * Create a file at ~/.gitignore
|
22
18
|
# * Include files you want ignored
|
@@ -48,3 +44,5 @@ pkg
|
|
48
44
|
|
49
45
|
# For rubinius:
|
50
46
|
#*.rbc
|
47
|
+
|
48
|
+
.ruby-version
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
data/apiculture.gemspec
CHANGED
@@ -4,14 +4,13 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'apiculture/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
|
-
s.name =
|
7
|
+
s.name = 'apiculture'
|
8
8
|
s.version = Apiculture::VERSION
|
9
9
|
|
10
|
-
s.require_paths = [
|
11
|
-
s.authors = [
|
12
|
-
s.
|
13
|
-
s.
|
14
|
-
s.email = "me@julik.nl"
|
10
|
+
s.require_paths = ['lib']
|
11
|
+
s.authors = ['Julik Tarkhanov', 'WeTransfer']
|
12
|
+
s.description = 'A toolkit for building REST APIs on top of Rack'
|
13
|
+
s.email = 'me@julik.nl'
|
15
14
|
|
16
15
|
# Prevent pushing this gem to RubyGems.org.
|
17
16
|
# To allow pushes either set the 'allowed_push_host'
|
@@ -23,26 +22,26 @@ Gem::Specification.new do |s|
|
|
23
22
|
'public gem pushes.'
|
24
23
|
end
|
25
24
|
|
26
|
-
s.files = `git ls-files -z`.split("\x0")
|
25
|
+
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
|
27
26
|
s.extra_rdoc_files = [
|
28
|
-
|
29
|
-
|
27
|
+
'LICENSE.txt',
|
28
|
+
'README.md'
|
30
29
|
]
|
31
|
-
s.homepage =
|
32
|
-
s.licenses = [
|
33
|
-
s.rubygems_version =
|
34
|
-
s.summary =
|
30
|
+
s.homepage = 'https://github.com/WeTransfer/apiculture'
|
31
|
+
s.licenses = ['MIT']
|
32
|
+
s.rubygems_version = '2.4.5.1'
|
33
|
+
s.summary = 'Sweet API sauce on top of Rack'
|
35
34
|
|
36
|
-
s.add_runtime_dependency 'mustermann', '~> 1'
|
37
35
|
s.add_runtime_dependency 'builder', '~> 3'
|
38
|
-
s.add_runtime_dependency 'rdiscount', '~> 2'
|
39
36
|
s.add_runtime_dependency 'github-markup', '~> 3'
|
40
37
|
s.add_runtime_dependency 'mustache', '~> 1'
|
38
|
+
s.add_runtime_dependency 'mustermann', '~> 3'
|
39
|
+
s.add_runtime_dependency 'rdiscount', '~> 2'
|
41
40
|
|
41
|
+
s.add_development_dependency 'bundler', '~> 2.0'
|
42
|
+
s.add_development_dependency 'cgi'
|
42
43
|
s.add_development_dependency 'rack-test'
|
43
|
-
s.add_development_dependency
|
44
|
-
s.add_development_dependency
|
45
|
-
s.add_development_dependency
|
46
|
-
s.add_development_dependency "bundler", "~> 1.0"
|
47
|
-
s.add_development_dependency "simplecov", ">= 0"
|
44
|
+
s.add_development_dependency 'rake'
|
45
|
+
s.add_development_dependency 'rdoc', '~> 6.0'
|
46
|
+
s.add_development_dependency 'rspec', '~> 3'
|
48
47
|
end
|
data/gemfiles/Gemfile.rack-1.x
CHANGED
data/gemfiles/Gemfile.rack-2.x
CHANGED
data/lib/apiculture/app.rb
CHANGED
@@ -13,19 +13,19 @@ class Apiculture::App
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def get(url, **options, &handler_blk)
|
16
|
-
define_action :get, url, options, &handler_blk
|
16
|
+
define_action :get, url, **options, &handler_blk
|
17
17
|
end
|
18
18
|
|
19
19
|
def post(url, **options, &handler_blk)
|
20
|
-
define_action :post, url, options, &handler_blk
|
20
|
+
define_action :post, url, **options, &handler_blk
|
21
21
|
end
|
22
22
|
|
23
23
|
def put(url, **options, &handler_blk)
|
24
|
-
define_action :put, url, options, &handler_blk
|
24
|
+
define_action :put, url, **options, &handler_blk
|
25
25
|
end
|
26
26
|
|
27
27
|
def delete(url, **options, &handler_blk)
|
28
|
-
define_action :delete, url, options, &handler_blk
|
28
|
+
define_action :delete, url, **options, &handler_blk
|
29
29
|
end
|
30
30
|
|
31
31
|
def actions
|
@@ -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
|
@@ -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
|
@@ -4,7 +4,7 @@ require 'json'
|
|
4
4
|
module Apiculture::SinatraInstanceMethods
|
5
5
|
NEWLINE = "\n"
|
6
6
|
DEFAULT = :__default__
|
7
|
-
|
7
|
+
|
8
8
|
# Convert the given structure to JSON, set the content-type and
|
9
9
|
# return the JSON string
|
10
10
|
def json_response(structure, status: DEFAULT)
|
@@ -12,7 +12,7 @@ module Apiculture::SinatraInstanceMethods
|
|
12
12
|
status(status) unless status == DEFAULT
|
13
13
|
JSON.pretty_generate(structure)
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
# Bail out from an action by sending a halt() via Sinatra. Is most useful for
|
17
17
|
# handling access denied, invalid resource and other types of situations
|
18
18
|
# where you don't want the request to continue, but still would like to
|
@@ -20,10 +20,10 @@ module Apiculture::SinatraInstanceMethods
|
|
20
20
|
# with it's own JSON means.
|
21
21
|
def json_halt(with_error_message, status: 400, **attrs_for_json_response)
|
22
22
|
# Pretty-print + newline to be terminal-friendly
|
23
|
-
err_str = JSON.pretty_generate({error: with_error_message}.merge(attrs_for_json_response)) + NEWLINE
|
23
|
+
err_str = JSON.pretty_generate({error: with_error_message}.merge(**attrs_for_json_response)) + NEWLINE
|
24
24
|
halt status, {'Content-Type' => 'application/json'}, [err_str]
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
# Handles the given action via the given class, passing it the instance variables
|
28
28
|
# given in the keyword arguments
|
29
29
|
def action_result(action_class, **action_ivars)
|
@@ -31,7 +31,7 @@ module Apiculture::SinatraInstanceMethods
|
|
31
31
|
unless call_result.is_a?(Array) || call_result.is_a?(Hash) || (call_result.nil? && @status == 204)
|
32
32
|
raise "Action result should be an Array, a Hash or it can be nil but only if status is 204, instead it was a #{call_result.class}"
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
json_response call_result if call_result
|
36
36
|
end
|
37
37
|
end
|
data/lib/apiculture/version.rb
CHANGED