From 00b2992d21976752d6caa801a7b3038d4be5559c Mon Sep 17 00:00:00 2001 From: jasl Date: Tue, 5 Dec 2017 22:36:35 +0800 Subject: [PATCH] loosen rails requirement --- Gemfile.lock | 6 +- README.md | 21 ++-- lib/active_model/type.rb | 49 ++++++++ lib/active_model/type/big_integer.rb | 15 +++ lib/active_model/type/binary.rb | 52 ++++++++ lib/active_model/type/boolean.rb | 34 +++++ lib/active_model/type/date.rb | 56 +++++++++ lib/active_model/type/date_time.rb | 46 +++++++ lib/active_model/type/decimal.rb | 70 +++++++++++ lib/active_model/type/float.rb | 36 ++++++ lib/active_model/type/helpers.rb | 6 + .../helpers/accepts_multiparameter_time.rb | 37 ++++++ lib/active_model/type/helpers/mutable.rb | 20 +++ lib/active_model/type/helpers/numeric.rb | 37 ++++++ lib/active_model/type/helpers/time_value.rb | 79 ++++++++++++ lib/active_model/type/immutable_string.rb | 32 +++++ lib/active_model/type/integer.rb | 70 +++++++++++ lib/active_model/type/registry.rb | 70 +++++++++++ lib/active_model/type/string.rb | 26 ++++ lib/active_model/type/time.rb | 48 +++++++ lib/active_model/type/value.rb | 118 ++++++++++++++++++ lib/options_model.rb | 4 + options_model.gemspec | 4 +- 23 files changed, 921 insertions(+), 15 deletions(-) create mode 100644 lib/active_model/type.rb create mode 100644 lib/active_model/type/big_integer.rb create mode 100644 lib/active_model/type/binary.rb create mode 100644 lib/active_model/type/boolean.rb create mode 100644 lib/active_model/type/date.rb create mode 100644 lib/active_model/type/date_time.rb create mode 100644 lib/active_model/type/decimal.rb create mode 100644 lib/active_model/type/float.rb create mode 100644 lib/active_model/type/helpers.rb create mode 100644 lib/active_model/type/helpers/accepts_multiparameter_time.rb create mode 100644 lib/active_model/type/helpers/mutable.rb create mode 100644 lib/active_model/type/helpers/numeric.rb create mode 100644 lib/active_model/type/helpers/time_value.rb create mode 100644 lib/active_model/type/immutable_string.rb create mode 100644 lib/active_model/type/integer.rb create mode 100644 lib/active_model/type/registry.rb create mode 100644 lib/active_model/type/string.rb create mode 100644 lib/active_model/type/time.rb create mode 100644 lib/active_model/type/value.rb diff --git a/Gemfile.lock b/Gemfile.lock index 2b36ef4..e6346ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,8 +2,8 @@ PATH remote: . specs: options_model (0.0.6) - activemodel (~> 5.0) - activesupport (~> 5.0) + activemodel (>= 4.2, < 6.0) + activesupport (>= 4.2, < 6.0) GEM remote: https://rubygems.org/ @@ -117,4 +117,4 @@ DEPENDENCIES rails (~> 5.0) BUNDLED WITH - 1.15.4 + 1.16.0 diff --git a/README.md b/README.md index 96df96b..66eb653 100644 --- a/README.md +++ b/README.md @@ -11,24 +11,25 @@ support attribute: ## Usage +```ruby class Person < OptionsModel::Base - attribute :name, :string - attribute :age, :integer + attribute :name, :string + attribute :age, :integer - validates :name, presence: true + validates :name, presence: true end class Book < OptionsModel::Base - embeds_one :author, class_name: 'Person' + embeds_one :author, class_name: 'Person' - attribute :title, :string - attribute :tags, :string, array: true - attribute :price, :decimal, default: 0 - attribute :meta, :json, default: {} - attribute :bought_at, :datetime, default: -> { Time.new } + attribute :title, :string + attribute :tags, :string, array: true + attribute :price, :decimal, default: 0 + attribute :bought_at, :datetime, default: -> { Time.new } - validates :title, presence: true + validates :title, presence: true end +``` ## Installation Add this line to your application's Gemfile: diff --git a/lib/active_model/type.rb b/lib/active_model/type.rb new file mode 100644 index 0000000..b0ed67f --- /dev/null +++ b/lib/active_model/type.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +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/float" +require "active_model/type/immutable_string" +require "active_model/type/integer" +require "active_model/type/string" +require "active_model/type/time" + +require "active_model/type/registry" + +module ActiveModel + module Type + @registry = Registry.new + + class << self + attr_accessor :registry # :nodoc: + + # Add a new type to the registry, allowing it to be get through ActiveModel::Type#lookup + 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) + register(:binary, Type::Binary) + register(:boolean, Type::Boolean) + register(:date, Type::Date) + register(:datetime, Type::DateTime) + register(:decimal, Type::Decimal) + register(:float, Type::Float) + register(:immutable_string, Type::ImmutableString) + register(:integer, Type::Integer) + register(:string, Type::String) + register(:time, Type::Time) + end +end diff --git a/lib/active_model/type/big_integer.rb b/lib/active_model/type/big_integer.rb new file mode 100644 index 0000000..89e43bc --- /dev/null +++ b/lib/active_model/type/big_integer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "active_model/type/integer" + +module ActiveModel + module Type + class BigInteger < Integer # :nodoc: + private + + def max_value + ::Float::INFINITY + end + end + end +end diff --git a/lib/active_model/type/binary.rb b/lib/active_model/type/binary.rb new file mode 100644 index 0000000..dc2eca1 --- /dev/null +++ b/lib/active_model/type/binary.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module ActiveModel + module Type + class Binary < Value # :nodoc: + def type + :binary + end + + def binary? + true + end + + def cast(value) + if value.is_a?(Data) + value.to_s + else + super + end + end + + def serialize(value) + return if value.nil? + Data.new(super) + end + + def changed_in_place?(raw_old_value, value) + old_value = deserialize(raw_old_value) + old_value != value + end + + class Data # :nodoc: + def initialize(value) + @value = value.to_s + end + + def to_s + @value + end + alias_method :to_str, :to_s + + def hex + @value.unpack("H*")[0] + end + + def ==(other) + other == to_s || super + end + end + end + end +end diff --git a/lib/active_model/type/boolean.rb b/lib/active_model/type/boolean.rb new file mode 100644 index 0000000..c7f6451 --- /dev/null +++ b/lib/active_model/type/boolean.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module ActiveModel + module Type + # == Active \Model \Type \Boolean + # + # A class that behaves like a boolean type, including rules for coercion of user input. + # + # === Coercion + # Values set from user input will first be coerced into the appropriate ruby type. + # Coercion behavior is roughly mapped to Ruby's boolean semantics. + # + # - "false", "f" , "0", +0+ or any other value in +FALSE_VALUES+ will be coerced to +false+ + # - Empty strings are coerced to +nil+ + # - All other values will be coerced to +true+ + class Boolean < Value + FALSE_VALUES = [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"].to_set + + def type # :nodoc: + :boolean + end + + private + + def cast_value(value) + if value == "" + nil + else + !FALSE_VALUES.include?(value) + end + end + end + end +end diff --git a/lib/active_model/type/date.rb b/lib/active_model/type/date.rb new file mode 100644 index 0000000..0ee26ae --- /dev/null +++ b/lib/active_model/type/date.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module ActiveModel + module Type + class Date < Value # :nodoc: + include Helpers::AcceptsMultiparameterTime.new + + def type + :date + end + + def serialize(value) + cast(value) + end + + def type_cast_for_schema(value) + value.to_s(:db).inspect + end + + private + + def cast_value(value) + if value.is_a?(::String) + return if value.empty? + fast_string_to_date(value) || fallback_string_to_date(value) + elsif value.respond_to?(:to_date) + value.to_date + else + value + end + end + + ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ + def fast_string_to_date(string) + if string =~ ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def new_date(year, mon, mday) + if year && year != 0 + ::Date.new(year, mon, mday) rescue nil + end + end + + def value_from_multiparameter_assignment(*) + time = super + time && time.to_date + end + end + end +end diff --git a/lib/active_model/type/date_time.rb b/lib/active_model/type/date_time.rb new file mode 100644 index 0000000..666ed9e --- /dev/null +++ b/lib/active_model/type/date_time.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module ActiveModel + module Type + class DateTime < Value # :nodoc: + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 4 => 0, 5 => 0 } + ) + + def type + :datetime + end + + private + + def cast_value(value) + return apply_seconds_precision(value) unless value.is_a?(::String) + return if value.empty? + + fast_string_to_time(value) || fallback_string_to_time(value) + end + + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 + end + + def fallback_string_to_time(string) + time_hash = ::Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) + end + + def value_from_multiparameter_assignment(values_hash) + missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } + if missing_parameter + raise ArgumentError, missing_parameter + end + super + end + end + end +end diff --git a/lib/active_model/type/decimal.rb b/lib/active_model/type/decimal.rb new file mode 100644 index 0000000..12bf1d2 --- /dev/null +++ b/lib/active_model/type/decimal.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require "bigdecimal/util" + +module ActiveModel + module Type + class Decimal < Value # :nodoc: + include Helpers::Numeric + BIGDECIMAL_PRECISION = 18 + + def type + :decimal + end + + def type_cast_for_schema(value) + value.to_s.inspect + end + + private + + def cast_value(value) + casted_value = \ + case value + when ::Float + convert_float_to_big_decimal(value) + when ::Numeric + BigDecimal(value, precision || BIGDECIMAL_PRECISION) + when ::String + begin + value.to_d + rescue ArgumentError + BigDecimal(0) + end + else + if value.respond_to?(:to_d) + value.to_d + else + cast_value(value.to_s) + end + end + + apply_scale(casted_value) + end + + def convert_float_to_big_decimal(value) + if precision + BigDecimal(apply_scale(value), float_precision) + else + value.to_d + end + end + + def float_precision + if precision.to_i > ::Float::DIG + 1 + ::Float::DIG + 1 + else + precision.to_i + end + end + + def apply_scale(value) + if scale + value.round(scale) + else + value + end + end + end + end +end diff --git a/lib/active_model/type/float.rb b/lib/active_model/type/float.rb new file mode 100644 index 0000000..b493f64 --- /dev/null +++ b/lib/active_model/type/float.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module ActiveModel + module Type + class Float < Value # :nodoc: + include Helpers::Numeric + + def type + :float + end + + def type_cast_for_schema(value) + return "::Float::NAN" if value.try(:nan?) + case value + when ::Float::INFINITY then "::Float::INFINITY" + when -::Float::INFINITY then "-::Float::INFINITY" + else super + end + end + + alias serialize cast + + private + + def cast_value(value) + case value + when ::Float then value + when "Infinity" then ::Float::INFINITY + when "-Infinity" then -::Float::INFINITY + when "NaN" then ::Float::NAN + else value.to_f + end + end + end + end +end diff --git a/lib/active_model/type/helpers.rb b/lib/active_model/type/helpers.rb new file mode 100644 index 0000000..403f0a9 --- /dev/null +++ b/lib/active_model/type/helpers.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +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" diff --git a/lib/active_model/type/helpers/accepts_multiparameter_time.rb b/lib/active_model/type/helpers/accepts_multiparameter_time.rb new file mode 100644 index 0000000..e2a8271 --- /dev/null +++ b/lib/active_model/type/helpers/accepts_multiparameter_time.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module ActiveModel + module Type + module Helpers # :nodoc: all + class AcceptsMultiparameterTime < Module + def initialize(defaults: {}) + define_method(:cast) do |value| + if value.is_a?(Hash) + value_from_multiparameter_assignment(value) + else + super(value) + end + end + + define_method(:assert_valid_value) do |value| + if value.is_a?(Hash) + value_from_multiparameter_assignment(value) + else + super(value) + end + end + + define_method(:value_from_multiparameter_assignment) do |values_hash| + defaults.each do |k, v| + values_hash[k] ||= v + end + return unless values_hash[1] && values_hash[2] && values_hash[3] + values = values_hash.sort.map(&:last) + ::Time.send(default_timezone, *values) + end + private :value_from_multiparameter_assignment + end + end + end + end +end diff --git a/lib/active_model/type/helpers/mutable.rb b/lib/active_model/type/helpers/mutable.rb new file mode 100644 index 0000000..1cbea64 --- /dev/null +++ b/lib/active_model/type/helpers/mutable.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module ActiveModel + module Type + module Helpers # :nodoc: all + module Mutable + def cast(value) + deserialize(serialize(value)) + end + + # +raw_old_value+ will be the `_before_type_cast` version of the + # value (likely a string). +new_value+ will be the current, type + # cast value. + def changed_in_place?(raw_old_value, new_value) + raw_old_value != serialize(new_value) + end + end + end + end +end diff --git a/lib/active_model/type/helpers/numeric.rb b/lib/active_model/type/helpers/numeric.rb new file mode 100644 index 0000000..bfb6692 --- /dev/null +++ b/lib/active_model/type/helpers/numeric.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module ActiveModel + module Type + module Helpers # :nodoc: all + module Numeric + def cast(value) + value = \ + case value + when true then 1 + when false then 0 + when ::String then value.presence + else value + end + super(value) + end + + def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: + super || number_to_non_number?(old_value, new_value_before_type_cast) + end + + private + + def number_to_non_number?(old_value, new_value_before_type_cast) + old_value != nil && non_numeric_string?(new_value_before_type_cast) + end + + def non_numeric_string?(value) + # 'wibble'.to_i will give zero, we want to make sure + # that we aren't marking int zero to string zero as + # changed. + value.to_s !~ /\A-?\d+\.?\d*\z/ + end + end + end + end +end diff --git a/lib/active_model/type/helpers/time_value.rb b/lib/active_model/type/helpers/time_value.rb new file mode 100644 index 0000000..21613c7 --- /dev/null +++ b/lib/active_model/type/helpers/time_value.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require "active_support/core_ext/time/zones" + +module ActiveModel + module Type + module Helpers # :nodoc: all + module TimeValue + def serialize(value) + value = apply_seconds_precision(value) + + if value.acts_like?(:time) + zone_conversion_method = is_utc? ? :getutc : :getlocal + + if value.respond_to?(zone_conversion_method) + value = value.send(zone_conversion_method) + end + end + + value + end + + def is_utc? + ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC" + end + + def default_timezone + if is_utc? + :utc + else + :local + end + end + + def apply_seconds_precision(value) + return value unless precision && value.respond_to?(:usec) + number_of_insignificant_digits = 6 - precision + round_power = 10**number_of_insignificant_digits + value.change(usec: value.usec - value.usec % round_power) + end + + def type_cast_for_schema(value) + value.to_s(:db).inspect + end + + def user_input_in_time_zone(value) + value.in_time_zone + end + + private + + 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 + is_utc? ? time : time.getlocal + else + ::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 =~ 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 + end + end + end + end +end diff --git a/lib/active_model/type/immutable_string.rb b/lib/active_model/type/immutable_string.rb new file mode 100644 index 0000000..ee380f9 --- /dev/null +++ b/lib/active_model/type/immutable_string.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module ActiveModel + module Type + class ImmutableString < Value # :nodoc: + def type + :string + end + + def serialize(value) + case value + when ::Numeric, ActiveSupport::Duration then value.to_s + when true then "t" + when false then "f" + else super + end + end + + private + + def cast_value(value) + result = \ + case value + when true then "t" + when false then "f" + else value.to_s + end + result.freeze + end + end + end +end diff --git a/lib/active_model/type/integer.rb b/lib/active_model/type/integer.rb new file mode 100644 index 0000000..b3626c4 --- /dev/null +++ b/lib/active_model/type/integer.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module ActiveModel + module Type + class Integer < Value # :nodoc: + include Helpers::Numeric + + # Column storage size in bytes. + # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc. + DEFAULT_LIMIT = 4 + + def initialize(*) + super + @range = min_value...max_value + end + + def type + :integer + end + + def deserialize(value) + return if value.nil? + value.to_i + end + + def serialize(value) + result = cast(value) + if result + ensure_in_range(result) + end + result + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + + attr_reader :range + + private + + def cast_value(value) + case value + when true then 1 + when false then 0 + else + value.to_i rescue nil + end + end + + def ensure_in_range(value) + unless range.cover?(value) + raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes" + end + end + + def max_value + 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign + end + + def min_value + -max_value + end + + def _limit + limit || DEFAULT_LIMIT + end + end + end +end diff --git a/lib/active_model/type/registry.rb b/lib/active_model/type/registry.rb new file mode 100644 index 0000000..b2bc9e0 --- /dev/null +++ b/lib/active_model/type/registry.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +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 = find_registration(symbol, *args) + + if registration + registration.call(self, symbol, *args) + else + raise ArgumentError, "Unknown type #{symbol.inspect}" + end + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + + attr_reader :registrations + + private + + def registration_klass + Registration + end + + def find_registration(symbol, *args) + registrations.find { |r| r.matches?(symbol, *args) } + end + end + + class Registration + # Options must be taken because of https://bugs.ruby-lang.org/issues/10856 + def initialize(name, block, **) + @name = name + @block = block + 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 + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + + attr_reader :name, :block + end + end + # :startdoc: +end diff --git a/lib/active_model/type/string.rb b/lib/active_model/type/string.rb new file mode 100644 index 0000000..031b548 --- /dev/null +++ b/lib/active_model/type/string.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "active_model/type/immutable_string" + +module ActiveModel + module Type + class String < ImmutableString # :nodoc: + def changed_in_place?(raw_old_value, new_value) + if new_value.is_a?(::String) + raw_old_value != new_value + end + end + + private + + def cast_value(value) + case value + when ::String then ::String.new(value) + when true then "t".freeze + when false then "f".freeze + else value.to_s + end + end + end + end +end diff --git a/lib/active_model/type/time.rb b/lib/active_model/type/time.rb new file mode 100644 index 0000000..bc068c7 --- /dev/null +++ b/lib/active_model/type/time.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module ActiveModel + module Type + class Time < Value # :nodoc: + include Helpers::TimeValue + include Helpers::AcceptsMultiparameterTime.new( + defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } + ) + + def type + :time + end + + def user_input_in_time_zone(value) + return unless value.present? + + case value + when ::String + value = "2000-01-01 #{value}" + when ::Time + value = value.change(year: 2000, day: 1, month: 1) + end + + super(value) + end + + private + + def cast_value(value) + return value unless value.is_a?(::String) + return if value.empty? + + if value.start_with?("2000-01-01") + dummy_time_value = value + else + dummy_time_value = "2000-01-01 #{value}" + end + + fast_string_to_time(dummy_time_value) || begin + time_hash = ::Date._parse(dummy_time_value) + return if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) + end + end + end + end +end diff --git a/lib/active_model/type/value.rb b/lib/active_model/type/value.rb new file mode 100644 index 0000000..babb544 --- /dev/null +++ b/lib/active_model/type/value.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +module ActiveModel + module Type + class Value + attr_reader :precision, :scale, :limit + + def initialize(precision: nil, limit: nil, scale: nil) + @precision = precision + @scale = scale + @limit = limit + end + + def type # :nodoc: + end + + # Converts a value from database input to the appropriate ruby type. The + # return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. The default + # implementation just calls Value#cast. + # + # +value+ The raw input, as provided from the database. + def deserialize(value) + cast(value) + end + + # Type casts a value from user input (e.g. from a setter). This value may + # be a string from the form builder, or a ruby object passed to a setter. + # There is currently no way to differentiate between which source it came + # from. + # + # The return value of this method will be returned from + # ActiveRecord::AttributeMethods::Read#read_attribute. See also: + # Value#cast_value. + # + # +value+ The raw input, as provided to the attribute setter. + def cast(value) + cast_value(value) unless value.nil? + end + + # Casts a value from the ruby type to a type that the database knows how + # to understand. The returned value from this method should be a + # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or + # +nil+. + def serialize(value) + value + end + + # Type casts a value for schema dumping. This method is private, as we are + # hoping to remove it entirely. + def type_cast_for_schema(value) # :nodoc: + value.inspect + end + + # These predicates are not documented, as I need to look further into + # their use, and see if they can be removed entirely. + def binary? # :nodoc: + false + end + + # Determines whether a value has changed for dirty checking. +old_value+ + # and +new_value+ will always be type-cast. Types should not need to + # override this method. + def changed?(old_value, new_value, _new_value_before_type_cast) + old_value != new_value + end + + # Determines whether the mutable value has been modified since it was + # read. Returns +false+ by default. If your type returns an object + # which could be mutated, you should override this method. You will need + # to either: + # + # - pass +new_value+ to Value#serialize and compare it to + # +raw_old_value+ + # + # or + # + # - pass +raw_old_value+ to Value#deserialize and compare it to + # +new_value+ + # + # +raw_old_value+ The original value, before being passed to + # +deserialize+. + # + # +new_value+ The current value, after type casting. + def changed_in_place?(raw_old_value, new_value) + false + end + + def map(value) # :nodoc: + yield value + end + + def ==(other) + self.class == other.class && + precision == other.precision && + scale == other.scale && + limit == other.limit + end + alias eql? == + + def hash + [self.class, precision, scale, limit].hash + end + + def assert_valid_value(*) + end + + private + + # Convenience method for types which do not need separate type casting + # behavior for user and database inputs. Called by Value#cast for + # values except +nil+. + def cast_value(value) # :doc: + value + end + end + end +end diff --git a/lib/options_model.rb b/lib/options_model.rb index c5dfa2c..5720daf 100644 --- a/lib/options_model.rb +++ b/lib/options_model.rb @@ -10,5 +10,9 @@ require "options_model/base" +unless defined?(ActiveModel::Type) + require "active_model/type" +end + module OptionsModel end diff --git a/options_model.gemspec b/options_model.gemspec index 55a223e..2ddc96f 100644 --- a/options_model.gemspec +++ b/options_model.gemspec @@ -21,8 +21,8 @@ Gem::Specification.new do |s| s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] - s.add_dependency "activemodel", "~> 5.0" - s.add_dependency "activesupport", "~> 5.0" + s.add_dependency "activemodel", ">= 4.2", "< 6.0" + s.add_dependency "activesupport", ">= 4.2", "< 6.0" s.add_development_dependency "rails", "~> 5.0" end