From a20bb3711c95676fbd83b6445b8bd26c24e0542d Mon Sep 17 00:00:00 2001 From: Lewis Buckley Date: Sun, 13 Aug 2023 12:51:01 +0100 Subject: [PATCH 1/4] Support scoping attribute keys with a generated prefix 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. --- README.md | 4 +- lib/kredis/attributes.rb | 108 ++++++++++++++++++++++----------------- test/scope_test.rb | 56 ++++++++++++++++++++ 3 files changed, 119 insertions(+), 49 deletions(-) create mode 100644 test/scope_test.rb diff --git a/README.md b/README.md index d328ad9..332feae 100644 --- a/README.md +++ b/README.md @@ -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" diff --git a/lib/kredis/attributes.rb b/lib/kredis/attributes.rb index b844501..d1ebd28 100644 --- a/lib/kredis/attributes.rb +++ b/lib/kredis/attributes.rb @@ -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 @@ -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) diff --git a/test/scope_test.rb b/test/scope_test.rb new file mode 100644 index 0000000..c9c800f --- /dev/null +++ b/test/scope_test.rb @@ -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 From e25a9bb5342a7ae42a6d6a9b211e10a2a8bcac47 Mon Sep 17 00:00:00 2001 From: Lewis Buckley Date: Sun, 13 Aug 2023 20:36:50 +0100 Subject: [PATCH 2/4] Add test for nil scope --- test/scope_test.rb | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/scope_test.rb b/test/scope_test.rb index c9c800f..dd32fdd 100644 --- a/test/scope_test.rb +++ b/test/scope_test.rb @@ -28,6 +28,7 @@ class Family kredis_list :members kredis_list :pets, key: "pets" + kredis_list :members_with_nil_scope, scope: ->(person) { nil } def id 1 @@ -36,7 +37,10 @@ def id class ScopeTest < ActiveSupport::TestCase - setup { @person = Person.new } + setup do + @person = Person.new + @family = Family.new + end test "key is scoped" do assert_equal @person.names_with_scope.key, "identities:1:people:names_with_scope" @@ -46,11 +50,15 @@ class ScopeTest < ActiveSupport::TestCase assert_equal @person.names_with_scope_and_key.key, "identities:1:custom_key_example" end + test "scope is nil and key is generated normally" do + assert_equal @family.members_with_nil_scope.key, "families:1:members_with_nil_scope" + end + test "custom key" do - assert_equal Family.new.pets.key, "pets" + assert_equal @family.pets.key, "pets" end test "key without scope" do - assert_equal Family.new.members.key, "families:1:members" + assert_equal @family.members.key, "families:1:members" end end From 04a8849511922ca5748a093259d1bfed7c8a1e7c Mon Sep 17 00:00:00 2001 From: Lewis Buckley Date: Sun, 13 Aug 2023 20:45:23 +0100 Subject: [PATCH 3/4] Remove redundant array --- lib/kredis/attributes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kredis/attributes.rb b/lib/kredis/attributes.rb index d1ebd28..a4bf798 100644 --- a/lib/kredis/attributes.rb +++ b/lib/kredis/attributes.rb @@ -121,7 +121,7 @@ def kredis_key_evaluated(scope, key, name) end def kredis_key_for_model(model = self) - [ model.class.name.tableize.tr("/", ":") ] + model.class.name.tableize.tr("/", ":") end def extract_kredis_id(model = self) From ac9dc6a7b55398bc95216c3bf79cb97a8e1535b5 Mon Sep 17 00:00:00 2001 From: Lewis Buckley Date: Sun, 13 Aug 2023 20:47:47 +0100 Subject: [PATCH 4/4] Move test classes to ScopeTest namespace --- test/scope_test.rb | 54 +++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/test/scope_test.rb b/test/scope_test.rb index dd32fdd..34e0916 100644 --- a/test/scope_test.rb +++ b/test/scope_test.rb @@ -2,56 +2,56 @@ require "test_helper" -class Identity - def id - 1 +class ScopeTest < ActiveSupport::TestCase + class Identity + def id + 1 + end end -end -class Person - include Kredis::Attributes + 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}" } + 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 identity + Identity.new + end - def example_method - "example" + def example_method + "example" + end end -end -class Family - include Kredis::Attributes + class Family + include Kredis::Attributes - kredis_list :members - kredis_list :pets, key: "pets" - kredis_list :members_with_nil_scope, scope: ->(person) { nil } + kredis_list :members + kredis_list :pets, key: "pets" + kredis_list :members_with_nil_scope, scope: ->(person) { nil } - def id - 1 + def id + 1 + end end -end -class ScopeTest < ActiveSupport::TestCase setup do @person = Person.new @family = Family.new end test "key is scoped" do - assert_equal @person.names_with_scope.key, "identities:1:people:names_with_scope" + assert_equal @person.names_with_scope.key, "scope_test:identities:1:scope_test: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" + assert_equal @person.names_with_scope_and_key.key, "scope_test:identities:1:custom_key_example" end test "scope is nil and key is generated normally" do - assert_equal @family.members_with_nil_scope.key, "families:1:members_with_nil_scope" + assert_equal @family.members_with_nil_scope.key, "scope_test:families:1:members_with_nil_scope" end test "custom key" do @@ -59,6 +59,6 @@ class ScopeTest < ActiveSupport::TestCase end test "key without scope" do - assert_equal @family.members.key, "families:1:members" + assert_equal @family.members.key, "scope_test:families:1:members" end end