mobility 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +19 -0
  3. data/Gemfile.lock +153 -0
  4. data/Guardfile +70 -0
  5. data/README.md +603 -13
  6. data/Rakefile +42 -0
  7. data/lib/generators/mobility/install_generator.rb +45 -0
  8. data/lib/generators/mobility/templates/create_string_translations.rb +15 -0
  9. data/lib/generators/mobility/templates/create_text_translations.rb +15 -0
  10. data/lib/mobility.rb +203 -2
  11. data/lib/mobility/active_model.rb +6 -0
  12. data/lib/mobility/active_model/attribute_methods.rb +27 -0
  13. data/lib/mobility/active_model/backend_resetter.rb +26 -0
  14. data/lib/mobility/active_record.rb +39 -0
  15. data/lib/mobility/active_record/backend_resetter.rb +26 -0
  16. data/lib/mobility/active_record/model_translation.rb +14 -0
  17. data/lib/mobility/active_record/string_translation.rb +7 -0
  18. data/lib/mobility/active_record/text_translation.rb +7 -0
  19. data/lib/mobility/active_record/translation.rb +14 -0
  20. data/lib/mobility/attributes.rb +210 -0
  21. data/lib/mobility/backend.rb +152 -0
  22. data/lib/mobility/backend/active_model.rb +7 -0
  23. data/lib/mobility/backend/active_model/dirty.rb +84 -0
  24. data/lib/mobility/backend/active_record.rb +13 -0
  25. data/lib/mobility/backend/active_record/column.rb +52 -0
  26. data/lib/mobility/backend/active_record/column/query_methods.rb +40 -0
  27. data/lib/mobility/backend/active_record/hash_valued.rb +58 -0
  28. data/lib/mobility/backend/active_record/hstore.rb +36 -0
  29. data/lib/mobility/backend/active_record/hstore/query_methods.rb +53 -0
  30. data/lib/mobility/backend/active_record/jsonb.rb +43 -0
  31. data/lib/mobility/backend/active_record/jsonb/query_methods.rb +53 -0
  32. data/lib/mobility/backend/active_record/key_value.rb +126 -0
  33. data/lib/mobility/backend/active_record/key_value/query_methods.rb +63 -0
  34. data/lib/mobility/backend/active_record/query_methods.rb +36 -0
  35. data/lib/mobility/backend/active_record/serialized.rb +93 -0
  36. data/lib/mobility/backend/active_record/serialized/query_methods.rb +32 -0
  37. data/lib/mobility/backend/active_record/table.rb +197 -0
  38. data/lib/mobility/backend/active_record/table/query_methods.rb +91 -0
  39. data/lib/mobility/backend/cache.rb +110 -0
  40. data/lib/mobility/backend/column.rb +52 -0
  41. data/lib/mobility/backend/dirty.rb +28 -0
  42. data/lib/mobility/backend/fallbacks.rb +89 -0
  43. data/lib/mobility/backend/hstore.rb +21 -0
  44. data/lib/mobility/backend/jsonb.rb +21 -0
  45. data/lib/mobility/backend/key_value.rb +71 -0
  46. data/lib/mobility/backend/null.rb +24 -0
  47. data/lib/mobility/backend/orm_delegator.rb +33 -0
  48. data/lib/mobility/backend/sequel.rb +14 -0
  49. data/lib/mobility/backend/sequel/column.rb +40 -0
  50. data/lib/mobility/backend/sequel/column/query_methods.rb +24 -0
  51. data/lib/mobility/backend/sequel/dirty.rb +54 -0
  52. data/lib/mobility/backend/sequel/hash_valued.rb +51 -0
  53. data/lib/mobility/backend/sequel/hstore.rb +36 -0
  54. data/lib/mobility/backend/sequel/hstore/query_methods.rb +42 -0
  55. data/lib/mobility/backend/sequel/jsonb.rb +43 -0
  56. data/lib/mobility/backend/sequel/jsonb/query_methods.rb +42 -0
  57. data/lib/mobility/backend/sequel/key_value.rb +139 -0
  58. data/lib/mobility/backend/sequel/key_value/query_methods.rb +48 -0
  59. data/lib/mobility/backend/sequel/query_methods.rb +22 -0
  60. data/lib/mobility/backend/sequel/serialized.rb +133 -0
  61. data/lib/mobility/backend/sequel/serialized/query_methods.rb +20 -0
  62. data/lib/mobility/backend/sequel/table.rb +149 -0
  63. data/lib/mobility/backend/sequel/table/query_methods.rb +48 -0
  64. data/lib/mobility/backend/serialized.rb +53 -0
  65. data/lib/mobility/backend/table.rb +93 -0
  66. data/lib/mobility/backend_resetter.rb +44 -0
  67. data/lib/mobility/configuration.rb +31 -0
  68. data/lib/mobility/core_ext/nil.rb +10 -0
  69. data/lib/mobility/core_ext/object.rb +19 -0
  70. data/lib/mobility/core_ext/string.rb +16 -0
  71. data/lib/mobility/instance_methods.rb +34 -0
  72. data/lib/mobility/orm.rb +4 -0
  73. data/lib/mobility/sequel.rb +26 -0
  74. data/lib/mobility/sequel/backend_resetter.rb +26 -0
  75. data/lib/mobility/sequel/column_changes.rb +29 -0
  76. data/lib/mobility/sequel/model_translation.rb +20 -0
  77. data/lib/mobility/sequel/string_translation.rb +7 -0
  78. data/lib/mobility/sequel/text_translation.rb +7 -0
  79. data/lib/mobility/sequel/translation.rb +53 -0
  80. data/lib/mobility/translates.rb +75 -0
  81. data/lib/mobility/wrapper.rb +31 -0
  82. metadata +152 -12
  83. data/.gitignore +0 -9
  84. data/.rspec +0 -2
  85. data/.travis.yml +0 -5
  86. data/bin/console +0 -14
  87. data/bin/setup +0 -8
  88. data/mobility.gemspec +0 -32
