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 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