zero-rails_openapi 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,9 @@
1
+ require "open_api/version"
2
+ require "open_api/config"
3
+ require "open_api/generator"
4
+ require "open_api/dsl"
5
+
6
+ module OpenApi
7
+ include Config
8
+ include Generator
9
+ end
@@ -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
@@ -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