@@ -0,0 +1,110 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Caches values fetched from the backend so subsequent fetches can be performed
6
+ more quickly.
7
+
8
+ By default, the cache stores cached values in a simple hash, returned from the
9
+ {new_cache} method added to the including backend instance class. To use a
10
+ different cache class, simply define a +new_cache+ method in the backend and
11
+ return a new instance of the cache class (many backends do this, see
12
+ {Mobility::Backend::KeyValue} for one example.)
13
+
14
+ The cache is reset by the {clear_cache} method, which by default simply assigns
15
+ the result of {new_cache} to the +@cache+ instance variable. This behaviour can
16
+ also be customized by defining a +new_cache+ method on the backend class (see
17
+ {Mobility::Backend::ActiveRecord::Table#new_cache} for an example of a backend that does this).
18
+
19
+ The cache is reset when one of a set of events happens (saving, reloading,
20
+ etc.). See {BackendResetter} for details.
21
+
22
+ Values are added to the cache in two ways:
23
+
24
+ 1. first read from backend
25
+ 2. any write to backend
26
+
27
+ The latter can be customized by defining the {write_to_cache?} method, which by
28
+ default returns +false+. If set to +true+, then writes will only update the
29
+ cache and not hit the backend. This is a sensible setting in case the cache is
30
+ actually an object which directly stores the translation (see one of the
31
+ ORM-specific implementations of {Mobility::Backend::KeyValue} for examples of
32
+ this).
33
+
34
+ =end
35
+ module Cache
36
+ # @group Backend Accessors
37
+ # @!macro backend_reader
38
+ def read(locale, **options)
39
+ if write_to_cache? || cache.has_key?(locale)
40
+ cache[locale]
41
+ else
42
+ cache[locale] = super
43
+ end
44
+ end
45
+
46
+ # @!macro backend_writer
47
+ def write(locale, value, **options)
48
+ cache[locale] = write_to_cache? ? value : super
49
+ end
50
+ # @!endgroup
51
+
52
+ # Adds hook to {Backend::Setup#setup_model} to include instance of
53
+ # model-specific {BackendResetter} subclass when setting up
54
+ # model class, to trigger cache resetting at specific events (saving,
55
+ # reloading, etc.)
56
+ module Setup
57
+ # @param model_class Model class
58
+ # @param [Array<String>] attributes Backend attributes
59
+ # @param [Hash] options Backend options
60
+ def setup_model(model_class, attributes, **options)
61
+ super
62
+ model_class.include BackendResetter.for(model_class).new(attributes) { clear_cache }
63
+ end
64
+ end
65
+
66
+ # @!group Cache Methods
67
+ # @!parse
68
+ # def new_cache
69
+ # {}
70
+ # end
71
+ #
72
+ # @!parse
73
+ # def write_to_cache?
74
+ # false
75
+ # end
76
+ #
77
+ # @!parse
78
+ # def clear_cache
79
+ # @cache = new_cache
80
+ # end
81
+ # @!endgroup
82
+
83
+ # Includes cache methods to backend (unless they are already defined) and
84
+ # extends backend class with {Mobility::Cache::Setup} for backend resetting.
85
+ def self.included(backend_class)
86
+ backend_class.class_eval do
87
+ extend Setup
88
+
89
+ def new_cache
90
+ {}
91
+ end unless method_defined?(:new_cache)
92
+
93
+ def write_to_cache?
94
+ false
95
+ end unless method_defined?(:write_to_cache?)
96
+
97
+ def clear_cache
98
+ @cache = new_cache
99
+ end unless method_defined?(:clear_cache)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def cache
106
+ @cache ||= new_cache
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,52 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Stores translated attribute as a column on the model table.
6
+
7
+ To use this backend, ensure that the model table has columns named
8
+ +<attribute>_<locale>+ for every locale in +I18n.available_locales+.
9
+
10
+ ==Backend Options
11
+
12
+ There are no options for this backend. Also, the +locale_accessors+ option will
13
+ be ignored if set, since it would cause a conflict with column accessors.
14
+
15
+ @see Mobility::Backend::ActiveRecord::Column
16
+ @see Mobility::Backend::Sequel::Column
17
+
18
+ =end
19
+ module Column
20
+ include OrmDelegator
21
+
22
+ # @!group Backend Accessors
23
+ #
24
+ # @!macro backend_reader
25
+ def read(locale, **options)
26
+ model.send(column(locale))
27
+ end
28
+
29
+ # @!macro backend_writer
30
+ def write(locale, value, **options)
31
+ model.send("#{column(locale)}=", value)
32
+ end
33
+ # @!endgroup
34
+
35
+ # Returns name of column where translated attribute is stored
36
+ # @param [Symbol] locale
37
+ # @return [String]
38
+ def column(locale = Mobility.locale)
39
+ Column.column_name_for(attribute, locale)
40
+ end
41
+
42
+ # Returns name of column where translated attribute is stored
43
+ # @param [String] attribute
44
+ # @param [Symbol] locale
45
+ # @return [String]
46
+ def self.column_name_for(attribute, locale = Mobility.locale)
47
+ normalized_locale = locale.to_s.downcase.sub("-", "_")
48
+ "#{attribute}_#{normalized_locale}".to_sym
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,28 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Dirty tracking for Mobility attributes. See class-specific implementations for
6
+ details.
7
+
8
+ @see Mobility::Backend::ActiveModel::Dirty
9
+ @see Mobility::Backend::Sequel::Dirty
10
+
11
+ =end
12
+ module Dirty
13
+ # @param model_class Class of model this backend is defined on.
14
+ # @return [Backend]
15
+ # @raise [ArgumentError] if model class does not support dirty tracking
16
+ def self.for(model_class)
17
+ model_class ||= Object
18
+ if Loaded::ActiveRecord && model_class.ancestors.include?(::ActiveModel::Dirty)
19
+ Backend::ActiveModel::Dirty
20
+ elsif Loaded::Sequel && model_class < ::Sequel::Model
21
+ Backend::Sequel::Dirty
22
+ else
23
+ raise ArgumentError, "#{model_class.to_s} does not support Dirty module."
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,89 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Falls back to one or more alternative locales in case no value is defined for a
6
+ given locale.
7
+
8
+ For +fallbacks: true+, Mobility will use the value of
9
+ {Mobility::Configuration#default_fallbacks} for the fallbacks instance. This
10
+ defaults to an instance of +I18n::Locale::Fallbacks+, but can be configured
11
+ (see {Mobility::Configuration}).
12
+
13
+ If a hash is passed to the +fallbacks+ option, a new fallbacks instance will be
14
+ created for the model with the hash defining additional fallbacks.
15
+
16
+ In addition, fallbacks can be disabled when reading by passing `fallbacks:
17
+ false` to the reader method. This can be useful to determine the actual value
18
+ of the translated attribute, including a possible +nil+ value.
19
+
20
+ @see https://github.com/svenfuchs/i18n/wiki/Fallbacks I18n Fallbacks
21
+
22
+ @example With default fallbacks enabled (falls through to default locale)
23
+ class Post
24
+ translates :title, fallbacks: true
25
+ end
26
+
27
+ I18n.default_locale = :en
28
+ Mobility.locale = :en
29
+ post = Post.new(title: "foo")
30
+
31
+ Mobility.locale = :ja
32
+ post.title
33
+ #=> "foo"
34
+
35
+ post.title = "bar"
36
+ post.title
37
+ #=> "bar"
38
+
39
+ @example With additional fallbacks enabled
40
+ class Post
41
+ translates :title, fallbacks: { :'en-US' => 'de-DE', :pt => 'de-DE' }
42
+ end
43
+
44
+ Mobility.locale = :'de-DE'
45
+ post = Post.new(title: "foo")
46
+
47
+ Mobility.locale = :'en-US'
48
+ post.title
49
+ #=> "foo"
50
+
51
+ post.title = "bar"
52
+ post.title
53
+ #=> "bar"
54
+
55
+ @example Disabling fallbacks when reading value
56
+ class Post
57
+ translates :title, fallbacks: true
58
+ end
59
+
60
+ I18n.default_locale = :en
61
+ Mobility.locale = :en
62
+ post = Post.new(title: "foo")
63
+
64
+ Mobility.locale = :ja
65
+ post.title
66
+ #=> "foo"
67
+ post.title(fallbacks: false)
68
+ #=> nil
69
+ =end
70
+ module Fallbacks
71
+ # @!group Backend Accessors
72
+ # @!macro backend_reader
73
+ # @option options [Boolean] fallbacks +false+ to disable fallbacks on lookup
74
+ def read(locale, **options)
75
+ return super if options[:fallbacks] == false
76
+ fallbacks[locale].detect do |locale|
77
+ value = super(locale)
78
+ break value if value.present?
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def fallbacks
85
+ @fallbacks ||= Mobility.default_fallbacks
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,21 @@
1
+ module Mobility
2
+ module Backend
3
+
4
+ =begin
5
+
6
+ Stores translations as hash on Postgres hstore column.
7
+
8
+ ==Backend Options
9
+
10
+ This backend has no options.
11
+
12
+ @see Mobility::Backend::ActiveRecord::Hstore
13
+ @see Mobility::Backend::Sequel::Hstore
14
+ @see https://www.postgresql.org/docs/current/static/hstore.html PostgreSQL Documentation for hstore
15
+
16
+ =end
17
+ module Hstore
18
+ include OrmDelegator
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Mobility
2
+ module Backend
3
+
4
+ =begin
5
+
6
+ Stores translations as hash on Postgres jsonb column.
7
+
8
+ ==Backend Options
9
+
10
+ This backend has no options.
11
+
12
+ @see Mobility::Backend::ActiveRecord::Jsonb
13
+ @see Mobility::Backend::Sequel::Jsonb
14
+ @see https://www.postgresql.org/docs/current/static/datatype-json.html PostgreSQL Documentation for JSON Types
15
+
16
+ =end
17
+ module Jsonb
18
+ include OrmDelegator
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,71 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Stores attribute translation as attribute/value pair on a shared translations
6
+ table, using a polymorphic relationship between a translation class and models
7
+ using the backend. By default, two tables are assumed to be present supporting
8
+ string and text translations: a +mobility_text_translations+ table for text-valued translations and a
9
+ +mobility_string_translations+ table for string-valued translations (the only
10
+ difference being the column type of the +value+ column on the table).
11
+
12
+ ==Backend Options
13
+
14
+ ===+association_name+
15
+
16
+ Name of association on model. Defaults to +mobility_text_translations+ (if
17
+ +type+ is +:text+) or +mobility_string_translations+ (if +type+ is +:string+).
18
+ If specified, ensure name does not overlap with other methods on model or with
19
+ the association name used by other backends on model (otherwise one will
20
+ overwrite the other).
21
+
22
+ ===+type+
23
+
24
+ Currently, either +:text+ or +:string+ is supported. Determines which class to
25
+ use for translations, which in turn determines which table to use to store
26
+ translations (by default +mobility_text_translations+ for text type,
27
+ +mobility_string_translations+ for string type).
28
+
29
+ ===+class_name+
30
+
31
+ Class to use for translations when defining association. By default,
32
+ {Mobility::ActiveRecord::TextTranslation} or
33
+ {Mobility::ActiveRecord::StringTranslation} for ActiveRecord models (similar
34
+ for Sequel models). If string is passed in, it will be constantized to get the
35
+ class.
36
+
37
+ @see Mobility::Backend::ActiveRecord::KeyValue
38
+ @see Mobility::Backend::Sequel::KeyValue
39
+
40
+ =end
41
+ module KeyValue
42
+ include OrmDelegator
43
+
44
+ # Simple cache to memoize translations as a hash so they can be fetched
45
+ # quickly.
46
+ class TranslationsCache
47
+ # @param backend Instance of KeyValue backend to cache
48
+ # @return [TranslationsCache]
49
+ def initialize(backend)
50
+ @cache = Hash.new { |hash, locale| hash[locale] = backend.translation_for(locale) }
51
+ end
52
+
53
+ # @param locale [Symbol] Locale to fetch
54
+ def [](locale)
55
+ @cache[locale].value
56
+ end
57
+
58
+ # @param locale [Symbol] Locale to set
59
+ # @param value [String] Value to set
60
+ def []=(locale, value)
61
+ @cache[locale].value = value
62
+ end
63
+
64
+ # @yield [locale, translation]
65
+ def each_translation &block
66
+ @cache.each_value &block
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,24 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Backend which does absolutely nothing. Mostly for testing purposes.
6
+
7
+ =end
8
+ class Null
9
+ include Backend
10
+
11
+ # @!group Backend Accessors
12
+ # @return [NilClass]
13
+ def read(*); end
14
+
15
+ # @return [NilClass]
16
+ def write(*); end
17
+ # @!endgroup
18
+
19
+ # @!group Backend Configuration
20
+ def self.configure!(*); end
21
+ # @!endgroup
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ module Mobility
2
+ module Backend
3
+ =begin
4
+
5
+ Adds {#for} method to backend to return ORM-specific backend.
6
+
7
+ @example KeyValue backend for AR model
8
+ class Post < ActiveRecord::Base
9
+ # ...
10
+ end
11
+ Mobility::Backend::KeyValue.for(Post)
12
+ #=> Mobility::Backend::ActiveRecord::KeyValue
13
+
14
+ =end
15
+ module OrmDelegator
16
+ # @param [Class] model_class Class of model
17
+ # @return [Class] Class of backend to use for model
18
+ def for(model_class)
19
+ if Loaded::ActiveRecord && model_class < ::ActiveRecord::Base
20
+ const_get(name.split("::").insert(-2, "ActiveRecord").join("::"))
21
+ elsif Loaded::Sequel && model_class < ::Sequel::Model
22
+ const_get(name.split("::").insert(-2, "Sequel").join("::"))
23
+ else
24
+ raise ArgumentError, "#{name.split('::').last} backend can only be used by ActiveRecord or Sequel models"
25
+ end
26
+ end
27
+
28
+ def self.included(base)
29
+ base.extend(self)
30
+ end
31
+ end
32
+ end
33
+ end