Skip to content

Commit

Permalink
Default values on initialization (rails#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
lewispb authored Aug 6, 2023
1 parent 916f6ef commit cc15c2f
Show file tree
Hide file tree
Showing 22 changed files with 557 additions and 66 deletions.
53 changes: 40 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:

Expand All @@ -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
Expand All @@ -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`
Expand Down
1 change: 1 addition & 0 deletions lib/kredis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
65 changes: 37 additions & 28 deletions lib/kredis/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
36 changes: 36 additions & 0 deletions lib/kredis/default_values.rb
Original file line number Diff line number Diff line change
@@ -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
28 changes: 14 additions & 14 deletions lib/kredis/types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions lib/kredis/types/counter.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -26,4 +28,9 @@ def value
def reset
del
end

private
def set_default
increment by: default
end
end
23 changes: 19 additions & 4 deletions lib/kredis/types/enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
7 changes: 7 additions & 0 deletions lib/kredis/types/flag.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,4 +18,9 @@ def marked?
def remove
del
end

private
def set_default
mark if default
end
end
Loading

0 comments on commit cc15c2f

Please sign in to comment.