countrizable 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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