lib/oas_rails/json_schema_generator.rb



require 'json'

module OasRails
  # The JsonSchemaGenerator module provides methods to transform string representations
  # of data types into JSON schema formats.
  module JsonSchemaGenerator
    # Processes a string representing a data type and converts it into a JSON schema.
    #
    # @param str [String] The string representation of a data type.
    # @return [Hash] A hash containing the required flag and the JSON schema.
    def self.process_string(str)
      parsed = parse_type(str)
      {
        required: parsed[:required],
        json_schema: to_json_schema(parsed)
      }
    end

    # Parses a string representing a data type and determines its JSON schema type.
    #
    # @param str [String] The string representation of a data type.
    # @return [Hash] A hash containing the type, whether it's required, and any additional properties.
    def self.parse_type(str)
      required = str.start_with?('!')
      type = str.sub(/^!/, '').strip

      case type
      when /^Hash\{(.+)\}$/i
        { type: :object, required:, properties: parse_object_properties(::Regexp.last_match(1)) }
      when /^Array<(.+)>$/i
        { type: :array, required:, items: parse_type(::Regexp.last_match(1)) }
      else
        { type: type.downcase.to_sym, required: }
      end
    end

    # Parses the properties of an object type from a string.
    #
    # @param str [String] The string representation of the object's properties.
    # @return [Hash] A hash where keys are property names and values are their JSON schema types.
    def self.parse_object_properties(str)
      properties = {}
      stack = []
      current_key = ''
      current_value = ''

      str.each_char.with_index do |char, index|
        case char
        when '{', '<'
          stack.push(char)
          current_value += char
        when '}', '>'
          stack.pop
          current_value += char
        when ','
          if stack.empty?
            properties[current_key.strip.to_sym] = parse_type(current_value.strip)
            current_key = ''
            current_value = ''
          else
            current_value += char
          end
        when ':'
          if stack.empty?
            current_key = current_value
            current_value = ''
          else
            current_value += char
          end
        else
          current_value += char
        end

        properties[current_key.strip.to_sym] = parse_type(current_value.strip) if index == str.length - 1 && !current_key.empty?
      end

      properties
    end

    # Converts a parsed data type into a JSON schema format.
    #
    # @param parsed [Hash] The parsed data type hash.
    # @return [Hash] The JSON schema representation of the parsed data type.
    def self.to_json_schema(parsed)
      case parsed[:type]
      when :object
        schema = {
          type: 'object',
          properties: {}
        }
        required_props = []
        parsed[:properties].each do |key, value|
          schema[:properties][key] = to_json_schema(value)
          required_props << key.to_s if value[:required]
        end
        schema[:required] = required_props unless required_props.empty?
        schema
      when :array
        {
          type: 'array',
          items: to_json_schema(parsed[:items])
        }
      else
        ruby_type_to_json_schema_type(parsed[:type])
      end
    end

    # Converts a Ruby data type into its corresponding JSON schema type.
    #
    # @param type [Symbol, String] The Ruby data type.
    # @return [Hash, String] The JSON schema type or a hash with additional format information.
    def self.ruby_type_to_json_schema_type(type)
      case type.to_s.downcase
      when 'string' then { type: "string" }
      when 'integer' then { type: "integer" }
      when 'float' then { type: "float" }
      when 'boolean' then { type: "boolean" }
      when 'array' then { type: "array" }
      when 'hash' then { type: "hash" }
      when 'nil' then { type: "null" }
      when 'date' then { type: "string", format: "date" }
      when 'datetime' then { type: "string", format: "date-time" }
      else type.to_s.downcase
      end
    end
  end
end