diff --git a/README.md b/README.md index ca27c60..d328ad9 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,10 @@ list = Kredis.list "mylist" list << "hello world!" # => RPUSH mylist "hello world!" [ "hello world!" ] == list.elements # => LRANGE mylist 0, -1 -integer_list = Kredis.list "myintegerlist", typed: :integer -integer_list.append([ 1, 2, 3 ]) # => RPUSH myintegerlist "1" "2" "3" -integer_list << 4 # => RPUSH myintegerlist "4" -[ 1, 2, 3, 4 ] == integer_list.elements # => LRANGE myintegerlist 0 -1 +integer_list = Kredis.list "myintegerlist", typed: :integer, default: [ 1, 2, 3 ] # => EXISTS? myintegerlist, RPUSH myintegerlist "1" "2" "3" +integer_list.append([ 4, 5, 6 ]) # => RPUSH myintegerlist "4" "5" "6" +integer_list << 7 # => RPUSH myintegerlist "7" +[ 1, 2, 3, 4, 5, 6, 7 ] == integer_list.elements # => LRANGE myintegerlist 0 -1 unique_list = Kredis.unique_list "myuniquelist" unique_list.append(%w[ 2 3 4 ]) # => LREM myuniquelist 0, "2" + LREM myuniquelist 0, "3" + LREM myuniquelist 0, "4" + RPUSH myuniquelist "2", "3", "4" @@ -163,15 +163,7 @@ sleep 0.6.seconds false == flag.marked? #=> EXISTS myflag ``` -And using structures on a different than the default `shared` redis instance, relying on `config/redis/secondary.yml`: - -```ruby -one_string = Kredis.string "mystring" -two_string = Kredis.string "mystring", config: :secondary - -one_string.value = "just on shared" -two_string.value != one_string.value -``` +### Models You can use all these structures in models: @@ -197,6 +189,29 @@ person.morning.value = "blue" # => SET people:5:morning true == person.morning.blue? # => GET people:5:morning ``` +### Default values + +You can set a default value for all types. For example: + +```ruby +list = Kredis.list "favorite_colors", default: [ "red", "green", "blue" ] + +# or, in a model +class Person < ApplicationRecord + kredis_string :name, default: "Unknown" + kredis_list :favorite_colors, default: [ "red", "green", "blue" ] +end +``` + +There's a performance overhead to consider though. When you first read or write an attribute in a model, Kredis will +check if the underlying Redis key exists, while watching for concurrent changes, and if it does not, +write the specified default value. + +This means that using default values in a typical Rails app additional Redis calls (WATCH, EXISTS, UNWATCH) will be +executed for each Kredis attribute with a default value read or written during a request. + +### Callbacks + You can also define `after_change` callbacks that trigger on mutations: ```ruby @@ -209,6 +224,18 @@ class Person < ApplicationRecord end ``` +### Multiple Redis servers + +And using structures on a different than the default `shared` redis instance, relying on `config/redis/secondary.yml`: + +```ruby +one_string = Kredis.string "mystring" +two_string = Kredis.string "mystring", config: :secondary + +one_string.value = "just on shared" +two_string.value != one_string.value +``` + ## Installation 1. Run `./bin/bundle add kredis` diff --git a/lib/kredis.rb b/lib/kredis.rb index 2505032..28f0baf 100644 --- a/lib/kredis.rb +++ b/lib/kredis.rb @@ -10,6 +10,7 @@ require "kredis/log_subscriber" require "kredis/namespace" require "kredis/type_casting" +require "kredis/default_values" require "kredis/types" require "kredis/attributes" diff --git a/lib/kredis/attributes.rb b/lib/kredis/attributes.rb index e237455..b844501 100644 --- a/lib/kredis/attributes.rb +++ b/lib/kredis/attributes.rb @@ -8,56 +8,56 @@ def kredis_proxy(name, key: nil, config: :shared, after_change: nil) kredis_connection_with __method__, name, key, config: config, after_change: after_change end - def kredis_string(name, key: nil, config: :shared, after_change: nil, expires_in: nil) - kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in + def kredis_string(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil) + kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in end - def kredis_integer(name, key: nil, config: :shared, after_change: nil, expires_in: nil) - kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in + def kredis_integer(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil) + kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in end - def kredis_decimal(name, key: nil, config: :shared, after_change: nil, expires_in: nil) - kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in + def kredis_decimal(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil) + kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in end - def kredis_datetime(name, key: nil, config: :shared, after_change: nil, expires_in: nil) - kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in + def kredis_datetime(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil) + kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in end - def kredis_flag(name, key: nil, config: :shared, after_change: nil, expires_in: nil) - kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in + def kredis_flag(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil) + kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in define_method("#{name}?") do send(name).marked? end end - def kredis_float(name, key: nil, config: :shared, after_change: nil, expires_in: nil) - kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in + def kredis_float(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil) + kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in end def kredis_enum(name, key: nil, values:, default:, config: :shared, after_change: nil) kredis_connection_with __method__, name, key, values: values, default: default, config: config, after_change: after_change end - def kredis_json(name, key: nil, config: :shared, after_change: nil, expires_in: nil) - kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in + def kredis_json(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil) + kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in end - def kredis_list(name, key: nil, typed: :string, config: :shared, after_change: nil) - kredis_connection_with __method__, name, key, typed: typed, config: config, after_change: after_change + def kredis_list(name, key: nil, default: nil, typed: :string, config: :shared, after_change: nil) + kredis_connection_with __method__, name, key, default: default, typed: typed, config: config, after_change: after_change end - def kredis_unique_list(name, limit: nil, key: nil, typed: :string, config: :shared, after_change: nil) - kredis_connection_with __method__, name, key, limit: limit, typed: typed, config: config, after_change: after_change + def kredis_unique_list(name, limit: nil, key: nil, default: nil, typed: :string, config: :shared, after_change: nil) + kredis_connection_with __method__, name, key, default: default, limit: limit, typed: typed, config: config, after_change: after_change end - def kredis_ordered_set(name, limit: nil, key: nil, typed: :string, config: :shared, after_change: nil) - kredis_connection_with __method__, name, key, limit: limit, typed: typed, config: config, after_change: after_change + def kredis_set(name, key: nil, default: nil, typed: :string, config: :shared, after_change: nil) + kredis_connection_with __method__, name, key, default: default, typed: typed, config: config, after_change: after_change end - def kredis_set(name, key: nil, typed: :string, config: :shared, after_change: nil) - kredis_connection_with __method__, name, key, typed: typed, config: config, after_change: after_change + def kredis_ordered_set(name, limit: nil, default: nil, key: nil, typed: :string, config: :shared, after_change: nil) + kredis_connection_with __method__, name, key, default: default, limit: limit, typed: typed, config: config, after_change: after_change end def kredis_slot(name, key: nil, config: :shared, after_change: nil) @@ -68,16 +68,16 @@ def kredis_slots(name, available:, key: nil, config: :shared, after_change: nil) kredis_connection_with __method__, name, key, available: available, config: config, after_change: after_change end - def kredis_counter(name, key: nil, config: :shared, after_change: nil, expires_in: nil) - kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in + def kredis_counter(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil) + kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in end - def kredis_hash(name, key: nil, typed: :string, config: :shared, after_change: nil) - kredis_connection_with __method__, name, key, typed: typed, config: config, after_change: after_change + def kredis_hash(name, key: nil, default: nil, typed: :string, config: :shared, after_change: nil) + kredis_connection_with __method__, name, key, default: default, typed: typed, config: config, after_change: after_change end - def kredis_boolean(name, key: nil, config: :shared, after_change: nil, expires_in: nil) - kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in + def kredis_boolean(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil) + kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in end private @@ -90,6 +90,7 @@ def kredis_connection_with(method, name, key, **options) if instance_variable_defined?(ivar_symbol) instance_variable_get(ivar_symbol) else + options[:default] = kredis_default_evaluated(options[:default]) if options[:default] new_type = Kredis.send(type, kredis_key_evaluated(key) || kredis_key_for_attribute(name), **options) instance_variable_set ivar_symbol, after_change ? enrich_after_change_with_record_access(new_type, after_change) : new_type @@ -121,4 +122,12 @@ def enrich_after_change_with_record_access(type, original_after_change) when Symbol then Kredis::Types::CallbacksProxy.new(type, ->(_) { send(original_after_change) }) end end + + def kredis_default_evaluated(default) + case default + when Proc then Proc.new { default.call(self) } + when Symbol then send(default) + else default + end + end end diff --git a/lib/kredis/default_values.rb b/lib/kredis/default_values.rb new file mode 100644 index 0000000..53352d2 --- /dev/null +++ b/lib/kredis/default_values.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Kredis::DefaultValues + extend ActiveSupport::Concern + + prepended do + attr_writer :default + + proxying :watch, :unwatch, :exists? + + def default + case @default + when Proc then @default.call + when Symbol then send(@default) + else @default + end + end + + private + def set_default + raise NotImplementedError, "Kredis type #{self.class} needs to define #set_default" + end + end + + def initialize(...) + super + + if default + watch do + set_default unless exists? + + unwatch + end + end + end +end diff --git a/lib/kredis/types.rb b/lib/kredis/types.rb index 47522c2..6ccdb2a 100644 --- a/lib/kredis/types.rb +++ b/lib/kredis/types.rb @@ -41,40 +41,40 @@ def json(key, default: nil, config: :shared, after_change: nil, expires_in: nil) end - def counter(key, expires_in: nil, config: :shared, after_change: nil) - type_from(Counter, config, key, after_change: after_change, expires_in: expires_in) + def counter(key, expires_in: nil, default: nil, config: :shared, after_change: nil) + type_from(Counter, config, key, after_change: after_change, default: default, expires_in: expires_in) end def cycle(key, values:, expires_in: nil, config: :shared, after_change: nil) type_from(Cycle, config, key, after_change: after_change, values: values, expires_in: expires_in) end - def flag(key, config: :shared, after_change: nil, expires_in: nil) - type_from(Flag, config, key, after_change: after_change, expires_in: expires_in) + def flag(key, default: nil, config: :shared, after_change: nil, expires_in: nil) + type_from(Flag, config, key, after_change: after_change, default: default, expires_in: expires_in) end def enum(key, values:, default:, config: :shared, after_change: nil) type_from(Enum, config, key, after_change: after_change, values: values, default: default) end - def hash(key, typed: :string, config: :shared, after_change: nil) - type_from(Hash, config, key, after_change: after_change, typed: typed) + def hash(key, typed: :string, default: nil, config: :shared, after_change: nil) + type_from(Hash, config, key, after_change: after_change, default: default, typed: typed) end - def list(key, typed: :string, config: :shared, after_change: nil) - type_from(List, config, key, after_change: after_change, typed: typed) + def list(key, default: nil, typed: :string, config: :shared, after_change: nil) + type_from(List, config, key, after_change: after_change, default: default, typed: typed) end - def unique_list(key, typed: :string, limit: nil, config: :shared, after_change: nil) - type_from(UniqueList, config, key, after_change: after_change, typed: typed, limit: limit) + def unique_list(key, default: nil, typed: :string, limit: nil, config: :shared, after_change: nil) + type_from(UniqueList, config, key, after_change: after_change, default: default, typed: typed, limit: limit) end - def set(key, typed: :string, config: :shared, after_change: nil) - type_from(Set, config, key, after_change: after_change, typed: typed) + def set(key, default: nil, typed: :string, config: :shared, after_change: nil) + type_from(Set, config, key, after_change: after_change, default: default, typed: typed) end - def ordered_set(key, typed: :string, limit: nil, config: :shared, after_change: nil) - type_from(OrderedSet, config, key, after_change: after_change, typed: typed, limit: limit) + def ordered_set(key, default: nil, typed: :string, limit: nil, config: :shared, after_change: nil) + type_from(OrderedSet, config, key, after_change: after_change, default: default, typed: typed, limit: limit) end def slot(key, config: :shared, after_change: nil) diff --git a/lib/kredis/types/counter.rb b/lib/kredis/types/counter.rb index daeb4a9..2b82981 100644 --- a/lib/kredis/types/counter.rb +++ b/lib/kredis/types/counter.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Kredis::Types::Counter < Kredis::Types::Proxying + prepend Kredis::DefaultValues + proxying :multi, :set, :incrby, :decrby, :get, :del, :exists? attr_accessor :expires_in @@ -26,4 +28,9 @@ def value def reset del end + + private + def set_default + increment by: default + end end diff --git a/lib/kredis/types/enum.rb b/lib/kredis/types/enum.rb index 985ba15..b5047d4 100644 --- a/lib/kredis/types/enum.rb +++ b/lib/kredis/types/enum.rb @@ -3,9 +3,13 @@ require "active_support/core_ext/object/inclusion" class Kredis::Types::Enum < Kredis::Types::Proxying - proxying :set, :get, :del, :exists? + prepend Kredis::DefaultValues - attr_accessor :values, :default + InvalidDefault = Class.new(StandardError) + + proxying :set, :get, :del, :exists?, :multi + + attr_accessor :values def initialize(...) super @@ -19,11 +23,14 @@ def value=(value) end def value - get || default + get end def reset - del + multi do + del + set_default + end end private @@ -33,4 +40,12 @@ def define_predicates_for_values define_singleton_method("#{defined_value}!") { self.value = defined_value } end end + + def set_default + if default.in?(values) || default.nil? + set default + else + raise InvalidDefault, "Default value #{default.inspect} for #{key} is not a valid option (Valid values: #{values.join(", ")})" + end + end end diff --git a/lib/kredis/types/flag.rb b/lib/kredis/types/flag.rb index b5902fd..b7c0491 100644 --- a/lib/kredis/types/flag.rb +++ b/lib/kredis/types/flag.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Kredis::Types::Flag < Kredis::Types::Proxying + prepend Kredis::DefaultValues + proxying :set, :exists?, :del attr_accessor :expires_in @@ -16,4 +18,9 @@ def marked? def remove del end + + private + def set_default + mark if default + end end diff --git a/lib/kredis/types/hash.rb b/lib/kredis/types/hash.rb index c33fb7a..5b76f47 100644 --- a/lib/kredis/types/hash.rb +++ b/lib/kredis/types/hash.rb @@ -3,6 +3,8 @@ require "active_support/core_ext/hash" class Kredis::Types::Hash < Kredis::Types::Proxying + prepend Kredis::DefaultValues + proxying :hget, :hset, :hmget, :hdel, :hgetall, :hkeys, :hvals, :del, :exists? attr_accessor :typed @@ -44,4 +46,9 @@ def keys def values strings_to_types(hvals || [], typed) end + + private + def set_default + update(**default) + end end diff --git a/lib/kredis/types/list.rb b/lib/kredis/types/list.rb index aa85150..0c1e06f 100644 --- a/lib/kredis/types/list.rb +++ b/lib/kredis/types/list.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Kredis::Types::List < Kredis::Types::Proxying + prepend Kredis::DefaultValues + proxying :lrange, :lrem, :lpush, :ltrim, :rpush, :exists?, :del attr_accessor :typed @@ -30,4 +32,9 @@ def clear def last(n = nil) n ? lrange(-n, -1) : lrange(-1, -1).first end + + private + def set_default + append default + end end diff --git a/lib/kredis/types/ordered_set.rb b/lib/kredis/types/ordered_set.rb index 05a4b3f..52a408d 100644 --- a/lib/kredis/types/ordered_set.rb +++ b/lib/kredis/types/ordered_set.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Kredis::Types::OrderedSet < Kredis::Types::Proxying + prepend Kredis::DefaultValues + proxying :multi, :zrange, :zrem, :zadd, :zremrangebyrank, :zcard, :exists?, :del attr_accessor :typed @@ -74,4 +76,8 @@ def trim(from_beginning:) zremrangebyrank(0, -(limit + 1)) end end + + def set_default + append default + end end diff --git a/lib/kredis/types/proxy.rb b/lib/kredis/types/proxy.rb index b3a1d2a..6941115 100644 --- a/lib/kredis/types/proxy.rb +++ b/lib/kredis/types/proxy.rb @@ -22,6 +22,16 @@ def multi(*args, **kwargs, &block) end end + def watch(&block) + redis.watch(key) do + block.call + end + end + + def unwatch + redis.unwatch + end + def method_missing(method, *args, **kwargs) Kredis.instrument :proxy, **log_message(method, *args, **kwargs) do failsafe do diff --git a/lib/kredis/types/scalar.rb b/lib/kredis/types/scalar.rb index 28f365d..798b87c 100644 --- a/lib/kredis/types/scalar.rb +++ b/lib/kredis/types/scalar.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true class Kredis::Types::Scalar < Kredis::Types::Proxying + prepend Kredis::DefaultValues + proxying :set, :get, :exists?, :del, :expire, :expireat - attr_accessor :typed, :default, :expires_in + attr_accessor :typed, :expires_in def value=(value) set type_to_string(value, typed), ex: expires_in @@ -38,4 +40,9 @@ def expire_in(seconds) def expire_at(datetime) expireat datetime.to_i end + + private + def set_default + self.value = default + end end diff --git a/lib/kredis/types/set.rb b/lib/kredis/types/set.rb index 870c52b..4a157b5 100644 --- a/lib/kredis/types/set.rb +++ b/lib/kredis/types/set.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Kredis::Types::Set < Kredis::Types::Proxying + prepend Kredis::DefaultValues + proxying :smembers, :sadd, :srem, :multi, :del, :sismember, :scard, :spop, :exists?, :srandmember attr_accessor :typed @@ -49,4 +51,9 @@ def sample(count = nil) strings_to_types(srandmember(count), typed) end end + + private + def set_default + add default + end end diff --git a/test/attributes_test.rb b/test/attributes_test.rb index fbd861b..a655abf 100644 --- a/test/attributes_test.rb +++ b/test/attributes_test.rb @@ -12,25 +12,38 @@ class Person kredis_list :names kredis_list :names_with_custom_key_via_lambda, key: ->(p) { "person:#{p.id}:names_customized" } kredis_list :names_with_custom_key_via_method, key: :generate_key + kredis_list :names_with_default_via_lambda, default: ->(p) { ["Random", p.name] } kredis_unique_list :skills, limit: 2 + kredis_unique_list :skills_with_default_via_lambda, default: ->(p) { ["Random", "Random", p.name] } kredis_ordered_set :reading_list, limit: 2 kredis_flag :special kredis_flag :temporary_special, expires_in: 1.second kredis_string :address + kredis_string :address_with_default_via_lambda, default: ->(p) { p.name } kredis_integer :age + kredis_integer :age_with_default_via_lambda, default: ->(p) { Date.today.year - p.birthdate.year } kredis_decimal :salary + kredis_decimal :salary_with_default_via_lambda, default: ->(p) { p.hourly_wage * 40 * 52 } kredis_datetime :last_seen_at + kredis_datetime :last_seen_at_with_default_via_lambda, default: ->(p) { p.last_login } kredis_float :height + kredis_float :height_with_default_via_lambda, default: ->(p) { JSON.parse(p.anthropometry)["height"] } kredis_enum :morning, values: %w[ bright blue black ], default: "bright" + kredis_enum :eye_color_with_default_via_lambda, values: %w[ hazel blue brown ], default: ->(p) { { ha: "hazel", bl: "blue", br: "brown" }[p.eye_color.to_sym] } kredis_slot :attention kredis_slots :meetings, available: 3 kredis_set :vacations + kredis_set :vacations_with_default_via_lambda, default: ->(p) { JSON.parse(p.vacation_destinations).map { |location| location["city"] } } kredis_json :settings + kredis_json :settings_with_default_via_lambda, default: ->(p) { JSON.parse(p.anthropometry).merge(eye_color: p.eye_color) } kredis_counter :amount + kredis_counter :amount_with_default_via_lambda, default: ->(p) { Date.today.year - p.birthdate.year } kredis_counter :expiring_amount, expires_in: 1.second kredis_string :temporary_password, expires_in: 1.second kredis_hash :high_scores, typed: :integer + kredis_hash :high_scores_with_default_via_lambda, typed: :integer, default: ->(p) { { high_score: JSON.parse(p.scores).max } } kredis_boolean :onboarded + kredis_boolean :adult_with_default_via_lambda, default: ->(p) { Date.today.year - p.birthdate.year >= 18 } def self.name "Person" @@ -40,6 +53,41 @@ def id 8 end + def name + "Jason" + end + + def birthdate + Date.today - 25.years + end + + def anthropometry + { height: 73.2, weight: 182.4 }.to_json + end + + def eye_color + "ha" + end + + def scores + [10, 28, 2, 7].to_json + end + + def hourly_wage + 15.26 + end + + def last_login + Time.new(2002, 10, 31, 2, 2, 2, "+02:00") + end + + def vacation_destinations + [ + { city: "Paris", region: "Île-de-France", country: "FR" }, + { city: "Paris", region: "Texas", country: "US" } + ].to_json + end + private def generate_key "some-generated-key" @@ -86,6 +134,11 @@ class AttributesTest < ActiveSupport::TestCase assert_equal %w[ david kasper ], Kredis.redis.lrange("some-generated-key", 0, -1) end + test "list with default proc value" do + assert_equal %w[ Random Jason ], @person.names_with_default_via_lambda.elements + assert_equal %w[ Random Jason ], Kredis.redis.lrange("people:8:names_with_default_via_lambda", 0, -1) + end + test "unique list" do @person.skills.prepend(%w[ trolling photography ]) @person.skills.prepend("racing") @@ -93,6 +146,11 @@ class AttributesTest < ActiveSupport::TestCase assert_equal %w[ racing photography ], @person.skills.elements end + test "unique list with default proc value" do + assert_equal %w[ Random Jason ], @person.skills_with_default_via_lambda.elements + assert_equal %w[ Random Jason ], Kredis.redis.lrange("people:8:skills_with_default_via_lambda", 0, -1) + end + test "ordered set" do @person.reading_list.prepend(%w[ rework shapeup remote ]) assert_equal %w[ remote shapeup ], @person.reading_list.elements @@ -119,30 +177,57 @@ class AttributesTest < ActiveSupport::TestCase assert_not @person.address.assigned? end + test "string with default proc value" do + assert_equal "Jason", @person.address_with_default_via_lambda.to_s + + @person.address.clear + assert_not @person.address.assigned? + end + test "integer" do @person.age.value = 41 assert_equal 41, @person.age.value assert_equal "41", @person.age.to_s end + test "integer with default proc value" do + assert_equal 25, @person.age_with_default_via_lambda.value + assert_equal "25", @person.age_with_default_via_lambda.to_s + end + test "decimal" do @person.salary.value = 10000.07 assert_equal 10000.07, @person.salary.value assert_equal "0.1000007e5", @person.salary.to_s end + test "decimal with default proc value" do + assert_equal 31_740.80.to_d, @person.salary_with_default_via_lambda.value + assert_equal "0.317408e5", @person.salary_with_default_via_lambda.to_s + end + test "float" do @person.height.value = 1.85 assert_equal 1.85, @person.height.value assert_equal "1.85", @person.height.to_s end - test "datetime" do + test "float with default proc value" do + assert_not_equal 73.2, Kredis.redis.get("people:8:height_with_default_via_lambda") + assert_equal 73.2, @person.height_with_default_via_lambda.value + assert_equal "73.2", @person.height_with_default_via_lambda.to_s + end + + test "datetime with default proc value" do freeze_time @person.last_seen_at.value = Time.now assert_equal Time.now, @person.last_seen_at.value end + test "datetime" do + assert_equal Time.new(2002, 10, 31, 2, 2, 2, "+02:00"), @person.last_seen_at_with_default_via_lambda.value + end + test "slot" do assert @person.attention.reserve assert_not @person.attention.available? @@ -205,6 +290,11 @@ class AttributesTest < ActiveSupport::TestCase assert @person.morning.bright? end + test "enum with default proc value" do + assert @person.eye_color_with_default_via_lambda.hazel? + end + + test "set" do @person.vacations.add "paris" @person.vacations.add "paris" @@ -220,11 +310,23 @@ class AttributesTest < ActiveSupport::TestCase assert_equal "paris", @person.vacations.take end + test "set with default proc value" do + assert_equal [ "Paris" ], @person.vacations_with_default_via_lambda.members + assert_equal [ "Paris" ], Kredis.redis.smembers("people:8:vacations_with_default_via_lambda") + end + test "json" do @person.settings.value = { "color" => "red", "count" => 2 } assert_equal({ "color" => "red", "count" => 2 }, @person.settings.value) end + test "json with default proc value" do + expect = { "height" => 73.2, "weight" => 182.4, "eye_color" => "ha" } + assert_equal expect, @person.settings_with_default_via_lambda.value + assert_equal expect.to_json, Kredis.redis.get("people:8:settings_with_default_via_lambda") + end + + test "counter" do @person.amount.increment assert_equal 1, @person.amount.value @@ -239,6 +341,13 @@ class AttributesTest < ActiveSupport::TestCase end end + test "counter with default proc value" do + @person.amount_with_default_via_lambda.increment + assert_equal 26, @person.amount_with_default_via_lambda.value + @person.amount_with_default_via_lambda.decrement + assert_equal 25, @person.amount_with_default_via_lambda.value + end + test "hash" do @person.high_scores.update(space_invaders: 100, pong: 42) assert_equal({ "space_invaders" => 100, "pong" => 42 }, @person.high_scores.to_h) @@ -246,6 +355,10 @@ class AttributesTest < ActiveSupport::TestCase assert_equal([ 100, 42 ], @person.high_scores.values) end + test "hash with default proc value" do + assert_equal({ "high_score" => 28 }, @person.high_scores_with_default_via_lambda.to_h) + end + test "boolean" do @person.onboarded.value = true assert @person.onboarded.value @@ -254,6 +367,10 @@ class AttributesTest < ActiveSupport::TestCase assert_not @person.onboarded.value end + test "boolean with default proc value" do + assert @person.adult_with_default_via_lambda.value + end + test "missing id to constrain key" do assert_raise NotImplementedError do MissingIdPerson.new.anything diff --git a/test/types/counter_test.rb b/test/types/counter_test.rb index 4e4c607..3b6c8bc 100644 --- a/test/types/counter_test.rb +++ b/test/types/counter_test.rb @@ -80,4 +80,43 @@ class CounterTest < ActiveSupport::TestCase @counter.increment assert @counter.exists? end + + test "default value" do + @counter = Kredis.counter "mycounter", default: 10 + assert_equal 10, @counter.value + end + + test "expiring counter with default" do + @counter = Kredis.counter "mycounter", default: ->() { 10 }, expires_in: 1.second + + @counter.increment + assert_equal 11, @counter.value + + sleep 0.5.seconds + + @counter.increment + assert_equal 12, @counter.value + + sleep 0.5.seconds + + # Defaults are only set on initialization + assert_equal 0, @counter.value + end + + test "default via proc" do + @counter = Kredis.counter "mycounter", default: ->() { 10 } + assert_equal 10, @counter.value + @counter.decrement + assert_equal 9, @counter.value + end + + test "concurrent initialization with default" do + 5.times.map do + Thread.new do + Kredis.counter("mycounter", default: 5).increment + end + end.each(&:join) + + assert_equal 10, Kredis.counter("mycounter").value + end end diff --git a/test/types/enum_test.rb b/test/types/enum_test.rb index d433f8d..d89f882 100644 --- a/test/types/enum_test.rb +++ b/test/types/enum_test.rb @@ -9,6 +9,22 @@ class EnumTest < ActiveSupport::TestCase assert_equal "one", @enum.value end + test "default via proc" do + @enum = Kredis.enum "myenum2", values: %w[ one two three ], default: ->() { "two" } + assert_equal "two", @enum.value + end + + test "default can be nil" do + enum = Kredis.enum "myenum3", values: [ 1, 2, 3 ], default: nil + assert_nil enum.value + end + + test "default value has to be valid if not nil" do + assert_raises Kredis::Types::Enum::InvalidDefault do + Kredis.enum "myenum4", values: [ 1, 2, 3 ], default: 4 + end + end + test "predicates" do assert @enum.one? @@ -39,9 +55,10 @@ class EnumTest < ActiveSupport::TestCase end test "exists?" do - assert_not @enum.exists? + enum = Kredis.enum "numbers", values: %w[ one two three ], default: nil + assert_not enum.exists? - @enum.value = "one" - assert @enum.exists? + enum.value = "one" + assert enum.exists? end end diff --git a/test/types/hash_test.rb b/test/types/hash_test.rb index ab9e5ad..67b3e1d 100644 --- a/test/types/hash_test.rb +++ b/test/types/hash_test.rb @@ -92,4 +92,36 @@ class HashTest < ActiveSupport::TestCase @hash[:key] = :value assert @hash.exists? end + + test "default value" do + @hash = Kredis.hash "myhash", typed: :integer, default: { space_invaders: "100", pong: "42" } + assert_equal({ "space_invaders" => 100, "pong" => 42 }, @hash.to_h) + assert_equal(%w[ space_invaders pong ], @hash.keys) + assert_equal([100, 42], @hash.values) + assert_equal(100, @hash["space_invaders"]) + assert_equal([100, 42], @hash.values_at("space_invaders", "pong")) + end + + test "update with default" do + @hash = Kredis.hash "myhash", typed: :integer, default: { space_invaders: "100", pong: "42" } + @hash.update(ping: "54") + assert_equal(%w[ space_invaders pong ping ], @hash.keys) + end + + test "[]= with default" do + @hash = Kredis.hash "myhash", typed: :integer, default: { space_invaders: "100", pong: "42" } + @hash[:ping] = "54" + assert_equal(%w[ space_invaders pong ping ], @hash.keys) + end + + test "delete with default" do + @hash = Kredis.hash "myhash", typed: :integer, default: { space_invaders: "100", pong: "42" } + @hash.delete(:pong) + assert_equal(%w[ space_invaders ], @hash.keys) + end + + test "default via proc" do + @hash = Kredis.hash "myhash", typed: :integer, default: ->() { { space_invaders: "100", pong: "42" } } + assert_equal({ "space_invaders" => 100, "pong" => 42 }, @hash.to_h) + end end diff --git a/test/types/list_test.rb b/test/types/list_test.rb index 93626ea..7507548 100644 --- a/test/types/list_test.rb +++ b/test/types/list_test.rb @@ -75,4 +75,60 @@ class ListTest < ActiveSupport::TestCase @list.ltrim(-3, -2) assert_equal %w[ 2 3 ], @list.elements end + + + test "default" do + @list = Kredis.list "mylist", default: %w[ 1 2 3 ] + + assert_equal %w[ 1 2 3 ], @list.elements + end + + test "default empty array" do + @list = Kredis.list "mylist", default: [] + + assert_equal [], @list.elements + end + + test "default with nil" do + @list = Kredis.list "mylist", default: nil + + assert_equal [], @list.elements + end + + test "default via proc" do + @list = Kredis.list "mylist", default: ->() { %w[ 1 2 3 ] } + + assert_equal %w[ 1 2 3 ], @list.elements + end + + test "append with default" do + @list = Kredis.list "mylist", default: ->() { %w[ 1 ] } + @list.append(%w[ 2 3 ]) + @list.append(4) + assert_equal %w[ 1 2 3 4 ], @list.elements + end + + test "prepend with default" do + @list = Kredis.list "mylist", default: ->() { %w[ 1 ] } + @list.prepend(%w[ 2 3 ]) + @list.prepend(4) + assert_equal %w[ 4 3 2 1 ], @list.elements + end + + test "remove with default" do + @list = Kredis.list "mylist", default: ->() { %w[ 1 2 3 4 ] } + @list.remove(%w[ 1 2 ]) + @list.remove(3) + assert_equal %w[ 4 ], @list.elements + end + + test "concurrent initialization with default" do + 5.times.map do |i| + Thread.new do + Kredis.list("mylist", default: [ 10, 20, 30 ]).append(i) + end + end.each(&:join) + + assert_equal [ 0, 1, 2, 3, 4, 10, 20, 30 ], Kredis.list("mylist", typed: :integer).to_a.sort + end end diff --git a/test/types/scalar_test.rb b/test/types/scalar_test.rb index 5bd3bb9..41bdf60 100644 --- a/test/types/scalar_test.rb +++ b/test/types/scalar_test.rb @@ -116,11 +116,36 @@ class ScalarTest < ActiveSupport::TestCase assert_equal 8, integer.value assert_equal "8", integer.value.to_s + integer.clear + + json = Kredis.json "myscalar", default: { one: 1, string: "hello" } + assert_equal({ "one" => 1, "string" => "hello" }, json.value) + end + + test "default via proc" do + integer = Kredis.scalar "myscalar", typed: :integer, default: ->() { 8 } + assert_equal 8, integer.value + + integer.value = 5 + assert_equal 5, integer.value + + integer.clear + assert_equal 8, integer.value - json = Kredis.json "myscalar", default: { "one" => 1, "string" => "hello" } + integer.clear + + json = Kredis.json "myscalar", default: ->() { { one: 1, string: "hello" } } assert_equal({ "one" => 1, "string" => "hello" }, json.value) end + test "does not cache proc results after clear" do + hex = Kredis.scalar "myscalar", default: ->() { SecureRandom.hex } + original_default_value = hex.value + assert_equal original_default_value, hex.value + hex.clear + assert_not_equal original_default_value, hex.value + end + test "returns default when failing open" do integer = Kredis.scalar "myscalar", typed: :integer, default: 8 integer.value = 42 diff --git a/test/types/set_test.rb b/test/types/set_test.rb index 0d0c5b4..84d2947 100644 --- a/test/types/set_test.rb +++ b/test/types/set_test.rb @@ -21,7 +21,7 @@ class SetTest < ActiveSupport::TestCase test "remove" do @set.add(%w[ 1 2 3 4 ]) - @set.remove(%w[ 2 3 ]) + @set.remove([%w[ 2 3 ]]) @set.remove("1") assert_equal %w[ 4 ], @set.members end @@ -100,4 +100,45 @@ class SetTest < ActiveSupport::TestCase assert @set.sample.in?([ 1.5, 2.7 ]) assert_equal [ 1.5, 2.7 ], @set.sample(2).sort end + + + test "default" do + @set = Kredis.set "mylist", default: %w[ 1 2 3 ] + assert_equal %w[ 1 2 3 ], @set.members + end + + test "default is an empty array" do + @set = Kredis.set "mylist", default: [] + assert_equal [], @set.members + end + + test "default is nil" do + @set = Kredis.set "mylist", default: nil + assert_equal [], @set.members + end + + test "default via proc" do + @set = Kredis.set "mylist", default: -> () { %w[ 3 3 1 2 ] } + assert_equal %w[ 1 2 3 ], @set.members + end + + test "add with default" do + @set = Kredis.set "mylist", typed: :integer, default: -> () { %w[ 1 2 3 ] } + @set.add(%w[ 5 6 7 ]) + assert_equal [1, 2, 3, 5, 6, 7], @set.members + end + + test "remove with default" do + @set = Kredis.set "mylist", default: -> () { %w[ 1 2 3 4 ] } + @set.remove(%w[ 2 3 ]) + @set.remove("1") + assert_equal %w[ 4 ], @set.members + end + + test "replace with default" do + @set = Kredis.set "mylist", typed: :integer, default: -> () { %w[ 1 2 3 ] } + @set.add(%w[ 5 6 7 ]) + @set.replace(%w[ 8 9 10 ]) + assert_equal [ 8, 9, 10 ], @set.members + end end diff --git a/test/types/unique_list_test.rb b/test/types/unique_list_test.rb index 83748f4..8ac91ed 100644 --- a/test/types/unique_list_test.rb +++ b/test/types/unique_list_test.rb @@ -75,4 +75,22 @@ class UniqueListTest < ActiveSupport::TestCase @list.prepend(%w[ 1 1 1 ]) assert_equal %w[ 1 ], @list.elements end + + test "default" do + @list = Kredis.unique_list "myuniquelist", default: %w[ 1 2 3 ] + + assert_equal %w[ 1 2 3 ], @list.elements + end + + test "default via proc" do + @list = Kredis.unique_list "myuniquelist", default: ->() { %w[ 1 2 3 3 ] } + + assert_equal %w[ 1 2 3 ], @list.elements + end + + test "prepend with default" do + @list = Kredis.unique_list "myuniquelist", default: %w[ 1 2 3 ] + @list.prepend(%w[ 6 7 8 ]) + assert_equal %w[ 8 7 6 1 2 3 ], @list.elements + end end