zero-rails_openapi 1.7.0 → 2.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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenApi
2
4
  module DSL
3
5
  module SchemaObjHelpers
@@ -7,11 +9,11 @@ module OpenApi
7
9
  # id!: { type: Integer, enum: 0..5, desc: 'user id' }
8
10
  # }, should have description within schema
9
11
  if t.key?(:type)
10
- SchemaObj.new(t[:type], t).process(inside_desc: true)
12
+ SchemaObj.new(t[:type], t).process
11
13
 
12
14
  # For supporting combined schema in nested schema.
13
15
  elsif (t.keys & %i[ one_of any_of all_of not ]).present?
14
- CombinedSchema.new(t).process(inside_desc: true)
16
+ CombinedSchema.new(t).process
15
17
  else
16
18
  obj_type(t)
17
19
  end
@@ -22,59 +24,33 @@ module OpenApi
22
24
 
23
25
  t.each do |prop_name, prop_type|
24
26
  obj_type[:required] << prop_name.to_s.delete('!') if prop_name['!']
25
- obj_type[:properties][prop_name.to_s.delete('!').to_sym] = processed_type(prop_type)
27
+ obj_type[:properties][prop_name.to_s.delete('!').to_sym] = recg_schema_type(prop_type)
26
28
  end
27
29
  obj_type.keep_if &value_present
28
30
  end
29
31
 
30
32
  def array_type(t)
31
- t = t.size == 1 ? t.first : { one_of: t }
32
33
  {
33
34
  type: 'array',
34
- items: processed_type(t)
35
+ items: recg_schema_type(t.one? ? t[0] : { one_of: t })
35
36
  }
36
37
  end
37
38
 
38
- def process_range_enum_and_lth
39
- self[:_enum] = str_range_to_a(_enum) if _enum.is_a?(Range)
40
- self[:_length] = str_range_to_a(_length) if _length.is_a?(Range)
41
-
42
- values = _enum || _value
43
- self._enum = Array(values) if truly_present?(values)
44
- end
45
-
46
39
  def str_range_to_a(val)
47
40
  val_class = val.first.class
48
41
  action = :"to_#{val_class.to_s.downcase[0]}"
49
42
  (val.first.to_s..val.last.to_s).to_a.map(&action)
50
43
  end
51
44
 
52
- def process_enum_info
53
- # Support this writing for auto generating desc from enum.
54
- # enum!: {
55
- # 'all_desc': :all,
56
- # 'one_desc': :one
57
- # }
58
- self._enum ||= (e = self[:enum!])
59
- return unless e.is_a? Hash
60
- @enum_info = e
61
- self._enum = e.values
62
- end
63
-
64
- # TODO: more info and desc configure
65
45
  def auto_generate_desc
66
- return __desc if _enum.blank?
67
-
68
- if @enum_info.present?
69
- @enum_info.each_with_index do |(info, value), index|
70
- __desc.concat "<br/>#{index + 1}/ #{info}: #{value}"
46
+ if @bang_enum.is_a?(Hash)
47
+ @bang_enum.each_with_index do |(info, value), index|
48
+ self._desc = _desc + "<br/>#{index + 1}/ #{info}: #{value}"
71
49
  end
72
50
  else
73
- _enum.each_with_index do |value, index|
74
- __desc.concat "<br/>#{index + 1}/ #{value}"
75
- end
51
+ @bang_enum.each_with_index { |value, index| self._desc = _desc + "<br/>#{index + 1}/ #{value}" }
76
52
  end
77
- __desc
53
+ _desc
78
54
  end
79
55
  end
80
56
  end
@@ -1,16 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+
1
5
  require 'open_api/version'
6
+ require 'open_api/support/tip'
2
7
  require 'open_api/config'
3
- require 'open_api/generator'
8
+ require 'open_api/router'
4
9
  require 'open_api/dsl'
5
10
 
6
11
  module OpenApi
