railstack/go-on-rails

View on GitHub
lib/generators/gor/converter.rb

Summary

Maintainability
B
4 hrs
Test Coverage
module GoOnRails
  class Convertor
    TYPE_MAP = {
      "string"     => "string",
      "text"       => "string",
      "boolean"    => "bool",
      "integer(1)" => "int8",
      "integer(2)" => "int16",
      "integer(3)" => "int32",
      "integer(4)" => "int64",
      "integer(8)" => "int64",
      "float"      => "float64",
      "datetime"   => "time.Time",
      "date"       => "time.Time",
      "inet"       => "string"
    }.freeze

    # COALESCE datetime typed field for different databases
    # sqlite3 is dependent on the driver: https://github.com/railstack/go-sqlite3, details see: https://github.com/mattn/go-sqlite3/pull/468
    DATETIME_COALESCE_MAP = {
      "sqlite3"  => "CAST(COALESCE(%s, '0001-01-01T00:00:00Z') as text) AS %s",
      "mysql"    => "COALESCE(%s, CONVERT_TZ('0001-01-01 00:00:00','+00:00','UTC')) AS %s",
      "postgres" => "COALESCE(%s, (TIMESTAMP WITH TIME ZONE '0001-01-01 00:00:00+00') AT TIME ZONE 'UTC') AS %s"
    }.freeze

    # types need special treatment in the nullable_map() method
    SPECIAL_COALESCE_TYPES = %w[inet].freeze

    def initialize(klass, models, database)
      @klass = klass
      @models = models
      @database = database
    end
    attr_accessor :klass, :models, :database

    def convert
      get_schema_info
    end

    private

    def get_schema_info
      struct_info = {
        col_names: [],
        timestamp_cols: [],
        has_datetime_type: false,
        struct_body: "",
      }

      validation = GoOnRails::Validator.new(self.klass)
      # store fields by if nullable
      fields = { yes: [], no: [] }

      self.klass.columns.each_with_index do |col, index|
        tags = []

        # add struct tag
        tags << struct_tag(col, validation)

        col_type = col.type.to_s
        struct_info[:has_datetime_type] = true if %w(datetime time).include? col_type
        if col_type == "datetime" and %w(created_at updated_at).include? col.name
          struct_info[:timestamp_cols] << col.name
        end

        case col_type
        when "integer"
          type = TYPE_MAP["integer(#{col.limit})"] || "int64"
          type = "u#{type}" if col.sql_type.match("unsigned").present?
        else
          type = TYPE_MAP[col_type] || "string"
        end

        # check the fields if nullable
        if col.null == true
          if SPECIAL_COALESCE_TYPES.include?(col_type)
            fields[:yes] << [col.name, col_type]
          else
            fields[:yes] << [col.name, type]
          end
        else
          fields[:no] << col.name
        end

        struct_info[:col_names] << col.name unless col.name == "id"
        struct_info[:struct_body] << sprintf("%s %s `%s`\n", col.name.camelize, type, tags.join(" "))
      end

      assoc = get_associations
      struct_info[:struct_body] << assoc[:struct_body]
      struct_info[:assoc_info] = assoc[:assoc_info]
      struct_info[:has_assoc_dependent] = assoc[:has_assoc_dependent]
      struct_info[:select_fields] = nullable_select_str(fields)

      return struct_info
    end

    def get_struct_name
      self.klass.table_name.camelize
    end

    def get_associations
      builder = GoOnRails::Association.new(self.klass, self.models)
      builder.get_schema_info
    end

    def struct_tag(col, validation)
      valid_tags = validation.build_validator_tag(col)
      "json:\"#{col.name},omitempty\" db:\"#{col.name}\" #{valid_tags}"
    end

    def nullable_select_str(fields)
      fields[:yes].map do |f|
        sprintf(nullable_map(f[1]), "#{self.klass.table_name}.#{f[0]}", f[0])
      end.concat(
        fields[:no].map do |f|
          sprintf("%s.%s", self.klass.table_name, f)
        end
      ).join(", ")
    end

    def nullable_map(type)
      case type
      when "string"
        "COALESCE(%s, '') AS %s"
      when "int8", "int16", "int32", "int64"
        "COALESCE(%s, 0) AS %s"
      when "float64"
        "COALESCE(%s, 0.0) AS %s"
      when "bool"
        "COALESCE(%s, FALSE) AS %s"
      when "time.Time"
        DATETIME_COALESCE_MAP[self.database]
      when "inet"
        "COALESCE(%s, '0.0.0.0') AS %s"
      else
        # FIXME: here just return the column name, may skip some nullable field and cause an error in a query
        "%s"
      end
    end
  end
end

require_relative 'association'
require_relative 'validator'