require 'oas_objs/helpers' require 'open_api/config' require 'oas_objs/ref_obj' require 'oas_objs/example_obj' module OpenApi module DSL # https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#schemaObject class SchemaObj < Hash include Helpers attr_accessor :processed, :type def initialize(type, schema_hash) self.processed = { } # [Note] Here is no limit to type, even if the input isn't up to OAS, # like: double, float, hash. # My consideration is, OAS can't express some cases like: # `total_price` should be double, is_a `price`, and match /^.*\..*$/ # However, user can decide how to write -- # `type: number, format: double`, or `type: double` self.type = type merge! schema_hash end def process_for(param_name = nil, options = { desc_inside: false }) return processed if @preprocessed processed.merge! processed_type reducx processed_enum_and_length, processed_range, processed_is_and_format(param_name), { pattern: _pattern&.inspect&.delete('/'), default: _default.nil? ? nil : '_default', examples: self[:examples].present? ? ExampleObj.new(self[:examples], self[:exp_by]).process : nil }, { as: _as, permit: _permit, not_permit: _npermit, req_if: _req_if, opt_if: _opt_if } then_merge! processed[:default] = _default unless _default.nil? reducx(processed_desc options).then_merge! end alias process process_for def preprocess_with_desc desc, param_name = nil self.__desc = desc process_for param_name @preprocessed = true __desc end def processed_desc(options) result = __desc ? self.__desc = process_desc : _desc options[:desc_inside] ? { description: result } : nil end # TODO: more info # TODO: desc configure def process_desc if processed[:enum].present? if @enum_info.present? @enum_info.each_with_index do |(info, value), index| __desc.concat "
#{index + 1}/ #{info}: #{value}" end else processed[:enum].each_with_index do |value, index| __desc.concat "
#{index + 1}/ #{value}" end end end __desc end def processed_type(type = self.type) t = type.class.in?([Hash, Array, Symbol]) ? type : type.to_s.downcase if t.is_a? Hash # For supporting this: # form 'desc', data: { # id!: { type: Integer, enum: 0..5, desc: 'user id' } # } if t.key?(:type) SchemaObj.new(t[:type], t).process_for(@prop_name, desc_inside: true) # For supporting combined schema in nested schema. elsif (t.keys & %i[ one_of any_of all_of not ]).present? CombinedSchema.new(t).process_for(@prop_name, desc_inside: true) else recursive_obj_type t end elsif t.is_a? Array recursive_array_type t elsif t.is_a? Symbol RefObj.new(:schema, t).process elsif t.in? %w[float double int32 int64] # to README: 这些值应该传 string 进来, symbol 只允许 $ref { type: t.match?('int') ? 'integer' : 'number', format: t} elsif t.in? %w[binary base64] { type: 'string', format: t} elsif t.eql? 'file' { type: 'string', format: Config.dft_file_format } elsif t.eql? 'datetime' { type: 'string', format: 'date-time' } else # other string { type: t } end end def recursive_obj_type(t) # DSL use { prop_name: prop_type } to represent object structure return processed_type(t) if !t.is_a?(Hash) || (t.keys & %i[ type one_of any_of all_of not ]).present? _schema = { type: 'object', properties: { }, required: [ ] } t.each do |prop_name, prop_type| @prop_name = prop_name _schema[:required] << "#{prop_name}".delete('!') if prop_name['!'] _schema[:properties]["#{prop_name}".delete('!').to_sym] = recursive_obj_type prop_type end _schema.keep_if(&value_present) end def recursive_array_type(t) if t.is_a? Array { type: 'array', # TODO: [[String], [Integer]] <= One Of? Object?(0=>[S], 1=>[I]) items: recursive_array_type(t.first) } else processed_type t end end def processed_enum_and_length # Support this writing for auto generating desc from enum. # enum: { # 'all_data': :all, # 'one_page': :one # } if _enum.is_a? Hash @enum_info = _enum self._enum = _enum.values end %i[_enum _length].each do |key| value = self.send(key) self[key] = value.to_a if value.present? && value.is_a?(Range) end # generate_enums_by_enum_array values = _enum || _value self._enum = Array(values) if truly_present?(values) # generate length range fields by _lth array lth = _length || [ ] max = lth.is_a?(Array) ? lth.first : ("#{lth}".match?('ge') ? "#{lth}".split('_').last.to_i : nil) min = lth.is_a?(Array) ? lth.last : ("#{lth}".match?('le') ? "#{lth}".split('_').last.to_i : nil) if processed[:type] == 'array' { minItems: max, maxItems: min } else { minLength: max, maxLength: min } end.merge!(enum: _enum).keep_if &value_present end def processed_range range = _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 processed_is_and_format(name) return if name.nil? recognize_is_options_in name { }.tap do |it| # `format` that generated in process_type() may be overwrote here. it.merge!(format: _format || _is) if processed[:format].blank? || _format.present? it.merge! is: _is end end def recognize_is_options_in(name) # identify whether `is` patterns matched the name, if so, generate `is`. Config.is_options.each do |pattern| self._is = pattern or break if name.match?(/#{pattern}/) end if _is.nil? end { # SELF_MAPPING _enum: %i[ enum values allowable_values ], _value: %i[ must_be value allowable_value ], _range: %i[ range number_range ], _length: %i[ length lth size ], _is: %i[ is_a is ], # NOT OAS Spec, see documentation/parameter.md _format: %i[ format fmt ], _pattern: %i[ pattern regexp pt reg ], _default: %i[ default dft default_value ], _desc: %i[ desc description d ], __desc: %i[ desc! description! d! ], _as: %i[ as to for map mapping ], # NOT OAS Spec, it's for zero-params_processor _permit: %i[ permit pmt ], # NOT OAS Spec, it's for zero-params_processor _npermit: %i[ npmt not_permit unpermit ], # NOT OAS Spec, it's for zero-params_processor _req_if: %i[ req_if req_when ], # NOT OAS Spec, it's for zero-params_processor _opt_if: %i[ opt_if opt_when ], # NOT OAS Spec, it's for zero-params_processor }.each do |key, aliases| define_method key do aliases.each do |alias_name| break if self[key] == false self[key] ||= self[alias_name] end if self[key].nil? self[key] end define_method "#{key}=" do |value| self[key] = value end end end end end __END__ Schema Object Examples Primitive Sample { "type": "string", "format": "email", "examples": { "exp1": { "value": 'val' } } } Simple Model { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string" }, "address": { "$ref": "#/components/schemas/Address" }, "age": { "type": "integer", "format": "int32", "minimum": 0 } } }