address_concern 2.0.1 → 2.1.0

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