From f70e6b204427578ba0cf1488239ed7fd00a25983 Mon Sep 17 00:00:00 2001 From: zhandao Date: Fri, 1 Feb 2019 16:19:21 +0800 Subject: [PATCH 01/15] Refactor: start --- lib/oas_objs/schema_obj_helpers.rb | 2 +- lib/open_api.rb | 63 +++++++++++++++++++++++++++--- lib/open_api/config.rb | 2 +- lib/open_api/dsl.rb | 2 +- lib/open_api/dsl/api.rb | 2 +- lib/open_api/dsl/components.rb | 4 +- lib/open_api/dsl/helpers.rb | 37 +++--------------- lib/open_api/generator.rb | 63 ------------------------------ lib/open_api/support/tip.rb | 26 ++++++++++++ lib/open_api/version.rb | 2 +- zero-rails_openapi.gemspec | 12 +++--- 11 files changed, 103 insertions(+), 112 deletions(-) create mode 100644 lib/open_api/support/tip.rb diff --git a/lib/oas_objs/schema_obj_helpers.rb b/lib/oas_objs/schema_obj_helpers.rb index ea0c707..33e7e55 100644 --- a/lib/oas_objs/schema_obj_helpers.rb +++ b/lib/oas_objs/schema_obj_helpers.rb @@ -69,7 +69,7 @@ def auto_generate_desc if @enum_info.present? @enum_info.each_with_index do |(info, value), index| - self.__desc = __desc + "
#{index + 1}/ #{info}: #{value}" + self.__desc = __desc + "
#{index + 1}/ #{info}: #{value}" # FIXME end else _enum.each_with_index do |value, index| diff --git a/lib/open_api.rb b/lib/open_api.rb index fceeea3..3003353 100644 --- a/lib/open_api.rb +++ b/lib/open_api.rb @@ -1,18 +1,71 @@ # frozen_string_literal: true +require 'colorize' + require 'open_api/version' +require 'open_api/support/tip' require 'open_api/config' require 'open_api/generator' require 'open_api/dsl' module OpenApi - include Generator + module_function + cattr_accessor :routes_index, default: { } + cattr_accessor :docs, default: { } + + def write_docs(args = { if: true }) + docs = generate_docs + Tip.loaded if ENV['RAILS_ENV'] + return unless args[:if] + + FileUtils.mkdir_p Config.file_output_path + docs.each do |name, doc| + File.write "#{Config.file_output_path}/#{name}.json", JSON.pretty_generate(doc) + Tip.generated(name.to_s.rjust(docs.keys.map(&:size).max)) + end + end + + def generate_docs + return Tip.no_config if Config.docs.keys.blank? + + # TODO + # :nocov: + Dir['./app/controllers/**/*_controller.rb'].each do |file| + file.sub('./app/controllers/', '').sub('.rb', '').camelize.constantize + end + # :nocov: + Dir[*Array(Config.doc_location)].each { |file| require file } + Config.docs.keys.map { |name| [ name, generate_doc(name) ] }.to_h + end + + def generate_doc(doc_name) + settings, doc = init_hash(doc_name) + [*(bdc = settings[:base_doc_classes]), *bdc.flat_map(&:descendants)].each do |ctrl| + doc_info = ctrl.instance_variable_get('@doc_info') + next if doc_info.nil? + + doc[:paths].merge!(ctrl.instance_variable_get('@api_info') || { }) + doc[:tags] << doc_info[:tag] + doc[:components].deep_merge!(doc_info[:components] || { }) + OpenApi.routes_index[ctrl.instance_variable_get('@route_base')] = doc_name + end + + doc[:components].delete_if { |_, v| v.blank? } + doc[:tags] = doc[:tags].sort { |a, b| a[:name] <=> b[:name] } + doc[:paths] = doc[:paths].sort.to_h - cattr_accessor :routes_index do - { } + OpenApi.docs[doc_name] = doc#.delete_if { |_, v| v.blank? } end - cattr_accessor :docs do - { } + def init_hash(doc_name) + settings = Config.docs[doc_name] + doc = { openapi: '3.0.0', **settings.slice(:info, :servers) }.merge!( + security: settings[:global_security], tags: [ ], paths: { }, + components: { + securitySchemes: settings[:securitySchemes] || { }, + schemas: { }, parameters: { }, requestBodies: { } + } + ) + [ settings, doc ] end end diff --git a/lib/open_api/config.rb b/lib/open_api/config.rb index f928f79..192dcbb 100644 --- a/lib/open_api/config.rb +++ b/lib/open_api/config.rb @@ -34,7 +34,7 @@ module Config { # # [REQUIRED] At least one doc. # zero_rails: { - # # [REQUIRED] ZRO will scan all the descendants of the base_doc_classes, and then generate their docs. + # # [REQUIRED] OpenApi will scan all the descendants of the base_doc_classes, and then generate their docs. # base_doc_classes: [ApplicationController], # # # [REQUIRED] Info Object: The info section contains API information diff --git a/lib/open_api/dsl.rb b/lib/open_api/dsl.rb index 02f8d4c..1e7dbcc 100644 --- a/lib/open_api/dsl.rb +++ b/lib/open_api/dsl.rb @@ -41,7 +41,7 @@ def api action, summary = '', id: nil, tag: nil, http: http_method = nil, skip: # select the routing info (corresponding to the current method) from routing list. action_path = "#{@route_base ||= controller_path}##{action}" routes = ctrl_routes_list&.select { |api| api[:action_path][/^#{action_path}$/].present? } - return puts ' ZRO'.red + " Route mapping failed: #{action_path}" if routes.blank? + return Tip.no_route(action_path) if routes.blank? api = Api.new(action_path, skip: Array(skip), use: Array(use)) .merge! description: '', summary: summary, operationId: id || "#{@doc_info[:tag][:name]}_#{action.to_s.camelize}", diff --git a/lib/open_api/dsl/api.rb b/lib/open_api/dsl/api.rb index fa079f1..23644ed 100644 --- a/lib/open_api/dsl/api.rb +++ b/lib/open_api/dsl/api.rb @@ -44,7 +44,7 @@ def param param_type, name, type, required, schema_info = { } # [ header header! path path! query query! cookie cookie! ] def _param_agent name, type = nil, **schema_info schema = process_schema_info(type, schema_info) - return puts ' ZRO'.red + " Syntax Error: param `#{name}` has no schema type!" if schema[:illegal?] + return Tip.param_no_type(name) if schema[:illegal?] param @param_type, name, schema[:type], @necessity, schema[:combined] || schema[:info] end diff --git a/lib/open_api/dsl/components.rb b/lib/open_api/dsl/components.rb index e780366..230df63 100644 --- a/lib/open_api/dsl/components.rb +++ b/lib/open_api/dsl/components.rb @@ -10,7 +10,7 @@ class Components < Hash def schema component_key, type = nil, **schema_info schema = process_schema_info(type, schema_info, model: component_key) - return puts ' ZRO'.red + " Syntax Error: component schema `#{component_key}` has no type!" if schema[:illegal?] + return Tip.schema_no_type(component_key) if schema[:illegal?] self[:schemas][component_key.to_s.to_sym] = (schema[:combined] or SchemaObj.new(type = schema[:info], { })).process end @@ -29,7 +29,7 @@ def param component_key, param_type, name, type, required, schema_info = { } # [ header header! path path! query query! cookie cookie! ] def _param_agent component_key, name, type = nil, **schema_info schema = process_schema_info(type, schema_info) - return puts ' ZRO'.red + " Syntax Error: param `#{name}` has no schema type!" if schema[:illegal?] + return Tip.param_no_type(name) if schema[:illegal?] param component_key, @param_type, name, schema[:type], @necessity, schema[:combined] || schema[:info] end diff --git a/lib/open_api/dsl/helpers.rb b/lib/open_api/dsl/helpers.rb index 4e5940c..d1ba3ed 100644 --- a/lib/open_api/dsl/helpers.rb +++ b/lib/open_api/dsl/helpers.rb @@ -9,36 +9,11 @@ def self.included(base) # :nocov: def load_schema(model) # TODO: test - # About `show_attrs`, see: - # (1) BuilderSupport module: https://github.com/zhandao/zero-rails/blob/master/app/models/concerns/builder_support.rb - # (2) config in model: https://github.com/zhandao/zero-rails/tree/master/app/models/good.rb - # (3) jbuilder file: https://github.com/zhandao/zero-rails/blob/master/app/views/api/v1/goods/index.json.jbuilder - # In a word, BuilderSupport let you control the `output fields and nested association infos` very easily. - if model.respond_to? :show_attrs - _load_schema_based_on_show_attr(model) - else - model.columns.map { |column| _type_mapping(column) } - end.compact.reduce({ }, :merge!) rescue '' - end - - def _type_mapping(column) - type = column.sql_type_metadata.type.to_s.camelize - type = 'DateTime' if type == 'Datetime' - { column.name.to_sym => Object.const_get(type) } - end - - def _load_schema_based_on_show_attr(model) - columns = model.column_names.map(&:to_sym) - model.show_attrs.map do |attr| - if columns.include?(attr) - index = columns.index(attr) - _type_mapping(model.columns[index]) - elsif attr[/_info/] - # TODO: 如何获知关系是 many?因为不能只判断结尾是否 ‘s’ - assoc_model = Object.const_get(attr.to_s.split('_').first.singularize.camelize) - { attr => load_schema(assoc_model) } - end rescue next - end + model.columns.map do |column| + type = column.sql_type_metadata.type.to_s.camelize + type = 'DateTime' if type == 'Datetime' + [ column.name.to_sym, Object.const_get(type) ] + end.to_h rescue '' end # :nocov: @@ -55,7 +30,7 @@ def _combined_schema(one_of: nil, all_of: nil, any_of: nil, not: nil, **other) def process_schema_info(schema_type, schema_info, model: nil) combined_schema = _combined_schema(schema_info) type = schema_info[:type] ||= schema_type - schema_info = load_schema(model) if model.try(:superclass) == (Config.active_record_base || ApplicationRecord) + schema_info = load_schema(model) if Config.active_record_base && model.try(:superclass) == Config.active_record_base { illegal?: type.nil? && combined_schema.nil?, combined: combined_schema, diff --git a/lib/open_api/generator.rb b/lib/open_api/generator.rb index a920f01..334a491 100644 --- a/lib/open_api/generator.rb +++ b/lib/open_api/generator.rb @@ -1,74 +1,11 @@ # frozen_string_literal: true require 'open_api/config' -require 'colorize' module OpenApi module Generator module_function - def self.included(base) - base.extend ClassMethods - end - - module ClassMethods - def generate_docs(doc_name = nil) - return puts ' ZRO'.red + ' No documents have been configured!' if Config.docs.keys.blank? - - # TODO - # :nocov: - Dir['./app/controllers/**/*_controller.rb'].each do |file| - file.sub('./app/controllers/', '').sub('.rb', '').camelize.constantize - end - # :nocov: - Dir[*Array(Config.doc_location)].each { |file| require file } - (doc_name || Config.docs.keys).map { |name| { name => generate_doc(name) } }.reduce({ }, :merge!) - end - - def generate_doc(doc_name) - settings = Config.docs[doc_name] - doc = { openapi: '3.0.0', **settings.slice(:info, :servers) }.merge!( - security: settings[:global_security], tags: [ ], paths: { }, - components: { - securitySchemes: settings[:securitySchemes] || { }, - schemas: { }, parameters: { }, requestBodies: { } - } - ) - - [*(bdc = settings[:base_doc_classes]), *bdc.flat_map(&:descendants)].each do |ctrl| - doc_info = ctrl.instance_variable_get('@doc_info') - next if doc_info.nil? - - doc[:paths].merge!(ctrl.instance_variable_get('@api_info') || { }) - doc[:tags] << doc_info[:tag] - doc[:components].deep_merge!(doc_info[:components] || { }) - OpenApi.routes_index[ctrl.instance_variable_get('@route_base')] = doc_name - end - - doc[:components].delete_if { |_, v| v.blank? } - doc[:tags] = doc[:tags].sort { |a, b| a[:name] <=> b[:name] } - doc[:paths] = doc[:paths].sort.to_h - - OpenApi.docs[doc_name] = doc#.delete_if { |_, v| v.blank? } - end - - def write_docs(generate_files: true) - docs = generate_docs - puts ' ZRO loaded.'.green if ENV['RAILS_ENV'] - return unless generate_files - # :nocov: - output_path = Config.file_output_path - FileUtils.mkdir_p output_path - max_length = docs.keys.map(&:size).sort.last - docs.each do |doc_name, doc| - puts ' ZRO'.green + " `#{doc_name.to_s.rjust(max_length)}.json` has been generated." - File.open("#{output_path}/#{doc_name}.json", 'w') { |file| file.write JSON.pretty_generate doc } - end - # :nocov: - end - end - # end of module - def routes @routes ||= if (file = Config.rails_routes_file) diff --git a/lib/open_api/support/tip.rb b/lib/open_api/support/tip.rb new file mode 100644 index 0000000..0ed5c01 --- /dev/null +++ b/lib/open_api/support/tip.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module OpenApi + module Tip + extend self + + def no_config; puts ' OpenApi'.red + ' No documents have been configured!' end + def loaded; puts ' OpenApi'.green + ' loaded' end + + def generated(name) + puts ' OpenApi'.green + " `#{name}.json` has been generated." + end + + def schema_no_type(component_key) + puts ' OpenApi'.red + " Syntax Error: component schema `#{component_key}` has no type!" + end + + def param_no_type(name) + puts ' OpenApi'.red + " Syntax Error: param `#{name}` has no schema type!" + end + + def no_route(action_path) + puts ' OpenApi'.red + " Route mapping failed: #{action_path}" + end + end +end diff --git a/lib/open_api/version.rb b/lib/open_api/version.rb index 4fb21eb..01938b9 100644 --- a/lib/open_api/version.rb +++ b/lib/open_api/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module OpenApi - VERSION = '1.7.0' + VERSION = '2.0.0' end diff --git a/zero-rails_openapi.gemspec b/zero-rails_openapi.gemspec index d6ed7bf..8f02d40 100644 --- a/zero-rails_openapi.gemspec +++ b/zero-rails_openapi.gemspec @@ -23,15 +23,15 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.3.0' - spec.add_development_dependency 'bundler', '~> 1.16.a' - spec.add_development_dependency 'rake', '~> 10.0' - spec.add_development_dependency 'rspec', '~> 3.0' + spec.add_development_dependency 'bundler' + spec.add_development_dependency 'rake' + spec.add_development_dependency 'rspec' spec.add_development_dependency 'simplecov' spec.add_development_dependency 'pry' - spec.add_runtime_dependency 'colorize' - spec.add_runtime_dependency 'activesupport', '>= 4.1' - spec.add_runtime_dependency 'rails', '>= 4.1' + spec.add_dependency 'colorize' + spec.add_dependency 'activesupport', '>= 4.1' + spec.add_dependency 'rails', '>= 4.1' # spec.post_install_message = "" end From deb71527f5cca4bd8cf598aea660eca49a08d855 Mon Sep 17 00:00:00 2001 From: zhandao Date: Fri, 1 Feb 2019 18:02:17 +0800 Subject: [PATCH 02/15] Refactor: Packing instance variables --- lib/open_api.rb | 37 +++++++++++++-------------- lib/open_api/dsl.rb | 50 ++++++++++++++++--------------------- lib/open_api/generator.rb | 2 -- lib/open_api/support/tip.rb | 2 +- spec/components_spec.rb | 2 +- spec/dsl_spec.rb | 7 ++---- spec/spec_dsl.rb | 4 +-- spec/support/api_doc.rb | 2 +- 8 files changed, 47 insertions(+), 59 deletions(-) diff --git a/lib/open_api.rb b/lib/open_api.rb index 3003353..47af9f8 100644 --- a/lib/open_api.rb +++ b/lib/open_api.rb @@ -13,10 +13,9 @@ module OpenApi cattr_accessor :routes_index, default: { } cattr_accessor :docs, default: { } - def write_docs(args = { if: true }) - docs = generate_docs - Tip.loaded if ENV['RAILS_ENV'] - return unless args[:if] + def write_docs(if: true, read_on_controller: true) + (docs = generate_docs(read_on_controller)) and Tip.loaded + return unless binding.local_variable_get :if FileUtils.mkdir_p Config.file_output_path docs.each do |name, doc| @@ -25,29 +24,23 @@ def write_docs(args = { if: true }) end end - def generate_docs + def generate_docs(read_on_controller) return Tip.no_config if Config.docs.keys.blank? - - # TODO - # :nocov: - Dir['./app/controllers/**/*_controller.rb'].each do |file| - file.sub('./app/controllers/', '').sub('.rb', '').camelize.constantize - end - # :nocov: + traverse_controllers if read_on_controller Dir[*Array(Config.doc_location)].each { |file| require file } Config.docs.keys.map { |name| [ name, generate_doc(name) ] }.to_h end def generate_doc(doc_name) settings, doc = init_hash(doc_name) - [*(bdc = settings[:base_doc_classes]), *bdc.flat_map(&:descendants)].each do |ctrl| - doc_info = ctrl.instance_variable_get('@doc_info') - next if doc_info.nil? + [*(bdc = settings[:base_doc_classes]), *bdc.flat_map(&:descendants)].each do |kls| + next if kls.oas[:doc].blank? - doc[:paths].merge!(ctrl.instance_variable_get('@api_info') || { }) - doc[:tags] << doc_info[:tag] - doc[:components].deep_merge!(doc_info[:components] || { }) - OpenApi.routes_index[ctrl.instance_variable_get('@route_base')] = doc_name + doc[:paths].merge!(kls.oas[:apis]) + binding.pry unless kls.oas[:doc][:tag] + doc[:tags] << kls.oas[:doc][:tag] + doc[:components].deep_merge!(kls.oas[:doc][:components] || { }) + OpenApi.routes_index[kls.oas[:route_base]] = doc_name end doc[:components].delete_if { |_, v| v.blank? } @@ -68,4 +61,10 @@ def init_hash(doc_name) ) [ settings, doc ] end + + def traverse_controllers + Dir['./app/controllers/**/*_controller.rb'].each do |file| + file.sub('./app/controllers/', '').sub('.rb', '').camelize.constantize + end + end end diff --git a/lib/open_api/dsl.rb b/lib/open_api/dsl.rb index 1e7dbcc..b33c92b 100644 --- a/lib/open_api/dsl.rb +++ b/lib/open_api/dsl.rb @@ -2,73 +2,67 @@ require 'open_api/dsl/api' require 'open_api/dsl/components' -require 'colorize' module OpenApi module DSL - def self.included(base) - base.extend ClassMethods - end + extend ActiveSupport::Concern + + class_methods do + def oas + @oas ||= { doc: { }, dry_blocks: { }, apis: { }, tag_name: try(:controller_name)&.camelize } + end - # TODO: Doc-Block Comments - module ClassMethods def route_base path - @route_base = path - @doc_tag = path.split('/').last.camelize + oas[:route_base] = path + oas[:tag_name] = path.split('/').last.camelize end - def doc_tag name: nil, desc: '', external_doc_url: nil - # apis will group by the tags. - @doc_tag = name if name.present? - @doc_tag ||= controller_name.camelize - tag = (@doc_info = { })[:tag] = { name: @doc_tag } - tag[:description] = desc if desc.present? - tag[:externalDocs] = { description: 'ref', url: external_doc_url } if external_doc_url + # APIs will be grouped by tags. + def doc_tag name: nil, **tag_info # description: ..., externalDocs: ... + oas[:doc][:tag] = { name: name || oas[:tag_name], **tag_info } end def components &block - doc_tag if @doc_info.nil? + doc_tag if oas[:doc].blank? structure = %i[ schemas responses parameters examples requestBodies securitySchemes ].map { |k| [k, { }] }.to_h current_doc = Components.new.merge!(structure) current_doc.instance_exec(&block) current_doc.process_objs - (@doc_info[:components] ||= { }).deep_merge!(current_doc) + (oas[:doc][:components] ||= { }).deep_merge!(current_doc) end - def api action, summary = '', id: nil, tag: nil, http: http_method = nil, skip: [ ], use: [ ], &block - doc_tag if @doc_info.nil? + def api action, summary = '', id: nil, tag: nil, http: nil, skip: [ ], use: [ ], &block + doc_tag if oas[:doc].blank? # select the routing info (corresponding to the current method) from routing list. - action_path = "#{@route_base ||= controller_path}##{action}" + action_path = "#{oas[:route_base] ||= controller_path}##{action}" routes = ctrl_routes_list&.select { |api| api[:action_path][/^#{action_path}$/].present? } return Tip.no_route(action_path) if routes.blank? api = Api.new(action_path, skip: Array(skip), use: Array(use)) - .merge! description: '', summary: summary, operationId: id || "#{@doc_info[:tag][:name]}_#{action.to_s.camelize}", - tags: [tag || @doc_tag], parameters: [ ], requestBody: '', responses: { }, callbacks: { }, + .merge! description: '', summary: summary, operationId: id || "#{oas[:doc][:tag][:name]}_#{action.to_s.camelize}", + tags: [tag || oas[:doc][:tag][:name]], parameters: [ ], requestBody: '', responses: { }, callbacks: { }, links: { }, security: [ ], servers: [ ] - [action, :all].each { |blk_key| @zro_dry_blocks&.[](blk_key)&.each { |blk| api.instance_eval(&blk) } } + [action, :all].each { |blk_key| oas[:dry_blocks][blk_key]&.each { |blk| api.instance_eval(&blk) } } api.param_use = api.param_skip = [ ] # `skip` and `use` only affect `api_dry`'s blocks api.instance_exec(&block) if block_given? api.process_objs api.delete_if { |_, v| v.blank? } routes.each do |route| - path = (@api_info ||= { })[route[:path]] ||= { } + path = oas[:apis][route[:path]] ||= { } (http || route[:http_verb]).split('|').each { |verb| path[verb] = api } end api end - # method could be symbol array, like: %i[ .. ] def api_dry action = :all, desc = '', &block - @zro_dry_blocks ||= { } - Array(action).each { |a| (@zro_dry_blocks[a.to_sym] ||= [ ]) << block } + Array(action).each { |a| (oas[:dry_blocks][a.to_sym] ||= [ ]) << block } end def ctrl_routes_list - Generator.routes_list[@route_base] + Generator.routes_list[oas[:route_base]] end end end diff --git a/lib/open_api/generator.rb b/lib/open_api/generator.rb index 334a491..2a1bfd5 100644 --- a/lib/open_api/generator.rb +++ b/lib/open_api/generator.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'open_api/config' - module OpenApi module Generator module_function diff --git a/lib/open_api/support/tip.rb b/lib/open_api/support/tip.rb index 0ed5c01..63ad937 100644 --- a/lib/open_api/support/tip.rb +++ b/lib/open_api/support/tip.rb @@ -5,7 +5,7 @@ module Tip extend self def no_config; puts ' OpenApi'.red + ' No documents have been configured!' end - def loaded; puts ' OpenApi'.green + ' loaded' end + def loaded; puts ' OpenApi'.green + ' loaded' if ENV['RAILS_ENV'] end def generated(name) puts ' OpenApi'.green + " `#{name}.json` has been generated." diff --git a/spec/components_spec.rb b/spec/components_spec.rb index 860d5a3..95a17ad 100644 --- a/spec/components_spec.rb +++ b/spec/components_spec.rb @@ -1,7 +1,7 @@ require 'spec_dsl' def clear - after_do { @doc_info[:components] = { } } + after_do { oas[:doc][:components] = { } } end RSpec.describe OpenApi::DSL::Components do diff --git a/spec/dsl_spec.rb b/spec/dsl_spec.rb index 46bbb63..d620455 100644 --- a/spec/dsl_spec.rb +++ b/spec/dsl_spec.rb @@ -4,7 +4,7 @@ set_doc desc :route_base, subject: :paths do - before_do { route_base 'examples' } + before_do { @oas = nil; route_base 'examples' } make -> { api :action }, 'is not mapped to goods#action', eq: Hash.new make -> { api :index }, 'is mapped to examples#index', has_key!: :'examples/index' @@ -14,22 +14,19 @@ after_do { route_base 'goods' } end - desc :doc_tag do make -> do - doc_tag name: :Other, desc: 'tag desc', external_doc_url: 'url' + doc_tag name: :Other, description: 'tag desc' api :action end, has_keys!: %i[ tags paths ] focus_on :tags, 0 expect_its :name, eq: :Other expect_its :description, eq: 'tag desc' - expect_its :externalDocs, eq: { description: 'ref', url: 'url' } focus_on :paths, :'goods/action', :get, :tags, 0 expect_it eq: :Other end - desc :api, subject: :paths do context 'when this action is not configured routing' do make -> { api :no_routing_action }, 'refuses to be generated', eq: Hash.new diff --git a/spec/spec_dsl.rb b/spec/spec_dsl.rb index f007ac5..fa04dc5 100644 --- a/spec/spec_dsl.rb +++ b/spec/spec_dsl.rb @@ -23,8 +23,8 @@ def _dsl(block) end def _write_docs - OpenApi.write_docs(generate_files: false) - GoodsDoc.class_eval { undo_dry; @api_info = { } } + OpenApi.write_docs(if: false) + GoodsDoc.class_eval { undo_dry; oas[:apis] = { } } end def _do!(block) diff --git a/spec/support/api_doc.rb b/spec/support/api_doc.rb index 850561d..3fa57b3 100644 --- a/spec/support/api_doc.rb +++ b/spec/support/api_doc.rb @@ -3,7 +3,7 @@ class ApiDoc < Object class << self def undo_dry - @zro_dry_blocks = nil + oas[:dry_blocks] = { } end def inherited(subclass) From 73791fce333b9f505b1b0911b5d3d3e5d69296fa Mon Sep 17 00:00:00 2001 From: zhandao Date: Fri, 1 Feb 2019 19:27:18 +0800 Subject: [PATCH 03/15] Refactor & Support using `tag` in api_dry --- lib/oas_objs/callback_obj.rb | 3 +-- lib/open_api/dsl.rb | 46 ++++++++++++++++++---------------- lib/open_api/dsl/api.rb | 3 +++ lib/open_api/dsl/components.rb | 4 +++ 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/lib/oas_objs/callback_obj.rb b/lib/oas_objs/callback_obj.rb index 5bd7974..89f96a5 100644 --- a/lib/oas_objs/callback_obj.rb +++ b/lib/oas_objs/callback_obj.rb @@ -37,8 +37,7 @@ def processed_url end def processed_block - api = Api.new.merge! parameters: [ ], requestBody: '', responses: { } - api.instance_exec(&(self.block || -> { })) + (api = Api.new).instance_exec(&(self.block || -> { })) api.process_objs api.delete_if { |_, v| v.blank? } end diff --git a/lib/open_api/dsl.rb b/lib/open_api/dsl.rb index b33c92b..e8d89d6 100644 --- a/lib/open_api/dsl.rb +++ b/lib/open_api/dsl.rb @@ -9,7 +9,8 @@ module DSL class_methods do def oas - @oas ||= { doc: { }, dry_blocks: { }, apis: { }, tag_name: try(:controller_name)&.camelize } + @oas ||= { doc: { }, dry_blocks: { }, apis: { }, route_base: try(:controller_path), + tag_name: try(:controller_name)&.camelize } end def route_base path @@ -24,46 +25,49 @@ def doc_tag name: nil, **tag_info # description: ..., externalDocs: ... def components &block doc_tag if oas[:doc].blank? - structure = %i[ schemas responses parameters examples requestBodies securitySchemes ].map { |k| [k, { }] }.to_h - current_doc = Components.new.merge!(structure) - current_doc.instance_exec(&block) + (current_doc = Components.new).instance_exec(&block) current_doc.process_objs - (oas[:doc][:components] ||= { }).deep_merge!(current_doc) end def api action, summary = '', id: nil, tag: nil, http: nil, skip: [ ], use: [ ], &block doc_tag if oas[:doc].blank? - # select the routing info (corresponding to the current method) from routing list. - action_path = "#{oas[:route_base] ||= controller_path}##{action}" + action_path = "#{oas[:route_base]}##{action}" routes = ctrl_routes_list&.select { |api| api[:action_path][/^#{action_path}$/].present? } return Tip.no_route(action_path) if routes.blank? + tag = tag || oas[:doc][:tag][:name] api = Api.new(action_path, skip: Array(skip), use: Array(use)) - .merge! description: '', summary: summary, operationId: id || "#{oas[:doc][:tag][:name]}_#{action.to_s.camelize}", - tags: [tag || oas[:doc][:tag][:name]], parameters: [ ], requestBody: '', responses: { }, callbacks: { }, - links: { }, security: [ ], servers: [ ] - [action, :all].each { |blk_key| oas[:dry_blocks][blk_key]&.each { |blk| api.instance_eval(&blk) } } - api.param_use = api.param_skip = [ ] # `skip` and `use` only affect `api_dry`'s blocks + .merge!(summary: summary, tags: [tag], operationId: id || "#{tag}_#{action.to_s.camelize}") + _api_dry(api, action, tag) api.instance_exec(&block) if block_given? api.process_objs api.delete_if { |_, v| v.blank? } - - routes.each do |route| - path = oas[:apis][route[:path]] ||= { } - (http || route[:http_verb]).split('|').each { |verb| path[verb] = api } - end - - api + _set_apis(api, routes, http) end - def api_dry action = :all, desc = '', &block - Array(action).each { |a| (oas[:dry_blocks][a.to_sym] ||= [ ]) << block } + def api_dry action_or_tags = :all, &block + Array(action_or_tags).each { |a| (oas[:dry_blocks][a.to_sym] ||= [ ]) << block } end def ctrl_routes_list Generator.routes_list[oas[:route_base]] end + + def _api_dry(api, action, tag) + [action, tag, :all].each do |blk_key| + oas[:dry_blocks][blk_key]&.each { |blk| api.instance_eval(&blk) } + end + api.param_use = api.param_skip = [ ] # `skip` and `use` only affect `api_dry`'s blocks + end + + def _set_apis(api, routes, http) + routes.each do |route| + path = oas[:apis][route[:path]] ||= { } + (http || route[:http_verb]).split('|').each { |verb| path[verb] = api } + end + api + end end end end diff --git a/lib/open_api/dsl/api.rb b/lib/open_api/dsl/api.rb index 23644ed..4705f1e 100644 --- a/lib/open_api/dsl/api.rb +++ b/lib/open_api/dsl/api.rb @@ -15,6 +15,9 @@ def initialize(action_path = '', skip: [ ], use: [ ]) self.param_skip = skip self.param_use = use self.param_descs = { } + + self.merge!(description: '', parameters: [ ], requestBody: '', responses: { }, + callbacks: { }, links: { }, security: [ ], servers: [ ]) end def this_api_is_invalid! explain = '' diff --git a/lib/open_api/dsl/components.rb b/lib/open_api/dsl/components.rb index 230df63..b830c87 100644 --- a/lib/open_api/dsl/components.rb +++ b/lib/open_api/dsl/components.rb @@ -8,6 +8,10 @@ class Components < Hash include DSL::CommonDSL include DSL::Helpers + def initialize + self.merge!(%i[ schemas responses parameters examples requestBodies securitySchemes ].map { |k| [ k, { } ] }.to_h) + end + def schema component_key, type = nil, **schema_info schema = process_schema_info(type, schema_info, model: component_key) return Tip.schema_no_type(component_key) if schema[:illegal?] From 0c240620227da8852f86eb8a0b6f9c976d17434c Mon Sep 17 00:00:00 2001 From: zhandao Date: Fri, 1 Feb 2019 19:45:54 +0800 Subject: [PATCH 04/15] Remove `order` & Sort the responses by code --- lib/open_api/dsl/api.rb | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/open_api/dsl/api.rb b/lib/open_api/dsl/api.rb index 4705f1e..d9f3975 100644 --- a/lib/open_api/dsl/api.rb +++ b/lib/open_api/dsl/api.rb @@ -20,7 +20,7 @@ def initialize(action_path = '', skip: [ ], use: [ ]) callbacks: { }, links: { }, security: [ ], servers: [ ]) end - def this_api_is_invalid! explain = '' + def this_api_is_invalid!(*) self[:deprecated] = true end @@ -128,13 +128,6 @@ def server url, desc: '' self[:servers] << { url: url, description: desc } end - def order *param_names - self.param_order = param_names - # be used when `api_dry` - self.param_use = param_order if param_use.blank? - self.param_skip = param_use - param_order - end - def param_examples exp_by = :all, examples_hash exp_by = self[:parameters].map(&:name) if exp_by == :all self[:examples] = ExampleObj.new(examples_hash, exp_by, multiple: true).process @@ -144,10 +137,9 @@ def param_examples exp_by = :all, examples_hash def process_objs self[:parameters].map!(&:process) - self[:parameters].sort_by! { |param| param_order.index(param[:name]) || Float::INFINITY } if param_order.present? - self[:requestBody] = self[:requestBody].try(:process) self[:responses].each { |code, response| self[:responses][code] = response.process } + self[:responses] = self[:responses].sort.to_h end end end From 514995c951608bdbd92040d8d7dcdfd7e5f8c9f9 Mon Sep 17 00:00:00 2001 From: zhandao Date: Sat, 2 Feb 2019 13:54:52 +0800 Subject: [PATCH 05/15] Totally Changed `dry` --- lib/open_api/dsl.rb | 24 ++++-------- lib/open_api/dsl/api.rb | 17 ++++++--- spec/api_spec.rb | 82 ++++++++++++++++++++--------------------- spec/dsl_spec.rb | 10 ++--- 4 files changed, 64 insertions(+), 69 deletions(-) diff --git a/lib/open_api/dsl.rb b/lib/open_api/dsl.rb index e8d89d6..f728e82 100644 --- a/lib/open_api/dsl.rb +++ b/lib/open_api/dsl.rb @@ -30,16 +30,17 @@ def components &block (oas[:doc][:components] ||= { }).deep_merge!(current_doc) end - def api action, summary = '', id: nil, tag: nil, http: nil, skip: [ ], use: [ ], &block + def api action, summary = '', id: nil, tag: nil, http: nil, &block doc_tag if oas[:doc].blank? action_path = "#{oas[:route_base]}##{action}" - routes = ctrl_routes_list&.select { |api| api[:action_path][/^#{action_path}$/].present? } + routes = Generator.routes_list[oas[:route_base]] + &.select { |api| api[:action_path][/^#{action_path}$/].present? } return Tip.no_route(action_path) if routes.blank? tag = tag || oas[:doc][:tag][:name] - api = Api.new(action_path, skip: Array(skip), use: Array(use)) - .merge!(summary: summary, tags: [tag], operationId: id || "#{tag}_#{action.to_s.camelize}") - _api_dry(api, action, tag) + api = Api.new(action_path) + .merge!(summary: summary, tags: [tag], operationId: id || "#{tag}_#{action.to_s.camelize}") + [action, tag, :all].each { |key| api.dry_blocks.concat(oas[:dry_blocks][key] || [ ]) } api.instance_exec(&block) if block_given? api.process_objs api.delete_if { |_, v| v.blank? } @@ -47,18 +48,7 @@ def api action, summary = '', id: nil, tag: nil, http: nil, skip: [ ], use: [ ], end def api_dry action_or_tags = :all, &block - Array(action_or_tags).each { |a| (oas[:dry_blocks][a.to_sym] ||= [ ]) << block } - end - - def ctrl_routes_list - Generator.routes_list[oas[:route_base]] - end - - def _api_dry(api, action, tag) - [action, tag, :all].each do |blk_key| - oas[:dry_blocks][blk_key]&.each { |blk| api.instance_eval(&blk) } - end - api.param_use = api.param_skip = [ ] # `skip` and `use` only affect `api_dry`'s blocks + Array(action_or_tags).each { |a| (oas[:dry_blocks][a] ||= [ ]) << block } end def _set_apis(api, routes, http) diff --git a/lib/open_api/dsl/api.rb b/lib/open_api/dsl/api.rb index d9f3975..7207896 100644 --- a/lib/open_api/dsl/api.rb +++ b/lib/open_api/dsl/api.rb @@ -8,13 +8,12 @@ class Api < Hash include DSL::CommonDSL include DSL::Helpers - attr_accessor :action_path, :param_skip, :param_use, :param_descs, :param_order + attr_accessor :action_path, :dry_skip, :dry_only, :dry_blocks, :param_descs, :param_order - def initialize(action_path = '', skip: [ ], use: [ ]) + def initialize(action_path = '') self.action_path = action_path - self.param_skip = skip - self.param_use = use self.param_descs = { } + self.dry_blocks = [ ] self.merge!(description: '', parameters: [ ], requestBody: '', responses: { }, callbacks: { }, links: { }, security: [ ], servers: [ ]) @@ -32,10 +31,16 @@ def desc desc, param_descs = { } self.param_descs = param_descs self[:description] = desc end + + def dry only: nil, skip: nil, none: false + self.dry_skip = skip + self.dry_only = none ? [:none] : only + dry_blocks.each { |blk| instance_eval(&blk) } + self.dry_skip = self.dry_only = nil + end def param param_type, name, type, required, schema_info = { } - return if param_skip.include?(name) - return if param_use.present? && param_use.exclude?(name) + return if dry_skip&.include?(name) || dry_only&.exclude?(name) schema_info[:desc] ||= param_descs[name] schema_info[:desc!] ||= param_descs[:"#{name}!"] diff --git a/spec/api_spec.rb b/spec/api_spec.rb index 12711e2..d158f8c 100644 --- a/spec/api_spec.rb +++ b/spec/api_spec.rb @@ -52,15 +52,15 @@ end } - make -> { api :action, use: [ ] }, 'uses all', has_size: 2 - make -> { api :action, use: [:none] }, then_it('only uses :none') { be_nil } - make -> { api :action, use: [:page] }, has_size: 1 - make -> { api :action, skip: [ ] }, 'skips nothing', has_size: 2 - make -> { api :action, skip: [:page] }, has_size: 1 + make -> { api(:action) { dry } }, 'uses all', has_size: 2 + make -> { api(:action) { dry none: true } }, then_it('only uses :none') { be_nil } + make -> { api(:action) { dry only: [:page] } }, has_size: 1 + make -> { api(:action) { dry } }, 'skips nothing', has_size: 2 + make -> { api(:action) { dry skip: [:page] } }, has_size: 1 - make -> { api(:action, use: [:nothing]) { param :query, :page, Integer, :req } }, + make -> { api(:action) { dry none: true; param :query, :page, Integer, :req } }, 'not skip the params inside block', has_size: 1 - make -> { api(:action, skip: [:per]) { param :query, :per, Integer, :req } }, + make -> { api(:action) { dry skip: [:per]; param :query, :per, Integer, :req } }, 'not skip the params inside block', has_size: 2 after_do { undo_dry } @@ -289,40 +289,40 @@ end - desc :order, subject: :parameters do - context 'when using in .api' do - api -> do - query :page, String - path :id, Integer - order :id, :page - end, has_size!: 2 - it { expect(item_0).to include name: :id } - it { expect(item_1).to include name: :page } - end - - context 'when using in .api_dry' do - before_do! do - api_dry do - header :token, String - path :id, Integer - order :id, :name, :age, :token, :remarks - end - - api :action do - query :remarks, String - query :name, String - query :age, String - end - - undo_dry - end - - focus_on :subject, desc: '`order` will auto generate `use` and `skip`, so:' - expect_it { have_size 5 } - expect_its(0) { include name: :id } - expect_its(4) { include name: :remarks } - end - end + # desc :order, subject: :parameters do + # context 'when using in .api' do + # api -> do + # query :page, String + # path :id, Integer + # order :id, :page + # end, has_size!: 2 + # it { expect(item_0).to include name: :id } + # it { expect(item_1).to include name: :page } + # end + # + # context 'when using in .api_dry' do + # before_do! do + # api_dry do + # header :token, String + # path :id, Integer + # order :id, :name, :age, :token, :remarks + # end + # + # api :action do + # query :remarks, String + # query :name, String + # query :age, String + # end + # + # undo_dry + # end + # + # focus_on :subject, desc: '`order` will auto generate `use` and `skip`, so:' + # expect_it { have_size 5 } + # expect_its(0) { include name: :id } + # expect_its(4) { include name: :remarks } + # end + # end desc :param_examples, subject: :examples do diff --git a/spec/dsl_spec.rb b/spec/dsl_spec.rb index d620455..10630bf 100644 --- a/spec/dsl_spec.rb +++ b/spec/dsl_spec.rb @@ -47,18 +47,18 @@ desc :api_dry, subject: :paths do context 'when using the default :all parameter' do make -> do - api_dry { resp :success, 'success response' } - api :create - api :index + api_dry { response :success, 'success response' } + api(:create) { dry } + api(:index) { dry } end, 'makes all actions have a :success response', has_keys: { goods: [ get: [responses: [:success]], post: [responses: [:success]] ] } end context 'when the action is specified' do make -> do - api_dry(:index) { resp :success, 'success response' } + api_dry(:index) { response :success, 'success response' } api :create - api :index + api(:index) { dry } end, has_keys!: :goods focus_on :goods, :get expect_its :responses, has_keys: :success From dda6d1f4af3f268152c95cc78ae204aae360b14b Mon Sep 17 00:00:00 2001 From: zhandao Date: Sat, 2 Feb 2019 15:00:47 +0800 Subject: [PATCH 06/15] Change: the way to use `dry` --- lib/oas_objs/callback_obj.rb | 3 +-- lib/open_api/config.rb | 2 ++ lib/open_api/dsl.rb | 11 ++++------- lib/open_api/dsl/api.rb | 22 ++++++++++++++++------ 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/oas_objs/callback_obj.rb b/lib/oas_objs/callback_obj.rb index 89f96a5..a933ca5 100644 --- a/lib/oas_objs/callback_obj.rb +++ b/lib/oas_objs/callback_obj.rb @@ -37,8 +37,7 @@ def processed_url end def processed_block - (api = Api.new).instance_exec(&(self.block || -> { })) - api.process_objs + (api = Api.new).run_dsl(&(self.block || -> { })) api.delete_if { |_, v| v.blank? } end end diff --git a/lib/open_api/config.rb b/lib/open_api/config.rb index 192dcbb..55c99a4 100644 --- a/lib/open_api/config.rb +++ b/lib/open_api/config.rb @@ -7,6 +7,8 @@ module OpenApi module Config include ConfigDSL + cattr_accessor :default_run_dry, default: false + # [REQUIRED] The location where .json doc file will be output. cattr_accessor :file_output_path do 'public/open_api' diff --git a/lib/open_api/dsl.rb b/lib/open_api/dsl.rb index f728e82..7dd55bf 100644 --- a/lib/open_api/dsl.rb +++ b/lib/open_api/dsl.rb @@ -30,7 +30,7 @@ def components &block (oas[:doc][:components] ||= { }).deep_merge!(current_doc) end - def api action, summary = '', id: nil, tag: nil, http: nil, &block + def api action, summary = '', id: nil, tag: nil, http: nil, dry: Config.default_run_dry, &block doc_tag if oas[:doc].blank? action_path = "#{oas[:route_base]}##{action}" routes = Generator.routes_list[oas[:route_base]] @@ -38,17 +38,14 @@ def api action, summary = '', id: nil, tag: nil, http: nil, &block return Tip.no_route(action_path) if routes.blank? tag = tag || oas[:doc][:tag][:name] - api = Api.new(action_path) - .merge!(summary: summary, tags: [tag], operationId: id || "#{tag}_#{action.to_s.camelize}") + api = Api.new(action_path, summary: summary, tags: [tag], id: id || "#{tag}_#{action.to_s.camelize}") [action, tag, :all].each { |key| api.dry_blocks.concat(oas[:dry_blocks][key] || [ ]) } - api.instance_exec(&block) if block_given? - api.process_objs - api.delete_if { |_, v| v.blank? } + api.run_dsl(dry: dry, &block) _set_apis(api, routes, http) end def api_dry action_or_tags = :all, &block - Array(action_or_tags).each { |a| (oas[:dry_blocks][a] ||= [ ]) << block } + Array(action_or_tags).each { |a| (oas[:dry_blocks][a.to_sym] ||= [ ]) << block } end def _set_apis(api, routes, http) diff --git a/lib/open_api/dsl/api.rb b/lib/open_api/dsl/api.rb index 7207896..547de7a 100644 --- a/lib/open_api/dsl/api.rb +++ b/lib/open_api/dsl/api.rb @@ -8,14 +8,15 @@ class Api < Hash include DSL::CommonDSL include DSL::Helpers - attr_accessor :action_path, :dry_skip, :dry_only, :dry_blocks, :param_descs, :param_order + attr_accessor :action_path, :dry_skip, :dry_only, :dry_blocks, :dryed, :param_descs, :param_order - def initialize(action_path = '') + def initialize(action_path = '', summary: nil, tags: [ ], id: nil) self.action_path = action_path self.param_descs = { } self.dry_blocks = [ ] - self.merge!(description: '', parameters: [ ], requestBody: '', responses: { }, + self.merge!(summary: summary, operationId: id, tags: tags, + description: '', parameters: [ ], requestBody: '', responses: { }, callbacks: { }, links: { }, security: [ ], servers: [ ]) end @@ -31,12 +32,14 @@ def desc desc, param_descs = { } self.param_descs = param_descs self[:description] = desc end - + def dry only: nil, skip: nil, none: false - self.dry_skip = skip - self.dry_only = none ? [:none] : only + return if dry_blocks.blank? || dryed + self.dry_skip = skip && Array(skip) + self.dry_only = none ? [:none] : only && Array(only) dry_blocks.each { |blk| instance_eval(&blk) } self.dry_skip = self.dry_only = nil + self.dryed = true end def param param_type, name, type, required, schema_info = { } @@ -140,11 +143,18 @@ def param_examples exp_by = :all, examples_hash alias examples param_examples + def run_dsl(dry: false, &block) + instance_exec(&block) if block_given? + dry() if dry + process_objs + end + def process_objs self[:parameters].map!(&:process) self[:requestBody] = self[:requestBody].try(:process) self[:responses].each { |code, response| self[:responses][code] = response.process } self[:responses] = self[:responses].sort.to_h + self.delete_if { |_, v| v.blank? } end end end From ea46a1be5d2fe24d01657b4153e2523abfcc55a8 Mon Sep 17 00:00:00 2001 From: zhandao Date: Mon, 4 Feb 2019 19:27:36 +0800 Subject: [PATCH 07/15] Change `do_* by` to `in_*` & Remove descs --- lib/oas_objs/param_obj.rb | 6 ++--- lib/oas_objs/schema_obj.rb | 6 ++--- lib/open_api.rb | 3 --- lib/open_api/dsl.rb | 6 ++--- lib/open_api/dsl/api.rb | 49 +++++++++++++++------------------- lib/open_api/dsl/common_dsl.rb | 8 ------ lib/open_api/dsl/components.rb | 17 +++++++----- lib/open_api/dsl/helpers.rb | 2 -- lib/open_api/generator.rb | 7 ++--- spec/api_spec.rb | 28 +++---------------- 10 files changed, 46 insertions(+), 86 deletions(-) diff --git a/lib/oas_objs/param_obj.rb b/lib/oas_objs/param_obj.rb index 7da6de3..da17ce9 100644 --- a/lib/oas_objs/param_obj.rb +++ b/lib/oas_objs/param_obj.rb @@ -12,12 +12,12 @@ class ParamObj < Hash def initialize(name, param_type, type, required, schema) self.processed = { - name: name, - in: param_type, + name: name.to_s.delete('!').to_sym, + in: param_type.to_s.delete('!'), required: required.to_s[/req/].present? } self.schema = schema.is_a?(CombinedSchema) ? schema : SchemaObj.new(type, schema) - merge! schema + merge!(schema.is_a?(Hash) ? schema : { type: schema }) end def process diff --git a/lib/oas_objs/schema_obj.rb b/lib/oas_objs/schema_obj.rb index 933ee4f..ed16300 100644 --- a/lib/oas_objs/schema_obj.rb +++ b/lib/oas_objs/schema_obj.rb @@ -15,11 +15,11 @@ class SchemaObj < Hash attr_accessor :processed, :type, :preprocessed - def initialize(type, schema_info) + def initialize(type = nil, schema_info) + merge!(schema_info.is_a?(Hash) ? schema_info : { type: schema_info }) self.preprocessed = false self.processed = { } - self.type = type - merge! schema_info + self.type = type || self[:type] end def process(options = { inside_desc: false }) diff --git a/lib/open_api.rb b/lib/open_api.rb index 47af9f8..7046a21 100644 --- a/lib/open_api.rb +++ b/lib/open_api.rb @@ -35,9 +35,7 @@ def generate_doc(doc_name) settings, doc = init_hash(doc_name) [*(bdc = settings[:base_doc_classes]), *bdc.flat_map(&:descendants)].each do |kls| next if kls.oas[:doc].blank? - doc[:paths].merge!(kls.oas[:apis]) - binding.pry unless kls.oas[:doc][:tag] doc[:tags] << kls.oas[:doc][:tag] doc[:components].deep_merge!(kls.oas[:doc][:components] || { }) OpenApi.routes_index[kls.oas[:route_base]] = doc_name @@ -46,7 +44,6 @@ def generate_doc(doc_name) doc[:components].delete_if { |_, v| v.blank? } doc[:tags] = doc[:tags].sort { |a, b| a[:name] <=> b[:name] } doc[:paths] = doc[:paths].sort.to_h - OpenApi.docs[doc_name] = doc#.delete_if { |_, v| v.blank? } end diff --git a/lib/open_api/dsl.rb b/lib/open_api/dsl.rb index 7dd55bf..8318cd1 100644 --- a/lib/open_api/dsl.rb +++ b/lib/open_api/dsl.rb @@ -25,9 +25,9 @@ def doc_tag name: nil, **tag_info # description: ..., externalDocs: ... def components &block doc_tag if oas[:doc].blank? - (current_doc = Components.new).instance_exec(&block) - current_doc.process_objs - (oas[:doc][:components] ||= { }).deep_merge!(current_doc) + (components = Components.new).instance_exec(&block) + components.process_objs + (oas[:doc][:components] ||= { }).deep_merge!(components) end def api action, summary = '', id: nil, tag: nil, http: nil, dry: Config.default_run_dry, &block diff --git a/lib/open_api/dsl/api.rb b/lib/open_api/dsl/api.rb index 547de7a..a73c1af 100644 --- a/lib/open_api/dsl/api.rb +++ b/lib/open_api/dsl/api.rb @@ -8,16 +8,13 @@ class Api < Hash include DSL::CommonDSL include DSL::Helpers - attr_accessor :action_path, :dry_skip, :dry_only, :dry_blocks, :dryed, :param_descs, :param_order + attr_accessor :action_path, :dry_skip, :dry_only, :dry_blocks, :dryed, :param_order def initialize(action_path = '', summary: nil, tags: [ ], id: nil) self.action_path = action_path - self.param_descs = { } self.dry_blocks = [ ] - - self.merge!(summary: summary, operationId: id, tags: tags, - description: '', parameters: [ ], requestBody: '', responses: { }, - callbacks: { }, links: { }, security: [ ], servers: [ ]) + self.merge!(summary: summary, operationId: id, tags: tags, description: '', parameters: [ ], + requestBody: '', responses: { }, callbacks: { }, links: { }, security: [ ], servers: [ ]) end def this_api_is_invalid!(*) @@ -28,11 +25,12 @@ def this_api_is_invalid!(*) alias this_api_is_unused! this_api_is_invalid! alias this_api_is_under_repair! this_api_is_invalid! - def desc desc, param_descs = { } - self.param_descs = param_descs + def desc desc self[:description] = desc end + alias description desc + def dry only: nil, skip: nil, none: false return if dry_blocks.blank? || dryed self.dry_skip = skip && Array(skip) @@ -45,32 +43,29 @@ def dry only: nil, skip: nil, none: false def param param_type, name, type, required, schema_info = { } return if dry_skip&.include?(name) || dry_only&.exclude?(name) - schema_info[:desc] ||= param_descs[name] - schema_info[:desc!] ||= param_descs[:"#{name}!"] param_obj = ParamObj.new(name, param_type, type, required, schema_info) # The definition of the same name parameter will be overwritten fill_in_parameters(param_obj) end - # [ header header! path path! query query! cookie cookie! ] - def _param_agent name, type = nil, **schema_info - schema = process_schema_info(type, schema_info) - return Tip.param_no_type(name) if schema[:illegal?] - param @param_type, name, schema[:type], @necessity, schema[:combined] || schema[:info] - end + alias parameter param - # For supporting this: (just like `form '', data: { }` usage) - # do_query by: { - # :search_type => { type: String }, - # :export! => { type: Boolean } - # } %i[ header header! path path! query query! cookie cookie! ].each do |param_type| - define_method "do_#{param_type}" do |by:, **common_schema| - by.each do |param_name, schema| - action = "#{param_type}#{param_name['!']}".sub('!!', '!') - type, schema = schema.is_a?(Hash) ? [schema[:type], schema] : [schema, { }] - args = [ param_name.to_s.delete('!').to_sym, type, schema.reverse_merge!(common_schema) ] - send(action, *args) + define_method param_type do |name, type = nil, **schema_info| + schema = process_schema_info(type, schema_info) + return Tip.param_no_type(name) if schema[:illegal?] + param param_type, name, schema[:type], (param_type['!'] ? :req : :opt), + schema[:combined] || schema[:info] + end + + # For supporting this: (just like `form '', data: { }` usage) + # in_query( + # :search_type => String, + # :export! => { type: Boolean } + # ) + define_method "in_#{param_type}" do |params| + params.each_pair do |param_name, schema| + param param_type, param_name.to_sym, nil, (param_type['!'] || param_name['!'] ? :req : :opt), schema end end end diff --git a/lib/open_api/dsl/common_dsl.rb b/lib/open_api/dsl/common_dsl.rb index 9b70817..46030d9 100644 --- a/lib/open_api/dsl/common_dsl.rb +++ b/lib/open_api/dsl/common_dsl.rb @@ -13,14 +13,6 @@ module OpenApi module DSL module CommonDSL - %i[ header header! path path! query query! cookie cookie! ].each do |param_type| - define_method param_type do |*args| - @necessity = param_type['!'] ? :req : :opt - @param_type = param_type.to_s.delete('!') # OR: caller[0][/`.*'/][1..-2].to_sym - _param_agent *args - end - end - %i[ body body! ].each do |method| define_method method do |*args| @necessity = method['!'] ? :req : :opt diff --git a/lib/open_api/dsl/components.rb b/lib/open_api/dsl/components.rb index b830c87..ce7d954 100644 --- a/lib/open_api/dsl/components.rb +++ b/lib/open_api/dsl/components.rb @@ -30,15 +30,17 @@ def param component_key, param_type, name, type, required, schema_info = { } self[:parameters][component_key] = ParamObj.new(name, param_type, type, required, schema_info).process end - # [ header header! path path! query query! cookie cookie! ] - def _param_agent component_key, name, type = nil, **schema_info - schema = process_schema_info(type, schema_info) - return Tip.param_no_type(name) if schema[:illegal?] - param component_key, @param_type, name, schema[:type], @necessity, schema[:combined] || schema[:info] + %i[ header header! path path! query query! cookie cookie! ].each do |param_type| + define_method param_type do |component_key, name, type = nil, **schema_info| + schema = process_schema_info(type, schema_info) + return Tip.param_no_type(name) if schema[:illegal?] + param component_key, param_type.to_s.delete('!'), name, schema[:type], (param_type['!'] ? :req : :opt), + schema[:combined] || schema[:info] + end + + arrow_enable param_type end - arrow_enable :_param_agent - def request_body component_key, required, media_type, data: { }, desc: '', **options cur = self[:requestBodies][component_key] cur = RequestBodyObj.new(required, desc) unless cur.is_a?(RequestBodyObj) @@ -86,6 +88,7 @@ def api_key scheme_name, field:, in: 'header', **other_info def process_objs self[:requestBodies].each { |key, body| self[:requestBodies][key] = body.process } self[:responses].each { |code, response| self[:responses][code] = response.process } + self.delete_if { |_, v| v.blank? } end end end diff --git a/lib/open_api/dsl/helpers.rb b/lib/open_api/dsl/helpers.rb index d1ba3ed..fb18252 100644 --- a/lib/open_api/dsl/helpers.rb +++ b/lib/open_api/dsl/helpers.rb @@ -7,7 +7,6 @@ def self.included(base) base.extend ClassMethods end - # :nocov: def load_schema(model) # TODO: test model.columns.map do |column| type = column.sql_type_metadata.type.to_s.camelize @@ -15,7 +14,6 @@ def load_schema(model) # TODO: test [ column.name.to_sym, Object.const_get(type) ] end.to_h rescue '' end - # :nocov: def fill_in_parameters(param_obj) index = self[:parameters].map(&:name).index(param_obj.name) diff --git a/lib/open_api/generator.rb b/lib/open_api/generator.rb index 2a1bfd5..8f53c36 100644 --- a/lib/open_api/generator.rb +++ b/lib/open_api/generator.rb @@ -36,9 +36,7 @@ def routes_list end def get_actions_by_route_base(route_base) - routes_list[route_base]&.map do |action_info| - action_info[:action_path].split('#').last - end + routes_list[route_base]&.map { |action_info| action_info[:action_path].split('#').last } end def find_path_httpverb_by(route_base, action) @@ -46,8 +44,7 @@ def find_path_httpverb_by(route_base, action) if action_info[:action_path].split('#').last == action.to_s return [ action_info[:path], action_info[:http_verb].split('|').first ] end - end - nil + end ; nil end end end diff --git a/spec/api_spec.rb b/spec/api_spec.rb index d158f8c..2cd29ed 100644 --- a/spec/api_spec.rb +++ b/spec/api_spec.rb @@ -21,20 +21,6 @@ desc :desc do api -> { desc 'description for api #action.' }, has_key: :description - - context "when uniting parameters' description" do - let(:params) { subject[:parameters] } - - before_dsl! do - desc '#action', name: 'name', age!: 'age', id: 'id' - query :name, String - query :age, Integer - query :id, Integer, desc: 'override' - end - it { expect(params[0]).to include name: :name, description: 'name' } - it { expect(params[1]).to include name: :age, description: 'age' } - it { expect(params[2]).to include name: :id, description: 'override' } - end end @@ -95,22 +81,14 @@ end describe '#do_*:' do - api -> { do_query by: { } }, then_it { be_nil } - - api -> { do_header by: { key: Integer, token!: String } }, has_size!: 2 + api -> { in_header(key: Integer, token!: String) }, has_size!: 2 it { expect(item_0).to include name: :key, required: false } it { expect(item_1).to include name: :token, required: true } context 'when calling bang method' do - api -> { do_path! by: { id: Integer, name: String } }, '---> has 2 required items:', has_size!: 2 + api -> { in_path!(id: Integer, name: { type: String }) }, '---> has 2 required items:', has_size!: 2 it { expect(item_0).to include name: :id, required: true} - it { expect(item_1).to include name: :name, required: true} - end - - context 'when passing common schema' do - api -> { do_query by: { id: Integer, name: String }, pmt: true }, '---> has 2 required items:', has_size!: 2 - it { expect(item_0[:schema]).to include permit: true } - it { expect(item_1[:schema]).to include permit: true } + it { expect(item_1).to include name: :name, required: true, schema: { type: 'string' }} end end From 304f13388fcddcba659ce903aed04d704b2be15e Mon Sep 17 00:00:00 2001 From: zhandao Date: Tue, 5 Feb 2019 23:28:07 +0800 Subject: [PATCH 08/15] Refactor --- lib/oas_objs/param_obj.rb | 2 +- lib/oas_objs/schema_obj.rb | 5 ++-- lib/open_api/config.rb | 2 +- lib/open_api/dsl/api.rb | 42 +++++++++++++++++--------------- lib/open_api/dsl/common_dsl.rb | 33 ------------------------- lib/open_api/dsl/components.rb | 39 +++++++++++++++++------------ lib/open_api/dsl/helpers.rb | 33 +++++++++++++------------ spec/api_spec.rb | 2 +- spec/oas_objs/schema_obj_spec.rb | 34 +++++++++++++------------- 9 files changed, 87 insertions(+), 105 deletions(-) delete mode 100644 lib/open_api/dsl/common_dsl.rb diff --git a/lib/oas_objs/param_obj.rb b/lib/oas_objs/param_obj.rb index da17ce9..332fd08 100644 --- a/lib/oas_objs/param_obj.rb +++ b/lib/oas_objs/param_obj.rb @@ -17,7 +17,7 @@ def initialize(name, param_type, type, required, schema) required: required.to_s[/req/].present? } self.schema = schema.is_a?(CombinedSchema) ? schema : SchemaObj.new(type, schema) - merge!(schema.is_a?(Hash) ? schema : { type: schema }) + merge!(schema) end def process diff --git a/lib/oas_objs/schema_obj.rb b/lib/oas_objs/schema_obj.rb index ed16300..f390f8a 100644 --- a/lib/oas_objs/schema_obj.rb +++ b/lib/oas_objs/schema_obj.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'oas_objs/helpers' -require 'open_api/config' require 'oas_objs/ref_obj' require 'oas_objs/example_obj' require 'oas_objs/schema_obj_helpers' @@ -15,8 +14,8 @@ class SchemaObj < Hash attr_accessor :processed, :type, :preprocessed - def initialize(type = nil, schema_info) - merge!(schema_info.is_a?(Hash) ? schema_info : { type: schema_info }) + def initialize(type = nil, schema) + merge!(schema) self.preprocessed = false self.processed = { } self.type = type || self[:type] diff --git a/lib/open_api/config.rb b/lib/open_api/config.rb index 55c99a4..a41e3e9 100644 --- a/lib/open_api/config.rb +++ b/lib/open_api/config.rb @@ -26,7 +26,7 @@ module Config nil end - cattr_accessor :active_record_base do + cattr_accessor :model_base do nil end diff --git a/lib/open_api/dsl/api.rb b/lib/open_api/dsl/api.rb index a73c1af..b88bdf5 100644 --- a/lib/open_api/dsl/api.rb +++ b/lib/open_api/dsl/api.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -require 'open_api/dsl/common_dsl' +require 'open_api/dsl/helpers' module OpenApi module DSL class Api < Hash - include DSL::CommonDSL include DSL::Helpers attr_accessor :action_path, :dry_skip, :dry_only, :dry_blocks, :dryed, :param_order @@ -40,22 +39,22 @@ def dry only: nil, skip: nil, none: false self.dryed = true end - def param param_type, name, type, required, schema_info = { } + def param param_type, name, type, required, schema = { } return if dry_skip&.include?(name) || dry_only&.exclude?(name) - param_obj = ParamObj.new(name, param_type, type, required, schema_info) + schema = process_schema_input(type, schema) + return Tip.param_no_type(name) if schema[:illegal?] + param_obj = ParamObj.new(name, param_type, type, required, schema[:combined] || schema[:info]) # The definition of the same name parameter will be overwritten - fill_in_parameters(param_obj) + index = self[:parameters].map(&:name).index(param_obj.name) + index ? self[:parameters][index] = param_obj : self[:parameters] << param_obj end alias parameter param %i[ header header! path path! query query! cookie cookie! ].each do |param_type| - define_method param_type do |name, type = nil, **schema_info| - schema = process_schema_info(type, schema_info) - return Tip.param_no_type(name) if schema[:illegal?] - param param_type, name, schema[:type], (param_type['!'] ? :req : :opt), - schema[:combined] || schema[:info] + define_method param_type do |name, type = nil, **schema| + param param_type, name, type, (param_type['!'] ? :req : :opt), schema end # For supporting this: (just like `form '', data: { }` usage) @@ -65,7 +64,7 @@ def param param_type, name, type, required, schema_info = { } # ) define_method "in_#{param_type}" do |params| params.each_pair do |param_name, schema| - param param_type, param_name.to_sym, nil, (param_type['!'] || param_name['!'] ? :req : :opt), schema + param param_type, param_name, nil, (param_type['!'] || param_name['!'] ? :req : :opt), schema end end end @@ -75,15 +74,15 @@ def param_ref component_key, *keys end # options: `exp_by` and `examples` - def request_body required, media_type, data: { }, **options - desc = options.delete(:desc) || '' + def request_body required, media_type, data: { }, desc: '', **options self[:requestBody] = RequestBodyObj.new(required, desc) unless self[:requestBody].is_a?(RequestBodyObj) self[:requestBody].add_or_fusion(media_type, { data: data , **options }) end - # [ body body! ] - def _request_body_agent media_type, data: { }, **options - request_body @necessity, media_type, data: data, **options + %i[ body body! ].each do |method| + define_method method do |media_type, data: { }, **options| + request_body (method['!'] ? :req : :opt), media_type, data: data, **options + end end def body_ref component_key @@ -111,6 +110,14 @@ def file! media_type, data: { type: File }, **options body! media_type, data: data, **options end + def response code, desc, media_type = nil, data: { }, type: nil + self[:responses][code] = ResponseObj.new(desc) unless (self[:responses] ||= { })[code].is_a?(ResponseObj) + self[:responses][code].add_or_fusion(desc, media_type, { data: type || data }) + end + + alias_method :resp, :response + alias_method :error, :response + def response_ref code_compkey_hash code_compkey_hash.each { |code, component_key| self[:responses][code] = RefObj.new(:response, component_key) } end @@ -141,10 +148,7 @@ def param_examples exp_by = :all, examples_hash def run_dsl(dry: false, &block) instance_exec(&block) if block_given? dry() if dry - process_objs - end - def process_objs self[:parameters].map!(&:process) self[:requestBody] = self[:requestBody].try(:process) self[:responses].each { |code, response| self[:responses][code] = response.process } diff --git a/lib/open_api/dsl/common_dsl.rb b/lib/open_api/dsl/common_dsl.rb deleted file mode 100644 index 46030d9..0000000 --- a/lib/open_api/dsl/common_dsl.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'oas_objs/schema_obj' -require 'oas_objs/combined_schema' -require 'oas_objs/param_obj' -require 'oas_objs/response_obj' -require 'oas_objs/request_body_obj' -require 'oas_objs/ref_obj' -require 'oas_objs/example_obj' -require 'oas_objs/callback_obj' -require 'open_api/dsl/helpers' - -module OpenApi - module DSL - module CommonDSL - %i[ body body! ].each do |method| - define_method method do |*args| - @necessity = method['!'] ? :req : :opt - _request_body_agent *args - end - end - - # `code`: when defining components, `code` means `component_key` - def response code, desc, media_type = nil, data: { }, type: nil - self[:responses][code] = ResponseObj.new(desc) unless (self[:responses] ||= { })[code].is_a?(ResponseObj) - self[:responses][code].add_or_fusion(desc, media_type, { data: type || data }) - end - - alias_method :resp, :response - alias_method :error, :response - end - end -end diff --git a/lib/open_api/dsl/components.rb b/lib/open_api/dsl/components.rb index ce7d954..2d261c8 100644 --- a/lib/open_api/dsl/components.rb +++ b/lib/open_api/dsl/components.rb @@ -1,19 +1,18 @@ # frozen_string_literal: true -require 'open_api/dsl/common_dsl' +require 'open_api/dsl/helpers' module OpenApi module DSL class Components < Hash - include DSL::CommonDSL include DSL::Helpers def initialize - self.merge!(%i[ schemas responses parameters examples requestBodies securitySchemes ].map { |k| [ k, { } ] }.to_h) + merge!(%i[ schemas responses parameters examples requestBodies securitySchemes ].map { |k| [ k, { } ] }.to_h) end def schema component_key, type = nil, **schema_info - schema = process_schema_info(type, schema_info, model: component_key) + schema = process_schema_input(type, schema_info, model: component_key) return Tip.schema_no_type(component_key) if schema[:illegal?] self[:schemas][component_key.to_s.to_sym] = (schema[:combined] or SchemaObj.new(type = schema[:info], { })).process end @@ -26,16 +25,16 @@ def example component_key, examples_hash arrow_enable :example - def param component_key, param_type, name, type, required, schema_info = { } - self[:parameters][component_key] = ParamObj.new(name, param_type, type, required, schema_info).process + def param component_key, param_type, name, type, required, schema = { } + schema = process_schema_input(type, schema) + return Tip.param_no_type(name) if schema[:illegal?] + self[:parameters][component_key] = + ParamObj.new(name, param_type, type, required, schema[:combined] || schema[:info]).process end %i[ header header! path path! query query! cookie cookie! ].each do |param_type| - define_method param_type do |component_key, name, type = nil, **schema_info| - schema = process_schema_info(type, schema_info) - return Tip.param_no_type(name) if schema[:illegal?] - param component_key, param_type.to_s.delete('!'), name, schema[:type], (param_type['!'] ? :req : :opt), - schema[:combined] || schema[:info] + define_method param_type do |component_key, name, type = nil, **schema| + param component_key, param_type, name, type, (param_type['!'] ? :req : :opt), schema end arrow_enable param_type @@ -47,12 +46,22 @@ def request_body component_key, required, media_type, data: { }, desc: '', **opt self[:requestBodies][component_key] = cur.add_or_fusion(media_type, { data: data, **options }) end - # [ body body! ] - def _request_body_agent component_key, media_type, data: { }, **options - request_body component_key, @necessity, media_type, data: data, **options + %i[ body body! ].each do |method| + define_method method do |component_key, media_type, data: { }, **options| + request_body component_key, (method['!'] ? :req : :opt), media_type, data: data, **options + end + end + + arrow_enable :body + arrow_enable :body! + + def response component_key, desc, media_type = nil, data: { }, type: nil + self[:responses][component_key] = ResponseObj.new(desc) unless (self[:responses] ||= { })[component_key].is_a?(ResponseObj) + self[:responses][component_key].add_or_fusion(desc, media_type, { data: type || data }) end - arrow_enable :_request_body_agent + alias_method :resp, :response + alias_method :error, :response arrow_enable :resp arrow_enable :response diff --git a/lib/open_api/dsl/helpers.rb b/lib/open_api/dsl/helpers.rb index fb18252..b68825f 100644 --- a/lib/open_api/dsl/helpers.rb +++ b/lib/open_api/dsl/helpers.rb @@ -1,38 +1,41 @@ # frozen_string_literal: true +require 'oas_objs/schema_obj' +require 'oas_objs/combined_schema' +require 'oas_objs/param_obj' +require 'oas_objs/response_obj' +require 'oas_objs/request_body_obj' +require 'oas_objs/ref_obj' +require 'oas_objs/example_obj' +require 'oas_objs/callback_obj' + module OpenApi module DSL module Helpers - def self.included(base) - base.extend ClassMethods - end + extend ActiveSupport::Concern def load_schema(model) # TODO: test - model.columns.map do |column| + return unless Config.model_base && model.try(:superclass) == Config.model_base + model.columns.map do |column| type = column.sql_type_metadata.type.to_s.camelize type = 'DateTime' if type == 'Datetime' [ column.name.to_sym, Object.const_get(type) ] end.to_h rescue '' end - def fill_in_parameters(param_obj) - index = self[:parameters].map(&:name).index(param_obj.name) - index.present? ? self[:parameters][index] = param_obj : self[:parameters] << param_obj - end - def _combined_schema(one_of: nil, all_of: nil, any_of: nil, not: nil, **other) input = (_not = binding.local_variable_get(:not)) || one_of || all_of || any_of CombinedSchema.new(one_of: one_of, all_of: all_of, any_of: any_of, _not: _not) if input end - def process_schema_info(schema_type, schema_info, model: nil) - combined_schema = _combined_schema(schema_info) - type = schema_info[:type] ||= schema_type - schema_info = load_schema(model) if Config.active_record_base && model.try(:superclass) == Config.active_record_base + def process_schema_input(schema_type, schema, model: nil) + schema = { type: schema } unless schema.is_a?(Hash) + combined_schema = _combined_schema(schema) + type = schema[:type] ||= schema_type { illegal?: type.nil? && combined_schema.nil?, combined: combined_schema, - info: schema_info, + info: load_schema(model) || schema, type: type } end @@ -50,7 +53,7 @@ def arrow_writing_support end end - module ClassMethods + class_methods do def arrow_enable method alias_method :"_#{method}", method define_method method do |*args| diff --git a/spec/api_spec.rb b/spec/api_spec.rb index 2cd29ed..fa36a67 100644 --- a/spec/api_spec.rb +++ b/spec/api_spec.rb @@ -80,7 +80,7 @@ expect_it eq: 'integer' end - describe '#do_*:' do + describe '#in_*:' do api -> { in_header(key: Integer, token!: String) }, has_size!: 2 it { expect(item_0).to include name: :key, required: false } it { expect(item_1).to include name: :token, required: true } diff --git a/spec/oas_objs/schema_obj_spec.rb b/spec/oas_objs/schema_obj_spec.rb index 9ee7e6a..dc08266 100644 --- a/spec/oas_objs/schema_obj_spec.rb +++ b/spec/oas_objs/schema_obj_spec.rb @@ -144,23 +144,23 @@ api -> { query :info, String, enum: (1.2..3.4) }, has_key!: :enum it { expect(enum).to have_size ('1.2'..'3.4').to_a.size } end - - let(:description) { %i[ paths goods/action get parameters ].reduce(OpenApi.docs[:zro].deep_symbolize_keys, &:[])[0][:description] } - - context 'when passing Array to enum!' do - api -> { query :info, String, enum!: %w[ a b ], desc!: 'info: ' }, has_key!: :enum - it { expect(description).to eq 'info:
1/ a
2/ b' } - - context 'when not passing desc!' do - api -> { query :info, String, enum!: %w[ a b ] }, has_key!: :enum - it('parameter has not desc') { expect(description).to eq nil } - end - end - - context 'when passing Hash to enum!' do - api -> { query :info, String, enum!: { 'desc1': :a, 'desc2': :b }, desc!: 'info: ' }, has_key!: :enum - it { expect(description).to eq 'info:
1/ desc1: a
2/ desc2: b' } - end + # + # let(:description) { %i[ paths goods/action get parameters ].reduce(OpenApi.docs[:zro].deep_symbolize_keys, &:[])[0][:description] } + # + # context 'when passing Array to enum!' do + # api -> { query :info, String, enum!: %w[ a b ], desc!: 'info: ' }, has_key!: :enum + # it { expect(description).to eq 'info:
1/ a
2/ b' } + # + # context 'when not passing desc!' do + # api -> { query :info, String, enum!: %w[ a b ] }, has_key!: :enum + # it('parameter has not desc') { expect(description).to eq nil } + # end + # end + # + # context 'when passing Hash to enum!' do + # api -> { query :info, String, enum!: { 'desc1': :a, 'desc2': :b }, desc!: 'info: ' }, has_key!: :enum + # it { expect(description).to eq 'info:
1/ desc1: a
2/ desc2: b' } + # end end From 98be61fb2c80c046d5fdf3cbe2c0af9c53b9d957 Mon Sep 17 00:00:00 2001 From: zhandao Date: Mon, 11 Feb 2019 12:31:51 +0800 Subject: [PATCH 09/15] R / SchemaObj first time --- lib/oas_objs/example_obj.rb | 1 + lib/oas_objs/helpers.rb | 1 + lib/oas_objs/media_type_obj.rb | 2 +- lib/oas_objs/param_obj.rb | 7 +---- lib/oas_objs/schema_obj.rb | 50 ++++++++++-------------------- lib/oas_objs/schema_obj_helpers.rb | 23 +++++--------- spec/oas_objs/schema_obj_spec.rb | 48 ++++++++++++---------------- 7 files changed, 47 insertions(+), 85 deletions(-) diff --git a/lib/oas_objs/example_obj.rb b/lib/oas_objs/example_obj.rb index 502f0ff..9e3e5ca 100644 --- a/lib/oas_objs/example_obj.rb +++ b/lib/oas_objs/example_obj.rb @@ -17,6 +17,7 @@ def initialize(exp, keys_of_value = nil, multiple: false) end def process + return unless examples_hash || example_value return self.processed = example_value if example_value self.processed = diff --git a/lib/oas_objs/helpers.rb b/lib/oas_objs/helpers.rb index 74ff908..f58c717 100644 --- a/lib/oas_objs/helpers.rb +++ b/lib/oas_objs/helpers.rb @@ -34,6 +34,7 @@ def to_processed(who) def then_merge! # to_processed processed.tap { |it| it.merge! @assign if truly_present?(@assign) } + # processed end end end diff --git a/lib/oas_objs/media_type_obj.rb b/lib/oas_objs/media_type_obj.rb index fbe93ca..dbdaf90 100644 --- a/lib/oas_objs/media_type_obj.rb +++ b/lib/oas_objs/media_type_obj.rb @@ -17,7 +17,7 @@ def initialize(media_type, hash) self.examples = ExampleObj.new(examples_hash, exp_by, multiple: true) if examples_hash.present? self.media_type = media_type_mapping media_type - self.schema = SchemaObj.new(schema_type, hash) + self.schema = SchemaObj.new(schema_type, hash.except(:type, :data)) end def process diff --git a/lib/oas_objs/param_obj.rb b/lib/oas_objs/param_obj.rb index 332fd08..3a0dc6c 100644 --- a/lib/oas_objs/param_obj.rb +++ b/lib/oas_objs/param_obj.rb @@ -21,16 +21,11 @@ def initialize(name, param_type, type, required, schema) end def process - assign(desc).to_processed :description assign(schema.process).to_processed :schema + assign(schema.processed[:description]).to_processed :description processed end - def desc - return self[:desc] || self[:description] if (self[:desc!] || self[:description!]).blank? - schema.__desc # not a copy of __desc, means desc() will change if schema.__desc changes. - end - def name processed[:name] end diff --git a/lib/oas_objs/schema_obj.rb b/lib/oas_objs/schema_obj.rb index f390f8a..3f559bb 100644 --- a/lib/oas_objs/schema_obj.rb +++ b/lib/oas_objs/schema_obj.rb @@ -12,28 +12,28 @@ class SchemaObj < Hash include SchemaObjHelpers include Helpers - attr_accessor :processed, :type, :preprocessed + attr_accessor :processed, :type def initialize(type = nil, schema) merge!(schema) - self.preprocessed = false - self.processed = { } + self.processed = { type: nil, format: nil, **schema.except(:type, :range, :enum!, *SELF_MAPPING.values.flatten) } self.type = type || self[:type] end def process(options = { inside_desc: false }) processed.merge!(processed_type) - reducx(additional_properties, enum_and_length, range, format, pattern_default_and_other, desc(options)).then_merge! + reducx(additional_properties, enum_and_length, range, format, other, desc(options)).then_merge! + processed.keep_if &value_present end def desc(inside_desc:) - result = __desc ? auto_generate_desc : _desc - return unless inside_desc + result = @bang_enum.present? ? auto_generate_desc : _desc + # return unless inside_desc { description: result } end - def processed_type(type = self.type) - t = type.class.in?([Hash, Array, Symbol]) ? type : type.to_s.downcase + def processed_type(t = self.type) + t = t.class.in?([Hash, Array, Symbol]) ? t : t.to_s.downcase if t.is_a? Hash hash_type(t) elsif t.is_a? Array @@ -84,53 +84,35 @@ def enum_and_length end def range - range = _range || { } + range = self[:range] || { } { minimum: range[:gt] || range[:ge], exclusiveMinimum: range[:gt].present? ? true : nil, maximum: range[:lt] || range[:le], exclusiveMaximum: range[:lt].present? ? true : nil - }.keep_if &value_present + } end def format - result = { is: _is } # `format` that generated in process_type() may be overwrote here. - result[:format] = _format || _is if processed[:format].blank? || _format.present? - result + processed[:format].blank? ? { format: self[:format] || self[:is_a] } : { } end - def pattern_default_and_other + def other { pattern: _pattern.is_a?(String) ? _pattern : _pattern&.inspect&.delete('/'), - default: _default, - example: _exp.present? ? ExampleObj.new(_exp).process : nil, - examples: _exps.present? ? ExampleObj.new(_exps, self[:exp_by], multiple: true).process : nil, - as: _as, permit: _permit, not_permit: _npermit, req_if: _req_if, opt_if: _opt_if, blankable: _blank + example: ExampleObj.new(self[:example]).process, + examples: ExampleObj.new(self[:examples], self[:exp_by], multiple: true).process } end - { # SELF_MAPPING + SELF_MAPPING = { _enum: %i[ enum in values allowable_values ], - _value: %i[ must_be value allowable_value ], - _range: %i[ range number_range ], _length: %i[ length lth size ], - _format: %i[ format fmt ], - _pattern: %i[ pattern regexp pt reg ], - _default: %i[ default dft default_value ], + _pattern: %i[ pattern regexp ], _desc: %i[ desc description d ], - __desc: %i[ desc! description! d! ], - _exp: %i[ example ], - _exps: %i[ examples ], _addProp: %i[ additional_properties add_prop values_type ], - _is: %i[ is_a is ], # NOT OAS Spec, see documentation/parameter.md - _as: %i[ as to for map mapping ], # NOT OAS Spec, it's for zero-params_processor - _permit: %i[ permit pmt ], # ditto - _npermit: %i[ npmt not_permit unpermit ], # ditto - _req_if: %i[ req_if req_when ], # ditto - _opt_if: %i[ opt_if opt_when ], # ditto - _blank: %i[ blank blankable ], # ditto }.each do |key, aliases| define_method key do return self[key] unless self[key].nil? diff --git a/lib/oas_objs/schema_obj_helpers.rb b/lib/oas_objs/schema_obj_helpers.rb index 33e7e55..416cc4f 100644 --- a/lib/oas_objs/schema_obj_helpers.rb +++ b/lib/oas_objs/schema_obj_helpers.rb @@ -40,9 +40,6 @@ def array_type(t) def process_range_enum_and_lth self[:_enum] = str_range_to_a(_enum) if _enum.is_a?(Range) self[:_length] = str_range_to_a(_length) if _length.is_a?(Range) - - values = _enum || _value - self._enum = Array(values) if truly_present?(values) end def str_range_to_a(val) @@ -57,26 +54,22 @@ def process_enum_info # 'all_desc': :all, # 'one_desc': :one # } - self._enum ||= (e = self[:enum!]) - return unless e.is_a? Hash - @enum_info = e - self._enum = e.values + return unless (@bang_enum = self[:enum!]) + self._enum ||= @bang_enum.is_a?(Hash) ? @bang_enum.values : @bang_enum end # TODO: more info and desc configure def auto_generate_desc - return __desc if _enum.blank? - - if @enum_info.present? - @enum_info.each_with_index do |(info, value), index| - self.__desc = __desc + "
#{index + 1}/ #{info}: #{value}" # FIXME + if @bang_enum.is_a?(Hash) + @bang_enum.each_with_index do |(info, value), index| + self._desc = _desc + "
#{index + 1}/ #{info}: #{value}" # FIXME end else - _enum.each_with_index do |value, index| - self.__desc = __desc + "
#{index + 1}/ #{value}" + @bang_enum.each_with_index do |value, index| + self._desc = _desc + "
#{index + 1}/ #{value}" end end - __desc + _desc end end end diff --git a/spec/oas_objs/schema_obj_spec.rb b/spec/oas_objs/schema_obj_spec.rb index dc08266..869c316 100644 --- a/spec/oas_objs/schema_obj_spec.rb +++ b/spec/oas_objs/schema_obj_spec.rb @@ -88,11 +88,11 @@ expect_its :properties, has_keys: %i[ first last ] end - context 'with key :type' do + context 'with key :type' do # ??? # OR: query :info, type: { type: String, desc: 'info' } - api -> { query :info, { type: String, desc: 'info' }, desc: 'api desc' }, 'has description within schema', - has_key!: :description - it { expect(description).to eq 'info' } # not_to eq 'api desc' + # api -> { query :info, { type: String, desc: 'info' }, desc: 'api desc' }, 'has description within schema', + # has_key!: :description + # it { expect(description).to eq 'info' } # not_to eq 'api desc' end context 'when having keys in [ one_of any_of all_of not ]' do @@ -125,13 +125,8 @@ it { expect(max_items).to eq 20 } end - context 'when enum is or not an array' do + context 'when enum is an array' do api -> { query :info, String, enum: ['a'] }, include: { enum: ['a'] } - api -> { query :info, String, enum: 'a' }, include: { enum: ['a'] } - end - - context 'when using must_be (value)' do - api -> { query :info, String, must_be: 'a' }, 'is also enum', include: { enum: ['a'] } end context 'when passing Range to lth' do @@ -144,23 +139,18 @@ api -> { query :info, String, enum: (1.2..3.4) }, has_key!: :enum it { expect(enum).to have_size ('1.2'..'3.4').to_a.size } end - # - # let(:description) { %i[ paths goods/action get parameters ].reduce(OpenApi.docs[:zro].deep_symbolize_keys, &:[])[0][:description] } - # - # context 'when passing Array to enum!' do - # api -> { query :info, String, enum!: %w[ a b ], desc!: 'info: ' }, has_key!: :enum - # it { expect(description).to eq 'info:
1/ a
2/ b' } - # - # context 'when not passing desc!' do - # api -> { query :info, String, enum!: %w[ a b ] }, has_key!: :enum - # it('parameter has not desc') { expect(description).to eq nil } - # end - # end - # - # context 'when passing Hash to enum!' do - # api -> { query :info, String, enum!: { 'desc1': :a, 'desc2': :b }, desc!: 'info: ' }, has_key!: :enum - # it { expect(description).to eq 'info:
1/ desc1: a
2/ desc2: b' } - # end + + let(:description) { %i[ paths goods/action get parameters ].reduce(OpenApi.docs[:zro].deep_symbolize_keys, &:[])[0][:description] } + + context 'when passing Array to enum!' do + api -> { query :info, String, enum!: %w[ a b ], desc: 'info: ' }, has_key!: :enum + it { expect(description).to eq 'info:
1/ a
2/ b' } + end + + context 'when passing Hash to enum!' do + api -> { query :info, String, enum!: { 'desc1': :a, 'desc2': :b }, desc: 'info: ' }, has_key!: :enum + it { expect(description).to eq 'info:
1/ desc1: a
2/ desc2: b' } + end end @@ -173,7 +163,7 @@ desc :is_and_format do correct do - api -> { query :email, Integer, is: :email }, include: { is: :email, format: :email } + api -> { query :email, Integer, is_a: :email }, include: { is_a: :email, format: :email } end end @@ -190,7 +180,7 @@ end describe ':default' do - api -> { query :info, String, dft: 'default' }, include: { default: 'default' } + api -> { query :info, String, default: 'default' }, include: { default: 'default' } end describe ':example' do From 0c60df05657b3a8c83e2ed9438a8646525f14ae3 Mon Sep 17 00:00:00 2001 From: zhandao Date: Mon, 11 Feb 2019 15:27:18 +0800 Subject: [PATCH 10/15] R / SchemaObj the last time --- lib/oas_objs/combined_schema.rb | 4 +- lib/oas_objs/schema_obj.rb | 64 +++++++++++++++--------------- lib/oas_objs/schema_obj_helpers.rb | 31 +++------------ spec/oas_objs/schema_obj_spec.rb | 8 +++- 4 files changed, 47 insertions(+), 60 deletions(-) diff --git a/lib/oas_objs/combined_schema.rb b/lib/oas_objs/combined_schema.rb index b7be525..a36b939 100644 --- a/lib/oas_objs/combined_schema.rb +++ b/lib/oas_objs/combined_schema.rb @@ -15,11 +15,11 @@ def initialize(combined_schema) @schemas = combined_schema.values.first end - def process(options = { inside_desc: false }) + def process() processed[@mode] = @schemas.map do |schema| type = schema.is_a?(Hash) ? schema[:type] : schema schema = { } unless schema.is_a?(Hash) - SchemaObj.new(type, schema).process(options) + SchemaObj.new(type, schema).process end processed end diff --git a/lib/oas_objs/schema_obj.rb b/lib/oas_objs/schema_obj.rb index 3f559bb..6ebf39b 100644 --- a/lib/oas_objs/schema_obj.rb +++ b/lib/oas_objs/schema_obj.rb @@ -20,19 +20,18 @@ def initialize(type = nil, schema) self.type = type || self[:type] end - def process(options = { inside_desc: false }) - processed.merge!(processed_type) - reducx(additional_properties, enum_and_length, range, format, other, desc(options)).then_merge! + def process + processed.merge!(recg_schema_type) + reducx(additional_properties, enum, length, range, format, other, desc).then_merge! processed.keep_if &value_present end - def desc(inside_desc:) + def desc result = @bang_enum.present? ? auto_generate_desc : _desc - # return unless inside_desc { description: result } end - def processed_type(t = self.type) + def recg_schema_type(t = self.type) t = t.class.in?([Hash, Array, Symbol]) ? t : t.to_s.downcase if t.is_a? Hash hash_type(t) @@ -57,39 +56,45 @@ def processed_type(t = self.type) end def additional_properties - return { } if processed[:type] != 'object' || _addProp.nil? + return if processed[:type] != 'object' || _addProp.nil? { - additionalProperties: SchemaObj.new(_addProp, { }).process(inside_desc: true) + additionalProperties: SchemaObj.new(_addProp, { }).process } end - def enum_and_length - process_enum_info - process_range_enum_and_lth - - # generate length range fields by _lth array - if (lth = _length || '').is_a?(Array) - min, max = [lth.first&.to_i, lth.last&.to_i] - elsif lth['ge'] - min = lth.to_s.split('_').last.to_i - elsif lth['le'] - max = lth.to_s.split('_').last.to_i + def enum + self._enum = str_range_to_a(_enum) if _enum.is_a?(Range) + # Support this writing for auto generating desc from enum. + # enum!: { + # 'all_desc': :all, + # 'one_desc': :one + # } + if (@bang_enum = self[:enum!]) + self._enum ||= @bang_enum.is_a?(Hash) ? @bang_enum.values : @bang_enum end + { enum: _enum } + end + + def length + return unless _length + self._length = str_range_to_a(_length) if _length.is_a?(Range) - if processed[:type] == 'array' - { minItems: min, maxItems: max } + if _length.is_a?(Array) + min, max = [ _length.first&.to_i, _length.last&.to_i ] else - { minLength: min, maxLength: max } - end.merge!(enum: _enum).keep_if &value_present + min, max = _length[/ge_(.*)/, 1]&.to_i, _length[/le_(.*)/, 1]&.to_i + end + + processed[:type] == 'array' ? { minItems: min, maxItems: max } : { minLength: min, maxLength: max } end def range - range = self[:range] || { } + (range = self[:range]) or return { minimum: range[:gt] || range[:ge], - exclusiveMinimum: range[:gt].present? ? true : nil, + exclusiveMinimum: range[:gt].present? || nil, maximum: range[:lt] || range[:le], - exclusiveMaximum: range[:lt].present? ? true : nil + exclusiveMaximum: range[:lt].present? || nil } end @@ -114,18 +119,13 @@ def other _desc: %i[ desc description d ], _addProp: %i[ additional_properties add_prop values_type ], }.each do |key, aliases| - define_method key do - return self[key] unless self[key].nil? - aliases.each { |alias_name| self[key] = self[alias_name] if self[key].nil? } - self[key] - end + define_method(key) { self[key] || aliases.each { |aname| self[key] ||= self[aname] } and self[key] } define_method("#{key}=") { |value| self[key] = value } end end end end - __END__ Schema Object Examples diff --git a/lib/oas_objs/schema_obj_helpers.rb b/lib/oas_objs/schema_obj_helpers.rb index 416cc4f..3de6f38 100644 --- a/lib/oas_objs/schema_obj_helpers.rb +++ b/lib/oas_objs/schema_obj_helpers.rb @@ -9,11 +9,11 @@ def hash_type(t) # id!: { type: Integer, enum: 0..5, desc: 'user id' } # }, should have description within schema if t.key?(:type) - SchemaObj.new(t[:type], t).process(inside_desc: true) + SchemaObj.new(t[:type], t).process # For supporting combined schema in nested schema. elsif (t.keys & %i[ one_of any_of all_of not ]).present? - CombinedSchema.new(t).process(inside_desc: true) + CombinedSchema.new(t).process else obj_type(t) end @@ -24,50 +24,31 @@ def obj_type(t) t.each do |prop_name, prop_type| obj_type[:required] << prop_name.to_s.delete('!') if prop_name['!'] - obj_type[:properties][prop_name.to_s.delete('!').to_sym] = processed_type(prop_type) + obj_type[:properties][prop_name.to_s.delete('!').to_sym] = recg_schema_type(prop_type) end obj_type.keep_if &value_present end def array_type(t) - t = t.size == 1 ? t.first : { one_of: t } { type: 'array', - items: processed_type(t) + items: recg_schema_type(t.one? ? t[0] : { one_of: t }) } end - def process_range_enum_and_lth - self[:_enum] = str_range_to_a(_enum) if _enum.is_a?(Range) - self[:_length] = str_range_to_a(_length) if _length.is_a?(Range) - end - def str_range_to_a(val) val_class = val.first.class action = :"to_#{val_class.to_s.downcase[0]}" (val.first.to_s..val.last.to_s).to_a.map(&action) end - def process_enum_info - # Support this writing for auto generating desc from enum. - # enum!: { - # 'all_desc': :all, - # 'one_desc': :one - # } - return unless (@bang_enum = self[:enum!]) - self._enum ||= @bang_enum.is_a?(Hash) ? @bang_enum.values : @bang_enum - end - - # TODO: more info and desc configure def auto_generate_desc if @bang_enum.is_a?(Hash) @bang_enum.each_with_index do |(info, value), index| - self._desc = _desc + "
#{index + 1}/ #{info}: #{value}" # FIXME + self._desc = _desc + "
#{index + 1}/ #{info}: #{value}" end else - @bang_enum.each_with_index do |value, index| - self._desc = _desc + "
#{index + 1}/ #{value}" - end + @bang_enum.each_with_index { |value, index| self._desc = _desc + "
#{index + 1}/ #{value}" } end _desc end diff --git a/spec/oas_objs/schema_obj_spec.rb b/spec/oas_objs/schema_obj_spec.rb index 869c316..f210fb2 100644 --- a/spec/oas_objs/schema_obj_spec.rb +++ b/spec/oas_objs/schema_obj_spec.rb @@ -16,7 +16,7 @@ api -> { query :people, type: { name: String } }, include: { type: 'object' } end - desc :processed_type do + desc :recg_schema_type do context 'when not be one of the [Hash, Array, Symbol]' do context 'when in [ float double int32 int64 ]' do api -> { query :info, Float }, get: { type: 'number', format: 'float' } @@ -202,4 +202,10 @@ api -> { query :info, '{=>integer}' }, include: { additionalProperties: { type: 'integer' } } end end + + desc :custom_addition do + correct do + api -> { query :info, Integer, permit: true }, include: { permit: true } + end + end end From 7ab25a504277d0c782496f5d9e0fa69728fb0bbb Mon Sep 17 00:00:00 2001 From: zhandao Date: Mon, 11 Feb 2019 17:20:40 +0800 Subject: [PATCH 11/15] R / oas_objs --- lib/oas_objs/callback_obj.rb | 11 +++-------- lib/oas_objs/combined_schema.rb | 26 ++++++++++++++------------ lib/oas_objs/example_obj.rb | 15 +++++++-------- lib/oas_objs/helpers.rb | 23 ++--------------------- lib/oas_objs/media_type_obj.rb | 16 +++++++--------- lib/oas_objs/param_obj.rb | 5 +++-- lib/oas_objs/request_body_obj.rb | 3 ++- lib/oas_objs/response_obj.rb | 2 +- lib/oas_objs/schema_obj.rb | 12 +++++------- lib/open_api/dsl/components.rb | 2 +- lib/open_api/dsl/helpers.rb | 2 +- 11 files changed, 46 insertions(+), 71 deletions(-) diff --git a/lib/oas_objs/callback_obj.rb b/lib/oas_objs/callback_obj.rb index a933ca5..596e5ad 100644 --- a/lib/oas_objs/callback_obj.rb +++ b/lib/oas_objs/callback_obj.rb @@ -19,27 +19,22 @@ def initialize(event_name, http_method, callback_url, &block) def process { - self.event_name => { + event_name => { processed_url => { - self.http_method.downcase.to_sym => processed_block + http_method.downcase.to_sym => Api.new.run_dsl(&(self.block || -> { })) } } } end def processed_url - self.callback_url.gsub(/{[^{}]*}/) do |exp| + callback_url.gsub(/{[^{}]*}/) do |exp| key_location, key_name = exp[1..-2].split connector = key_location == 'body' ? '#/' : '.' key_location = '$request.' + key_location ['{', key_location, connector, key_name, '}'].join end end - - def processed_block - (api = Api.new).run_dsl(&(self.block || -> { })) - api.delete_if { |_, v| v.blank? } - end end end end diff --git a/lib/oas_objs/combined_schema.rb b/lib/oas_objs/combined_schema.rb index a36b939..9ab3c30 100644 --- a/lib/oas_objs/combined_schema.rb +++ b/lib/oas_objs/combined_schema.rb @@ -5,23 +5,25 @@ module DSL # https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject class CombinedSchema < Hash - attr_accessor :processed + include Helpers - def initialize(combined_schema) - self.processed = { } + attr_accessor :processed, :mode, :schemas + def initialize(combined_schema) combined_schema.delete_if { |_, v| v.nil? } - @mode = combined_schema.keys.first.to_s.sub('_not', 'not').camelize(:lower).to_sym - @schemas = combined_schema.values.first + self.mode = combined_schema.keys.first.to_s.camelize(:lower).to_sym + self.schemas = combined_schema.values.first end - def process() - processed[@mode] = @schemas.map do |schema| - type = schema.is_a?(Hash) ? schema[:type] : schema - schema = { } unless schema.is_a?(Hash) - SchemaObj.new(type, schema).process - end - processed + def process + self.processed = { + mode => + schemas.map do |schema| + type = schema.is_a?(Hash) ? schema[:type] : schema + schema = { } unless schema.is_a?(Hash) + SchemaObj.new(type, schema).process + end + } end end end diff --git a/lib/oas_objs/example_obj.rb b/lib/oas_objs/example_obj.rb index 9e3e5ca..3a3ba29 100644 --- a/lib/oas_objs/example_obj.rb +++ b/lib/oas_objs/example_obj.rb @@ -9,7 +9,7 @@ module DSL class ExampleObj < Hash include Helpers - attr_accessor :processed, :examples_hash, :example_value, :keys_of_value + attr_accessor :examples_hash, :example_value, :keys_of_value def initialize(exp, keys_of_value = nil, multiple: false) multiple ? self.examples_hash = exp : self.example_value = exp @@ -17,12 +17,11 @@ def initialize(exp, keys_of_value = nil, multiple: false) end def process - return unless examples_hash || example_value - return self.processed = example_value if example_value + return example_value if example_value + return unless examples_hash - self.processed = - examples_hash.map do |(name, value)| - value = + examples_hash.map do |(name, value)| + value = if keys_of_value.present? && value.is_a?(Array) { value: Hash[keys_of_value.zip(value)] } elsif value.is_a?(Symbol) && value['$'] @@ -31,8 +30,8 @@ def process { value: value } end - { name => value } - end + { name => value } + end end end end diff --git a/lib/oas_objs/helpers.rb b/lib/oas_objs/helpers.rb index f58c717..1d3e813 100644 --- a/lib/oas_objs/helpers.rb +++ b/lib/oas_objs/helpers.rb @@ -14,27 +14,8 @@ def value_present proc { |_, v| truly_present? v } end - # assign.to - def assign(value) - @assign = value.is_a?(Symbol) ? send("_#{value}") : value - self - end - - # reducx.then_merge! => for Hash - def reducx(*values) - @assign = values.compact.reduce({ }, :merge!).keep_if &value_present - self - end - - def to_processed(who) - return processed unless truly_present?(@assign) - processed[who.to_sym] = @assign - processed - end - - def then_merge! # to_processed - processed.tap { |it| it.merge! @assign if truly_present?(@assign) } - # processed + def reducing(*values) + values.compact.reduce(processed, :merge!).keep_if &value_present end end end diff --git a/lib/oas_objs/media_type_obj.rb b/lib/oas_objs/media_type_obj.rb index dbdaf90..135ba20 100644 --- a/lib/oas_objs/media_type_obj.rb +++ b/lib/oas_objs/media_type_obj.rb @@ -10,21 +10,20 @@ class MediaTypeObj < Hash attr_accessor :media_type, :schema, :examples def initialize(media_type, hash) - examples_hash = hash.delete(:examples) - exp_by = hash.delete(:exp_by) - schema_type = hash.values_at(:type, :data).compact.first - exp_by = schema_type.keys if exp_by == :all - + examples_hash = hash.delete(:examples) + exp_by = schema_type.keys if (exp_by = hash.delete(:exp_by)) == :all self.examples = ExampleObj.new(examples_hash, exp_by, multiple: true) if examples_hash.present? - self.media_type = media_type_mapping media_type - self.schema = SchemaObj.new(schema_type, hash.except(:type, :data)) + self.media_type = media_type_mapping(media_type) + self.schema = SchemaObj.new(hash.values_at(:type, :data).compact.first, + hash.except(:type, :data)) end def process + return { } if media_type.nil? schema_processed = schema.process result = schema_processed.values.join.blank? ? { } : { schema: schema_processed } result[:examples] = examples.process unless examples.nil? - media_type.nil? ? { } : { media_type => result } + { media_type => result } end # https://swagger.io/docs/specification/media-types/ @@ -48,7 +47,6 @@ def media_type_mapping(media_type) when :xlsx then 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' when :ppt then 'application/vnd.ms-powerpoint' when :pptx then 'application/vnd.openxmlformats-officedocument.presentationml.presentation' - # when :pdf then 'application/pdf' when :form then 'multipart/form-data'; when :form_data then 'multipart/form-data' when :text then 'text/*' when :plain then 'text/plain then charset=utf-8' diff --git a/lib/oas_objs/param_obj.rb b/lib/oas_objs/param_obj.rb index 3a0dc6c..edc096e 100644 --- a/lib/oas_objs/param_obj.rb +++ b/lib/oas_objs/param_obj.rb @@ -21,8 +21,9 @@ def initialize(name, param_type, type, required, schema) end def process - assign(schema.process).to_processed :schema - assign(schema.processed[:description]).to_processed :description + processed[:schema] = schema.process + desc = schema.processed[:description] + processed[:description] = desc if desc processed end diff --git a/lib/oas_objs/request_body_obj.rb b/lib/oas_objs/request_body_obj.rb index 376b0b5..10bed0a 100644 --- a/lib/oas_objs/request_body_obj.rb +++ b/lib/oas_objs/request_body_obj.rb @@ -11,6 +11,7 @@ class RequestBodyObj < Hash include Helpers attr_accessor :processed, :media_types + def initialize(required, desc) self.media_types = [ ] self.processed = { required: required['req'].present?, description: desc } @@ -22,7 +23,7 @@ def add_or_fusion(media_type, hash) end def process - assign(media_types.map(&:process).reduce({ }, &fusion)).to_processed 'content' + processed[:content] = media_types.map(&:process).reduce({ }, &fusion) processed end end diff --git a/lib/oas_objs/response_obj.rb b/lib/oas_objs/response_obj.rb index 0a5f4d9..7a4ddf5 100644 --- a/lib/oas_objs/response_obj.rb +++ b/lib/oas_objs/response_obj.rb @@ -22,7 +22,7 @@ def add_or_fusion(desc, media_type, hash) end def process - assign(media_types.map(&:process).reduce({ }, &fusion)).to_processed 'content' + processed[:content] = media_types.map(&:process).reduce({ }, &fusion) processed end end diff --git a/lib/oas_objs/schema_obj.rb b/lib/oas_objs/schema_obj.rb index 6ebf39b..28a83ca 100644 --- a/lib/oas_objs/schema_obj.rb +++ b/lib/oas_objs/schema_obj.rb @@ -15,19 +15,18 @@ class SchemaObj < Hash attr_accessor :processed, :type def initialize(type = nil, schema) - merge!(schema) + self.merge!(schema) self.processed = { type: nil, format: nil, **schema.except(:type, :range, :enum!, *SELF_MAPPING.values.flatten) } self.type = type || self[:type] end def process processed.merge!(recg_schema_type) - reducx(additional_properties, enum, length, range, format, other, desc).then_merge! - processed.keep_if &value_present + reducing(additional_properties, enum, length, range, format, other, desc) end def desc - result = @bang_enum.present? ? auto_generate_desc : _desc + return unless (result = @bang_enum.present? ? auto_generate_desc : _desc) { description: result } end @@ -99,8 +98,7 @@ def range end def format - # `format` that generated in process_type() may be overwrote here. - processed[:format].blank? ? { format: self[:format] || self[:is_a] } : { } + { format: self[:format] || self[:is_a] } unless processed[:format] end def other @@ -119,7 +117,7 @@ def other _desc: %i[ desc description d ], _addProp: %i[ additional_properties add_prop values_type ], }.each do |key, aliases| - define_method(key) { self[key] || aliases.each { |aname| self[key] ||= self[aname] } and self[key] } + define_method(key) { self[key] ||= self.values_at(*aliases).compact.first } define_method("#{key}=") { |value| self[key] = value } end end diff --git a/lib/open_api/dsl/components.rb b/lib/open_api/dsl/components.rb index 2d261c8..caaee6f 100644 --- a/lib/open_api/dsl/components.rb +++ b/lib/open_api/dsl/components.rb @@ -14,7 +14,7 @@ def initialize def schema component_key, type = nil, **schema_info schema = process_schema_input(type, schema_info, model: component_key) return Tip.schema_no_type(component_key) if schema[:illegal?] - self[:schemas][component_key.to_s.to_sym] = (schema[:combined] or SchemaObj.new(type = schema[:info], { })).process + self[:schemas][component_key.to_s.to_sym] = (schema[:combined] or SchemaObj.new(schema[:info], { })).process end arrow_enable :schema diff --git a/lib/open_api/dsl/helpers.rb b/lib/open_api/dsl/helpers.rb index b68825f..6194bea 100644 --- a/lib/open_api/dsl/helpers.rb +++ b/lib/open_api/dsl/helpers.rb @@ -25,7 +25,7 @@ def load_schema(model) # TODO: test def _combined_schema(one_of: nil, all_of: nil, any_of: nil, not: nil, **other) input = (_not = binding.local_variable_get(:not)) || one_of || all_of || any_of - CombinedSchema.new(one_of: one_of, all_of: all_of, any_of: any_of, _not: _not) if input + CombinedSchema.new(one_of: one_of, all_of: all_of, any_of: any_of, not: _not) if input end def process_schema_input(schema_type, schema, model: nil) From 8d398de0509034a253775f862f519a4055df969e Mon Sep 17 00:00:00 2001 From: zhandao Date: Mon, 11 Feb 2019 19:16:34 +0800 Subject: [PATCH 12/15] R / DSL --- lib/oas_objs/param_obj.rb | 3 +- lib/oas_objs/request_body_obj.rb | 5 ++-- lib/oas_objs/response_obj.rb | 5 ++-- lib/open_api.rb | 2 +- lib/open_api/dsl.rb | 2 +- lib/open_api/dsl/api.rb | 37 +++++++++--------------- lib/open_api/dsl/components.rb | 22 +++++--------- lib/open_api/dsl/helpers.rb | 10 ++----- lib/open_api/{generator.rb => router.rb} | 2 +- spec/api_spec.rb | 13 --------- spec/generator_spec.rb | 4 +-- 11 files changed, 35 insertions(+), 70 deletions(-) rename lib/open_api/{generator.rb => router.rb} (98%) diff --git a/lib/oas_objs/param_obj.rb b/lib/oas_objs/param_obj.rb index edc096e..72d0939 100644 --- a/lib/oas_objs/param_obj.rb +++ b/lib/oas_objs/param_obj.rb @@ -16,8 +16,7 @@ def initialize(name, param_type, type, required, schema) in: param_type.to_s.delete('!'), required: required.to_s[/req/].present? } - self.schema = schema.is_a?(CombinedSchema) ? schema : SchemaObj.new(type, schema) - merge!(schema) + merge!(self.schema = schema) end def process diff --git a/lib/oas_objs/request_body_obj.rb b/lib/oas_objs/request_body_obj.rb index 10bed0a..2868d2a 100644 --- a/lib/oas_objs/request_body_obj.rb +++ b/lib/oas_objs/request_body_obj.rb @@ -17,13 +17,14 @@ def initialize(required, desc) self.processed = { required: required['req'].present?, description: desc } end - def add_or_fusion(media_type, hash) + def absorb(media_type, hash) media_types << MediaTypeObj.new(media_type, hash) self end def process - processed[:content] = media_types.map(&:process).reduce({ }, &fusion) + content = media_types.map(&:process).reduce({ }, &fusion) + processed[:content] = content if content processed end end diff --git a/lib/oas_objs/response_obj.rb b/lib/oas_objs/response_obj.rb index 7a4ddf5..cab7b40 100644 --- a/lib/oas_objs/response_obj.rb +++ b/lib/oas_objs/response_obj.rb @@ -15,14 +15,15 @@ def initialize(desc) self.processed = { description: desc } end - def add_or_fusion(desc, media_type, hash) + def absorb(desc, media_type, hash) self.processed[:description] = desc if desc.present? media_types << MediaTypeObj.new(media_type, hash) self end def process - processed[:content] = media_types.map(&:process).reduce({ }, &fusion) + content = media_types.map(&:process).reduce({ }, &fusion) + processed[:content] = content if content processed end end diff --git a/lib/open_api.rb b/lib/open_api.rb index 7046a21..cde669c 100644 --- a/lib/open_api.rb +++ b/lib/open_api.rb @@ -5,7 +5,7 @@ require 'open_api/version' require 'open_api/support/tip' require 'open_api/config' -require 'open_api/generator' +require 'open_api/router' require 'open_api/dsl' module OpenApi diff --git a/lib/open_api/dsl.rb b/lib/open_api/dsl.rb index 8318cd1..eb31945 100644 --- a/lib/open_api/dsl.rb +++ b/lib/open_api/dsl.rb @@ -33,7 +33,7 @@ def components &block def api action, summary = '', id: nil, tag: nil, http: nil, dry: Config.default_run_dry, &block doc_tag if oas[:doc].blank? action_path = "#{oas[:route_base]}##{action}" - routes = Generator.routes_list[oas[:route_base]] + routes = Router.routes_list[oas[:route_base]] &.select { |api| api[:action_path][/^#{action_path}$/].present? } return Tip.no_route(action_path) if routes.blank? diff --git a/lib/open_api/dsl/api.rb b/lib/open_api/dsl/api.rb index b88bdf5..1550cee 100644 --- a/lib/open_api/dsl/api.rb +++ b/lib/open_api/dsl/api.rb @@ -13,7 +13,7 @@ def initialize(action_path = '', summary: nil, tags: [ ], id: nil) self.action_path = action_path self.dry_blocks = [ ] self.merge!(summary: summary, operationId: id, tags: tags, description: '', parameters: [ ], - requestBody: '', responses: { }, callbacks: { }, links: { }, security: [ ], servers: [ ]) + requestBody: nil, responses: { }, callbacks: { }, links: { }, security: [ ], servers: [ ]) end def this_api_is_invalid!(*) @@ -42,9 +42,8 @@ def dry only: nil, skip: nil, none: false def param param_type, name, type, required, schema = { } return if dry_skip&.include?(name) || dry_only&.exclude?(name) - schema = process_schema_input(type, schema) - return Tip.param_no_type(name) if schema[:illegal?] - param_obj = ParamObj.new(name, param_type, type, required, schema[:combined] || schema[:info]) + return unless schema = process_schema_input(type, schema, name) + param_obj = ParamObj.new(name, param_type, type, required, schema) # The definition of the same name parameter will be overwritten index = self[:parameters].map(&:name).index(param_obj.name) index ? self[:parameters][index] = param_obj : self[:parameters] << param_obj @@ -57,7 +56,7 @@ def param param_type, name, type, required, schema = { } param param_type, name, type, (param_type['!'] ? :req : :opt), schema end - # For supporting this: (just like `form '', data: { }` usage) + # For supporting: (just like `form '', data: { }`) # in_query( # :search_type => String, # :export! => { type: Boolean } @@ -75,8 +74,11 @@ def param_ref component_key, *keys # options: `exp_by` and `examples` def request_body required, media_type, data: { }, desc: '', **options - self[:requestBody] = RequestBodyObj.new(required, desc) unless self[:requestBody].is_a?(RequestBodyObj) - self[:requestBody].add_or_fusion(media_type, { data: data , **options }) + (self[:requestBody] ||= RequestBodyObj.new(required, desc)).absorb(media_type, { data: data , **options }) + end + + def body_ref component_key + self[:requestBody] = RefObj.new(:requestBody, component_key) end %i[ body body! ].each do |method| @@ -85,10 +87,6 @@ def request_body required, media_type, data: { }, desc: '', **options end end - def body_ref component_key - self[:requestBody] = RefObj.new(:requestBody, component_key) - end - def form data:, **options body :form, data: data, **options end @@ -102,24 +100,15 @@ def data name, type = nil, schema_info = { } form data: { name => schema_info } end - def file media_type, data: { type: File }, **options - body media_type, data: data, **options - end - - def file! media_type, data: { type: File }, **options - body! media_type, data: data, **options - end - def response code, desc, media_type = nil, data: { }, type: nil - self[:responses][code] = ResponseObj.new(desc) unless (self[:responses] ||= { })[code].is_a?(ResponseObj) - self[:responses][code].add_or_fusion(desc, media_type, { data: type || data }) + (self[:responses][code] ||= ResponseObj.new(desc)).absorb(desc, media_type, { data: type || data }) end alias_method :resp, :response alias_method :error, :response - def response_ref code_compkey_hash - code_compkey_hash.each { |code, component_key| self[:responses][code] = RefObj.new(:response, component_key) } + def response_ref code_and_compkey # = { } + code_and_compkey.each { |code, component_key| self[:responses][code] = RefObj.new(:response, component_key) } end def security_require scheme_name, scopes: [ ] @@ -128,7 +117,7 @@ def security_require scheme_name, scopes: [ ] alias security security_require alias auth security_require - alias need_auth security_require + alias auth_with security_require def callback event_name, http_method, callback_url, &block self[:callbacks].deep_merge! CallbackObj.new(event_name, http_method, callback_url, &block).process diff --git a/lib/open_api/dsl/components.rb b/lib/open_api/dsl/components.rb index caaee6f..c56b256 100644 --- a/lib/open_api/dsl/components.rb +++ b/lib/open_api/dsl/components.rb @@ -12,9 +12,8 @@ def initialize end def schema component_key, type = nil, **schema_info - schema = process_schema_input(type, schema_info, model: component_key) - return Tip.schema_no_type(component_key) if schema[:illegal?] - self[:schemas][component_key.to_s.to_sym] = (schema[:combined] or SchemaObj.new(schema[:info], { })).process + return unless schema = process_schema_input(type, schema_info, component_key, model: component_key) + self[:schemas][component_key.to_s.to_sym] = schema.process end arrow_enable :schema @@ -26,24 +25,19 @@ def example component_key, examples_hash arrow_enable :example def param component_key, param_type, name, type, required, schema = { } - schema = process_schema_input(type, schema) - return Tip.param_no_type(name) if schema[:illegal?] - self[:parameters][component_key] = - ParamObj.new(name, param_type, type, required, schema[:combined] || schema[:info]).process + return unless schema = process_schema_input(type, schema, name) + self[:parameters][component_key] = ParamObj.new(name, param_type, type, required, schema).process end %i[ header header! path path! query query! cookie cookie! ].each do |param_type| define_method param_type do |component_key, name, type = nil, **schema| param component_key, param_type, name, type, (param_type['!'] ? :req : :opt), schema end - arrow_enable param_type end def request_body component_key, required, media_type, data: { }, desc: '', **options - cur = self[:requestBodies][component_key] - cur = RequestBodyObj.new(required, desc) unless cur.is_a?(RequestBodyObj) - self[:requestBodies][component_key] = cur.add_or_fusion(media_type, { data: data, **options }) + (self[:requestBodies][component_key] ||= RequestBodyObj.new(required, desc)).absorb(media_type, { data: data, **options }) end %i[ body body! ].each do |method| @@ -56,8 +50,7 @@ def request_body component_key, required, media_type, data: { }, desc: '', **opt arrow_enable :body! def response component_key, desc, media_type = nil, data: { }, type: nil - self[:responses][component_key] = ResponseObj.new(desc) unless (self[:responses] ||= { })[component_key].is_a?(ResponseObj) - self[:responses][component_key].add_or_fusion(desc, media_type, { data: type || data }) + (self[:responses][component_key] ||= ResponseObj.new(desc)).absorb(desc, media_type, { data: type || data }) end alias_method :resp, :response @@ -88,8 +81,7 @@ def bearer_auth scheme_name, format = 'JWT', other_info = { } arrow_enable :bearer_auth def api_key scheme_name, field:, in: 'header', **other_info - _in = binding.local_variable_get(:in) - security_scheme scheme_name, { type: 'apiKey', name: field, in: _in, **other_info } + security_scheme scheme_name, { type: 'apiKey', name: field, in: binding.local_variable_get(:in), **other_info } end arrow_enable :api_key diff --git a/lib/open_api/dsl/helpers.rb b/lib/open_api/dsl/helpers.rb index 6194bea..e92581d 100644 --- a/lib/open_api/dsl/helpers.rb +++ b/lib/open_api/dsl/helpers.rb @@ -28,16 +28,12 @@ def _combined_schema(one_of: nil, all_of: nil, any_of: nil, not: nil, **other) CombinedSchema.new(one_of: one_of, all_of: all_of, any_of: any_of, not: _not) if input end - def process_schema_input(schema_type, schema, model: nil) + def process_schema_input(schema_type, schema, name, model: nil) schema = { type: schema } unless schema.is_a?(Hash) combined_schema = _combined_schema(schema) type = schema[:type] ||= schema_type - { - illegal?: type.nil? && combined_schema.nil?, - combined: combined_schema, - info: load_schema(model) || schema, - type: type - } + return Tip.param_no_type(name) if type.nil? && combined_schema.nil? + combined_schema || SchemaObj.new(type, load_schema(model) || schema) end # Arrow Writing: diff --git a/lib/open_api/generator.rb b/lib/open_api/router.rb similarity index 98% rename from lib/open_api/generator.rb rename to lib/open_api/router.rb index 8f53c36..9ac5499 100644 --- a/lib/open_api/generator.rb +++ b/lib/open_api/router.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module OpenApi - module Generator + module Router module_function def routes diff --git a/spec/api_spec.rb b/spec/api_spec.rb index fa36a67..055ecf8 100644 --- a/spec/api_spec.rb +++ b/spec/api_spec.rb @@ -164,19 +164,6 @@ end end end - - describe '#file and #file!' do - api -> { file :ppt }, has_its_structure! - focus_on :content - expect_it has_key: :'application/vnd.ms-powerpoint' - - step_into :'application/vnd.ms-powerpoint', :schema, :format - expect_it eq: OpenApi::Config.file_format - - context 'when calling the bang method' do - api -> { file! :doc }, include: { required: true } - end - end end describe '#body_ref' do diff --git a/spec/generator_spec.rb b/spec/generator_spec.rb index 01f8c40..08e5842 100644 --- a/spec/generator_spec.rb +++ b/spec/generator_spec.rb @@ -1,8 +1,8 @@ require 'support/open_api' -RSpec.describe OpenApi::Generator do +RSpec.describe OpenApi::Router do describe '.routes_list' do - subject { OpenApi::Generator.routes_list } + subject { OpenApi::Router.routes_list } it { is_expected.to have_keys 'goods' } let(:goods_routes) { subject['goods'] } From fc2754c086d301e33133ede6c8229f77acf6494b Mon Sep 17 00:00:00 2001 From: zhandao Date: Mon, 11 Feb 2019 20:13:13 +0800 Subject: [PATCH 13/15] D / rewrite 1 --- README.md | 86 +++++++------------ documentation/examples/examples_controller.rb | 60 ------------- documentation/examples/goods_doc.rb | 52 ----------- documentation/examples/open_api.rb | 26 +++--- lib/oas_objs/request_body_obj.rb | 2 +- lib/oas_objs/response_obj.rb | 2 +- lib/open_api/config.rb | 43 ++-------- 7 files changed, 48 insertions(+), 223 deletions(-) delete mode 100644 documentation/examples/examples_controller.rb delete mode 100644 documentation/examples/goods_doc.rb diff --git a/README.md b/README.md index 1e017d3..cddf169 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,7 @@ It may be a very useful tool if you want to write API document clearly. I'm looking forward to your issue and PR!** - [Need Help](https://github.com/zhandao/zero-rails_openapi/issues/14) - - If you have any questions, please read the test code first. - such as [api DSL](spec/api_spec.rb) and [schema Obj](spec/oas_objs/schema_obj_spec.rb). + (Test cases are rich, like: [api DSL](spec/api_spec.rb) and [schema Obj](spec/oas_objs/schema_obj_spec.rb)) ## Table of Contents @@ -86,10 +83,6 @@ $ bundle - Or install it yourself as: - - $ gem install zero-rails_openapi - ## Configure Create an initializer, configure ZRO and define your OpenApi documents. @@ -97,61 +90,44 @@ This is the simplest example: ```ruby - # config/initializers/open_api.rb + # in config/initializers/open_api.rb require 'open_api' - OpenApi::Config.tap do |c| - # [REQUIRED] The output location where .json doc file will be written to. - c.file_output_path = 'public/open_api' - - c.open_api_docs = { - # The definition of the document `homepage`. - homepage: { - # [REQUIRED] ZRO will scan all the descendants of base_doc_classes, then generate their docs. - base_doc_classes: [Api::V1::BaseController], - - # [REQUIRED] OAS Info Object: The section contains API information. - info: { - # [REQUIRED] The title of the application. - title: 'Homepage APIs', - # Description of the application. - description: 'API documentation of Rails Application.
' \ - 'Optional multiline or single-line Markdown-formatted description ' \ - 'in [CommonMark](http://spec.commonmark.org/) or `HTML`.', - # [REQUIRED] The version of the OpenAPI document - # (which is distinct from the OAS version or the API implementation version). - version: '1.0.0' - } - } - } + OpenApi::Config.class_eval do |c| + # Part 1: configs of this gem + self.file_output_path = 'public/open_api' + + # Part 2: config (DSL) for generating OpenApi info + open_api :doc_name, base_doc_classes: [ApiDoc] + info version: '1.0.0', title: 'Homepage APIs'#, description: .. + # server 'http://localhost:3000', desc: 'Internal staging server for testing' + # bearer_auth :Authorization end ``` + +### Part 1: configs of this gem - In addition to directly using Hash, - you can also use DSL to define the document information: - - ```ruby - # config/initializers/open_api.rb - require 'open_api' +1. `file_output_path`(required): The location where .json doc file will be output. +2. `default_run_dry`: defaults to run dry blocks even if the `dry` method is not called in the (Basic) DSL block. defaults to `false`. +3. `doc_location`: give regular expressions for file or folder paths. `Dir[doc_location]` will be `require` before document generates. + this option is only for not writing spec in controllers. +4. `rails_routes_file`: give a txt's file path (which's content is the copy of `rails routes`'s output). This will speed up document generation. +5. `model_base`: The parent class of models in your application. This option is for auto loading schema from database. +6. `file_format` - OpenApi::Config.tap do |c| - c.file_output_path = 'public/open_api' +### Part 2: config (DSL) for generating OpenApi info - c.instance_eval do - open_api :homepage_api, base_doc_classes: [ApiDoc] - info version: '1.0.0', title: 'Homepage APIs' - end - end - ``` + See all the DSLs: [config_dsl.rb](lib/open_api/config_dsl.rb) - For more detailed configuration: [open_api.rb](documentation/examples/open_api.rb) - See all the settings options: [config.rb](lib/open_api/config.rb) - See all the Document Definition DSL: [config_dsl.rb](lib/open_api/config_dsl.rb) +## DSL Usage -## Usage - DSL +There are two kinds of DSL for this gem: **basic** and **inside basic**. +1. Basic DSLs are class methods which is for declaring your APIs, components, and spec code DRYing ... +2. DSLs written inside the block of Basic DSLs, is for declaring the parameters, responses (and so on) of the specified API and component. -### First of all, `include OpenApi::DSL` to your base class (which is for writing docs), for example: +### First of all, `include OpenApi::DSL` to your base class (which is for writing spec): +For example: ```ruby # app/controllers/api/api_controller.rb class ApiController < ActionController::API @@ -166,16 +142,12 @@ ```ruby class Api::ExamplesController < ApiController api :index, 'GET list' do - query :page, Integer#, desc: 'page, greater than 1', range: { ge: 1 }, dft: 1 + query :page, Integer#, range: { ge: 1 }, default: 1 query :rows, Integer#, desc: 'per page', range: { ge: 1 }, default: 10 end end ``` - For more example, see [goods_doc.rb](documentation/examples/goods_doc.rb), and - [examples_controller.rb](documentation/examples/examples_controller.rb), - or [HERE](https://github.com/zhandao/zero-rails/tree/master/app/_docs/v1). - ### Basic DSL ([source code](lib/open_api/dsl.rb)) #### (1) `route_base` [optional if you're writing DSL in controller] diff --git a/documentation/examples/examples_controller.rb b/documentation/examples/examples_controller.rb deleted file mode 100644 index 8b267a4..0000000 --- a/documentation/examples/examples_controller.rb +++ /dev/null @@ -1,60 +0,0 @@ -class Api::V1::ExamplesController < Api::V1::BaseController - doc_tag name: 'ExampleTagName', desc: 'ExamplesController\'s APIs' - - components do - schema :DogSchema => [ String, dft: 'doge' ] - schema :PetSchema => [ not: [ Integer, Boolean ] ] - query! :UidQuery => [ :uid, String, desc: 'user uid' ] - path! :IdPath => [ :id, Integer, desc: 'product id' ] - resp :BadRqResp => [ 'bad request', :json ] - end - - - api_dry %i[ index show ], 'common parts of :index and :show' do - header! :Token, String - response 1000, 'data export', :pdf, type: File - end - - - api :index, 'GET examples', use: :Token do - this_api_is_invalid! 'do not use!' - desc '**GET** list of examples,
and get the status 200.', - id: 'user id', - email: 'email addr\'s desc' - email = 'a@b.c' - - query! :count, Integer, enum: 0..5, length: [1, 2], pattern: /^[0-9]$/, range: { gt: 0, le: 5 } - query! :done, Boolean, must_be: false, default: true, desc: 'must be false' - query :email, String, lth: :ge_3, default: email # is_a: :email - file :pdf, 'upload a file: the media type should be application/pdf' - - query :test_type, type: String - query :combination, one_of: [ :DogSchema, String, { type: Integer, desc: 'integer input'}] - form data: { - :combination => { any_of: [ Integer, String ] } - } - - response :success, 'success response', :json#, data: :Pet - security :Token - - resp 200, '', :json, data: { - a: String - } - end - - - api :show, skip: :Token do - param_ref :IdPath, :UidQuery - response_ref 400 => :BadRqResp - end - - - api :create do - form! data: { - :name! => String, # <= schema_type is `String` - :password! => { type: String, pattern: /[0-9]{6,10}/, desc: 'password' }, - # optional - :remarks => { type: String, desc: 'remarks' }, # <= schema_type is `String`, and schema_info is { desc: '..' } - } - end -end diff --git a/documentation/examples/goods_doc.rb b/documentation/examples/goods_doc.rb deleted file mode 100644 index 761927c..0000000 --- a/documentation/examples/goods_doc.rb +++ /dev/null @@ -1,52 +0,0 @@ -class V2::GoodsDoc < ApiDoc - SCHEMA_DRY = { a: 1, b: 2 } - - # skip: [ 'Token' ] do # you can also skip parameters - api :index, 'GET list of goods.', use: [ 'Token', :page, :rows ] do # use parameters write in AutoGenDoc#api_dry - desc 'listing goods', - view!: 'search view, allows:
', - search_type!: 'search field, allows:
' - - # Single `query` - query :view, String, enum!: { - 'all goods (default)': :all, - 'only online': :online, - 'only offline': :offline, - 'expensive goods': :expensive, - 'cheap goods': :cheap, - }, **SCHEMA_DRY # >>> Here is a little trick! <<< - # Batch `query` - do_query by: { - :search_type => { type: String, enum: %w[ name creator category price ] }, - :value => String, - :export => { type: Boolean, desc: 'export as Excel format', examples: { - :right_input => true, - :wrong_input => 'wrong input' - }} - } - end - - - api :create, 'POST create a good', use: 'Token' do - form! data: { - :name! => { type: String, desc: 'good\'s name' }, - :category_id! => { type: Integer, desc: 'sub_category\'s id', npmt: true, range: { ge: 1 }, as: :cate }, - :price! => { type: Float, desc: 'good\'s price', range: { ge: 0 } }, - # -- optional - :is_online => { type: Boolean, desc: 'it\'s online?' }, - :remarks => { type: String, desc: 'remarks' }, - :pic_path => { type: String, desc: 'picture url', is: :url }, - }, - exp_by: %i[ name category_id price ], - examples: { - :right_input => [ 'good1', 6, 5.7 ], - :wrong_input => [ 'good2', 0, -1 ] - } - end - - - api :show, 'GET the specified Good.', use: [ 'Token', :id ] - - - api :destroy, 'DELETE the specified Good.', use: [ 'Token', :id ] -end diff --git a/documentation/examples/open_api.rb b/documentation/examples/open_api.rb index 471c489..5c29ecb 100644 --- a/documentation/examples/open_api.rb +++ b/documentation/examples/open_api.rb @@ -1,25 +1,23 @@ require 'open_api' -OpenApi::Config.tap do |c| +OpenApi::Config.class_eval do # Config DSL - c.instance_eval do - open_api :zero_rails, base_doc_classes: [ApiDoc] - info version: '0.0.1', title: 'Zero Rails APIs', description: 'API documentation of Zero-Rails Application.' - server 'http://localhost:3000', desc: 'Main (production) server' - server 'http://localhost:3000', desc: 'Internal staging server for testing' - bearer_auth :Token - global_auth :Token - end + open_api :zero_rails, base_doc_classes: [ApiDoc] + info version: '0.0.1', title: 'Zero Rails APIs', description: 'API documentation of Zero-Rails Application.' + server 'http://localhost:3000', desc: 'Main (production) server' + server 'http://localhost:3000', desc: 'Internal staging server for testing' + bearer_auth :Token + global_auth :Token # [REQUIRED] The location where .json doc file will be output. - c.file_output_path = 'public/open_api' + self.file_output_path = 'public/open_api' # [Optional] Use this txt instead of running `rails routes`. - # c.rails_routes_file = 'config/routes.txt' + # self.rails_routes_file = 'config/routes.txt' # Everything about OAS3 is on https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md # Getting started: https://swagger.io/docs/specification/basic-structure/ - c.open_api_docs = { + self.open_api_docs = { blog_api: { # [REQUIRED] ZRO will scan all the descendants of the base_doc_classes, then generate their docs. base_doc_classes: [ApiController], @@ -96,9 +94,7 @@ end -Object.const_set('Boolean', 'boolean') # Support `Boolean` writing in DSL - -OpenApi.write_docs generate_files: !Rails.env.production? +OpenApi.write_docs if: !Rails.env.production? __END__ diff --git a/lib/oas_objs/request_body_obj.rb b/lib/oas_objs/request_body_obj.rb index 2868d2a..485ea44 100644 --- a/lib/oas_objs/request_body_obj.rb +++ b/lib/oas_objs/request_body_obj.rb @@ -24,7 +24,7 @@ def absorb(media_type, hash) def process content = media_types.map(&:process).reduce({ }, &fusion) - processed[:content] = content if content + processed[:content] = content if content.present? processed end end diff --git a/lib/oas_objs/response_obj.rb b/lib/oas_objs/response_obj.rb index cab7b40..0f9af7d 100644 --- a/lib/oas_objs/response_obj.rb +++ b/lib/oas_objs/response_obj.rb @@ -23,7 +23,7 @@ def absorb(desc, media_type, hash) def process content = media_types.map(&:process).reduce({ }, &fusion) - processed[:content] = content if content + processed[:content] = content if content.present? processed end end diff --git a/lib/open_api/config.rb b/lib/open_api/config.rb index a41e3e9..7e8fa0c 100644 --- a/lib/open_api/config.rb +++ b/lib/open_api/config.rb @@ -10,50 +10,19 @@ module Config cattr_accessor :default_run_dry, default: false # [REQUIRED] The location where .json doc file will be output. - cattr_accessor :file_output_path do - 'public/open_api' - end - - cattr_accessor :generate_doc do - true - end + cattr_accessor :file_output_path, default: 'public/open_api' - cattr_accessor :doc_location do - ['./app/**/*_doc.rb'] - end + cattr_accessor :doc_location, default: ['./app/**/*_doc.rb'] - cattr_accessor :rails_routes_file do - nil - end + cattr_accessor :rails_routes_file - cattr_accessor :model_base do - nil - end + cattr_accessor :model_base # Everything about OAS3 is on https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md # Getting started: https://swagger.io/docs/specification/basic-structure/ - cattr_accessor :open_api_docs do - { - # # [REQUIRED] At least one doc. - # zero_rails: { - # # [REQUIRED] OpenApi will scan all the descendants of the base_doc_classes, and then generate their docs. - # base_doc_classes: [ApplicationController], - # - # # [REQUIRED] Info Object: The info section contains API information - # info: { - # # [REQUIRED] The title of the application. - # title: 'Zero Rails Apis', - # # [REQUIRED] The version of the OpenAPI document - # # (which is distinct from the OpenAPI Specification version or the API implementation version). - # version: '0.0.1' - # } - # } - } - end + cattr_accessor :open_api_docs, default: { } - cattr_accessor :file_format do - 'binary' - end + cattr_accessor :file_format, default: 'binary' def self.docs open_api_docs From 85951a784d694da4d5314613d9d78d37ae81f070 Mon Sep 17 00:00:00 2001 From: zhandao Date: Tue, 12 Feb 2019 11:41:37 +0800 Subject: [PATCH 14/15] D / rewrite 2 --- README.md | 394 +++++++++--------- documentation/parameter.md | 14 - .../examples => examples}/auto_gen_desc.rb | 0 .../examples => examples}/auto_gen_doc.rb | 0 .../examples => examples}/open_api.rb | 0 .../examples => examples}/output_example.json | 0 lib/open_api/dsl/api.rb | 11 +- 7 files changed, 202 insertions(+), 217 deletions(-) rename {documentation/examples => examples}/auto_gen_desc.rb (100%) rename {documentation/examples => examples}/auto_gen_doc.rb (100%) rename {documentation/examples => examples}/open_api.rb (100%) rename {documentation/examples => examples}/output_example.json (100%) diff --git a/README.md b/README.md index cddf169..be804b0 100644 --- a/README.md +++ b/README.md @@ -32,21 +32,22 @@ - [Configure](#configure) - [Usage - DSL](#usage---dsl) - [Basic DSL](#basic-dsl) - - [route_base](#1-route_base-optional-if-youre-writing-dsl-in-controller) + - [route_base](#1-route_base-required-if-youre-not-writing-dsl-in-controller) - [doc_tag](#2-doc_tag-optional) - [components](#3-components-optional) - - [api_dry](#4-api_dry-optional) - - [api](#5-api-required) - - [DSL methods inside `api` and `api_dry`'s block](#dsl-methods-inside-api-and-api_drys-block) - - [this_api_is_invalid!](#1-this_api_is_invalid-its-aliases) - - [desc](#2-desc-description-for-the-current-api-and-its-inputs-parameters-and-request-body) + - [api](#4-api-required) + - [api_dry](#5-api_dry-optional) + - [DSLs written inside `api` and `api_dry`'s block](#dsl-methods-inside-api-and-api_drys-block) + - [this_api_is_invalid!](#1-this_api_is_invalid-and-its-aliases) + - [desc](#2-desc-description-for-the-current-api) - [param family methods](#3-param-family-methods-oas---parameter-object) - [request_body family methods](#4-request_body-family-methods-oas---request-body-object) - [response family methods](#5-response-family-methods-oas---response-object) - [callback](#6-callback-oas---callback-object) - [Authentication and Authorization](#7-authentication-and-authorization) - [server](#8-overriding-global-servers-by-server) - - [DSL methods inside `components`'s block](#dsl-methods-inside-componentss-block-code-source) + - [DSLs written inside `components`'s block](#dsl-methods-inside-componentss-block-code-source) + - [Schema and Type](#schema-and-type) - [Run! - Generate JSON documentation file](#run---generate-json-documentation-file) - [Use Swagger UI(very beautiful web page) to show your Documentation](#use-swagger-uivery-beautiful-web-page-to-show-your-documentation) - [Tricks](#tricks) @@ -93,7 +94,7 @@ # in config/initializers/open_api.rb require 'open_api' - OpenApi::Config.class_eval do |c| + OpenApi::Config.class_eval do # Part 1: configs of this gem self.file_output_path = 'public/open_api' @@ -107,13 +108,13 @@ ### Part 1: configs of this gem -1. `file_output_path`(required): The location where .json doc file will be output. -2. `default_run_dry`: defaults to run dry blocks even if the `dry` method is not called in the (Basic) DSL block. defaults to `false`. -3. `doc_location`: give regular expressions for file or folder paths. `Dir[doc_location]` will be `require` before document generates. - this option is only for not writing spec in controllers. -4. `rails_routes_file`: give a txt's file path (which's content is the copy of `rails routes`'s output). This will speed up document generation. -5. `model_base`: The parent class of models in your application. This option is for auto loading schema from database. -6. `file_format` + 1. `file_output_path`(required): The location where .json doc file will be output. + 2. `default_run_dry`: defaults to run dry blocks even if the `dry` method is not called in the (Basic) DSL block. defaults to `false`. + 3. `doc_location`: give regular expressions for file or folder paths. `Dir[doc_location]` will be `require` before document generates. + this option is only for not writing spec in controllers. + 4. `rails_routes_file`: give a txt's file path (which's content is the copy of `rails routes`'s output). This will speed up document generation. + 5. `model_base`: The parent class of models in your application. This option is for auto loading schema from database. + 6. `file_format` ### Part 2: config (DSL) for generating OpenApi info @@ -121,19 +122,19 @@ ## DSL Usage -There are two kinds of DSL for this gem: **basic** and **inside basic**. -1. Basic DSLs are class methods which is for declaring your APIs, components, and spec code DRYing ... -2. DSLs written inside the block of Basic DSLs, is for declaring the parameters, responses (and so on) of the specified API and component. + There are two kinds of DSL for this gem: **basic** and **inside basic**. + 1. Basic DSLs are class methods which is for declaring your APIs, components, and spec code DRYing ... + 2. DSLs written inside the block of Basic DSLs, is for declaring the parameters, responses (and so on) of the specified API and component. -### First of all, `include OpenApi::DSL` to your base class (which is for writing spec): +### First of all, `include OpenApi::DSL` in your base class (which is for writing spec): -For example: - ```ruby - # app/controllers/api/api_controller.rb - class ApiController < ActionController::API - include OpenApi::DSL - end - ``` + For example: + ```ruby + # in app/controllers/api/api_controller.rb + class ApiController < ActionController::API + include OpenApi::DSL + end + ``` ### DSL Usage Example @@ -148,103 +149,109 @@ For example: end ``` -### Basic DSL ([source code](lib/open_api/dsl.rb)) +### Basic DSL + + [source code](lib/open_api/dsl.rb) -#### (1) `route_base` [optional if you're writing DSL in controller] +#### (1) `route_base` [required if you're not writing DSL in controller] ```ruby - # method signature - route_base(path) - # usage + # ** Method Signature + route_base path + # ** Usage route_base 'api/v1/examples' ``` - It is optional because `route_base` defaults to `controller_path`. - [Here's a trick](#trick1---write-the-dsl-somewhere-else): Using `route_base`, you can write the DSL somewhere else - to simplify the current controller. + [Usage](#trick1---write-the-dsl-somewhere-else): write the DSL somewhere else to simplify the current controller. #### (2) `doc_tag` [optional] ```ruby - # method signature - doc_tag(name: nil, desc: '', external_doc_url: nil) - # usage - doc_tag name: 'ExampleTagName', desc: "ExamplesController's APIs" + # ** Method Signature + doc_tag name: nil, **tag_info + # ** Usage + doc_tag name: 'ExampleTagName', description: "ExamplesController's APIs"#, externalDocs: ... ``` - This method allows you to set the Tag (which is a node of [OpenApi Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#openapi-object)). + This method allows you to set the Tag (which is a node of [OpenApi Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#openapi-object)) + of all the APIs in the class. - Tag's name defaults to controller_name. desc and external_doc_url are optional. + Tag's name defaults to controller_name. #### (3) `components` [optional] ```ruby - # method signature + # ** Method Signature components(&block) - # usage + # ** Usage components do - # DSL for defining components + # (block inside) DSL for defining components schema :DogSchema => [ { id: Integer, name: String }, dft: { id: 1, name: 'pet' } ] query! :UidQuery => [ :uid, String, desc: 'uid' ] - resp :BadRqResp => [ 'bad request', :json ] + response :BadRqResp => [ 'bad request', :json ] end # to use component - api :action, 'summary' do + api :action do query :doge, :DogSchema # to use a Schema component param_ref :UidQuery # to use a Parameter component response_ref :BadRqResp # to use a Response component end ``` - Component can be used to simplify your DSL code - (that is, to refer to the defined Component object by `*_ref` methods). - Each RefObj is associated with components through component key. - We suggest that component keys should be camelized, and must be Symbol. -#### (4) `api_dry` [optional] + We suggest that component keys should be camelized, and **must be Symbol**. + +#### (4) `api` [required] + + For defining API (or we could say controller action). + + ```ruby + # ** Method Signature + api action_name, summary = '', id: nil, tag: nil, http: nil, dry: Config.default_run_dry, &block + # ** Usage + api :index, '(SUMMARY) this api blah blah ...', # block ... + ``` + + Parameters explanation: + 1. action_name: must be the same as controller action name + 2. id: operationId + 3. http: HTTP method (like: 'GET' or 'GET|POST') + +#### (5) `api_dry` [optional] This method is for DRYing. + The blocks passed to `api_dry` will be executed to the specified APIs which are having the actions or tags in the class. ```ruby - # method signature - api_dry(action = :all, desc = '', &block) - # usage + # ** Method Signature + api_dry action_or_tags = :all, &block + # ** Usage api_dry :all, 'common response' # block ... api_dry :index # block ... + api_dry :TagA # block ... + api_dry [:index, :show] do - query! #... + query #... end ``` - - As you think, the block will be executed to each specified API(action) **firstly**. - -#### (5) `api` [required] - - Define the specified API (or we could say controller action). - - ```ruby - # method signature - api(action, summary = '', http: nil, skip: [ ], use: [ ], &block) - # usage - api :index, '(SUMMARY) this api blah blah ...', # block ... - ``` - - `use` and `skip` options: to use or skip the parameters defined in `api_dry`. - + + And then you should call `dry` method ([detailed info]()) for executing the declared dry blocks: ```ruby - api :show, 'summary', use: [:id] # it will only take :id from DRYed result to define the API :show + api :index do + dry + end ``` -### DSL methods inside [api]() and [api_dry]()'s block +### DSLs written inside [api](#4-api-required) and [api_dry](#5-api_dry-optional)'s block - [source code](lib/open_api/dsl/api_info_obj.rb) + [source code](lib/open_api/dsl/api.rb) These following methods in the block describe the specified API action: description, valid?, - parameters, request body, responses, securities, servers. + parameters, request body, responses, securities and servers. (Here corresponds to OAS [Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#operationObject)) -#### (1) `this_api_is_invalid!`, its aliases: +#### (1) `this_api_is_invalid!`, and its aliases: ``` this_api_is_expired! this_api_is_unused! @@ -252,114 +259,99 @@ For example: ``` ```ruby - # method signature - this_api_is_invalid!(explain = '') - # usage - this_api_is_invalid! 'this api is expired!' + # ** Method Signature + this_api_is_invalid!(*) + # ** Usage + this_api_is_invalid! 'cause old version' ``` - Then `deprecated` of this API will be set to true. + After that, `deprecated` field of this API will be set to true. -#### (2) `desc`: description for the current API and its inputs (parameters and request body) +#### (2) `desc`: description for the current API ```ruby - # method signature - desc(desc, param_descs = { }) - # usage - desc "current API's description", - id: 'desc of the parameter :id', - email: 'desc of the parameter :email' + # ** Method Signature + desc string + # ** Usage + desc "current API's description" ``` - You can of course describe the input in it's DSL method (like `query! :done ...`, [this line](https://github.com/zhandao/zero-rails_openapi#dsl-usage-example)), - but that will make it long and ugly. We recommend that unite descriptions in this place. - - In addition, when you want to dry the same parameters (each with a different description), it will be of great use. - #### (3) `param` family methods (OAS - [Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#parameterObject)) - Define the parameters for the API (action). + To define parameter for APIs. ``` - param - param_ref # for reuse component, - # it links sepcified RefObjs (by component keys) to current parameters. - header, path, query, cookie # will pass specified parameter location to `param` - header!, path!, query!, cookie! # bang method of above methods - do_* by: { parameter_definations } # batch definition parameters, such as do_path, do_query - order # order parameters by names array you passed - examples # define examples of parameters + param # 1. normal usage + param_ref # 2. links sepcified RefObjs (by component keys) to current parameters. + header, path, query, cookie # 3. passes specified parameter location (like header) to `param` + header!, path!, query!, cookie! # 4. bang method of above methods + in_* by: { parameter_definations } # 5. batch definition, such as `in_path`, `in_query` + examples # 6. examples of parameters ``` - **The bang method (which's name is end of a exclamation point `!`) means this param is required, so without `!` means optional.** - **THE SAME BELOW.** + **The bang method and param_name (which's name is end of a exclamation point `!`) means this param is required. Without `!` means optional. THE SAME BELOW.** ```ruby - # `param_type` just is the location of parameter, like: query, path - # `schema_type` is the type of parameter, like: String, Integer (must be a constant) - # For more explanation, please click the link below ↓↓↓ - # method signature - param(param_type, param_name, schema_type, is_required, schema_info = { }) - # usage - param :query, :page, Integer, :req, range: { gt: 0, le: 5 }, desc: 'page' - - - # method signature - param_ref(component_key, *component_keys) # should pass at least 1 key - # usage - param_ref :IdPath - param_ref :IdPath, :NameQuery, :TokenHeader - - - ### method signature - header(param_name, schema_type = nil, **schema_info) - header!(param_name, schema_type = nil, **schema_info) - query!(param_name, schema_type = nil, **schema_info) - # ... - ### usage - header! 'Token', String - query! :readed, Boolean, must_be: true, default: false - # The same effect as above, but not simple - param :query, :readed, Boolean, :req, must_be: true, default: false - # - # When schema_type is a Object - # (describe by hash, key means prop's name, value means prop's schema_type) - query :good, { name: String, price: Float, spec: { size: String, weight: Integer } }, desc: 'good info' - # Or you can use `type:` to sign the schema_type, maybe this is clearer for describing object - query :good, type: { name: String, price: Float, spec: { size: String, weight: Integer } }, desc: 'good info' - # - query :good_name, type: String # It's also OK, but some superfluous - query :good_name, String # recommended - # About Combined Schema (`one_of` ..), see the link below. - - - # method signature - do_query(by:) - # usage - do_query by: { + # Part 1 + # param_type: location of parameter, like: query, path [A] + # param_name: name of parameter, it can be Symbol or String [B] + # schema_type: type of parameter, like: String, Integer (must be a constant). see #schema-and-type + # required: :required / :req OR :optional / :opt + # schema: see #schema-and-type (including combined schema) + # ** Method Signature + param param_type, param_name, schema_type, required, schema = { } + # ** Usage + param :query, :page, Integer, :req, range: { gt: 0, le: 5 }, desc: 'page number' + + # Part 2 + # ** Method Signature + param_ref *component_key # should pass at least 1 key + # ** Usage + param_ref :IdPath#, :NameQuery, :TokenHeader + + # Part 3 & 4 + # ** Method Signature + header param_name, schema_type = nil, **schema + query! param_name, schema_type = nil, **schema + # ** Usage + header :'X-Token', String + query! :readed, Boolean, default: false + # The same effect as above, but not concise + param :query, :readed, Boolean, :req, default: false + + # Part 5 + # ** Method Signature + in_query **params_and_schema + # ** Usage + in_query( search_type: String, search_val: String, - export!: Boolean - } - # The same effect as above, but a little bit repetitive + export!: { type: Boolean, desc: 'export as pdf' } + ) + # The same effect as above query :search_type, String query :search_val, String - query! :export, Boolean - - - # method signature - # `exp_by` (select_example_by): choose the example fields. - examples(exp_by = :all, examples_hash) - # usage - # it defines 2 examples by using parameter :id and :name - # if pass :all to `exp_by`, keys will be all the parameter's names. + query! :export, Boolean, desc: 'export as pdf' + + # Part 6 + # ** Method Signature + examples exp_params = :all, examples_hash + # ** Usage + # Suppose we have three parameters: id, name, age + # * normal + examples( + right_input: [ 1, 'user', 26 ], + wrong_input: [ 2, 'resu', 35 ] + ) + # * using exp_params examples [:id, :name], { - :right_input => [ 1, 'user'], # == { id: 1, name: 'user' } - :wrong_input => [ -1, '' ] + right_input: [ 1, 'user' ], + wrong_input: [ 2, 'resu' ] } ``` - [This trick show you how to define combined schema (by using `one_of` ..)](#trick6---combined-schema-one-of--all-of--any-of--not) + [A] OpenAPI 3.0 distinguishes between the following parameter types based on the parameter location: + **header, path, query, cookie**. [more info](https://swagger.io/docs/specification/describing-parameters/) - [**>> More About `param` DSL <<**](documentation/parameter.md) + [B] If `param_type` is path, for example: if the API path is `/good/:id`, you have to declare a path parameter named `id` #### (4) `request_body` family methods (OAS - [Request Body Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#requestBodyObject)) @@ -376,23 +368,23 @@ For example: Bang methods(!) means the specified media-type body is required. ```ruby - # method signature + # ** Method Signature request_body(required, media_type, data: { }, **options) - # usage + # ** Usage # (1) `data` contains all the attributes required by this request body. # (2) `param_name!` means it is required, otherwise without '!' means optional. request_body :opt, :form, data: { id!: Integer, name: { type: String, desc: 'name' } }, desc: 'form-data' - # method signature + # ** Method Signature body_ref(component_key) - # usage + # ** Usage body_ref :UpdateDogeBody - # method signature + # ** Method Signature body!(media_type, data: { }, **options) - # usage + # ** Usage body :json @@ -400,7 +392,7 @@ For example: def form data:, **options body :form, data: data, **options end - # usage + # ** Usage form! data: { name: String, password: String, @@ -425,7 +417,7 @@ For example: schema_info[:type] = type if type.present? form data: { name => schema_info } end - # usage: please look at the 4th point below + # ** Usage: please look at the 4th point below # about `file` def file! media_type, data: { type: File }, **options @@ -473,21 +465,21 @@ For example: ``` ```ruby - # method signature + # ** Method Signature response(code, desc, media_type = nil, data: { }, type: nil) - # usage + # ** Usage resp 200, 'json response', :json, data: { name: 'test' } response 200, 'query result', :pdf, type: File # same as: response 200, 'query result', :pdf, data: File - # method signature + # ** Method Signature response_ref(code_compkey_hash) - # usage + # ** Usage response_ref 700 => :AResp, 800 => :BResp ``` - **practice:** Automatically generate responses based on the agreed error class. [AutoGenDoc](documentation/examples/auto_gen_doc.rb#L63) + **practice:** Automatically generate responses based on the agreed error class. [AutoGenDoc](examples/auto_gen_doc.rb#L63) ### (6) Callback (OAS - [Callback Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#callback-object)) @@ -511,9 +503,9 @@ For example: To define callbacks, you can use `callback` method: ```ruby - # method signature + # ** Method Signature callback(event_name, http_method, callback_url, &block) - # usage + # ** Usage callback :myEvent, :post, 'localhost:3000/api/goods' do query :name, String data :token, String @@ -545,16 +537,16 @@ For example: ``` It's very simple to use (if you understand the above document) ```ruby - # method signature + # ** Method Signature security_scheme(scheme_name, other_info) - # usage + # ** Usage security_scheme :BasicAuth, { type: 'http', scheme: 'basic', desc: 'basic auth' } - # method signature + # ** Method Signature base_auth(scheme_name, other_info = { }) bearer_auth(scheme_name, format = 'JWT', other_info = { }) api_key(scheme_name, field:, in:, **other_info) - # usage + # ** Usage base_auth :BasicAuth, desc: 'basic auth' # the same effect as ↑↑↑ bearer_auth :Token api_key :ApiKeyAuth, field: 'X-API-Key', in: 'header', desc: 'pass api key to header' @@ -578,9 +570,9 @@ For example: ``` Name is different, signature and usage is similar. ```ruby - # method signature + # ** Method Signature security_require(scheme_name, scopes: [ ]) - # usage + # ** Usage global_auth :Token need_auth :Token auth :OAuth, scopes: %w[ read_example admin ] @@ -589,13 +581,13 @@ For example: #### (8) Overriding Global Servers by `server` ```ruby - # method signature + # ** Method Signature server(url, desc: '') - # usage + # ** Usage server 'http://localhost:3000', desc: 'local' ``` -### DSL methods inside [components]()'s block ([code source](lib/open_api/dsl/components.rb)) +### DSLs written inside [components]()'s block ([code source](lib/open_api/dsl/components.rb)) (Here corresponds to OAS [Components Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#componentsObject)) @@ -621,9 +613,9 @@ For example: (2) You can use `schema` to define a Schema Component. ```ruby - # method signature + # ** Method Signature schema(component_key, type = nil, **schema_info) - # usage + # ** Usage schema :Dog => [ String, desc: 'dogee' ] # <= schema_type is `String` # advance usage schema :Dog => [ @@ -641,7 +633,21 @@ For example: # it will automatically read the db schema to generate the component. schema User # easy! And the component_key will be :User ``` - [1] see: [Type](documentation/parameter.md#type-schema_type) + [1] see: [Type](#schema-and-type) + +### Schema and Type + +```ruby +# When schema_type is a Object + # (describe by hash, key means prop's name, value means prop's schema_type) + query :good, { name: String, price: Float, spec: { size: String, weight: Integer } }, desc: 'good info' + # Or you can use `type:` to sign the schema_type, maybe this is clearer for describing object + query :good, type: { name: String, price: Float, spec: { size: String, weight: Integer } }, desc: 'good info' + # + query :good_name, type: String # It's also OK, but some superfluous + query :good_name, String # recommended + # About Combined Schema (`one_of` ..), see the link below. +``` ## Run! - Generate JSON Documentation File @@ -707,7 +713,7 @@ For example: Method `api_dry` is for DRY but its scope is limited to the current controller. - I have no idea of best practices, But you can look at this [file](documentation/examples/auto_gen_doc.rb). + I have no idea of best practices, But you can look at this [file](examples/auto_gen_doc.rb). The implementation of the file is: do `api_dry` when inherits the base controller inside `inherited` method. You can use `sort` to specify the order of parameters. @@ -741,7 +747,7 @@ For example: 'cheap goods': :borrow, } ``` - Read this [file](documentation/examples/auto_gen_desc.rb) to learn more. + Read this [file](examples/auto_gen_desc.rb) to learn more. ### Trick4 - Skip or Use parameters define in api_dry @@ -750,13 +756,13 @@ For example: api :index, 'desc', skip: [ :Token ] ``` - Look at this [file](documentation/examples/goods_doc.rb) to learn more. + Look at this [file](examples/goods_doc.rb) to learn more. ### Trick5 - Auto Generate index/show Actions's Response-Types Based on DB Schema Use method `load_schema` in `api_dry`. - See this [file](documentation/examples/auto_gen_doc.rb#L51) for uasge information. + See this [file](examples/auto_gen_doc.rb#L51) for uasge information. ### Trick6 - Combined Schema (one_of / all_of / any_of / not) @@ -798,8 +804,6 @@ For example: ## Development - TODO .. - After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). @@ -810,4 +814,4 @@ For example: ## Code of Conduct - Everyone interacting in the Zero-OpenApi project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md). + Everyone interacting in the Zero-RailsOpenApi project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md). diff --git a/documentation/parameter.md b/documentation/parameter.md index 60ae6c3..024df04 100644 --- a/documentation/parameter.md +++ b/documentation/parameter.md @@ -1,17 +1,6 @@ ### More Explanation for `param` and `schema_info` -#### param_type (param_location) -OpenAPI 3.0 distinguishes between the following parameter types based on the parameter location: -**header, path, query, cookie**. [more](https://swagger.io/docs/specification/describing-parameters/) - -#### name (param_name) -The name of parameter. It can be Symbol or String. - -If param_type is :path, it must correspond to the associated path segment form -the routing path, for example: if the API path is `/good/:id`, you have to declare a path parameter with name `id` to it. - #### type (schema_type) -Parameter's (schema) type. We call it `schema_type` because it is inside SchemaObj. Support all [data types](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#dataTypes) defined in OAS. @@ -35,9 +24,6 @@ All the types you can use as following: You can use `Object.const_set()` to define a constant that does not exist, but note that the value you set could not be a Symbol (it will be explained as a Ref Object), should be a String. -#### required - :opt or :req - #### Schema Hash The [[schema]](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#schemaObject) defining the type used for the parameter. diff --git a/documentation/examples/auto_gen_desc.rb b/examples/auto_gen_desc.rb similarity index 100% rename from documentation/examples/auto_gen_desc.rb rename to examples/auto_gen_desc.rb diff --git a/documentation/examples/auto_gen_doc.rb b/examples/auto_gen_doc.rb similarity index 100% rename from documentation/examples/auto_gen_doc.rb rename to examples/auto_gen_doc.rb diff --git a/documentation/examples/open_api.rb b/examples/open_api.rb similarity index 100% rename from documentation/examples/open_api.rb rename to examples/open_api.rb diff --git a/documentation/examples/output_example.json b/examples/output_example.json similarity index 100% rename from documentation/examples/output_example.json rename to examples/output_example.json diff --git a/lib/open_api/dsl/api.rb b/lib/open_api/dsl/api.rb index 1550cee..d2d6b14 100644 --- a/lib/open_api/dsl/api.rb +++ b/lib/open_api/dsl/api.rb @@ -56,11 +56,6 @@ def param param_type, name, type, required, schema = { } param param_type, name, type, (param_type['!'] ? :req : :opt), schema end - # For supporting: (just like `form '', data: { }`) - # in_query( - # :search_type => String, - # :export! => { type: Boolean } - # ) define_method "in_#{param_type}" do |params| params.each_pair do |param_name, schema| param param_type, param_name, nil, (param_type['!'] || param_name['!'] ? :req : :opt), schema @@ -127,9 +122,9 @@ def server url, desc: '' self[:servers] << { url: url, description: desc } end - def param_examples exp_by = :all, examples_hash - exp_by = self[:parameters].map(&:name) if exp_by == :all - self[:examples] = ExampleObj.new(examples_hash, exp_by, multiple: true).process + def param_examples exp_params = :all, examples_hash + exp_params = self[:parameters].map(&:name) if exp_params == :all + self[:examples] = ExampleObj.new(examples_hash, exp_params, multiple: true).process end alias examples param_examples From 2e60868113ec8bf731ac5526e6980da17069c766 Mon Sep 17 00:00:00 2001 From: zhandao Date: Tue, 12 Feb 2019 16:31:25 +0800 Subject: [PATCH 15/15] D / rewrite done --- README.md | 434 +++++++++++++++---------------- README_zh.md | 10 +- documentation/parameter.md | 55 ---- examples/auto_gen_desc.rb | 29 --- lib/oas_objs/media_type_obj.rb | 4 +- lib/oas_objs/schema_obj.rb | 2 +- lib/open_api/dsl/api.rb | 12 +- lib/open_api/dsl/components.rb | 8 +- spec/oas_objs/schema_obj_spec.rb | 2 +- 9 files changed, 233 insertions(+), 323 deletions(-) delete mode 100644 documentation/parameter.md delete mode 100644 examples/auto_gen_desc.rb diff --git a/README.md b/README.md index be804b0..8b798c5 100644 --- a/README.md +++ b/README.md @@ -30,33 +30,36 @@ - [About OAS](#about-oas) (OpenAPI Specification) - [Installation](#installation) - [Configure](#configure) -- [Usage - DSL](#usage---dsl) - - [Basic DSL](#basic-dsl) - - [route_base](#1-route_base-required-if-youre-not-writing-dsl-in-controller) - - [doc_tag](#2-doc_tag-optional) - - [components](#3-components-optional) - - [api](#4-api-required) - - [api_dry](#5-api_dry-optional) - - [DSLs written inside `api` and `api_dry`'s block](#dsl-methods-inside-api-and-api_drys-block) - - [this_api_is_invalid!](#1-this_api_is_invalid-and-its-aliases) - - [desc](#2-desc-description-for-the-current-api) - - [param family methods](#3-param-family-methods-oas---parameter-object) - - [request_body family methods](#4-request_body-family-methods-oas---request-body-object) - - [response family methods](#5-response-family-methods-oas---response-object) - - [callback](#6-callback-oas---callback-object) - - [Authentication and Authorization](#7-authentication-and-authorization) - - [server](#8-overriding-global-servers-by-server) - - [DSLs written inside `components`'s block](#dsl-methods-inside-componentss-block-code-source) - - [Schema and Type](#schema-and-type) +- [DSL Usage](#dsl-usage) + - [a.Basic DSL](#basic-dsl) + - [a.1. route_base](#1-route_base-required-if-youre-not-writing-dsl-in-controller) + - [a.2. doc_tag](#2-doc_tag-optional) + - [a.3. components](#3-components-optional) + - [a.4. api](#4-api-required) + - [a.5. api_dry](#5-api_dry-optional) + - [b. DSLs written inside `api` and `api_dry`'s block](#dsls-written-inside-api-and-api_drys-block) + - [b.1. this_api_is_invalid!](#1-this_api_is_invalid-and-its-aliases) + - [b.2. desc](#2-desc-description-for-the-current-api) + - [b.3. param family methods](#3-param-family-methods-oas---parameter-object) + - [b.4. request_body family methods](#4-request_body-family-methods-oas---request-body-object) + - [b.5. response family methods](#5-response-family-methods-oas---response-object) + - [b.6. callback](#6-callback-oas---callback-object) + - [b.7. Authentication and Authorization](#7-authentication-and-authorization) + - [b.8. server](#8-overriding-global-servers-by-server) + - [b.9. dry](#9-dry) + - [c. DSLs written inside `components`'s block](#dsls-written-inside-componentss-block) + - [d. Schema and Type](#schema-and-type) + - [d.1. (Schema) Type](#schema-type) + - [d.2. Schema](#schema) + - [d.3. Combined Schema](#combined-schema) - [Run! - Generate JSON documentation file](#run---generate-json-documentation-file) - [Use Swagger UI(very beautiful web page) to show your Documentation](#use-swagger-uivery-beautiful-web-page-to-show-your-documentation) - [Tricks](#tricks) - [Write DSL somewhere else](#trick1---write-the-dsl-somewhere-else) - [Global DRYing](#trick2---global-drying) - - [Auto generate description](#trick3---auto-generate-description) + - [Auto generate description form enum](#trick3---auto-generate-description-form-enum) - [Skip or Use parameters define in `api_dry`](#trick4---skip-or-use-parameters-define-in-api_dry) - [Atuo Generate index/show Actions's Responses Based on DB Schema](#trick5---auto-generate-indexshow-actionss-response-types-based-on-db-schema) - - [Combined Schema (one_of / all_of / any_of / not)](#trick6---combined-schema-one_of--all_of--any_of--not) - [Troubleshooting](#troubleshooting) - [About `OpenApi.docs` and `OpenApi.routes_index`](#about-openapidocs-and-openapiroutes_index) @@ -356,131 +359,110 @@ #### (4) `request_body` family methods (OAS - [Request Body Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#requestBodyObject)) OpenAPI 3.0 uses the requestBody keyword to distinguish the payload from parameters. + + Notice: Each API has only ONE request body object. Each request body object can has multiple media types. + It means: call `request_body` multiple times, (schemas) will be deeply merged (let's call it [fusion](#fusion)) into a request body object. ``` - request_body - body_ref # for reuse component, - # it links sepcified RefObjs (by component keys) to current body. - body, body! # alias of request_body - form, form! # define a multipart/form-data body - data # define [a] property in the form-data body - file, file! # define a File media-type body + request_body # 1. normal usage + body_ref # 2. it links sepcified RefObjs (by component keys) to the body. + body, body! # 3. alias of request_body + form, form! # 4. to define a multipart/form-data request_body + data # 5. to define [a] property in the form-data request_body ``` Bang methods(!) means the specified media-type body is required. ```ruby + # Part 1 # ** Method Signature - request_body(required, media_type, data: { }, **options) + # a. `data` contains the attributes (params, or properties) and their schemas required by the request body + # b. `attr_name!` means it is required, without '!' means optional + # c. options: desc / exp_params and examples + # d. available `media_type` see: + # https://github.com/zhandao/zero-rails_openapi/blob/master/lib/oas_objs/media_type_obj.rb#L29 + request_body required, media_type, data: { }, desc: '', **options # ** Usage - # (1) `data` contains all the attributes required by this request body. - # (2) `param_name!` means it is required, otherwise without '!' means optional. - request_body :opt, :form, data: { id!: Integer, name: { type: String, desc: 'name' } }, desc: 'form-data' - + request_body :opt, :form, data: { + id!: Integer, + name: { type: String, desc: 'name' } + }, desc: 'a form-data' + # Part 2 # ** Method Signature - body_ref(component_key) + body_ref component_key # ** Usage - body_ref :UpdateDogeBody - + body_ref :UpdateUserBody + # Part 3 # ** Method Signature - body!(media_type, data: { }, **options) + body! media_type, data: { }, **options # ** Usage body :json - - # method implement - def form data:, **options + # Part 4 + # ** method Implement + def form data:, **options # or `form!` body :form, data: data, **options end # ** Usage form! data: { - name: String, - password: String, - password_confirmation: String + name!: String, + password: { type: String, pattern: /[0-9]{6,10}/ }, } - # advance usage - form data: { - :name! => { type: String, desc: 'user name' }, - :password! => { type: String, pattern: /[0-9]{6,10}/, desc: 'password' }, - # optional - :remarks => { type: String, desc: 'remarks' }, - }, exp_by: %i[ name password ], - examples: { # ↓ ↓ - :right_input => [ 'user1', '123456' ], - :wrong_input => [ 'user2', 'abc' ] - }, - desc: 'for creating a user' - - - # method implement - def data name, type = nil, schema_info = { } - schema_info[:type] = type if type.present? - form data: { name => schema_info } - end - # ** Usage: please look at the 4th point below - # about `file` - def file! media_type, data: { type: File }, **options - body! media_type, data: data, **options - end + # Part 5 + # ** Method Signature + data name, type = nil, schema = { } + # ** Usage + data :password!, String, pattern: /[0-9]{6,10}/ + ``` + + + How **fusion** works: + 1. Difference media types will be merged into `requestBody["content"]` + + ```ruby + form data: { } + body :json, data: { } + # will generate: "content": { "multipart/form-data": { }, "application/json": { } } ``` - 1. `media_type`: we provide some [mapping](lib/oas_objs/media_type_obj.rb) from symbols to real media-types. - 2. `schema_info`: as above (see param). - 3. `exp_by` and `examples`: for the above example, the following has the same effect: - ``` - examples: { - :right_input => { name: 'user1', password: '123456' }, - :wrong_input => { name: 'user2', password: 'abc' } - } - ``` - 4. *[IMPORTANT]* Each request bodies you declared will **FUSION** together. - (1) Media-Types will be merged to `requestBody["content"]` - ```ruby - form data: { }, desc: 'desc' - body :json, data: { }, desc: 'desc' - # will generate: "content": { "multipart/form-data": { }, "application/json": { } } - ``` - (2) The same media-types will fusion, but not merge: - (So that you can write `form` separately, and make `data` method possible.) - ```ruby - data :param_a!, String - data :param_b, Integer - # or same as: - form data: { :param_a! => String } - form data: { :param_b => Integer } - # will generate: { "param_a": { "type": "string" }, "param_b": { "type": "integer" } } (call it X) - # therefore: - # "content": { "multipart/form-data": - # { "schema": { "type": "object", "properties": { X }, "required": [ "param_a" ] } - # } - ``` + 2. The same media-types will be deeply merged together, including their `required` array: + (So that you can call `form` multiple times) + + ```ruby + data :param_a!, String + data :param_b, Integer + # or same as: + form data: { :param_a! => String } + form data: { :param_b => Integer } + # will generate: { "param_a": { "type": "string" }, "param_b": { "type": "integer" } } (call it X) + # therefore: + # "content": { "multipart/form-data": + # { "schema": { "type": "object", "properties": { X }, "required": [ "param_a" ] } + # } + ``` #### (5) `response` family methods (OAS - [Response Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#response-object)) - Define the responses for the API (action). + To define the response for APIs. ``` - response # aliases: `resp` and `error` - response_ref + response # 1. aliases: `resp` and `error` + response_ref # 2. it links sepcified RefObjs (by component keys) to the response. ``` ```ruby # ** Method Signature - response(code, desc, media_type = nil, data: { }, type: nil) + response code, desc, media_type = nil, data: { } # ** Usage - resp 200, 'json response', :json, data: { name: 'test' } - response 200, 'query result', :pdf, type: File - # same as: + resp 200, 'success', :json, data: { name: 'test' } response 200, 'query result', :pdf, data: File # ** Method Signature - response_ref(code_compkey_hash) + response_ref code_and_compkey_hash # ** Usage response_ref 700 => :AResp, 800 => :BResp ``` - **practice:** Automatically generate responses based on the agreed error class. [AutoGenDoc](examples/auto_gen_doc.rb#L63) - ### (6) Callback (OAS - [Callback Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#callback-object)) [About Callbacks](https://swagger.io/docs/specification/callbacks/) @@ -501,10 +483,10 @@ ... ``` - To define callbacks, you can use `callback` method: + `callback` method is for defining callbacks. ```ruby # ** Method Signature - callback(event_name, http_method, callback_url, &block) + callback event_name, http_method, callback_url, &block # ** Usage callback :myEvent, :post, 'localhost:3000/api/goods' do query :name, String @@ -528,7 +510,7 @@ ##### Define Security Scheme - Use these DSL in your initializer or `components` block: + Use these DSL **in your initializer config or `components` block**: ``` security_scheme # alias `auth_scheme` base_auth # will call `security_scheme` @@ -538,16 +520,16 @@ It's very simple to use (if you understand the above document) ```ruby # ** Method Signature - security_scheme(scheme_name, other_info) + security_scheme scheme_name, other_info # ** Usage security_scheme :BasicAuth, { type: 'http', scheme: 'basic', desc: 'basic auth' } # ** Method Signature - base_auth(scheme_name, other_info = { }) - bearer_auth(scheme_name, format = 'JWT', other_info = { }) - api_key(scheme_name, field:, in:, **other_info) + base_auth scheme_name, other_info = { } + bearer_auth scheme_name, format = 'JWT', other_info = { } + api_key scheme_name, field:, in:, **other_info # ** Usage - base_auth :BasicAuth, desc: 'basic auth' # the same effect as ↑↑↑ + base_auth :BasicAuth, desc: 'basic auth' # the same effect as above bearer_auth :Token api_key :ApiKeyAuth, field: 'X-API-Key', in: 'header', desc: 'pass api key to header' ``` @@ -555,112 +537,166 @@ ##### Apply Security ``` - # In initializer - # Global effectiveness - global_security_require - global_security # alias - global_auth # alias + # Use in initializer (Global effectiveness) + global_security_require # alias: global_security & global_auth - # In `api`'s block - # Only valid for the current controller - security_require - security # alias - auth # alias - need_auth # alias + # Use in `api`'s block (Only valid for the current controller) + security_require # alias security & auth_with ``` - Name is different, signature and usage is similar. ```ruby # ** Method Signature - security_require(scheme_name, scopes: [ ]) + security_require scheme_name, scopes: [ ] # ** Usage global_auth :Token - need_auth :Token - auth :OAuth, scopes: %w[ read_example admin ] + auth_with :OAuth, scopes: %w[ read_example admin ] ``` #### (8) Overriding Global Servers by `server` ```ruby # ** Method Signature - server(url, desc: '') + server url, desc: '' # ** Usage server 'http://localhost:3000', desc: 'local' ``` + +#### (9) `dry` -### DSLs written inside [components]()'s block ([code source](lib/open_api/dsl/components.rb)) + You have to call `dry` method inside `api` block, or pass `dry: true` as parameter of `api`, + for executing the dry blocks you declared before. Otherwise nothing will happen. + + ```ruby + # ** Method Signature + dry only: nil, skip: nil, none: false + + # ** Usage + # In general, just: + dry + # To skip some params declared in dry blocks: + dry skip: [:id, :name] + # `only` is used to specify which parameters will be taken from dry blocks + dry only: [:id] + ``` - (Here corresponds to OAS [Components Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#componentsObject)) +### DSLs written inside [components](#3-components-optional)'s block + [code source](lib/open_api/dsl/components.rb) (Here corresponds to OAS [Components Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#componentsObject)) Inside `components`'s block, - you can use the same DSL as [[DSL methods inside `api` and `api_dry`'s block]](#dsl-methods-inside-api-and-api_drys-block). - But there are two differences: + you can use the same DSLs as [DSLs written inside `api` and `api_dry`'s block](#dsls-written-inside-api-and-api_drys-block). + But notice there are two differences: - (1) Each method needs to pass one more parameter `component_key` - (in the first parameter position), - this will be used as the reference name for the component. + (1) Each method needs to pass one more parameter `component_key` (as the first parameter), + it will be used as the reference name for the component. ```ruby - query! :UidQuery, :uid, String - ``` - This writing is feasible but not recommended, - because component's key and parameter's name seem easy to confuse. - The recommended writing is: - - ```ruby - query! :UidQuery => [:uid, String] + query! :UidQuery, :uid, String, desc: 'it is a component' + # ↑ ↑ + # component_key param_name + + # You can also use "arrow writing", it may be easier to understand + query! :UidQuery => [:uid, String, desc: ''] ``` (2) You can use `schema` to define a Schema Component. ```ruby # ** Method Signature - schema(component_key, type = nil, **schema_info) + schema component_key, type = nil, **schema # ** Usage - schema :Dog => [ String, desc: 'dogee' ] # <= schema_type is `String` + schema :Dog => [ String, desc: 'doge' ] # advance usage schema :Dog => [ { - id!: Integer, - name: { type: String, must_be: 'name', desc: 'name' } - }, # <= this hash is schema type[1] - dft: { id: 1, name: 'pet' }, - desc: 'dogee' + id!: Integer, + name: { type: String, desc: 'doge name' } + }, default: { id: 1, name: 'pet' } ] - # or (unrecommended) - schema :Dog, { id!: Integer, name: String }, dft: { id: 1, name: 'pet' }, desc: 'dogee' + # or flatten writing + schema :Dog, { id!: Integer, name: String }, default: { id: 1, name: 'pet' } # # pass a ActiveRecord class constant as `component_key`, - # it will automatically read the db schema to generate the component. + # it will automatically load schema from database and then generate the component. schema User # easy! And the component_key will be :User ``` - [1] see: [Type](#schema-and-type) + To enable load schema from database, you must set [model base](#part-1-configs-of-this-gem) correctly. ### Schema and Type -```ruby -# When schema_type is a Object - # (describe by hash, key means prop's name, value means prop's schema_type) - query :good, { name: String, price: Float, spec: { size: String, weight: Integer } }, desc: 'good info' - # Or you can use `type:` to sign the schema_type, maybe this is clearer for describing object - query :good, type: { name: String, price: Float, spec: { size: String, weight: Integer } }, desc: 'good info' - # - query :good_name, type: String # It's also OK, but some superfluous - query :good_name, String # recommended - # About Combined Schema (`one_of` ..), see the link below. -``` + schema and type -- contain each other + +#### (Schema) Type + + Support all [data types](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#dataTypes) in OAS. + + 1. String / 'binary' / 'base64' / 'uri' + 2. Integer / Long / 'int32' / 'int64' / Float / Double + 3. File (it will be converted to `{ type: 'string', format: Config.file_format }`) + 4. Date / DateTime + 5. 'boolean' + 6. Array / Array[\] (like: `Array[String]`, `[String]`) + 7. Nested Array (like: `[[[Integer]]]`) + 8. Object / Hash (Object with properties) + Example: `{ id!: Integer, name: String }` + 9. Nested Hash: `{ id!: Integer, name: { first: String, last: String } }` + 10. Nested Array[Nested Hash]: `[[{ id!: Integer, name: { first: String, last: String } }]]` + 11. Symbol Value: it will generate a Schema Reference Object link to the component correspond to ComponentKey, like: :IdPath, :NameQuery + + **Notice** that Symbol is not allowed in all cases except 11. + +#### Schema + + [OAS Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#schemaObject) + and [source code](https://github.com/zhandao/zero-rails_openapi/blob/master/lib/oas_objs/schema_obj.rb) + + Schema (Hash) is for defining properties of parameters, responses and request bodies. + + The following property keys will be process slightly: + 1. desc / description / d + 2. enum / in / values / allowable_values + should be Array or Range + 3. range: allow value in this continuous range + should be Range or like `{ gt: 0, le: 5 }` + 4. length / size / lth + should be an Integer, Integer Array, Integer Range, + or the following format Symbol: `:gt_`, `:ge_`, `:lt_`, `:le_` (:ge_5 means "greater than or equal 5"; :lt_9 means "lower than 9") + 5. pattern / regxp + 6. additional_properties / add_prop / values_type + 7. example + 8. examples + 9. format + 10. default: default value + 11. type + + The other keys will be directly merged. Such as: + 1. `title: 'Property Title'` + 2. `myCustomKey: 'Value'` + +#### Combined Schema + + Very easy to use: + ```ruby + query :combination, one_of: [ :GoodSchema, String, { type: Integer, desc: 'integer input' } ] + + form data: { + :combination_in_form => { any_of: [ Integer, String ] } + } + + schema :PetSchema => [ not: [ Integer, Boolean ] ] + ``` + + OAS: [link1](https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/), + [link2](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject) ## Run! - Generate JSON Documentation File Use `OpenApi.write_docs`: ```ruby - # initializer - OpenApi.write_docs generate_files: !Rails.env.production? - - # or run directly in console - OpenApi.write_docs # will generate json doc files + OpenApi.write_docs# if: !Rails.env.production? ``` + `if` option is used to control whether a JSON document is generated or not. + Then the JSON files will be written to the directories you set. (Each API a file.) ## Use Swagger UI(very beautiful web page) to show your Documentation @@ -718,26 +754,15 @@ You can use `sort` to specify the order of parameters. -### Trick3 - Auto Generate Description +### Trick3 - Auto Generate Description from Enum + Just use `enum!`: ```ruby - desc 'api desc', - search_type!: 'search field, allows:
' - query :search_type, String, enum: %w[name creator category price] - - # or - - query :search_type, String, desc!: 'search field, allows:
', - enum: %w[name creator category price] + query :search_type, String, desc: 'search field, allows:
', enum!: %w[name creator category price] + # it will generate: + "search field, allows:
1/ name
2/ creator,
3/ category
4/ price
" ``` - - Notice `!` use (`search_type!`, `desc!`), it tells ZRO to append - information that analyzed from definitions (enum, must_be ..) to description automatically. - - Any one of above will generate: - > search field, allows:
1/ name
2/ creator,
3/ category
4/ price
- - You can also use Hash to define `enum`: + Or Hash `enum!`: ```ruby query :view, String, desc: 'allows values
', enum!: { 'all goods (default)': :all, @@ -747,37 +772,6 @@ 'cheap goods': :borrow, } ``` - Read this [file](examples/auto_gen_desc.rb) to learn more. - -### Trick4 - Skip or Use parameters define in api_dry - - Pass `skip: []` and `use: []` to `api` like following code: - ```ruby - api :index, 'desc', skip: [ :Token ] - ``` - - Look at this [file](examples/goods_doc.rb) to learn more. - -### Trick5 - Auto Generate index/show Actions's Response-Types Based on DB Schema - - Use method `load_schema` in `api_dry`. - - See this [file](examples/auto_gen_doc.rb#L51) for uasge information. - -### Trick6 - Combined Schema (one_of / all_of / any_of / not) - - ```ruby - query :combination, one_of: [ :GoodSchema, String, { type: Integer, desc: 'integer input' } ] - - form data: { - :combination_in_form => { any_of: [ Integer, String ] } - } - - schema :PetSchema => [ not: [ Integer, Boolean ] ] - ``` - - OAS: [link1](https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/), - [link2](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject) ## Troubleshooting diff --git a/README_zh.md b/README_zh.md index 4b15cc3..d9114df 100644 --- a/README_zh.md +++ b/README_zh.md @@ -359,11 +359,11 @@ # method signature - # `exp_by` (select_example_by): choose the example fields. - examples(exp_by = :all, examples_hash) + # `exp_params` (select_example_by): choose the example fields. + examples(exp_params = :all, examples_hash) # usage # it defines 2 examples by using parameter :id and :name - # if pass :all to `exp_by`, keys will be all the parameter's names. + # if pass :all to `exp_params`, keys will be all the parameter's names. examples [:id, :name], { :right_input => [ 1, 'user'], # == { id: 1, name: 'user' } :wrong_input => [ -1, '' ] @@ -425,7 +425,7 @@ :password! => { type: String, pattern: /[0-9]{6,10}/, desc: 'password' }, # optional :remarks => { type: String, desc: 'remarks' }, - }, exp_by: %i[ name password ], + }, exp_params: %i[ name password ], examples: { # ↓ ↓ :right_input => [ 'user1', '123456' ], :wrong_input => [ 'user2', 'abc' ] @@ -448,7 +448,7 @@ 1. `media_type`: we provide some [mapping](lib/oas_objs/media_type_obj.rb) from symbols to real media-types. 2. `schema_info`: as above (see param). - 3. `exp_by` and `examples`: for the above example, the following has the same effect: + 3. `exp_params` and `examples`: for the above example, the following has the same effect: ``` examples: { :right_input => { name: 'user1', password: '123456' }, diff --git a/documentation/parameter.md b/documentation/parameter.md deleted file mode 100644 index 024df04..0000000 --- a/documentation/parameter.md +++ /dev/null @@ -1,55 +0,0 @@ -### More Explanation for `param` and `schema_info` - -#### type (schema_type) - -Support all [data types](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#dataTypes) defined in OAS. - -In addition, you can use `format` in schema_info to define in fine detail the data type being used, like: -int32, float, date ... -All the types you can use as following: - - **String, 'binary', 'base64'** - - **Integer, Long, 'int32', 'int64', Float, Double** - - **File** (it will be converted to `{ type: 'string', format: Config.file_format }`) - - **Date, DateTime** - - **Boolean** - - **Array**: `Array[String]` or `[String]` - - Nested Array: `[[[Integer]]]` - - **Object**: you can use just `Object`, or use a hash to declare its properties `{ id!: Integer, name: String }` - (`!` bang key means it is required). - - Nested Object: `{ id!: Integer, name: { first: String, last: String } }` - - Nested Array and Object: `[[{ id!: Integer, name: { first: String, last: String } }]]` - - **:ComponentKey**: pass **Symbol** value to type will generate a Schema Reference Object link - to the component correspond to ComponentKey, like: :IdPath, :NameQuery - - You can use `Object.const_set()` to define a constant that does not exist, but note that - the value you set could not be a Symbol (it will be explained as a Ref Object), should be a String. - -#### Schema Hash - -The [[schema]](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#schemaObject) defining the type used for the parameter. -schema_info(optional) will be used to generate Schema Object inside Parameter Object. -[source code](https://github.com/zhandao/zero-rails_openapi/blob/master/lib/oas_objs/schema_obj.rb) -You can set the schema by following keys (all are optional), the words in parentheses are available aliases of the keys: - - **enum (values, allowable_values)** - Must be Array or Range(will be converted to Array) - - **must_be (value, allowable_value)** - Single value, could be a String, Array ... - - **range (number_range)** - Allow value in this continuous range. Set this field like this: `{ gt: 0, le: 5 }` - - **length (lth)** - Must be an Integer, Integer Array, Integer Range, or the following format Symbol: `:gt_`, `:ge_`, `:lt_`, `:le_`, examples: :ge_5 means "greater than or equal 5"; :lt_9 means "lower than 9". - - **format (fmt)** - - **is (is_a)** - 1. It's not in OAS, just an addition field for better express.You can see it as `format`, but in fact they are quite different. - 2. Look at this example: the `format` is set to "int32", but you also want to express that this - schema is an "id" format —— this cannot be expressed in the current OAS version. - 3. So I suggest that the value of `format` should related to data type, `is` should be an entity. - 4. ZRO defaults to identify whether `is` patterns matched the name, then automatically generate `is`. - for example the parameter name "user_email" will generate "is: email". Default `is` options are: - [email phone password uuid uri url time date], to overwrite it you can set it in initializer `c.is_options = %w[]`. - 5. If type is Object, for describing each property's schema, the only way is use ref type, like: `{ id: :Id, name: :Name }` - - **pattern (regexp, pr, reg)** - Regexp or Time Format - - **default (dft, default_value)** - - **as** # TODO - - **example & examples** # TODO diff --git a/examples/auto_gen_desc.rb b/examples/auto_gen_desc.rb deleted file mode 100644 index 38f6626..0000000 --- a/examples/auto_gen_desc.rb +++ /dev/null @@ -1,29 +0,0 @@ -class V1::GoodsDoc < BaseDoc - api :index, 'GET list of goods.' do - desc 'listing goods', - view!: 'search view, allows:
', - # '1/ all goods (default):all
' \ - # '2/ only online:online
' \ - # '3/ only offline:offline
' \ - # '4/ expensive goods:expensive
' \ - # '5/ cheap goods:cheap
', - search_type!: 'search field, allows:
' - # '1/ name
2/ creator,
3/ category
4/ price
' - - # Instead of: - # query :view, String, enum: %w[ all online offline expensive cheap ] - query :view, String, enum!: { - 'all goods (default)': :all, - 'only online': :online, - 'only offline': :offline, - 'expensive goods': :expensive, - 'cheap goods': :cheap - } - query :search_type, String, enum: %w[ name creator category price ] - # Same as: - # query :search_type, String, desc!: 'search field, allows:
', - # enum: %w[ name creator category price ] - - # TODO: Support `desc: '', auto_desc: true or %i[ enum must_be ]` - end -end diff --git a/lib/oas_objs/media_type_obj.rb b/lib/oas_objs/media_type_obj.rb index 135ba20..b5ffe1d 100644 --- a/lib/oas_objs/media_type_obj.rb +++ b/lib/oas_objs/media_type_obj.rb @@ -11,8 +11,8 @@ class MediaTypeObj < Hash def initialize(media_type, hash) examples_hash = hash.delete(:examples) - exp_by = schema_type.keys if (exp_by = hash.delete(:exp_by)) == :all - self.examples = ExampleObj.new(examples_hash, exp_by, multiple: true) if examples_hash.present? + exp_params = schema_type.keys if (exp_params = hash.delete(:exp_params)) == :all + self.examples = ExampleObj.new(examples_hash, exp_params, multiple: true) if examples_hash.present? self.media_type = media_type_mapping(media_type) self.schema = SchemaObj.new(hash.values_at(:type, :data).compact.first, hash.except(:type, :data)) diff --git a/lib/oas_objs/schema_obj.rb b/lib/oas_objs/schema_obj.rb index 28a83ca..5dac009 100644 --- a/lib/oas_objs/schema_obj.rb +++ b/lib/oas_objs/schema_obj.rb @@ -105,7 +105,7 @@ def other { pattern: _pattern.is_a?(String) ? _pattern : _pattern&.inspect&.delete('/'), example: ExampleObj.new(self[:example]).process, - examples: ExampleObj.new(self[:examples], self[:exp_by], multiple: true).process + examples: ExampleObj.new(self[:examples], self[:exp_params], multiple: true).process } end diff --git a/lib/open_api/dsl/api.rb b/lib/open_api/dsl/api.rb index d2d6b14..c9de713 100644 --- a/lib/open_api/dsl/api.rb +++ b/lib/open_api/dsl/api.rb @@ -67,7 +67,7 @@ def param_ref component_key, *keys self[:parameters] += [component_key, *keys].map { |key| RefObj.new(:parameter, key) } end - # options: `exp_by` and `examples` + # options: `exp_params` and `examples` def request_body required, media_type, data: { }, desc: '', **options (self[:requestBody] ||= RequestBodyObj.new(required, desc)).absorb(media_type, { data: data , **options }) end @@ -90,13 +90,13 @@ def form! data:, **options body! :form, data: data, **options end - def data name, type = nil, schema_info = { } - schema_info[:type] = type if type.present? - form data: { name => schema_info } + def data name, type = nil, schema = { } + schema[:type] = type if type.present? + form data: { name => schema } end - def response code, desc, media_type = nil, data: { }, type: nil - (self[:responses][code] ||= ResponseObj.new(desc)).absorb(desc, media_type, { data: type || data }) + def response code, desc, media_type = nil, data: { } + (self[:responses][code] ||= ResponseObj.new(desc)).absorb(desc, media_type, { data: data }) end alias_method :resp, :response diff --git a/lib/open_api/dsl/components.rb b/lib/open_api/dsl/components.rb index c56b256..2350188 100644 --- a/lib/open_api/dsl/components.rb +++ b/lib/open_api/dsl/components.rb @@ -11,8 +11,8 @@ def initialize merge!(%i[ schemas responses parameters examples requestBodies securitySchemes ].map { |k| [ k, { } ] }.to_h) end - def schema component_key, type = nil, **schema_info - return unless schema = process_schema_input(type, schema_info, component_key, model: component_key) + def schema component_key, type = nil, **schema + return unless schema = process_schema_input(type, schema, component_key, model: component_key) self[:schemas][component_key.to_s.to_sym] = schema.process end @@ -49,8 +49,8 @@ def request_body component_key, required, media_type, data: { }, desc: '', **opt arrow_enable :body arrow_enable :body! - def response component_key, desc, media_type = nil, data: { }, type: nil - (self[:responses][component_key] ||= ResponseObj.new(desc)).absorb(desc, media_type, { data: type || data }) + def response component_key, desc, media_type = nil, data: { } + (self[:responses][component_key] ||= ResponseObj.new(desc)).absorb(desc, media_type, { data: data }) end alias_method :resp, :response diff --git a/spec/oas_objs/schema_obj_spec.rb b/spec/oas_objs/schema_obj_spec.rb index f210fb2..cc4ef5e 100644 --- a/spec/oas_objs/schema_obj_spec.rb +++ b/spec/oas_objs/schema_obj_spec.rb @@ -188,7 +188,7 @@ end describe ':examples' do - api -> { query :info, { name: String, age: Integer }, examples: { input1: ['a', 1], input2: ['b, 2'] }, exp_by: %i[ name age ] }, + api -> { query :info, { name: String, age: Integer }, examples: { input1: ['a', 1], input2: ['b, 2'] }, exp_params: %i[ name age ] }, has_key!: :examples focus_on :examples expect_its 0, eq: { input1: { value: { name: 'a', age: 1 } } }