zero-rails_openapi 1.3.3 → 1.4.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,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