zero-rails_openapi 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +35 -0
- data/LICENSE.txt +21 -0
- data/README.md +245 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/examples/examples_controller.rb +36 -0
- data/lib/examples/open_api.rb +87 -0
- data/lib/oas_objs/helpers.rb +41 -0
- data/lib/oas_objs/media_type_obj.rb +86 -0
- data/lib/oas_objs/param_obj.rb +83 -0
- data/lib/oas_objs/ref_obj.rb +29 -0
- data/lib/oas_objs/request_body_obj.rb +54 -0
- data/lib/oas_objs/response_obj.rb +44 -0
- data/lib/oas_objs/schema_obj.rb +187 -0
- data/lib/open_api.rb +9 -0
- data/lib/open_api/config.rb +34 -0
- data/lib/open_api/dsl.rb +58 -0
- data/lib/open_api/dsl_inside_block.rb +175 -0
- data/lib/open_api/generator.rb +75 -0
- data/lib/open_api/version.rb +3 -0
- data/lib/takes/open_api.rake +6 -0
- data/zero-rails_openapi.gemspec +37 -0
- metadata +116 -0
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'oas_objs/helpers'
|
2
|
+
require 'open_api/config'
|
3
|
+
require 'oas_objs/ref_obj'
|
4
|
+
|
5
|
+
module OpenApi
|
6
|
+
module DSL
|
7
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#schemaObject
|
8
|
+
class SchemaObj < Hash
|
9
|
+
include Helpers
|
10
|
+
|
11
|
+
attr_accessor :processed, :type
|
12
|
+
def initialize(type, schema_hash)
|
13
|
+
self.processed = { }
|
14
|
+
# [Note] Here is no limit to type, even if the input isn't up to OAS,
|
15
|
+
# like: double, float, hash.
|
16
|
+
# My consideration is, OAS can't express some cases like:
|
17
|
+
# `total_price` should be double, is_a `price`, and match /^.*\..*$/
|
18
|
+
# However, user can decide how to write --
|
19
|
+
# `type: number, format: double`, or `type: double`
|
20
|
+
self.type = type
|
21
|
+
self.merge! schema_hash
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def process_for(param_name = nil)
|
26
|
+
processed.merge! processed_type
|
27
|
+
all(processed_enum_and_length,
|
28
|
+
processed_range,
|
29
|
+
processed_is_and_format(param_name),
|
30
|
+
{ pattern: _pattern&.inspect&.delete('/'),
|
31
|
+
default: _default }
|
32
|
+
).for_merge
|
33
|
+
end
|
34
|
+
alias_method :process, :process_for
|
35
|
+
|
36
|
+
def processed_type(type = self.type)
|
37
|
+
t = type.class.in?([Hash, Array, Symbol]) ? type : "#{type}".downcase
|
38
|
+
if t.is_a? Hash
|
39
|
+
recursive_obj_type t
|
40
|
+
elsif t.is_a? Array
|
41
|
+
recursive_array_type t
|
42
|
+
elsif t.is_a? Symbol
|
43
|
+
RefObj.new(:schema, t).process
|
44
|
+
elsif t.in? %w[float double int32 int64] # TTTTTIP: 这些值应该传 string 进来, symbol 只允许 $ref
|
45
|
+
{ type: t.match?('int') ? 'integer' : 'number', format: t}
|
46
|
+
elsif t.in? %w[binary base64]
|
47
|
+
{ type: 'string', format: t}
|
48
|
+
elsif t.eql? 'file'
|
49
|
+
{ type: 'string', format: OpenApi.config.dft_file_format }
|
50
|
+
else # other string
|
51
|
+
{ type: t }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
def recursive_obj_type(t) # DSL use { k:v } to represent object structure
|
55
|
+
return processed_type(t) unless t.is_a? Hash
|
56
|
+
|
57
|
+
_schema = {
|
58
|
+
type: 'object',
|
59
|
+
properties: { },
|
60
|
+
required: [ ]
|
61
|
+
}
|
62
|
+
t.each do |k, v|
|
63
|
+
_schema[:required] << "#{k}".delete('!') if "#{k}".match? '!'
|
64
|
+
_schema[:properties]["#{k}".delete('!').to_sym] = recursive_obj_type v
|
65
|
+
end
|
66
|
+
_schema.keep_if &value_present
|
67
|
+
end
|
68
|
+
def recursive_array_type(t)
|
69
|
+
if t.is_a? Array
|
70
|
+
{
|
71
|
+
type: 'array',
|
72
|
+
items: recursive_array_type(t[0])
|
73
|
+
}
|
74
|
+
else
|
75
|
+
processed_type t
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def processed_enum_and_length
|
80
|
+
%i[_enum _length].each do |key|
|
81
|
+
value = self.send(key)
|
82
|
+
self[key] = value.to_a if value.present? && value.is_a?(Range)
|
83
|
+
end
|
84
|
+
|
85
|
+
# generate_enums_by_enum_array
|
86
|
+
values = _enum || _value
|
87
|
+
self._enum = (values.is_a?(Array) ? values : [values]) if truly_present?(values)
|
88
|
+
|
89
|
+
# generate length range fields by _lth array
|
90
|
+
lth = _length || [ ]
|
91
|
+
if self[:type] == 'array'
|
92
|
+
{
|
93
|
+
minItems: lth.is_a?(Array) ? lth.first : nil,
|
94
|
+
maxItems: lth.is_a?(Array) ? lth.last : nil
|
95
|
+
}
|
96
|
+
else
|
97
|
+
{
|
98
|
+
minLength: lth.is_a?(Array) ? lth.first : ("#{lth}".match?('ge') ? "#{lth}".split('_').last.to_i : nil),
|
99
|
+
maxLength: lth.is_a?(Array) ? lth.last : ("#{lth}".match?('le') ? "#{lth}".split('_').last.to_i : nil)
|
100
|
+
}
|
101
|
+
end.merge!(enum: _enum).keep_if &value_present
|
102
|
+
end
|
103
|
+
|
104
|
+
def processed_range
|
105
|
+
range = _range || { }
|
106
|
+
{
|
107
|
+
minimum: range[:gt] || range[:ge],
|
108
|
+
exclusiveMinimum: range[:gt].present? ? true : nil,
|
109
|
+
maximum: range[:lt] || range[:le],
|
110
|
+
exclusiveMaximum: range[:lt].present? ? true : nil
|
111
|
+
}.keep_if &value_present
|
112
|
+
end
|
113
|
+
|
114
|
+
def processed_is_and_format(name)
|
115
|
+
return if name.nil?
|
116
|
+
recognize_is_options_in name
|
117
|
+
{ }.tap do |it|
|
118
|
+
# `format` that generated in process_type() may be overwrote here.
|
119
|
+
it.merge!(format: _format || _is) if processed[:format].blank? || _format.present?
|
120
|
+
it.merge! is: _is
|
121
|
+
end
|
122
|
+
end
|
123
|
+
def recognize_is_options_in(name)
|
124
|
+
# identify whether `is` patterns matched the name, if so, generate `is`.
|
125
|
+
OpenApi.config.is_options.each do |pattern|
|
126
|
+
self._is = pattern or break if "#{name}".match? /#{pattern}/
|
127
|
+
end if _is.nil?
|
128
|
+
self.delete :_is if _is.in?([:x, :we])
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
{ # SELF_MAPPING
|
133
|
+
_enum: %i[enum values allowable_values],
|
134
|
+
_value: %i[must_be value allowable_value ],
|
135
|
+
_range: %i[range number_range ],
|
136
|
+
_length: %i[length lth ],
|
137
|
+
_is: %i[is_a is ], # NOT OAS Spec, just a addition
|
138
|
+
_format: %i[format fmt ],
|
139
|
+
_pattern: %i[pattern regexp pr reg ],
|
140
|
+
_default: %i[default dft default_value ],
|
141
|
+
}.each do |key, aliases|
|
142
|
+
define_method key do
|
143
|
+
aliases.each do |alias_name|
|
144
|
+
break if self[key] == false
|
145
|
+
self[key] ||= self[alias_name]
|
146
|
+
end if self[key].nil?
|
147
|
+
self[key]
|
148
|
+
end
|
149
|
+
define_method "#{key}=" do |value| self[key] = value end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
__END__
|
157
|
+
|
158
|
+
Schema Object Examples
|
159
|
+
|
160
|
+
Primitive Sample
|
161
|
+
|
162
|
+
{
|
163
|
+
"type": "string",
|
164
|
+
"format": "email"
|
165
|
+
}
|
166
|
+
|
167
|
+
Simple Model
|
168
|
+
|
169
|
+
{
|
170
|
+
"type": "object",
|
171
|
+
"required": [
|
172
|
+
"name"
|
173
|
+
],
|
174
|
+
"properties": {
|
175
|
+
"name": {
|
176
|
+
"type": "string"
|
177
|
+
},
|
178
|
+
"address": {
|
179
|
+
"$ref": "#/components/schemas/Address"
|
180
|
+
},
|
181
|
+
"age": {
|
182
|
+
"type": "integer",
|
183
|
+
"format": "int32",
|
184
|
+
"minimum": 0
|
185
|
+
}
|
186
|
+
}
|
187
|
+
}
|
data/lib/open_api.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module OpenApi
|
2
|
+
module Config
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
DEFAULT_CONFIG = {
|
8
|
+
is_options: %w[email phone password uuid uri url time date],
|
9
|
+
dft_file_format: 'binary'
|
10
|
+
}
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def config
|
14
|
+
@config ||= ActiveSupport::InheritableOptions.new(DEFAULT_CONFIG)
|
15
|
+
end
|
16
|
+
|
17
|
+
def configure(&block)
|
18
|
+
config.instance_eval &block
|
19
|
+
end
|
20
|
+
|
21
|
+
### config options
|
22
|
+
# register_apis = {
|
23
|
+
# version: {
|
24
|
+
# :file_output_path, :root_controller
|
25
|
+
# info: {}
|
26
|
+
# }}
|
27
|
+
# is_options = %w[]
|
28
|
+
|
29
|
+
def apis
|
30
|
+
@apis ||= @config.register_apis
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/open_api/dsl.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'open_api/dsl_inside_block'
|
2
|
+
|
3
|
+
module OpenApi
|
4
|
+
module DSL
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
# TODO: Doc-Block Comments
|
10
|
+
module ClassMethods
|
11
|
+
def apis_set desc = '', external_doc_url = '', &block
|
12
|
+
@_api_infos, @_ctrl_infos = { }, { }
|
13
|
+
# current `tag`, this means that tags is currently divided by controllers.
|
14
|
+
tag = @_ctrl_infos[:tag] = { name: controller_name.camelize }
|
15
|
+
tag[:description] = desc if desc.present?
|
16
|
+
tag[:externalDocs] = { description: 'ref', url: external_doc_url } if external_doc_url.present?
|
17
|
+
|
18
|
+
current_ctrl = @_ctrl_infos[:components] = CtrlInfoObj.new
|
19
|
+
current_ctrl.instance_eval &block if block_given?
|
20
|
+
end
|
21
|
+
|
22
|
+
def open_api method, summary = '', &block
|
23
|
+
# select the routing info corresponding to the current method from the routing list.
|
24
|
+
action_path = "#{controller_path}##{method}"
|
25
|
+
routes_info = ctrl_routes_list.select { |api| api[:action_path].match? /^#{action_path}$/ }.first
|
26
|
+
puts "[zero-rails_openapi] Routing mapping failed: #{controller_path}##{method}" or return if routes_info.nil?
|
27
|
+
|
28
|
+
# structural { path: { http_method:{ } } }, for Paths Object.
|
29
|
+
# it will be merged into :paths
|
30
|
+
path = @_api_infos[routes_info[:path]] ||= { }
|
31
|
+
current_api = path[routes_info[:http_verb]] =
|
32
|
+
ApiInfoObj.new(action_path).merge!( summary: summary, operationId: method, tags: [controller_name.camelize] )
|
33
|
+
|
34
|
+
current_api.tap do |it|
|
35
|
+
it.instance_eval &block if block_given?
|
36
|
+
[method, :all].each do |key| # blocks_store_key
|
37
|
+
@apis_blocks[key]&.each { |blk| it.instance_eval &blk }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# For DRY; method could be symbol array
|
43
|
+
def open_api_set method = :all, desc = '', &block
|
44
|
+
@apis_blocks ||= { }
|
45
|
+
if method.is_a? Array
|
46
|
+
method.each { |m| (@apis_blocks[m.to_sym] ||= [ ]) << block }
|
47
|
+
else
|
48
|
+
(@apis_blocks[method.to_sym] ||= [ ]) << block
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def ctrl_routes_list
|
53
|
+
@routes_list ||= Generator.generate_routes_list
|
54
|
+
@routes_list[controller_path]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'oas_objs/schema_obj'
|
2
|
+
require 'oas_objs/param_obj'
|
3
|
+
require 'oas_objs/response_obj'
|
4
|
+
require 'oas_objs/request_body_obj'
|
5
|
+
require 'oas_objs/ref_obj'
|
6
|
+
|
7
|
+
module OpenApi
|
8
|
+
module DSL
|
9
|
+
module CommonDSL
|
10
|
+
def arrow_writing_support
|
11
|
+
Proc.new do |args, executor|
|
12
|
+
if args.count == 1 && args[0].is_a?(Hash)
|
13
|
+
send(executor, args[0].keys.first, *args[0].values.first)
|
14
|
+
else
|
15
|
+
send(executor, *args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
%i[header header! path path! query query! cookie cookie!].each do |param_type|
|
21
|
+
define_method param_type do |*args|
|
22
|
+
@param_type = param_type
|
23
|
+
_param_agent *args
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
%i[body body!].each do |method|
|
28
|
+
define_method method do |*args|
|
29
|
+
@_method_name = method
|
30
|
+
_request_body_agent *args
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# code represents `component_key` when u declare response component
|
35
|
+
def _response code, desc, media_type = nil, schema_hash = { }
|
36
|
+
(self[:responses] ||= { }).merge! ResponseObj.new(code, desc, media_type, schema_hash).process
|
37
|
+
end
|
38
|
+
|
39
|
+
def response *args
|
40
|
+
arrow_writing_support.call(args, :_response)
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_response desc, media_type = nil, schema_hash = { }
|
44
|
+
response :default, desc, media_type, schema_hash
|
45
|
+
end
|
46
|
+
|
47
|
+
{ # alias_methods mapping
|
48
|
+
response: %i[resp error_response ],
|
49
|
+
default_response: %i[dft_resp ],
|
50
|
+
error_response: %i[other_response oth_resp error err_resp],
|
51
|
+
}.each do |original_name, aliases|
|
52
|
+
aliases.each do |alias_name|
|
53
|
+
alias_method alias_name, original_name
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end # ----------------------------------------- end of CommonDSL
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
class CtrlInfoObj < Hash
|
62
|
+
include DSL::CommonDSL
|
63
|
+
|
64
|
+
def _schema component_key, type, schema_hash
|
65
|
+
(self[:schemas] ||= { }).merge! component_key => SchemaObj.new(type, schema_hash).process
|
66
|
+
end
|
67
|
+
def schema *args
|
68
|
+
arrow_writing_support.call(args, :_schema)
|
69
|
+
end
|
70
|
+
|
71
|
+
def param component_key, param_type, name, type, required, schema_hash = { }
|
72
|
+
(self[:parameters] ||= { })
|
73
|
+
.merge! component_key => ParamObj.new(name, param_type, type, required, schema_hash).process
|
74
|
+
end
|
75
|
+
|
76
|
+
def _param_agent *args
|
77
|
+
arrow_writing_support.call(args, :_param_arg_agent)
|
78
|
+
end
|
79
|
+
|
80
|
+
def _param_arg_agent component_key, name, type, schema_hash = { }
|
81
|
+
param component_key, "#{@param_type}".delete('!'), name, type,
|
82
|
+
("#{@param_type}".match?(/!/) ? :req : :opt), schema_hash
|
83
|
+
end
|
84
|
+
|
85
|
+
def request_body component_key, required, media_type, desc = '', schema_hash = { }
|
86
|
+
self[:requestBodies] = { component_key => RequestBodyObj.new(required, media_type, desc, schema_hash).process }
|
87
|
+
end
|
88
|
+
|
89
|
+
def _request_body_agent *args
|
90
|
+
arrow_writing_support.call(args, :_request_body_arg_agent)
|
91
|
+
end
|
92
|
+
|
93
|
+
def _request_body_arg_agent component_key, media_type, desc = '', schema_hash = { }
|
94
|
+
request_body component_key, ("#{@_method_name}".match?(/!/) ? :req : :opt), media_type, desc, schema_hash
|
95
|
+
end
|
96
|
+
end # ----------------------------------------- end of CtrlInfoObj
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
class ApiInfoObj < Hash
|
102
|
+
include DSL::CommonDSL
|
103
|
+
|
104
|
+
attr_accessor :action_path
|
105
|
+
def initialize(action_path)
|
106
|
+
self.action_path = action_path
|
107
|
+
end
|
108
|
+
|
109
|
+
def this_api_is_invalid! explain = ''
|
110
|
+
self[:deprecated] = true
|
111
|
+
end
|
112
|
+
alias_method :this_api_is_expired!, :this_api_is_invalid!
|
113
|
+
alias_method :this_api_is_unused!, :this_api_is_invalid!
|
114
|
+
alias_method :this_api_is_under_repair, :this_api_is_invalid!
|
115
|
+
|
116
|
+
def desc desc, inputs_descs = { }
|
117
|
+
@inputs_descs = inputs_descs
|
118
|
+
self[:description] = desc
|
119
|
+
end
|
120
|
+
|
121
|
+
def param param_type, name, type, required, schema_hash = { }
|
122
|
+
schema_hash[:desc] = @inputs_descs[name] if @inputs_descs&.[](name).present?
|
123
|
+
(self[:parameters] ||= [ ]) << ParamObj.new(name, param_type, type, required, schema_hash).process
|
124
|
+
end
|
125
|
+
|
126
|
+
def _param_agent name, type, schema_hash = { }
|
127
|
+
param "#{@param_type}".delete('!'), name, type, ("#{@param_type}".match?(/!/) ? :req : :opt), schema_hash
|
128
|
+
end
|
129
|
+
|
130
|
+
def param_ref component_key, *keys
|
131
|
+
(self[:parameters] ||= [ ]).concat [component_key].concat(keys).map { |key| RefObj.new(:parameter, key).process }
|
132
|
+
end
|
133
|
+
|
134
|
+
def request_body required, media_type, desc = '', schema_hash = { }
|
135
|
+
self[:requestBody] = RequestBodyObj.new(required, media_type, desc, schema_hash).process
|
136
|
+
end
|
137
|
+
|
138
|
+
def _request_body_agent media_type, desc = '', schema_hash = { }
|
139
|
+
request_body ("#{@_method_name}".match?(/!/) ? :req : :opt), media_type, desc, schema_hash
|
140
|
+
end
|
141
|
+
|
142
|
+
def body_ref component_key
|
143
|
+
self[:requestBody] = RefObj.new(:parameter, component_key).process
|
144
|
+
end
|
145
|
+
|
146
|
+
def response_ref code_compkey_hash
|
147
|
+
code_compkey_hash.each do |code, component_key|
|
148
|
+
(self[:responses] ||= { }).merge! code => RefObj.new(:response, component_key).process
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# 注意同时只能写一句 request body,包括 form 和 file
|
153
|
+
def form desc = '', schema_hash = { }
|
154
|
+
body :form, desc, schema_hash
|
155
|
+
end
|
156
|
+
def form! desc = '', schema_hash = { }
|
157
|
+
body! :form, desc, schema_hash
|
158
|
+
end
|
159
|
+
def file media_type, desc = '', schema_hash = { type: File }
|
160
|
+
body media_type, desc, schema_hash
|
161
|
+
end
|
162
|
+
def file! media_type, desc = '', schema_hash = { type: File }
|
163
|
+
body! media_type, desc, schema_hash
|
164
|
+
end
|
165
|
+
|
166
|
+
def security scheme_name, requirements = [ ]
|
167
|
+
(self[:security] ||= [ ]) << { scheme_name => requirements }
|
168
|
+
end
|
169
|
+
|
170
|
+
def server url, desc
|
171
|
+
(self[:servers] ||= [ ]) << { url: url, description: desc }
|
172
|
+
end
|
173
|
+
end # ----------------------------------------- end of ApiInfoObj
|
174
|
+
end
|
175
|
+
end
|