tomyum 0.1.0.a
Sign up to get free protection for your applications and to get access to all the features.
- 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
|