countrizable 0.1.1

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 (37) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +15 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +28 -0
  5. data/Rakefile +27 -0
  6. data/lib/countrizable/active_record/act_macro.rb +116 -0
  7. data/lib/countrizable/active_record/adapter.rb +108 -0
  8. data/lib/countrizable/active_record/adapter_dirty.rb +56 -0
  9. data/lib/countrizable/active_record/attributes.rb +26 -0
  10. data/lib/countrizable/active_record/class_methods.rb +129 -0
  11. data/lib/countrizable/active_record/country_attributes_query.rb +181 -0
  12. data/lib/countrizable/active_record/country_value.rb +45 -0
  13. data/lib/countrizable/active_record/exceptions.rb +13 -0
  14. data/lib/countrizable/active_record/instance_methods.rb +246 -0
  15. data/lib/countrizable/active_record/migration.rb +215 -0
  16. data/lib/countrizable/active_record.rb +14 -0
  17. data/lib/countrizable/i18n/country_code.rb +9 -0
  18. data/lib/countrizable/i18n.rb +5 -0
  19. data/lib/countrizable/interpolation.rb +28 -0
  20. data/lib/countrizable/railtie.rb +4 -0
  21. data/lib/countrizable/version.rb +3 -0
  22. data/lib/countrizable.rb +95 -0
  23. data/lib/i18n/country_code.rb +7 -0
  24. data/lib/patches/active_record/persistence.rb +17 -0
  25. data/lib/patches/active_record/query_method.rb +3 -0
  26. data/lib/patches/active_record/rails4/query_method.rb +35 -0
  27. data/lib/patches/active_record/rails4/serialization.rb +22 -0
  28. data/lib/patches/active_record/rails4/uniqueness_validator.rb +42 -0
  29. data/lib/patches/active_record/rails5/uniqueness_validator.rb +47 -0
  30. data/lib/patches/active_record/rails5_1/serialization.rb +22 -0
  31. data/lib/patches/active_record/rails5_1/uniqueness_validator.rb +45 -0
  32. data/lib/patches/active_record/relation.rb +12 -0
  33. data/lib/patches/active_record/serialization.rb +5 -0
  34. data/lib/patches/active_record/uniqueness_validator.rb +7 -0
  35. data/lib/patches/active_record/xml_attribute_serializer.rb +23 -0
  36. data/lib/tasks/countrizable_tasks.rake +4 -0
  37. metadata +259 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '039886e40e62d429443e1ade142f7ed8a6b5b3e83a12b7a7fca4fa109b5b3aff'
