zero-rails_openapi 1.3.3 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,28 @@
1
+ module OpenApi
2
+ module DSL
3
+ # https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/
4
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject
5
+ class CombinedSchema < Hash
6
+ attr_accessor :processed
7
+
8
+ def initialize(combined_schema)
9
+ self.processed = { }
10
+
11
+ combined_schema.delete_if { |_, v| v.nil? }
12
+ @mode = combined_schema.keys.first.to_s.sub('_not', 'not').camelize(:lower).to_sym
13
+ @schemas = combined_schema.values.first
14
+ end
15
+
16
+ def process_for(param_name = nil, options = { desc_inside: false })
17
+ processed.tap do |it|
18
+ it[@mode] = @schemas.map do |schema|
19
+ type = schema.is_a?(Hash) ? schema[:type] : schema
20
+ schema = { } unless schema.is_a?(Hash)
21
+ SchemaObj.new(type, schema).process_for(param_name, options) end
22
+ end
23
+ end
24
+
25
+ alias process process_for
26
+ end
27
+ end
28
+ end
@@ -10,7 +10,7 @@ module OpenApi
10
10
  attr_accessor :processed, :examples_hash, :keys_of_value
11
11
 
12
12
  def initialize(examples_hash, keys_of_value = nil)
13
- self.examples_hash = examples_hash
13
+ self.examples_hash = examples_hash
14
14
  self.keys_of_value = keys_of_value
15
15
  end
16
16
 
@@ -1,6 +1,5 @@
1
1
  module OpenApi
2
2
  module Helpers
3
-
4
3
  # TODO: comment-block doc
5
4
  def truly_present?(obj)
6
5
  obj == false || obj.present?
@@ -23,11 +22,13 @@ module OpenApi
23
22
  end
24
23
 
25
24
  def to_processed(who)
25
+ return processed unless truly_present?(@assign)
26
+
26
27
  if who.is_a?(Symbol)
27
28
  send("#{who}=", @assign)
28
29
  else
29
30
  processed[who.to_sym] = @assign
30
- end if truly_present?(@assign)
31
+ end
31
32
 
32
33
  processed
33
34
  end
@@ -21,7 +21,7 @@ module OpenApi
21
21
  def process
22
22
  schema_processed = schema.process
23
23
  result = schema_processed.values.join.blank? ? { } : { schema: schema_processed }
24
- result.merge!(examples: examples.process) unless examples.nil?
24
+ result[:examples] = examples.process unless examples.nil?
25
25
  media_type.nil? ? { } : { media_type => result }
26
26
  end
27
27
 
@@ -7,19 +7,20 @@ module OpenApi
7
7
  include Helpers
8
8
 
9
9
  attr_accessor :processed, :schema
10
- def initialize(name, param_type, type, required, schema_hash)
10
+
11
+ def initialize(name, param_type, type, required, schema)
11
12
  self.processed = {
12
13
  name: name,
13
14
  in: param_type,
14
- required: required.to_s.match?(/req/),
15
+ required: required.to_s.match?(/req/)
15
16
  }
16
- self.schema = SchemaObj.new(type, schema_hash)
17
- merge! schema_hash
17
+ self.schema = schema.is_a?(CombinedSchema) ? schema : SchemaObj.new(type, schema)
18
+ merge! schema
18
19
  end
19
20
 
20
21
  def process
21
22
  assign(desc).to_processed 'description'
22
- processed.tap { |it| it[:schema] = schema.process_for self.processed[:name] }
23
+ processed.tap { |it| it[:schema] = schema.process_for(processed[:name]) }
23
24
  end
24
25
 
25
26
  def desc
@@ -21,7 +21,7 @@ module OpenApi
21
21
  processed
22
22
  end
23
23
 
24
- def override type_hash
24
+ def override(type_hash)
25
25
  @hash[:type].merge!(type_hash)
26
26
  self.media_type = MediaTypeObj.new(@mt, @hash)
27
27
  self
@@ -34,7 +34,7 @@ module OpenApi
34
34
  {
35
35
  pattern: _pattern&.inspect&.delete('/'),
36
36
  default: _default.nil? ? nil : '_default',
37
- examples: self[:examples].present? ? ExampleObj.new(self[:examples], self[:exp_by]).process : nil,
37
+ examples: self[:examples].present? ? ExampleObj.new(self[:examples], self[:exp_by]).process : nil
38
38
  },
