zero-rails_openapi 1.0.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/.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
|