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.
@@ -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