4
+ data.tar.gz: 51e4c3986a33eef57b917eceaa7fd6fb882ffcaabd66ff796fa2e33e72fd48bc
5
+ SHA512:
6
+ metadata.gz: d76198119c24f64c9c77b27bd0c45d77c224001f2c443b8ffacc1e8913406fbb09ae72b4c115c28ba46e98fa04f320389b3f8b1b735fd8c14d90ed083fb7eba1
7
+ data.tar.gz: 0ffc9b2df18f3d71f878f2ddde6eaea2f4c0d65a33aeb67dd5965ca4a704b8bdba8866be1bf997f63eb864a035bd05d74b5d54c53893b841fb1424f3583af110
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3
+
4
+ # Declare your gem's dependencies in countrizable.gemspec.
5
+ # Bundler will treat runtime dependencies like base dependencies, and
6
+ # development dependencies will be added by default to the :development group.
7
+ gemspec
8
+
9
+ # Declare any dependencies that are still in development here instead of in
10
+ # your gemspec. These might include edge Rails or gems from your path or
11
+ # Git. Remember to move these dependencies to your gemspec before releasing
12
+ # your gem to rubygems.org.
13
+
14
+ # To use a debugger
15
+ # gem 'byebug', group: [:development, :test]
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018 zeopix
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Countrizable
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'countrizable'
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install countrizable
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Countrizable'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'bundler/gem_tasks'
18
+
19
+ require 'rake/testtask'
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'test'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = false
25
+ end
26
+
27
+ task default: :test
@@ -0,0 +1,116 @@
1
+ module Countrizable
2
+ module ActiveRecord
3
+ module ActMacro
4
+ def country_attribute(*attr_names)
5
+ options = attr_names.extract_options!
6
+ # Bypass setup_countries! if the initial bootstrapping is done already.
7
+ setup_countries!(options) unless country_attribute?
8
+ check_columns!(attr_names)
9
+
10
+ # Add any extra country attributes.
11
+ attr_names = attr_names.map(&:to_sym)
12
+ attr_names -= country_attribute_names if defined?(country_attribute_names)
13
+
14
+ allow_country_of_attributes(attr_names) if attr_names.present?
15
+ end
16
+
17
+ def class_name
18
+ @class_name ||= begin
19
+ class_name = table_name[table_name_prefix.length..-(table_name_suffix.length + 1)].downcase.camelize
20
+ pluralize_table_names ? class_name.singularize : class_name
21
+ end
22
+ end
23
+
24
+ def country_attribute?
25
+ included_modules.include?(InstanceMethods)
26
+ end
27
+
28
+ protected
29
+
30
+ def allow_country_of_attributes(attr_names)
31
+ attr_names.each do |attr_name|
32
+ # Detect and apply serialization.
33
+ enable_serializable_attribute(attr_name)
34
+
35
+ # Create accessors for the attribute.
36
+ define_country_attr_accessor(attr_name)
37
+ define_country_values_accessor(attr_name)
38
+
39
+ # Add attribute to the list.
40
+ self.country_attribute_names << attr_name
41
+ end
42
+
43
+ begin
44
+ if ::ActiveRecord::VERSION::STRING > "5.0" && table_exists? && country_value_class.table_exists?
45
+ self.ignored_columns += country_attribute_names.map(&:to_s)
46
+ reset_column_information
47
+ end
48
+ rescue ::ActiveRecord::NoDatabaseError
49
+ warn 'Unable to connect to a database. Countrizable skipped ignoring columns of country attributes.'
50
+ end
51
+ end
52
+
53
+ def check_columns!(attr_names)
54
+ # If tables do not exist or Rails version is greater than 5, do not warn about conflicting columns
55
+ return unless ::ActiveRecord::VERSION::STRING < "5.0" && table_exists? && country_value_class.table_exists?
56
+ if (overlap = attr_names.map(&:to_s) & column_names).present?
57
+ ActiveSupport::Deprecation.warn(
58
+ ["You have defined one or more country attributes with names that conflict with column(s) on the model table. ",
59
+ "Countrizable does not support this configuration anymore, remove or rename column(s) on the model table.\n",
60
+ "Model name (table name): #{model_name} (#{table_name})\n",
61
+ "Attribute name(s): #{overlap.join(', ')}\n"].join
62
+ )
63
+ end
64
+ rescue ::ActiveRecord::NoDatabaseError
65
+ warn 'Unable to connect to a database. Countrizable skipped checking attributes with conflicting column names.'
66
+ end
67
+
68
+ def apply_countrizable_options(options)
69
+ options[:table_name] ||= "#{table_name.singularize}_country_values"
70
+ options[:foreign_key] ||= class_name.foreign_key
71
+
72
+ class_attribute :country_attribute_names, :country_value_options, :fallbacks_for_empty_country_values
73
+ self.country_attribute_names = []
74
+ self.country_value_options = options
75
+ self.fallbacks_for_empty_country_values = options[:fallbacks_for_empty_country_values]
76
+ end
77
+
78
+ def enable_serializable_attribute(attr_name)
79
+ serializer = self.countrizable_serialized_attributes[attr_name]
80
+ if serializer.present?
81
+ if defined?(::ActiveRecord::Coders::YAMLColumn) &&
82
+ serializer.is_a?(::ActiveRecord::Coders::YAMLColumn)
83
+ serializer = serializer.object_class
84
+ end
85
+
86
+ country_value_class.send :serialize, attr_name, serializer
87
+ end
88
+ end
89
+
90
+ def setup_countries!(options)
91
+ apply_countrizable_options(options)
92
+
93
+ include InstanceMethods
94
+ extend ClassMethods, Migration
95
+
96
+ country_value_class.table_name = options[:table_name]
97
+
98
+ has_many :country_values, :class_name => country_value_class.name,
99
+ :foreign_key => options[:foreign_key],
100
+ :dependent => :destroy,
101
+ :extend => HasManyExtensions,
102
+ :autosave => false,
103
+ :inverse_of => :countrizable_model
104
+
105
+ after_create :save_country_values!
106
+ after_update :save_country_values!
107
+ end
108
+ end
109
+
110
+ module HasManyExtensions
111
+ def find_or_initialize_by_country_code(country_code)
112
+ with_country_code(country_code.to_s).first || build(:country_code => country_code.to_s)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,108 @@
1
+ module Countrizable
2
+ module ActiveRecord
3
+ class Adapter
4
+ # The cache caches attributes that already were looked up for read access.
5
+ # The stash keeps track of new or changed values that need to be saved.
6
+ attr_accessor :record, :stash
7
+ private :record=, :stash=
8
+
9
+ delegate :country_value_class, :to => :'record.class'
10
+
11
+ def initialize(record)
12
+ @record = record
13
+ @stash = Attributes.new
14
+ end
15
+
16
+ def fetch_stash(country_code, name)
17
+ stash.read(country_code, name)
18
+ end
19
+
20
+ delegate :contains?, :to => :stash, :prefix => :stash
21
+ delegate :write, :to => :stash
22
+
23
+ def fetch(country_code, name)
24
+ record.countrizable_fallbacks(country_code).each do |fallback|
25
+ value = stash.contains?(fallback, name) ? fetch_stash(fallback, name) : fetch_attribute(fallback, name)
26
+
27
+ unless fallbacks_for?(value)
28
+ set_metadata(value, :country_code => fallback, :requested_country_code => country_code)
29
+ return value
30
+ end
31
+ end
32
+
33
+ return nil
34
+ end
35
+
36
+ def save_country_values!
37
+ stash.each do |country_code, attrs|
38
+ next if attrs.empty?
39
+
40
+ country_value = record.country_values_by_country_code[country_code] ||
41
+ record.country_values.build(country_code: country_code.to_s)
42
+ attrs.each do |name, value|
43
+ value = value.val if value.is_a?(Arel::Nodes::Casted)
44
+ country_value[name] = value
45
+ end
46
+
47
+ ensure_foreign_key_for(country_value)
48
+ country_value.save!
49
+ end
50
+
51
+ reset
52
+ end
53
+
54
+ def reset
55
+ stash.clear
56
+ end
57
+
58
+ protected
59
+
60
+ # Sometimes the country_values is initialised before a foreign key can be set.
61
+ def ensure_foreign_key_for(country_value)
62
+ # AR >= 4.1 reflections renamed to _reflections
63
+ country_value[country_value.class.reflections.stringify_keys["countrizable_model"].foreign_key] = record.id
64
+ end
65
+
66
+ def type_cast(name, value)
67
+ return value.presence unless column = column_for_attribute(name)
68
+
69
+ column.type_cast value
70
+ end
71
+
72
+ def column_for_attribute(name)
73
+ country_value_class.columns_hash[name.to_s]
74
+ end
75
+
76
+ def unserializable_attribute?(name, column)
77
+ column.text? && country_value_class.serialized_attributes[name.to_s]
78
+ end
79
+
80
+ def fetch_attribute(country_code, name)
81
+ country_value = record.country_value_for(country_code, false)
82
+ if country_value
83
+ country_value.send(name)
84
+ else
85
+ record.class.country_value_class.new.send(name)
86
+ end
87
+ end
88
+
89
+ def set_metadata(object, metadata)
90
+ object.country_value_metadata.merge!(metadata) if object.respond_to?(:country_value_metadata)
91
+ object
92
+ end
93
+
94
+ def country_value_metadata_accessor(object)
95
+ return if obj.respond_to?(:country_value_metadata)
96
+ class << object; attr_accessor :country_value_metadata end
97
+ object.country_value_metadata ||= {}
98
+ end
99
+
100
+ def fallbacks_for?(object)
101
+ object.nil? || (fallbacks_for_empty_country_values? && object.blank?)
102
+ end
103
+
104
+ delegate :fallbacks_for_empty_country_values?, :to => :record, :prefix => false
105
+ prepend AdapterDirty
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,56 @@
1
+ module Countrizable
2
+ module ActiveRecord
3
+ module AdapterDirty
4
+ def write country_code, name, value
5
+ # Dirty tracking, paraphrased from
6
+ # ActiveRecord::AttributeMethods::Dirty#write_attribute.
7
+ name = name.to_s
8
+ store_old_value name, country_code
9
+ old_values = dirty[name]
10
+ old_value = old_values[country_code]
11
+ is_changed = record.send :attribute_changed?, name
12
+ if is_changed && value == old_value
13
+ # If there's already a change, delete it if this undoes the change.
14
+ old_values.delete country_code
15
+ if old_values.empty?
16
+ _reset_attribute name
17
+ end
18
+ elsif !is_changed
19
+ # If there's not a change yet, record it.
20
+ record.send(:attribute_will_change!, name) if old_value != value
21
+ end
22
+
23
+ super country_code, name, value
24
+ end
25
+
26
+ attr_writer :dirty
27
+ def dirty
28
+ @dirty ||= {}
29
+ end
30
+
31
+ def store_old_value name, country_code
32
+ dirty[name] ||= {}
33
+ unless dirty[name].key? country_code
34
+ old = fetch(country_code, name)
35
+ old = old.dup if old.duplicable?
36
+ dirty[name][country_code] = old
37
+ end
38
+ end
39
+
40
+ def clear_dirty
41
+ self.dirty = {}
42
+ end
43
+
44
+ def _reset_attribute name
45
+ record.send("#{name}=", record.changed_attributes[name])
46
+ record.send(:clear_attribute_changes, [name])
47
+ end
48
+
49
+ def reset
50
+ clear_dirty
51
+ super
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,26 @@
1
+ # Helper class for storing values per country_codes. Used by Countrizable::Adapter
2
+ # to stash and cache attribute values.
3
+
4
+ module Countrizable
5
+ module ActiveRecord
6
+ class Attributes < Hash # TODO: Think about using HashWithIndifferentAccess ?
7
+ def [](country_code)
8
+ country_code = country_code.to_sym
9
+ self[country_code] = {} unless has_key?(country_code)
10
+ self.fetch(country_code)
11
+ end
12
+
13
+ def contains?(country_code, name)
14
+ self[country_code].has_key?(name.to_s)
15
+ end
16
+
17
+ def read(country_code, name)
18
+ self[country_code][name.to_s]
19
+ end
20
+
21
+ def write(country_code, name, value)
22
+ self[country_code][name.to_s] = value
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,129 @@
1
+ module Countrizable
2
+ module ActiveRecord
3
+ module ClassMethods
4
+ delegate :values_country_codes, :set_country_values_table_name, :to => :country_value_class
5
+
6
+ if ::ActiveRecord::VERSION::STRING < "5.0.0"
7
+ def columns_hash
8
+ super.except(*country_attribute_names.map(&:to_s))
9
+ end
10
+ end
11
+
12
+ def with_country_codes(*country_code)
13
+ all.merge country_value_class.with_country_codes(*country_codes)
14
+ end
15
+
16
+ def with_country_values(*country_codes)
17
+ country_codes = values_country_codes if country_codes.empty?
18
+ preload(:country_values).joins(:country_values).readonly(false).with_country_codes(country_codes).tap do |query|
19
+ query.distinct! unless country_codes.flatten.one?
20
+ end
21
+ end
22
+
23
+ def with_required_attributes
24
+ warn 'with_required_attributes is deprecated and will be removed in the next release of Countrizable.'
25
+ required_country_attributes.inject(all) do |scope, name|
26
+ scope.where("#{country_column_name(name)} IS NOT NULL")
27
+ end
28
+ end
29
+
30
+ def with_country_attribute(name, value, country = Countrizable.fallbacks)
31
+ with_country_values.where(
32
+ country_column_name(name) => value,
33
+ country_column_name(:country_code) => Array(country_codes).map(&:to_s)
34
+ )
35
+ end
36
+
37
+ def country_attributed?(name)
38
+ country_attribute_names.include?(name.to_sym)
39
+ end
40
+
41
+ def required_attributes
42
+ warn 'required_attributes is deprecated and will be removed in the next release of Countrizable.'
43
+ validators.map { |v| v.attributes if v.is_a?(ActiveModel::Validations::PresenceValidator) }.flatten
44
+ end
45
+
46
+ def required_country_attributes
47
+ warn 'required_country_attributes is deprecated and will be removed in the next release of Countrizable.'
48
+ country_attribute_names & required_attributes
49
+ end
50
+
51
+ def country_value_class
52
+ @country_value_class ||= begin
53
+ if self.const_defined?(:CountryValue, false)
54
+ klass = self.const_get(:CountryValue, false)
55
+ else
56
+ klass = self.const_set(:CountryValue, Class.new(Countrizable::ActiveRecord::CountryValue))
57
+ end
58
+
59
+ klass.belongs_to :countrizable_model,
60
+ class_name: self.name,
61
+ foreign_key: country_value_options[:foreign_key],
62
+ inverse_of: :country_values,
63
+ touch: country_value_options.fetch(:touch, false)
64
+ klass
65
+ end
66
+ end
67
+
68
+ def country_values_table_name
69
+ country_value_class.table_name
70
+ end
71
+
72
+ def country_column_name(name)
73
+ "#{country_value_class.table_name}.#{name}"
74
+ end
75
+
76
+ private
77
+
78
+ # Override the default relation method in order to return a subclass
79
+ # of ActiveRecord::Relation with custom finder and calculation methods
80
+ # for country attributes.
81
+ def relation
82
+ super.extending!(CountryAttributesQuery)
83
+ end
84
+
85
+ protected
86
+
87
+ def define_country_attr_reader(name)
88
+ define_method(name) do |*args|
89
+ Countrizable::Interpolation.interpolate(name, self, args)
90
+ end
91
+ alias_method :"#{name}_before_type_cast", name
92
+ end
93
+
94
+ def define_country_attr_writer(name)
95
+ define_method(:"#{name}=") do |value|
96
+ write_attribute(name, value)
97
+ end
98
+ end
99
+
100
+ def define_country_attr_accessor(name)
101
+ attribute(name, ::ActiveRecord::Type::Value.new)
102
+ define_country_attr_reader(name)
103
+ define_country_attr_writer(name)
104
+ end
105
+
106
+ def define_country_values_reader(name)
107
+ define_method(:"#{name}_country_values") do
108
+ hash = country_attribute_by_country_code(name)
109
+ countrizable.stash.keys.each_with_object(hash) do |country_code, result|
110
+ result[country_code] = countrizable.fetch_stash(country_code, name) if countrizable.stash_contains?(country_code, name)
111
+ end
112
+ end
113
+ end
114
+
115
+ def define_country_values_writer(name)
116
+ define_method(:"#{name}_country_values=") do |value|
117
+ value.each do |(country_code, _value)|
118
+ write_attribute name, _value, :country_code => country_code
119
+ end
120
+ end
121
+ end
122
+
123
+ def define_country_values_accessor(name)
124
+ define_country_values_reader(name)
125
+ define_country_values_writer(name)
126
+ end
127
+ end
128
+ end
129
+ end