zero-rails_openapi 1.2.0 → 1.3.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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +39 -17
- data/documentation/examples/auto_gen_desc.rb +8 -5
- data/documentation/examples/auto_gen_doc.rb +86 -0
- data/documentation/examples/example_output_doc.json +2998 -0
- data/documentation/examples/examples_controller.rb +1 -1
- data/documentation/examples/goods_doc.rb +38 -0
- data/documentation/examples/open_api.rb +49 -1
- data/documentation/parameter.md +3 -2
- data/lib/oas_objs/helpers.rb +2 -0
- data/lib/oas_objs/media_type_obj.rb +1 -1
- data/lib/oas_objs/schema_obj.rb +21 -13
- data/lib/open_api.rb +0 -1
- data/lib/open_api/config.rb +95 -29
- data/lib/open_api/dsl.rb +19 -12
- data/lib/open_api/dsl_inside_block.rb +14 -5
- data/lib/open_api/generator.rb +24 -6
- data/lib/open_api/helpers.rb +35 -0
- data/lib/open_api/version.rb +1 -1
- metadata +7 -4
- data/documentation/examples/auto_gen_dsl.rb +0 -42
@@ -16,7 +16,7 @@ class Api::V1::ExamplesController < Api::V1::BaseController
|
|
16
16
|
response '567', 'query result export', :pdf, type: File
|
17
17
|
end
|
18
18
|
|
19
|
-
open_api :index, '(SUMMARY) this api blah blah ...' do
|
19
|
+
open_api :index, '(SUMMARY) this api blah blah ...', builder: :template1 do
|
20
20
|
this_api_is_invalid! 'this api is expired!'
|
21
21
|
desc 'Optional multiline or single-line Markdown-formatted description',
|
22
22
|
id: 'user id',
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class V2::GoodsDoc < BaseDoc
|
2
|
+
|
3
|
+
open_api :index, 'Get list of Goods.', builder: :index,
|
4
|
+
use: [ :Token ] do # use parameters write in AutoGenDoc#api_dry
|
5
|
+
# skip: %i[ Token ] do # you can also skip parameters
|
6
|
+
desc 'listing Goods',
|
7
|
+
view!: 'search view, allows::<br/>',
|
8
|
+
search_type!: 'search field, allows:<br/>'
|
9
|
+
|
10
|
+
query :view, String, enum: {
|
11
|
+
'all goods (default)': :all,
|
12
|
+
'only online': :online,
|
13
|
+
'only offline': :offline,
|
14
|
+
'expensive goods': :expensive,
|
15
|
+
'cheap goods': :cheap,
|
16
|
+
}
|
17
|
+
query :search_type, String, enum: %w[name creator category price]
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
open_api :create, 'Create a Good', builder: :success_or_not, use: token do
|
22
|
+
form! 'for creating a good', data: {
|
23
|
+
:name! => { type: String, desc: 'good\'s name' },
|
24
|
+
:category_id! => { type: Integer, desc: 'sub_category\'s id', npmt: true, range: { ge: 1 }, as: :cate },
|
25
|
+
:price! => { type: Float, desc: 'good\'s price', range: { ge: 0} },
|
26
|
+
# -- optional
|
27
|
+
:is_online => { type: Boolean, desc: 'it\'s online?' },
|
28
|
+
:remarks => { type: String, desc: 'remarks' },
|
29
|
+
:pic_path => { type: String, desc: 'picture url', is: :url },
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
open_api :show, 'Show a Good.', builder: :show, use: [ :Token, :id ]
|
35
|
+
|
36
|
+
|
37
|
+
open_api :destroy, 'Delete a Good.', builder: :success_or_not, use: [ :Token, :id ]
|
38
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'open_api'
|
2
2
|
|
3
|
-
OpenApi.
|
3
|
+
OpenApi::Config.tap do |c|
|
4
4
|
# [REQUIRED] The location where .json doc file will be output.
|
5
5
|
c.file_output_path = 'public/open_api'
|
6
6
|
|
@@ -82,6 +82,54 @@ OpenApi.configure do |c|
|
|
82
82
|
global_security: [{ ApiKeyAuth: [] }],
|
83
83
|
}
|
84
84
|
}
|
85
|
+
|
86
|
+
c.generate_jbuilder_file = true
|
87
|
+
c.overwrite_jbuilder_file = false
|
88
|
+
c.jbuilder_templates = {
|
89
|
+
index: (
|
90
|
+
<<-FILE
|
91
|
+
json.partial! 'api/base', total: @data.count
|
92
|
+
|
93
|
+
json.data do
|
94
|
+
# @data = @data.page(@_page).per(@_rows) if @_page || @_rows
|
95
|
+
# json.array! @data do |datum|
|
96
|
+
json.array! @data.page(@_page).per(@_rows) do |datum|
|
97
|
+
json.(datum, *datum.show_attrs) if datum.present?
|
98
|
+
end
|
99
|
+
end
|
100
|
+
FILE
|
101
|
+
),
|
102
|
+
|
103
|
+
show: (
|
104
|
+
<<-FILE
|
105
|
+
json.partial! 'api/base', total: 1
|
106
|
+
|
107
|
+
json.data do
|
108
|
+
json.array! [ @data ] do |datum|
|
109
|
+
json.(datum, *datum.show_attrs) if datum.present?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
FILE
|
113
|
+
),
|
114
|
+
|
115
|
+
success: (
|
116
|
+
<<-FILE
|
117
|
+
json.partial! 'api/success'
|
118
|
+
FILE
|
119
|
+
),
|
120
|
+
|
121
|
+
success_or_not: (
|
122
|
+
<<-FILE
|
123
|
+
unless @status
|
124
|
+
# @_code, @_msg = @error_info.present? ? @error_info : ApiError.action_failed.info
|
125
|
+
end
|
126
|
+
|
127
|
+
json.partial! 'api/base', total: 0
|
128
|
+
json.data ''
|
129
|
+
FILE
|
130
|
+
),
|
131
|
+
}
|
132
|
+
|
85
133
|
end
|
86
134
|
|
87
135
|
Object.const_set('Boolean', 'boolean') # Support `Boolean` writing in DSL
|
data/documentation/parameter.md
CHANGED
@@ -15,7 +15,7 @@ int32, float, date ...
|
|
15
15
|
All the types you can use are:
|
16
16
|
- **String, 'binary', 'base64'**
|
17
17
|
- **Integer, Long, 'int32', 'int64', Float, Double**
|
18
|
-
- **File** (it will be converted as `{ type: 'string', format:
|
18
|
+
- **File** (it will be converted as `{ type: 'string', format: Config.dft_file_format }`)
|
19
19
|
- **Date, DateTime**
|
20
20
|
- **Boolean**
|
21
21
|
- **Array**: `Array[String]` or `[String]`
|
@@ -58,4 +58,5 @@ You can set the schema by following keys (all are optional), the words in parent
|
|
58
58
|
[email phone password uuid uri url time date], to overwrite it you can set it in initializer `c.is_options = %w[]`.
|
59
59
|
5. If type is Object, for describing each property's schema, the only way is use ref type, like: `{ id: :Id, name: :Name }`
|
60
60
|
- **pattern (regexp, pr, reg)**
|
61
|
-
- **default (dft, default_value)**
|
61
|
+
- **default (dft, default_value)**
|
62
|
+
- **as** # TODO
|
data/lib/oas_objs/helpers.rb
CHANGED
@@ -10,11 +10,13 @@ module OpenApi
|
|
10
10
|
proc { |_, v| truly_present? v }
|
11
11
|
end
|
12
12
|
|
13
|
+
# assign.to
|
13
14
|
def assign(value)
|
14
15
|
@assign = value.is_a?(Symbol) ? send("_#{value}") : value
|
15
16
|
self
|
16
17
|
end
|
17
18
|
|
19
|
+
# reduceee.then_merge! => for Hash
|
18
20
|
def reduceee(*values)
|
19
21
|
@assign = values.compact.reduce({ }, :merge).keep_if &value_present
|
20
22
|
self
|
@@ -7,7 +7,7 @@ module OpenApi
|
|
7
7
|
attr_accessor :media_type, :schema
|
8
8
|
def initialize(media_type, schema_hash)
|
9
9
|
self.media_type = media_type_mapping media_type
|
10
|
-
self.schema = SchemaObj.new(schema_hash
|
10
|
+
self.schema = SchemaObj.new(schema_hash.values_at(:type, :data, :t).compact.first, schema_hash)
|
11
11
|
end
|
12
12
|
|
13
13
|
def process
|
data/lib/oas_objs/schema_obj.rb
CHANGED
@@ -30,12 +30,15 @@ module OpenApi
|
|
30
30
|
processed_range,
|
31
31
|
processed_is_and_format(param_name),
|
32
32
|
{
|
33
|
-
pattern:
|
34
|
-
default:
|
33
|
+
pattern: _pattern&.inspect&.delete('/'),
|
34
|
+
default: _default,
|
35
|
+
as: _as,
|
36
|
+
permit: _permit,
|
37
|
+
not_permit: _npermit,
|
35
38
|
}
|
36
39
|
then_merge!
|
37
40
|
|
38
|
-
|
41
|
+
reduceee(processed_desc options).then_merge!
|
39
42
|
end
|
40
43
|
alias_method :process, :process_for
|
41
44
|
|
@@ -51,15 +54,17 @@ module OpenApi
|
|
51
54
|
options[:desc_inside] ? { description: result } : nil
|
52
55
|
end
|
53
56
|
|
57
|
+
# TODO: more info
|
58
|
+
# TODO: desc configure
|
54
59
|
def process_desc
|
55
60
|
if processed[:enum].present?
|
56
61
|
if @enum_info.present?
|
57
62
|
@enum_info.each_with_index do |(info, value), index|
|
58
|
-
__desc.concat "
|
63
|
+
__desc.concat "<br/>#{index + 1}/ #{info}: #{value}"
|
59
64
|
end
|
60
65
|
else
|
61
66
|
processed[:enum].each_with_index do |value, index|
|
62
|
-
__desc.concat "
|
67
|
+
__desc.concat "<br/>#{index + 1}/ #{value}"
|
63
68
|
end
|
64
69
|
end
|
65
70
|
end
|
@@ -69,7 +74,7 @@ module OpenApi
|
|
69
74
|
def processed_type(type = self.type)
|
70
75
|
t = type.class.in?([Hash, Array, Symbol]) ? type : "#{type}".downcase
|
71
76
|
if t.is_a? Hash
|
72
|
-
# For
|
77
|
+
# For supporting writing:
|
73
78
|
# form 'desc', data: {
|
74
79
|
# id!: { type: Integer, enum: 0..5, desc: 'user id' }
|
75
80
|
# }
|
@@ -87,7 +92,7 @@ module OpenApi
|
|
87
92
|
elsif t.in? %w[binary base64]
|
88
93
|
{ type: 'string', format: t}
|
89
94
|
elsif t.eql? 'file'
|
90
|
-
{ type: 'string', format:
|
95
|
+
{ type: 'string', format: Config.dft_file_format }
|
91
96
|
elsif t.eql? 'datetime'
|
92
97
|
{ type: 'string', format: 'date-time' }
|
93
98
|
else # other string
|
@@ -177,7 +182,7 @@ module OpenApi
|
|
177
182
|
end
|
178
183
|
def recognize_is_options_in(name)
|
179
184
|
# identify whether `is` patterns matched the name, if so, generate `is`.
|
180
|
-
|
185
|
+
Config.is_options.each do |pattern|
|
181
186
|
self._is = pattern or break if name.match? /#{pattern}/
|
182
187
|
end if _is.nil?
|
183
188
|
self.delete :_is if _is.in?([:x, :we])
|
@@ -188,13 +193,16 @@ module OpenApi
|
|
188
193
|
_enum: %i[ enum values allowable_values ],
|
189
194
|
_value: %i[ must_be value allowable_value ],
|
190
195
|
_range: %i[ range number_range ],
|
191
|
-
_length: %i[ length lth
|
192
|
-
_is: %i[ is_a is ], # NOT OAS Spec,
|
196
|
+
_length: %i[ length lth size ],
|
197
|
+
_is: %i[ is_a is ], # NOT OAS Spec, see documentation/parameter.md
|
193
198
|
_format: %i[ format fmt ],
|
194
|
-
_pattern: %i[ pattern regexp
|
199
|
+
_pattern: %i[ pattern regexp pt reg ],
|
195
200
|
_default: %i[ default dft default_value ],
|
196
|
-
_desc: %i[ desc description
|
197
|
-
__desc: %i[ desc! description!
|
201
|
+
_desc: %i[ desc description d ],
|
202
|
+
__desc: %i[ desc! description! d! ],
|
203
|
+
_as: %i[ as to for map mapping ], # NOT OAS Spec, it's for zero-params_processor
|
204
|
+
_permit: %i[ permit pmt ], # NOT OAS Spec, it's for zero-params_processor
|
205
|
+
_npermit: %i[ npmt not_permit unpermit ], # NOT OAS Spec, it's for zero-params_processor
|
198
206
|
}.each do |key, aliases|
|
199
207
|
define_method key do
|
200
208
|
aliases.each do |alias_name|
|
data/lib/open_api.rb
CHANGED
data/lib/open_api/config.rb
CHANGED
@@ -1,34 +1,100 @@
|
|
1
1
|
module OpenApi
|
2
2
|
module Config
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
3
|
+
# [REQUIRED] The location where .json doc file will be output.
|
4
|
+
cattr_accessor :file_output_path do
|
5
|
+
'public/open_api'
|
6
|
+
end
|
7
|
+
|
8
|
+
cattr_accessor :generate_doc do
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
# Everything about OAS3 is on https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md
|
13
|
+
# Getting started: https://swagger.io/docs/specification/basic-structure/
|
14
|
+
cattr_accessor :register_docs do
|
15
|
+
{
|
16
|
+
# [REQUIRED] At least one doc.
|
17
|
+
zero_rails: {
|
18
|
+
# [REQUIRED] ZRO will scan all the descendants of the root_controller, and then generate their docs.
|
19
|
+
root_controller: ApplicationController,
|
20
|
+
|
21
|
+
# [REQUIRED] Info Object: The info section contains API information
|
22
|
+
info: {
|
23
|
+
# [REQUIRED] The title of the application.
|
24
|
+
title: 'Zero Rails Apis',
|
25
|
+
# [REQUIRED] The version of the OpenAPI document
|
26
|
+
# (which is distinct from the OpenAPI Specification version or the API implementation version).
|
27
|
+
version: '0.0.1'
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
cattr_accessor :is_options do
|
34
|
+
%w[ email phone password uuid uri url time date ]
|
35
|
+
end
|
36
|
+
|
37
|
+
cattr_accessor :dft_file_format do
|
38
|
+
'binary'
|
39
|
+
end
|
40
|
+
|
41
|
+
cattr_accessor :generate_jbuilder_file do
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
cattr_accessor :overwrite_jbuilder_file do
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
cattr_accessor :jbuilder_templates do
|
50
|
+
{
|
51
|
+
index: (
|
52
|
+
<<-FILE
|
53
|
+
json.partial! 'api/base', total: @data.count
|
54
|
+
|
55
|
+
json.data do
|
56
|
+
# @data = @data.page(@_page).per(@_rows) if @_page || @_rows
|
57
|
+
# json.array! @data do |datum|
|
58
|
+
json.array! @data.page(@_page).per(@_rows) do |datum|
|
59
|
+
json.(datum, *datum.show_attrs) if datum.present?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
FILE
|
63
|
+
),
|
64
|
+
|
65
|
+
show: (
|
66
|
+
<<-FILE
|
67
|
+
json.partial! 'api/base', total: 1
|
68
|
+
|
69
|
+
json.data do
|
70
|
+
json.array! [ @data ] do |datum|
|
71
|
+
json.(datum, *datum.show_attrs) if datum.present?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
FILE
|
75
|
+
),
|
76
|
+
|
77
|
+
success: (
|
78
|
+
<<-FILE
|
79
|
+
json.partial! 'api/success'
|
80
|
+
FILE
|
81
|
+
),
|
82
|
+
|
83
|
+
success_or_not: (
|
84
|
+
<<-FILE
|
85
|
+
unless @status
|
86
|
+
# @_code, @_msg = @error_info.present? ? @error_info : ApiError.action_failed.info
|
87
|
+
end
|
88
|
+
|
89
|
+
json.partial! 'api/base', total: 0
|
90
|
+
json.data ''
|
91
|
+
FILE
|
92
|
+
),
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.docs
|
97
|
+
register_docs
|
32
98
|
end
|
33
99
|
end
|
34
100
|
end
|
data/lib/open_api/dsl.rb
CHANGED
@@ -24,33 +24,40 @@ module OpenApi
|
|
24
24
|
current_ctrl.instance_eval &block if block_given?
|
25
25
|
end
|
26
26
|
|
27
|
-
def open_api method, summary = '', &block
|
27
|
+
def open_api method, summary = '', options = { }, &block
|
28
28
|
apis_set if @_ctrl_infos.nil?
|
29
29
|
|
30
30
|
# select the routing info (corresponding to the current method) from the routing list.
|
31
31
|
action_path = "#{@_ctrl_path ||= controller_path}##{method}"
|
32
32
|
routes_info = ctrl_routes_list&.select { |api| api[:action_path].match? /^#{action_path}$/ }&.first
|
33
|
-
|
33
|
+
pp "[ZRO Warnning] Routing mapping failed: #{@_ctrl_path}##{method}" and return if routes_info.nil?
|
34
34
|
|
35
|
-
# structural { path: { http_method:{ } } }, for Paths Object.
|
35
|
+
# structural { #path: { #http_method:{ } } }, for pushing into Paths Object.
|
36
36
|
path = (@_api_infos ||= { })[routes_info[:path]] ||= { }
|
37
37
|
current_api = path[routes_info[:http_verb]] =
|
38
|
-
ApiInfoObj.new(action_path)
|
38
|
+
ApiInfoObj.new(action_path, options.slice(:skip, :use))
|
39
39
|
.merge! description: '', summary: summary, operationId: method, tags: [@_apis_tag],
|
40
|
-
parameters: [ ], requestBody: '', responses: { },
|
41
|
-
security: [ ], servers: [ ]
|
40
|
+
parameters: [ ], requestBody: '', responses: { }, security: [ ], servers: [ ]
|
42
41
|
|
43
|
-
|
42
|
+
if (builder = options.values_at(:builder, :bd, :jbuilder).compact.first).present?
|
43
|
+
Generator
|
44
|
+
.generate_builder_file path: action_path.split('#').first,
|
45
|
+
action: action_path.split('#').last,
|
46
|
+
builder: builder
|
47
|
+
end
|
48
|
+
|
49
|
+
current_api.tap do |api|
|
44
50
|
[method, :all].each do |key| # blocks_store_key
|
45
|
-
@_apis_blocks&.[](key)&.each { |blk|
|
51
|
+
@_apis_blocks&.[](key)&.each { |blk| api.instance_eval &blk }
|
46
52
|
end
|
47
|
-
|
48
|
-
|
49
|
-
|
53
|
+
api.param_use = nil
|
54
|
+
api.instance_eval &block if block_given?
|
55
|
+
api.instance_eval { process_params }
|
56
|
+
api.delete_if { |_, v| v.blank? }
|
50
57
|
end
|
51
58
|
end
|
52
59
|
|
53
|
-
#
|
60
|
+
# method could be symbol array, like: %i[ .. ]
|
54
61
|
def api_dry method = :all, desc = '', &block
|
55
62
|
@_apis_blocks ||= { }
|
56
63
|
if method.is_a? Array
|
@@ -3,6 +3,7 @@ require 'oas_objs/param_obj'
|
|
3
3
|
require 'oas_objs/response_obj'
|
4
4
|
require 'oas_objs/request_body_obj'
|
5
5
|
require 'oas_objs/ref_obj'
|
6
|
+
require 'open_api/helpers'
|
6
7
|
|
7
8
|
module OpenApi
|
8
9
|
module DSL
|
@@ -100,10 +101,13 @@ module OpenApi
|
|
100
101
|
|
101
102
|
class ApiInfoObj < Hash
|
102
103
|
include DSL::CommonDSL
|
104
|
+
include DSL::Helpers
|
103
105
|
|
104
|
-
attr_accessor :action_path
|
105
|
-
def initialize(action_path)
|
106
|
+
attr_accessor :action_path, :param_skip, :param_use
|
107
|
+
def initialize(action_path, options = { })
|
106
108
|
self.action_path = action_path
|
109
|
+
self.param_skip = options[:skip] || [ ]
|
110
|
+
self.param_use = options[:use] || [ ]
|
107
111
|
end
|
108
112
|
|
109
113
|
def this_api_is_invalid! explain = ''
|
@@ -131,6 +135,9 @@ module OpenApi
|
|
131
135
|
end
|
132
136
|
|
133
137
|
def param param_type, name, type, required, schema_hash = { }
|
138
|
+
return if param_skip.include? name
|
139
|
+
return unless param_use.include? name if param_use.present?
|
140
|
+
|
134
141
|
if @inputs_descs&.[](name).present?
|
135
142
|
schema_hash[:desc] = @inputs_descs[name]
|
136
143
|
elsif @inputs_descs&.[]("#{name}!".to_sym).present?
|
@@ -139,7 +146,9 @@ module OpenApi
|
|
139
146
|
|
140
147
|
param_obj = ParamObj.new(name, param_type, type, required, schema_hash)
|
141
148
|
# The definition of the same name parameter will be overwritten
|
142
|
-
index = self[:parameters].map
|
149
|
+
index = self[:parameters].map do |p|
|
150
|
+
p.processed[:name] if p.is_a? ParamObj
|
151
|
+
end.index name
|
143
152
|
if index.present?
|
144
153
|
self[:parameters][index] = param_obj
|
145
154
|
else
|
@@ -148,8 +157,8 @@ module OpenApi
|
|
148
157
|
end
|
149
158
|
|
150
159
|
def process_params
|
151
|
-
self[:parameters].each_with_index do |
|
152
|
-
self[:parameters][index] =
|
160
|
+
self[:parameters].each_with_index do |p, index|
|
161
|
+
self[:parameters][index] = p.is_a?(ParamObj) ? p.process : p
|
153
162
|
end
|
154
163
|
end
|
155
164
|
|