7
- include Generator
12
+ module_function
13
+ cattr_accessor :routes_index, default: { }
14
+ cattr_accessor :docs, default: { }
15
+
16
+ def write_docs(if: true, read_on_controller: true)
17
+ (docs = generate_docs(read_on_controller)) and Tip.loaded
18
+ return unless binding.local_variable_get :if
19
+
20
+ FileUtils.mkdir_p Config.file_output_path
21
+ docs.each do |name, doc|
22
+ File.write "#{Config.file_output_path}/#{name}.json", JSON.pretty_generate(doc)
23
+ Tip.generated(name.to_s.rjust(docs.keys.map(&:size).max))
24
+ end
25
+ end
26
+
27
+ def generate_docs(read_on_controller)
28
+ return Tip.no_config if Config.docs.keys.blank?
29
+ traverse_controllers if read_on_controller
30
+ Dir[*Array(Config.doc_location)].each { |file| require file }
31
+ Config.docs.keys.map { |name| [ name, generate_doc(name) ] }.to_h
32
+ end
33
+
34
+ def generate_doc(doc_name)
35
+ settings, doc = init_hash(doc_name)
36
+ [*(bdc = settings[:base_doc_classes]), *bdc.flat_map(&:descendants)].each do |kls|
37
+ next if kls.oas[:doc].blank?
38
+ doc[:paths].merge!(kls.oas[:apis])
39
+ doc[:tags] << kls.oas[:doc][:tag]
40
+ doc[:components].deep_merge!(kls.oas[:doc][:components] || { })
41
+ OpenApi.routes_index[kls.oas[:route_base]] = doc_name
42
+ end
43
+
44
+ doc[:components].delete_if { |_, v| v.blank? }
45
+ doc[:tags] = doc[:tags].sort { |a, b| a[:name] <=> b[:name] }
46
+ doc[:paths] = doc[:paths].sort.to_h
47
+ OpenApi.docs[doc_name] = doc#.delete_if { |_, v| v.blank? }
48
+ end
8
49
 
9
- cattr_accessor :routes_index do
10
- { }
50
+ def init_hash(doc_name)
51
+ settings = Config.docs[doc_name]
52
+ doc = { openapi: '3.0.0', **settings.slice(:info, :servers) }.merge!(
53
+ security: settings[:global_security], tags: [ ], paths: { },
54
+ components: {
55
+ securitySchemes: settings[:securitySchemes] || { },
56
+ schemas: { }, parameters: { }, requestBodies: { }
57
+ }
58
+ )
59
+ [ settings, doc ]
11
60
  end
12
61
 
13
- cattr_accessor :docs do
14
- { }
62
+ def traverse_controllers
63
+ Dir['./app/controllers/**/*_controller.rb'].each do |file|
64
+ file.sub('./app/controllers/', '').sub('.rb', '').camelize.constantize
65
+ end
15
66
  end
16
67
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'open_api/config_dsl'
2
4
  require 'active_support/all'
3
5
 
@@ -5,51 +7,22 @@ module OpenApi
5
7
  module Config
6
8
  include ConfigDSL
7
9
 
8
- # [REQUIRED] The location where .json doc file will be output.
9
- cattr_accessor :file_output_path do
10
- 'public/open_api'
11
- end
10
+ cattr_accessor :default_run_dry, default: false
12
11
 
13
- cattr_accessor :generate_doc do
14
- true
15
- end
12
+ # [REQUIRED] The location where .json doc file will be output.
13
+ cattr_accessor :file_output_path, default: 'public/open_api'
16
14
 
17
- cattr_accessor :doc_location do
18
- ['./app/**/*_doc.rb']
19
- end
15
+ cattr_accessor :doc_location, default: ['./app/**/*_doc.rb']
20
16
 
21
- cattr_accessor :rails_routes_file do
22
- nil
23
- end
17
+ cattr_accessor :rails_routes_file
24
18
 
25
- cattr_accessor :active_record_base do
26
- nil
27
- end
19
+ cattr_accessor :model_base
28
20
 
29
21
  # Everything about OAS3 is on https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md
30
22
  # Getting started: https://swagger.io/docs/specification/basic-structure/
