Skip to content

Commit 4e22cf1

Browse files
Support defining multiple stores with the same store_attribute
1 parent 7d6dce4 commit 4e22cf1

File tree

4 files changed

+95
-43
lines changed

4 files changed

+95
-43
lines changed

lib/active_record/typed_store/dsl.rb

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,12 @@ class DSL
99
def initialize(store_name, options)
1010
@coder = options.fetch(:coder) { default_coder(store_name) }
1111
@store_name = store_name
12-
@prefix =
13-
case options[:prefix]
14-
when String, Symbol
15-
"#{options[:prefix]}_"
16-
when true
17-
"#{store_name}_"
18-
when false, nil
19-
""
20-
else
21-
raise ArgumentError, "Unexpected type for prefix option. Expected string, symbol, or boolean"
22-
end
23-
@suffix =
24-
case options[:suffix]
25-
when String, Symbol
26-
"_#{options[:suffix]}"
27-
when true
28-
"_#{store_name}"
29-
when false, nil
30-
""
31-
else
32-
raise ArgumentError, "Unexpected type for suffix option. Expected string, symbol, or boolean"
33-
end
34-
@accessors = if options[:accessors] == false
35-
{}
36-
elsif options[:accessors].is_a?(Array)
37-
options[:accessors].each_with_object({}) do |accessor_name, hash|
38-
hash[accessor_name] = accessor_key_for(accessor_name)
39-
end
40-
end
4112
@fields = {}
13+
end
14+
15+
def store_accessors(options)
16+
set_affixes(options)
17+
@accessors = options[:accessors]
4218
yield self
4319
end
4420

@@ -53,8 +29,10 @@ def default_coder(attribute_name)
5329
end
5430

5531
def accessors
56-
@accessors || @fields.values.select(&:accessor).each_with_object({}) do |field, hash|
57-
hash[field.name] = accessor_key_for(field.name)
32+
@fields.values.select(&:accessor).each_with_object({}) do |field, hash|
33+
next if @accessors.is_a?(Array) && !@accessors.include?(field.name)
34+
35+
hash[field.name] = accessor_key_for(field.name) if field.accessor
5836
end
5937
end
6038

@@ -63,13 +41,39 @@ def accessors
6341
NO_DEFAULT_GIVEN = Object.new
6442
[:string, :text, :integer, :float, :time, :datetime, :date, :boolean, :decimal, :any].each do |type|
6543
define_method(type) do |name, **options|
44+
options[:accessor] = false if @accessors == false
6645
@fields[name] = Field.new(name, type, options)
6746
end
6847
end
6948
alias_method :date_time, :datetime
7049

7150
private
7251

52+
def set_affixes(options)
53+
@prefix =
54+
case options[:prefix]
55+
when String, Symbol
56+
"#{options[:prefix]}_"
57+
when true
58+
"#{@store_name}_"
59+
when false, nil
60+
""
61+
else
62+
raise ArgumentError, "Unexpected type for prefix option. Expected string, symbol, or boolean"
63+
end
64+
@suffix =
65+
case options[:suffix]
66+
when String, Symbol
67+
"_#{options[:suffix]}"
68+
when true
69+
"_#{@store_name}"
70+
when false, nil
71+
""
72+
else
73+
raise ArgumentError, "Unexpected type for suffix option. Expected string, symbol, or boolean"
74+
end
75+
end
76+
7377
def accessor_key_for(name)
7478
"#{@prefix}#{name}#{@suffix}"
7579
end

lib/active_record/typed_store/extension.rb

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,34 @@ def typed_store(store_attribute, options={}, &block)
1212
unless self < Behavior
1313
include Behavior
1414
class_attribute :typed_stores, :store_accessors, instance_accessor: false
15+
16+
def inherited(sub_class)
17+
super(sub_class)
18+
19+
if self.respond_to? :typed_stores
20+
# Copy the store to the sub class to avoid mutation of the store in parent class
21+
sub_class.typed_stores = self.typed_stores.map do |store_attribute, store|
22+
new_store = store.dup
23+
new_store.instance_variable_set(:'@fields', store.fields.dup)
24+
[store_attribute, new_store]
25+
end.to_h
26+
end
27+
end
1528
end
1629

