tomyum 0.1.0.a
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/README.md +1106 -0
- data/lib/tomyum/assertions.rb +80 -0
- data/lib/tomyum/attributes/array.rb +11 -0
- data/lib/tomyum/attributes/attribute.rb +130 -0
- data/lib/tomyum/attributes/boolean.rb +22 -0
- data/lib/tomyum/attributes/currency.rb +19 -0
- data/lib/tomyum/attributes/date.rb +11 -0
- data/lib/tomyum/attributes/float.rb +13 -0
- data/lib/tomyum/attributes/integer.rb +14 -0
- data/lib/tomyum/attributes/ip_address.rb +15 -0
- data/lib/tomyum/attributes/number.rb +24 -0
- data/lib/tomyum/attributes/object.rb +71 -0
- data/lib/tomyum/attributes/schema.rb +23 -0
- data/lib/tomyum/attributes/string.rb +36 -0
- data/lib/tomyum/attributes/time.rb +19 -0
- data/lib/tomyum/attributes/uri.rb +19 -0
- data/lib/tomyum/attributes/visitor.rb +136 -0
- data/lib/tomyum/attributes.rb +92 -0
- data/lib/tomyum/endpoint.rb +102 -0
- data/lib/tomyum/endpoints/method.rb +90 -0
- data/lib/tomyum/endpoints/params.rb +115 -0
- data/lib/tomyum/error.rb +17 -0
- data/lib/tomyum/functions.rb +49 -0
- data/lib/tomyum/generators/generator.rb +16 -0
- data/lib/tomyum/generators/grpc/generator.rb +10 -0
- data/lib/tomyum/generators/open_api/generator.rb +205 -0
- data/lib/tomyum/generators/open_api/property_generator.rb +111 -0
- data/lib/tomyum/generators.rb +3 -0
- data/lib/tomyum/registry.rb +75 -0
- data/lib/tomyum/resolvable.rb +11 -0
- data/lib/tomyum/resolver.rb +99 -0
- data/lib/tomyum/serializer.rb +125 -0
- data/lib/tomyum/serializers/serializable.rb +23 -0
- data/lib/tomyum/server/app.rb +33 -0
- data/lib/tomyum/server/document.rb +20 -0
- data/lib/tomyum/server/documents/redoc.rb +36 -0
- data/lib/tomyum/server/documents/swagger.rb +47 -0
- data/lib/tomyum/server/routes.rb +0 -0
- data/lib/tomyum/support.rb +13 -0
- data/lib/tomyum/validator.rb +205 -0
- data/lib/tomyum/validators/normalizable.rb +24 -0
- data/lib/tomyum/validators/proxy.rb +77 -0
- data/lib/tomyum/validators/validatable.rb +48 -0
- data/lib/tomyum/version.rb +3 -0
- data/lib/tomyum.rb +28 -0
- metadata +202 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
require_relative "registry"
|
2
|
+
require_relative "attributes/visitor"
|
3
|
+
require_relative "serializers/serializable"
|
4
|
+
require_relative "validators/normalizable"
|
5
|
+
require_relative "validators/validatable"
|
6
|
+
|
7
|
+
# Attributes
|
8
|
+
require_relative "attributes/boolean"
|
9
|
+
require_relative "attributes/string"
|
10
|
+
require_relative "attributes/number"
|
11
|
+
require_relative "attributes/integer"
|
12
|
+
require_relative "attributes/float"
|
13
|
+
require_relative "attributes/array"
|
14
|
+
require_relative "attributes/object"
|
15
|
+
require_relative "attributes/schema"
|
16
|
+
require_relative "attributes/date"
|
17
|
+
require_relative "attributes/time"
|
18
|
+
require_relative "attributes/currency"
|
19
|
+
require_relative "attributes/uri"
|
20
|
+
require_relative "attributes/ip_address"
|
21
|
+
|
22
|
+
module Tomyum
|
23
|
+
# Attributes registry holds the references to all attributes
|
24
|
+
#
|
25
|
+
# @see .create
|
26
|
+
module Attributes
|
27
|
+
include Tomyum::Assertions
|
28
|
+
KeyError = Tomyum::Error.create "Attribute %s already exists"
|
29
|
+
|
30
|
+
@registry = Registry.new({}, error: KeyError) do |id, name, options, block|
|
31
|
+
attribute = get(id)
|
32
|
+
attribute.new(name, options, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
# Holds reference to all attribute objects
|
37
|
+
#
|
38
|
+
# @return [Registry]
|
39
|
+
attr_accessor :registry
|
40
|
+
|
41
|
+
# Creates new attribute (Delegates to Registry#create).
|
42
|
+
#
|
43
|
+
# @return [Attribute]
|
44
|
+
#
|
45
|
+
# @example Creates primitive attribute
|
46
|
+
# Attributes.create(:string, :username, required: true)
|
47
|
+
#
|
48
|
+
# @example Creates compound attribute
|
49
|
+
# Attributes.create(:object, :user) do
|
50
|
+
# string :username
|
51
|
+
# end
|
52
|
+
def create(type, name, options = {}, &block)
|
53
|
+
registry.create(type, name, options, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Registers new Attribute (alias for Registry#register)
|
57
|
+
#
|
58
|
+
# @example
|
59
|
+
# register(:custom, CustomAttribute)
|
60
|
+
def register(type, klass, override: false)
|
61
|
+
registry.register(type, klass, override: override)
|
62
|
+
end
|
63
|
+
|
64
|
+
def key?(key)
|
65
|
+
registry.key?(key)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
ALL = {
|
70
|
+
string: String,
|
71
|
+
number: Number,
|
72
|
+
integer: Integer,
|
73
|
+
float: Float,
|
74
|
+
boolean: Boolean,
|
75
|
+
object: Object,
|
76
|
+
array: Array,
|
77
|
+
schema: Schema,
|
78
|
+
time: Time,
|
79
|
+
date: Date,
|
80
|
+
uri: URI,
|
81
|
+
currency: Currency,
|
82
|
+
ip_address: IPAddress,
|
83
|
+
}.freeze
|
84
|
+
|
85
|
+
SCALAR_TYPES = %i[string number integer float boolean].freeze
|
86
|
+
NATIVE_TYPES = SCALAR_TYPES + %i[array object].freeze
|
87
|
+
NON_SCALAR_TYPES = ALL.keys - SCALAR_TYPES
|
88
|
+
|
89
|
+
# register pre-defined attributes
|
90
|
+
ALL.each { |name, attr| register(name, attr) }
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require_relative "endpoints/params"
|
2
|
+
require_relative "endpoints/method"
|
3
|
+
|
4
|
+
module Tomyum
|
5
|
+
class Endpoint
|
6
|
+
include Tomyum::Assertions
|
7
|
+
|
8
|
+
# See `docs/api/endpoint-validations.md`.
|
9
|
+
HTTP_METHODS_WITHOUT_BODY = %i[get head trace]
|
10
|
+
HTTP_METHODS_WITH_BODY = %i[post put patch delete]
|
11
|
+
HTTP_METHODS = HTTP_METHODS_WITHOUT_BODY + HTTP_METHODS_WITH_BODY
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# Converts `/users/:id/friends` to `users_id_friends`
|
15
|
+
def normalize_name(name)
|
16
|
+
name.to_s.delete(":").gsub(/(\A\/+|\/+\z)/, '').tr('/', '_').to_sym
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
DEFAULT_OPTIONS = {
|
21
|
+
schema: nil,
|
22
|
+
title: nil,
|
23
|
+
description: nil,
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
attr_reader :path, :name, :schema
|
27
|
+
attr_accessor :title, :description
|
28
|
+
|
29
|
+
def initialize(path, options = {}, &block)
|
30
|
+
options = DEFAULT_OPTIONS.merge(options)
|
31
|
+
@schema = options[:schema]
|
32
|
+
@path = path
|
33
|
+
@name = normalize_name(options[:name])
|
34
|
+
@methods = {}
|
35
|
+
@params = nil
|
36
|
+
@block = block
|
37
|
+
@loaded = false
|
38
|
+
@title = options[:title]
|
39
|
+
@description = options[:description]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Defines http methods dynamically e.g. :get, :post ...
|
43
|
+
# endpoint.define do
|
44
|
+
# get :index, path: "/"
|
45
|
+
# post :create
|
46
|
+
# end
|
47
|
+
HTTP_METHODS.each do |method|
|
48
|
+
define_method method do |action, path: "/", **options|
|
49
|
+
create_method(method, action, path: path, **options)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Lazily builds all params and return methods hash
|
54
|
+
def methods(resolver = nil)
|
55
|
+
ensure_loaded
|
56
|
+
|
57
|
+
@methods.each { |name, _| method(name, resolver: resolver) }
|
58
|
+
end
|
59
|
+
|
60
|
+
# Lazily builds params for speicified method
|
61
|
+
def method(name, resolver: nil)
|
62
|
+
ensure_loaded
|
63
|
+
assert_in @methods, name.to_sym
|
64
|
+
|
65
|
+
@methods[name.to_sym].tap { |method| method.build_params(params, resolver) }
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns default params
|
69
|
+
def params(&block)
|
70
|
+
ensure_loaded
|
71
|
+
|
72
|
+
@params ||= Tomyum::Endpoints::Params.new(:default, schema: schema, built: true, &block || -> {})
|
73
|
+
end
|
74
|
+
|
75
|
+
# Registers http verb method
|
76
|
+
#
|
77
|
+
# create_method(:post, :create, path: "/")
|
78
|
+
def create_method(verb, action, path: "/", **options)
|
79
|
+
# Automatically adds it self as schema value
|
80
|
+
options[:schema] = options.fetch(:schema, schema)
|
81
|
+
|
82
|
+
@methods[action.to_sym] = Tomyum::Endpoints::Method.new(verb, action: action, path: path, **options)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def normalize_name(name)
|
88
|
+
(name || @schema || self.class.normalize_name(@path)).to_sym
|
89
|
+
end
|
90
|
+
|
91
|
+
# Lazily executes &block
|
92
|
+
def ensure_loaded
|
93
|
+
return if @loaded
|
94
|
+
|
95
|
+
# We need to set status here; otherwise, we'll have
|
96
|
+
# error when there's nested block.
|
97
|
+
@loaded = true
|
98
|
+
|
99
|
+
instance_exec(&@block) if @block
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Tomyum
|
2
|
+
module Endpoints
|
3
|
+
# Represents HTTP method
|
4
|
+
#
|
5
|
+
# @example Create new +POST+ {Method}
|
6
|
+
# Method.new(:post, action: :create, path: "/", schema: :user, as: :list)
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# Method.new(:post, {
|
10
|
+
# action: :create,
|
11
|
+
# path: "/",
|
12
|
+
# validators: [CustomValidator]
|
13
|
+
# })
|
14
|
+
class Method
|
15
|
+
DEFAULT_OPTIONS = {
|
16
|
+
as: nil,
|
17
|
+
params: [],
|
18
|
+
exclude: false,
|
19
|
+
schema: nil,
|
20
|
+
status: 200,
|
21
|
+
title: nil,
|
22
|
+
description: nil,
|
23
|
+
validators: [],
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
attr_reader :verb, :path, :action, :params, :schema, :status, :as
|
27
|
+
attr_accessor :title, :description
|
28
|
+
|
29
|
+
def initialize(verb, action:, path: "/", **options)
|
30
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
31
|
+
@verb = verb
|
32
|
+
@path = path
|
33
|
+
@action = action
|
34
|
+
|
35
|
+
# Create ParamsAttribute instance
|
36
|
+
@params = create_params(action, **options.slice(:params, :schema, :exclude))
|
37
|
+
@schema = @options[:schema]
|
38
|
+
@as = @options[:as]
|
39
|
+
@status = @options[:status]
|
40
|
+
@title = @options[:title]
|
41
|
+
@description = @options[:description]
|
42
|
+
@validator = nil
|
43
|
+
@validators = @options[:validators]
|
44
|
+
@resolver = nil
|
45
|
+
@result = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate(params = {}, resolver = nil)
|
49
|
+
@validator = create_validator(@resolver || resolver)
|
50
|
+
@result = @validator.validate(@params, params, validators: @validators)
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid?(*args)
|
54
|
+
validate(*args)
|
55
|
+
|
56
|
+
@result&.errors&.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
def errors
|
60
|
+
@result&.errors
|
61
|
+
end
|
62
|
+
|
63
|
+
# Does it have request body? (only non GET verb can have request body)
|
64
|
+
def body?
|
65
|
+
Tomyum::Endpoint::HTTP_METHODS_WITH_BODY.include?(@verb) && @params.keys.any?
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_params(default = {}, resolver = nil)
|
69
|
+
@resolver = resolver if resolver
|
70
|
+
@params = @params.build(default, resolver)
|
71
|
+
end
|
72
|
+
|
73
|
+
def permit_params(params = {}, target = nil)
|
74
|
+
return params unless params.respond_to?(:permit)
|
75
|
+
|
76
|
+
params.permit(*@params.permit(params))
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def create_params(action, **options)
|
82
|
+
Tomyum::Endpoints::Params.new(action, **options)
|
83
|
+
end
|
84
|
+
|
85
|
+
def create_validator(*args)
|
86
|
+
Tomyum::Validator.new(*args)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Tomyum
|
2
|
+
module Endpoints
|
3
|
+
# Usage
|
4
|
+
#
|
5
|
+
# Params.new(:create)
|
6
|
+
# Params.new(:index, params: :list)
|
7
|
+
# Params.new(:create, params: %i[amount currency])
|
8
|
+
# Params.new(:create, params: %i[amount], exclude: true)
|
9
|
+
# Params.new(:create, params: false)
|
10
|
+
# Params.new :create, params: -> {
|
11
|
+
# integer :amount
|
12
|
+
# }
|
13
|
+
# Params.new(:index, schema: :user, as: :list)
|
14
|
+
#
|
15
|
+
class Params < Attributes::Object
|
16
|
+
include Tomyum::Assertions
|
17
|
+
include Tomyum::Resolvable
|
18
|
+
|
19
|
+
InvalidType = Tomyum::Error.create("Invalid params type")
|
20
|
+
NotBuilt = Tomyum::Error.create("Params need to be built first")
|
21
|
+
|
22
|
+
DEFAULT_OPTIONS = {
|
23
|
+
exclude: false,
|
24
|
+
schema: nil,
|
25
|
+
built: false,
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
attr_reader :action, :params, :schema
|
29
|
+
|
30
|
+
def initialize(action, params: false, **options, &block)
|
31
|
+
super(action, options.except(DEFAULT_OPTIONS.keys))
|
32
|
+
|
33
|
+
options = DEFAULT_OPTIONS.merge(options)
|
34
|
+
@action = action.to_sym
|
35
|
+
@exclude = options[:exclude]
|
36
|
+
@schema = options[:schema]
|
37
|
+
@params = block_given? ? block : params
|
38
|
+
@built = options[:built]
|
39
|
+
end
|
40
|
+
|
41
|
+
def build(default = {}, resolver = nil)
|
42
|
+
# Flag as built
|
43
|
+
@built = true
|
44
|
+
|
45
|
+
case @params
|
46
|
+
when Params
|
47
|
+
@params
|
48
|
+
when true
|
49
|
+
# use default params
|
50
|
+
default
|
51
|
+
when false
|
52
|
+
# use no params
|
53
|
+
self
|
54
|
+
when Proc
|
55
|
+
instance_exec(&@params)
|
56
|
+
self
|
57
|
+
when Symbol
|
58
|
+
# use params from other service
|
59
|
+
resolver.endpoint(@params).params
|
60
|
+
when Array
|
61
|
+
assert_subset_of(default.keys, @params)
|
62
|
+
|
63
|
+
# override default params by using specified symbol
|
64
|
+
keys = @exclude ? default.keys - @params : @params
|
65
|
+
keys.each { |name| self[name] = default[name] }
|
66
|
+
self
|
67
|
+
else
|
68
|
+
raise InvalidType
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def built?
|
73
|
+
@built
|
74
|
+
end
|
75
|
+
|
76
|
+
# Build permitted params for ActionController::Params
|
77
|
+
def permit(params = {})
|
78
|
+
raise NotBuilt unless built?
|
79
|
+
|
80
|
+
build_permitted_params(attributes, params)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def build_permitted_params(attrs, params = {})
|
86
|
+
attrs.map do |name, attr|
|
87
|
+
# permit array
|
88
|
+
# TODO: params.permit(:foo, array: [:key1, :key2])
|
89
|
+
next { name => [] } if attr.kind_of?(Tomyum::Attributes::Array)
|
90
|
+
|
91
|
+
# permit non-object
|
92
|
+
next name unless attr.kind_of?(Tomyum::Attributes::Object)
|
93
|
+
|
94
|
+
# Rails 5.0 doesn't support arbitary hash params
|
95
|
+
# 5.1+ supports this, via `key: {}` empty hash.
|
96
|
+
#
|
97
|
+
# The work around is to get hash keys from params
|
98
|
+
# and assign them to permit keys
|
99
|
+
# params = params&.fetch(name, {})
|
100
|
+
data = params&.fetch(name, {})
|
101
|
+
keys = build_permitted_params(attr.attributes, data)
|
102
|
+
|
103
|
+
{ attr.name => normalize_permitted_params(keys, data) }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def normalize_permitted_params(keys, params = {})
|
108
|
+
return keys unless keys.empty?
|
109
|
+
return {} if params.empty?
|
110
|
+
|
111
|
+
params.keys
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/tomyum/error.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Tomyum
|
2
|
+
# @example
|
3
|
+
# InvaldKey = Error.create("Invalid %s key")
|
4
|
+
# raise InvalidKey, "id"
|
5
|
+
class Error < StandardError
|
6
|
+
def self.create(msg)
|
7
|
+
Class.new(Error) do
|
8
|
+
MESSAGE.replace msg
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
MESSAGE = "Error"
|
13
|
+
def initialize(*args)
|
14
|
+
super args.any? ? MESSAGE % args : MESSAGE
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Tomyum
|
2
|
+
# FP helpers
|
3
|
+
module Functions
|
4
|
+
class Result
|
5
|
+
attr_reader :value, :errors
|
6
|
+
|
7
|
+
def initialize(value, errors = nil)
|
8
|
+
@value = value
|
9
|
+
@errors = errors
|
10
|
+
end
|
11
|
+
|
12
|
+
def compose(f)
|
13
|
+
return self if errors
|
14
|
+
|
15
|
+
f.(value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def result(value, errors = nil)
|
20
|
+
Result.new(value, errors)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Compose functions
|
24
|
+
# compose : (a -> b) -> (b -> c) -> a -> c
|
25
|
+
def compose(*fns)
|
26
|
+
->(v) {
|
27
|
+
fns.reduce(fns.shift.(v)) do |x, f|
|
28
|
+
x.respond_to?(:compose) ? x.compose(f) : f.(x)
|
29
|
+
end
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
# A composition that early returns value
|
34
|
+
# f : a -> b
|
35
|
+
# g : a -> c
|
36
|
+
def compose_result(*fns)
|
37
|
+
->(v) {
|
38
|
+
fns.reduce(fns.shift.(v)) do |x, f|
|
39
|
+
x ? x : f.(v)
|
40
|
+
end
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Curry instance method
|
45
|
+
def curry(method, *args)
|
46
|
+
self.method(method).curry[*args]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Tomyum
|
2
|
+
module Generators
|
3
|
+
class Generator
|
4
|
+
include Tomyum::Assertions
|
5
|
+
include Tomyum::Resolvable
|
6
|
+
|
7
|
+
def initialize(resolver = nil)
|
8
|
+
@resolver = resolver
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate(*args)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
|
3
|
+
module Tomyum
|
4
|
+
module Generators
|
5
|
+
module OpenAPI
|
6
|
+
# Generate Open API v3 Specifications
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# resolver = Resolver.new(schemas: [])
|
10
|
+
# generator = OpenApi::Generator.new(resolver, {
|
11
|
+
# title: "Awesome API",
|
12
|
+
# version: "1.0.0",
|
13
|
+
# # other infos ...
|
14
|
+
# })
|
15
|
+
#
|
16
|
+
# generator = OpenApi::Generator.new(resolver) do
|
17
|
+
# title "Awesome API"
|
18
|
+
# version "1.0.0"
|
19
|
+
# servers []
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# generator.generate # => { }
|
23
|
+
class Generator < Tomyum::Generators::Generator
|
24
|
+
OPEN_API_VERSION = "3.0.0".freeze
|
25
|
+
OPEN_API_ID_REGEX = /:([\w]+)/.freeze
|
26
|
+
EMPTY_RESPONSE = "Response".freeze
|
27
|
+
|
28
|
+
OPTIONS = {
|
29
|
+
version: nil,
|
30
|
+
title: nil,
|
31
|
+
description: nil,
|
32
|
+
servers: []
|
33
|
+
}
|
34
|
+
|
35
|
+
def initialize(resolver = nil, options = {})
|
36
|
+
super(resolver)
|
37
|
+
|
38
|
+
@options = OPTIONS.merge(options)
|
39
|
+
end
|
40
|
+
|
41
|
+
def generate
|
42
|
+
{
|
43
|
+
openapi: OPEN_API_VERSION,
|
44
|
+
info: {
|
45
|
+
version: @options[:version],
|
46
|
+
title: @options[:title],
|
47
|
+
description: @options[:description],
|
48
|
+
},
|
49
|
+
servers: generate_server_urls(@options[:servers]),
|
50
|
+
paths: generate_paths(resolver.endpoints.keys),
|
51
|
+
components: {
|
52
|
+
schemas: generate_schemas(resolver.schemas.keys),
|
53
|
+
},
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# share for both specs
|
58
|
+
def generate_params(method, base_url = nil)
|
59
|
+
generate_path_params(method, base_url) + generate_query_params(method)
|
60
|
+
end
|
61
|
+
|
62
|
+
def generate_param(param, in: :query, schema: nil)
|
63
|
+
return unless param.serializable?
|
64
|
+
|
65
|
+
{
|
66
|
+
name: param.name.to_s,
|
67
|
+
in: binding.local_variable_get(:in),
|
68
|
+
schema: PropertyGenerator.generate(param, schema: schema),
|
69
|
+
}.tap do |parameter|
|
70
|
+
parameter[:required] = param.required if param.required
|
71
|
+
parameter[:deprecated] = param.deprecated if param.deprecated
|
72
|
+
parameter[:description] = param.description if param.description
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# path contains id? e.g. /users/:id
|
77
|
+
def generate_path_params(method, base_url = nil)
|
78
|
+
extract_ids(url(base_url, method.path))
|
79
|
+
.map { |id| Attributes::String.new(id, required: true) }
|
80
|
+
.map { |id| generate_param(id, in: :path) }
|
81
|
+
end
|
82
|
+
|
83
|
+
# convert request body to `query` params when HTTP verb is `GET`
|
84
|
+
def generate_query_params(method)
|
85
|
+
return [] unless method.verb == :get
|
86
|
+
|
87
|
+
method.params.attributes.values.each_with_object([]) do |param, params|
|
88
|
+
# include original schema when method.schema differs from params.schema
|
89
|
+
schema = method.params.schema if method.schema != method.params.schema
|
90
|
+
params << generate_param(param, schema: schema)
|
91
|
+
end.compact
|
92
|
+
end
|
93
|
+
|
94
|
+
def group_methods_by_path(methods)
|
95
|
+
methods.values.each_with_object({}) do |method, paths|
|
96
|
+
paths[method.path] ||= []
|
97
|
+
paths[method.path] << method
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# -- end --
|
102
|
+
def generate_server_urls(urls = [])
|
103
|
+
urls.map { |name, url| { url: url, description: name } }
|
104
|
+
end
|
105
|
+
|
106
|
+
def generate_paths(services)
|
107
|
+
services.each_with_object({}) do |name, paths|
|
108
|
+
# no need to initialize
|
109
|
+
next unless (endpoint = resolver.endpoint(name))
|
110
|
+
|
111
|
+
generate_operations(endpoint, paths)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def generate_operations(endpoint, paths)
|
116
|
+
base_url = endpoint.path
|
117
|
+
group_methods_by_path(endpoint.methods(resolver)).each do |path, methods|
|
118
|
+
resource_url = convert_id_params(url(base_url, path))
|
119
|
+
paths[resource_url] = {}
|
120
|
+
|
121
|
+
methods.each do |method|
|
122
|
+
paths[resource_url][method.verb] = generate_operation(method, base_url)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operationObject
|
128
|
+
def generate_operation(method, base_url = nil)
|
129
|
+
{
|
130
|
+
tags: [method.schema.to_s.humanize.pluralize],
|
131
|
+
parameters: generate_params(method, base_url),
|
132
|
+
}.tap do |operation|
|
133
|
+
operation[:requestBody] = generate_request_params(method) if method.body?
|
134
|
+
operation[:summary] = method.description if method.description
|
135
|
+
operation[:responses] = generate_responses(method)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def generate_request_params(method)
|
140
|
+
{
|
141
|
+
"content": {
|
142
|
+
"application/x-www-form-urlencoded": {
|
143
|
+
schema: generate_schema(method.params),
|
144
|
+
},
|
145
|
+
},
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
def generate_responses(method)
|
150
|
+
{
|
151
|
+
method.status => generate_response(method),
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
def generate_response(method)
|
156
|
+
{
|
157
|
+
description: method.schema&.to_s&.humanize || EMPTY_RESPONSE,
|
158
|
+
}.tap do |response|
|
159
|
+
response[:content] = generate_json_response(method) if method.schema.present?
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def generate_json_response(method)
|
164
|
+
{
|
165
|
+
"application/json": {
|
166
|
+
schema: PropertyGenerator.generate(method.schema, as: method.as),
|
167
|
+
},
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject
|
172
|
+
def generate_schemas(serializers)
|
173
|
+
serializers.each_with_object({}) do |name, schemas|
|
174
|
+
# no need to initialize
|
175
|
+
next unless (object = resolver.schema(name))
|
176
|
+
|
177
|
+
schemas[name] = generate_schema(object)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def generate_schema(object)
|
182
|
+
self.class.assert_kind_of Tomyum::Attributes::Attribute, object
|
183
|
+
|
184
|
+
PropertyGenerator.generate(object)
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
# convert :id to {id} format
|
190
|
+
def convert_id_params(url)
|
191
|
+
url.gsub(OPEN_API_ID_REGEX, '{\1}')
|
192
|
+
end
|
193
|
+
|
194
|
+
def extract_ids(path)
|
195
|
+
path.scan(OPEN_API_ID_REGEX).flatten
|
196
|
+
end
|
197
|
+
|
198
|
+
# join relative url
|
199
|
+
def url(*parts)
|
200
|
+
"/" + parts.compact.map { |part| part.gsub(/^\/|\/$/, "") }.reject(&:blank?).join("/")
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|