Move ActiveRecord::Type to ActiveModel
The first step of bringing typecasting to ActiveModel
This commit is contained in:
parent
b223d729d8
commit
9cc8c6f373
59
activemodel/lib/active_model/type.rb
Normal file
59
activemodel/lib/active_model/type.rb
Normal file
@ -0,0 +1,59 @@
|
||||
require 'active_model/type/helpers'
|
||||
require 'active_model/type/value'
|
||||
|
||||
require 'active_model/type/big_integer'
|
||||
require 'active_model/type/binary'
|
||||
require 'active_model/type/boolean'
|
||||
require 'active_model/type/date'
|
||||
require 'active_model/type/date_time'
|
||||
require 'active_model/type/decimal'
|
||||
require 'active_model/type/decimal_without_scale'
|
||||
require 'active_model/type/float'
|
||||
require 'active_model/type/integer'
|
||||
require 'active_model/type/string'
|
||||
require 'active_model/type/text'
|
||||
require 'active_model/type/time'
|
||||
require 'active_model/type/unsigned_integer'
|
||||
|
||||
require 'active_model/type/registry'
|
||||
require 'active_model/type/type_map'
|
||||
require 'active_model/type/hash_lookup_type_map'
|
||||
|
||||
module ActiveModel
|
||||
module Type
|
||||
@registry = Registry.new
|
||||
|
||||
class << self
|
||||
attr_accessor :registry # :nodoc:
|
||||
delegate :add_modifier, to: :registry
|
||||
|
||||
# Add a new type to the registry, allowing it to be referenced as a
|
||||
# symbol by ActiveModel::Attributes::ClassMethods#attribute. If your
|
||||
# type is only meant to be used with a specific database adapter, you can
|
||||
# do so by passing +adapter: :postgresql+. If your type has the same
|
||||
# name as a native type for the current adapter, an exception will be
|
||||
# raised unless you specify an +:override+ option. +override: true+ will
|
||||
# cause your type to be used instead of the native type. +override:
|
||||
# false+ will cause the native type to be used over yours if one exists.
|
||||
def register(type_name, klass = nil, **options, &block)
|
||||
registry.register(type_name, klass, **options, &block)
|
||||
end
|
||||
|
||||
def lookup(*args, **kwargs) # :nodoc:
|
||||
registry.lookup(*args, **kwargs)
|
||||
end
|
||||
end
|
||||
|
||||
register(:big_integer, Type::BigInteger, override: false)
|
||||
register(:binary, Type::Binary, override: false)
|
||||
register(:boolean, Type::Boolean, override: false)
|
||||
register(:date, Type::Date, override: false)
|
||||
register(:date_time, Type::DateTime, override: false)
|
||||
register(:decimal, Type::Decimal, override: false)
|
||||
register(:float, Type::Float, override: false)
|
||||
register(:integer, Type::Integer, override: false)
|
||||
register(:string, Type::String, override: false)
|
||||
register(:text, Type::Text, override: false)
|
||||
register(:time, Type::Time, override: false)
|
||||
end
|
||||
end
|
@ -1,6 +1,6 @@
|
||||
require 'active_record/type/integer'
|
||||
require 'active_model/type/integer'
|
||||
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class BigInteger < Integer # :nodoc:
|
||||
private
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class Binary < Value # :nodoc:
|
||||
def type
|
@ -1,6 +1,8 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class Boolean < Value # :nodoc:
|
||||
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
|
||||
|
||||
def type
|
||||
:boolean
|
||||
end
|
||||
@ -11,7 +13,7 @@ def cast_value(value)
|
||||
if value == ''
|
||||
nil
|
||||
else
|
||||
!ConnectionAdapters::Column::FALSE_VALUES.include?(value)
|
||||
!FALSE_VALUES.include?(value)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class Date < Value # :nodoc:
|
||||
include Helpers::AcceptsMultiparameterTime.new
|
||||
@ -24,8 +24,9 @@ def cast_value(value)
|
||||
end
|
||||
end
|
||||
|
||||
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
|
||||
def fast_string_to_date(string)
|
||||
if string =~ ConnectionAdapters::Column::Format::ISO_DATE
|
||||
if string =~ ISO_DATE
|
||||
new_date $1.to_i, $2.to_i, $3.to_i
|
||||
end
|
||||
end
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class DateTime < Value # :nodoc:
|
||||
include Helpers::TimeValue
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class Decimal < Value # :nodoc:
|
||||
include Helpers::Numeric
|
@ -1,6 +1,6 @@
|
||||
require 'active_record/type/big_integer'
|
||||
require 'active_model/type/big_integer'
|
||||
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class DecimalWithoutScale < BigInteger # :nodoc:
|
||||
def type
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class Float < Value # :nodoc:
|
||||
include Helpers::Numeric
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class HashLookupTypeMap < TypeMap # :nodoc:
|
||||
def alias_type(type, alias_type)
|
4
activemodel/lib/active_model/type/helpers.rb
Normal file
4
activemodel/lib/active_model/type/helpers.rb
Normal file
@ -0,0 +1,4 @@
|
||||
require 'active_model/type/helpers/accepts_multiparameter_time'
|
||||
require 'active_model/type/helpers/numeric'
|
||||
require 'active_model/type/helpers/mutable'
|
||||
require 'active_model/type/helpers/time_value'
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
module Helpers
|
||||
class AcceptsMultiparameterTime < Module # :nodoc:
|
||||
@ -17,10 +17,7 @@ def initialize(defaults: {})
|
||||
end
|
||||
return unless values_hash[1] && values_hash[2] && values_hash[3]
|
||||
values = values_hash.sort.map(&:last)
|
||||
::Time.send(
|
||||
ActiveRecord::Base.default_timezone,
|
||||
*values
|
||||
)
|
||||
::Time.send(default_timezone, *values)
|
||||
end
|
||||
private :value_from_multiparameter_assignment
|
||||
end
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
module Helpers
|
||||
module Mutable # :nodoc:
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
module Helpers
|
||||
module Numeric # :nodoc:
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
module Helpers
|
||||
module TimeValue # :nodoc:
|
||||
@ -10,7 +10,7 @@ def serialize(value)
|
||||
end
|
||||
|
||||
if value.acts_like?(:time)
|
||||
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
|
||||
zone_conversion_method = is_utc? ? :getutc : :getlocal
|
||||
|
||||
if value.respond_to?(zone_conversion_method)
|
||||
value = value.send(zone_conversion_method)
|
||||
@ -20,6 +20,14 @@ def serialize(value)
|
||||
value
|
||||
end
|
||||
|
||||
def is_utc?
|
||||
::Time.zone_default =~ 'UTC'
|
||||
end
|
||||
|
||||
def default_timezone
|
||||
::Time.zone_default
|
||||
end
|
||||
|
||||
def type_cast_for_schema(value)
|
||||
"'#{value.to_s(:db)}'"
|
||||
end
|
||||
@ -34,20 +42,23 @@ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
|
||||
# Treat 0000-00-00 00:00:00 as nil.
|
||||
return if year.nil? || (year == 0 && mon == 0 && mday == 0)
|
||||
|
||||
|
||||
if offset
|
||||
time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
|
||||
return unless time
|
||||
|
||||
time -= offset
|
||||
Base.default_timezone == :utc ? time : time.getlocal
|
||||
is_utc? ? time : time.getlocal
|
||||
else
|
||||
::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
|
||||
::Time.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
|
||||
|
||||
# Doesn't handle time zones.
|
||||
def fast_string_to_time(string)
|
||||
if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME
|
||||
if string =~ ISO_DATETIME
|
||||
microsec = ($7.to_r * 1_000_000).to_i
|
||||
new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
|
||||
end
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class Integer < Value # :nodoc:
|
||||
include Helpers::Numeric
|
151
activemodel/lib/active_model/type/registry.rb
Normal file
151
activemodel/lib/active_model/type/registry.rb
Normal file
@ -0,0 +1,151 @@
|
||||
module ActiveModel
|
||||
# :stopdoc:
|
||||
module Type
|
||||
class Registry
|
||||
def initialize
|
||||
@registrations = []
|
||||
end
|
||||
|
||||
def register(type_name, klass = nil, **options, &block)
|
||||
block ||= proc { |_, *args| klass.new(*args) }
|
||||
registrations << registration_klass.new(type_name, block, **options)
|
||||
end
|
||||
|
||||
def lookup(symbol, *args)
|
||||
registration = registrations
|
||||
.select { |r| r.matches?(symbol, *args) }
|
||||
.max
|
||||
|
||||
if registration
|
||||
registration.call(self, symbol, *args)
|
||||
else
|
||||
raise ArgumentError, "Unknown type #{symbol.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def add_modifier(options, klass, **args)
|
||||
registrations << decoration_registration_klass.new(options, klass, **args)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :registrations
|
||||
|
||||
private
|
||||
|
||||
def registration_klass
|
||||
Registration
|
||||
end
|
||||
|
||||
def decoration_registration_klass
|
||||
DecorationRegistration
|
||||
end
|
||||
end
|
||||
|
||||
class Registration
|
||||
def initialize(name, block, override: nil)
|
||||
@name = name
|
||||
@block = block
|
||||
@override = override
|
||||
end
|
||||
|
||||
def call(_registry, *args, **kwargs)
|
||||
if kwargs.any? # https://bugs.ruby-lang.org/issues/10856
|
||||
block.call(*args, **kwargs)
|
||||
else
|
||||
block.call(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def matches?(type_name, *args, **kwargs)
|
||||
type_name == name# && matches_adapter?(**kwargs)
|
||||
end
|
||||
|
||||
def <=>(other)
|
||||
# if conflicts_with?(other)
|
||||
# raise TypeConflictError.new("Type #{name} was registered for all
|
||||
# adapters, but shadows a native type with
|
||||
# the same name for #{other.adapter}".squish)
|
||||
# end
|
||||
priority <=> other.priority
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :name, :block, :override
|
||||
|
||||
def priority
|
||||
result = 0
|
||||
# if adapter
|
||||
# result |= 1
|
||||
# end
|
||||
if override
|
||||
result |= 2
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
# def priority_except_adapter
|
||||
# priority & 0b111111100
|
||||
# end
|
||||
|
||||
private
|
||||
|
||||
# def matches_adapter?(adapter: nil, **)
|
||||
# (self.adapter.nil? || adapter == self.adapter)
|
||||
# end
|
||||
|
||||
# def conflicts_with?(other)
|
||||
# same_priority_except_adapter?(other) &&
|
||||
# has_adapter_conflict?(other)
|
||||
# end
|
||||
|
||||
# def same_priority_except_adapter?(other)
|
||||
# priority_except_adapter == other.priority_except_adapter
|
||||
# end
|
||||
|
||||
# def has_adapter_conflict?(other)
|
||||
# (override.nil? && other.adapter) ||
|
||||
# (adapter && other.override.nil?)
|
||||
# end
|
||||
end
|
||||
|
||||
class DecorationRegistration < Registration
|
||||
def initialize(options, klass)
|
||||
@options = options
|
||||
@klass = klass
|
||||
# @adapter = adapter
|
||||
end
|
||||
|
||||
def call(registry, *args, **kwargs)
|
||||
subtype = registry.lookup(*args, **kwargs.except(*options.keys))
|
||||
klass.new(subtype)
|
||||
end
|
||||
|
||||
def matches?(*args, **kwargs)
|
||||
matches_options?(**kwargs)
|
||||
end
|
||||
|
||||
def priority
|
||||
super | 4
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :options, :klass
|
||||
|
||||
private
|
||||
|
||||
def matches_options?(**kwargs)
|
||||
options.all? do |key, value|
|
||||
kwargs[key] == value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class TypeConflictError < StandardError
|
||||
end
|
||||
|
||||
# :startdoc:
|
||||
end
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class String < Value # :nodoc:
|
||||
def type
|
@ -1,6 +1,6 @@
|
||||
require 'active_record/type/string'
|
||||
require 'active_model/type/string'
|
||||
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class Text < String # :nodoc:
|
||||
def type
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class Time < Value # :nodoc:
|
||||
include Helpers::TimeValue
|
@ -1,6 +1,6 @@
|
||||
require 'concurrent'
|
||||
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class TypeMap # :nodoc:
|
||||
def initialize
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class UnsignedInteger < Integer # :nodoc:
|
||||
private
|
@ -1,4 +1,4 @@
|
||||
module ActiveRecord
|
||||
module ActiveModel
|
||||
module Type
|
||||
class Value
|
||||
attr_reader :precision, :scale, :limit
|
@ -19,7 +19,7 @@ def query_attribute(attr_name)
|
||||
if Numeric === value || value !~ /[^0-9]/
|
||||
!value.to_i.zero?
|
||||
else
|
||||
return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
|
||||
return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
|
||||
!value.blank?
|
||||
end
|
||||
elsif value.respond_to?(:zero?)
|
||||
|
@ -5,13 +5,6 @@ module ActiveRecord
|
||||
module ConnectionAdapters
|
||||
# An abstract definition of a column in a table.
|
||||
class Column
|
||||
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
|
||||
|
||||
module Format
|
||||
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
|
||||
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
|
||||
end
|
||||
|
||||
attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation
|
||||
|
||||
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
|
||||
|
@ -1,26 +1,28 @@
|
||||
require 'active_record/type/helpers'
|
||||
require 'active_record/type/value'
|
||||
require 'active_model/type/helpers'
|
||||
require 'active_model/type/value'
|
||||
|
||||
require 'active_record/type/big_integer'
|
||||
require 'active_record/type/binary'
|
||||
require 'active_record/type/boolean'
|
||||
require 'active_record/type/date'
|
||||
require 'active_record/type/date_time'
|
||||
require 'active_record/type/decimal'
|
||||
require 'active_record/type/decimal_without_scale'
|
||||
require 'active_record/type/float'
|
||||
require 'active_record/type/integer'
|
||||
require 'active_record/type/serialized'
|
||||
require 'active_record/type/string'
|
||||
require 'active_record/type/text'
|
||||
require 'active_record/type/time'
|
||||
require 'active_record/type/unsigned_integer'
|
||||
require 'active_model/type/big_integer'
|
||||
require 'active_model/type/binary'
|
||||
require 'active_model/type/boolean'
|
||||
require 'active_model/type/date'
|
||||
require 'active_model/type/date_time'
|
||||
require 'active_model/type/decimal'
|
||||
require 'active_model/type/decimal_without_scale'
|
||||
require 'active_model/type/float'
|
||||
require 'active_model/type/integer'
|
||||
require 'active_model/type/string'
|
||||
require 'active_model/type/text'
|
||||
require 'active_model/type/time'
|
||||
require 'active_model/type/unsigned_integer'
|
||||
|
||||
require 'active_record/type/adapter_specific_registry'
|
||||
require 'active_record/type/type_map'
|
||||
require 'active_record/type/hash_lookup_type_map'
|
||||
require 'active_model/type/registry'
|
||||
require 'active_model/type/type_map'
|
||||
require 'active_model/type/hash_lookup_type_map'
|
||||
|
||||
require 'active_record/type/internal/abstract_json'
|
||||
require 'active_record/type/internal/timezone'
|
||||
require 'active_record/type/serialized'
|
||||
require 'active_record/type/adapter_specific_registry'
|
||||
|
||||
module ActiveRecord
|
||||
module Type
|
||||
@ -53,6 +55,32 @@ def current_adapter_name
|
||||
end
|
||||
end
|
||||
|
||||
class Date < ActiveModel::Type::Date
|
||||
include Internal::Timezone
|
||||
end
|
||||
|
||||
class DateTime < ActiveModel::Type::DateTime
|
||||
include Internal::Timezone
|
||||
end
|
||||
class Time < ActiveModel::Type::Time
|
||||
include Internal::Timezone
|
||||
end
|
||||
|
||||
Helpers = ActiveModel::Type::Helpers
|
||||
BigInteger = ActiveModel::Type::BigInteger
|
||||
Binary = ActiveModel::Type::Binary
|
||||
Boolean = ActiveModel::Type::Boolean
|
||||
Decimal = ActiveModel::Type::Decimal
|
||||
DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale
|
||||
Float = ActiveModel::Type::Float
|
||||
Integer = ActiveModel::Type::Integer
|
||||
String = ActiveModel::Type::String
|
||||
Text = ActiveModel::Type::Text
|
||||
UnsignedInteger = ActiveModel::Type::UnsignedInteger
|
||||
Value = ActiveModel::Type::Value
|
||||
TypeMap = ActiveModel::Type::TypeMap
|
||||
HashLookupTypeMap = ActiveModel::Type::HashLookupTypeMap
|
||||
|
||||
register(:big_integer, Type::BigInteger, override: false)
|
||||
register(:binary, Type::Binary, override: false)
|
||||
register(:boolean, Type::Boolean, override: false)
|
||||
|
@ -1,35 +1,18 @@
|
||||
require 'active_model/type/registry'
|
||||
|
||||
module ActiveRecord
|
||||
# :stopdoc:
|
||||
module Type
|
||||
class AdapterSpecificRegistry
|
||||
def initialize
|
||||
@registrations = []
|
||||
class AdapterSpecificRegistry < ActiveModel::Type::Registry
|
||||
private
|
||||
|
||||
def registration_klass
|
||||
Registration
|
||||
end
|
||||
|
||||
def register(type_name, klass = nil, **options, &block)
|
||||
block ||= proc { |_, *args| klass.new(*args) }
|
||||
registrations << Registration.new(type_name, block, **options)
|
||||
def decoration_registration_klass
|
||||
DecorationRegistration
|
||||
end
|
||||
|
||||
def lookup(symbol, *args)
|
||||
registration = registrations
|
||||
.select { |r| r.matches?(symbol, *args) }
|
||||
.max
|
||||
|
||||
if registration
|
||||
registration.call(self, symbol, *args)
|
||||
else
|
||||
raise ArgumentError, "Unknown type #{symbol.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def add_modifier(options, klass, **args)
|
||||
registrations << DecorationRegistration.new(options, klass, **args)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :registrations
|
||||
end
|
||||
|
||||
class Registration
|
||||
@ -135,7 +118,7 @@ def matches_options?(**kwargs)
|
||||
end
|
||||
end
|
||||
|
||||
class TypeConflictError < StandardError
|
||||
class TypeConflictError < ::ActiveModel::TypeConflictError
|
||||
end
|
||||
|
||||
# :startdoc:
|
||||
|
@ -1,4 +0,0 @@
|
||||
require 'active_record/type/helpers/accepts_multiparameter_time'
|
||||
require 'active_record/type/helpers/numeric'
|
||||
require 'active_record/type/helpers/mutable'
|
||||
require 'active_record/type/helpers/time_value'
|
@ -1,8 +1,8 @@
|
||||
module ActiveRecord
|
||||
module Type
|
||||
module Internal # :nodoc:
|
||||
class AbstractJson < Type::Value # :nodoc:
|
||||
include Type::Helpers::Mutable
|
||||
class AbstractJson < ActiveModel::Type::Value # :nodoc:
|
||||
include ActiveModel::Type::Helpers::Mutable
|
||||
|
||||
def type
|
||||
:json
|
||||
|
15
activerecord/lib/active_record/type/internal/timezone.rb
Normal file
15
activerecord/lib/active_record/type/internal/timezone.rb
Normal file
@ -0,0 +1,15 @@
|
||||
module ActiveRecord
|
||||
module Type
|
||||
module Internal
|
||||
module Timezone
|
||||
def is_utc?
|
||||
ActiveRecord::Base.default_timezone == :utc
|
||||
end
|
||||
|
||||
def default_timezone
|
||||
ActiveRecord::Base.default_timezone
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,7 +1,7 @@
|
||||
module ActiveRecord
|
||||
module Type
|
||||
class Serialized < DelegateClass(Type::Value) # :nodoc:
|
||||
include Helpers::Mutable
|
||||
class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
|
||||
include ActiveModel::Type::Helpers::Mutable
|
||||
|
||||
attr_reader :subtype, :coder
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user