Skip to content

Commit

Permalink
Support scoping attribute keys with a generated prefix
Browse files Browse the repository at this point in the history
It's common for models to belong a parent model, such as a user. We can
now scope the attribute keys to the parent model, so that the keys can
be easily identified and deleted in case of a user deletion.
  • Loading branch information
lewispb committed Aug 13, 2023
1 parent cc15c2f commit a20bb37
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 49 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,12 @@ class Person < ApplicationRecord
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_names_key
kredis_unique_list :skills, limit: 2
kredis_unique_list :skills, limit: 2, scope: :user # stored at users:1:person:skills
kredis_enum :morning, values: %w[ bright blue black ], default: "bright"
kredis_counter :steps, expires_in: 1.hour

belongs_to :user

private
def generate_names_key
"key-generated-from-private-method"
Expand Down
108 changes: 60 additions & 48 deletions lib/kredis/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,94 +4,96 @@ 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, scope: nil, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, scope: scope, config: config, after_change: after_change
end

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
def kredis_string(name, key: nil, scope: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, scope: scope, default: default, config: config, after_change: after_change, expires_in: expires_in
end

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
def kredis_integer(name, key: nil, scope: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, scope: scope, default: default, config: config, after_change: after_change, expires_in: expires_in
end

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
def kredis_decimal(name, key: nil, scope: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, scope: scope, default: default, config: config, after_change: after_change, expires_in: expires_in
end

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
def kredis_datetime(name, key: nil, scope: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, scope: scope, default: default, config: config, after_change: after_change, expires_in: expires_in
end

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

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

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

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

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

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

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

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

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

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

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
def kredis_boolean(name, key: nil, scope: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, scope: scope, default: default, config: config, after_change: after_change, expires_in: expires_in
end

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

define_method(name) do
if instance_variable_defined?(ivar_symbol)
instance_variable_get(ivar_symbol)
else
type = method.to_s.delete_prefix("kredis_")
after_change = options.delete(:after_change)
scope = options.delete(:scope)
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)

new_type = Kredis.send(type, kredis_key_evaluated(scope, key, name), **options)
instance_variable_set ivar_symbol,
after_change ? enrich_after_change_with_record_access(new_type, after_change) : new_type
end
Expand All @@ -100,20 +102,30 @@ def kredis_connection_with(method, name, key, **options)
end

private
def kredis_key_evaluated(key)
case key
when String then key
when Proc then key.call(self)
when Symbol then send(key)
def kredis_key_evaluated(scope, key, name)
scope = case scope
when String then scope
when Proc then scope.call(self)
when Symbol then [ kredis_key_for_model(send(scope)), extract_kredis_id(send(scope)) ]
end

custom_key = case key
when String then key
when Proc then key.call(self)
when Symbol then send(key)
end

default_key = -> { [ kredis_key_for_model, scope ? nil : extract_kredis_id, name ] }

[ scope, (custom_key.presence || default_key.call) ].flatten.compact.join(":")
end

def kredis_key_for_attribute(name)
"#{self.class.name.tableize.tr("/", ":")}:#{extract_kredis_id}:#{name}"
def kredis_key_for_model(model = self)
[ model.class.name.tableize.tr("/", ":") ]
end

def extract_kredis_id
try(:id) or raise NotImplementedError, "kredis needs a unique id, either implement an id method or pass a custom key."
def extract_kredis_id(model = self)
model.try(:id) or raise NotImplementedError, "#{model.class} needs a unique id for Kredis, either implement an id method or pass a custom key"
end

def enrich_after_change_with_record_access(type, original_after_change)
Expand Down
56 changes: 56 additions & 0 deletions test/scope_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

require "test_helper"

class Identity
def id
1
end
end

class Person
include Kredis::Attributes

kredis_list :names_with_scope, scope: :identity
kredis_list :names_with_scope_and_key, scope: :identity, key: ->(person) { "custom_key_#{person.example_method}" }

def identity
Identity.new
end

def example_method
"example"
end
end

class Family
include Kredis::Attributes

kredis_list :members
kredis_list :pets, key: "pets"

def id
1
end
end


class ScopeTest < ActiveSupport::TestCase
setup { @person = Person.new }

test "key is scoped" do
assert_equal @person.names_with_scope.key, "identities:1:people:names_with_scope"
end

test "key is scoped and has custom key component" do
assert_equal @person.names_with_scope_and_key.key, "identities:1:custom_key_example"
end

test "custom key" do
assert_equal Family.new.pets.key, "pets"
end

test "key without scope" do
assert_equal Family.new.members.key, "families:1:members"
end
end

0 comments on commit a20bb37

Please sign in to comment.