31
- cattr_accessor :open_api_docs do
32
- {
33
- # # [REQUIRED] At least one doc.
34
- # zero_rails: {
35
- # # [REQUIRED] ZRO will scan all the descendants of the base_doc_classes, and then generate their docs.
36
- # base_doc_classes: [ApplicationController],
37
- #
38
- # # [REQUIRED] Info Object: The info section contains API information
39
- # info: {
40
- # # [REQUIRED] The title of the application.
41
- # title: 'Zero Rails Apis',
42
- # # [REQUIRED] The version of the OpenAPI document
43
- # # (which is distinct from the OpenAPI Specification version or the API implementation version).
44
- # version: '0.0.1'
45
- # }
46
- # }
47
- }
48
- end
23
+ cattr_accessor :open_api_docs, default: { }
49
24
 
50
- cattr_accessor :file_format do
51
- 'binary'
52
- end
25
+ cattr_accessor :file_format, default: 'binary'
53
26
 
54
27
  def self.docs
55
28
  open_api_docs
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenApi
2
4
  module ConfigDSL
3
5
  def self.included(base)
@@ -1,73 +1,60 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'open_api/dsl/api'
2
4
  require 'open_api/dsl/components'
3
- require 'colorize'
4
5
 
5
6
  module OpenApi
6
7
  module DSL
7
- def self.included(base)
8
- base.extend ClassMethods
9
- end
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ def oas
12
+ @oas ||= { doc: { }, dry_blocks: { }, apis: { }, route_base: try(:controller_path),
13
+ tag_name: try(:controller_name)&.camelize }
14
+ end
10
15
 
11
- # TODO: Doc-Block Comments
12
- module ClassMethods
13
16
  def route_base path
14
- @route_base = path
15
- @doc_tag = path.split('/').last.camelize
17
+ oas[:route_base] = path
18
+ oas[:tag_name] = path.split('/').last.camelize
16
19
  end
17
20
 
18
- def doc_tag name: nil, desc: '', external_doc_url: nil
19
- # apis will group by the tags.
20
- @doc_tag = name if name.present?
21
- @doc_tag ||= controller_name.camelize
22
- tag = (@doc_info = { })[:tag] = { name: @doc_tag }
23
- tag[:description] = desc if desc.present?
24
- tag[:externalDocs] = { description: 'ref', url: external_doc_url } if external_doc_url
21
+ # APIs will be grouped by tags.
22
+ def doc_tag name: nil, **tag_info # description: ..., externalDocs: ...
23
+ oas[:doc][:tag] = { name: name || oas[:tag_name], **tag_info }
25
24
  end
26
25
 
27
26
  def components &block
28
- doc_tag if @doc_info.nil?
29
- structure = %i[ schemas responses parameters examples requestBodies securitySchemes ].map { |k| [k, { }] }.to_h
30
- current_doc = Components.new.merge!(structure)
31
- current_doc.instance_exec(&block)
32
- current_doc.process_objs
33
-
34
- (@doc_info[:components] ||= { }).deep_merge!(current_doc)
27
+ doc_tag if oas[:doc].blank?
28
+ (components = Components.new).instance_exec(&block)
29
+ components.process_objs
30
+ (oas[:doc][:components] ||= { }).deep_merge!(components)
35
31
  end
36
32
 
