diff --git a/lib/kredis/attributes.rb b/lib/kredis/attributes.rb index 21c3ce6..001a349 100644 --- a/lib/kredis/attributes.rb +++ b/lib/kredis/attributes.rb @@ -2,56 +2,56 @@ module Kredis::Attributes extend ActiveSupport::Concern class_methods do - def kredis_proxy(name, key: nil, config: :shared, after_change: nil) - kredis_connection_with __method__, name, key, config: config, after_change: after_change + def kredis_proxy(name, key: nil, default: nil, config: :shared, after_change: nil) + kredis_connection_with __method__, name, key, default: default, 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_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_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_slot(name, key: nil, config: :shared, after_change: nil) @@ -62,16 +62,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 @@ -84,6 +84,7 @@ def kredis_connection_with(method, name, key, **options) if instance_variable_defined?(ivar_symbol) instance_variable_get(ivar_symbol) else + options.merge!(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 @@ -115,4 +116,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/type/json.rb b/lib/kredis/type/json.rb index 4183d5f..77b8a8c 100644 --- a/lib/kredis/type/json.rb +++ b/lib/kredis/type/json.rb @@ -8,7 +8,11 @@ def type end def cast_value(value) - JSON.load(value) + if value.is_a? Hash + value.stringify_keys + else + JSON.load(value) + end end def serialize(value) diff --git a/lib/kredis/types.rb b/lib/kredis/types.rb index 16ad906..385c31f 100644 --- a/lib/kredis/types.rb +++ b/lib/kredis/types.rb @@ -1,8 +1,8 @@ module Kredis::Types autoload :CallbacksProxy, "kredis/types/callbacks_proxy" - def proxy(key, config: :shared, after_change: nil) - type_from(Proxy, config, key, after_change: after_change) + def proxy(key, default: nil, config: :shared, after_change: nil) + type_from(Proxy, config, key, after_change: after_change, default: default) end @@ -39,36 +39,36 @@ 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 slot(key, config: :shared, after_change: nil) diff --git a/lib/kredis/types/counter.rb b/lib/kredis/types/counter.rb index cacebb3..dd2befb 100644 --- a/lib/kredis/types/counter.rb +++ b/lib/kredis/types/counter.rb @@ -4,24 +4,27 @@ class Kredis::Types::Counter < Kredis::Types::Proxying attr_accessor :expires_in def increment(by: 1) - multi do - set 0, ex: expires_in, nx: true - incrby by - end[-1] + init_default_in_multi { incrby by } end def decrement(by: 1) - multi do - set 0, ex: expires_in, nx: true - decrby by - end[-1] + init_default_in_multi { decrby by } end def value - get.to_i + (get || default).to_i end def reset del end + + private + def set_default(value) + set value.to_i, ex: expires_in, nx: true + end + + def default + super.to_i + end end diff --git a/lib/kredis/types/enum.rb b/lib/kredis/types/enum.rb index b14403f..046a808 100644 --- a/lib/kredis/types/enum.rb +++ b/lib/kredis/types/enum.rb @@ -3,7 +3,7 @@ class Kredis::Types::Enum < Kredis::Types::Proxying proxying :set, :get, :del, :exists? - attr_accessor :values, :default + attr_accessor :values def initialize(...) super @@ -17,7 +17,7 @@ def value=(value) end def value - get || default + get || default.presence_in(values) end def reset diff --git a/lib/kredis/types/hash.rb b/lib/kredis/types/hash.rb index bc1b990..08e4fa1 100644 --- a/lib/kredis/types/hash.rb +++ b/lib/kredis/types/hash.rb @@ -1,28 +1,37 @@ require "active_support/core_ext/hash" class Kredis::Types::Hash < Kredis::Types::Proxying - proxying :hget, :hset, :hmget, :hdel, :hgetall, :hkeys, :hvals, :del, :exists? + ZERO_FIELDS_ADDED = 0 + + proxying :hset, :hdel, :hgetall, :del, :exists?, :multi, :callnx attr_accessor :typed def [](key) - string_to_type(hget(key), typed) + string_to_type(entries[key], typed) end def []=(key, value) update key => value end + def update(**entries) - hset entries.transform_values{ |val| type_to_string(val, typed) } if entries.flatten.any? + return ZERO_FIELDS_ADDED if entries.flatten.blank? + + init_default_in_multi do + hset entries.transform_values{ |val| type_to_string(val, typed) } + end end def values_at(*keys) - strings_to_types(hmget(keys) || [], typed) + strings_to_types(entries.values_at(*keys) || [], typed) end def delete(*keys) - hdel keys if keys.flatten.any? + return ZERO_FIELDS_ADDED if keys.flatten.blank? + + init_default_in_multi { hdel keys } end def remove @@ -31,15 +40,20 @@ def remove alias clear remove def entries - (hgetall || {}).transform_values { |val| string_to_type(val, typed) }.with_indifferent_access + (hgetall.presence || default || {}).transform_values { |val| string_to_type(val, typed) }.with_indifferent_access end alias to_h entries def keys - hkeys || [] + entries.keys || [] end def values - strings_to_types(hvals || [], typed) + strings_to_types(entries.values || [], typed) end + + private + def set_default(entries) + callnx(:hset, entries.transform_values{ |val| type_to_string(val, typed) }) + end end diff --git a/lib/kredis/types/list.rb b/lib/kredis/types/list.rb index cbda3bd..7d31a19 100644 --- a/lib/kredis/types/list.rb +++ b/lib/kredis/types/list.rb @@ -1,27 +1,45 @@ class Kredis::Types::List < Kredis::Types::Proxying - proxying :lrange, :lrem, :lpush, :rpush, :exists?, :del + proxying :lrange, :lrem, :lpush, :rpush, :exists?, :del, :callnx attr_accessor :typed def elements - strings_to_types(lrange(0, -1) || [], typed) + values = init_default_in_multi { lrange(0, -1) } + strings_to_types(values || [], typed) end alias to_a elements def remove(*elements) - types_to_strings(elements, typed).each { |element| lrem 0, element } + return [] if elements.flatten.blank? + + init_default_in_multi do + types_to_strings(elements, typed).each { |element| lrem 0, element } + end end def prepend(*elements) - lpush types_to_strings(elements, typed) if elements.flatten.any? + return self.elements.count if elements.flatten.blank? + + init_default_in_multi do + lpush types_to_strings(elements, typed) + end end def append(*elements) - rpush types_to_strings(elements, typed) if elements.flatten.any? + return self.elements.count if elements.flatten.blank? + + init_default_in_multi do + rpush types_to_strings(elements, typed) + end end alias << append def clear del end + + private + def set_default(elements) + callnx(:rpush, types_to_strings(Array(elements), typed)) + end end diff --git a/lib/kredis/types/proxy.rb b/lib/kredis/types/proxy.rb index b609a6f..8d61c51 100644 --- a/lib/kredis/types/proxy.rb +++ b/lib/kredis/types/proxy.rb @@ -12,6 +12,8 @@ def initialize(redis, key, **options) end def multi(*args, **kwargs, &block) + return block.call if self.pipeline # return and execute block for nested multi pipeline + redis.multi(*args, **kwargs) do |pipeline| self.pipeline = pipeline block.call @@ -28,6 +30,21 @@ def method_missing(method, *args, **kwargs) end end + CALLNX = <<~LUA + if redis.call("exists", KEYS[1]) == 0 then + redis.call("%{method}", KEYS[1], unpack(ARGV)) + end + LUA + def callnx(method, values) + safe_method_name = method.to_s.gsub(/[^a-z_]/, '_') + cmd = format(CALLNX, method: safe_method_name) + Kredis.instrument :proxy, **log_message(:callnx, *([safe_method_name] + Array(values))) do + failsafe do + redis.eval cmd, Array(key), Array(values).flatten + end + end + end + private def redis pipeline || @redis diff --git a/lib/kredis/types/proxying.rb b/lib/kredis/types/proxying.rb index 3607f0f..89979bc 100644 --- a/lib/kredis/types/proxying.rb +++ b/lib/kredis/types/proxying.rb @@ -1,14 +1,15 @@ require "active_support/core_ext/module/delegation" class Kredis::Types::Proxying - attr_accessor :proxy, :key + attr_accessor :proxy, :redis, :key def self.proxying(*commands) delegate *commands, to: :proxy end def initialize(redis, key, **options) - @key = key + @redis, @key = redis, key + @default = options.delete(:default) @proxy = Kredis::Types::Proxy.new(redis, key) options.each { |key, value| send("#{key}=", value) } end @@ -19,4 +20,28 @@ def failsafe(returning: nil, &block) private delegate :type_to_string, :string_to_type, :types_to_strings, :strings_to_types, to: :Kredis + + def default + if @default.is_a?(Proc) + @default.call + else + @default + end + end + + def init_default_in_multi(&block) + if (default_value = default).blank? + block.call + else + multi_results = proxy.multi do + set_default(default_value) + block.call + end + Array(multi_results)[-1] + end + end + + def set_default(value) + raise NotImplementedError, "kredis type needs to define #set_default" + end end diff --git a/lib/kredis/types/scalar.rb b/lib/kredis/types/scalar.rb index 03ddf52..8ad44f0 100644 --- a/lib/kredis/types/scalar.rb +++ b/lib/kredis/types/scalar.rb @@ -1,24 +1,18 @@ class Kredis::Types::Scalar < Kredis::Types::Proxying 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 end def value - value_after_casting = string_to_type(get, typed) - - if value_after_casting.nil? - default - else - value_after_casting - end + string_to_type(init_default_in_multi{ get }, typed) end def to_s - get || default&.to_s + value.to_s end def assigned? @@ -36,4 +30,9 @@ def expire_in(seconds) def expire_at(datetime) expireat datetime.to_i end + + private + def set_default(value) + set type_to_string(value, typed), ex: expires_in, nx: true + end end diff --git a/lib/kredis/types/set.rb b/lib/kredis/types/set.rb index 5f7a1e0..0ff514a 100644 --- a/lib/kredis/types/set.rb +++ b/lib/kredis/types/set.rb @@ -1,42 +1,58 @@ class Kredis::Types::Set < Kredis::Types::Proxying - proxying :smembers, :sadd, :srem, :multi, :del, :sismember, :scard, :spop, :exists? + proxying :smembers, :sadd, :srem, :multi, :del, :sismember, :scard, :spop, :exists?, :callnx attr_accessor :typed def members - strings_to_types(smembers || [], typed).sort + values = init_default_in_multi { smembers } + strings_to_types(values || [], typed).sort end alias to_a members def add(*members) - sadd types_to_strings(members, typed) if members.flatten.any? + return size if members.flatten.blank? + + init_default_in_multi do + sadd types_to_strings(members, typed) + end end alias << add def remove(*members) - srem types_to_strings(members, typed) if members.flatten.any? + return size if members.flatten.blank? + + init_default_in_multi do + srem types_to_strings(members, typed) + end end def replace(*members) + return size if members.flatten.blank? + multi do del add members - end + end[-1] end def include?(member) - sismember type_to_string(member, typed) + init_default_in_multi { sismember type_to_string(member, typed) } end def size - scard.to_i + init_default_in_multi { scard }.to_i end def take - spop + init_default_in_multi { spop } end def clear del end + + private + def set_default(members) + callnx(:sadd, types_to_strings(Array(members), typed)) + end end diff --git a/lib/kredis/types/unique_list.rb b/lib/kredis/types/unique_list.rb index eb4bbcc..1b9f775 100644 --- a/lib/kredis/types/unique_list.rb +++ b/lib/kredis/types/unique_list.rb @@ -26,4 +26,9 @@ def append(elements) end end alias << append + + private + def set_default(elements) + callnx(:rpush, types_to_strings(Array(elements).uniq, typed)) + end end diff --git a/test/attributes_test.rb b/test/attributes_test.rb index 5f4df6d..4aaaa05 100644 --- a/test/attributes_test.rb +++ b/test/attributes_test.rb @@ -10,24 +10,37 @@ 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_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" @@ -37,6 +50,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" @@ -83,6 +131,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") @@ -90,6 +143,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 "flag" do assert_not @person.special? @@ -111,30 +169,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? @@ -197,6 +282,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" @@ -212,11 +302,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 @@ -231,6 +333,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) @@ -238,6 +347,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 @@ -246,6 +359,10 @@ class AttributesTest < ActiveSupport::TestCase refute @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 821b7b5..85e90a1 100644 --- a/test/types/counter_test.rb +++ b/test/types/counter_test.rb @@ -78,4 +78,32 @@ 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.6.seconds + + assert_equal 10, @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 end diff --git a/test/types/enum_test.rb b/test/types/enum_test.rb index 84c587b..7224065 100644 --- a/test/types/enum_test.rb +++ b/test/types/enum_test.rb @@ -7,6 +7,18 @@ class EnumTest < ActiveSupport::TestCase assert_equal "one", @enum.value end + test "default via proc" do + @enum = Kredis.enum "myenum", values: %w[ one two three ], default: ->() { "two" } + assert_equal "two", @enum.value + end + + test "does not set default for invalid option" do + enum = Kredis.enum "myenum1", values: [ 1, 2, 3 ], default: ->() { nil } + assert_nil enum.value + enum = Kredis.enum "myenum2", values: [ 1, 2, 3 ], default: ->() { 4 } + assert_nil enum.value + end + test "predicates" do assert @enum.one? diff --git a/test/types/hash_test.rb b/test/types/hash_test.rb index 3f607f9..5a5b1b8 100644 --- a/test/types/hash_test.rb +++ b/test/types/hash_test.rb @@ -18,7 +18,15 @@ class HashTest < ActiveSupport::TestCase end test "update" do - @hash.update(key: :value) + update_count = @hash.update(key: :value) + assert_equal(1, update_count) + + update_count = @hash.update(key: :value) + assert_equal(0, update_count) + + update_count = @hash.update + assert_equal(0, update_count) + @hash.update("key2" => "value2", "key3" => "value3") assert_equal({ "key" => "value", "key2" => "value2", "key3" => "value3" }, @hash.to_h) end @@ -33,9 +41,17 @@ class HashTest < ActiveSupport::TestCase @hash.update("key2" => "value2", "key3" => "value3") assert_equal({ "key" => "value", "key2" => "value2", "key3" => "value3" }, @hash.to_h) - @hash.delete("key") + delete_count = @hash.delete("key") + assert_equal(1, delete_count) + assert_equal({ "key2" => "value2", "key3" => "value3" }, @hash.to_h) + + delete_count = @hash.delete("bogus") + assert_equal(0, delete_count) assert_equal({ "key2" => "value2", "key3" => "value3" }, @hash.to_h) + delete_count = @hash.delete + assert_equal(0, delete_count) + @hash.delete("key2", "key3") assert_equal({}, @hash.to_h) end @@ -90,4 +106,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 7e7aa5f..f8b83eb 100644 --- a/test/types/list_test.rb +++ b/test/types/list_test.rb @@ -11,8 +11,10 @@ class ListTest < ActiveSupport::TestCase end test "append nothing" do - @list.append(%w[ 1 2 3 ]) - @list.append([]) + list_length = @list.append(%w[ 1 2 3 ]) + assert_equal 3, list_length + list_length = @list.append([]) + assert_equal 3, list_length assert_equal %w[ 1 2 3 ], @list.to_a end @@ -23,8 +25,10 @@ class ListTest < ActiveSupport::TestCase end test "prepend nothing" do - @list.prepend("1", "2", "3") - @list.prepend([]) + list_length = @list.prepend("1", "2", "3") + assert_equal 3, list_length + list_length = @list.prepend([]) + assert_equal 3, list_length assert_equal %w[ 3 2 1 ], @list.elements end @@ -35,6 +39,16 @@ class ListTest < ActiveSupport::TestCase assert_equal %w[ 4 ], @list.elements end + test "remove nothing" do + @list.append(%w[ 1 2 3 4 ]) + @list.remove(%w[ 1 2 ]) + removed_items = @list.remove(3) + assert_equal %w[ 3 ], removed_items + removed_items = @list.remove + assert_equal [], removed_items + assert_equal %w[ 4 ], @list.elements + end + test "clear" do @list.append(%w[ 1 2 3 4 ]) @list.clear @@ -57,4 +71,49 @@ class ListTest < ActiveSupport::TestCase @list.append(%w[ 1 2 3 ]) assert @list.exists? 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 end diff --git a/test/types/scalar_test.rb b/test/types/scalar_test.rb index 03a3ddc..edf91f9 100644 --- a/test/types/scalar_test.rb +++ b/test/types/scalar_test.rb @@ -95,11 +95,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 - json = Kredis.json "myscalar", default: { "one" => 1, "string" => "hello" } + integer.clear + assert_equal 8, integer.value + + 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 b536262..ba665be 100644 --- a/test/types/set_test.rb +++ b/test/types/set_test.rb @@ -12,8 +12,10 @@ class SetTest < ActiveSupport::TestCase end test "add nothing" do - @set.add(%w[ 1 2 3 ]) - @set.add([]) + set_size = @set.add(%w[ 1 2 3 ]) + assert_equal 3, set_size + set_size = @set.add([]) + assert_equal 3, set_size assert_equal %w[ 1 2 3 ], @set.to_a end @@ -25,8 +27,10 @@ class SetTest < ActiveSupport::TestCase end test "remove nothing" do - @set.add(%w[ 1 2 3 4 ]) - @set.remove([]) + set_size = @set.add(%w[ 1 2 3 4 ]) + assert_equal 4, set_size + set_size = @set.remove([]) + assert_equal 4, set_size assert_equal %w[ 1 2 3 4 ], @set.members end @@ -36,6 +40,15 @@ class SetTest < ActiveSupport::TestCase assert_equal %w[ 5 6 ], @set.members end + test "replace nothing" do + @set.add(%w[ 1 2 3 4 ]) + set_size = @set.replace(%w[ 5 6 ]) + assert_equal 2, set_size + set_size = @set.replace + assert_equal 2, set_size + assert_equal %w[ 5 6 ], @set.members + end + test "include" do @set.add("1", "2", "3", "4") assert @set.include?("1") @@ -88,4 +101,44 @@ class SetTest < ActiveSupport::TestCase @set.add(%w[ 1 2 3 ]) assert @set.exists? 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 [1, 2, 3, 8, 9, 10], @set.members + end end diff --git a/test/types/unique_list_test.rb b/test/types/unique_list_test.rb index 17762db..fc03b2f 100644 --- a/test/types/unique_list_test.rb +++ b/test/types/unique_list_test.rb @@ -73,4 +73,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