apiculture 0.2.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d78cc86595f72f0ec4ea08adaa5b545798eb54653aafedb47d3e6923351b3b76
4
- data.tar.gz: 532aa3794d65dfd962d4d5984a29678e026b5da43efd1f5ad5dbaea4d4ee6ff1
3
+ metadata.gz: 1669c3138f92cf9b5c0db92933612e3d3c15430a0b641141eae633859663d069
4
+ data.tar.gz: cb9c4458d470ee191a62b9df0ab28cc0fdb432ec28ad155f22e8497a91633692
5
5
  SHA512:
6
- metadata.gz: b351b52e4a10c1c7ee2b7d4f7b5c8f63d4ed8b28f65348ebf63ec0f064ccb55b9bfd6cad6f21d49c34867436dfc50036e67f4d8fdb206f3525581346ee7db3a8
7
- data.tar.gz: 9a0d97711023461335d4237f44e077daf2b1669c9509bb53dcd79571fb8a157188fb06b9d93db28ab9df69f170d152c50746ce8c27f73eb2af7781963d8e26bf
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
@@ -1,3 +1,7 @@
1
1
  ## 0.2.0
2
2
  * Add ruby 3 support
3
3
  * Bump `mustermann` dependency to '~> 3'
4
+
5
+
6
+ ## 0.2.1
7
+ * Add support for nested fields
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
- [![Build Status](https://travis-ci.org/WeTransfer/apiculture.svg?branch=master)](https://travis-ci.org/WeTransfer/apiculture)
5
+ [![CI](https://github.com/WeTransfer/apiculture/actions/workflows/ci.yml/badge.svg)](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
- if s.respond_to?(:metadata)
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
- 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
- }
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)
@@ -1,3 +1,3 @@
1
1
  module Apiculture
2
- VERSION = '0.2.0'.freeze
2
+ VERSION = '0.2.2'.freeze
3
3
  end
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
- @apiculture_actions_and_docs ||= []
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.0
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: 2023-02-08 00:00:00.000000000 Z
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.2.33
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
@@ -1,13 +0,0 @@
1
- gemfile:
2
- - gemfiles/Gemfile.rack-1.x
3
- - gemfiles/Gemfile.rack-2.x
4
- rvm:
5
- - 2.3
6
- - 2.4
7
- - 2.5
8
- - 2.6
9
- - 2.7
10
- - 3.0
11
- - 3.2
12
- sudo: false
13
- cache: bundler
@@ -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
@@ -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
@@ -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