39
39
  { as: _as, permit: _permit, not_permit: _npermit, req_if: _req_if, opt_if: _opt_if }
40
40
  then_merge!
@@ -75,14 +75,17 @@ module OpenApi
75
75
  end
76
76
 
77
77
  def processed_type(type = self.type)
78
- t = type.class.in?([Hash, Array, Symbol]) ? type : "#{type}".downcase
78
+ t = type.class.in?([Hash, Array, Symbol]) ? type : type.to_s.downcase
79
79
  if t.is_a? Hash
80
- # For supporting writing:
80
+ # For supporting this:
81
81
  # form 'desc', data: {
82
82
  # id!: { type: Integer, enum: 0..5, desc: 'user id' }
83
83
  # }
84
- if t.key? :type
85
- SchemaObj.new(t[:type], t).process_for @prop_name, desc_inside: true
84
+ if t.key?(:type)
85
+ SchemaObj.new(t[:type], t).process_for(@prop_name, desc_inside: true)
86
+ # For supporting combined schema in nested schema.
87
+ elsif (t.keys & %i[ one_of any_of all_of not ]).present?
88
+ CombinedSchema.new(t).process_for(@prop_name, desc_inside: true)
86
89
  else
87
90
  recursive_obj_type t
88
91
  end
@@ -104,7 +107,7 @@ module OpenApi
104
107
  end
105
108
 
106
109
  def recursive_obj_type(t) # DSL use { prop_name: prop_type } to represent object structure
107
- return processed_type(t) if !t.is_a?(Hash) || t.key?(:type)
110
+ return processed_type(t) if !t.is_a?(Hash) || (t.keys & %i[ type one_of any_of all_of not ]).present?
108
111
 