30+
self.typed_stores ||= {}
1731
store_options = options.slice(:prefix, :suffix)
18-
dsl = DSL.new(store_attribute, options, &block)
19-
self.typed_stores = (self.typed_stores || {}).merge(store_attribute => dsl)
32+
dsl = self.typed_stores[store_attribute] || DSL.new(store_attribute, options)
33+
dsl.store_accessors(options, &block)
34+
self.typed_stores[store_attribute] = dsl
2035
self.store_accessors = typed_stores.each_value.flat_map { |d| d.accessors.values }.map { |a| -a.to_s }.to_set
2136

2237
typed_klass = TypedHash.create(dsl.fields.values)
23-
const_set("#{store_attribute}_hash".camelize, typed_klass)
38+
const_name = "#{store_attribute}_hash".camelize
39+
if const_defined?(const_name) && const_get(const_name).to_s == "#{self}/#{store_attribute}_hash".camelize
40+
remove_const(const_name)
41+
end
42+
const_set(const_name, typed_klass)
2443

2544
if ActiveRecord.version >= Gem::Version.new('6.1.0.alpha')
2645
attribute(store_attribute) do |subtype|

spec/active_record/typed_store_spec.rb

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,41 @@
500500

501501
end
502502

503+
shared_examples 'an inherited model' do
504+
let(:model) { described_class.new }
505+
506+
it 'can be serialized' do
507+
model.update(new_attribute: "foobar")
508+
expect(model.reload.new_attribute).to be == "foobar"
509+
end
510+
511+
it 'is casted' do
512+
model.update(new_attribute: 42)
513+
expect(model.settings[:new_attribute]).to be == '42'
514+
end
515+
516+
it 'parent classes are not modified' do
517+
sub_klass = Class.new(model.class) { typed_store(:settings) { |t| t.boolean :another_new_attribute } }
518+
expect(sub_klass.store_accessors).to include 'another_new_attribute'
519+
expect(sub_klass.store_accessors).to include 'new_attribute'
520+
expect(model.class.store_accessors).not_to include 'another_new_attribute'
521+
expect(model.class.store_accessors).to include 'new_attribute'
522+
expect(model.class.superclass.store_accessors).not_to include 'another_new_attribute'
523+
expect(model.class.superclass.store_accessors).not_to include 'new_attribute'
524+
end
525+
526+
it 'can redefine fields' do
527+
expect(model.age).to eq 18
528+
end
529+
530+
it 'merges the previous store' do
531+
settings = model.class.typed_stores[:settings]
532+
expect(model).to respond_to :new_attribute
533+
expect(settings.keys).to include :new_attribute
534+
expect(settings.fields[:age].default).to eq 18
535+
end
536+
end
537+
503538
shared_examples 'a store' do |retain_type = true, settings_type = :text|
504539
let(:model) { described_class.new }
505540

@@ -944,15 +979,8 @@
944979
describe InheritedTypedStoreModel do
945980
let(:model) { described_class.new }
946981

947-
it 'can be serialized' do
948-
model.update(new_attribute: "foobar")
949-
expect(model.reload.new_attribute).to be == "foobar"
950-
end
951-
952-
it 'is casted' do
953-
model.update(new_attribute: 42)
954-
expect(model.settings[:new_attribute]).to be == '42'
955-
end
982+
it_should_behave_like 'an inherited model'
983+
it_should_behave_like 'a model supporting arrays'
956984
end
957985

958986
describe DirtyTrackingModel do

spec/support/models.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ class InheritedTypedStoreModel < YamlTypedStoreModel
145145

146146
typed_store :settings do |t|
147147
t.string :new_attribute
148+
t.integer :age, default: 18, null: false
148149
end
149150
end
150151

0 commit comments

Comments
 (0)