zhandao/zero-rails_openapi

View on GitHub
lib/oas_objs/schema_obj.rb

Summary

Maintainability
A
35 mins
Test Coverage
# frozen_string_literal: true

require 'oas_objs/helpers'
require 'oas_objs/ref_obj'
require 'oas_objs/example_obj'
require 'oas_objs/schema_obj_helpers'

module OpenApi
  module DSL
    # https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#schemaObject
    class SchemaObj < Hash
      include SchemaObjHelpers
      include Helpers

      attr_accessor :processed, :type

      def initialize(type = nil, 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)
        reducing(additional_properties, enum, length, range, format, other, desc)
      end

      def desc
        return unless (result = @bang_enum.present? ? auto_generate_desc : _desc)
        { description: result }
      end

      def recg_schema_type(t = self.type)
        t = t.class.in?([Hash, Array, Symbol]) ? t : t.to_s.downcase
        case t
        when Hash then hash_type(t)
        when Array then array_type(t)
        when Symbol then RefObj.new(:schema, t).process
        when *%w[ float double int32 int64 ]
          { type: t['int'] ? 'integer' : 'number', format: t }
        when *%w[ date binary base64 uri ]
          { type: 'string', format: t }
        when 'datetime'
          { type: 'string', format: 'date-time' }
        when 'file' # TODO
          { type: 'string', format: Config.file_format }
        when /{=>.*}/
          self[:values_type] = t[3..-2]
          { type: 'object' }
        else # other string
          { type: t }
        end
      end

      def additional_properties
        return if processed[:type] != 'object'
        default = Config.additional_properties_default_value_of_type_object
        return { additionalProperties: default } if _addProp.nil? && !default.nil?

        value = _addProp.in?([true, false]) ? _addProp : SchemaObj.new(_addProp, { }).process
        { additionalProperties: value }
      end

      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 _length.is_a?(Array)
          min, max = [ _length.first&.to_i, _length.last&.to_i ]
        else
          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]) or return
        {
                     minimum: range[:gt] || range[:ge],
            exclusiveMinimum: range[:gt].present? || nil,
                     maximum: range[:lt] || range[:le],
            exclusiveMaximum: range[:lt].present? || nil
        }
      end

      def format
        { format: self[:format] || self[:is_a] } unless processed[:format]
      end

      def other
        {
            pattern:  _pattern.is_a?(String) ? _pattern : _pattern&.inspect&.delete('/'),
            example:  ExampleObj.new(self[:example]).process,
            examples: ExampleObj.new(self[:examples], self[:exp_params], multiple: true).process
        }
      end


      SELF_MAPPING = {
          _enum:    %i[ enum in  values  allowable_values ],
          _length:  %i[ length   lth     size             ],
          _pattern: %i[ pattern  regexp                   ],
          _desc:    %i[ desc     description  d           ],
          _addProp: %i[ additional_properties add_prop values_type ],
      }.each do |key, aliases|
        define_method(key)       { self[key] ||= self.values_at(*aliases).compact.first }
        define_method("#{key}=") { |value| self[key] = value }
      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
    }
  }
}