109
112
  _schema = {
110
113
  type: 'object',
@@ -116,7 +119,7 @@ module OpenApi
116
119
  _schema[:required] << "#{prop_name}".delete('!') if prop_name['!']
117
120
  _schema[:properties]["#{prop_name}".delete('!').to_sym] = recursive_obj_type prop_type
118
121
  end
119
- _schema.keep_if &value_present
122
+ _schema.keep_if(&value_present)
120
123
  end
121
124
 
122
125
  def recursive_array_type(t)
@@ -185,7 +188,7 @@ module OpenApi
185
188
  def recognize_is_options_in(name)
186
189
  # identify whether `is` patterns matched the name, if so, generate `is`.
187
190
  Config.is_options.each do |pattern|
188
- self._is = pattern or break if name.match? /#{pattern}/
191
+ self._is = pattern or break if name.match?(/#{pattern}/)
189
192
  end if _is.nil?
190
193
  end
191
194
 
data/lib/open_api.rb CHANGED
@@ -5,4 +5,12 @@ require "open_api/dsl"
5
5
 
6
6
  module OpenApi
7
7
  include Generator
8
+
9
+ cattr_accessor :paths_index do
10
+ { }
11
+ end
12
+
13
+ cattr_accessor :docs do
14
+ { }
15
+ end
8
16
  end
@@ -14,12 +14,16 @@ module OpenApi
14
14
  true
15
15
  end
16
16
 
17
+ cattr_accessor :rails_routes_file do
18
+ nil
19
+ end
20
+
17
21
  # Everything about OAS3 is on https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md
18
22
  # Getting started: https://swagger.io/docs/specification/basic-structure/
19
- cattr_accessor :register_docs do
23
+ cattr_accessor :open_api_docs do
20
24
  {
21
25
  # # [REQUIRED] At least one doc.
22
- # zero_rails_api: {
26
+ # zero_rails: {
23
27
  # # [REQUIRED] ZRO will scan all the descendants of the root_controller, and then generate their docs.
24
28
  # root_controller: ApplicationController,
25
29
  #
@@ -36,7 +40,7 @@ module OpenApi
36
40
  end
37
41
 
38
42
  cattr_accessor :is_options do
39
- %w[ email phone password uuid uri url time date ]
43
+ %w[ email phone mobile password uuid uri url time date ]
40
44
  end
41
45
 
42
46
  cattr_accessor :dft_file_format do
@@ -52,58 +56,11 @@ module OpenApi
52
56
  end
53
57
 
54
58
  cattr_accessor :jbuilder_templates do
55
- {
56
- index: (
57
- <<~FILE
58
- # *** Generated by ZRO [ please make sure that you have checked this file ] ***
59
- json.partial! 'api/base', total: @data.size
60
-
61
- json.data do
62
- # @data = @data.page(@_page).per(@_rows) if @_page || @_rows
63
- # json.array! @data do |datum|
64
- json.array! @data.page(@_page).per(@_rows) do |datum|
65
- json.(datum, *datum.show_attrs) if datum.present?
66
- end
67
- end
68
- FILE
69
- ),
70
-
71
- show: (
72
- <<~FILE
73
- # *** Generated by ZRO [ please make sure that you have checked this file ] ***
74
- json.partial! 'api/base', total: 1
75
-
76
- json.data do
77
- json.array! [ @data ] do |datum|
78
- json.(datum, *datum.show_attrs) if datum.present?
79
- end
80
- end
81
- FILE
82
- ),
83
-
84
- success: (
85
- <<~FILE
86
- # *** Generated by ZRO [ please make sure that you have checked this file ] ***
87
- json.partial! 'api/success'
88
- FILE
89
- ),
90
-
91
- success_or_not: (
92
- <<~FILE
93
- # *** Generated by ZRO [ please make sure that you have checked this file ] ***
94
- unless @status
95
- # @_code, @_msg = @error_info.present? ? @error_info : ApiError.action_failed.info
96
- end
97
-
98
- json.partial! 'api/base', total: 0
99
- json.data ''
100
- FILE
101
- ),
102
- }
59
+ { }
103
60
  end
104
61
 
105
62
  def self.docs
106
- register_docs
63
+ open_api_docs
107
64
  end
108
65
  end
109
66
  end
@@ -4,27 +4,45 @@ module OpenApi
4
4
  base.class_eval do
5
5
  module_function
6
6
 
7
- def api name, root_controller:
7
+ def open_api name, root_controller:
8
8
  @api = name
9
- register_docs[name] = { root_controller: root_controller }
9
+ open_api_docs[name] = { root_controller: root_controller }
10
10
  end
11
11
 
12
12
  def info version:, title:, **addition
13
- register_docs[@api].merge! version: version, title: title, **addition
13
+ open_api_docs[@api][:info] = { version: version, title: title, **addition }
14
14
  end
15
15
 
16
16
  def server url, desc: ''
17
- (register_docs[@api][:servers] ||= [ ]) << { url: url, description: desc }
17
+ (open_api_docs[@api][:servers] ||= [ ]) << { url: url, description: desc }
18
18
  end
19
19
 
20
- def security requirement
21
- (register_docs[@api][:global_security] ||= [ ]) << requirement
20
+ def security_scheme scheme_name, other_info# = { }
21
+ other_info[:description] = other_info.delete(:desc) if other_info.key?(:desc)
22
+ (open_api_docs[@api][:security_schemes] ||= { })[scheme_name] = other_info
22
23
  end
23
24
 
24
- alias_method :security_require, :security
25
+ def base_auth scheme_name, other_info = { }
26
+ security_scheme scheme_name, { type: 'http', scheme: 'basic' }.merge(other_info)
27
+ end
28
+
29
+ def bearer_auth scheme_name, format = 'JWT', other_info = { }
30
+ security_scheme scheme_name, { type: 'http', scheme: 'bearer', bearerFormat: format }.merge(other_info)
31
+ end
32
+
33
+ def api_key scheme_name, field:, in:, **other_info
34
+ _in = binding.local_variable_get(:in)
35
+ security_scheme scheme_name, { type: 'apiKey', name: field, in: _in }.merge(other_info)
36
+ end
37
+
38
+ def global_security_require scheme_name, scopes: [ ]
39
+ (open_api_docs[@api][:global_security] ||= [ ]) << { scheme_name => scopes }
40
+ end
25
41
 
26
- def security_scheme scheme_name, schema# = { }
27
- (register_docs[@api][:global_security_schemes] ||= { }).merge! scheme_name => schema
42
+ class << self
43
+ alias global_security global_security_require
44
+ alias global_auth global_security_require
45
+ alias auth_scheme security_scheme
28
46
  end
29
47
  end
30
48
  end
data/lib/open_api/dsl.rb CHANGED
@@ -30,40 +30,35 @@ module OpenApi
30
30
  current_ctrl._process_objs
31
31
  end
32
32
 
33
- def open_api action, summary = '', builder: nil, skip: [ ], use: [ ], &block
33
+ def api action, summary = '', http: nil, builder: nil, skip: [ ], use: [ ], &block
34
34
  apis_tag if @_ctrl_infos.nil?
35
-
36
35
  # select the routing info (corresponding to the current method) from routing list.
37
36
  action_path = "#{@_ctrl_path ||= controller_path}##{action}"
38
- routes_info = ctrl_routes_list&.select { |api| api[:action_path].match? /^#{action_path}$/ }&.first
37
+ routes_info = ctrl_routes_list&.select { |api| api[:action_path].match?(/^#{action_path}$/) }&.first
39
38
  pp "[ZRO Warning] Routing mapping failed: #{@_ctrl_path}##{action}" and return if routes_info.nil?
40
- Generator.generate_builder_file(action_path, builder) if builder.present?
39
+ Generator.generate_builder_file(action_path, builder)
41
40
 
42
- # structural { #path: { #http_method:{ } } }, for pushing into Paths Object.
43
- path = (@_api_infos ||= { })[routes_info[:path]] ||= { }
44
- current_api = path[routes_info[:http_verb]] =
45
- ApiInfoObj.new(action_path, skip: Array(skip), use: Array(use))
46
- .merge! description: '', summary: summary, operationId: action, tags: [@_apis_tag],
47
- parameters: [ ], requestBody: '', responses: { }, security: [ ], servers: [ ]
41
+ api = ApiInfoObj.new(action_path, skip: Array(skip), use: Array(use))
42
+ .merge! description: '', summary: summary, operationId: action, tags: [@_apis_tag],
43
+ parameters: [ ], requestBody: '', responses: { }, security: [ ], servers: [ ]
44
+ [action, :all].each { |blk_key| @_api_dry_blocks&.[](blk_key)&.each { |blk| api.instance_eval(&blk) } }
45
+ api.param_use = [ ] # `skip` and `use` only affect `api_dry`'s blocks
46
+ api.instance_eval(&block) if block_given?
47
+ api._process_objs
48
+ api.delete_if { |_, v| v.blank? }
48
49
 
49
- current_api.tap do |api|
50
- [action, :all].each do |key| # blocks_store_key
51
- @_apis_blocks&.[](key)&.each { |blk| api.instance_eval(&blk) }
52
- end
53
- api.param_use = [ ] # skip 和 use 是对 dry 块而言的
54
- api.instance_eval(&block) if block_given?
55
- api._process_objs
56
- api.delete_if { |_, v| v.blank? }
57
- end
50
+ path = (@_api_infos ||= { })[routes_info[:path]] ||= { }
51
+ http_verbs = (http || routes_info[:http_verb]).split('|')
52
+ http_verbs.each { |verb| path[verb] = api }
58
53
  end
59
54
 
60
55
  # method could be symbol array, like: %i[ .. ]
61
56
  def api_dry action = :all, desc = '', &block
62
- @_apis_blocks ||= { }
57
+ @_api_dry_blocks ||= { }
63
58
  if action.is_a? Array
64
- action.each { |m| (@_apis_blocks[m.to_sym] ||= [ ]) << block }
59
+ action.each { |m| (@_api_dry_blocks[m.to_sym] ||= [ ]) << block }
65
60
  else
66
- (@_apis_blocks[action.to_sym] ||= [ ]) << block
61
+ (@_api_dry_blocks[action.to_sym] ||= [ ]) << block
67
62
  end
68
63
  end
69
64
 
@@ -30,7 +30,7 @@ module OpenApi
30
30
 
31
31
  def param param_type, name, type, required, schema_hash = { }
32
32
  return if param_skip.include?(name)
33
- return if param_use.present? && !param_use.include?(name)
33
+ return if param_use.present? && param_use.exclude?(name)
34
34
 
35
35
  _t = nil
36
36
  schema_hash[:desc] = _t if (_t = param_descs[name]).present?
@@ -38,16 +38,16 @@ module OpenApi
38
38
 
39
39
  param_obj = ParamObj.new(name, param_type, type, required, schema_hash)
40
40
  # The definition of the same name parameter will be overwritten
41
- index = self[:parameters].map { |p| p.processed[:name] if p.is_a? ParamObj }.index name
41
+ index = self[:parameters].map { |p| p.processed[:name] if p.is_a?(ParamObj) }.index name
42
42
  index.present? ? self[:parameters][index] = param_obj : self[:parameters] << param_obj
43
43
  end
44
44
 
45
- # Support this writing: (just like `form '', data: { }`)
45
+ # For supporting this: (just like `form '', data: { }` usage)
46
46
  # do_query by: {
47
47
  # :search_type => { type: String },
48
48
  # :export! => { type: Boolean }
49
49
  # }
50
- %i[header header! path path! query query! cookie cookie!].each do |param_type|
50
+ %i[ header header! path path! query query! cookie cookie! ].each do |param_type|
51
51
  define_method "do_#{param_type}" do |by:|
52
52
  by.each do |key, value|
53
53
  args = [ key.dup.to_s.delete('!').to_sym, value.delete(:type), value ]
@@ -56,7 +56,12 @@ module OpenApi
56
56
  end unless param_type.to_s['!']
57
57
  end
58
58
 
59
- def _param_agent name, type, schema_hash = { }
59
+ def _param_agent name, type = nil, one_of: nil, all_of: nil, any_of: nil, not: nil, **schema_hash
60
+ (schema_hash = type) and (type = type.delete(:type)) if type.is_a?(Hash) && type.key?(:type)
61
+ type = schema_hash[:type] if type.nil?
62
+
63
+ combined_schema = one_of || all_of || any_of || (_not = binding.local_variable_get(:not))
64
+ schema_hash = CombinedSchema.new(one_of: one_of, all_of: all_of, any_of: any_of, _not: _not) if combined_schema
60
65
  param "#{@param_type}".delete('!'), name, type, (@param_type['!'] ? :req : :opt), schema_hash
61
66
  end
62
67
 
@@ -76,9 +81,9 @@ module OpenApi
76
81
  self[:requestBody] = RefObj.new(:requestBody, component_key).process
77
82
  end
78
83
 
79
- def override_response code, type_hash
84
+ def merge_to_resp code, by:
80
85
  _response = self[:responses].fetch(code)
81
- self[:responses][code] = _response.override(type_hash).process
86
+ self[:responses][code] = _response.override(by).process
82
87
  end
83
88
 
84
89
  def response_ref code_compkey_hash
@@ -105,33 +110,31 @@ module OpenApi
105
110
  body! media_type, desc, hash
106
111
  end
107
112
 
108
- def security scheme_name, requirements = [ ]
109
- self[:security] << { scheme_name => requirements }
113
+ def security_require scheme_name, scopes: [ ]
114
+ self[:security] << { scheme_name => scopes }
110
115
  end
111
116
 
117
+ alias security security_require
118
+ alias auth security_require
119
+ alias need_auth security_require
120
+
112
121
  def server url, desc
113
122
  self[:servers] << { url: url, description: desc }
114
123
  end
115
124
 
116
125
  def order *param_names
117
126
  self.param_order = param_names
127
+ self.param_use = param_order if param_use.blank?
128
+ self.param_skip = param_use - param_order
118
129
  end
119
130
 
120
131
  def param_examples exp_by = :all, examples_hash
121
132
  _process_objs
122
133
  exp_by = self[:parameters].map { |p| p[:name] } if exp_by == :all
123
- # TODO: ref obj
124
- # exp_in_params = self[:parameters].map { |p| p[:schema][:examples] }.compact
125
- # examples_hash.map! do |key, value|
126
- # if value == []
127
- # if key.in?(exp_in_params.map { |e| e.keys }.flatten.uniq)
128
- # # TODO
129
- # end
130
- # end
131
- # end
132
134
  self[:examples] = ExampleObj.new(examples_hash, exp_by).process
133
135
  end
134
- alias_method :examples, :param_examples
136
+
137
+ alias examples param_examples
135
138
 
136
139
 
137
140
  def _process_objs
@@ -141,13 +144,13 @@ module OpenApi
141
144
 
142
145
  # Parameters sorting
143
146
  self[:parameters].clone.each do |p|
144
- self[:parameters][param_order.index(p[:name])] = p
147
+ self[:parameters][param_order.index(p[:name]) || -1] = p
145
148
  end if param_order.present?
146
149
 
147
150
  self[:responses]&.each do |code, obj|
148
151
  self[:responses][code] = obj.process if obj.is_a?(ResponseObj)
149
152
  end
150
153
  end
151
- end # ----------------------------------------- end of ApiInfoObj
154
+ end
152
155
  end
153
156
  end