37
- def api action, summary = '', id: nil, tag: nil, http: http_method = nil, skip: [ ], use: [ ], &block
38
- doc_tag if @doc_info.nil?
39
- # select the routing info (corresponding to the current method) from routing list.
40
- action_path = "#{@route_base ||= controller_path}##{action}"
41
- routes = ctrl_routes_list&.select { |api| api[:action_path][/^#{action_path}$/].present? }
42
- return puts ' ZRO'.red + " Route mapping failed: #{action_path}" if routes.blank?
33
+ def api action, summary = '', id: nil, tag: nil, http: nil, dry: Config.default_run_dry, &block
34
+ doc_tag if oas[:doc].blank?
35
+ action_path = "#{oas[:route_base]}##{action}"
36
+ routes = Router.routes_list[oas[:route_base]]
37
+ &.select { |api| api[:action_path][/^#{action_path}$/].present? }
38
+ return Tip.no_route(action_path) if routes.blank?
39
+
40
+ tag = tag || oas[:doc][:tag][:name]
41
+ api = Api.new(action_path, summary: summary, tags: [tag], id: id || "#{tag}_#{action.to_s.camelize}")
42
+ [action, tag, :all].each { |key| api.dry_blocks.concat(oas[:dry_blocks][key] || [ ]) }
43
+ api.run_dsl(dry: dry, &block)
44
+ _set_apis(api, routes, http)
45
+ end
43
46
 
44
- api = Api.new(action_path, skip: Array(skip), use: Array(use))
45
- .merge! description: '', summary: summary, operationId: id || "#{@doc_info[:tag][:name]}_#{action.to_s.camelize}",
46
- tags: [tag || @doc_tag], parameters: [ ], requestBody: '', responses: { }, callbacks: { },
47
- links: { }, security: [ ], servers: [ ]
48
- [action, :all].each { |blk_key| @zro_dry_blocks&.[](blk_key)&.each { |blk| api.instance_eval(&blk) } }
49
- api.param_use = api.param_skip = [ ] # `skip` and `use` only affect `api_dry`'s blocks
50
- api.instance_exec(&block) if block_given?
51
- api.process_objs
52
- api.delete_if { |_, v| v.blank? }
47
+ def api_dry action_or_tags = :all, &block
48
+ Array(action_or_tags).each { |a| (oas[:dry_blocks][a.to_sym] ||= [ ]) << block }
49
+ end
53
50
 
51
+ def _set_apis(api, routes, http)
54
52
  routes.each do |route|
55
- path = (@api_info ||= { })[route[:path]] ||= { }
53
+ path = oas[:apis][route[:path]] ||= { }
56
54
  (http || route[:http_verb]).split('|').each { |verb| path[verb] = api }
57
55
  end
58
-
59
56
  api
60
57
  end
61
-
62
- # method could be symbol array, like: %i[ .. ]
63
- def api_dry action = :all, desc = '', &block
64
- @zro_dry_blocks ||= { }
65
- Array(action).each { |a| (@zro_dry_blocks[a.to_sym] ||= [ ]) << block }
66
- end
67
-
68
- def ctrl_routes_list
69
- Generator.routes_list[@route_base]
70
- end
71
58
  end
72
59
  end
73
60
  end
@@ -1,21 +1,22 @@
1
- require 'open_api/dsl/common_dsl'
1
+ # frozen_string_literal: true
2
+
3
+ require 'open_api/dsl/helpers'
2
4
 
3
5
  module OpenApi
4
6
  module DSL
5
7
  class Api < Hash
6
- include DSL::CommonDSL
7
8
  include DSL::Helpers
8
9
 
9
- attr_accessor :action_path, :param_skip, :param_use, :param_descs, :param_order
10
+ attr_accessor :action_path, :dry_skip, :dry_only, :dry_blocks, :dryed, :param_order
10
11
 
11
- def initialize(action_path = '', skip: [ ], use: [ ])
12
+ def initialize(action_path = '', summary: nil, tags: [ ], id: nil)
12
13
  self.action_path = action_path
13
- self.param_skip = skip
14
- self.param_use = use
15
- self.param_descs = { }
14
+ self.dry_blocks = [ ]
15
+ self.merge!(summary: summary, operationId: id, tags: tags, description: '', parameters: [ ],
16
+ requestBody: nil, responses: { }, callbacks: { }, links: { }, security: [ ], servers: [ ])
16
17
  end
17
18
 
18
- def this_api_is_invalid! explain = ''
19
+ def this_api_is_invalid!(*)
19
20
  self[:deprecated] = true
20
21
  end
21
22
 
@@ -23,41 +24,41 @@ module OpenApi
23
24
  alias this_api_is_unused! this_api_is_invalid!
24
25
  alias this_api_is_under_repair! this_api_is_invalid!
25
26
 
26
- def desc desc, param_descs = { }
27
- self.param_descs = param_descs
27
+ def desc desc
28
28
  self[:description] = desc
29
29
  end
30
30
 
31
- def param param_type, name, type, required, schema_info = { }
32
- return if param_skip.include?(name)
33
- return if param_use.present? && param_use.exclude?(name)
31
+ alias description desc
34
32
 
35
- schema_info[:desc] ||= param_descs[name]
36
- schema_info[:desc!] ||= param_descs[:"#{name}!"]
37
- param_obj = ParamObj.new(name, param_type, type, required, schema_info)
38
- # The definition of the same name parameter will be overwritten
39
- fill_in_parameters(param_obj)
33
+ def dry only: nil, skip: nil, none: false
34
+ return if dry_blocks.blank? || dryed
35
+ self.dry_skip = skip && Array(skip)
36
+ self.dry_only = none ? [:none] : only && Array(only)
37
+ dry_blocks.each { |blk| instance_eval(&blk) }
38
+ self.dry_skip = self.dry_only = nil
39
+ self.dryed = true
40
40
  end
41
41
 
42
- # [ header header! path path! query query! cookie cookie! ]
43
- def _param_agent name, type = nil, **schema_info
44
- schema = process_schema_info(type, schema_info)
45
- return puts ' ZRO'.red + " Syntax Error: param `#{name}` has no schema type!" if schema[:illegal?]
46
- param @param_type, name, schema[:type], @necessity, schema[:combined] || schema[:info]
42
+ def param param_type, name, type, required, schema = { }
43
+ return if dry_skip&.include?(name) || dry_only&.exclude?(name)
44
+
45
+ return unless schema = process_schema_input(type, schema, name)
46
+ param_obj = ParamObj.new(name, param_type, type, required, schema)
47
+ # The definition of the same name parameter will be overwritten
48
+ index = self[:parameters].map(&:name).index(param_obj.name)
49
+ index ? self[:parameters][index] = param_obj : self[:parameters] << param_obj
47
50
  end
48
51
 
49
- # For supporting this: (just like `form '', data: { }` usage)
50
- # do_query by: {
51
- # :search_type => { type: String },
52
- # :export! => { type: Boolean }
53
- # }
52
+ alias parameter param
53
+
54
54
  %i[ header header! path path! query query! cookie cookie! ].each do |param_type|
55
- define_method "do_#{param_type}" do |by:, **common_schema|
56
- by.each do |param_name, schema|
57
- action = "#{param_type}#{param_name['!']}".sub('!!', '!')
58
- type, schema = schema.is_a?(Hash) ? [schema[:type], schema] : [schema, { }]
59
- args = [ param_name.to_s.delete('!').to_sym, type, schema.reverse_merge!(common_schema) ]
60
- send(action, *args)
55
+ define_method param_type do |name, type = nil, **schema|
56
+ param param_type, name, type, (param_type['!'] ? :req : :opt), schema
57
+ end
58
+
59
+ define_method "in_#{param_type}" do |params|
60
+ params.each_pair do |param_name, schema|
61
+ param param_type, param_name, nil, (param_type['!'] || param_name['!'] ? :req : :opt), schema
61
62
  end
62
63
  end
63
64
  end
@@ -66,22 +67,21 @@ module OpenApi
66
67
  self[:parameters] += [component_key, *keys].map { |key| RefObj.new(:parameter, key) }
67
68
  end
68
69
 
69
- # options: `exp_by` and `examples`
70
- def request_body required, media_type, data: { }, **options
71
- desc = options.delete(:desc) || ''
72
- self[:requestBody] = RequestBodyObj.new(required, desc) unless self[:requestBody].is_a?(RequestBodyObj)
73
- self[:requestBody].add_or_fusion(media_type, { data: data , **options })
74
- end
75
-
76
- # [ body body! ]
77
- def _request_body_agent media_type, data: { }, **options
78
- request_body @necessity, media_type, data: data, **options
70
+ # options: `exp_params` and `examples`
71
+ def request_body required, media_type, data: { }, desc: '', **options
72
+ (self[:requestBody] ||= RequestBodyObj.new(required, desc)).absorb(media_type, { data: data , **options })
79
73
  end
80
74
 
81
75
  def body_ref component_key
82
76
  self[:requestBody] = RefObj.new(:requestBody, component_key)
83
77
  end
84
78
 
79
+ %i[ body body! ].each do |method|
80
+ define_method method do |media_type, data: { }, **options|
81
+ request_body (method['!'] ? :req : :opt), media_type, data: data, **options
82
+ end
83
+ end
84
+
85
85
  def form data:, **options
86
86
  body :form, data: data, **options
87
87
  end
@@ -90,21 +90,20 @@ module OpenApi
90
90
  body! :form, data: data, **options
91
91
  end
92
92
 
93
- def data name, type = nil, schema_info = { }
94
- schema_info[:type] = type if type.present?
95
- form data: { name => schema_info }
93
+ def data name, type = nil, schema = { }
94
+ schema[:type] = type if type.present?
95
+ form data: { name => schema }
96
96
  end
97
97
 
98
- def file media_type, data: { type: File }, **options
99
- body media_type, data: data, **options
98
+ def response code, desc, media_type = nil, data: { }
99
+ (self[:responses][code] ||= ResponseObj.new(desc)).absorb(desc, media_type, { data: data })
100
100
  end
101
101
 
102
- def file! media_type, data: { type: File }, **options
103
- body! media_type, data: data, **options
104
- end
102
+ alias_method :resp, :response
103
+ alias_method :error, :response
105
104
 
106
- def response_ref code_compkey_hash
107
- code_compkey_hash.each { |code, component_key| self[:responses][code] = RefObj.new(:response, component_key) }
105
+ def response_ref code_and_compkey # = { }
106
+ code_and_compkey.each { |code, component_key| self[:responses][code] = RefObj.new(:response, component_key) }
108
107
  end
109
108
 
110
109
  def security_require scheme_name, scopes: [ ]
@@ -113,7 +112,7 @@ module OpenApi
113
112
 
114
113
  alias security security_require
115
114
  alias auth security_require
116
- alias need_auth security_require
115
+ alias auth_with security_require
117
116
 
118
117
  def callback event_name, http_method, callback_url, &block
119
118
  self[:callbacks].deep_merge! CallbackObj.new(event_name, http_method, callback_url, &block).process
@@ -123,26 +122,22 @@ module OpenApi
123
122
  self[:servers] << { url: url, description: desc }
124
123
  end
125
124
 
126
- def order *param_names
127
- self.param_order = param_names
128
- # be used when `api_dry`
129
- self.param_use = param_order if param_use.blank?
130
- self.param_skip = param_use - param_order
131
- end
132
-
133
- def param_examples exp_by = :all, examples_hash
134
- exp_by = self[:parameters].map(&:name) if exp_by == :all
135
- self[:examples] = ExampleObj.new(examples_hash, exp_by, multiple: true).process
125
+ def param_examples exp_params = :all, examples_hash
126
+ exp_params = self[:parameters].map(&:name) if exp_params == :all
127
+ self[:examples] = ExampleObj.new(examples_hash, exp_params, multiple: true).process
136
128
  end
137
129
 
138
130
  alias examples param_examples
139
131
 
140
- def process_objs
141
- self[:parameters].map!(&:process)
142
- self[:parameters].sort_by! { |param| param_order.index(param[:name]) || Float::INFINITY } if param_order.present?
132
+ def run_dsl(dry: false, &block)
133
+ instance_exec(&block) if block_given?
134
+ dry() if dry
143
135
 
136
+ self[:parameters].map!(&:process)
144
137
  self[:requestBody] = self[:requestBody].try(:process)
145
138
  self[:responses].each { |code, response| self[:responses][code] = response.process }
139
+ self[:responses] = self[:responses].sort.to_h
140
+ self.delete_if { |_, v| v.blank? }
146
141
  end
147
142
  end
148
143
  end