Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add dynamic helper instance methods #5

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion lib/trasto/instance_methods.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,85 @@
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,
# 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"))
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

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)
Expand All @@ -26,5 +91,27 @@ 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

# 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
11 changes: 10 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -23,11 +31,12 @@

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
t.hstore :body_i18n
t.string :constant
end
end
end
Expand Down
98 changes: 97 additions & 1 deletion spec/trasto_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -89,6 +96,15 @@
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

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
Expand All @@ -102,6 +118,86 @@
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

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

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