From ddceba3f57877e267d26a2d7a02517d62fc1ae54 Mon Sep 17 00:00:00 2001 From: Jonathan Allard Date: Sat, 17 Jan 2015 12:11:51 -0500 Subject: [PATCH 1/4] Implement dynamic accessors --- lib/trasto/instance_methods.rb | 70 ++++++++++++++++++++++++++++- spec/spec_helper.rb | 1 + spec/trasto_spec.rb | 80 ++++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 1 deletion(-) diff --git a/lib/trasto/instance_methods.rb b/lib/trasto/instance_methods.rb index 76f52d8..5bf869c 100644 --- a/lib/trasto/instance_methods.rb +++ b/lib/trasto/instance_methods.rb @@ -1,7 +1,53 @@ module Trasto module InstanceMethods + def method_missing(method, *args, &block) + # Return super only if above can respond without us. + # If we call super and above cannot respond without us, + # infinite recursion occurs. + return super if respond_to?(method) && !respond_to_missing?(method) + + if match = match_translate_method(method) + # We define accessors after getting the value in the hopes of + # capturing a new locale that would be useful. + return access_translated_value(*match, args).tap do + define_translation_accessors! + end + end + + super + end + + def respond_to_missing?(method, include_privates = false) + !!match_translate_method(method) + end + + def define_translation_accessors! + self.class.translatable_columns.each do |column| + return unless column_value = send("#{column}_i18n") + + column_value.keys.each do |locale| + define_singleton_method "#{column}_#{locale}" do + read_translated_value(column, locale) + end + + define_singleton_method "#{column}_#{locale}=" do |value| + write_translated_value(column, locale, value) + end + end + end + end + private + def access_translated_value(column, locale, write_mode, args = []) + if write_mode + write_translated_value(column, locale, args[0]) + else + read_translated_value(column, locale) + end + end + + # Finds a suitable translation for column def read_localized_value(column) return nil unless (column_value = send("#{column}_i18n")) @@ -12,9 +58,20 @@ def read_localized_value(column) nil end + # Reads column value in specified locale + def read_translated_value(column, locale) + return unless column_value = send("#{column}_i18n") + column_value[locale] + end + def write_localized_value(column, value) + write_translated_value(column, I18n.locale, value) + end + + def write_translated_value(column, locale, value) translations = send("#{column}_i18n") || {} - send("#{column}_i18n=", translations.merge(I18n.locale => value).with_indifferent_access) + translations.merge!(locale => value).with_indifferent_access + send("#{column}_i18n=", translations) end def locales_for_reading_column(column) @@ -26,5 +83,16 @@ def locales_for_reading_column(column) end end end + + # Returns an array with column, name and maybe equal if match, else + # returns nil. + # + # [column, locale, "=" or nil] + def match_translate_method(method) + ary = method.to_s.scan(/^(.*)_([a-z]{2})(=)?$/).flatten + return nil if ary.empty? + + ary if self.class.translates?(ary[0]) + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 59dc422..a5385ef 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -28,6 +28,7 @@ create_table :posts, force: true do |t| t.hstore :title_i18n t.hstore :body_i18n + t.string :constant end end end diff --git a/spec/trasto_spec.rb b/spec/trasto_spec.rb index 6b02127..032efdf 100644 --- a/spec/trasto_spec.rb +++ b/spec/trasto_spec.rb @@ -23,6 +23,13 @@ expect(Post.new).to respond_to :title expect(Post.new).not_to respond_to :body end + + it "should leave other columns intact" do + Post.translates :title + post = Post.new(constant: "tau") + + expect(post.constant).to eq "tau" + end end describe ActiveRecord::Base, '.translates?' do @@ -89,6 +96,10 @@ post.title_i18n = nil expect(post.title).to be_nil end + + it "should generate helper methods" do + expect(post.title_en).to eq "Hello" + end end describe Post, '#title=' do @@ -105,3 +116,72 @@ expect(post.title_i18n['de']).to eq('Hallo') end end + +describe Post, "dynamic accessors" do + before do + Post.translates :title + I18n.locale = :en + end + + let(:post) do + Post.new(title_i18n: { + en: "Free breakfast", + fr: "Déjeuner gratuit", + }) + end + + it "should respond_to title_en" do + expect(post).to respond_to(:title_en) + end + + describe "#title_en" do + it "should return the localized value" do + expect(post.title_en).to eq "Free breakfast" + expect(post.title_fr).to eq "Déjeuner gratuit" + expect(post.title_es).to be nil + end + + it "should define readers when used" do + post.title_en + expect(post.methods.include?(:title_en)).to be true + expect(post.methods.include?(:title_fr)).to be true + expect(post.title_en).to eq "Free breakfast" + expect(post.title_fr).to eq "Déjeuner gratuit" + end + + it "should not define accessors until used" do + expect(post.methods.include?(:title_en)).to be false + expect(post.methods.include?(:title_fr)).to be false + end + + it "should work with an empty field" do + post = Post.new + expect(post.title_en).to be nil + end + end + + describe "#title_es=" do + it "should write the localized value" do + post.title_es = "Desayuno gratis" + expect(post.title_i18n["es"]).to eq "Desayuno gratis" + end + + it "should define writers when used" do + post.title_es = "Desayuno gratis" + expect(post.methods.include?(:title_en=)).to be true + expect(post.methods.include?(:title_es=)).to be true + expect(post.title_es).to eq "Desayuno gratis" + end + + it "should not define accessors until used" do + expect(post.methods.include?(:title_en=)).to be false + expect(post.methods.include?(:title_es=)).to be false + end + + it "should work in mass attribute assignment" do + newpost = Post.new(title_en: "Great job", title_fr: "Beau travail") + expect(newpost.title_en).to eq "Great job" + expect(newpost.title_fr).to eq "Beau travail" + end + end +end From 521a3801cfa00bc5c02d533efffb9f299b9d6d20 Mon Sep 17 00:00:00 2001 From: Jonathan Allard Date: Sat, 17 Jan 2015 14:37:09 -0500 Subject: [PATCH 2/4] Translation indifferent access --- lib/trasto/instance_methods.rb | 3 +++ spec/trasto_spec.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/trasto/instance_methods.rb b/lib/trasto/instance_methods.rb index 5bf869c..cb79b3f 100644 --- a/lib/trasto/instance_methods.rb +++ b/lib/trasto/instance_methods.rb @@ -50,17 +50,20 @@ def access_translated_value(column, locale, write_mode, args = []) # Finds a suitable translation for column def read_localized_value(column) return nil unless (column_value = send("#{column}_i18n")) + column_value = column_value.with_indifferent_access # Rails < 4.1 locales_for_reading_column(column).each do |locale| value = column_value[locale] return value if value.present? end + nil end # Reads column value in specified locale def read_translated_value(column, locale) return unless column_value = send("#{column}_i18n") + column_value = column_value.with_indifferent_access column_value[locale] end diff --git a/spec/trasto_spec.rb b/spec/trasto_spec.rb index 032efdf..985ab63 100644 --- a/spec/trasto_spec.rb +++ b/spec/trasto_spec.rb @@ -113,7 +113,7 @@ it 'should assign in the current locale' do post.title = 'Hallo' expect(post.title).to eq('Hallo') - expect(post.title_i18n['de']).to eq('Hallo') + expect(post.title_i18n.with_indifferent_access['de']).to eq('Hallo') end end From 3268ffefa4090ea5c023135476af00b62be7e483 Mon Sep 17 00:00:00 2001 From: Jonathan Allard Date: Sat, 24 Jan 2015 16:34:05 -0500 Subject: [PATCH 3/4] Fix mutable bug in AR 4.1 See rails/rails#6127 --- lib/trasto/instance_methods.rb | 16 ++++++++++++++++ spec/trasto_spec.rb | 11 +++++++++++ 2 files changed, 27 insertions(+) diff --git a/lib/trasto/instance_methods.rb b/lib/trasto/instance_methods.rb index cb79b3f..63b69a8 100644 --- a/lib/trasto/instance_methods.rb +++ b/lib/trasto/instance_methods.rb @@ -1,5 +1,10 @@ module Trasto module InstanceMethods + def self.included(base) + prepend HstoreUpdatePatch if ActiveRecord::VERSION::STRING < "4.2.0" + super + end + def method_missing(method, *args, &block) # Return super only if above can respond without us. # If we call super and above cannot respond without us, @@ -97,5 +102,16 @@ def match_translate_method(method) ary if self.class.translates?(ary[0]) end + + # Patches an ActiveRecord bug that wouldn't update the hstore when + # changed. Affects 4.0, 4.1. + # + # See https://github.com/rails/rails/issues/6127 + module HstoreUpdatePatch + def write_translated_value(column, locale, value) + send("#{column}_i18n_will_change!") + super + end + end end end diff --git a/spec/trasto_spec.rb b/spec/trasto_spec.rb index 985ab63..42fbccc 100644 --- a/spec/trasto_spec.rb +++ b/spec/trasto_spec.rb @@ -184,4 +184,15 @@ expect(newpost.title_fr).to eq "Beau travail" end end + + describe "#title_fr=" do + it "should update existing hstore in AR 4.1" do + post.save; post.reload + + post.title_fr = "Nouveau" + post.save; post.reload + + expect(post.title_fr).to eq "Nouveau" + end + end end From 6de73541aa4513c2aae1768dccdb975feb9faef0 Mon Sep 17 00:00:00 2001 From: Jonathan Allard Date: Sun, 25 Jan 2015 14:59:45 -0500 Subject: [PATCH 4/4] Fix type map reloading bug on AR4.1 tests --- spec/spec_helper.rb | 10 +++++++++- spec/trasto_spec.rb | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a5385ef..1b9f1d2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,14 @@ Object.send(:remove_const, 'SubPost') load 'app/post.rb' end + + # If you really need a clean run (for eg. Travis) + # config.after :suite do + # ActiveRecord::Base.connection.execute <<-eosql + # DROP TABLE posts; + # DROP EXTENSION IF EXISTS hstore CASCADE; + # eosql + # end end # Test against real ActiveRecord models. @@ -23,7 +31,7 @@ silence_stream(STDOUT) do ActiveRecord::Schema.define(version: 0) do - execute 'CREATE EXTENSION IF NOT EXISTS hstore' + enable_extension "hstore" create_table :posts, force: true do |t| t.hstore :title_i18n diff --git a/spec/trasto_spec.rb b/spec/trasto_spec.rb index 42fbccc..e76aeee 100644 --- a/spec/trasto_spec.rb +++ b/spec/trasto_spec.rb @@ -100,6 +100,11 @@ it "should generate helper methods" do expect(post.title_en).to eq "Hello" end + + it "should be able to persist" do + post.save; post.reload + expect(post.title_i18n['en']).to eq "Hello" + end end describe Post, '#title=' do