openapi_blocks 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 +7 -0
- data/CHANGELOG.md +86 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +304 -0
- data/README.pt-BR.md +495 -0
- data/Rakefile +12 -0
- data/app/controllers/openapi_blocks/spec_controller.rb +110 -0
- data/config/routes.rb +7 -0
- data/lib/openapi_blocks/base.rb +52 -0
- data/lib/openapi_blocks/builder.rb +23 -0
- data/lib/openapi_blocks/cache.rb +28 -0
- data/lib/openapi_blocks/configuration/contact_builder.rb +23 -0
- data/lib/openapi_blocks/configuration/info_builder.rb +42 -0
- data/lib/openapi_blocks/configuration/license_builder.rb +19 -0
- data/lib/openapi_blocks/configuration/security_builder.rb +33 -0
- data/lib/openapi_blocks/configuration/server_builder.rb +19 -0
- data/lib/openapi_blocks/configuration/servers_builder.rb +21 -0
- data/lib/openapi_blocks/configuration.rb +55 -0
- data/lib/openapi_blocks/engine.rb +13 -0
- data/lib/openapi_blocks/file_watcher.rb +42 -0
- data/lib/openapi_blocks/middleware.rb +54 -0
- data/lib/openapi_blocks/operation_builder.rb +57 -0
- data/lib/openapi_blocks/railtie.rb +19 -0
- data/lib/openapi_blocks/routing/extractor.rb +187 -0
- data/lib/openapi_blocks/routing/operation.rb +45 -0
- data/lib/openapi_blocks/schema/extractor.rb +103 -0
- data/lib/openapi_blocks/schema/types.rb +52 -0
- data/lib/openapi_blocks/schema/validator.rb +86 -0
- data/lib/openapi_blocks/spec/components.rb +67 -0
- data/lib/openapi_blocks/spec/document.rb +47 -0
- data/lib/openapi_blocks/spec/paths.rb +17 -0
- data/lib/openapi_blocks/version.rb +5 -0
- data/lib/openapi_blocks.rb +35 -0
- data/sig/openapi_blocks.rbs +4 -0
- metadata +177 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiBlocks
|
|
4
|
+
class Cache # rubocop:disable Style/Documentation
|
|
5
|
+
def initialize
|
|
6
|
+
@store = {}
|
|
7
|
+
@mutex = Mutex.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get(key)
|
|
11
|
+
@store[key]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def set(key, value)
|
|
15
|
+
@mutex.synchronize { @store[key] = value }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def invalidate!(key = nil)
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
key ? @store.delete(key) : @store.clear
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def cached?(key)
|
|
25
|
+
@store.key?(key)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiBlocks
|
|
4
|
+
class Configuration
|
|
5
|
+
class ContactBuilder # rubocop:disable Style/Documentation
|
|
6
|
+
def name(value = nil)
|
|
7
|
+
value ? @name = value : @name
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def email(value = nil)
|
|
11
|
+
value ? @email = value : @email
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def url(value = nil)
|
|
15
|
+
value ? @url = value : @url
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
{ name: @name, email: @email, url: @url }.compact
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "contact_builder"
|
|
4
|
+
require_relative "license_builder"
|
|
5
|
+
|
|
6
|
+
module OpenapiBlocks
|
|
7
|
+
class Configuration
|
|
8
|
+
class InfoBuilder # rubocop:disable Style/Documentation
|
|
9
|
+
def title(value = nil)
|
|
10
|
+
value ? @title = value : @title
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def version(value = nil)
|
|
14
|
+
value ? @version = value : @version
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def description(value = nil)
|
|
18
|
+
value ? @description = value : @description
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def contact(&block)
|
|
22
|
+
@contact = ContactBuilder.new
|
|
23
|
+
@contact.instance_eval(&block) if block
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def license(&block)
|
|
27
|
+
@license = LicenseBuilder.new
|
|
28
|
+
@license.instance_eval(&block) if block
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
{
|
|
33
|
+
title: @title,
|
|
34
|
+
version: @version,
|
|
35
|
+
description: @description,
|
|
36
|
+
contact: @contact&.to_h,
|
|
37
|
+
license: @license&.to_h
|
|
38
|
+
}.compact
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiBlocks
|
|
4
|
+
class Configuration
|
|
5
|
+
class LicenseBuilder # rubocop:disable Style/Documentation
|
|
6
|
+
def name(value = nil)
|
|
7
|
+
value ? @name = value : @name
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def url(value = nil)
|
|
11
|
+
value ? @url = value : @url
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
{ name: @name, url: @url }.compact
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiBlocks
|
|
4
|
+
class Configuration
|
|
5
|
+
class SecurityBuilder # rubocop:disable Style/Documentation
|
|
6
|
+
attr_reader :schemes
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@schemes = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def bearer_token(format: "JWT")
|
|
13
|
+
@schemes[:bearerAuth] = {
|
|
14
|
+
type: "http",
|
|
15
|
+
scheme: "bearer",
|
|
16
|
+
bearerFormat: format
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def api_key(name: "X-API-Key", in: :header)
|
|
21
|
+
@schemes[:apiKey] = {
|
|
22
|
+
type: "apiKey",
|
|
23
|
+
name: name,
|
|
24
|
+
in: binding.local_variable_get(:in).to_s
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
@schemes
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiBlocks
|
|
4
|
+
class Configuration
|
|
5
|
+
class ServerBuilder # rubocop:disable Style/Documentation
|
|
6
|
+
def url(value = nil)
|
|
7
|
+
value ? @url = value : @url
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def description(value = nil)
|
|
11
|
+
value ? @description = value : @description
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
{ url: @url, description: @description }.compact
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "server_builder"
|
|
4
|
+
|
|
5
|
+
module OpenapiBlocks
|
|
6
|
+
class Configuration
|
|
7
|
+
class ServersBuilder # rubocop:disable Style/Documentation
|
|
8
|
+
attr_reader :servers
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@servers = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def server(&block)
|
|
15
|
+
s = ServerBuilder.new
|
|
16
|
+
s.instance_eval(&block) if block
|
|
17
|
+
@servers << s
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "configuration/info_builder"
|
|
4
|
+
require_relative "configuration/servers_builder"
|
|
5
|
+
require_relative "configuration/security_builder"
|
|
6
|
+
|
|
7
|
+
module OpenapiBlocks
|
|
8
|
+
class Configuration # rubocop:disable Style/Documentation
|
|
9
|
+
SUPPORTED_VERSIONS = %w[3.1.0 3.0.3].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :openapi_version
|
|
12
|
+
attr_accessor :watch
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@openapi_version = "3.1.0"
|
|
16
|
+
@watch = :development
|
|
17
|
+
@info = InfoBuilder.new
|
|
18
|
+
@servers = []
|
|
19
|
+
@security = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def openapi_version=(version)
|
|
23
|
+
unless SUPPORTED_VERSIONS.include?(version.to_s)
|
|
24
|
+
raise ArgumentError,
|
|
25
|
+
"Unsupported OpenAPI version: #{version.inspect}. Supported versions: #{SUPPORTED_VERSIONS.join(', ')}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@openapi_version = version.to_s
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def info(&block)
|
|
32
|
+
@info.instance_eval(&block) if block
|
|
33
|
+
@info
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def servers(&block)
|
|
37
|
+
builder = ServersBuilder.new
|
|
38
|
+
builder.instance_eval(&block) if block
|
|
39
|
+
@servers = builder.servers
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def security(&block)
|
|
43
|
+
@security ||= SecurityBuilder.new
|
|
44
|
+
@security.instance_eval(&block) if block
|
|
45
|
+
@security
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_h
|
|
49
|
+
{
|
|
50
|
+
info: @info.to_h,
|
|
51
|
+
servers: @servers.map(&:to_h)
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
|
|
5
|
+
module OpenapiBlocks
|
|
6
|
+
class Engine < Rails::Engine # rubocop:disable Style/Documentation
|
|
7
|
+
isolate_namespace OpenapiBlocks
|
|
8
|
+
|
|
9
|
+
engine_name "openapi_blocks"
|
|
10
|
+
|
|
11
|
+
# config.autoload_paths << File.expand_path("app/controllers", __dir__)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiBlocks
|
|
4
|
+
class FileWatcher # rubocop:disable Style/Documentation
|
|
5
|
+
WATCH_PATTERNS = [
|
|
6
|
+
"app/openapi/**/*.rb",
|
|
7
|
+
"app/models/**/*.rb",
|
|
8
|
+
"config/routes.rb",
|
|
9
|
+
"db/schema.rb"
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(root_path)
|
|
13
|
+
@root_path = root_path
|
|
14
|
+
@mtimes = {}
|
|
15
|
+
snapshot!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def stale?
|
|
19
|
+
watched_files.any? do |file|
|
|
20
|
+
mtime(file) != @mtimes[file]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def snapshot!
|
|
25
|
+
watched_files.each do |file|
|
|
26
|
+
@mtimes[file] = mtime(file)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def watched_files
|
|
33
|
+
WATCH_PATTERNS.flat_map do |pattern|
|
|
34
|
+
Dir.glob(File.join(@root_path, pattern))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mtime(file)
|
|
39
|
+
File.exist?(file) ? File.mtime(file) : nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "file_watcher"
|
|
4
|
+
require_relative "cache"
|
|
5
|
+
|
|
6
|
+
module OpenapiBlocks
|
|
7
|
+
class Middleware # rubocop:disable Style/Documentation
|
|
8
|
+
CACHE_KEY = :openapi_spec
|
|
9
|
+
|
|
10
|
+
def initialize(app)
|
|
11
|
+
@app = app
|
|
12
|
+
@cache = Cache.new
|
|
13
|
+
@file_watcher = FileWatcher.new(root_path)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(env)
|
|
17
|
+
@app.call(env)
|
|
18
|
+
ensure
|
|
19
|
+
invalidate_if_stale!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def invalidate_if_stale!
|
|
25
|
+
return unless watch_enabled?
|
|
26
|
+
return unless @file_watcher.stale?
|
|
27
|
+
|
|
28
|
+
@cache.invalidate!(CACHE_KEY)
|
|
29
|
+
@file_watcher.snapshot!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def spec
|
|
33
|
+
@cache.set(CACHE_KEY, Builder.build) unless @cache.cached?(CACHE_KEY)
|
|
34
|
+
|
|
35
|
+
@cache.get(CACHE_KEY)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def watch_enabled?
|
|
39
|
+
watched_envs.include?(current_env)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def watched_envs
|
|
43
|
+
Array(OpenapiBlocks.configuration.watch)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def current_env
|
|
47
|
+
Rails.env.to_sym
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def root_path
|
|
51
|
+
Rails.root.to_s
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiBlocks
|
|
4
|
+
class OperationBuilder # rubocop:disable Style/Documentation
|
|
5
|
+
attr_reader :_summary, :_description, :_parameters, :_responses, :_security, :_tags
|
|
6
|
+
|
|
7
|
+
def summary(value = nil)
|
|
8
|
+
value ? @_summary = value : @_summary
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def description(value = nil)
|
|
12
|
+
value ? @_description = value : @_description
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def tags(*values)
|
|
16
|
+
values.any? ? @_tags = values : @_tags
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parameter(name, in:, type:, description: nil, required: false)
|
|
20
|
+
@_parameters ||= []
|
|
21
|
+
@_parameters << {
|
|
22
|
+
name: name,
|
|
23
|
+
in: binding.local_variable_get(:in),
|
|
24
|
+
type: type,
|
|
25
|
+
description: description,
|
|
26
|
+
required: required
|
|
27
|
+
}.compact
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def response(status, description:, schema: nil)
|
|
31
|
+
@_responses ||= {}
|
|
32
|
+
@_responses[status.to_s] = {
|
|
33
|
+
description: description,
|
|
34
|
+
schema: schema
|
|
35
|
+
}.compact
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def security(*schemes)
|
|
39
|
+
@_security = schemes.map { |s| { s => [] } }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def no_security!
|
|
43
|
+
@_security = []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_h
|
|
47
|
+
{
|
|
48
|
+
summary: @_summary,
|
|
49
|
+
description: @_description,
|
|
50
|
+
parameters: @_parameters,
|
|
51
|
+
responses: @_responses,
|
|
52
|
+
security: @_security,
|
|
53
|
+
tags: @_tags
|
|
54
|
+
}.compact
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
|
|
5
|
+
module OpenapiBlocks
|
|
6
|
+
class Railtie < Rails::Railtie # rubocop:disable Style/Documentation
|
|
7
|
+
initializer "openapi_blocks.middleware" do |app|
|
|
8
|
+
app.middleware.use OpenapiBlocks::Middleware
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer "openapi_blocks.autoload", before: :set_autoload_paths do |app|
|
|
12
|
+
app.config.eager_load_paths << app.root.join("app/openapi")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
config.to_prepare do
|
|
16
|
+
Dir[Rails.root.join("app/openapi/**/*.rb")].each { |f| require f }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "operation"
|
|
4
|
+
|
|
5
|
+
module OpenapiBlocks
|
|
6
|
+
module Routing
|
|
7
|
+
class Extractor # rubocop:disable Style/Documentation,Metrics/ClassLength
|
|
8
|
+
IGNORED_CONTROLLERS = %w[
|
|
9
|
+
rails/
|
|
10
|
+
action_mailbox/
|
|
11
|
+
active_storage/
|
|
12
|
+
openapi_blocks/
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(app = Rails.application)
|
|
16
|
+
@app = app
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def extract
|
|
20
|
+
routes.each_with_object({}) do |operation, hash|
|
|
21
|
+
next unless operation.valid?
|
|
22
|
+
|
|
23
|
+
hash[operation.path] ||= {}
|
|
24
|
+
|
|
25
|
+
operation.verbs.each do |verb|
|
|
26
|
+
hash[operation.path][verb] = build_operation(operation)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def routes
|
|
34
|
+
@app.routes.routes.filter_map do |route|
|
|
35
|
+
defaults = route.defaults
|
|
36
|
+
next unless defaults[:controller] && defaults[:action]
|
|
37
|
+
next if IGNORED_CONTROLLERS.any? { |c| defaults[:controller].start_with?(c) }
|
|
38
|
+
|
|
39
|
+
Operation.new(
|
|
40
|
+
controller: defaults[:controller],
|
|
41
|
+
action: defaults[:action],
|
|
42
|
+
path: route.path.spec.to_s
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_operation(operation) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity
|
|
48
|
+
meta = operation_meta(operation)
|
|
49
|
+
parameters = build_parameters(operation, meta)
|
|
50
|
+
tags = build_tags(operation, meta)
|
|
51
|
+
|
|
52
|
+
op = {
|
|
53
|
+
tags: tags,
|
|
54
|
+
summary: meta&._summary || build_summary(operation),
|
|
55
|
+
operationId: build_operation_id(operation),
|
|
56
|
+
responses: meta&._responses ? build_custom_responses(meta) : build_default_responses(operation)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
op[:description] = meta._description if meta&._description
|
|
60
|
+
op[:parameters] = parameters if parameters.any?
|
|
61
|
+
op[:requestBody] = build_request_body(operation) if operation.has_body
|
|
62
|
+
op[:security] = meta._security if meta&._security
|
|
63
|
+
|
|
64
|
+
op
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_tags(operation, meta)
|
|
68
|
+
return meta._tags if meta&._tags&.any?
|
|
69
|
+
|
|
70
|
+
openapi_class = find_openapi_class(operation)
|
|
71
|
+
return openapi_class._tags if openapi_class&._tags&.any?
|
|
72
|
+
|
|
73
|
+
[operation.schema_name]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def build_summary(operation)
|
|
77
|
+
"#{operation.action.humanize} #{operation.schema_name}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_operation_id(operation)
|
|
81
|
+
"#{operation.action}#{operation.schema_name}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_parameters(operation, meta)
|
|
85
|
+
params = build_path_parameters(operation)
|
|
86
|
+
params += build_query_parameters(meta) if meta&._parameters
|
|
87
|
+
params
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_path_parameters(operation)
|
|
91
|
+
operation.path_parameters.map do |param|
|
|
92
|
+
{
|
|
93
|
+
name: param,
|
|
94
|
+
in: "path",
|
|
95
|
+
required: true,
|
|
96
|
+
schema: { type: "string" }
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def build_query_parameters(meta)
|
|
102
|
+
meta._parameters.map do |param|
|
|
103
|
+
{
|
|
104
|
+
name: param[:name],
|
|
105
|
+
in: param[:in].to_s,
|
|
106
|
+
required: param[:required] || false,
|
|
107
|
+
description: param[:description],
|
|
108
|
+
schema: { type: param[:type].to_s }
|
|
109
|
+
}.compact
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def build_default_responses(operation) # rubocop:disable Metrics/MethodLength
|
|
114
|
+
status = operation.action == "create" ? "201" : "200"
|
|
115
|
+
ref = { "$ref" => "#/components/schemas/#{operation.schema_name}" }
|
|
116
|
+
|
|
117
|
+
responses = {
|
|
118
|
+
status => {
|
|
119
|
+
description: "#{operation.action.humanize} #{operation.schema_name}",
|
|
120
|
+
content: {
|
|
121
|
+
"application/json" => {
|
|
122
|
+
schema: operation.action == "index" ? { type: "array", items: ref } : ref
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
responses["422"] = { description: "Unprocessable entity" } if operation.has_body
|
|
129
|
+
responses["404"] = { description: "Not found" } if %w[show update destroy].include?(operation.action)
|
|
130
|
+
|
|
131
|
+
responses
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def build_custom_responses(meta)
|
|
135
|
+
meta._responses.each_with_object({}) do |(status, response), hash|
|
|
136
|
+
hash[status.to_s] = build_response_object(response)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def build_response_object(response)
|
|
141
|
+
obj = { description: response[:description] }
|
|
142
|
+
return obj unless response[:schema]
|
|
143
|
+
|
|
144
|
+
obj[:content] = {
|
|
145
|
+
"application/json" => {
|
|
146
|
+
schema: resolve_schema(response[:schema])
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
obj
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def resolve_schema(schema)
|
|
153
|
+
case schema
|
|
154
|
+
in { type: :array, items: Symbol => ref }
|
|
155
|
+
{ type: "array", items: { "$ref" => "#/components/schemas/#{ref.to_s.classify}" } }
|
|
156
|
+
in Symbol => ref
|
|
157
|
+
{ "$ref" => "#/components/schemas/#{ref.to_s.classify}" }
|
|
158
|
+
else
|
|
159
|
+
schema
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def build_request_body(operation)
|
|
164
|
+
ref = { "$ref" => "#/components/schemas/#{operation.schema_name}Input" }
|
|
165
|
+
|
|
166
|
+
{
|
|
167
|
+
required: true,
|
|
168
|
+
content: {
|
|
169
|
+
"application/json" => { schema: ref }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def operation_meta(operation)
|
|
175
|
+
openapi_class = find_openapi_class(operation)
|
|
176
|
+
openapi_class&._operations&.dig(operation.action.to_sym)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def find_openapi_class(operation)
|
|
180
|
+
openapi_name = "#{operation.schema_name}Openapi"
|
|
181
|
+
Object.const_get(openapi_name)
|
|
182
|
+
rescue NameError
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiBlocks
|
|
4
|
+
module Routing
|
|
5
|
+
class Operation # rubocop:disable Style/Documentation
|
|
6
|
+
ACTIONS_MAP = {
|
|
7
|
+
"index" => { verbs: ["get"], has_body: false },
|
|
8
|
+
"show" => { verbs: ["get"], has_body: false },
|
|
9
|
+
"create" => { verbs: ["post"], has_body: true },
|
|
10
|
+
"update" => { verbs: %w[put patch], has_body: true },
|
|
11
|
+
"destroy" => { verbs: ["delete"], has_body: false }
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :verbs, :path, :action, :controller, :has_body
|
|
15
|
+
|
|
16
|
+
def initialize(route)
|
|
17
|
+
@controller = route[:controller]
|
|
18
|
+
@action = route[:action]
|
|
19
|
+
@path = normalize_path(route[:path])
|
|
20
|
+
@verbs = ACTIONS_MAP.dig(@action, :verbs)
|
|
21
|
+
@has_body = ACTIONS_MAP.dig(@action, :has_body)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def valid?
|
|
25
|
+
ACTIONS_MAP.key?(@action) && @verbs.present?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def path_parameters
|
|
29
|
+
@path.scan(/\{(\w+)\}/).flatten
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def schema_name
|
|
33
|
+
@controller.split("/").last.classify
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def normalize_path(path)
|
|
39
|
+
path
|
|
40
|
+
.gsub("(.:format)", "")
|
|
41
|
+
.gsub(/:(\w+)/, '{\1}')
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|