Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callbacks proxy #35

Merged
merged 12 commits into from
Jul 3, 2021
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ person.morning.value = "blue" # => SET people:1:morning
true == person.morning.blue? # => GET people:1:morning
```

You can also define `after_change` callbacks that trigger on mutations:

```ruby
class Person < ApplicationRecord
kredis_list :names, after_change: ->(p) { }
kredis_unique_list :skills, limit: 2, after_change: :skillset_changed

def skillset_changed
end
end
```

## Installation

Expand Down
1 change: 1 addition & 0 deletions lib/kredis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require "kredis/type_casting"
require "kredis/types"
require "kredis/attributes"
require "kredis/callbacks_proxy"

require "kredis/railtie" if defined?(Rails::Railtie)

Expand Down
63 changes: 34 additions & 29 deletions lib/kredis/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,76 +2,81 @@ module Kredis::Attributes
extend ActiveSupport::Concern

class_methods do
def kredis_proxy(name, key: nil, config: :shared)
kredis_connection_with __method__, name, key, config: config
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)
kredis_connection_with __method__, name, key, config: config
def kredis_string(name, key: nil, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change
end

def kredis_integer(name, key: nil, config: :shared)
kredis_connection_with __method__, name, key, config: config
def kredis_integer(name, key: nil, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change
end

def kredis_decimal(name, key: nil, config: :shared)
kredis_connection_with __method__, name, key, config: config
def kredis_decimal(name, key: nil, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change
end

def kredis_datetime(name, key: nil, config: :shared)
kredis_connection_with __method__, name, key, config: config
def kredis_datetime(name, key: nil, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change
end

def kredis_flag(name, key: nil, config: :shared)
kredis_connection_with __method__, name, key, config: config
def kredis_flag(name, key: nil, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change

define_method("#{name}?") do
send(name).marked?
end
end

def kredis_enum(name, key: nil, values:, default:, config: :shared)
kredis_connection_with __method__, name, key, values: values, default: default, config: config
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)
kredis_connection_with __method__, name, key, config: config
def kredis_json(name, key: nil, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change
end

def kredis_list(name, key: nil, typed: :string, config: :shared)
kredis_connection_with __method__, name, key, typed: typed, config: config
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
end

def kredis_unique_list(name, limit: nil, key: nil, typed: :string, config: :shared)
kredis_connection_with __method__, name, key, limit: limit, typed: typed, config: config
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
end

def kredis_set(name, key: nil, typed: :string, config: :shared)
kredis_connection_with __method__, name, key, typed: typed, config: config
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
end

def kredis_slot(name, key: nil, config: :shared)
kredis_connection_with __method__, name, key, config: config
def kredis_slot(name, key: nil, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change
end

def kredis_slots(name, available:, key: nil, config: :shared)
kredis_connection_with __method__, name, key, available: available, config: config
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)
kredis_connection_with __method__, name, key, config: config
def kredis_counter(name, key: nil, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change
end

private
def kredis_connection_with(method, name, key, **options)
ivar_symbol = :"@#{name}_#{method}"
type = method.to_s.sub("kredis_", "")
callback = options.delete(:after_change)

define_method(name) do
if instance_variable_defined?(ivar_symbol)
instance_variable_get(ivar_symbol)
else
instance_variable_set(ivar_symbol, Kredis.send(type, kredis_key_evaluated(key) || kredis_key_for_attribute(name), **options))
callbacks_proxy = Kredis::CallbacksProxy.new(
Kredis.send(type, kredis_key_evaluated(key) || kredis_key_for_attribute(name), **options),
self,
callback)
instance_variable_set(ivar_symbol, callbacks_proxy)
end
end
end
Expand Down
33 changes: 33 additions & 0 deletions lib/kredis/callbacks_proxy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class Kredis::CallbacksProxy
attr_reader :type
delegate :to_s, to: :type

CALLBACK_OPERATIONS = {
Kredis::Types::Counter => %i[ increment decrement reset ],
Kredis::Types::Cycle => %i[ next ],
Kredis::Types::Enum => %i[ value= reset ],
Kredis::Types::Flag => %i[ mark remove ],
Kredis::Types::List => %i[ remove prepend append ],
Kredis::Types::Scalar => %i[ value= clear ],
Kredis::Types::Set => %i[ add remove replace take clear ],
Kredis::Types::Slots => %i[ reserve release reset ]
}

def initialize(type, record, callback)
@type, @record, @callback = type, record, callback
end

def method_missing(method, *args, **kwargs, &block)
result = @type.send(method, *args, **kwargs, &block)

if CALLBACK_OPERATIONS[@type.class]&.include? method
if @callback.respond_to? :call
@callback.call(@record)
elsif @callback.is_a? Symbol
@record.send(@callback)
end
end

result
end
end
144 changes: 144 additions & 0 deletions test/callbacks_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
require "test_helper"

class Person
include Kredis::Attributes

kredis_list :names_with_proc_callback, after_change: ->(p) { p.callback_flag = true }
kredis_list :names_with_method_callback, after_change: :changed
kredis_flag :special_with_proc_callback, after_change: ->(p) { p.callback_flag = true }
kredis_flag :special_with_method_callback, after_change: :changed
kredis_string :address_with_proc_callback, after_change: ->(p) { p.callback_flag = true }
kredis_string :address_with_method_callback, after_change: :changed
kredis_enum :morning_with_proc_callback, values: %w[ bright blue black ], default: "bright", after_change: ->(p) { p.callback_flag = true }
kredis_enum :morning_with_method_callback, values: %w[ bright blue black ], default: "bright", after_change: :changed
kredis_slot :attention_with_proc_callback, after_change: ->(p) { p.callback_flag = true }
kredis_slot :attention_with_method_callback, after_change: :changed
kredis_set :vacations_with_proc_callback, after_change: ->(p) { p.callback_flag = true }
kredis_set :vacations_with_method_callback, after_change: :changed
kredis_json :settings_with_proc_callback, after_change: ->(p) { p.callback_flag = true }
kredis_json :settings_with_method_callback, after_change: :changed
kredis_counter :amount_with_proc_callback, after_change: ->(p) { p.callback_flag = true }
kredis_counter :amount_with_method_callback, after_change: :changed

attr_accessor :callback_flag

def initialize
@callback_flag = false
end

def self.name
"Person"
end

def id
8
end

def changed
@callback_flag = true
end
end
julianrubisch marked this conversation as resolved.
Show resolved Hide resolved

class CallbacksTest < ActiveSupport::TestCase
setup do
@person = Person.new

refute @person.callback_flag
end

test "list with after_change proc callback" do
@person.names_with_proc_callback.append %w[ david kasper ]

assert @person.callback_flag
end

test "list with after_change method callback" do
@person.names_with_method_callback.append %w[ david kasper ]

assert @person.callback_flag
end

test "flag with after_change proc callback" do
@person.special_with_proc_callback.mark

assert @person.callback_flag
end

test "flag with after_change method callback" do
@person.special_with_method_callback.mark

assert @person.callback_flag
end

test "string with after_change proc callback" do
@person.address_with_proc_callback.value = "Copenhagen"

assert @person.callback_flag
end

test "string with after_change method callback" do
@person.address_with_proc_callback.value = "Copenhagen"

assert @person.callback_flag
end

test "slot with after_change proc callback" do
@person.attention_with_proc_callback.reserve

assert @person.callback_flag
end

test "slot with after_change method callback" do
@person.attention_with_method_callback.reserve

assert @person.callback_flag
end

test "enum with after_change proc callback" do
@person.morning_with_proc_callback.value = "blue"

assert @person.callback_flag
end

test "enum with after_change method callback" do
@person.morning_with_method_callback.value = "blue"

assert @person.callback_flag
end

test "set with after_change proc callback" do
@person.vacations_with_proc_callback.add "paris"

assert @person.callback_flag
end

test "set with after_change method callback" do
@person.vacations_with_method_callback.add "paris"

assert @person.callback_flag
end

test "json with after_change proc callback" do
@person.settings_with_proc_callback.value = { "color" => "red", "count" => 2 }

assert @person.callback_flag
end

test "json with after_change method callback" do
@person.settings_with_method_callback.value = { "color" => "red", "count" => 2 }

assert @person.callback_flag
end

test "counter with after_change proc callback" do
@person.amount_with_proc_callback.increment

assert @person.callback_flag
end

test "counter with after_change method callback" do
@person.amount_with_method_callback.increment

assert @person.callback_flag
end
end