From 73c9afdee97a9b75e3bcc9142e25ea74eb38521b Mon Sep 17 00:00:00 2001 From: Markus Doits Date: Fri, 23 Jul 2021 18:37:49 +0200 Subject: [PATCH 01/82] make fallthrough accessors compatible with ruby 3 `method_missing` does not swallow kwargs on ruby 3 anymore --- lib/mobility.rb | 3 +++ lib/mobility/plugins/fallthrough_accessors.rb | 7 +++++++ .../plugins/fallthrough_accessors_spec.rb | 16 ++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/lib/mobility.rb b/lib/mobility.rb index 8bdf50e73..dd3f9d059 100644 --- a/lib/mobility.rb +++ b/lib/mobility.rb @@ -73,6 +73,9 @@ class Comment plugin enabled. =end + +def ruby2_keywords(*); end unless respond_to?(:ruby2_keywords, true) + module Mobility # A generic exception used by Mobility. class Error < StandardError diff --git a/lib/mobility/plugins/fallthrough_accessors.rb b/lib/mobility/plugins/fallthrough_accessors.rb index 14fa6f88c..7f69b35a8 100644 --- a/lib/mobility/plugins/fallthrough_accessors.rb +++ b/lib/mobility/plugins/fallthrough_accessors.rb @@ -55,6 +55,13 @@ def define_fallthrough_accessors(*names) end end + # Following is needed in order to not swallow `kwargs` on ruby >= 3.0. + # Otherwise `kwargs` are not passed by `super` to a possible other + # `method_missing` defined like this: + # + # def method_missing(name, *args, **kwargs, &block); end + ruby2_keywords :method_missing + define_method :respond_to_missing? do |method_name, include_private = false| (method_name =~ method_name_regex) || super(method_name, include_private) end diff --git a/spec/mobility/plugins/fallthrough_accessors_spec.rb b/spec/mobility/plugins/fallthrough_accessors_spec.rb index e01f72971..35f3efdfc 100644 --- a/spec/mobility/plugins/fallthrough_accessors_spec.rb +++ b/spec/mobility/plugins/fallthrough_accessors_spec.rb @@ -40,6 +40,22 @@ def method_missing(method_name, *args, &block) expect(instance.foo(**options)).to eq([options]) end + it 'passes kwargs to super when method does not match' do + mod = Module.new do + def method_missing(method_name, *args, **kwargs, &block) + (method_name == :foo) ? [args, kwargs] : super + end + end + + model_class = Class.new + model_class.include translations, mod + + instance = model_class.new + + kwargs = { some: 'params' } + expect(instance.foo(**kwargs)).to eq([[], kwargs]) + end + it 'does not pass on empty keyword options hash to super' do mod = Module.new do def method_missing(method_name, *args, &block) From 2d24acbd797201137f7f05f95e4d32e32967f100 Mon Sep 17 00:00:00 2001 From: Markus Doits Date: Sun, 25 Jul 2021 21:22:49 +0200 Subject: [PATCH 02/82] run ci with ruby 3, too --- .github/workflows/ci.yml | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4015f114..4467b600c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: fail-fast: false matrix: ruby: + - '3.0' - '2.7' - '2.6' database: @@ -45,6 +46,10 @@ jobs: experimental: [false] feature: ['unit'] include: + - ruby: '3.0' + feature: 'unit' + orm: + experimental: false - ruby: '2.7' feature: 'unit' orm: @@ -57,6 +62,40 @@ jobs: feature: 'unit' orm: experimental: false + - ruby: '3.0' + feature: 'rails' + orm: + name: 'active_record' + version: '6.1' + database: 'sqlite3' + experimental: false + - ruby: '3.0' + feature: 'performance' + experimental: false + - ruby: '3.0' + feature: 'i18n_fallbacks' + experimental: false + - ruby: '3.0' + database: 'sqlite3' + feature: 'unit' + orm: + name: 'active_record' + version: '7.0' + experimental: true + - ruby: '3.0' + database: 'mysql' + feature: 'unit' + orm: + name: 'active_record' + version: '7.0' + experimental: true + - ruby: '3.0' + database: 'postgres' + feature: 'unit' + orm: + name: 'active_record' + version: '7.0' + experimental: true - ruby: '2.7' feature: 'rails' orm: @@ -96,6 +135,22 @@ jobs: orm: name: 'active_record' version: '4.2' + - ruby: '3.0' + orm: + name: 'active_record' + version: '4.2' + - ruby: '3.0' + orm: + name: 'active_record' + version: '5.0' + - ruby: '3.0' + orm: + name: 'active_record' + version: '5.1' + - ruby: '3.0' + orm: + name: 'active_record' + version: '5.2' env: DB: ${{ matrix.database }} BUNDLE_JOBS: 4 From 7970ee460342c7476a57c708a6c7f734d34583f5 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 6 Aug 2021 11:20:30 +0900 Subject: [PATCH 03/82] Release 1.1.3 --- CHANGELOG.md | 5 +++++ README.md | 2 +- lib/mobility/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d26be5a3..ee4d9f96b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ ## 1.1 +### 1.1.3 +- Do not swallow keyword args on ruby 3 in fallthrough accessors + ([#520](https://github.com/shioyama/mobility/pull/520)) thanks + [doits](https://github.com/doits)! + ### 1.1.2 - Check whether class responds to mobility_attribute? ([#515](https://github.com/shioyama/mobility/pull/515)) diff --git a/README.md b/README.md index f826d1b2c..4cad15f0b 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Installation Add this line to your application's Gemfile: ```ruby -gem 'mobility', '~> 1.1.2' +gem 'mobility', '~> 1.1.3' ``` ### ActiveRecord (Rails) diff --git a/lib/mobility/version.rb b/lib/mobility/version.rb index a895730ba..5cb5d7045 100644 --- a/lib/mobility/version.rb +++ b/lib/mobility/version.rb @@ -8,7 +8,7 @@ def self.gem_version module VERSION MAJOR = 1 MINOR = 1 - TINY = 2 + TINY = 3 PRE = nil STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") From b9a70f217f034e04f9a71b789a9529ec770e7e06 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Tue, 14 Sep 2021 13:11:19 +0900 Subject: [PATCH 04/82] Add missing changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee4d9f96b..536077d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - Do not swallow keyword args on ruby 3 in fallthrough accessors ([#520](https://github.com/shioyama/mobility/pull/520)) thanks [doits](https://github.com/doits)! +- Assign blank values in pg hash backends + ([#516](https://github.com/shioyama/mobility/pull/516)) ### 1.1.2 - Check whether class responds to mobility_attribute? From 366352aef191bc705c2c4dc65621b5bb2f11269d Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 14:55:19 +0900 Subject: [PATCH 05/82] Clearly distinguish backend classes from their configured subclasses In practice, every backend class that is used in a model is a subclass of one of the base classes. Configured backend classes have a model class and set of options, whereas the base backend classes do not. However, previously you could still call `options` or `model_class` on the base classes and you would get back a `nil`. This is confusing since those getter methods should never be called on the base backend classes. In this commit I'm restructuring the code such that: - getter methods raise on the base backend class - methods which do not make sense on the base backend class are moved to a module which is included into backend subclasses. I think this should avoid weird bugs and misunderstandings. --- lib/mobility/backend.rb | 81 ++++++++++++++----- spec/mobility/backend_spec.rb | 45 +++++++---- .../backends/active_record/table_spec.rb | 2 +- spec/mobility/backends/sequel/table_spec.rb | 2 +- spec/mobility/plugins/backend_spec.rb | 15 ++-- 5 files changed, 102 insertions(+), 43 deletions(-) diff --git a/lib/mobility/backend.rb b/lib/mobility/backend.rb index d4e3a15bb..879268a57 100644 --- a/lib/mobility/backend.rb +++ b/lib/mobility/backend.rb @@ -117,7 +117,6 @@ def options # Extend included class with +setup+ method and other class methods def self.included(base) base.extend ClassMethods - base.singleton_class.attr_reader :options, :model_class end # Defines setup hooks for backend to customize model class. @@ -147,17 +146,6 @@ def setup &block def inherited(subclass) subclass.instance_variable_set(:@setup_block, @setup_block) - subclass.instance_variable_set(:@options, @options) - subclass.instance_variable_set(:@model_class, @model_class) - end - - # Call setup block on a class with attributes and options. - # @param model_class Class to be setup-ed - # @param [Array] attribute_names - # @param [Hash] options - def setup_model(model_class, attribute_names) - return unless setup_block = @setup_block - model_class.class_exec(attribute_names, options, &setup_block) end # Build a subclass of this backend class for a given set of options @@ -167,11 +155,7 @@ def setup_model(model_class, attribute_names) # @param [Hash] options # @return [Class] backend subclass def build_subclass(model_class, options) - Class.new(self) do - @model_class = model_class - configure(options) if respond_to?(:configure) - @options = options.freeze - end + ConfiguredBackend.build(self, model_class, options) end # Create instance and class methods to access value on options hash @@ -188,10 +172,22 @@ def #{name} EOM end - # Show useful information about this backend class, if it has no name. - # @return [String] - def inspect - name ? super : "#<#{superclass.name}>" + def options + raise_unconfigured!(:options) + end + + def model_class + raise_unconfigured!(:model_class) + end + + def setup_model(_model_class, _attributes) + raise_unconfigured!(:setup_model) + end + + private + + def raise_unconfigured!(method_name) + raise UnconfiguredError, "You are calling #{method_name} on an unconfigured backend class." end end @@ -204,5 +200,48 @@ def write(value, options = {}) backend.write(locale, value, options) end end + + class ConfiguredError < StandardError; end + class UnconfiguredError < StandardError; end +=begin + +Module included in configured backend classes, which in addition to methods on +the parent backend class also have a +model_class+ and set of +options+. + +=end + module ConfiguredBackend + def self.build(backend_class, model_class, options) + Class.new(backend_class) do + extend ConfiguredBackend + + @model_class = model_class + configure(options) if respond_to?(:configure) + @options = options.freeze + end + end + + def self.extended(klass) + klass.singleton_class.attr_reader :options, :model_class + end + + # Call setup block on a class with attributes and options. + # @param model_class Class to be setup-ed + # @param [Array] attribute_names + # @param [Hash] options + def setup_model(model_class, attribute_names) + return unless setup_block = @setup_block + model_class.class_exec(attribute_names, options, &setup_block) + end + + def inherited(_) + raise ConfiguredError, "Configured backends cannot be subclassed." + end + + # Show subclassed backend class name, if it has one. + # @return [String] + def inspect + (name = superclass.name) ? "#<#{name}>" : super + end + end end end diff --git a/spec/mobility/backend_spec.rb b/spec/mobility/backend_spec.rb index 91bbcaabb..85a20424c 100644 --- a/spec/mobility/backend_spec.rb +++ b/spec/mobility/backend_spec.rb @@ -142,22 +142,10 @@ def bar it "assigns setup block to descendants" do model_class = Class.new subclass = backend_class.build_subclass(model_class, foo: "bar") - Class.new(subclass).setup_model(model_class, ["title"]) + subclass.setup_model(model_class, ["title"]) expect(model_class.foo).to eq("foo") end - it "assigns options to descendants" do - model_class = Class.new - subclass = backend_class.build_subclass(model_class, foo: "bar") - expect(Class.new(subclass).options).to eq(foo: "bar") - end - - it "assigns model_class to descendants" do - model_class = Class.new - subclass = backend_class.build_subclass(model_class, foo: "bar") - expect(Class.new(subclass).model_class).to eq(model_class) - end - it "concatenates blocks when called multiple times" do backend_class.class_eval do setup do |attributes, options| @@ -182,13 +170,40 @@ def self.foobar end end end + + describe ".build_subclass" do + it "raises when subclassed again" do + model_class = Class.new + subclass = backend_class.build_subclass(model_class, foo: "bar") + expect { Class.new(subclass) }.to raise_error(Mobility::Backend::ConfiguredError) + end + end + + describe "methods which raise unless called on configured subclass" do + it "raises when unconfigured" do + expect { backend_class.options }.to raise_error(Mobility::Backend::UnconfiguredError) + expect { backend_class.model_class }.to raise_error(Mobility::Backend::UnconfiguredError) + expect { backend_class.setup_model(Class.new, {}) }.to raise_error(Mobility::Backend::UnconfiguredError) + end + + it "returns value when configured" do + model_class = double("model class") + options = double("options") + subclass = backend_class.build_subclass(model_class, options) + expect(subclass.model_class).to eq(model_class) + expect(subclass.options).to eq(options) + end + end end - describe ".inspect" do + describe "#inspect on configured backend" do it "returns superclass name" do backend = stub_const 'MyBackend', Class.new + model_class = stub_const 'Model', Class.new backend.include(described_class) - expect(Class.new(backend).inspect).to match(/MyBackend/) + options = { foo: "bar" } + subclass = backend.build_subclass(model_class, options) + expect(subclass.inspect).to match(/MyBackend/) end end end diff --git a/spec/mobility/backends/active_record/table_spec.rb b/spec/mobility/backends/active_record/table_spec.rb index afe39bc3d..774768e63 100644 --- a/spec/mobility/backends/active_record/table_spec.rb +++ b/spec/mobility/backends/active_record/table_spec.rb @@ -243,7 +243,7 @@ let(:options) { {} } let(:backend_class) do - Class.new(described_class) { @model_class = Article } + described_class.build_subclass(Article, {}) end it "sets association_name" do diff --git a/spec/mobility/backends/sequel/table_spec.rb b/spec/mobility/backends/sequel/table_spec.rb index b6a09aa1b..291b7a470 100644 --- a/spec/mobility/backends/sequel/table_spec.rb +++ b/spec/mobility/backends/sequel/table_spec.rb @@ -148,7 +148,7 @@ let(:options) { {} } let(:backend_class) do - Class.new(described_class) { @model_class = Article } + described_class.build_subclass(Article, {}) end it "sets association_name" do diff --git a/spec/mobility/plugins/backend_spec.rb b/spec/mobility/plugins/backend_spec.rb index dadaa69cf..15138f1c4 100644 --- a/spec/mobility/plugins/backend_spec.rb +++ b/spec/mobility/plugins/backend_spec.rb @@ -19,7 +19,8 @@ describe "#included" do it "calls build_subclass on backend class with options merged with default options" do - expect(backend_class).to receive(:build_subclass).with(model_class, hash_including(foo: "bar")).and_return(Class.new(backend_class)) + configured_backend_class = double.as_null_object + expect(backend_class).to receive(:build_subclass).with(model_class, hash_including(foo: "bar")).and_return(configured_backend_class) translations = translations_class.new("title", backend: backend_class, foo: "bar") model_class.include translations end @@ -33,11 +34,13 @@ it "freezes backend options after inclusion into model class" do translations = translations_class.new("title", backend: backend_class) model_class.include translations - expect(backend_class.options).to be_frozen + expect(translations.backend_class.options).to be_frozen end it "calls setup_model on backend class with model_class and attributes" do - expect(backend_class).to receive(:setup_model).with(model_class, ["title"]) + configured_backend_class = double("configured backend class") + allow(backend_class).to receive(:build_subclass).and_return(configured_backend_class) + expect(configured_backend_class).to receive(:setup_model).with(model_class, ["title"]) model_class.include translations_class.new("title", backend: backend_class) end @@ -278,8 +281,10 @@ def OtherBackendClass.valid_keys expect(translations_class.new("title").inspect).to eq("#") end - it "calls setup_model on backend" do - expect(FooBackend).to receive(:setup_model).with(model_class, ["title"]) + it "calls setup_model on configured backend" do + configured_backend_class = double + expect(FooBackend).to receive(:build_subclass).with(model_class, backend: [:foo, {}]).and_return(configured_backend_class) + expect(configured_backend_class).to receive(:setup_model).with(model_class, ["title"]) model_class.include translations_class.new("title") end end From a14c3082786f587bcce57d24325acfc5b94d4646 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 16:31:45 +0900 Subject: [PATCH 06/82] Bump version to 3.2.0.alpha --- lib/mobility/version.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mobility/version.rb b/lib/mobility/version.rb index 5cb5d7045..c485d4e4c 100644 --- a/lib/mobility/version.rb +++ b/lib/mobility/version.rb @@ -7,9 +7,9 @@ def self.gem_version module VERSION MAJOR = 1 - MINOR = 1 - TINY = 3 - PRE = nil + MINOR = 2 + TINY = 0 + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end From e5368661f7dbe22aca62c44d5cb56d7e8f9444b5 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 16:37:25 +0900 Subject: [PATCH 07/82] Accept passing configured backend class as third argument to setup --- lib/mobility/backend.rb | 18 ++++-- spec/mobility/backend_spec.rb | 107 ++++++++++++++++++++++++---------- 2 files changed, 90 insertions(+), 35 deletions(-) diff --git a/lib/mobility/backend.rb b/lib/mobility/backend.rb index 879268a57..d2983e15c 100644 --- a/lib/mobility/backend.rb +++ b/lib/mobility/backend.rb @@ -135,9 +135,11 @@ def valid_keys def setup &block if @setup_block setup_block = @setup_block - @setup_block = lambda do |*args| - class_exec(*args, &setup_block) - class_exec(*args, &block) + exec_setup_block = method(:exec_setup_block) + @setup_block = lambda do |attributes, options, backend_class| + [setup_block, block].each do |blk| + exec_setup_block.call(self, attributes, options, backend_class, &blk) + end end else @setup_block = block @@ -189,6 +191,14 @@ def setup_model(_model_class, _attributes) def raise_unconfigured!(method_name) raise UnconfiguredError, "You are calling #{method_name} on an unconfigured backend class." end + + def exec_setup_block(model_class, *args, &block) + if block.arity == 3 + model_class.class_exec(*args[0..2], &block) + else + model_class.class_exec(*args[0..1], &block) + end + end end Translation = Struct.new(:backend, :locale) do @@ -230,7 +240,7 @@ def self.extended(klass) # @param [Hash] options def setup_model(model_class, attribute_names) return unless setup_block = @setup_block - model_class.class_exec(attribute_names, options, &setup_block) + exec_setup_block(model_class, attribute_names, options, self, &setup_block) end def inherited(_) diff --git a/spec/mobility/backend_spec.rb b/spec/mobility/backend_spec.rb index 85a20424c..8539ab9b9 100644 --- a/spec/mobility/backend_spec.rb +++ b/spec/mobility/backend_spec.rb @@ -106,9 +106,9 @@ def each_locale end describe ".setup" do - before do - backend_class.class_eval do - setup do |attributes, options| + context "with two block arguments" do + before do + backend_class.setup do |attributes, options| def self.foo "foo" end @@ -123,32 +123,30 @@ def bar end end end - end - it "stores setup as block which is called in model class" do - model_class = Class.new - backend_class.build_subclass(model_class, foo: "bar").setup_model(model_class, ["title"]) - expect(model_class.foo).to eq("foo") - expect(model_class.new.bar).to eq("bar") - end + it "stores setup as block which is called in model class" do + model_class = Class.new + backend_class.build_subclass(model_class, foo: "bar").setup_model(model_class, ["title"]) + expect(model_class.foo).to eq("foo") + expect(model_class.new.bar).to eq("bar") + end - it "passes attributes and options to setup block when called on class" do - model_class = Class.new - backend_class.build_subclass(model_class, foo: "bar").setup_model(model_class, ["title"]) - expect(model_class.new.my_attributes).to eq(["title"]) - expect(model_class.new.my_options).to eq({ foo: "bar" }) - end + it "passes attributes and options to setup block when called on class" do + model_class = Class.new + backend_class.build_subclass(model_class, foo: "bar").setup_model(model_class, ["title"]) + expect(model_class.new.my_attributes).to eq(["title"]) + expect(model_class.new.my_options).to eq({ foo: "bar" }) + end - it "assigns setup block to descendants" do - model_class = Class.new - subclass = backend_class.build_subclass(model_class, foo: "bar") - subclass.setup_model(model_class, ["title"]) - expect(model_class.foo).to eq("foo") - end + it "assigns setup block to descendants" do + model_class = Class.new + subclass = backend_class.build_subclass(model_class, foo: "bar") + subclass.setup_model(model_class, ["title"]) + expect(model_class.foo).to eq("foo") + end - it "concatenates blocks when called multiple times" do - backend_class.class_eval do - setup do |attributes, options| + it "concatenates blocks when called multiple times" do + backend_class.setup do |attributes, options| def self.foo "foo2" end @@ -159,14 +157,61 @@ def self.foobar "foobar" end end + model_class = Class.new + backend_class.build_subclass(model_class, foo: "bar").setup_model(model_class, ["title", "content"]) + + aggregate_failures do + expect(model_class.foo).to eq("foo2") + expect(model_class.new.baz).to eq("titlecontent baz") + expect(model_class.foobar).to eq("foobar") + end end - model_class = Class.new - backend_class.build_subclass(model_class, foo: "bar").setup_model(model_class, ["title", "content"]) + end - aggregate_failures do - expect(model_class.foo).to eq("foo2") - expect(model_class.new.baz).to eq("titlecontent baz") - expect(model_class.foobar).to eq("foobar") + context "with 3 block arguments" do + before do + backend_class.setup do |attributes, options, klass| + define_method :my_attributes do + attributes + end + define_method :my_options do + options + end + define_method :my_backend_class do + klass + end + end + end + + it "passes configured backend class as third argument" do + model_class = Class.new + configured_backend_class = backend_class.build_subclass(model_class, foo: "bar") + configured_backend_class.setup_model(model_class, ["title"]) + + model = model_class.new + + expect(model.my_attributes).to eq(["title"]) + expect(model.my_options).to eq({ foo: "bar" }) + expect(model.my_backend_class).to eq(configured_backend_class) + end + + it "works with block concatenation" do + backend_class.setup do |attributes, options| + define_method :my_attributes_2 do + attributes + end + end + model_class = Class.new + configured_backend_class = backend_class.build_subclass(model_class, foo: "bar") + configured_backend_class.setup_model(model_class, ["title"]) + + model = model_class.new + + aggregate_failures do + expect(model.my_attributes).to eq(["title"]) + expect(model.my_options).to eq({ foo: "bar" }) + expect(model.my_backend_class).to eq(configured_backend_class) + end end end end From 707ed9254d459b8c2e90d63f8eaf1561b1818283 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 17:09:44 +0900 Subject: [PATCH 08/82] Add inline docs mentioning optional third argument --- lib/mobility/backend.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/mobility/backend.rb b/lib/mobility/backend.rb index d2983e15c..7a1df5bd2 100644 --- a/lib/mobility/backend.rb +++ b/lib/mobility/backend.rb @@ -21,8 +21,8 @@ module Mobility corresponding to valid keys for configuring this backend. - implement a +configure+ class method to apply any normalization to the keys on the options hash included in +valid_keys+ -- call the +setup+ method yielding attributes and options to configure the - model class +- call the +setup+ method yielding attributes and options (and optionally the + configured backend class) to configure the model class @example Defining a Backend class MyBackend @@ -47,6 +47,13 @@ def self.configure(options) setup do |attributes, options| # Do something with attributes and options in context of model class. end + + # The block can optionally take the configured backend class as its third + # argument: + # + # setup do |attributes, options, backend_class| + # ... + # end end @see Mobility::Translations From 968e4c1a64c5c6842071e972e49dc30fea8486ed Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 17:15:45 +0900 Subject: [PATCH 09/82] Extract defining after_destroy callback into class method --- .../backends/active_record/key_value.rb | 22 +++++++++----- lib/mobility/backends/sequel/key_value.rb | 29 ++++++++++++------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/lib/mobility/backends/active_record/key_value.rb b/lib/mobility/backends/active_record/key_value.rb index 2d101b2f4..8cbfab167 100644 --- a/lib/mobility/backends/active_record/key_value.rb +++ b/lib/mobility/backends/active_record/key_value.rb @@ -65,6 +65,18 @@ def apply_scope(relation, predicate, locale = Mobility.locale, invert: false) end end + # Called from setup block. Can be overridden to customize behaviour. + def define_after_destroy_callback + # Ensure we only call after destroy hook once per translations class + b = self + translation_classes = [class_name, *Mobility::Backends::ActiveRecord::KeyValue::Translation.descendants].uniq + model_class.after_destroy do + @mobility_after_destroy_translation_classes = [] unless defined?(@mobility_after_destroy_translation_classes) + (translation_classes - @mobility_after_destroy_translation_classes).each { |klass| klass.where(b.belongs_to => self).destroy_all } + @mobility_after_destroy_translation_classes += translation_classes + end + end + private def join_translations(relation, key, locale, join_type) @@ -149,7 +161,7 @@ def visit_default(_) end end - setup do |attributes, options| + setup do |attributes, options, backend_class| association_name = options[:association_name] translation_class = options[:class_name] key_column = options[:key_column] @@ -191,13 +203,7 @@ def visit_default(_) include const_set(module_name, callback_methods) end - # Ensure we only call after destroy hook once per translations class - translation_classes = [translation_class, *Mobility::Backends::ActiveRecord::KeyValue::Translation.descendants].uniq - after_destroy do - @mobility_after_destroy_translation_classes = [] unless defined?(@mobility_after_destroy_translation_classes) - (translation_classes - @mobility_after_destroy_translation_classes).each { |klass| klass.where(belongs_to => self).destroy_all } - @mobility_after_destroy_translation_classes += translation_classes - end + backend_class.define_after_destroy_callback end # Returns translation for a given locale, or builds one if none is present. diff --git a/lib/mobility/backends/sequel/key_value.rb b/lib/mobility/backends/sequel/key_value.rb index e47875af9..f02f1f0ae 100644 --- a/lib/mobility/backends/sequel/key_value.rb +++ b/lib/mobility/backends/sequel/key_value.rb @@ -51,6 +51,22 @@ def prepare_dataset(dataset, predicate, locale) end end + # Called from setup block. Can be overridden to customize behaviour. + def define_after_destroy_callback + # Clean up *all* leftover translations of this model, only once. + b = self + translation_classes = [class_name, *Mobility::Backends::Sequel::KeyValue::Translation.descendants].uniq + model_class.define_method :after_destroy do + super() + + @mobility_after_destroy_translation_classes = [] unless defined?(@mobility_after_destroy_translation_classes) + (translation_classes - @mobility_after_destroy_translation_classes).each do |klass| + klass.where(:"#{b.belongs_to}_id" => id, :"#{b.belongs_to}_type" => self.class.name).destroy + end + @mobility_after_destroy_translation_classes += translation_classes + end + end + private def join_translations(dataset, attr, locale, join_type) @@ -125,7 +141,7 @@ def visit_sql_identifier(identifier, locale) backend = self - setup do |attributes, options| + setup do |attributes, options, backend_class| association_name = options[:association_name] translation_class = options[:class_name] key_column = options[:key_column] @@ -165,17 +181,8 @@ def visit_sql_identifier(identifier, locale) end include callback_methods - # Clean up *all* leftover translations of this model, only once. - translation_classes = [translation_class, *Mobility::Backends::Sequel::KeyValue::Translation.descendants].uniq - define_method :after_destroy do - super() + backend_class.define_after_destroy_callback - @mobility_after_destroy_translation_classes = [] unless defined?(@mobility_after_destroy_translation_classes) - (translation_classes - @mobility_after_destroy_translation_classes).each do |klass| - klass.where(belongs_to_id => id, belongs_to_type => self.class.name).destroy - end - @mobility_after_destroy_translation_classes += translation_classes - end include(mod = Module.new) backend.define_column_changes(mod, attributes) end From b13e59689204e4793116053d02a19a27d4a63167 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 20:47:58 +0900 Subject: [PATCH 10/82] Extract defining save callbacks into class method --- .../backends/active_record/key_value.rb | 16 +++++++---- lib/mobility/backends/sequel/key_value.rb | 28 +++++++++++-------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/lib/mobility/backends/active_record/key_value.rb b/lib/mobility/backends/active_record/key_value.rb index 8cbfab167..1582b1e0f 100644 --- a/lib/mobility/backends/active_record/key_value.rb +++ b/lib/mobility/backends/active_record/key_value.rb @@ -65,6 +65,15 @@ def apply_scope(relation, predicate, locale = Mobility.locale, invert: false) end end + def define_before_save_callback + b = self + model_class.before_save do + send(b.association_name).select { |t| t.send(b.value_column).blank? }.each do |translation| + send(b.association_name).destroy(translation) + end + end + end + # Called from setup block. Can be overridden to customize behaviour. def define_after_destroy_callback # Ensure we only call after destroy hook once per translations class @@ -182,11 +191,8 @@ def visit_default(_) class_name: translation_class.name, inverse_of: belongs_to, autosave: true - before_save do - send(association_name).select { |t| t.send(value_column).blank? }.each do |translation| - send(association_name).destroy(translation) - end - end + + backend_class.define_before_save_callback module_name = "MobilityArKeyValue#{association_name.to_s.camelcase}" unless const_defined?(module_name) diff --git a/lib/mobility/backends/sequel/key_value.rb b/lib/mobility/backends/sequel/key_value.rb index f02f1f0ae..c0a380b7f 100644 --- a/lib/mobility/backends/sequel/key_value.rb +++ b/lib/mobility/backends/sequel/key_value.rb @@ -51,6 +51,21 @@ def prepare_dataset(dataset, predicate, locale) end end + def define_save_callbacks(attributes) + b = self + callback_methods = Module.new do + define_method :before_save do + super() + send(b.association_name).select { |t| attributes.include?(t.__send__(b.key_column)) && Util.blank?(t.__send__(b.value_column)) }.each(&:destroy) + end + define_method :after_save do + super() + attributes.each { |attribute| mobility_backends[attribute].save_translations } + end + end + model_class.include callback_methods + end + # Called from setup block. Can be overridden to customize behaviour. def define_after_destroy_callback # Clean up *all* leftover translations of this model, only once. @@ -169,18 +184,7 @@ def visit_sql_identifier(identifier, locale) clearer: proc { send_(:"#{association_name}_dataset").update(belongs_to_id => nil, belongs_to_type => nil) }, class: translation_class - callback_methods = Module.new do - define_method :before_save do - super() - send(association_name).select { |t| attributes.include?(t.__send__(key_column)) && Util.blank?(t.__send__(value_column)) }.each(&:destroy) - end - define_method :after_save do - super() - attributes.each { |attribute| mobility_backends[attribute].save_translations } - end - end - include callback_methods - + backend_class.define_save_callbacks(attributes) backend_class.define_after_destroy_callback include(mod = Module.new) From 9c54414599a73aa6727967362cd0d6b6895b6c10 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 20:48:08 +0900 Subject: [PATCH 11/82] Fix indenting --- lib/mobility/backends/active_record/key_value.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mobility/backends/active_record/key_value.rb b/lib/mobility/backends/active_record/key_value.rb index 1582b1e0f..439ee63ca 100644 --- a/lib/mobility/backends/active_record/key_value.rb +++ b/lib/mobility/backends/active_record/key_value.rb @@ -173,9 +173,9 @@ def visit_default(_) setup do |attributes, options, backend_class| association_name = options[:association_name] translation_class = options[:class_name] - key_column = options[:key_column] - value_column = options[:value_column] - belongs_to = options[:belongs_to] + key_column = options[:key_column] + value_column = options[:value_column] + belongs_to = options[:belongs_to] # Track all attributes for this association, so that we can limit the scope # of keys for the association to only these attributes. We need to track the From f93309c246a8feb2e389fcce6d34c47174070aeb Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 21:07:08 +0900 Subject: [PATCH 12/82] Extract defining association into backend class method --- .../backends/active_record/key_value.rb | 37 +++++++++------ lib/mobility/backends/sequel/key_value.rb | 46 ++++++++++--------- 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/lib/mobility/backends/active_record/key_value.rb b/lib/mobility/backends/active_record/key_value.rb index 439ee63ca..5ea3ccbc5 100644 --- a/lib/mobility/backends/active_record/key_value.rb +++ b/lib/mobility/backends/active_record/key_value.rb @@ -65,6 +65,27 @@ def apply_scope(relation, predicate, locale = Mobility.locale, invert: false) end end + # Called from setup block. Can be overridden to customize behaviour. + def define_has_many_association(attributes) + # Track all attributes for this association, so that we can limit the scope + # of keys for the association to only these attributes. We need to track the + # attributes assigned to the association in case this setup code is called + # multiple times, so we don't "forget" earlier attributes. + # + attrs_method_name = :"__#{association_name}_attributes" + association_attributes = (model_class.instance_variable_get(:"@#{attrs_method_name}") || []) + attributes + model_class.instance_variable_set(:"@#{attrs_method_name}", association_attributes) + + b = self + + model_class.has_many association_name, ->{ where b.key_column => association_attributes }, + as: belongs_to, + class_name: class_name.name, + inverse_of: belongs_to, + autosave: true + end + + # Called from setup block. Can be overridden to customize behaviour. def define_before_save_callback b = self model_class.before_save do @@ -177,21 +198,7 @@ def visit_default(_) value_column = options[:value_column] belongs_to = options[:belongs_to] - # Track all attributes for this association, so that we can limit the scope - # of keys for the association to only these attributes. We need to track the - # attributes assigned to the association in case this setup code is called - # multiple times, so we don't "forget" earlier attributes. - # - attrs_method_name = :"__#{association_name}_attributes" - association_attributes = (instance_variable_get(:"@#{attrs_method_name}") || []) + attributes - instance_variable_set(:"@#{attrs_method_name}", association_attributes) - - has_many association_name, ->{ where key_column => association_attributes }, - as: belongs_to, - class_name: translation_class.name, - inverse_of: belongs_to, - autosave: true - + backend_class.define_has_many_association(attributes) backend_class.define_before_save_callback module_name = "MobilityArKeyValue#{association_name.to_s.camelcase}" diff --git a/lib/mobility/backends/sequel/key_value.rb b/lib/mobility/backends/sequel/key_value.rb index c0a380b7f..e59813ca0 100644 --- a/lib/mobility/backends/sequel/key_value.rb +++ b/lib/mobility/backends/sequel/key_value.rb @@ -51,6 +51,30 @@ def prepare_dataset(dataset, predicate, locale) end end + def define_one_to_many_association(attributes) + belongs_to_id = :"#{belongs_to}_id" + belongs_to_type = :"#{belongs_to}_type" + + # Track all attributes for this association, so that we can limit the scope + # of keys for the association to only these attributes. We need to track the + # attributes assigned to the association in case this setup code is called + # multiple times, so we don't "forget" earlier attributes. + # + attrs_method_name = :"#{association_name}_attributes" + association_attributes = (model_class.instance_variable_get(:"@#{attrs_method_name}") || []) + attributes + model_class.instance_variable_set(:"@#{attrs_method_name}", association_attributes) + + model_class.one_to_many association_name, + reciprocal: belongs_to, + key: belongs_to_id, + reciprocal_type: :one_to_many, + conditions: { belongs_to_type => model_class.to_s, key_column => association_attributes }, + adder: proc { |translation| translation.update(belongs_to_id => pk, belongs_to_type => self.class.to_s) }, + remover: proc { |translation| translation.update(belongs_to_id => nil, belongs_to_type => nil) }, + clearer: proc { send_(:"#{association_name}_dataset").update(belongs_to_id => nil, belongs_to_type => nil) }, + class: class_name + end + def define_save_callbacks(attributes) b = self callback_methods = Module.new do @@ -162,28 +186,8 @@ def visit_sql_identifier(identifier, locale) key_column = options[:key_column] value_column = options[:value_column] belongs_to = options[:belongs_to] - belongs_to_id = :"#{belongs_to}_id" - belongs_to_type = :"#{belongs_to}_type" - - # Track all attributes for this association, so that we can limit the scope - # of keys for the association to only these attributes. We need to track the - # attributes assigned to the association in case this setup code is called - # multiple times, so we don't "forget" earlier attributes. - # - attrs_method_name = :"#{association_name}_attributes" - association_attributes = (instance_variable_get(:"@#{attrs_method_name}") || []) + attributes - instance_variable_set(:"@#{attrs_method_name}", association_attributes) - - one_to_many association_name, - reciprocal: belongs_to, - key: belongs_to_id, - reciprocal_type: :one_to_many, - conditions: { belongs_to_type => self.to_s, key_column => association_attributes }, - adder: proc { |translation| translation.update(belongs_to_id => pk, belongs_to_type => self.class.to_s) }, - remover: proc { |translation| translation.update(belongs_to_id => nil, belongs_to_type => nil) }, - clearer: proc { send_(:"#{association_name}_dataset").update(belongs_to_id => nil, belongs_to_type => nil) }, - class: translation_class + backend_class.define_one_to_many_association(attributes) backend_class.define_save_callbacks(attributes) backend_class.define_after_destroy_callback From da9de510d67621a3a4dbfab83f3febda7636f18b Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 21:13:38 +0900 Subject: [PATCH 13/82] Extract defining initialize_dup into class method --- .../backends/active_record/key_value.rb | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/lib/mobility/backends/active_record/key_value.rb b/lib/mobility/backends/active_record/key_value.rb index 5ea3ccbc5..4f4581690 100644 --- a/lib/mobility/backends/active_record/key_value.rb +++ b/lib/mobility/backends/active_record/key_value.rb @@ -85,6 +85,24 @@ def define_has_many_association(attributes) autosave: true end + def define_initialize_dup + b = self + module_name = "MobilityArKeyValue#{association_name.to_s.camelcase}" + unless const_defined?(module_name) + callback_methods = Module.new do + define_method :initialize_dup do |source| + super(source) + self.send("#{b.association_name}=", source.send(b.association_name).map(&:dup)) + # Set inverse on associations + send(b.association_name).each do |translation| + translation.send(:"#{b.belongs_to}=", self) + end + end + end + model_class.include const_set(module_name, callback_methods) + end + end + # Called from setup block. Can be overridden to customize behaviour. def define_before_save_callback b = self @@ -191,31 +209,10 @@ def visit_default(_) end end - setup do |attributes, options, backend_class| - association_name = options[:association_name] - translation_class = options[:class_name] - key_column = options[:key_column] - value_column = options[:value_column] - belongs_to = options[:belongs_to] - + setup do |attributes, _options, backend_class| backend_class.define_has_many_association(attributes) + backend_class.define_initialize_dup backend_class.define_before_save_callback - - module_name = "MobilityArKeyValue#{association_name.to_s.camelcase}" - unless const_defined?(module_name) - callback_methods = Module.new do - define_method :initialize_dup do |source| - super(source) - self.send("#{association_name}=", source.send(association_name).map(&:dup)) - # Set inverse on associations - send(association_name).each do |translation| - translation.send(:"#{belongs_to}=", self) - end - end - end - include const_set(module_name, callback_methods) - end - backend_class.define_after_destroy_callback end From 4b1659b25e0d08b1028aad0bf6baf6a900d998ce Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 21:13:58 +0900 Subject: [PATCH 14/82] Sequel KeyValue setup block cleanup --- lib/mobility/backends/sequel/key_value.rb | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/mobility/backends/sequel/key_value.rb b/lib/mobility/backends/sequel/key_value.rb index e59813ca0..2a556ebcb 100644 --- a/lib/mobility/backends/sequel/key_value.rb +++ b/lib/mobility/backends/sequel/key_value.rb @@ -180,19 +180,13 @@ def visit_sql_identifier(identifier, locale) backend = self - setup do |attributes, options, backend_class| - association_name = options[:association_name] - translation_class = options[:class_name] - key_column = options[:key_column] - value_column = options[:value_column] - belongs_to = options[:belongs_to] - + setup do |attributes, _options, backend_class| backend_class.define_one_to_many_association(attributes) backend_class.define_save_callbacks(attributes) backend_class.define_after_destroy_callback include(mod = Module.new) - backend.define_column_changes(mod, attributes) + backend_class.define_column_changes(mod, attributes) end # Returns translation for a given locale, or initializes one if none is present. From 0428a489da895f2fdabd596cf7690565333c6e7e Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 21:20:24 +0900 Subject: [PATCH 15/82] Use backend_class everywhere --- lib/mobility/backends/sequel/container.rb | 6 ++---- lib/mobility/backends/sequel/key_value.rb | 2 -- lib/mobility/backends/sequel/pg_hash.rb | 8 +++----- lib/mobility/backends/sequel/table.rb | 6 ++---- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/lib/mobility/backends/sequel/container.rb b/lib/mobility/backends/sequel/container.rb index 73fcfb583..228ae11e5 100644 --- a/lib/mobility/backends/sequel/container.rb +++ b/lib/mobility/backends/sequel/container.rb @@ -57,9 +57,7 @@ def each_locale end end - backend = self - - setup do |attributes, options| + setup do |attributes, options, backend_class| column_name = options[:column_name] mod = Module.new do define_method :before_validation do @@ -71,7 +69,7 @@ def each_locale end end include mod - backend.define_hash_initializer(mod, [column_name]) + backend_class.define_hash_initializer(mod, [column_name]) plugin :defaults_setter attributes.each { |attribute| default_values[attribute.to_sym] = {} } diff --git a/lib/mobility/backends/sequel/key_value.rb b/lib/mobility/backends/sequel/key_value.rb index 2a556ebcb..4b0c0741f 100644 --- a/lib/mobility/backends/sequel/key_value.rb +++ b/lib/mobility/backends/sequel/key_value.rb @@ -178,8 +178,6 @@ def visit_sql_identifier(identifier, locale) end end - backend = self - setup do |attributes, _options, backend_class| backend_class.define_one_to_many_association(attributes) backend_class.define_save_callbacks(attributes) diff --git a/lib/mobility/backends/sequel/pg_hash.rb b/lib/mobility/backends/sequel/pg_hash.rb index 52f12fe2d..bacc5ddfc 100644 --- a/lib/mobility/backends/sequel/pg_hash.rb +++ b/lib/mobility/backends/sequel/pg_hash.rb @@ -33,9 +33,7 @@ def translations model[column_name.to_sym] end - backend = self - - setup do |attributes, options| + setup do |attributes, options, backend_class| columns = attributes.map { |attribute| (options[:column_affix] % attribute).to_sym } mod = Module.new do @@ -47,8 +45,8 @@ def translations end end include mod - backend.define_hash_initializer(mod, columns) - backend.define_column_changes(mod, attributes, column_affix: options[:column_affix]) + backend_class.define_hash_initializer(mod, columns) + backend_class.define_column_changes(mod, attributes, column_affix: options[:column_affix]) plugin :defaults_setter columns.each { |column| default_values[column] = {} } diff --git a/lib/mobility/backends/sequel/table.rb b/lib/mobility/backends/sequel/table.rb index 5f3494c34..71c5860fe 100644 --- a/lib/mobility/backends/sequel/table.rb +++ b/lib/mobility/backends/sequel/table.rb @@ -116,9 +116,7 @@ def visit_boolean(boolean, locale) end end - backend = self - - setup do |attributes, options| + setup do |attributes, options, backend_class| association_name = options[:association_name] subclass_name = options[:subclass_name] @@ -155,7 +153,7 @@ def visit_boolean(boolean, locale) include callback_methods include(mod = Module.new) - backend.define_column_changes(mod, attributes) + backend_class.define_column_changes(mod, attributes) end def translation_for(locale, **) From fbeb7b08a2d2df2ed44de734453d47c81bba209e Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 21:24:59 +0900 Subject: [PATCH 16/82] Add missing inline docs --- lib/mobility/backends/active_record/key_value.rb | 1 + lib/mobility/backends/sequel/key_value.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/mobility/backends/active_record/key_value.rb b/lib/mobility/backends/active_record/key_value.rb index 4f4581690..6b1db206a 100644 --- a/lib/mobility/backends/active_record/key_value.rb +++ b/lib/mobility/backends/active_record/key_value.rb @@ -85,6 +85,7 @@ def define_has_many_association(attributes) autosave: true end + # Called from setup block. Can be overridden to customize behaviour. def define_initialize_dup b = self module_name = "MobilityArKeyValue#{association_name.to_s.camelcase}" diff --git a/lib/mobility/backends/sequel/key_value.rb b/lib/mobility/backends/sequel/key_value.rb index 4b0c0741f..51b30bf0b 100644 --- a/lib/mobility/backends/sequel/key_value.rb +++ b/lib/mobility/backends/sequel/key_value.rb @@ -51,6 +51,7 @@ def prepare_dataset(dataset, predicate, locale) end end + # Called from setup block. Can be overridden to customize behaviour. def define_one_to_many_association(attributes) belongs_to_id = :"#{belongs_to}_id" belongs_to_type = :"#{belongs_to}_type" @@ -75,6 +76,7 @@ def define_one_to_many_association(attributes) class: class_name end + # Called from setup block. Can be overridden to customize behaviour. def define_save_callbacks(attributes) b = self callback_methods = Module.new do From b5d347aef1db1dc9e56cfea5bbc4e035200db4bd Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 23 Sep 2021 21:39:10 +0900 Subject: [PATCH 17/82] Pass model class to backend class methods --- .../backends/active_record/key_value.rb | 32 ++++++++++--------- lib/mobility/backends/sequel/key_value.rb | 28 ++++++++-------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/lib/mobility/backends/active_record/key_value.rb b/lib/mobility/backends/active_record/key_value.rb index 6b1db206a..e81a356fe 100644 --- a/lib/mobility/backends/active_record/key_value.rb +++ b/lib/mobility/backends/active_record/key_value.rb @@ -66,19 +66,19 @@ def apply_scope(relation, predicate, locale = Mobility.locale, invert: false) end # Called from setup block. Can be overridden to customize behaviour. - def define_has_many_association(attributes) + def define_has_many_association(klass, attributes) # Track all attributes for this association, so that we can limit the scope # of keys for the association to only these attributes. We need to track the # attributes assigned to the association in case this setup code is called # multiple times, so we don't "forget" earlier attributes. # attrs_method_name = :"__#{association_name}_attributes" - association_attributes = (model_class.instance_variable_get(:"@#{attrs_method_name}") || []) + attributes - model_class.instance_variable_set(:"@#{attrs_method_name}", association_attributes) + association_attributes = (klass.instance_variable_get(:"@#{attrs_method_name}") || []) + attributes + klass.instance_variable_set(:"@#{attrs_method_name}", association_attributes) b = self - model_class.has_many association_name, ->{ where b.key_column => association_attributes }, + klass.has_many association_name, ->{ where b.key_column => association_attributes }, as: belongs_to, class_name: class_name.name, inverse_of: belongs_to, @@ -86,7 +86,7 @@ def define_has_many_association(attributes) end # Called from setup block. Can be overridden to customize behaviour. - def define_initialize_dup + def define_initialize_dup(klass) b = self module_name = "MobilityArKeyValue#{association_name.to_s.camelcase}" unless const_defined?(module_name) @@ -100,14 +100,14 @@ def define_initialize_dup end end end - model_class.include const_set(module_name, callback_methods) + klass.include const_set(module_name, callback_methods) end end # Called from setup block. Can be overridden to customize behaviour. - def define_before_save_callback + def define_before_save_callback(klass) b = self - model_class.before_save do + klass.before_save do send(b.association_name).select { |t| t.send(b.value_column).blank? }.each do |translation| send(b.association_name).destroy(translation) end @@ -115,13 +115,15 @@ def define_before_save_callback end # Called from setup block. Can be overridden to customize behaviour. - def define_after_destroy_callback + def define_after_destroy_callback(klass) # Ensure we only call after destroy hook once per translations class b = self translation_classes = [class_name, *Mobility::Backends::ActiveRecord::KeyValue::Translation.descendants].uniq - model_class.after_destroy do + klass.after_destroy do @mobility_after_destroy_translation_classes = [] unless defined?(@mobility_after_destroy_translation_classes) - (translation_classes - @mobility_after_destroy_translation_classes).each { |klass| klass.where(b.belongs_to => self).destroy_all } + (translation_classes - @mobility_after_destroy_translation_classes).each do |translation_class| + translation_class.where(b.belongs_to => self).destroy_all + end @mobility_after_destroy_translation_classes += translation_classes end end @@ -211,10 +213,10 @@ def visit_default(_) end setup do |attributes, _options, backend_class| - backend_class.define_has_many_association(attributes) - backend_class.define_initialize_dup - backend_class.define_before_save_callback - backend_class.define_after_destroy_callback + backend_class.define_has_many_association(self, attributes) + backend_class.define_initialize_dup(self) + backend_class.define_before_save_callback(self) + backend_class.define_after_destroy_callback(self) end # Returns translation for a given locale, or builds one if none is present. diff --git a/lib/mobility/backends/sequel/key_value.rb b/lib/mobility/backends/sequel/key_value.rb index 51b30bf0b..c422da561 100644 --- a/lib/mobility/backends/sequel/key_value.rb +++ b/lib/mobility/backends/sequel/key_value.rb @@ -52,7 +52,7 @@ def prepare_dataset(dataset, predicate, locale) end # Called from setup block. Can be overridden to customize behaviour. - def define_one_to_many_association(attributes) + def define_one_to_many_association(klass, attributes) belongs_to_id = :"#{belongs_to}_id" belongs_to_type = :"#{belongs_to}_type" @@ -62,14 +62,14 @@ def define_one_to_many_association(attributes) # multiple times, so we don't "forget" earlier attributes. # attrs_method_name = :"#{association_name}_attributes" - association_attributes = (model_class.instance_variable_get(:"@#{attrs_method_name}") || []) + attributes - model_class.instance_variable_set(:"@#{attrs_method_name}", association_attributes) + association_attributes = (klass.instance_variable_get(:"@#{attrs_method_name}") || []) + attributes + klass.instance_variable_set(:"@#{attrs_method_name}", association_attributes) - model_class.one_to_many association_name, + klass.one_to_many association_name, reciprocal: belongs_to, key: belongs_to_id, reciprocal_type: :one_to_many, - conditions: { belongs_to_type => model_class.to_s, key_column => association_attributes }, + conditions: { belongs_to_type => klass.to_s, key_column => association_attributes }, adder: proc { |translation| translation.update(belongs_to_id => pk, belongs_to_type => self.class.to_s) }, remover: proc { |translation| translation.update(belongs_to_id => nil, belongs_to_type => nil) }, clearer: proc { send_(:"#{association_name}_dataset").update(belongs_to_id => nil, belongs_to_type => nil) }, @@ -77,7 +77,7 @@ def define_one_to_many_association(attributes) end # Called from setup block. Can be overridden to customize behaviour. - def define_save_callbacks(attributes) + def define_save_callbacks(klass, attributes) b = self callback_methods = Module.new do define_method :before_save do @@ -89,20 +89,20 @@ def define_save_callbacks(attributes) attributes.each { |attribute| mobility_backends[attribute].save_translations } end end - model_class.include callback_methods + klass.include callback_methods end # Called from setup block. Can be overridden to customize behaviour. - def define_after_destroy_callback + def define_after_destroy_callback(klass) # Clean up *all* leftover translations of this model, only once. b = self translation_classes = [class_name, *Mobility::Backends::Sequel::KeyValue::Translation.descendants].uniq - model_class.define_method :after_destroy do + klass.define_method :after_destroy do super() @mobility_after_destroy_translation_classes = [] unless defined?(@mobility_after_destroy_translation_classes) - (translation_classes - @mobility_after_destroy_translation_classes).each do |klass| - klass.where(:"#{b.belongs_to}_id" => id, :"#{b.belongs_to}_type" => self.class.name).destroy + (translation_classes - @mobility_after_destroy_translation_classes).each do |translation_class| + translation_class.where(:"#{b.belongs_to}_id" => id, :"#{b.belongs_to}_type" => self.class.name).destroy end @mobility_after_destroy_translation_classes += translation_classes end @@ -181,9 +181,9 @@ def visit_sql_identifier(identifier, locale) end setup do |attributes, _options, backend_class| - backend_class.define_one_to_many_association(attributes) - backend_class.define_save_callbacks(attributes) - backend_class.define_after_destroy_callback + backend_class.define_one_to_many_association(self, attributes) + backend_class.define_save_callbacks(self, attributes) + backend_class.define_after_destroy_callback(self) include(mod = Module.new) backend_class.define_column_changes(mod, attributes) From f218c7c5425258826e6f7a6b7ac66097d96722e0 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sat, 25 Sep 2021 21:58:18 +0900 Subject: [PATCH 18/82] Add Sequel querying integration spec --- spec/integration/sequel_compatibility_spec.rb | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/spec/integration/sequel_compatibility_spec.rb b/spec/integration/sequel_compatibility_spec.rb index e363e0181..07a311b19 100644 --- a/spec/integration/sequel_compatibility_spec.rb +++ b/spec/integration/sequel_compatibility_spec.rb @@ -2,4 +2,44 @@ #TODO: Add general compatibility specs for Sequel describe "Sequel compatibility", orm: :sequel do + include Helpers::Plugins + include Helpers::Translates + # Enable all plugins that are enabled by default pre v1.0 + plugins :sequel, :reader, :writer, :cache, :dirty, :presence, :query, :fallbacks + + before do + stub_const 'Article', Class.new(Sequel::Model) + Article.dataset = DB[:articles] + Article + end + + describe "querying on translated and untranslated attributes" do + %i[key_value table].each do |backend| + #TODO: update querying examples to correctly test untranslated attributes + context "#{backend} backend" do + before do + options = { backend: backend, fallbacks: false } + options[:type] = :string if backend == :key_value + translates Article, :title, **options + end + let!(:article1) { Article.create(title: "foo", slug: "bar") } + let!(:article2) { Article.create( slug: "baz") } + let!(:article4) { Article.create(title: "foo" ) } + + it "works with hash arguments" do + expect(Article.i18n.where(title: "foo", slug: "bar").select_all(:articles).all).to eq([article1]) + expect(Article.i18n.where(title: "foo" ).select_all(:articles).all).to match_array([article1, article4]) + expect(Article.i18n.where(title: "foo", slug: "baz").select_all(:articles).all).to eq([]) + expect(Article.i18n.where( slug: "baz").select_all(:articles).all).to match_array([article2]) + end + + it "works with virtual rows" do + expect(Article.i18n { (title =~ "foo") & (slug =~ "bar") }.select_all(:articles).all).to eq([article1]) + expect(Article.i18n { (title =~ "foo") }.select_all(:articles).all).to match_array([article1, article4]) + expect(Article.i18n { (title =~ "foo") & (slug =~ "baz") }.select_all(:articles).all).to eq([]) + expect(Article.i18n { (slug =~ "baz") }.select_all(:articles).all).to eq([article2]) + end + end + end + end end From a6c7eaf4cec898c5b8575fd16104da4ed759cb51 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sat, 25 Sep 2021 21:58:37 +0900 Subject: [PATCH 19/82] Fix handling of Sequel::SQL::Expression in KeyValue backend --- lib/mobility/backends/sequel/key_value.rb | 2 +- lib/mobility/plugins/sequel/query.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mobility/backends/sequel/key_value.rb b/lib/mobility/backends/sequel/key_value.rb index c422da561..3c805fbaa 100644 --- a/lib/mobility/backends/sequel/key_value.rb +++ b/lib/mobility/backends/sequel/key_value.rb @@ -131,7 +131,7 @@ def visit(predicate, locale) visit_sql_identifier(predicate, locale) when ::Sequel::SQL::BooleanExpression visit_boolean(predicate, locale) - when ::Sequel::SQL::Expression + when ::Sequel::SQL::ComplexExpression visit(predicate.args, locale) else {} diff --git a/lib/mobility/plugins/sequel/query.rb b/lib/mobility/plugins/sequel/query.rb index 041963d1e..a84b4af1e 100644 --- a/lib/mobility/plugins/sequel/query.rb +++ b/lib/mobility/plugins/sequel/query.rb @@ -61,7 +61,7 @@ def method_missing(m, *args) locale = args[0] || @global_locale @locales |= [locale] @model_class.mobility_backend_class(m).build_op(m.to_s, locale) - elsif @model_class.columns.include?(m.to_s) + elsif @model_class.columns.include?(m) ::Sequel::SQL::QualifiedIdentifier.new(@model_class.table_name, m) else super From 72959deb7420b5121a6f2d87279128409d280a2c Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sat, 25 Sep 2021 22:19:17 +0900 Subject: [PATCH 20/82] Expression -> ComplexExpression This code is not actually covered by tests, but Sequel::SQL::Expression does not have `args`, ComplexExpression does, so let's fix this. --- lib/mobility/backends/sequel/table.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mobility/backends/sequel/table.rb b/lib/mobility/backends/sequel/table.rb index 71c5860fe..bd1cb40c0 100644 --- a/lib/mobility/backends/sequel/table.rb +++ b/lib/mobility/backends/sequel/table.rb @@ -84,7 +84,7 @@ def visit(predicate, locale) visit_sql_identifier(predicate, locale) when ::Sequel::SQL::BooleanExpression visit_boolean(predicate, locale) - when ::Sequel::SQL::Expression + when ::Sequel::SQL::ComplexExpression visit(predicate.args, locale) else nil From 98c3de2829b2c1a8a9efb1b4fe5747817f2b191b Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sun, 26 Sep 2021 10:01:16 +0900 Subject: [PATCH 21/82] Remover Ruby 2.5 from build --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4467b600c..56e6d910a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,10 +58,6 @@ jobs: feature: 'unit' orm: experimental: false - - ruby: '2.5' - feature: 'unit' - orm: - experimental: false - ruby: '3.0' feature: 'rails' orm: From 7cbca6430efe1b6fe48f5165dc99ebca0f505000 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 9 Apr 2021 10:26:47 +0900 Subject: [PATCH 22/82] Add ColumnFallback plugin --- lib/mobility/plugins/active_record.rb | 2 + .../plugins/active_record/column_fallback.rb | 139 +++++++++++++ lib/mobility/plugins/column_fallback.rb | 15 ++ lib/mobility/plugins/sequel.rb | 2 + .../plugins/sequel/column_fallback.rb | 133 +++++++++++++ .../active_record/column_fallback_spec.rb | 177 +++++++++++++++++ .../plugins/sequel/column_fallback_spec.rb | 182 ++++++++++++++++++ 7 files changed, 650 insertions(+) create mode 100644 lib/mobility/plugins/active_record/column_fallback.rb create mode 100644 lib/mobility/plugins/column_fallback.rb create mode 100644 lib/mobility/plugins/sequel/column_fallback.rb create mode 100644 spec/mobility/plugins/active_record/column_fallback_spec.rb create mode 100644 spec/mobility/plugins/sequel/column_fallback_spec.rb diff --git a/lib/mobility/plugins/active_record.rb b/lib/mobility/plugins/active_record.rb index 2f410d4b1..d357a3012 100644 --- a/lib/mobility/plugins/active_record.rb +++ b/lib/mobility/plugins/active_record.rb @@ -4,6 +4,7 @@ require_relative "./active_record/cache" require_relative "./active_record/query" require_relative "./active_record/uniqueness_validation" +require_relative "./active_record/column_fallback" module Mobility =begin @@ -24,6 +25,7 @@ module ActiveRecord requires :active_record_cache requires :active_record_query requires :active_record_uniqueness_validation + requires :active_record_column_fallback included_hook do |klass| diff --git a/lib/mobility/plugins/active_record/column_fallback.rb b/lib/mobility/plugins/active_record/column_fallback.rb new file mode 100644 index 000000000..0b4b4bb3a --- /dev/null +++ b/lib/mobility/plugins/active_record/column_fallback.rb @@ -0,0 +1,139 @@ +# frozen-string-literal: true + +module Mobility + module Plugins +=begin + +Plugin to use an original column for a given locale, and otherwise use the backend. + +=end + module ActiveRecord + module ColumnFallback + extend Plugin + + requires :column_fallback, include: false + + included_hook do |_, backend_class| + case (column_fallback = options[:column_fallback]) + when TrueClass + backend_class.include I18nDefaultLocaleBackend + when Array, Proc + backend_class.include BackendModule.new(column_fallback) + else + raise ArgumentError, "column_fallback value must be a boolean, an array of locales or a proc" + end + end + + module I18nDefaultLocaleBackend + def read(locale, **) + locale == I18n.default_locale ? model.read_attribute(attribute) : super + end + + def write(locale, value, **) + locale == I18n.default_locale ? model.send(:write_attribute, attribute, value) : super + end + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def build_node(attr, locale) + if locale == I18n.default_locale + model_class.arel_table[attr] + else + super + end + end + end + end + + class BackendModule < Module + def initialize(column_fallback) + case (@column_fallback = column_fallback) + when Array + define_array_accessors + when Proc + define_proc_accessors + end + end + + def included(base) + base.extend(ClassMethods.new(@column_fallback)) + end + + private + + def define_array_accessors + column_fallback = @column_fallback + + module_eval <<-EOM, __FILE__, __LINE__ + 1 + def read(locale, **) + if #{column_fallback}.include?(locale) + model.read_attribute(attribute) + else + super + end + end + + def write(locale, value, **) + if #{column_fallback}.include?(locale) + model.send(:write_attribute, attribute, value) + else + super + end + end + EOM + end + + def define_proc_accessors + column_fallback = @column_fallback + + define_method :read do |locale, **options| + if column_fallback.call(locale) + model.read_attribute(attribute) + else + super(locale, **options) + end + end + + define_method :write do |locale, value, **options| + if column_fallback.call(locale) + model.send(:write_attribute, attribute, value) + else + super(locale, value, **options) + end + end + end + + class ClassMethods < Module + def initialize(column_fallback) + case column_fallback + when Array + module_eval <<-EOM, __FILE__, __LINE__ + 1 + def build_node(attr, locale) + if #{column_fallback}.include?(locale) + model_class.arel_table[attr] + else + super + end + end + EOM + when Proc + define_method(:build_node) do |attr, locale| + if column_fallback.call(locale) + model_class.arel_table[attr] + else + super(attr, locale) + end + end + end + end + end + end + end + end + + register_plugin(:active_record_column_fallback, ActiveRecord::ColumnFallback) + end +end diff --git a/lib/mobility/plugins/column_fallback.rb b/lib/mobility/plugins/column_fallback.rb new file mode 100644 index 000000000..64f1138e0 --- /dev/null +++ b/lib/mobility/plugins/column_fallback.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Mobility + module Plugins + module ColumnFallback + extend Plugin + + default false + + requires :backend, include: :before + end + + register_plugin(:column_fallback, ColumnFallback) + end +end diff --git a/lib/mobility/plugins/sequel.rb b/lib/mobility/plugins/sequel.rb index 2fc7ac18b..7db6b2050 100644 --- a/lib/mobility/plugins/sequel.rb +++ b/lib/mobility/plugins/sequel.rb @@ -9,6 +9,7 @@ require_relative "./sequel/dirty" require_relative "./sequel/cache" require_relative "./sequel/query" +require_relative "./sequel/column_fallback" module Mobility module Plugins @@ -26,6 +27,7 @@ module Sequel requires :sequel_dirty requires :sequel_cache requires :sequel_query + requires :sequel_column_fallback included_hook do |klass| unless sequel_class?(klass) diff --git a/lib/mobility/plugins/sequel/column_fallback.rb b/lib/mobility/plugins/sequel/column_fallback.rb new file mode 100644 index 000000000..d4c568e64 --- /dev/null +++ b/lib/mobility/plugins/sequel/column_fallback.rb @@ -0,0 +1,133 @@ +# frozen-string-literal: true + +module Mobility +=begin + +Plugin to use an original column for a given locale, and otherwise use the backend. + +=end + module Plugins + module Sequel + module ColumnFallback + extend Plugin + + requires :column_fallback, include: false + + included_hook do |_, backend_class| + case (column_fallback = options[:column_fallback]) + when TrueClass + backend_class.include I18nDefaultLocaleBackend + when Array, Proc + backend_class.include BackendModule.new(column_fallback) + end + end + + module I18nDefaultLocaleBackend + def read(locale, **) + locale == I18n.default_locale ? model[attribute.to_sym] : super + end + + def write(locale, value, **) + if locale == I18n.default_locale + model[attribute.to_sym] = value + else + super + end + end + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def build_op(attr, locale) + if locale == I18n.default_locale + ::Sequel::SQL::QualifiedIdentifier.new(model_class.table_name, attr.to_sym) + else + super + end + end + end + end + + class BackendModule < Module + def initialize(column_fallback) + case (@column_fallback = column_fallback) + when Array + define_array_accessors + when Proc + define_proc_accessors + end + end + + def included(base) + base.extend(ClassMethods.new(@column_fallback)) + end + + private + + def define_array_accessors + column_fallback = @column_fallback + + module_eval <<-EOM, __FILE__, __LINE__ + 1 + def read(locale, **) + #{column_fallback}.include?(locale) ? model[attribute.to_sym] : super + end + + def write(locale, value, **) + if #{column_fallback}.include?(locale) + model[attribute.to_sym] = value + else + super + end + end + EOM + end + + def define_proc_accessors + column_fallback = @column_fallback + + define_method :read do |locale, **options| + column_fallback.call(locale) ? model[attribute.to_sym] : super(locale, **options) + end + + define_method :write do |locale, value, **options| + if column_fallback.call(locale) + model[attribute.to_sym] = value + else + super(locale, value, **options) + end + end + end + + class ClassMethods < Module + def initialize(column_fallback) + case column_fallback + when Array + module_eval <<-EOM, __FILE__, __LINE__ + 1 + def build_op(attr, locale) + if #{column_fallback}.include?(locale) + ::Sequel::SQL::QualifiedIdentifier.new(model_class.table_name, attr.to_sym) + else + super + end + end + EOM + when Proc + define_method(:build_op) do |attr, locale| + if column_fallback.call(locale) + ::Sequel::SQL::QualifiedIdentifier.new(model_class.table_name, attr.to_sym) + else + super(attr, locale) + end + end + end + end + end + end + end + end + + register_plugin(:sequel_column_fallback, Sequel::ColumnFallback) + end +end diff --git a/spec/mobility/plugins/active_record/column_fallback_spec.rb b/spec/mobility/plugins/active_record/column_fallback_spec.rb new file mode 100644 index 000000000..0c182e32e --- /dev/null +++ b/spec/mobility/plugins/active_record/column_fallback_spec.rb @@ -0,0 +1,177 @@ +require "spec_helper" + +return unless defined?(ActiveRecord) + +require "mobility/plugins/active_record/column_fallback" + +describe Mobility::Plugins::ActiveRecord::ColumnFallback, orm: :active_record, type: :plugin do + plugins :active_record, :reader, :writer, :column_fallback + + let(:model_class) do + stub_const 'Article', Class.new(ActiveRecord::Base) + Article.include translations + Article + end + + context "column_fallback: true" do + plugin_setup :slug, column_fallback: true + + it "reads/writes from/to model column if locale is I18n.default_locale" do + instance.send(:write_attribute, :slug, "foo") + + Mobility.with_locale(:en) do + expect(instance.slug).to eq("foo") + instance.slug = "bar" + expect(instance.slug).to eq("bar") + expect(instance.read_attribute(:slug)).to eq("bar") + end + end + + it "reads/writes from/to backend if locale is not I18n.default_locale" do + instance.send(:write_attribute, :slug, "foo") + + Mobility.with_locale(:fr) do + expect(listener).to receive(:read).with(:fr, any_args).and_return("bar") + expect(instance.slug).to eq("bar") + + expect(listener).to receive(:write).with(:fr, "baz", any_args) + instance.slug = "baz" + end + end + end + + locales = [:de, :ja] + context "column_fallback: #{locales.inspect}" do + plugin_setup :slug, column_fallback: locales + + it "reads/writes from/to model column if locale is locales array" do + locales.each do |locale| + instance.send(:write_attribute, :slug, "foo") + + Mobility.with_locale(locale) do + expect(instance.slug).to eq("foo") + instance.slug = "bar" + expect(instance.slug).to eq("bar") + expect(instance.read_attribute(:slug)).to eq("bar") + end + end + end + + it "reads/writes from/to backend if locale is not in locales array" do + instance.send(:write_attribute, :slug, "foo") + + Mobility.with_locale(:fr) do + expect(listener).to receive(:read).with(:fr, any_args).and_return("bar") + expect(instance.slug).to eq("bar") + + expect(listener).to receive(:write).with(:fr, "baz", any_args) + instance.slug = "baz" + end + end + end + + context "column_fallback: ->(locale) { locale == :de }" do + plugin_setup :slug, column_fallback: ->(locale) { locale == :de } + + it "reads/writes from/to model column if proc returns false" do + instance.send(:write_attribute, :slug, "foo") + + Mobility.with_locale(:de) do + expect(instance.slug).to eq("foo") + instance.slug = "bar" + expect(instance.slug).to eq("bar") + expect(instance.read_attribute(:slug)).to eq("bar") + end + end + + it "reads/writes from/to backend if proc returns true" do + instance.send(:write_attribute, :slug, "foo") + + Mobility.with_locale(:fr) do + expect(listener).to receive(:read).with(:fr, any_args).and_return("bar") + expect(instance.slug).to eq("bar") + + expect(listener).to receive(:write).with(:fr, "baz", any_args) + instance.slug = "baz" + end + end + end + + describe "querying" do + plugins :active_record, :writer, :query, :column_fallback + + let(:model_class) do + stub_const 'Article', Class.new(ActiveRecord::Base) + end + + context "column_fallback: true" do + before do + translates model_class, :slug, backend: [:key_value, type: :string], column_fallback: true + end + + it "queries on model column if locale is I18n.default_locale" do + instance1 = model_class.new + instance1.send(:write_attribute, :slug, "foo") + instance1.save + + instance2 = model_class.new + Mobility.with_locale(:fr) { instance2.slug = "bar" } + instance2.save + + expect(model_class.i18n.find_by(slug: "foo", locale: :en)).to eq(instance1) + expect(model_class.i18n.find_by(slug: "foo", locale: :fr)).to eq(nil) + expect(model_class.i18n.find_by(slug: "bar", locale: :fr)).to eq(instance2) + expect(model_class.i18n.find_by(slug: "bar", locale: :en)).to eq(nil) + end + end + + locales = [:de, :ja] + context "column_fallback: #{locales.inspect}" do + before do + translates model_class, :slug, backend: [:key_value, type: :string], column_fallback: locales + end + + it "queries on model column if locale is locales array" do + instance1 = model_class.new + Mobility.with_locale(:de) { instance1.send(:write_attribute, :slug, "foo") } + instance1.save + + instance2 = model_class.new + Mobility.with_locale(:ja) { instance2.send(:write_attribute, :slug, "bar") } + instance2.save + + instance3 = model_class.new + Mobility.with_locale(:en) { instance3.slug = "baz" } + instance3.save + + expect(model_class.i18n.find_by(slug: "foo", locale: :de)).to eq(instance1) + expect(model_class.i18n.find_by(slug: "foo", locale: :en)).to eq(nil) + expect(model_class.i18n.find_by(slug: "bar", locale: :ja)).to eq(instance2) + expect(model_class.i18n.find_by(slug: "bar", locale: :en)).to eq(nil) + expect(model_class.i18n.find_by(slug: "baz", locale: :en)).to eq(instance3) + expect(model_class.i18n.find_by(slug: "baz", locale: :de)).to eq(nil) + end + end + + context "column_fallback: ->(locale) { locale == :de }" do + before do + translates model_class, :slug, backend: [:key_value, type: :string], column_fallback: ->(locale) { locale == :de } + end + + it "queries on model column if proc returns false" do + instance1 = model_class.new + Mobility.with_locale(:de) { instance1.send(:write_attribute, :slug, "foo") } + instance1.save + + instance2 = model_class.new + Mobility.with_locale(:ja) { instance2.slug = "bar" } + instance2.save + + expect(model_class.i18n.find_by(slug: "foo", locale: :de)).to eq(instance1) + expect(model_class.i18n.find_by(slug: "foo", locale: :ja)).to eq(nil) + expect(model_class.i18n.find_by(slug: "bar", locale: :ja)).to eq(instance2) + expect(model_class.i18n.find_by(slug: "bar", locale: :de)).to eq(nil) + end + end + end +end diff --git a/spec/mobility/plugins/sequel/column_fallback_spec.rb b/spec/mobility/plugins/sequel/column_fallback_spec.rb new file mode 100644 index 000000000..7a48897b7 --- /dev/null +++ b/spec/mobility/plugins/sequel/column_fallback_spec.rb @@ -0,0 +1,182 @@ +require "spec_helper" + +return unless defined?(Sequel) + +require "mobility/plugins/sequel/column_fallback" + +describe Mobility::Plugins::Sequel::ColumnFallback, orm: :sequel, type: :plugin do + plugins :sequel, :backend, :reader, :writer, :column_fallback + + let(:model_class) do + stub_const 'Article', Class.new(Sequel::Model) + Article.dataset = DB[:articles] + Article.include translations + Article + end + + context "column_fallback: true" do + plugin_setup :slug, column_fallback: true + + it "reads/writes from/to model column if locale is I18n.default_locale" do + instance[:slug] = "foo" + + Mobility.with_locale(:en) do + expect(instance.slug).to eq("foo") + instance.slug = "bar" + expect(instance.slug).to eq("bar") + expect(instance[:slug]).to eq("bar") + end + end + + it "reads/writes from/to backend if locale is not I18n.default_locale" do + instance[:slug] = "foo" + + Mobility.with_locale(:fr) do + expect(listener).to receive(:read).with(:fr, any_args).and_return("bar") + expect(instance.slug).to eq("bar") + + expect(listener).to receive(:write).with(:fr, "baz", any_args) + instance.slug = "baz" + end + end + end + + locales = [:de, :ja] + context "column_fallback: #{locales.inspect}" do + plugin_setup :slug, column_fallback: locales + + it "reads/writes from/to model column if locale is locales array" do + locales.each do |locale| + instance[:slug] = "foo" + + Mobility.with_locale(locale) do + expect(instance.slug).to eq("foo") + instance.slug = "bar" + expect(instance.slug).to eq("bar") + expect(instance[:slug]).to eq("bar") + end + end + end + + it "reads/writes from/to backend if locale is not in locales array" do + instance[:slug] = "foo" + + Mobility.with_locale(:fr) do + expect(listener).to receive(:read).with(:fr, any_args).and_return("bar") + expect(instance.slug).to eq("bar") + + expect(listener).to receive(:write).with(:fr, "baz", any_args) + instance.slug = "baz" + end + end + end + + context "column_fallback: ->(locale) { locale == :de }" do + plugin_setup :slug, column_fallback: ->(locale) { locale == :de } + + it "reads/writes from/to model column if proc returns false" do + instance[:slug] = "foo" + + Mobility.with_locale(:de) do + expect(instance.slug).to eq("foo") + instance.slug = "bar" + expect(instance.slug).to eq("bar") + expect(instance[:slug]).to eq("bar") + end + end + + it "reads/writes from/to backend if proc returns true" do + instance[:slug] = "foo" + + Mobility.with_locale(:fr) do + expect(listener).to receive(:read).with(:fr, any_args).and_return("bar") + expect(instance.slug).to eq("bar") + + expect(listener).to receive(:write).with(:fr, "baz", any_args) + instance.slug = "baz" + end + end + end + + describe "querying" do + # need to include because Sequel KeyValue backend depends on it, and we're + # using that backend in tests below + plugins :sequel, :writer, :query, :cache, :column_fallback + + let(:model_class) do + stub_const 'Article', Class.new(Sequel::Model) + Article.dataset = DB[:articles] + Article + end + + context "column_fallback: true" do + before do + translates model_class, :slug, backend: [:key_value, type: :string], column_fallback: true + end + + it "queries on model column if locale is I18n.default_locale" do + instance1 = model_class.new + instance1[:slug] = "foo" + instance1.save + + instance2 = model_class.new + Mobility.with_locale(:fr) { instance2.slug = "bar" } + instance2.save + + expect(model_class.i18n.where(slug: "foo", locale: :en).select_all(:articles).all).to match_array([instance1]) + expect(model_class.i18n.where(slug: "foo", locale: :fr).select_all(:articles).all).to eq([]) + expect(model_class.i18n.where(slug: "bar", locale: :fr).select_all(:articles).all).to eq([instance2]) + expect(model_class.i18n.where(slug: "bar", locale: :en).select_all(:articles).all).to eq([]) + end + end + + locales = [:de, :ja] + context "column_fallback: #{locales.inspect}" do + before do + translates model_class, :slug, backend: [:key_value, type: :string], column_fallback: locales + end + + it "queries on model column if locale is locales array" do + instance1 = model_class.new + Mobility.with_locale(:de) { instance1.slug = "foo" } + instance1.save + + instance2 = model_class.new + Mobility.with_locale(:ja) { instance2.slug = "bar" } + instance2.save + + instance3 = model_class.new + Mobility.with_locale(:en) { instance3.slug = "baz" } + instance3.save + + expect(model_class.i18n.where(slug: "foo", locale: :de).select_all(:articles).all).to eq([instance1]) + expect(model_class.i18n.where(slug: "foo", locale: :en).select_all(:articles).all).to eq([]) + expect(model_class.i18n.where(slug: "bar", locale: :ja).select_all(:articles).all).to eq([instance2]) + expect(model_class.i18n.where(slug: "bar", locale: :en).select_all(:articles).all).to eq([]) + expect(model_class.i18n.where(slug: "baz", locale: :en).select_all(:articles).all).to eq([instance3]) + expect(model_class.i18n.where(slug: "baz", locale: :de).select_all(:articles).all).to eq([]) + end + end + + context "column_fallback: ->(locale) { locale == :de }" do + before do + translates model_class, :slug, backend: [:key_value, type: :string], column_fallback: ->(locale) { locale == :de } + end + + it "queries on model column if proc returns false" do + instance1 = model_class.new + Mobility.with_locale(:de) { instance1.slug = "foo" } + instance1.save + + instance2 = model_class.new + Mobility.with_locale(:ja) { instance2.slug = "bar" } + instance2.save + + expect(model_class.i18n.where(slug: "foo", locale: :de).select_all(:articles).all).to eq([instance1]) + expect(model_class.i18n.where(slug: "foo", locale: :ja).select_all(:articles).all).to eq([]) + expect(model_class.i18n.where(slug: "bar", locale: :ja).select_all(:articles).all).to eq([instance2]) + expect(model_class.i18n.where(slug: "bar", locale: :de).select_all(:articles).all).to eq([]) + end + end + end +end From c622cdb8d517466d4f0ab9a832563f189092d0e1 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sun, 26 Sep 2021 11:04:45 +0900 Subject: [PATCH 23/82] Release 1.2.0 --- CHANGELOG.md | 12 ++++++++++++ README.md | 2 +- lib/mobility/version.rb | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 536077d59..1625a3741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ ## 1.1 +## 1.2 + +### 1.2.0 +- Add ColumnFallback plugin + ([#512](https://github.com/shioyama/mobility/pull/512)) +- Fix Sequel querying on untranslated attributes in `i18n` block + ([#529](https://github.com/shioyama/mobility/pull/529)) +- Allow passing configured backend class as third argument to setup + ([#528](https://github.com/shioyama/mobility/pull/528)) +- Clearly distinguish backend classes from their configured subclasses + ([#527](Clearly distinguish backend classes from their configured subclasses)) + ### 1.1.3 - Do not swallow keyword args on ruby 3 in fallthrough accessors ([#520](https://github.com/shioyama/mobility/pull/520)) thanks diff --git a/README.md b/README.md index 4cad15f0b..dd17afcab 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Installation Add this line to your application's Gemfile: ```ruby -gem 'mobility', '~> 1.1.3' +gem 'mobility', '~> 1.2.0' ``` ### ActiveRecord (Rails) diff --git a/lib/mobility/version.rb b/lib/mobility/version.rb index c485d4e4c..17ec90d2b 100644 --- a/lib/mobility/version.rb +++ b/lib/mobility/version.rb @@ -9,7 +9,7 @@ module VERSION MAJOR = 1 MINOR = 2 TINY = 0 - PRE = "alpha" + PRE = nil STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end From d0a70658d346a2f6d2f592a28f7a48da66e123e7 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sun, 26 Sep 2021 11:12:30 +0900 Subject: [PATCH 24/82] Fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1625a3741..5fda88b22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ - Allow passing configured backend class as third argument to setup ([#528](https://github.com/shioyama/mobility/pull/528)) - Clearly distinguish backend classes from their configured subclasses - ([#527](Clearly distinguish backend classes from their configured subclasses)) + ([#527](https://github.com/shioyama/mobility/pull/527)) ### 1.1.3 - Do not swallow keyword args on ruby 3 in fallthrough accessors From 6eec8a8d9f7268d2744125f6832459d701597a9d Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Mon, 27 Sep 2021 21:00:11 +0900 Subject: [PATCH 25/82] Refactor ColumnFallback plugin --- .../plugins/active_record/column_fallback.rb | 129 ++++-------------- .../plugins/sequel/column_fallback.rb | 119 ++++------------ 2 files changed, 54 insertions(+), 194 deletions(-) diff --git a/lib/mobility/plugins/active_record/column_fallback.rb b/lib/mobility/plugins/active_record/column_fallback.rb index 0b4b4bb3a..a07887824 100644 --- a/lib/mobility/plugins/active_record/column_fallback.rb +++ b/lib/mobility/plugins/active_record/column_fallback.rb @@ -14,120 +14,47 @@ module ColumnFallback requires :column_fallback, include: false included_hook do |_, backend_class| - case (column_fallback = options[:column_fallback]) + backend_class.include BackendInstanceMethods + backend_class.extend BackendClassMethods + end + + def self.use_column_fallback?(options, locale) + case column_fallback = options[:column_fallback] when TrueClass - backend_class.include I18nDefaultLocaleBackend - when Array, Proc - backend_class.include BackendModule.new(column_fallback) + locale == I18n.default_locale + when Array + column_fallback.include?(locale) + when Proc + column_fallback.call(locale) else - raise ArgumentError, "column_fallback value must be a boolean, an array of locales or a proc" + false end end - module I18nDefaultLocaleBackend + module BackendInstanceMethods def read(locale, **) - locale == I18n.default_locale ? model.read_attribute(attribute) : super + if ColumnFallback.use_column_fallback?(options, locale) + model.read_attribute(attribute) + else + super + end end def write(locale, value, **) - locale == I18n.default_locale ? model.send(:write_attribute, attribute, value) : super - end - - def self.included(base) - base.extend(ClassMethods) - end - - module ClassMethods - def build_node(attr, locale) - if locale == I18n.default_locale - model_class.arel_table[attr] - else - super - end + if ColumnFallback.use_column_fallback?(options, locale) + model.send(:write_attribute, attribute, value) + else + super end end end - class BackendModule < Module - def initialize(column_fallback) - case (@column_fallback = column_fallback) - when Array - define_array_accessors - when Proc - define_proc_accessors - end - end - - def included(base) - base.extend(ClassMethods.new(@column_fallback)) - end - - private - - def define_array_accessors - column_fallback = @column_fallback - - module_eval <<-EOM, __FILE__, __LINE__ + 1 - def read(locale, **) - if #{column_fallback}.include?(locale) - model.read_attribute(attribute) - else - super - end - end - - def write(locale, value, **) - if #{column_fallback}.include?(locale) - model.send(:write_attribute, attribute, value) - else - super - end - end - EOM - end - - def define_proc_accessors - column_fallback = @column_fallback - - define_method :read do |locale, **options| - if column_fallback.call(locale) - model.read_attribute(attribute) - else - super(locale, **options) - end - end - - define_method :write do |locale, value, **options| - if column_fallback.call(locale) - model.send(:write_attribute, attribute, value) - else - super(locale, value, **options) - end - end - end - - class ClassMethods < Module - def initialize(column_fallback) - case column_fallback - when Array - module_eval <<-EOM, __FILE__, __LINE__ + 1 - def build_node(attr, locale) - if #{column_fallback}.include?(locale) - model_class.arel_table[attr] - else - super - end - end - EOM - when Proc - define_method(:build_node) do |attr, locale| - if column_fallback.call(locale) - model_class.arel_table[attr] - else - super(attr, locale) - end - end - end + module BackendClassMethods + def build_node(attr, locale) + if ColumnFallback.use_column_fallback?(options, locale) + model_class.arel_table[attr] + else + super end end end diff --git a/lib/mobility/plugins/sequel/column_fallback.rb b/lib/mobility/plugins/sequel/column_fallback.rb index d4c568e64..fdfd6aac6 100644 --- a/lib/mobility/plugins/sequel/column_fallback.rb +++ b/lib/mobility/plugins/sequel/column_fallback.rb @@ -14,114 +14,47 @@ module ColumnFallback requires :column_fallback, include: false included_hook do |_, backend_class| - case (column_fallback = options[:column_fallback]) + backend_class.include BackendInstanceMethods + backend_class.extend BackendClassMethods + end + + def self.use_column_fallback?(options, locale) + case column_fallback = options[:column_fallback] when TrueClass - backend_class.include I18nDefaultLocaleBackend - when Array, Proc - backend_class.include BackendModule.new(column_fallback) + locale == I18n.default_locale + when Array + column_fallback.include?(locale) + when Proc + column_fallback.call(locale) + else + false end end - module I18nDefaultLocaleBackend + module BackendInstanceMethods def read(locale, **) - locale == I18n.default_locale ? model[attribute.to_sym] : super + if ColumnFallback.use_column_fallback?(options, locale) + model[attribute.to_sym] + else + super + end end def write(locale, value, **) - if locale == I18n.default_locale + if ColumnFallback.use_column_fallback?(options, locale) model[attribute.to_sym] = value else super end end - - def self.included(base) - base.extend(ClassMethods) - end - - module ClassMethods - def build_op(attr, locale) - if locale == I18n.default_locale - ::Sequel::SQL::QualifiedIdentifier.new(model_class.table_name, attr.to_sym) - else - super - end - end - end end - class BackendModule < Module - def initialize(column_fallback) - case (@column_fallback = column_fallback) - when Array - define_array_accessors - when Proc - define_proc_accessors - end - end - - def included(base) - base.extend(ClassMethods.new(@column_fallback)) - end - - private - - def define_array_accessors - column_fallback = @column_fallback - - module_eval <<-EOM, __FILE__, __LINE__ + 1 - def read(locale, **) - #{column_fallback}.include?(locale) ? model[attribute.to_sym] : super - end - - def write(locale, value, **) - if #{column_fallback}.include?(locale) - model[attribute.to_sym] = value - else - super - end - end - EOM - end - - def define_proc_accessors - column_fallback = @column_fallback - - define_method :read do |locale, **options| - column_fallback.call(locale) ? model[attribute.to_sym] : super(locale, **options) - end - - define_method :write do |locale, value, **options| - if column_fallback.call(locale) - model[attribute.to_sym] = value - else - super(locale, value, **options) - end - end - end - - class ClassMethods < Module - def initialize(column_fallback) - case column_fallback - when Array - module_eval <<-EOM, __FILE__, __LINE__ + 1 - def build_op(attr, locale) - if #{column_fallback}.include?(locale) - ::Sequel::SQL::QualifiedIdentifier.new(model_class.table_name, attr.to_sym) - else - super - end - end - EOM - when Proc - define_method(:build_op) do |attr, locale| - if column_fallback.call(locale) - ::Sequel::SQL::QualifiedIdentifier.new(model_class.table_name, attr.to_sym) - else - super(attr, locale) - end - end - end + module BackendClassMethods + def build_op(attr, locale) + if ColumnFallback.use_column_fallback?(options, locale) + ::Sequel::SQL::QualifiedIdentifier.new(model_class.table_name, attr.to_sym) + else + super end end end From cf6bbf4f773e9576f9e6c4f97d6619988ce17aeb Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Mon, 27 Sep 2021 21:35:48 +0900 Subject: [PATCH 26/82] Release 1.2.1 --- CHANGELOG.md | 12 ++++++++---- README.md | 2 +- lib/mobility/version.rb | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fda88b22..6ddccf91d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,15 +8,19 @@ ## 1.2 +### 1.2.1 +- Refactor ColumnFallback plugin + ([#530](https://github.com/shioyama/mobility/pull/530)) + ### 1.2.0 - Add ColumnFallback plugin - ([#512](https://github.com/shioyama/mobility/pull/512)) + ([#512](https://github.com/shioyama/mobility/pull/512)) - Fix Sequel querying on untranslated attributes in `i18n` block - ([#529](https://github.com/shioyama/mobility/pull/529)) + ([#529](https://github.com/shioyama/mobility/pull/529)) - Allow passing configured backend class as third argument to setup - ([#528](https://github.com/shioyama/mobility/pull/528)) + ([#528](https://github.com/shioyama/mobility/pull/528)) - Clearly distinguish backend classes from their configured subclasses - ([#527](https://github.com/shioyama/mobility/pull/527)) + ([#527](https://github.com/shioyama/mobility/pull/527)) ### 1.1.3 - Do not swallow keyword args on ruby 3 in fallthrough accessors diff --git a/README.md b/README.md index dd17afcab..51cdf0784 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Installation Add this line to your application's Gemfile: ```ruby -gem 'mobility', '~> 1.2.0' +gem 'mobility', '~> 1.2.1' ``` ### ActiveRecord (Rails) diff --git a/lib/mobility/version.rb b/lib/mobility/version.rb index 17ec90d2b..ba6236e0b 100644 --- a/lib/mobility/version.rb +++ b/lib/mobility/version.rb @@ -8,7 +8,7 @@ def self.gem_version module VERSION MAJOR = 1 MINOR = 2 - TINY = 0 + TINY = 1 PRE = nil STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") From 61a915eb7c5ee94d2ac343ff2b135554cb77ee77 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 1 Oct 2021 19:56:45 +0900 Subject: [PATCH 27/82] Remove released entry from unreleased section --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ddccf91d..b2f603425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,5 @@ # Mobility Changelog -## Unreleased -- Assign blank values in pg hash backends - ([#516](https://github.com/shioyama/mobility/pull/516)) - ## 1.1 ## 1.2 From e2e58b1db82d034a7dab6c5ca15b4e462990735d Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 1 Oct 2021 22:26:26 +0900 Subject: [PATCH 28/82] Simplify Fallbacks plugin --- lib/mobility/plugins/fallbacks.rb | 45 ++++++++++++++----------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/lib/mobility/plugins/fallbacks.rb b/lib/mobility/plugins/fallbacks.rb index be318df0e..d93bdafdb 100644 --- a/lib/mobility/plugins/fallbacks.rb +++ b/lib/mobility/plugins/fallbacks.rb @@ -117,8 +117,10 @@ module Fallbacks # Applies fallbacks plugin to attributes. Completely disables fallbacks # on model if option is +false+. included_hook do |_, backend_class| - fallbacks = options[:fallbacks] - backend_class.include(BackendReader.new(fallbacks, method(:generate_fallbacks))) unless fallbacks == false + backend_class.include(BackendInstanceMethods) unless options[:fallbacks] == false + # This is weird. We need to find a better way to allow customization of + # rarely-customized code like this. + backend_class.define_method(:generate_fallbacks, &method(:generate_fallbacks)) end private @@ -134,33 +136,26 @@ def [](locale) end end - class BackendReader < Module - def initialize(fallbacks_option, fallbacks_generator) - @fallbacks_generator = fallbacks_generator - define_read(convert_option_to_fallbacks(fallbacks_option)) - end - - private - - def define_read(fallbacks) - define_method :read do |locale, fallback: true, **options| - return super(locale, **options) if !fallback || options[:locale] + module BackendInstanceMethods + def read(locale, fallback: true, **accessor_options) + return super(locale, **options) if !fallback || accessor_options[:locale] - locales = fallback == true ? fallbacks[locale] : [locale, *fallback] - locales.each do |fallback_locale| - value = super(fallback_locale, **options) - return value if Util.present?(value) - end - - super(locale, **options) + locales = fallback == true ? fallbacks[locale] : [locale, *fallback] + locales.each do |fallback_locale| + value = super(fallback_locale, **accessor_options) + return value if Util.present?(value) end + + super(locale, **options) end - def convert_option_to_fallbacks(option) - if option.is_a?(::Hash) - @fallbacks_generator[option] - elsif option == true - @fallbacks_generator[{}] + private + + def fallbacks + if options[:fallbacks].is_a?(Hash) + generate_fallbacks(options[:fallbacks]) + elsif options[:fallbacks] == true + generate_fallbacks({}) else ::Hash.new { [] } end From 3d0ea304f6178ce50959d8de55a1ff013d7b8e5d Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 1 Oct 2021 22:36:34 +0900 Subject: [PATCH 29/82] Update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f603425..e3bdfca6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Mobility Changelog -## 1.1 +## Unreleased +- Simplify Fallbacks plugin + ([#531](https://github.com/shioyama/mobility/pull/531)) ## 1.2 @@ -18,6 +20,8 @@ - Clearly distinguish backend classes from their configured subclasses ([#527](https://github.com/shioyama/mobility/pull/527)) +## 1.1 + ### 1.1.3 - Do not swallow keyword args on ruby 3 in fallthrough accessors ([#520](https://github.com/shioyama/mobility/pull/520)) thanks From 893b31f7e81d7222a20572c19c7b3ae4beeb3db3 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sun, 3 Oct 2021 21:35:41 +0900 Subject: [PATCH 30/82] Fix Seuqel container op in Sequel Ref: https://github.com/jeremyevans/sequel/commit/c4ae5d42e5 --- lib/mobility/backends/sequel/container.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mobility/backends/sequel/container.rb b/lib/mobility/backends/sequel/container.rb index 228ae11e5..ac3fb454b 100644 --- a/lib/mobility/backends/sequel/container.rb +++ b/lib/mobility/backends/sequel/container.rb @@ -100,7 +100,7 @@ class InvalidColumnType < StandardError; end # @return [Mobility::Backends::Sequel::Container::JSONOp,Mobility::Backends::Sequel::Container::JSONBOp] def self.build_op(attr, locale) klass = const_get("#{options[:column_type].upcase}Op") - klass.new(klass.new(column_name.to_sym)[locale.to_s]).get_text(attr) + klass.new(klass.new(column_name.to_sym).get(locale.to_s)).get_text(attr) end class JSONOp < ::Sequel::Postgres::JSONOp; end From 42c5b2a70ec8a836db1bb0ed8246b4bbaa61441c Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 5 Feb 2021 21:16:44 +0900 Subject: [PATCH 31/82] Add failing test for Marshal.dump --- spec/mobility/plugins/backend_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/mobility/plugins/backend_spec.rb b/spec/mobility/plugins/backend_spec.rb index 15138f1c4..4ef0e8178 100644 --- a/spec/mobility/plugins/backend_spec.rb +++ b/spec/mobility/plugins/backend_spec.rb @@ -170,6 +170,22 @@ expect(other.mobility_backends[:title]).not_to eq(article.mobility_backends[:title]) end + + # regression test for https://github.com/shioyama/mobility/issues/494 + it "does not prevent Marshal from working" do + mod = translations_class.new("title", backend: :null) + stub_const('Article', model_class) + model_class.include mod + + article = model_class.new + article.mobility_backends[:title] + expect { + expect(serialized = Marshal.dump(article)).not_to be_nil + expect(deserialized = Marshal.load(serialized)).not_to be_nil + + expect(deserialized.mobility_backends[:title]).to be_a(Mobility::Backends::Null) + }.not_to raise_error + end end end From 3c29f4e77909d87a79577e7c8f923102820a3595 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 5 Feb 2021 21:41:27 +0900 Subject: [PATCH 32/82] Repalce Hash with named backends class --- lib/mobility/plugins/backend.rb | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/mobility/plugins/backend.rb b/lib/mobility/plugins/backend.rb index 3305b873f..16dd1dba8 100644 --- a/lib/mobility/plugins/backend.rb +++ b/lib/mobility/plugins/backend.rb @@ -114,15 +114,25 @@ def self.configure_default(defaults, key, backend = nil, backend_options = {}) defaults[key] = [backend, backend_options] if backend end + class MobilityBackends < Hash + def initialize(model) + @model = model + super() + end + + def [](name) + return fetch(name) if has_key?(name) + return self[name.to_sym] if String === name + self[name] = @model.class.mobility_backend_class(name).new(@model, name.to_s) + end + end + module InstanceMethods # Return a new backend for an attribute name. # @return [Hash] Hash of attribute names and backend instances # @api private def mobility_backends - @mobility_backends ||= ::Hash.new do |hash, name| - next hash[name.to_sym] if String === name - hash[name] = self.class.mobility_backend_class(name).new(self, name.to_s) - end + @mobility_backends ||= MobilityBackends.new(self) end def initialize_dup(other) From ae052096025dc3b9e369f82b4d800bcbc16da444 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 19 Mar 2021 10:08:13 +0900 Subject: [PATCH 33/82] Add equality operator for Mobility::Backend --- lib/mobility/backend.rb | 6 ++++++ spec/mobility/backend_spec.rb | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/mobility/backend.rb b/lib/mobility/backend.rb index 7a1df5bd2..1f813b04a 100644 --- a/lib/mobility/backend.rb +++ b/lib/mobility/backend.rb @@ -77,6 +77,12 @@ def initialize(*args) @attribute = args[1] end + def ==(backend) + backend.class == self.class && + backend.attribute == attribute && + backend.model == model + end + # @!macro [new] backend_reader # Gets the translated value for provided locale from configured backend. # @param [Symbol] locale Locale to read diff --git a/spec/mobility/backend_spec.rb b/spec/mobility/backend_spec.rb index 8539ab9b9..4aa612dc2 100644 --- a/spec/mobility/backend_spec.rb +++ b/spec/mobility/backend_spec.rb @@ -43,6 +43,24 @@ end end + describe "#==" do + it "returns true if two backends have the same class, model and attributes" do + expect(backend_class.new(model, attribute)).to eq(backend) + end + + it "returns false if backends have different classes" do + expect(Class.new(backend_class).new(model, attribute)).not_to eq(backend) + end + + it "returns false if backends have different attributes" do + expect(backend_class.new(model, "foo")).not_to eq(backend) + end + + it "returns false if backends have different models" do + expect(backend_class.new(double(:other_model), attribute)).not_to eq(backend) + end + end + describe "#each" do it "returns nothing by default" do backend = backend_class.new(model, attribute) From fc1d77af8463ca805bd15c18646e83af92fc90b6 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sun, 3 Oct 2021 21:51:23 +0900 Subject: [PATCH 34/82] Don't dump cached backends These break Marshal serialization. We can just not dump them (and regenerate them when loading back) to avoid the problem. --- lib/mobility/plugins/backend.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/mobility/plugins/backend.rb b/lib/mobility/plugins/backend.rb index 16dd1dba8..d8b7230f1 100644 --- a/lib/mobility/plugins/backend.rb +++ b/lib/mobility/plugins/backend.rb @@ -125,6 +125,14 @@ def [](name) return self[name.to_sym] if String === name self[name] = @model.class.mobility_backend_class(name).new(@model, name.to_s) end + + def marshal_dump + @model + end + + def marshal_load(model) + @model = model + end end module InstanceMethods From 30529fd57c3bdf355d8ea2b60491d0d1b05bc854 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sun, 3 Oct 2021 22:00:28 +0900 Subject: [PATCH 35/82] Release 1.2.2 --- CHANGELOG.md | 4 ++++ README.md | 2 +- lib/mobility/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3bdfca6c..076c1180a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ ## 1.2 +### 1.2.2 +- Make models work with `Marshal.dump` + ([#532](https://github.com/shioyama/mobility/pull/532)) + ### 1.2.1 - Refactor ColumnFallback plugin ([#530](https://github.com/shioyama/mobility/pull/530)) diff --git a/README.md b/README.md index 51cdf0784..1139c00d0 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Installation Add this line to your application's Gemfile: ```ruby -gem 'mobility', '~> 1.2.1' +gem 'mobility', '~> 1.2.2' ``` ### ActiveRecord (Rails) diff --git a/lib/mobility/version.rb b/lib/mobility/version.rb index ba6236e0b..77e835ebe 100644 --- a/lib/mobility/version.rb +++ b/lib/mobility/version.rb @@ -8,7 +8,7 @@ def self.gem_version module VERSION MAJOR = 1 MINOR = 2 - TINY = 1 + TINY = 2 PRE = nil STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") From b687bca0442ab3a942f4240d0c98a426837bd69a Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sun, 3 Oct 2021 22:05:51 +0900 Subject: [PATCH 36/82] Add missing changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 076c1180a..23f7f2136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ ### 1.2.2 - Make models work with `Marshal.dump` ([#532](https://github.com/shioyama/mobility/pull/532)) +- Fix Sequel container op in Sequel + ([#533](https://github.com/shioyama/mobility/pull/533)) ### 1.2.1 - Refactor ColumnFallback plugin From 68ead91153ddcc9083943a743317cda2876bcfec Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sun, 3 Oct 2021 22:38:56 +0900 Subject: [PATCH 37/82] Move #531 from unreleased to 1.2.2 in changelog --- CHANGELOG.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23f7f2136..142e08134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,5 @@ # Mobility Changelog -## Unreleased -- Simplify Fallbacks plugin - ([#531](https://github.com/shioyama/mobility/pull/531)) - ## 1.2 ### 1.2.2 @@ -11,6 +7,8 @@ ([#532](https://github.com/shioyama/mobility/pull/532)) - Fix Sequel container op in Sequel ([#533](https://github.com/shioyama/mobility/pull/533)) +- Simplify Fallbacks plugin + ([#531](https://github.com/shioyama/mobility/pull/531)) ### 1.2.1 - Refactor ColumnFallback plugin From 98383aedd1d57385dcef91478bbc013b2153378c Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Tue, 5 Oct 2021 21:51:52 +0900 Subject: [PATCH 38/82] Simplify AR PgHash backend --- .../backends/active_record/pg_hash.rb | 35 +++++++------------ .../backends/active_record/hstore_spec.rb | 8 +++++ .../backends/active_record/json_spec.rb | 8 +++++ .../backends/active_record/jsonb_spec.rb | 8 +++++ 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/lib/mobility/backends/active_record/pg_hash.rb b/lib/mobility/backends/active_record/pg_hash.rb index 381a86a3e..3fcb7c136 100644 --- a/lib/mobility/backends/active_record/pg_hash.rb +++ b/lib/mobility/backends/active_record/pg_hash.rb @@ -15,34 +15,25 @@ class PgHash include ActiveRecord include HashValued - # @!macro backend_iterator - def each_locale - super { |l| yield l.to_sym } + def read(locale, _options = nil) + translations[locale.to_s] end - def translations - model.read_attribute(column_name) + def write(locale, value, _options = nil) + if value.nil? + translations.delete(locale.to_s) + else + translations[locale.to_s] = value + end end - setup do |attributes, options = {}| - attributes.each { |attribute| store (options[:column_affix] % attribute), coder: Coder } + # @!macro backend_iterator + def each_locale + super { |l| yield l.to_sym } end - class Coder - def self.dump(obj) - if obj.is_a? ::Hash - obj.inject({}) do |translations, (locale, value)| - translations[locale] = value unless value.nil? - translations - end - else - raise ArgumentError, "Attribute is supposed to be a Hash, but was a #{obj.class}. -- #{obj.inspect}" - end - end - - def self.load(obj) - obj - end + def translations + model[column_name] end end private_constant :PgHash diff --git a/spec/mobility/backends/active_record/hstore_spec.rb b/spec/mobility/backends/active_record/hstore_spec.rb index 065c3e612..a2814e01b 100644 --- a/spec/mobility/backends/active_record/hstore_spec.rb +++ b/spec/mobility/backends/active_record/hstore_spec.rb @@ -27,6 +27,14 @@ include_dup_examples 'HstorePost' include_cache_key_examples 'HstorePost' + it "does not impact dirty tracking on original column" do + post = HstorePost.create! + post.reload + + expect(post.my_title_i18n).to eq({}) + expect(post.changes).to eq({}) + end + describe "non-text values" do it "converts non-string types to strings when saving" do post = HstorePost.new diff --git a/spec/mobility/backends/active_record/json_spec.rb b/spec/mobility/backends/active_record/json_spec.rb index ddd84ca41..5d0796818 100644 --- a/spec/mobility/backends/active_record/json_spec.rb +++ b/spec/mobility/backends/active_record/json_spec.rb @@ -26,6 +26,14 @@ include_dup_examples 'JsonPost' include_cache_key_examples 'JsonPost' + it "does not impact dirty tracking on original column" do + post = JsonPost.create! + post.reload + + expect(post.my_title_i18n).to eq({}) + expect(post.changes).to eq({}) + end + describe "non-text values" do it "stores non-string types as-is when saving", active_record_geq: '5.0' do backend = post.mobility_backends[:title] diff --git a/spec/mobility/backends/active_record/jsonb_spec.rb b/spec/mobility/backends/active_record/jsonb_spec.rb index 99e9f62e5..06d3d4b73 100644 --- a/spec/mobility/backends/active_record/jsonb_spec.rb +++ b/spec/mobility/backends/active_record/jsonb_spec.rb @@ -25,6 +25,14 @@ include_serialization_examples 'JsonbPost', column_affix: column_affix include_dup_examples 'JsonbPost' include_cache_key_examples 'JsonbPost' + + it "does not impact dirty tracking on original column" do + post = JsonbPost.create! + post.reload + + expect(post.my_title_i18n).to eq({}) + expect(post.changes).to eq({}) + end end context "with query plugin" do From 8873ccb0377cf11f09c5a92cdb14df3395afddbd Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Thu, 7 Oct 2021 21:58:16 +0900 Subject: [PATCH 39/82] Bump to 1.3.0.alpha --- lib/mobility/version.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mobility/version.rb b/lib/mobility/version.rb index 77e835ebe..591a11ef1 100644 --- a/lib/mobility/version.rb +++ b/lib/mobility/version.rb @@ -7,9 +7,9 @@ def self.gem_version module VERSION MAJOR = 1 - MINOR = 2 - TINY = 2 - PRE = nil + MINOR = 3 + TINY = 0 + PRE = alpha STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end From bfc5506ae2b0c63579620381a0c3c28f20cb6b7c Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 8 Oct 2021 21:05:50 +0900 Subject: [PATCH 40/82] Fix broken version --- lib/mobility/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mobility/version.rb b/lib/mobility/version.rb index 591a11ef1..68a2830d7 100644 --- a/lib/mobility/version.rb +++ b/lib/mobility/version.rb @@ -9,7 +9,7 @@ module VERSION MAJOR = 1 MINOR = 3 TINY = 0 - PRE = alpha + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end From 14d2ab96b8e4f815d3557b64c0db2de87ae6d22e Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sun, 17 Oct 2021 16:03:49 +0900 Subject: [PATCH 41/82] Add unreleased PR --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 142e08134..41e22be11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Mobility Changelog +## Unreleased + +- Fix ActiveRecord JSONB blank values + ([#536](https://github.com/shioyama/mobility/pull/536)) + ## 1.2 ### 1.2.2 From a5292e19f97e8102c3593179b4a6cc96ce4de7dd Mon Sep 17 00:00:00 2001 From: Markus Doits Date: Thu, 21 Oct 2021 16:26:09 +0200 Subject: [PATCH 42/82] fix passing wrong options to super in fallbacks plugin --- lib/mobility/plugins/fallbacks.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mobility/plugins/fallbacks.rb b/lib/mobility/plugins/fallbacks.rb index d93bdafdb..a498dd2da 100644 --- a/lib/mobility/plugins/fallbacks.rb +++ b/lib/mobility/plugins/fallbacks.rb @@ -138,7 +138,7 @@ def [](locale) module BackendInstanceMethods def read(locale, fallback: true, **accessor_options) - return super(locale, **options) if !fallback || accessor_options[:locale] + return super(locale, **accessor_options) if !fallback || accessor_options[:locale] locales = fallback == true ? fallbacks[locale] : [locale, *fallback] locales.each do |fallback_locale| @@ -146,7 +146,7 @@ def read(locale, fallback: true, **accessor_options) return value if Util.present?(value) end - super(locale, **options) + super(locale, **accessor_options) end private From 00407b13b81fe1f14c0cc85489608941e5cd2d4a Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Mon, 25 Oct 2021 22:36:55 +0900 Subject: [PATCH 43/82] Rename accessor_options to kwargs --- lib/mobility/plugins/fallbacks.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/mobility/plugins/fallbacks.rb b/lib/mobility/plugins/fallbacks.rb index a498dd2da..2d639c1cf 100644 --- a/lib/mobility/plugins/fallbacks.rb +++ b/lib/mobility/plugins/fallbacks.rb @@ -137,16 +137,16 @@ def [](locale) end module BackendInstanceMethods - def read(locale, fallback: true, **accessor_options) - return super(locale, **accessor_options) if !fallback || accessor_options[:locale] + def read(locale, fallback: true, **kwargs) + return super(locale, **kwargs) if !fallback || kwargs[:locale] locales = fallback == true ? fallbacks[locale] : [locale, *fallback] locales.each do |fallback_locale| - value = super(fallback_locale, **accessor_options) + value = super(fallback_locale, **kwargs) return value if Util.present?(value) end - super(locale, **accessor_options) + super(locale, **kwargs) end private From 7b4d4eb0e9060368078b6c2a0e469498261fbd01 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Mon, 25 Oct 2021 22:38:38 +0900 Subject: [PATCH 44/82] Release 1.2.3 --- CHANGELOG.md | 4 ++++ README.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41e22be11..f2092c2ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ## 1.2 +### 1.2.3 +- Fix passing wrong options to super in fallbacks plugin + ([#539](https://github.com/shioyama/mobility/pull/539)) + ### 1.2.2 - Make models work with `Marshal.dump` ([#532](https://github.com/shioyama/mobility/pull/532)) diff --git a/README.md b/README.md index 1139c00d0..311515aab 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Installation Add this line to your application's Gemfile: ```ruby -gem 'mobility', '~> 1.2.2' +gem 'mobility', '~> 1.2.3' ``` ### ActiveRecord (Rails) From c818900f5594ca7dfcaeab64a12b00e14893b6e9 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Mon, 25 Oct 2021 22:42:44 +0900 Subject: [PATCH 45/82] Add 1-1, 1-2 branches --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56e6d910a..7f9942d6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,11 +4,15 @@ on: push: branches: - master + - 1-2 + - 1-1 - 1-0 - 0-8 pull_request: branches: - master + - 1-2 + - 1-1 - 1-0 - 0-8 From de0d3e9e19b65b5d50ee8b70ae6a37f7531e649c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Hu=CC=88nig?= Date: Tue, 26 Oct 2021 19:54:47 +0200 Subject: [PATCH 46/82] Support primary keys other then :id on model classes --- lib/mobility/backends/active_record/key_value.rb | 2 +- lib/mobility/backends/active_record/table.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mobility/backends/active_record/key_value.rb b/lib/mobility/backends/active_record/key_value.rb index e81a356fe..24e0b7f5b 100644 --- a/lib/mobility/backends/active_record/key_value.rb +++ b/lib/mobility/backends/active_record/key_value.rb @@ -138,7 +138,7 @@ def join_translations(relation, key, locale, join_type) on(t[key_column].eq(key). and(t[:locale].eq(locale). and(t[:"#{belongs_to}_type"].eq(model_class.base_class.name). - and(t[:"#{belongs_to}_id"].eq(m[:id]))))).join_sources) + and(t[:"#{belongs_to}_id"].eq(m[model_class.primary_key] || m[:id]))))).join_sources) end def already_joined?(relation, name, locale, join_type) diff --git a/lib/mobility/backends/active_record/table.rb b/lib/mobility/backends/active_record/table.rb index 6148b10d7..195625a4b 100644 --- a/lib/mobility/backends/active_record/table.rb +++ b/lib/mobility/backends/active_record/table.rb @@ -145,7 +145,7 @@ def join_translations(relation, locale, join_type) m = model_class.arel_table t = model_class.const_get(subclass_name).arel_table.alias(table_alias(locale)) relation.joins(m.join(t, join_type). - on(t[foreign_key].eq(m[:id]). + on(t[foreign_key].eq(m[model_class.primary_key] || m[:id]). and(t[:locale].eq(locale))).join_sources) end From 05f8379f0b79e51dbe4b815b08a2bcd02bbc5ef5 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Wed, 3 Nov 2021 17:50:49 +0900 Subject: [PATCH 47/82] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2092c2ca..d4a153bbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Fix ActiveRecord JSONB blank values ([#536](https://github.com/shioyama/mobility/pull/536)) +- Support primary keys other then :id on model classes + ([#542](https://github.com/shioyama/mobility/pull/542)) ## 1.2 From 15be6e8c2950ee102973e997d154fac80e2881cd Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Wed, 17 Nov 2021 22:14:06 +0900 Subject: [PATCH 48/82] Fix fallbacks performance regression --- lib/mobility/plugins/fallbacks.rb | 33 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/mobility/plugins/fallbacks.rb b/lib/mobility/plugins/fallbacks.rb index 2d639c1cf..d7e27b187 100644 --- a/lib/mobility/plugins/fallbacks.rb +++ b/lib/mobility/plugins/fallbacks.rb @@ -117,10 +117,21 @@ module Fallbacks # Applies fallbacks plugin to attributes. Completely disables fallbacks # on model if option is +false+. included_hook do |_, backend_class| - backend_class.include(BackendInstanceMethods) unless options[:fallbacks] == false - # This is weird. We need to find a better way to allow customization of - # rarely-customized code like this. - backend_class.define_method(:generate_fallbacks, &method(:generate_fallbacks)) + unless options[:fallbacks] == false + backend_class.include(BackendInstanceMethods) + + fallbacks = + if options[:fallbacks].is_a?(Hash) + generate_fallbacks(options[:fallbacks]) + elsif options[:fallbacks] == true + generate_fallbacks({}) + else + ::Hash.new { [] } + end + + backend_class.singleton_class.attr_reader :fallbacks + backend_class.instance_variable_set(:@fallbacks, fallbacks) + end end private @@ -140,7 +151,7 @@ module BackendInstanceMethods def read(locale, fallback: true, **kwargs) return super(locale, **kwargs) if !fallback || kwargs[:locale] - locales = fallback == true ? fallbacks[locale] : [locale, *fallback] + locales = fallback == true ? self.class.fallbacks[locale] : [locale, *fallback] locales.each do |fallback_locale| value = super(fallback_locale, **kwargs) return value if Util.present?(value) @@ -148,18 +159,6 @@ def read(locale, fallback: true, **kwargs) super(locale, **kwargs) end - - private - - def fallbacks - if options[:fallbacks].is_a?(Hash) - generate_fallbacks(options[:fallbacks]) - elsif options[:fallbacks] == true - generate_fallbacks({}) - else - ::Hash.new { [] } - end - end end end From 9a54517f24cd4e4f56fc4a64e0d1f0d0bced61a1 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sun, 28 Nov 2021 13:46:10 +0900 Subject: [PATCH 49/82] Release 1.2.4 --- CHANGELOG.md | 4 ++++ README.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4a153bbc..3fe987c64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ ## 1.2 +### 1.2.4 +- Fix fallbacks performance regression + ([#548](https://github.com/shioyama/mobility/pull/548)) + ### 1.2.3 - Fix passing wrong options to super in fallbacks plugin ([#539](https://github.com/shioyama/mobility/pull/539)) diff --git a/README.md b/README.md index 311515aab..59773f0cf 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Installation Add this line to your application's Gemfile: ```ruby -gem 'mobility', '~> 1.2.3' +gem 'mobility', '~> 1.2.4' ``` ### ActiveRecord (Rails) From f3bd05ad622b0b6f9a79a5b57810bad989c0d44c Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 3 Dec 2021 21:53:04 +0900 Subject: [PATCH 50/82] Avoid referencing ActiveRecord::Base --- lib/rails/generators/mobility/backend_generators/base.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/rails/generators/mobility/backend_generators/base.rb b/lib/rails/generators/mobility/backend_generators/base.rb index c098346bd..8e2cfb90d 100644 --- a/lib/rails/generators/mobility/backend_generators/base.rb +++ b/lib/rails/generators/mobility/backend_generators/base.rb @@ -46,7 +46,9 @@ def data_source_exists? connection.data_source_exists?(table_name) end - delegate :connection, to: ::ActiveRecord::Base + def connection + ::ActiveRecord::Base.connection + end def truncate_index_name(index_name) if index_name.size < connection.index_name_length From 89cf6558eeb610983e6e43c34fc88a9adbd38956 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sat, 4 Dec 2021 17:28:19 +0900 Subject: [PATCH 51/82] Release 1.2.5 --- CHANGELOG.md | 4 ++++ README.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe987c64..4c16379b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ ## 1.2 +### 1.2.5 +- Avoid referencing ActiveRecord::Base + ([#550](https://github.com/shioyama/mobility/pull/550)) + ### 1.2.4 - Fix fallbacks performance regression ([#548](https://github.com/shioyama/mobility/pull/548)) diff --git a/README.md b/README.md index 59773f0cf..f5aa743e9 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Installation Add this line to your application's Gemfile: ```ruby -gem 'mobility', '~> 1.2.4' +gem 'mobility', '~> 1.2.5' ``` ### ActiveRecord (Rails) From 389fc675db3d24f3e4759dc1890b261607d07d0e Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Wed, 16 Feb 2022 10:13:02 +0900 Subject: [PATCH 52/82] Remove installation step for MySQL --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f9942d6d..a67d5d27d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,9 +188,6 @@ jobs: - name: Install Postgres run: sudo apt-get install libpq-dev postgresql-client -y if: matrix.database == 'postgres' - - name: Install MySQL - run: sudo apt-get install libmysqlclient-dev mysql-client -y - if: matrix.database == 'mysql' - id: cache-bundler uses: actions/cache@v2 with: From 967c92f46382949f14fae3834e3e711b542bd3e7 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Wed, 16 Feb 2022 10:29:00 +0900 Subject: [PATCH 53/82] Handle attribute_method_matchers rename --- lib/mobility/plugins/active_model/dirty.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/mobility/plugins/active_model/dirty.rb b/lib/mobility/plugins/active_model/dirty.rb index 30bd26fc7..3cd13a79b 100644 --- a/lib/mobility/plugins/active_model/dirty.rb +++ b/lib/mobility/plugins/active_model/dirty.rb @@ -153,7 +153,13 @@ def #{method_name}(attr_name, *rest#{kwargs}) # suffixes is simplest given they change from Rails version to version. def patterns @patterns ||= - (klass.attribute_method_matchers.map { |p| "#{p.prefix}%s#{p.suffix}" } - excluded_patterns) + begin + # Method name changes in Rails 7.1 + attribute_method_patterns = klass.respond_to?(:attribute_method_patterns) ? + klass.attribute_method_patterns : + klass.attribute_method_matchers + attribute_method_patterns.map { |p| "#{p.prefix}%s#{p.suffix}" } - excluded_patterns + end end private From 46a5ea5c9d11737cc999e08cb3390c0602eb95d9 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Wed, 16 Feb 2022 10:36:08 +0900 Subject: [PATCH 54/82] Test against Rails 7.0 --- .github/workflows/ci.yml | 22 ++++++++++++++-------- Gemfile | 6 +++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a67d5d27d..f852fc164 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,8 @@ jobs: version: '6.0' - name: 'active_record' version: '6.1' + - name: 'active_record' + version: '7.0' - name: 'sequel' version: '5' experimental: [false] @@ -66,7 +68,7 @@ jobs: feature: 'rails' orm: name: 'active_record' - version: '6.1' + version: '7.0' database: 'sqlite3' experimental: false - ruby: '3.0' @@ -80,27 +82,27 @@ jobs: feature: 'unit' orm: name: 'active_record' - version: '7.0' + version: 'edge' experimental: true - ruby: '3.0' database: 'mysql' feature: 'unit' orm: name: 'active_record' - version: '7.0' + version: 'edge' experimental: true - ruby: '3.0' database: 'postgres' feature: 'unit' orm: name: 'active_record' - version: '7.0' + version: 'edge' experimental: true - ruby: '2.7' feature: 'rails' orm: name: 'active_record' - version: '6.1' + version: '7.0' database: 'sqlite3' experimental: false - ruby: '2.7' @@ -114,23 +116,27 @@ jobs: feature: 'unit' orm: name: 'active_record' - version: '7.0' + version: 'edge' experimental: true - ruby: '2.7' database: 'mysql' feature: 'unit' orm: name: 'active_record' - version: '7.0' + version: 'edge' experimental: true - ruby: '2.7' database: 'postgres' feature: 'unit' orm: name: 'active_record' - version: '7.0' + version: 'edge' experimental: true exclude: + - ruby: '2.6' + orm: + name: 'active_record' + version: '7.0' - ruby: '2.7' orm: name: 'active_record' diff --git a/Gemfile b/Gemfile index cac693c84..68f2ded9b 100644 --- a/Gemfile +++ b/Gemfile @@ -8,11 +8,11 @@ orm, orm_version = ENV['ORM'], ENV['ORM_VERSION'] group :development, :test do case orm when 'active_record' - orm_version ||= '6.1' + orm_version ||= '7.0' case orm_version - when '4.2', '5.0', '5.1', '5.2', '6.0', '6.1' + when '4.2', '5.0', '5.1', '5.2', '6.0', '6.1', '7.0' gem 'activerecord', "~> #{orm_version}.0" - when '7.0' + when 'edge' git 'https://github.com/rails/rails.git', branch: 'main' do gem 'activerecord' gem 'activesupport' From 4bf96b713991d5cf379aa6caeae2a3c448c73302 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Wed, 16 Feb 2022 11:19:12 +0900 Subject: [PATCH 55/82] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c16379b1..fb0b04292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ([#536](https://github.com/shioyama/mobility/pull/536)) - Support primary keys other then :id on model classes ([#542](https://github.com/shioyama/mobility/pull/542)) +- Handle `attribute_metohd_matchers` rename (part of + [#560](https://github.com/shioyama/mobility/pull/560)) ## 1.2 From b678b1de1bb890b7861f2a5973b348a493e714a3 Mon Sep 17 00:00:00 2001 From: Markus Doits Date: Fri, 5 Nov 2021 18:50:20 +0100 Subject: [PATCH 56/82] clean up and refactor active record container backend fixes dirty tracking and creating empty translation hashes on read fixes #540 --- .../backends/active_record/container.rb | 49 ++++++++----------- .../backends/active_record/container_spec.rb | 13 +++++ 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/lib/mobility/backends/active_record/container.rb b/lib/mobility/backends/active_record/container.rb index 079790717..e72ab569b 100644 --- a/lib/mobility/backends/active_record/container.rb +++ b/lib/mobility/backends/active_record/container.rb @@ -22,7 +22,11 @@ class ActiveRecord::Container # @param [Hash] options # @return [String,Integer,Boolean] Value of translation def read(locale, _ = nil) - model_translations(locale)[attribute] + locale_translations = model_translations(locale) + + return unless locale_translations + + locale_translations[attribute.to_s] end # @note Translation may be a string, integer, boolean, hash or array @@ -33,7 +37,7 @@ def read(locale, _ = nil) # @return [String,Integer,Boolean] Updated value def write(locale, value, _ = nil) set_attribute_translation(locale, value) - model_translations(locale)[attribute] + read(locale) end # @!endgroup @@ -42,8 +46,7 @@ class << self # @option options [Symbol] column_name (:translations) Name of column on which to store translations # @raise [InvalidColumnType] if the type of the container column is not json or jsonb def configure(options) - options[:column_name] ||= :translations - options[:column_name] = options[:column_name].to_sym + options[:column_name] = options[:column_name]&.to_sym || :translations end # @!endgroup @@ -84,8 +87,6 @@ def each_locale end setup do |_attributes, options| - store options[:column_name], coder: Coder - # Fix for duping depth-2 jsonb column in AR < 5.0 if ::ActiveRecord::VERSION::STRING < '5.0' column_name = options[:column_name] @@ -107,32 +108,22 @@ def initialize_dup(source) private def model_translations(locale) - model[column_name][locale] ||= {} - end + translations = model[column_name] - def set_attribute_translation(locale, value) - translations = model[column_name] || {} - translations[locale.to_s] ||= {} - translations[locale.to_s][attribute] = value - model[column_name] = translations - end + return unless translations - class Coder - def self.dump(obj) - if obj.is_a? ::Hash - obj.inject({}) do |translations, (locale, value)| - value.each do |k, v| - (translations[locale] ||= {})[k] = v if v.present? - end - translations - end - else - raise ArgumentError, "Attribute is supposed to be a Hash, but was a #{obj.class}. -- #{obj.inspect}" - end - end + translations[locale.to_s] + end - def self.load(obj) - obj + def set_attribute_translation(locale, value) + locale_translations = model_translations(locale) + + if locale_translations + locale_translations[attribute.to_s] = value + locale_translations.compact! + model[column_name].compact! + elsif value + (model[column_name] ||= {})[locale.to_s] = { attribute.to_s => value } end end diff --git a/spec/mobility/backends/active_record/container_spec.rb b/spec/mobility/backends/active_record/container_spec.rb index 19016ee57..a4be3397b 100644 --- a/spec/mobility/backends/active_record/container_spec.rb +++ b/spec/mobility/backends/active_record/container_spec.rb @@ -20,6 +20,19 @@ include_accessor_examples 'ContainerPost' include_dup_examples 'ContainerPost' include_cache_key_examples 'ContainerPost' + + it 'does not change translations on access' do + post = ContainerPost.new + + expect { post.title }.not_to change { post.translations }.from({}) + end + + it 'does not mix up dirty tracking on access' do + post = ContainerPost.new + + expect { post.title }.not_to change { post.changes }.from({}) + expect(post.changed?).to be(false) + end end context "with query plugin" do From 87a3a98943d0ff7d7b94eab0cda100ddfe303869 Mon Sep 17 00:00:00 2001 From: Markus Doits Date: Fri, 5 Nov 2021 19:05:07 +0100 Subject: [PATCH 57/82] refactor spec to match other spec style --- .../backends/active_record/container_spec.rb | 25 +++++++++++-------- spec/support/matchers/not_change.rb | 1 + 2 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 spec/support/matchers/not_change.rb diff --git a/spec/mobility/backends/active_record/container_spec.rb b/spec/mobility/backends/active_record/container_spec.rb index a4be3397b..505c52bf7 100644 --- a/spec/mobility/backends/active_record/container_spec.rb +++ b/spec/mobility/backends/active_record/container_spec.rb @@ -21,17 +21,22 @@ include_dup_examples 'ContainerPost' include_cache_key_examples 'ContainerPost' - it 'does not change translations on access' do - post = ContainerPost.new - - expect { post.title }.not_to change { post.translations }.from({}) - end - - it 'does not mix up dirty tracking on access' do - post = ContainerPost.new + it 'does not change translations and dirty tracking' do + post = ContainerPost.create! + + aggregate_failures "on access" do + expect { post.title } + .to not_change { post.translations }.from({}) + .and not_change { post.changes }.from({}) + .and not_change { post.changed? }.from(false) + end - expect { post.title }.not_to change { post.changes }.from({}) - expect(post.changed?).to be(false) + aggregate_failures "on reload" do + expect { post.reload } + .to not_change { post.translations }.from({}) + .and not_change { post.changes }.from({}) + .and not_change { post.changed? }.from(false) + end end end diff --git a/spec/support/matchers/not_change.rb b/spec/support/matchers/not_change.rb new file mode 100644 index 000000000..93f5b66e5 --- /dev/null +++ b/spec/support/matchers/not_change.rb @@ -0,0 +1 @@ +RSpec::Matchers.define_negated_matcher :not_change, :change From d0a517ac20938e3420acc9fa3bd35e82ff9ce152 Mon Sep 17 00:00:00 2001 From: Markus Doits Date: Thu, 2 Dec 2021 17:14:36 +0100 Subject: [PATCH 58/82] updates after review --- .../backends/active_record/container.rb | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/mobility/backends/active_record/container.rb b/lib/mobility/backends/active_record/container.rb index e72ab569b..83ca63984 100644 --- a/lib/mobility/backends/active_record/container.rb +++ b/lib/mobility/backends/active_record/container.rb @@ -81,8 +81,8 @@ def get_column_type # @!macro backend_iterator def each_locale - model[column_name].each do |l, v| - yield l.to_sym if v.present? + model[column_name].each_key do |l| + yield l.to_sym end end @@ -108,22 +108,23 @@ def initialize_dup(source) private def model_translations(locale) - translations = model[column_name] - - return unless translations - - translations[locale.to_s] + model[column_name][locale.to_s] end def set_attribute_translation(locale, value) locale_translations = model_translations(locale) if locale_translations - locale_translations[attribute.to_s] = value - locale_translations.compact! - model[column_name].compact! - elsif value - (model[column_name] ||= {})[locale.to_s] = { attribute.to_s => value } + if value.nil? + locale_translations.delete(attribute.to_s) + + # delete empty locale hash if last attribute was just deleted + model[column_name].delete(locale.to_s) if locale_translations.empty? + else + locale_translations[attribute.to_s] = value + end + elsif !value.nil? + model[column_name][locale.to_s] = { attribute.to_s => value } end end From d75c88dfc7e6166475a63085c54819989b464341 Mon Sep 17 00:00:00 2001 From: Markus Doits Date: Thu, 2 Dec 2021 17:40:48 +0100 Subject: [PATCH 59/82] add spec about removing empty locale hash after last attribute is deleted --- .../backends/active_record/container_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/mobility/backends/active_record/container_spec.rb b/spec/mobility/backends/active_record/container_spec.rb index 505c52bf7..0f2a16d5e 100644 --- a/spec/mobility/backends/active_record/container_spec.rb +++ b/spec/mobility/backends/active_record/container_spec.rb @@ -38,6 +38,18 @@ .and not_change { post.changed? }.from(false) end end + + it 'deletes locale hash if last attribute is removed' do + post = ContainerPost.create! + + ::Mobility.with_locale(:en) { post.title = 'Title en' } + ::Mobility.with_locale(:de) { post.title = 'Title de' } + + expect { post.title = nil } + .to change { post.translations } + .from({ "en" => { "title" => "Title en" }, "de" => { "title" => "Title de" }}) + .to({ "de" => { "title" => "Title de" }}) + end end context "with query plugin" do From 73ca1be3276ac768da8028e38fe5e11d0fa3beec Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Mon, 28 Feb 2022 21:13:40 +0900 Subject: [PATCH 60/82] Add changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb0b04292..92b8bc1db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ ([#542](https://github.com/shioyama/mobility/pull/542)) - Handle `attribute_metohd_matchers` rename (part of [#560](https://github.com/shioyama/mobility/pull/560)) +- Clean up and refactor container backend + ([#543](https://github.com/shioyama/mobility/pull/543)), + thanks [doits](https://github.com/doits)! ## 1.2 From cfba9565ebbcb6871ef1e4ad09b8f012534c8a36 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Wed, 17 Nov 2021 20:58:32 +0900 Subject: [PATCH 61/82] Require mfa on rubygems --- mobility.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/mobility.gemspec b/mobility.gemspec index f6761eeaa..474106609 100644 --- a/mobility.gemspec +++ b/mobility.gemspec @@ -20,6 +20,7 @@ Gem::Specification.new do |spec| spec.metadata['homepage_uri'] = spec.homepage spec.metadata['source_code_uri'] = 'https://github.com/shioyama/mobility' spec.metadata['changelog_uri'] = 'https://github.com/shioyama/mobility/blob/master/CHANGELOG.md' + spec.metadata['rubygems_mfa_required'] = 'true' spec.files = Dir['{lib/**/*,[A-Z]*}'] spec.bindir = "exe" From c5ee5df2496c261b12a8e19ff7db83dc5c26ba88 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Wed, 17 Nov 2021 21:00:36 +0900 Subject: [PATCH 62/82] Add bug_tracker_uri --- mobility.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/mobility.gemspec b/mobility.gemspec index 474106609..0624df8f6 100644 --- a/mobility.gemspec +++ b/mobility.gemspec @@ -19,6 +19,7 @@ Gem::Specification.new do |spec| spec.metadata['homepage_uri'] = spec.homepage spec.metadata['source_code_uri'] = 'https://github.com/shioyama/mobility' + spec.metadata['bug_tracker_uri'] = 'https://github.com/shioyama/mobility/issues' spec.metadata['changelog_uri'] = 'https://github.com/shioyama/mobility/blob/master/CHANGELOG.md' spec.metadata['rubygems_mfa_required'] = 'true' From 7777dd4f81feaa6b0f99647ed7dbfb9adad7552d Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Wed, 2 Mar 2022 12:22:15 +0900 Subject: [PATCH 63/82] Update cert --- certs/shioyama.pem | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/certs/shioyama.pem b/certs/shioyama.pem index cc022e53e..b17aca9fa 100644 --- a/certs/shioyama.pem +++ b/certs/shioyama.pem @@ -1,7 +1,7 @@ -----BEGIN CERTIFICATE----- MIIEODCCAqCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDDBhjaHJp -cy9EQz1kZWppbWF0YS9EQz1jb20wHhcNMjEwMjE4MTMxMzA5WhcNMjIwMjE4MTMx -MzA5WjAjMSEwHwYDVQQDDBhjaHJpcy9EQz1kZWppbWF0YS9EQz1jb20wggGiMA0G +cy9EQz1kZWppbWF0YS9EQz1jb20wHhcNMjIwMzAyMDMyMTA5WhcNMjMwMzAyMDMy +MTA5WjAjMSEwHwYDVQQDDBhjaHJpcy9EQz1kZWppbWF0YS9EQz1jb20wggGiMA0G CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDURCKbt5oY0sCp4kYK1u5SLzVHg6Q1 2LejeQvUGpR3gulWqrq/507XRxE/9FSpLfgo3cGGYio1/gg2Yp7pBI4ZNEz8d2Vg 6caWLHYtHYF0/jlo177UspEF1bt3lCCmaA/ZyQpvoLi76Jf6VCBjepMqhLjeBSsA @@ -13,13 +13,13 @@ bRGON33BZ99gPiYdGfd3Pc/7FooteJASjKIO4Hman2ELRIdu6Bq+fIkTdJBcruS/ XL6xoRitCG7CX0IqmMKuLiKA/J0amAikHGsCAwEAAaN3MHUwCQYDVR0TBAIwADAL BgNVHQ8EBAMCBLAwHQYDVR0OBBYEFMNUGAhS68egZT6DOfJwrfIdCtT/MB0GA1Ud EQQWMBSBEmNocmlzQGRlamltYXRhLmNvbTAdBgNVHRIEFjAUgRJjaHJpc0BkZWpp -bWF0YS5jb20wDQYJKoZIhvcNAQELBQADggGBAH1RnWhD9bum/ijqzAAlkGWYzGza -h/3seA2bg1r5bbttFjD48f7RepfoAMxAqfiUWcukoukJeu7UY8jWmUIn9ut1oXct -Fh0YnueLFzzmppCCU+/SX5mc1y7mHYZHiU5n8qy1wJ6ljLWXVeprgJ96NdnmxuVU -dzPPSDTex/x7xBvHiaPc/uZSLc173N3qdY/Cd0B3/OflYeU2h5UpIHnmXrsONdMC -Xohy+Rrr2yT09MPYG+llpLHDnXmTnPsOZUSL5Q4c/iolodv4xJZKwLMZwrm2hQl9 -9Or9Os+qxY0zWxmWuAtTFrskLAMhckCPDEcqSZmW4CT1a/quC2Oh0y1GsXPcqtqt -hLRuwfTXGor6bg4CrU7GRbSqjvnBepct5lwZiZrOCnMEUpY+9Q8fwmG3o3B+wBsw -eBMcZq0d1tbtv1M1UXND9mOfhLZ31YvoSTPkrJiRpljUNgD0+ugelnr1/5X/9k8y -J9QOd3C5jpSShf/HMvpJnFuSYFm19cH9GrHjvw== +bWF0YS5jb20wDQYJKoZIhvcNAQELBQADggGBAHJPxoU7brN6goci9iclRYUq1Prs +51E87VRywUDysHpaHJoGRTqRJsQxi5aGZ9pIbiXGj9WJKKnrhiv5cM5fWtAsz564 +Ro+Zyx6UVt/2z9rcfYrnXtmC9wh+5/0UqAeoan9RiSd8lscQZ9pEY0E3cmzJRHSU +t8kwB2ipVkFO17mdTVgc3C2ZbWRq80eTzkELDBb+8xO0Cskyh4sGMTOKfHs2RWcJ +217Qeg0F9w0RcqwnORe5zmPihY9zswCPh0IUaJa1pNY+MLTff9LE/qTl3WVTgrif +HsSSnstQYPSLJ3hSP/cu1aOmdXlJiim//XlDQ9DNp4KWje3UW3DMRdTwjW5wPmUq +VA9Ij7DUPaZzUpy1NZEigf1GshvslOnvN5bgol1YFR46jpfZVlgt0K5XBQVNvp/A +QHgocnSksU5GOM+G2UhjVycbTamd+bCxjWAZTEDZNafFt5CmnfK1D1UTIblR/ci9 +fUDdW+GhxhobB8N1mtDRlhELoxLLjx7mSvJ3Wg== -----END CERTIFICATE----- From 7683433b80768e1d40cb916f706ce84e9cd4ae83 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Wed, 2 Mar 2022 12:15:49 +0900 Subject: [PATCH 64/82] Release 1.2.6 --- CHANGELOG.md | 4 ++++ README.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b8bc1db..62217e75e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ ## 1.2 +### 1.2.6 +- Require mfa on rubygems + ([#545](https://github.com/shioyama/mobility/pull/545)) + ### 1.2.5 - Avoid referencing ActiveRecord::Base ([#550](https://github.com/shioyama/mobility/pull/550)) diff --git a/README.md b/README.md index f5aa743e9..17617e190 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Installation Add this line to your application's Gemfile: ```ruby -gem 'mobility', '~> 1.2.5' +gem 'mobility', '~> 1.2.6' ``` ### ActiveRecord (Rails) From 2767e9b88d2c28cb1cbfd6fe39cc832696e996cf Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sat, 18 Jun 2022 10:48:48 +0900 Subject: [PATCH 65/82] Avoid querying same attribute more than once Fixes #564 --- lib/mobility/plugins/active_record/query.rb | 5 +++- .../plugins/active_record/query_spec.rb | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/mobility/plugins/active_record/query.rb b/lib/mobility/plugins/active_record/query.rb index 9803d4dd9..728942b4d 100644 --- a/lib/mobility/plugins/active_record/query.rb +++ b/lib/mobility/plugins/active_record/query.rb @@ -223,10 +223,13 @@ def _build(scope, opts, locale, invert) keys, predicates = opts.keys.map(&:to_s), [] + used_keys = [] + query_map = mods.inject(IDENTITY) do |qm, mod| - i18n_keys = mod.names & keys + i18n_keys = mod.names & keys - used_keys next qm if i18n_keys.empty? + used_keys += i18n_keys mod_predicates = i18n_keys.map do |key| build_predicate(scope.backend_node(key.to_sym, locale), opts.delete(key)) end diff --git a/spec/mobility/plugins/active_record/query_spec.rb b/spec/mobility/plugins/active_record/query_spec.rb index 1368eb10f..a096733cb 100644 --- a/spec/mobility/plugins/active_record/query_spec.rb +++ b/spec/mobility/plugins/active_record/query_spec.rb @@ -192,4 +192,29 @@ expect { query.order(:car_id) }.not_to raise_error end end + + describe "regression for #564" do + it "works if translates is called multiple times" do + stub_const 'Article', Class.new(ActiveRecord::Base) + 2.times { translates Article, :title, backend: :table } + + article = Article.create(title: "Title") + + expect(Article.i18n.where(title: "Title")).to eq([article]) + end + + it "handles intersecting attribute declarations" do + stub_const 'Article', Class.new(ActiveRecord::Base) + translates Article, :title, :content, backend: :key_value, type: :string + + # title defined below clobbers title defined above + translates Article, :title, backend: :table + + article1 = Article.create(title: "Title") + article2 = Article.create(title: "Title", content: "Content") + + expect(Article.i18n.where(title: "Title")).to match_array([article1, article2]) + expect(Article.i18n.where(title: "Title", content: "Content")).to eq([article2]) + end + end end From 60fdeafc25d07da1bc42e9c358b74a52b870448d Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sat, 18 Jun 2022 11:29:10 +0900 Subject: [PATCH 66/82] Avoid querying same attribute more than once (Sequel) --- lib/mobility/plugins/sequel/query.rb | 5 ++++- spec/mobility/plugins/sequel/query_spec.rb | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/mobility/plugins/sequel/query.rb b/lib/mobility/plugins/sequel/query.rb index a84b4af1e..434a41a6e 100644 --- a/lib/mobility/plugins/sequel/query.rb +++ b/lib/mobility/plugins/sequel/query.rb @@ -135,10 +135,13 @@ def _build(dataset, cond, locale, query_method) keys, predicates = cond.keys, [] model = dataset.model + used_keys = [] + query_map = attribute_modules(model).inject(IDENTITY) do |qm, mod| - i18n_keys = mod.names.map(&:to_sym) & keys + i18n_keys = mod.names.map(&:to_sym) & keys - used_keys next qm if i18n_keys.empty? + used_keys += i18n_keys mod_predicates = i18n_keys.map do |key| build_predicate(dataset.backend_op(key, locale), cond.delete(key)) end diff --git a/spec/mobility/plugins/sequel/query_spec.rb b/spec/mobility/plugins/sequel/query_spec.rb index 3b8647930..943f0bab7 100644 --- a/spec/mobility/plugins/sequel/query_spec.rb +++ b/spec/mobility/plugins/sequel/query_spec.rb @@ -157,4 +157,17 @@ end.select_all(:articles).all).to eq([article_ja]) end end + + describe "regression for #564 (Sequel version)" do + it "works if translates is called multiple times" do + stub_const 'Article', Class.new(Sequel::Model) + Article.dataset = DB[:articles] + + 2.times { translates Article, :title, backend: :table } + + article = Article.create(title: "Title") + + expect(Article.i18n.where(title: "Title").select_all(:articles).all).to eq([article]) + end + end end From 10fd352998f15837da9decfc7fb5ff02863950b9 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sat, 18 Jun 2022 21:03:45 +0900 Subject: [PATCH 67/82] Release 1.2.7 --- CHANGELOG.md | 5 +++++ README.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62217e75e..e6bcda658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ ## 1.2 +### 1.2.7 +- Do not query same attribute more than once, fixes + [#564](https://github.com/shioyama/mobility/pull/578) + ([#577](https://github.com/shioyama/mobility/pull/577)) + ### 1.2.6 - Require mfa on rubygems ([#545](https://github.com/shioyama/mobility/pull/545)) diff --git a/README.md b/README.md index 17617e190..a7ad1d247 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Installation Add this line to your application's Gemfile: ```ruby -gem 'mobility', '~> 1.2.6' +gem 'mobility', '~> 1.2.7' ``` ### ActiveRecord (Rails) From b3d8806845619f0a64ee0af5861b85ab5d904059 Mon Sep 17 00:00:00 2001 From: mrbrdo Date: Thu, 14 Apr 2022 21:14:22 +0200 Subject: [PATCH 68/82] Fix issues with subclassing, such as when using ActiveRecord STI. Fixes mobility to work if subclassing a subclass of a mobility-enabled class, and looks up mobility_backend_classes later in execution (upon being called) to be more flexible. --- lib/mobility/plugins/backend.rb | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/mobility/plugins/backend.rb b/lib/mobility/plugins/backend.rb index d8b7230f1..3ceedbb0c 100644 --- a/lib/mobility/plugins/backend.rb +++ b/lib/mobility/plugins/backend.rb @@ -159,16 +159,22 @@ def mobility_backend_class(name) raise KeyError, "No backend for: #{name}" end - def inherited(klass) - parent_classes = mobility_backend_classes.freeze # ensure backend classes are not modified after being inherited - klass.class_eval { @mobility_backend_classes = parent_classes.dup } - super - end - protected def mobility_backend_classes - @mobility_backend_classes ||= {} + if @mobility_backend_classes + @mobility_backend_classes + else + @mobility_backend_classes = {} + parent_class = self.superclass + while parent_class&.respond_to?(:mobility_backend_classes, true) + # ensure backend classes are not modified after being inherited + parent_class_classes = parent_class.mobility_backend_classes.freeze + @mobility_backend_classes.merge!(parent_class_classes) + parent_class = parent_class.superclass + end + @mobility_backend_classes + end end end From ca9f048f3783f23604a37d9dc648db8ac70e6499 Mon Sep 17 00:00:00 2001 From: mrbrdo Date: Tue, 3 May 2022 14:18:29 +0200 Subject: [PATCH 69/82] add failing test for #566 --- spec/mobility/plugins/backend_spec.rb | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/spec/mobility/plugins/backend_spec.rb b/spec/mobility/plugins/backend_spec.rb index 4ef0e8178..c5a4fa154 100644 --- a/spec/mobility/plugins/backend_spec.rb +++ b/spec/mobility/plugins/backend_spec.rb @@ -112,10 +112,31 @@ mod1 = translations_class.new("title", backend: backend_class_1) model_class.include mod1 - Class.new(model_class) + # accessing mobility_backend_classes necessary to trigger parent freeze + Class.new(model_class).send(:mobility_backend_classes) expect(model_class.send(:mobility_backend_classes)).to be_frozen end + + it "works with subclasses of subclasses" do + backend_class_1 = Class.new + backend_class_1.include Mobility::Backend + + model_subclass = Class.new(model_class) + model_subclass_2 = Class.new(model_subclass) + + # we specifically include translations after subclassing + mod1 = translations_class.new("title", backend: backend_class_1) + model_class.include mod1 + + title_backend_class_1 = model_class.mobility_backend_class("title") + title_backend_class_2 = model_subclass.mobility_backend_class("title") + title_backend_class_3 = model_subclass_2.mobility_backend_class("title") + + expect(title_backend_class_1).to be <(backend_class_1) + expect(title_backend_class_2).to be <(backend_class_1) + expect(title_backend_class_3).to be <(backend_class_1) + end end describe "#mobility_backends" do From 9ec2534d25327df425ce9d6d9fc9b9c85a615de8 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sat, 18 Jun 2022 21:59:19 +0900 Subject: [PATCH 70/82] Release 1.2.8 --- CHANGELOG.md | 9 +++++++-- README.md | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6bcda658..b2b2fc42e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,19 @@ ([#536](https://github.com/shioyama/mobility/pull/536)) - Support primary keys other then :id on model classes ([#542](https://github.com/shioyama/mobility/pull/542)) -- Handle `attribute_metohd_matchers` rename (part of - [#560](https://github.com/shioyama/mobility/pull/560)) - Clean up and refactor container backend ([#543](https://github.com/shioyama/mobility/pull/543)), thanks [doits](https://github.com/doits)! ## 1.2 +### 1.2.8 +- Fix issues with subclassing, such as when using AR STI, + fixes [#566](https://github.com/shioyama/mobility/issues/566) + ([#568](https://github.com/shioyama/mobility/pull/568)) +- Handle `attribute_method_matchers` rename (part of + [#560](https://github.com/shioyama/mobility/pull/560)) + ### 1.2.7 - Do not query same attribute more than once, fixes [#564](https://github.com/shioyama/mobility/pull/578) diff --git a/README.md b/README.md index a7ad1d247..445ea981e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Installation Add this line to your application's Gemfile: ```ruby -gem 'mobility', '~> 1.2.7' +gem 'mobility', '~> 1.2.8' ``` ### ActiveRecord (Rails) From 93f4c476f2f763c3f8e31fe36dacc4d569ebd6cd Mon Sep 17 00:00:00 2001 From: Phil Allcock Date: Tue, 1 Feb 2022 15:40:42 +0000 Subject: [PATCH 71/82] Update fallthrough_accessor regex to allow for ISO 639-2 codes --- lib/mobility/plugins/fallthrough_accessors.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mobility/plugins/fallthrough_accessors.rb b/lib/mobility/plugins/fallthrough_accessors.rb index 7f69b35a8..9e238f946 100644 --- a/lib/mobility/plugins/fallthrough_accessors.rb +++ b/lib/mobility/plugins/fallthrough_accessors.rb @@ -36,7 +36,7 @@ module FallthroughAccessors private def define_fallthrough_accessors(*names) - method_name_regex = /\A(#{names.join('|')})_([a-z]{2}(_[a-z]{2})?)(=?|\??)\z/.freeze + method_name_regex = /\A(#{names.join('|')})_([a-z]{2,3}(_[a-z]{2})?)(=?|\??)\z/.freeze define_method :method_missing do |method_name, *args, &block| if method_name =~ method_name_regex From dbabf48a18a5ddb2bc23acd80872bb9e4fb3c667 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sun, 19 Jun 2022 10:18:17 +0900 Subject: [PATCH 72/82] Update test to check ISO 639-2 (3 character) language code --- spec/mobility/plugins/fallthrough_accessors_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/mobility/plugins/fallthrough_accessors_spec.rb b/spec/mobility/plugins/fallthrough_accessors_spec.rb index 35f3efdfc..b46cbead0 100644 --- a/spec/mobility/plugins/fallthrough_accessors_spec.rb +++ b/spec/mobility/plugins/fallthrough_accessors_spec.rb @@ -22,7 +22,7 @@ def title=(_, **); end it_behaves_like "locale accessor", :title, 'en' it_behaves_like "locale accessor", :title, 'de' it_behaves_like "locale accessor", :title, 'pt-BR' - it_behaves_like "locale accessor", :title, 'ru' + it_behaves_like "locale accessor", :title, 'rus' it 'passes arguments and options to super when method does not match' do mod = Module.new do From b3420746e0ad7b2dea1d7fe13f2da6c513b93675 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Sun, 19 Jun 2022 10:29:41 +0900 Subject: [PATCH 73/82] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2b2fc42e..9af9a35b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ - Clean up and refactor container backend ([#543](https://github.com/shioyama/mobility/pull/543)), thanks [doits](https://github.com/doits)! +- Update fallthrough_accessor regex to allow for ISO 639-2 codes + ([#580](https://github.com/shioyama/mobility/pull/580)) + thanks [phil-allcock](https://github.com/phil-allcock)! ## 1.2 From 516e19f694de43770217d9c63e008cefb6120aa0 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 24 Jun 2022 21:49:56 +0900 Subject: [PATCH 74/82] Add regression test for #513 with select --- spec/mobility/plugins/active_record/query_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/mobility/plugins/active_record/query_spec.rb b/spec/mobility/plugins/active_record/query_spec.rb index a096733cb..5ae31dc73 100644 --- a/spec/mobility/plugins/active_record/query_spec.rb +++ b/spec/mobility/plugins/active_record/query_spec.rb @@ -160,7 +160,7 @@ m = ActiveRecord::Migration.new m.verbose = false - m.create_table :cars + m.create_table(:cars) { |t| t.string :name } stub_const('Car', Class.new(ActiveRecord::Base) do has_many :parking_lots has_many :car_parts @@ -190,6 +190,7 @@ query = ParkingLot.includes(car: :car_parts).references(:car).merge(Car.i18n) expect { query.first }.not_to raise_error expect { query.order(:car_id) }.not_to raise_error + expect { query.select(:name) }.not_to raise_error end end From 8af0306019c7c20aa5f2c13dcc4b25f2aef12b10 Mon Sep 17 00:00:00 2001 From: Boris Rorsvort Date: Wed, 22 Jun 2022 15:48:40 +0200 Subject: [PATCH 75/82] Check that class responds to mobility_attribute? --- lib/mobility/plugins/active_record/query.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/mobility/plugins/active_record/query.rb b/lib/mobility/plugins/active_record/query.rb index 728942b4d..0811a36b3 100644 --- a/lib/mobility/plugins/active_record/query.rb +++ b/lib/mobility/plugins/active_record/query.rb @@ -163,9 +163,8 @@ def order(opts, *rest) define_method method_name do |*attrs, &block| return super(*attrs, &block) if (method_name == 'select' && block.present?) - if ::ActiveRecord::VERSION::STRING < '7.0' - return super(*attrs, &block) unless @klass.respond_to?(:mobility_attribute?) - end + return super(*attrs, &block) unless @klass.respond_to?(:mobility_attribute?) + return super(*attrs, &block) unless attrs.any?(&@klass.method(:mobility_attribute?)) keys = attrs.dup From c0e429620b2355cd70750f1ea6dc21f667706aac Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 24 Jun 2022 22:18:52 +0900 Subject: [PATCH 76/82] Release 1.2.9 --- CHANGELOG.md | 5 +++++ README.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9af9a35b0..ca3cb5593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ ## 1.2 +### 1.2.9 +- Fix missing method `mobility_attribute?` on Rails 7 + ([#582](https://github.com/shioyama/mobility/pull/582) and + [#584](https://github.com/shioyama/mobility/pull/584)) + ### 1.2.8 - Fix issues with subclassing, such as when using AR STI, fixes [#566](https://github.com/shioyama/mobility/issues/566) diff --git a/README.md b/README.md index 445ea981e..f97f1b0ed 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Installation Add this line to your application's Gemfile: ```ruby -gem 'mobility', '~> 1.2.8' +gem 'mobility', '~> 1.2.9' ``` ### ActiveRecord (Rails) From afeb067488f215cb469cfc9d7ad0a3853ae8ac5d Mon Sep 17 00:00:00 2001 From: Kartik Luke Singh <3857139+fluke@users.noreply.github.com> Date: Fri, 12 Aug 2022 14:27:33 +0530 Subject: [PATCH 77/82] Add column_fallback to Mobility initializer This feature was implemented in 1.2 but I had to read the PR to understand how to use it. --- .../generators/mobility/templates/initializer.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/rails/generators/mobility/templates/initializer.rb b/lib/rails/generators/mobility/templates/initializer.rb index a73cd686d..16a49247b 100644 --- a/lib/rails/generators/mobility/templates/initializer.rb +++ b/lib/rails/generators/mobility/templates/initializer.rb @@ -58,6 +58,19 @@ # Or uncomment this line to include but disable by default, and only enable # per model by passing +dirty: true+ to +translates+. # dirty false + + # Column Fallback + # + # Uncomment line below to fallback to original column. You can pass + # +column_fallback: true+ to +translates+ to return original column on + # default locale, or pass +column_fallback: [:en, :de]+ to +translates+ + # to return original column for those locales or pass + # +column_fallback: ->(locale) { ... }+ to +translates to evaluate which + # locales to return original column for. + # column_fallback + # + # Or uncomment this line to enable column fallback with a global default. + # column_fallback true # Fallbacks # From ff509a2756280a5666963e0d8c6d532511e02002 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 2 Jul 2021 17:03:33 +0700 Subject: [PATCH 78/82] change translation classes to be destroyed --- .../backends/active_record/key_value.rb | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/lib/mobility/backends/active_record/key_value.rb b/lib/mobility/backends/active_record/key_value.rb index 24e0b7f5b..f2ac12c3e 100644 --- a/lib/mobility/backends/active_record/key_value.rb +++ b/lib/mobility/backends/active_record/key_value.rb @@ -212,11 +212,55 @@ def visit_default(_) end end - setup do |attributes, _options, backend_class| - backend_class.define_has_many_association(self, attributes) - backend_class.define_initialize_dup(self) - backend_class.define_before_save_callback(self) - backend_class.define_after_destroy_callback(self) + setup do |attributes, options| + association_name = options[:association_name] + translation_class = options[:class_name] + key_column = options[:key_column] + value_column = options[:value_column] + belongs_to = options[:belongs_to] + + # Track all attributes for this association, so that we can limit the scope + # of keys for the association to only these attributes. We need to track the + # attributes assigned to the association in case this setup code is called + # multiple times, so we don't "forget" earlier attributes. + # + attrs_method_name = :"__#{association_name}_attributes" + association_attributes = (instance_variable_get(:"@#{attrs_method_name}") || []) + attributes + instance_variable_set(:"@#{attrs_method_name}", association_attributes) + + has_many association_name, ->{ where key_column => association_attributes }, + as: belongs_to, + class_name: translation_class.name, + inverse_of: belongs_to, + autosave: true + before_save do + send(association_name).select { |t| t.send(value_column).blank? }.each do |translation| + send(association_name).destroy(translation) + end + end + + module_name = "MobilityArKeyValue#{association_name.to_s.camelcase}" + unless const_defined?(module_name) + callback_methods = Module.new do + define_method :initialize_dup do |source| + super(source) + self.send("#{association_name}=", source.send(association_name).map(&:dup)) + # Set inverse on associations + send(association_name).each do |translation| + translation.send(:"#{belongs_to}=", self) + end + end + end + include const_set(module_name, callback_methods) + end + + # Ensure we only call after destroy hook once per translations class + translation_classes = [translation_class, *translation_class.descendants].uniq + after_destroy do + @mobility_after_destroy_translation_classes = [] unless defined?(@mobility_after_destroy_translation_classes) + (translation_classes - @mobility_after_destroy_translation_classes).each { |klass| klass.where(belongs_to => self).destroy_all } + @mobility_after_destroy_translation_classes += translation_classes + end end # Returns translation for a given locale, or builds one if none is present. From c627349427e573a02fb0f5776ca1f146bfbc0dcd Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 5 Feb 2021 21:16:44 +0900 Subject: [PATCH 79/82] Add failing test for Marshal.dump --- spec/mobility/plugins/backend_spec.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spec/mobility/plugins/backend_spec.rb b/spec/mobility/plugins/backend_spec.rb index c5a4fa154..53e6909cb 100644 --- a/spec/mobility/plugins/backend_spec.rb +++ b/spec/mobility/plugins/backend_spec.rb @@ -193,7 +193,7 @@ end # regression test for https://github.com/shioyama/mobility/issues/494 - it "does not prevent Marshal from working" do + it "does not prevent Marshal.dump from working" do mod = translations_class.new("title", backend: :null) stub_const('Article', model_class) model_class.include mod @@ -201,10 +201,7 @@ article = model_class.new article.mobility_backends[:title] expect { - expect(serialized = Marshal.dump(article)).not_to be_nil - expect(deserialized = Marshal.load(serialized)).not_to be_nil - - expect(deserialized.mobility_backends[:title]).to be_a(Mobility::Backends::Null) + expect(Marshal.dump(article)).not_to be_nil }.not_to raise_error end end From b98b464ca34f367ccb94eff3a0e6e2536148ebcd Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 19 Mar 2021 10:55:59 +0900 Subject: [PATCH 80/82] Give backend subclasses a unique name so they can be marshalled --- lib/mobility/backend.rb | 64 ++++++++++----------------- spec/mobility/plugins/backend_spec.rb | 7 ++- 2 files changed, 29 insertions(+), 42 deletions(-) diff --git a/lib/mobility/backend.rb b/lib/mobility/backend.rb index 1f813b04a..3f8f66ccd 100644 --- a/lib/mobility/backend.rb +++ b/lib/mobility/backend.rb @@ -1,4 +1,6 @@ # frozen-string-literal: true +require 'digest/md5' + module Mobility =begin @@ -130,6 +132,10 @@ def options # Extend included class with +setup+ method and other class methods def self.included(base) base.extend ClassMethods + base.singleton_class.class_eval do + attr_accessor :options, :model_class + protected :options=, :model_class= + end end # Defines setup hooks for backend to customize model class. @@ -170,7 +176,16 @@ def inherited(subclass) # @param [Hash] options # @return [Class] backend subclass def build_subclass(model_class, options) - ConfiguredBackend.build(self, model_class, options) + Class.new(self).tap do |klass| + klass.model_class = model_class + klass.configure(options) if klass.respond_to?(:configure) + klass.options = options.freeze + + subclass_name = SubclassNameGenerator.call(model_class, self, options) + if subclass_name && !Object.const_defined?(subclass_name) + Object.const_set(subclass_name, klass) + end + end end # Create instance and class methods to access value on options hash @@ -224,47 +239,16 @@ def write(value, options = {}) end end - class ConfiguredError < StandardError; end - class UnconfiguredError < StandardError; end -=begin - -Module included in configured backend classes, which in addition to methods on -the parent backend class also have a +model_class+ and set of +options+. - -=end - module ConfiguredBackend - def self.build(backend_class, model_class, options) - Class.new(backend_class) do - extend ConfiguredBackend - - @model_class = model_class - configure(options) if respond_to?(:configure) - @options = options.freeze + module SubclassNameGenerator + def self.call(model_class, backend_class, options) + # If the model class and backend have names, we give the backend subclass a name + if model_class.name && backend_class.name + class_name_str = model_class.name.gsub('::', '_') + backend_name_str = backend_class.name.gsub('::', '_') + options_str = ::Digest::MD5.hexdigest(options.sort.inspect)[0..5] + [class_name_str, backend_name_str, options_str].join('__') end end - - def self.extended(klass) - klass.singleton_class.attr_reader :options, :model_class - end - - # Call setup block on a class with attributes and options. - # @param model_class Class to be setup-ed - # @param [Array] attribute_names - # @param [Hash] options - def setup_model(model_class, attribute_names) - return unless setup_block = @setup_block - exec_setup_block(model_class, attribute_names, options, self, &setup_block) - end - - def inherited(_) - raise ConfiguredError, "Configured backends cannot be subclassed." - end - - # Show subclassed backend class name, if it has one. - # @return [String] - def inspect - (name = superclass.name) ? "#<#{name}>" : super - end end end end diff --git a/spec/mobility/plugins/backend_spec.rb b/spec/mobility/plugins/backend_spec.rb index 53e6909cb..c5a4fa154 100644 --- a/spec/mobility/plugins/backend_spec.rb +++ b/spec/mobility/plugins/backend_spec.rb @@ -193,7 +193,7 @@ end # regression test for https://github.com/shioyama/mobility/issues/494 - it "does not prevent Marshal.dump from working" do + it "does not prevent Marshal from working" do mod = translations_class.new("title", backend: :null) stub_const('Article', model_class) model_class.include mod @@ -201,7 +201,10 @@ article = model_class.new article.mobility_backends[:title] expect { - expect(Marshal.dump(article)).not_to be_nil + expect(serialized = Marshal.dump(article)).not_to be_nil + expect(deserialized = Marshal.load(serialized)).not_to be_nil + + expect(deserialized.mobility_backends[:title]).to be_a(Mobility::Backends::Null) }.not_to raise_error end end From ac1105263a7cbc81da18c08f6730ec617607ecc6 Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 9 Apr 2021 10:26:47 +0900 Subject: [PATCH 81/82] Add OriginalColumn backend (AR only) --- lib/mobility/plugins/active_record.rb | 4 +- .../plugins/active_record/original_column.rb | 60 +++++++++++++++++++ lib/mobility/plugins/original_column.rb | 15 +++++ 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 lib/mobility/plugins/active_record/original_column.rb create mode 100644 lib/mobility/plugins/original_column.rb diff --git a/lib/mobility/plugins/active_record.rb b/lib/mobility/plugins/active_record.rb index d357a3012..865e5db8d 100644 --- a/lib/mobility/plugins/active_record.rb +++ b/lib/mobility/plugins/active_record.rb @@ -4,7 +4,7 @@ require_relative "./active_record/cache" require_relative "./active_record/query" require_relative "./active_record/uniqueness_validation" -require_relative "./active_record/column_fallback" +require_relative "./active_record/original_column" module Mobility =begin @@ -25,7 +25,7 @@ module ActiveRecord requires :active_record_cache requires :active_record_query requires :active_record_uniqueness_validation - requires :active_record_column_fallback + requires :active_record_original_column included_hook do |klass| diff --git a/lib/mobility/plugins/active_record/original_column.rb b/lib/mobility/plugins/active_record/original_column.rb new file mode 100644 index 000000000..0075f104c --- /dev/null +++ b/lib/mobility/plugins/active_record/original_column.rb @@ -0,0 +1,60 @@ +# frozen-string-literal: true + +module Mobility + module Plugins +=begin + +Plugin to use an original column for a given locale, and otherwise use the backend. + +=end + module ActiveRecord + module OriginalColumn + extend Plugin + + requires :original_column, include: false + + included_hook do |_, backend_class| + if options[:original_column] + backend_class.include BackendInstanceMethods + backend_class.extend BackendClassMethods + end + end + + module BackendInstanceMethods + def read(locale, **) + if locale == I18n.default_locale + model.read_attribute(attribute) + else + super + end + end + + def write(locale, value, **) + if locale == I18n.default_locale + model.write_attribute(attribute, value) + else + super + end + end + end + + module BackendClassMethods + def build_node(attr, locale) + if locale == I18n.default_locale + # is the MobilityExpressions plugin necessary here? + model_class.arel_table[attr].extend(Plugins::Arel::MobilityExpressions) + else + super + end + end + + # def apply_scope + # super + # end + end + end + end + + register_plugin(:active_record_original_column, ActiveRecord::OriginalColumn) + end +end diff --git a/lib/mobility/plugins/original_column.rb b/lib/mobility/plugins/original_column.rb new file mode 100644 index 000000000..e66c45cf6 --- /dev/null +++ b/lib/mobility/plugins/original_column.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Mobility + module Plugins + module OriginalColumn + extend Plugin + + default false + + requires :backend, include: :before + end + + register_plugin(:original_column, OriginalColumn) + end +end From d594a146af912be276adb10fc657549e166619cd Mon Sep 17 00:00:00 2001 From: Chris Salzberg Date: Fri, 9 Apr 2021 10:45:33 +0900 Subject: [PATCH 82/82] Remove unnecessary arel/relation stuff --- lib/mobility/plugins/active_record/original_column.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/mobility/plugins/active_record/original_column.rb b/lib/mobility/plugins/active_record/original_column.rb index 0075f104c..7ddb96f2e 100644 --- a/lib/mobility/plugins/active_record/original_column.rb +++ b/lib/mobility/plugins/active_record/original_column.rb @@ -41,16 +41,11 @@ def write(locale, value, **) module BackendClassMethods def build_node(attr, locale) if locale == I18n.default_locale - # is the MobilityExpressions plugin necessary here? - model_class.arel_table[attr].extend(Plugins::Arel::MobilityExpressions) + model_class.arel_table[attr] else super end end - - # def apply_scope - # super - # end end end end