Skip to content

Commit

Permalink
Prefix / Suffix feature
Browse files Browse the repository at this point in the history
  • Loading branch information
mmarusyk committed Apr 7, 2024
1 parent df95ba1 commit e956c02
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 12 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,26 @@ product.title #=> "Foo"
product.data #=> { "t" => "Foo" }
```

You can also pass in a `prefix` or `suffix` option.

```ruby
class Product < ActiveRecord::Base
jsonb_accessor :data,
title: [:string, prefix: :data],
external_id: [:integer, suffix: :attr]
end
```

This allows you to use `data_title` and `external_id_attr` for your getters and setters, but use `title` and `external_id` as the key in the `jsonb`.
Also, you can pass `true` as a value for `prefix` or `suffix` to use the json_accessor name.

```ruby
product = Product.new(data_title: "Foo", external_id_attr: 12314122)
product.data_title #=> "Foo"
product.external_id_attr #=> 12314122
product.data #=> { "title" => "Foo", "external_id" => 12314122 }
```

## Scopes

Jsonb Accessor provides several scopes to make it easier to query `jsonb` columns. `jsonb_contains`, `jsonb_number_where`, `jsonb_time_where`, and `jsonb_where` are available on all `ActiveRecord::Base` subclasses and don't require that you make use of the `jsonb_accessor` declaration.
Expand Down
23 changes: 23 additions & 0 deletions lib/jsonb_accessor/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,28 @@ def parse_date(datetime)
Time.zone.parse(datetime)
end
end

def define_attribute_name(json_attribute, name, prefix, suffix)
accessor_prefix =
case prefix
when String, Symbol
"#{prefix}_"
when TrueClass
"#{json_attribute}_"
else
""
end
accessor_suffix =
case suffix
when String, Symbol
"_#{suffix}"
when TrueClass
"_#{json_attribute}"
else
""
end

"#{accessor_prefix}#{name}#{accessor_suffix}"
end
end
end
36 changes: 24 additions & 12 deletions lib/jsonb_accessor/macro.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,29 @@ module JsonbAccessor
module Macro
module ClassMethods
def jsonb_accessor(jsonb_attribute, field_types)
names_and_store_keys = field_types.each_with_object({}) do |(name, type), mapping|
names_and_attribute_names = field_types.each_with_object({}) do |(name, type), mapping|
_type, options = Array(type)
mapping[name.to_s] = (options.try(:delete, :store_key) || name).to_s
prefix = options.try(:delete, :prefix)
suffix = options.try(:delete, :suffix)
mapping[name] = JsonbAccessor::Helpers.define_attribute_name(jsonb_attribute, name, prefix, suffix)
end

# Get attribute names to store keys mapping
attribute_names_and_store_keys = field_types.each_with_object({}) do |(name, type), mapping|
_type, options = Array(type)
attribute_name = names_and_attribute_names[name]
mapping[attribute_name] = (options.try(:delete, :store_key) || name).to_s
end

# Defines virtual attributes for each jsonb field.
field_types.each do |name, type|
next attribute name, type unless type.is_a?(Array)
next attribute name, *type unless type.last.is_a?(Hash)
attribute_name = names_and_attribute_names[name]

next attribute attribute_name, type unless type.is_a?(Array)
next attribute attribute_name, *type unless type.last.is_a?(Hash)

*args, keyword_args = type
attribute name, *args, **keyword_args
attribute attribute_name, *args, **keyword_args
end

store_key_mapping_method_name = "jsonb_store_key_mapping_for_#{jsonb_attribute}"
Expand All @@ -24,21 +35,22 @@ def jsonb_accessor(jsonb_attribute, field_types)
# Allows us to get a mapping of field names to store keys scoped to the column
define_method(store_key_mapping_method_name) do
superclass_mapping = superclass.try(store_key_mapping_method_name) || {}
superclass_mapping.merge(names_and_store_keys)
superclass_mapping.merge(attribute_names_and_store_keys)
end
end
# We extend with class methods here so we can use the results of methods it defines to define more useful methods later
extend class_methods

# Get field names to default values mapping
names_and_defaults = field_types.each_with_object({}) do |(name, type), mapping|
# Get attribute names to default values mapping
attribute_names_and_defaults = field_types.each_with_object({}) do |(name, type), mapping|
_type, options = Array(type)
field_default = options.try(:delete, :default)
mapping[name.to_s] = field_default unless field_default.nil?
attribute_name = names_and_attribute_names[name]
mapping[attribute_name] = field_default unless field_default.nil?
end

# Get store keys to default values mapping
store_keys_and_defaults = JsonbAccessor::Helpers.convert_keys_to_store_keys(names_and_defaults, public_send(store_key_mapping_method_name))
store_keys_and_defaults = JsonbAccessor::Helpers.convert_keys_to_store_keys(attribute_names_and_defaults, public_send(store_key_mapping_method_name))

# Define jsonb_defaults_mapping_for_<jsonb_attribute>
defaults_mapping_method_name = "jsonb_defaults_mapping_for_#{jsonb_attribute}"
Expand All @@ -62,7 +74,7 @@ def jsonb_accessor(jsonb_attribute, field_types)
# Setters are in a module to allow users to override them and still be able to use `super`.
setters = Module.new do
# Overrides the setter created by `attribute` above to make sure the jsonb attribute is kept in sync.
names_and_store_keys.each do |name, store_key|
attribute_names_and_store_keys.each do |name, store_key|
define_method("#{name}=") do |value|
super(value)

Expand Down Expand Up @@ -108,7 +120,7 @@ def jsonb_accessor(jsonb_attribute, field_types)

jsonb_values = public_send(jsonb_attribute) || {}
jsonb_values.each do |store_key, value|
name = names_and_store_keys.key(store_key)
name = attribute_names_and_store_keys.key(store_key)
next unless name

write_attribute(
Expand Down
188 changes: 188 additions & 0 deletions spec/jsonb_accessor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,194 @@ def build_class(jsonb_accessor_config, &block)
end
end

context "prefixes" do
let(:klass) do
build_class(foo: [:string, { default: "bar", prefix: :a }])
end

it "creates accessor attribute with the given prefix" do
expect(instance.a_foo).to eq("bar")
expect(instance.options).to eq("foo" => "bar")
end

context "when prefix is true" do
let(:klass) do
build_class(foo: [:string, { default: "bar", prefix: true }])
end

it "creates accessor attribute with the json_attribute name" do
expect(instance.options_foo).to eq("bar")
expect(instance.options).to eq("foo" => "bar")
end
end

context "inheritance" do
let(:subklass) do
Class.new(klass) do
jsonb_accessor :options, bar: [:integer, { default: 2 }]
end
end
let(:subklass_instance) { subklass.new }

it "includes default values from the parent in the jsonb hash" do
expect(subklass_instance.a_foo).to eq("bar")
expect(subklass_instance.bar).to eq(2)
expect(subklass_instance.options).to eq("foo" => "bar", "bar" => 2)
end
end

context "inheritance with prefix" do
let(:subklass) do
Class.new(klass) do
jsonb_accessor :options, bar: [:integer, { default: 2, prefix: :b }]
end
end

let(:subklass_instance) { subklass.new }

it "includes default values from the parent in the jsonb hash" do
expect(subklass_instance.a_foo).to eq("bar")
expect(subklass_instance.b_bar).to eq(2)
expect(subklass_instance.options).to eq("foo" => "bar", "bar" => 2)
end
end

context "with store keys" do
let(:klass) do
build_class(foo: [:string, { default: "bar", store_key: :g, prefix: :a }])
end

it "creates accessor attribute with the given prefix and with the given store key" do
expect(instance.a_foo).to eq("bar")
expect(instance.options).to eq("g" => "bar")
end

context "inheritance" do
let(:subklass) do
Class.new(klass) do
jsonb_accessor :options, bar: [:integer, { default: 2, store_key: :h }]
end
end
let(:subklass_instance) { subklass.new }

it "includes default values from the parent in the jsonb hash with the correct store keys" do
expect(subklass_instance.a_foo).to eq("bar")
expect(subklass_instance.bar).to eq(2)
expect(subklass_instance.options).to eq("g" => "bar", "h" => 2)
end
end

context "inheritance with prefix" do
let(:subklass) do
Class.new(klass) do
jsonb_accessor :options, bar: [:integer, { default: 2, store_key: :i, prefix: :b }]
end
end
let(:subklass_instance) { subklass.new }

it "includes default values from the parent in the jsonb hash with the correct store keys" do
expect(subklass_instance.a_foo).to eq("bar")
expect(subklass_instance.b_bar).to eq(2)
expect(subklass_instance.options).to eq("g" => "bar", "i" => 2)
end
end
end
end

context "suffixes" do
let(:klass) do
build_class(foo: [:string, { default: "bar", suffix: :a }])
end

it "creates accessor attribute with the given suffix" do
expect(instance.foo_a).to eq("bar")
expect(instance.options).to eq("foo" => "bar")
end

context "when suffix is true" do
let(:klass) do
build_class(foo: [:string, { default: "bar", suffix: true }])
end

it "creates accessor attribute with the json_attribute name" do
expect(instance.foo_options).to eq("bar")
expect(instance.options).to eq("foo" => "bar")
end
end

context "inheritance" do
let(:subklass) do
Class.new(klass) do
jsonb_accessor :options, bar: [:integer, { default: 2 }]
end
end
let(:subklass_instance) { subklass.new }

it "includes default values from the parent in the jsonb hash" do
expect(subklass_instance.foo_a).to eq("bar")
expect(subklass_instance.bar).to eq(2)
expect(subklass_instance.options).to eq("foo" => "bar", "bar" => 2)
end
end

context "inheritance with suffix" do
let(:subklass) do
Class.new(klass) do
jsonb_accessor :options, bar: [:integer, { default: 2, suffix: :b }]
end
end

let(:subklass_instance) { subklass.new }

it "includes default values from the parent in the jsonb hash" do
expect(subklass_instance.foo_a).to eq("bar")
expect(subklass_instance.bar_b).to eq(2)
expect(subklass_instance.options).to eq("foo" => "bar", "bar" => 2)
end
end

context "with store keys" do
let(:klass) do
build_class(foo: [:string, { default: "bar", store_key: :g, suffix: :a }])
end

it "creates accessor attribute with the given suffix and with the given store key" do
expect(instance.foo_a).to eq("bar")
expect(instance.options).to eq("g" => "bar")
end

context "inheritance" do
let(:subklass) do
Class.new(klass) do
jsonb_accessor :options, bar: [:integer, { default: 2, store_key: :h }]
end
end
let(:subklass_instance) { subklass.new }

it "includes default values from the parent in the jsonb hash with the correct store keys" do
expect(subklass_instance.foo_a).to eq("bar")
expect(subklass_instance.bar).to eq(2)
expect(subklass_instance.options).to eq("g" => "bar", "h" => 2)
end
end

context "inheritance with suffix" do
let(:subklass) do
Class.new(klass) do
jsonb_accessor :options, bar: [:integer, { default: 2, store_key: :i, suffix: :b }]
end
end
let(:subklass_instance) { subklass.new }

it "includes default values from the parent in the jsonb hash with the correct store keys" do
expect(subklass_instance.foo_a).to eq("bar")
expect(subklass_instance.bar_b).to eq(2)
expect(subklass_instance.options).to eq("g" => "bar", "i" => 2)
end
end
end
end

describe "#<jsonb_attribute>_where" do
let(:klass) do
build_class(
Expand Down
32 changes: 32 additions & 0 deletions spec/lib/jsonb_accessor/helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,36 @@
expect(subject.convert_store_keys_to_keys(attributes, store_key_mapping)).to eq(expected)
end
end

describe ".define_attribute_name" do
let(:json_attribute) { :options }
let(:name) { :foo }
let(:prefix) { :pref }
let(:suffix) { :suff }
let(:expected) { "#{prefix}_#{name}_#{suffix}" }

it "returns attribute name with prefix and suffix" do
expect(subject.define_attribute_name(json_attribute, name, prefix, suffix)).to eq(expected)
end

context "when affixes is true class" do
let(:prefix) { true }
let(:suffix) { true }
let(:expected) { "#{json_attribute}_#{name}_#{json_attribute}" }

it "returns attribute name with json_attribute prefix and suffix" do
expect(subject.define_attribute_name(json_attribute, name, prefix, suffix)).to eq(expected)
end
end

context "when affixes is nil" do
let(:prefix) { nil }
let(:suffix) { nil }
let(:expected) { name.to_s }

it "returns attribute name with json_attribute prefix and suffix" do
expect(subject.define_attribute_name(json_attribute, name, prefix, suffix)).to eq(expected)
end
end
end
end

0 comments on commit e956c02

Please sign in to comment.