apiculture 0.2.0 → 0.2.2
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/.github/dependabot.yml +16 -0
- data/.github/workflows/ci.yml +15 -0
- data/.github/workflows/validate.yml +28 -0
- data/CHANGELOG.md +4 -0
- data/README.md +1 -1
- data/apiculture.gemspec +1 -7
- data/lib/apiculture/app_documentation.rb +8 -8
- data/lib/apiculture/openapi_documentation.rb +26 -14
- data/lib/apiculture/version.rb +1 -1
- data/lib/apiculture.rb +16 -2
- metadata +6 -7
- data/.travis.yml +0 -13
- data/gemfiles/Gemfile.rack-1.x +0 -16
- data/gemfiles/Gemfile.rack-2.x +0 -16
- data/lib/apiculture/app.rb +0 -131
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1669c3138f92cf9b5c0db92933612e3d3c15430a0b641141eae633859663d069
|
4
|
+
data.tar.gz: cb9c4458d470ee191a62b9df0ab28cc0fdb432ec28ad155f22e8497a91633692
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 400c9e119a6d47b3812cbd93ac2fb827b68ba7d9e25d1201a007494c42eba7f5dd75009334b26dc21c8cd2f8b50193e858ee25ebfb3bfa057e52bd518f1e953d
|
7
|
+
data.tar.gz: 8b314eb600095c4ad707ce6fe3f0f793e33d14ae374f55eda70d7a226532a59d93c8b312aac370ac05024b9a1c03e647f72f272a188bacbc58885a2131bfb578
|
@@ -0,0 +1,16 @@
|
|
1
|
+
---
|
2
|
+
version: 2
|
3
|
+
updates:
|
4
|
+
- package-ecosystem: github-actions
|
5
|
+
directory: /
|
6
|
+
schedule:
|
7
|
+
interval: daily
|
8
|
+
time: "10:00"
|
9
|
+
timezone: Europe/Amsterdam
|
10
|
+
- package-ecosystem: bundler
|
11
|
+
directory: /
|
12
|
+
registries: "*"
|
13
|
+
schedule:
|
14
|
+
interval: daily
|
15
|
+
time: "09:00"
|
16
|
+
timezone: Europe/Amsterdam
|
@@ -0,0 +1,15 @@
|
|
1
|
+
name: CI
|
2
|
+
on:
|
3
|
+
pull_request:
|
4
|
+
branches:
|
5
|
+
- master
|
6
|
+
# dependabot PRs only have read permission so cannot run this workflow.
|
7
|
+
paths-ignore:
|
8
|
+
- "**/*.md"
|
9
|
+
- ".gitignore"
|
10
|
+
- "README.md"
|
11
|
+
- ".env.example"
|
12
|
+
- "docs/*"
|
13
|
+
jobs:
|
14
|
+
validate:
|
15
|
+
uses: ./.github/workflows/validate.yml
|
@@ -0,0 +1,28 @@
|
|
1
|
+
name: Validate
|
2
|
+
on:
|
3
|
+
workflow_call:
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
validate:
|
7
|
+
name: Validate
|
8
|
+
runs-on: ubuntu-latest
|
9
|
+
strategy:
|
10
|
+
matrix:
|
11
|
+
ruby-version:
|
12
|
+
- '2.6'
|
13
|
+
- '2.7'
|
14
|
+
- '3.0'
|
15
|
+
- '3.2'
|
16
|
+
- '3.3'
|
17
|
+
steps:
|
18
|
+
- uses: actions/checkout@v4
|
19
|
+
|
20
|
+
- name: Set up Ruby
|
21
|
+
uses: ruby/setup-ruby@v1
|
22
|
+
with:
|
23
|
+
ruby-version: ${{ matrix.ruby-version }}
|
24
|
+
bundler-cache: true
|
25
|
+
|
26
|
+
- name: Run tests
|
27
|
+
run: |
|
28
|
+
bundle exec rspec
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
A little toolkit for building RESTful API backends on top of Rack.
|
4
4
|
|
5
|
-
[](https://github.com/WeTransfer/apiculture/actions/workflows/ci.yml)
|
6
6
|
|
7
7
|
## Ideas
|
8
8
|
|
data/apiculture.gemspec
CHANGED
@@ -15,13 +15,7 @@ Gem::Specification.new do |s|
|
|
15
15
|
# Prevent pushing this gem to RubyGems.org.
|
16
16
|
# To allow pushes either set the 'allowed_push_host'
|
17
17
|
# To allow pushing to a single host or delete this section to allow pushing to any host.
|
18
|
-
|
19
|
-
s.metadata['allowed_push_host'] = 'https://rubygems.org'
|
20
|
-
else
|
21
|
-
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
22
|
-
'public gem pushes.'
|
23
|
-
end
|
24
|
-
|
18
|
+
s.metadata['allowed_push_host'] = 'https://rubygems.org'
|
25
19
|
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
|
26
20
|
s.extra_rdoc_files = [
|
27
21
|
'LICENSE.txt',
|
@@ -6,38 +6,38 @@ class Apiculture::AppDocumentation
|
|
6
6
|
def to_markdown
|
7
7
|
string.to_markdown.to_s rescue string.to_s
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
def to_html
|
11
11
|
'<section class="%s">%s</section>' % [Rack::Utils.escape_html(section_class), render_markdown(to_markdown)]
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
def render_markdown(s)
|
15
15
|
GitHub::Markup.render('section.markdown', s.to_s)
|
16
16
|
end
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
def initialize(app, mountpoint, action_definitions_and_markdown_segments)
|
20
20
|
@app_title = app.to_s
|
21
21
|
@mountpoint = mountpoint
|
22
22
|
@chunks = action_definitions_and_markdown_segments
|
23
23
|
end
|
24
|
-
|
24
|
+
|
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
|
28
|
+
end
|
29
29
|
|
30
30
|
def to_openapi
|
31
31
|
OpenApiDocumentation::Base.new(@app_title, @mountpoint, @chunks)
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
# Generates an HTML fragment string that can be included into another HTML document
|
35
35
|
def to_html_fragment
|
36
36
|
to_markdown_slices.map do |tagged_markdown|
|
37
37
|
tagged_markdown.to_html
|
38
38
|
end.join("\n\n")
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
def to_markdown_slices
|
42
42
|
markdown_slices = @chunks.map do | action_def_or_doc |
|
43
43
|
if action_def_or_doc.respond_to?(:http_verb) # ActionDefinition
|
@@ -48,7 +48,7 @@ class Apiculture::AppDocumentation
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
end
|
51
|
-
|
51
|
+
|
52
52
|
# Generates a complete HTML document string that can be saved into a file
|
53
53
|
def to_html
|
54
54
|
require 'mustache'
|
@@ -8,9 +8,9 @@ module OpenApiDocumentation
|
|
8
8
|
@paths = chunks.select { |chunk| chunk.respond_to?(:http_verb) }
|
9
9
|
@data = {
|
10
10
|
openapi: '3.0.0',
|
11
|
-
info: {
|
12
|
-
title: @app.to_s,
|
13
|
-
version: '0.0.1',
|
11
|
+
info: {
|
12
|
+
title: @app.to_s,
|
13
|
+
version: '0.0.1',
|
14
14
|
description: @app.to_s + " " + chunks.select { |chunk| chunk.respond_to?(:to_markdown) }.map(&:to_markdown).join("\n")
|
15
15
|
},
|
16
16
|
tags: []
|
@@ -27,7 +27,7 @@ module OpenApiDocumentation
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def spec
|
30
|
-
@data
|
30
|
+
@data
|
31
31
|
end
|
32
32
|
|
33
33
|
private
|
@@ -137,7 +137,7 @@ module OpenApiDocumentation
|
|
137
137
|
|
138
138
|
def build_request_body
|
139
139
|
return nil if VERBS_WITHOUT_BODY.include?(@path.http_verb)
|
140
|
-
|
140
|
+
|
141
141
|
body_params = Hash[ @path.parameters.collect do |parameter|
|
142
142
|
[parameter.name, {
|
143
143
|
type: Util.map_type(parameter.matchable),
|
@@ -171,7 +171,7 @@ module OpenApiDocumentation
|
|
171
171
|
unless response.jsonable_object_example.nil? || response.jsonable_object_example.empty?
|
172
172
|
_response[:content] = {
|
173
173
|
'application/json': {
|
174
|
-
schema:
|
174
|
+
schema:
|
175
175
|
{ type: 'object',
|
176
176
|
properties: Util.response_to_schema(response.jsonable_object_example) }
|
177
177
|
}
|
@@ -198,15 +198,27 @@ module OpenApiDocumentation
|
|
198
198
|
}.freeze
|
199
199
|
|
200
200
|
def self.response_to_schema(response)
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
201
|
+
case response
|
202
|
+
when NilClass
|
203
|
+
when String
|
204
|
+
{ type: 'string', example: response }
|
205
|
+
when Integer
|
206
|
+
{ type: 'integer', example: response }
|
207
|
+
when Float
|
208
|
+
{ type: 'float', example: response }
|
209
|
+
when Array
|
210
|
+
if response.empty?
|
211
|
+
{ type: 'array', items: {} }
|
212
|
+
else
|
213
|
+
{ type: 'array', items: response.map { |elem| response_to_schema(elem) } }
|
214
|
+
end
|
215
|
+
when Hash
|
216
|
+
response.each_with_object({}) do |(key, val), schema_hash|
|
217
|
+
schema_hash[key] = response_to_schema(val)
|
218
|
+
end
|
219
|
+
else
|
220
|
+
{ type: response.class.name.downcase, example: response.to_s }
|
208
221
|
end
|
209
|
-
schema
|
210
222
|
end
|
211
223
|
|
212
224
|
def self.map_type(type)
|
data/lib/apiculture/version.rb
CHANGED
data/lib/apiculture.rb
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
module Apiculture
|
3
3
|
require_relative 'apiculture/version'
|
4
4
|
require_relative 'apiculture/indifferent_hash'
|
5
|
-
require_relative 'apiculture/app'
|
6
5
|
require_relative 'apiculture/action'
|
7
6
|
require_relative 'apiculture/sinatra_instance_methods'
|
8
7
|
require_relative 'apiculture/action_definition'
|
@@ -16,6 +15,12 @@ module Apiculture
|
|
16
15
|
super
|
17
16
|
end
|
18
17
|
|
18
|
+
class Void
|
19
|
+
def <<(_item); end
|
20
|
+
def map; []; end
|
21
|
+
def select; []; end
|
22
|
+
end
|
23
|
+
|
19
24
|
IDENTITY_PROC = ->(arg) { arg }
|
20
25
|
|
21
26
|
AC_APPLY_TYPECAST_PROC = ->(cast_proc_or_method, v) {
|
@@ -277,7 +282,16 @@ module Apiculture
|
|
277
282
|
end
|
278
283
|
|
279
284
|
def apiculture_stack
|
280
|
-
|
285
|
+
if environment == "development"
|
286
|
+
@apiculture_actions_and_docs ||= []
|
287
|
+
else
|
288
|
+
@apiculture_actions_and_docs ||= Void.new
|
289
|
+
end
|
281
290
|
@apiculture_actions_and_docs
|
282
291
|
end
|
292
|
+
|
293
|
+
# Based on the RACK_ENV it will generate documentation or not
|
294
|
+
def environment
|
295
|
+
@environment ||= ENV.fetch("RACK_ENV", "development")
|
296
|
+
end
|
283
297
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: apiculture
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2024-07-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: builder
|
@@ -173,20 +173,19 @@ extra_rdoc_files:
|
|
173
173
|
- LICENSE.txt
|
174
174
|
- README.md
|
175
175
|
files:
|
176
|
+
- ".github/dependabot.yml"
|
177
|
+
- ".github/workflows/ci.yml"
|
178
|
+
- ".github/workflows/validate.yml"
|
176
179
|
- ".gitignore"
|
177
|
-
- ".travis.yml"
|
178
180
|
- CHANGELOG.md
|
179
181
|
- Gemfile
|
180
182
|
- LICENSE.txt
|
181
183
|
- README.md
|
182
184
|
- Rakefile
|
183
185
|
- apiculture.gemspec
|
184
|
-
- gemfiles/Gemfile.rack-1.x
|
185
|
-
- gemfiles/Gemfile.rack-2.x
|
186
186
|
- lib/apiculture.rb
|
187
187
|
- lib/apiculture/action.rb
|
188
188
|
- lib/apiculture/action_definition.rb
|
189
|
-
- lib/apiculture/app.rb
|
190
189
|
- lib/apiculture/app_documentation.rb
|
191
190
|
- lib/apiculture/app_documentation_tpl.mustache
|
192
191
|
- lib/apiculture/indifferent_hash.rb
|
@@ -216,7 +215,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
216
215
|
- !ruby/object:Gem::Version
|
217
216
|
version: '0'
|
218
217
|
requirements: []
|
219
|
-
rubygems_version: 3.
|
218
|
+
rubygems_version: 3.5.3
|
220
219
|
signing_key:
|
221
220
|
specification_version: 4
|
222
221
|
summary: Sweet API sauce on top of Rack
|
data/.travis.yml
DELETED
data/gemfiles/Gemfile.rack-1.x
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
source "http://rubygems.org"
|
2
|
-
gem 'rack', "~> 1"
|
3
|
-
|
4
|
-
gem 'mustermann', '~> 1'
|
5
|
-
gem 'builder', '~> 3'
|
6
|
-
gem 'rdiscount', '~> 2'
|
7
|
-
gem 'github-markup', '~> 1'
|
8
|
-
gem 'mustache', '~> 1'
|
9
|
-
|
10
|
-
group :development do
|
11
|
-
gem 'rack-test'
|
12
|
-
gem "rspec", "~> 3"
|
13
|
-
gem "rdoc", "~> 6.0"
|
14
|
-
gem "rake"
|
15
|
-
gem "bundler"
|
16
|
-
end
|
data/gemfiles/Gemfile.rack-2.x
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
source "http://rubygems.org"
|
2
|
-
gem 'rack', "~> 2"
|
3
|
-
|
4
|
-
gem 'mustermann', '~> 1'
|
5
|
-
gem 'builder', '~> 3'
|
6
|
-
gem 'rdiscount', '~> 2'
|
7
|
-
gem 'github-markup', '~> 1'
|
8
|
-
gem 'mustache', '~> 1'
|
9
|
-
|
10
|
-
group :development do
|
11
|
-
gem 'rack-test'
|
12
|
-
gem "rspec", "~> 3"
|
13
|
-
gem "rdoc", "~> 6.0"
|
14
|
-
gem "rake"
|
15
|
-
gem "bundler"
|
16
|
-
end
|
data/lib/apiculture/app.rb
DELETED
@@ -1,131 +0,0 @@
|
|
1
|
-
require'mustermann'
|
2
|
-
|
3
|
-
class Apiculture::App
|
4
|
-
|
5
|
-
class << self
|
6
|
-
def use(middlreware_factory, middleware_options, &middleware_blk)
|
7
|
-
@middleware_configurations ||= []
|
8
|
-
@middleware_configurations << [middleware_factory, middleware_options, middleware_blk]
|
9
|
-
end
|
10
|
-
|
11
|
-
def middleware_configurations
|
12
|
-
@middleware_configurations || []
|
13
|
-
end
|
14
|
-
|
15
|
-
def get(url, **options, &handler_blk)
|
16
|
-
define_action :get, url, **options, &handler_blk
|
17
|
-
end
|
18
|
-
|
19
|
-
def post(url, **options, &handler_blk)
|
20
|
-
define_action :post, url, **options, &handler_blk
|
21
|
-
end
|
22
|
-
|
23
|
-
def put(url, **options, &handler_blk)
|
24
|
-
define_action :put, url, **options, &handler_blk
|
25
|
-
end
|
26
|
-
|
27
|
-
def delete(url, **options, &handler_blk)
|
28
|
-
define_action :delete, url, **options, &handler_blk
|
29
|
-
end
|
30
|
-
|
31
|
-
def actions
|
32
|
-
@actions || []
|
33
|
-
end
|
34
|
-
|
35
|
-
def define_action(http_method, url_path, **options, &handler_blk)
|
36
|
-
@actions ||= []
|
37
|
-
@actions << [http_method.to_s.upcase, url_path, options, handler_blk]
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def call_without_middleware(env)
|
42
|
-
@env = env
|
43
|
-
|
44
|
-
# First try to route via actions...
|
45
|
-
given_http_method = env.fetch('REQUEST_METHOD')
|
46
|
-
given_path = env.fetch('PATH_INFO')
|
47
|
-
given_path = '/' + given_path unless given_path.start_with?('/')
|
48
|
-
|
49
|
-
action_list = self.class.actions
|
50
|
-
# TODO: I believe Sinatra matches bottom-up, not top-down.
|
51
|
-
action_list.reverse.each do | (action_http_method, action_url_path, action_options, action_handler_callable)|
|
52
|
-
route_pattern = Mustermann.new(action_url_path)
|
53
|
-
if given_http_method == action_http_method && route_params = route_pattern.params(given_path)
|
54
|
-
@request = Rack::Request.new(env)
|
55
|
-
@params.merge!(@request.params)
|
56
|
-
@route_params = route_params
|
57
|
-
|
58
|
-
match = route_pattern.match(given_path)
|
59
|
-
@route_params['captures'] = match.captures unless match.nil?
|
60
|
-
@params.merge!(@route_params)
|
61
|
-
return perform_action_block(&action_handler_callable)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
# and if nothing works out - respond with a 404
|
66
|
-
out = JSON.pretty_generate({
|
67
|
-
error: 'No matching action found for %s %s' % [given_http_method, given_path],
|
68
|
-
})
|
69
|
-
[404, {'Content-Type' => 'application/json', 'Content-Length' => out.bytesize.to_s}, [out]]
|
70
|
-
end
|
71
|
-
|
72
|
-
def self.call(env)
|
73
|
-
app = new
|
74
|
-
Rack::Builder.new do
|
75
|
-
(@middleware_configurations || []).each do |middleware_args|
|
76
|
-
use(*middleware_args)
|
77
|
-
end
|
78
|
-
run ->(env) { app.call_without_middleware(env) }
|
79
|
-
end.to_app.call(env)
|
80
|
-
end
|
81
|
-
|
82
|
-
attr_reader :request
|
83
|
-
attr_reader :env
|
84
|
-
attr_reader :params
|
85
|
-
|
86
|
-
def initialize
|
87
|
-
@status = 200
|
88
|
-
@content_type = 'text/plain'
|
89
|
-
@params = Apiculture::IndifferentHash.new
|
90
|
-
end
|
91
|
-
|
92
|
-
def content_type(new_type)
|
93
|
-
@content_type = Rack::Mime.mime_type('.%s' % new_type)
|
94
|
-
end
|
95
|
-
|
96
|
-
def status(status_code)
|
97
|
-
@status = status_code.to_i
|
98
|
-
end
|
99
|
-
|
100
|
-
def halt(rack_status, rack_headers, rack_body)
|
101
|
-
throw :halt, [rack_status, rack_headers, rack_body]
|
102
|
-
end
|
103
|
-
|
104
|
-
def perform_action_block(&blk)
|
105
|
-
# Executes the action in a Sinatra-like fashion - passing the route parameter values as
|
106
|
-
# arguments to the given block/callable. This is where in the future we should ditch
|
107
|
-
# the Sinatra calling conventions - Sinatra mandates that the action accept the route parameters
|
108
|
-
# as arguments and grab all the useful stuff from instance methods like `params` etc. whereas
|
109
|
-
# we probably want to have just Rack apps mounted per route (under an action)
|
110
|
-
response = catch(:halt) do
|
111
|
-
body_string_or_rack_triplet = instance_exec(*@route_params.values, &blk)
|
112
|
-
|
113
|
-
if rack_triplet?(body_string_or_rack_triplet)
|
114
|
-
return body_string_or_rack_triplet
|
115
|
-
end
|
116
|
-
|
117
|
-
[@status, {'Content-Type' => @content_type}, [body_string_or_rack_triplet]]
|
118
|
-
end
|
119
|
-
|
120
|
-
return response
|
121
|
-
end
|
122
|
-
|
123
|
-
def rack_triplet?(maybe_triplet)
|
124
|
-
maybe_triplet.is_a?(Array) &&
|
125
|
-
maybe_triplet.length == 3 &&
|
126
|
-
maybe_triplet[0].is_a?(Integer) &&
|
127
|
-
maybe_triplet[1].is_a?(Hash) &&
|
128
|
-
maybe_triplet[1].keys.all? {|k| k.is_a?(String) } &&
|
129
|
-
maybe_triplet[2].respond_to?(:each)
|
130
|
-
end
|
131
|
-
end
|