address_concern 2.0.1 → 2.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.
@@ -1,3 +1,4 @@
1
+ # Comparable to the ActsAsAddressable concern of effective_addresses.
1
2
  module AddressConcern::AddressAssociations
2
3
  extend ActiveSupport::Concern
3
4
  module ClassMethods
@@ -10,6 +11,7 @@ module AddressConcern::AddressAssociations
10
11
  # You can also pass options to the inverse has_one assocation in the Address model, via the
11
12
  # +inverse+ option:
12
13
  # belongs_to_address :home_address, inverse: {foreign_key: :physical_address_id}
14
+ #
13
15
  def belongs_to_address(name = :address, inverse: nil, **options)
14
16
  options.reverse_merge!({
15
17
  class_name: 'Address'
@@ -36,27 +38,46 @@ module AddressConcern::AddressAssociations
36
38
  end
37
39
  end
38
40
 
39
- # Creates a has_one +address+ association, representing the one and only address associated with the current record
40
- def has_address
41
- has_one :address, as: :addressable
42
- create_addressable_association_on_address
41
+ # Creates a has_one +address+ association. If you don't give a name, it will just be called
42
+ # "address", which can be used if this record only needs to be associated with a single address.
43
+ #
44
+ # If you need it to be associated with multiple address records, pass the name/type of each. For
45
+ # example:
46
+ #
47
+ # has_address :billing
48
+ #
49
+ def has_address(type = nil)
50
+ has_one address_name_for_type(type), -> { where({address_type: type}) }, class_name: 'Address', as: :addressable
51
+ create_addressable_association_on_address_if_needed
52
+ end
53
+
54
+ def address_name_for_type(type)
55
+ if type
56
+ :"#{type}_address"
57
+ else
58
+ :address
59
+ end
43
60
  end
44
61
 
45
62
  # Creates a has_many +addresses+ association, representing all addresses associated with the current record
46
- def has_addresses(options = {})
63
+ #
64
+ # If +types+ is given, adds a has_address(type) association for each type.
65
+ #
66
+ # Comparable to acts_as_addressable from effective_addresses.
67
+ #
68
+ def has_addresses(types = [])
47
69
  has_many :addresses, as: :addressable
48
- (options[:types] || ()).each do |type|
49
- has_one :"#{type}_address", -> { where({address_type: type}) }, class_name: 'Address', as: :addressable
70
+ (types || []).each do |type|
71
+ has_address(type)
50
72
  end
51
- create_addressable_association_on_address
73
+ create_addressable_association_on_address_if_needed
52
74
  end
53
75
 
54
- def create_addressable_association_on_address
76
+ def create_addressable_association_on_address_if_needed(**options)
55
77
  Address.class_eval do
56
- unless reflect_on_association(:addressable)
57
- belongs_to :addressable, :polymorphic => true
58
- end
78
+ return if reflect_on_association(:addressable)
59
79
  end
80
+ Address.belongs_to_addressable(**options)
60
81
  end
61
82
  end
62
83
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AddressConcern
4
+ module AttributesSlice
5
+ extend ActiveSupport::Concern
6
+
7
+ # Returns a hash containing the attributes with the keys passed in, similar to
8
+ # attributes.slice(*attr_names).
9
+ #
10
+ # This lets you use a list of attr_names as symbols to get a subset of attributes.
11
+ # Because writing attributes.symbolize_keys.slice is too long.
12
+ #
13
+ # Unlike with attributes.slice, these "attributes" can be any instance method of the receiver;
14
+ # they don't have to be present in the `attributes` hash itself. (The `attributes` hash only
15
+ # includes columns in the associated table, not "virtual attributes" that are available only as
16
+ # Ruby methods and not present as columns in the table. attributes.slice also doesn't let you
17
+ # access the attributes via any attribute aliases you've added.)
18
+ #
19
+ # If you _don't_ want to include virtual attributes, pass include_virtual: false.
20
+ #
21
+ # Examples:
22
+ # attributes_slice
23
+ # => {}
24
+ #
25
+ # attributes_slice(:name, :age, :confirmed?)
26
+ # => {:name => 'First Last', :age => 42, :confirmed? => true}
27
+ #
28
+ # attributes_slice(:name, is_confirmed: :confirmed?)
29
+ # => {:name => 'First Last', :is_confirmed => true}
30
+ #
31
+ # attributes_slice(:name, is_confirmed: -> { _1.confirmed? })
32
+ # => {:name => 'First Last', :is_confirmed => true}
33
+ #
34
+ def attributes_slice(*attr_names, include_virtual: true, **hash)
35
+ hash.transform_values { |_proc|
36
+ _proc.to_proc.call(self)
37
+ }.reverse_merge(
38
+ if include_virtual
39
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, hash|
40
+ hash[attr_name] = send(attr_name)
41
+ end
42
+ else
43
+ # When we only want "real" attributes
44
+ attributes.symbolize_keys.slice(*keys.map(&:to_sym)).with_indifferent_access
45
+ end
46
+ )
47
+ end
48
+ alias_method :read_attributes, :attributes_slice
49
+
50
+ def attributes_except(*keys)
51
+ attributes.symbolize_keys.except(*keys.map(&:to_sym))
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AddressConcern
4
+ module InspectBase
5
+ def inspect_base(*_items, class: true, id: true)
6
+ items = _items.map { |item|
7
+ if item.is_a?(Hash)
8
+ item.map { |k, v|
9
+ "#{k}: #{v}"
10
+ }.join(', ')
11
+ elsif item.respond_to?(:to_proc) && item.to_proc.arity <= 0
12
+ item.to_proc.(self)
13
+ else
14
+ item.to_s
15
+ end
16
+ }
17
+
18
+ _class = binding.local_variable_get(:class)
19
+ _id = binding.local_variable_get(:id)
20
+
21
+ '<' +
22
+ [
23
+ (_class == true ? self.class : _class),
24
+ ("#{self.id || 'new'}:" if _id),
25
+ ].join(' ') + ' ' +
26
+ [
27
+ *items,
28
+ ].filter_map(&:presence).map(&:to_s).join(', ') +
29
+ '>'
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ #AddressConcern.setup do |config|
2
+ # ...
3
+ #end
@@ -1,9 +1,13 @@
1
- require 'attribute_normalizer'
2
- require 'facets/string/cleanlines'
1
+ if false
2
+ require 'attribute_normalizer'
3
+ #require 'facets/string/cleanlines'
3
4
 
4
- AttributeNormalizer.configure do |config|
5
- config.normalizers[:cleanlines] = lambda do |input, options|
6
- input.to_s.cleanlines.to_a.join("\n")
5
+ require_relative '../core_extensions/string/cleanlines'
6
+ using String::Cleanlines
7
+
8
+ AttributeNormalizer.configure do |config|
9
+ config.normalizers[:cleanlines] = ->(input, options) {
10
+ input.to_s.cleanlines.to_a.join("\n")
11
+ }
7
12
  end
8
13
  end
9
-
@@ -0,0 +1,24 @@
1
+ # This file is based on effective_addresses/lib/effective_addresses/engine.rb
2
+
3
+ module AddressConcern
4
+ class Engine < ::Rails::Engine
5
+ engine_name 'address_concern'
6
+
7
+ config.autoload_paths += Dir["#{config.root}/app/models/concerns"]
8
+ config.eager_load_paths += Dir["#{config.root}/app/models/concerns"]
9
+
10
+ initializer 'address_concern.active_record' do |app|
11
+ ActiveSupport.on_load :active_record do
12
+ # These are currently required from lib/address_concern.rb
13
+ AddressConcern::Address::Base
14
+ AddressConcern::AddressAssociations
15
+ # ActiveRecord::Base.extend(AddressConcern::Address::Base)
16
+ end
17
+ end
18
+
19
+ # Set up our default configuration options.
20
+ #initializer 'address_concern.defaults', before: :load_config_initializers do |app|
21
+ # eval File.read("#{config.root}/config/address_concern.rb")
22
+ #end
23
+ end
24
+ end
@@ -1,5 +1,5 @@
1
1
  module AddressConcern
2
2
  def self.version
3
- "2.0.1"
3
+ "2.1.0"
4
4
  end
5
5
  end
@@ -1,11 +1,12 @@
1
1
  require 'rails'
2
2
  require 'carmen'
3
3
  require 'active_record'
4
- require 'active_record_ignored_attributes'
5
4
 
6
5
  Carmen.i18n_backend.append_locale_path File.join(File.dirname(__FILE__), '../config/locale/overlay/en')
7
6
 
8
7
  require 'address_concern/version'
9
8
  require 'address_concern/attribute_normalizer'
10
- require_relative 'address_concern/address'
11
- require_relative 'address_concern/address_associations'
9
+ require 'address_concern/engine'
10
+
11
+ require_relative '../app/models/concerns/address'
12
+ require_relative '../app/models/concerns/address_associations'
@@ -0,0 +1,11 @@
1
+ module Hash::Reorder
2
+ refine Hash do
3
+ def reorder(*order)
4
+ slice(*order).merge except(*order)
5
+ end
6
+
7
+ def reorder!(*order)
8
+ replace(reorder(*order))
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ module String::Cleanlines
2
+ refine String do
3
+ # Copied from acets/string/cleanlines.rb
4
+
5
+ # Returns an Enumerator for iterating over each
6
+ # line of the string, stripped of whitespace on
7
+ # either side.
8
+ #
9
+ # "this\nthat\nother\n".cleanlines.to_a #=> ['this', 'that', 'other']
10
+ #
11
+ def cleanlines(&block)
12
+ if block
13
+ scan(/^.*?$/) do |line|
14
+ block.call(line.strip)
15
+ end
16
+ else
17
+ str = self
18
+ Enumerator.new do |output|
19
+ str.scan(/^.*?$/) do |line|
20
+ output.yield(line.strip)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+
@@ -4,16 +4,20 @@ class CreateAddresses < ActiveRecord::Migration[4.2]
4
4
  t.references :addressable, :polymorphic => true
5
5
  t.string :address_type # to allow shipping/billing/etc. address
6
6
 
7
- t.string :name
8
7
  t.text :address
9
8
  t.string :city
9
+ t.string :state_code
10
10
  t.string :state
11
11
  t.string :postal_code
12
+ t.string :country_code
12
13
  t.string :country
13
- t.string :country_alpha2
14
- t.string :country_alpha3
15
- t.string :email
16
- t.string :phone
14
+
15
+ # You could add other columns, such as these, but they are arguably not technically part of an
16
+ # address. In any case, they are outside the scope of this library.
17
+ #t.string :name
18
+ #t.string :email
19
+ #t.string :phone
20
+
17
21
  t.timestamps
18
22
  end
19
23
 
@@ -21,13 +25,13 @@ class CreateAddresses < ActiveRecord::Migration[4.2]
21
25
  t.index :addressable_id
22
26
  t.index :addressable_type
23
27
  t.index :address_type
24
- t.index :name
28
+
29
+ #t.index :city
30
+ t.index :state_code
25
31
  t.index :state
32
+ #t.index :postal_code
33
+ t.index :country_code
26
34
  t.index :country
27
- t.index :country_alpha2
28
- t.index :country_alpha3
29
- t.index :email
30
- t.index :phone
31
35
  end
32
36
  end
33
37
 
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'acts_as_address' do
4
+ def klass
5
+ described_class
6
+ end
7
+
8
+ # These models' table only have a single column for state and country.
9
+ # This tests both the default (zero-config) behavior and how the same column can be used for
10
+ # non-default (name or code).
11
+ describe AddressWithNameOnly do
12
+ it do
13
+ expect(klass.state_name_attribute).to eq :state
14
+ expect(klass.state_code_attribute).to eq nil
15
+ expect(klass.country_name_attribute).to eq :country
16
+ expect(klass.country_code_attribute).to eq nil
17
+ end
18
+ end
19
+
20
+ describe AddressWithCodeOnly do
21
+ it do
22
+ expect(klass.state_name_attribute).to eq nil
23
+ expect(klass.state_code_attribute).to eq :state
24
+ expect(klass.country_name_attribute).to eq nil
25
+ expect(klass.country_code_attribute).to eq :country
26
+ end
27
+ end
28
+
29
+ # You can't use the same column for both name and code. If config tries to do that, which one
30
+ # takes precedence?
31
+ describe 'name_attribute == code_attribute' do
32
+ let(:klass) do
33
+ Class.new(ApplicationRecord) do
34
+ self.table_name = 'addresses'
35
+ acts_as_address(
36
+ country: {
37
+ name_attribute: 'country',
38
+ code_attribute: 'country',
39
+ }
40
+ )
41
+ end
42
+ end
43
+ let(:address) { klass.new }
44
+ it 'name takes precedence' do
45
+ expect(klass.country_name_attribute).to eq :country
46
+ expect(klass.country_code_attribute).to eq nil
47
+ end
48
+ end
49
+
50
+ describe 'address lines' do
51
+ describe Address do
52
+ it do
53
+ expect(klass.multi_line_address?).to eq true
54
+ expect(klass.address_attributes).to eq [:address]
55
+ end
56
+ end
57
+
58
+ describe AddressWithSeparateAddressColumns do
59
+ it do
60
+ expect(klass.multi_line_address?).to eq false
61
+ expect(klass.address_attributes).to eq [:address_1, :address_2, :address_3]
62
+ end
63
+ end
64
+ end
65
+ end
66
+