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.
- checksums.yaml +4 -4
- data/Readme.md +73 -6
- data/address_concern.gemspec +2 -3
- data/app/models/concerns/address.rb +784 -0
- data/{lib/address_concern → app/models/concerns}/address_associations.rb +33 -12
- data/app/models/concerns/attributes_slice.rb +54 -0
- data/app/models/concerns/inspect_base.rb +32 -0
- data/config/address_concern.rb +3 -0
- data/lib/address_concern/attribute_normalizer.rb +10 -6
- data/lib/address_concern/engine.rb +24 -0
- data/lib/address_concern/version.rb +1 -1
- data/lib/address_concern.rb +4 -3
- data/lib/core_extensions/hash/reorder.rb +11 -0
- data/lib/core_extensions/string/cleanlines.rb +28 -0
- data/lib/generators/address_concern/templates/migration.rb +14 -10
- data/spec/models/acts_as_address_spec.rb +66 -0
- data/spec/models/address_spec.rb +345 -117
- data/spec/spec_helper.rb +1 -5
- data/spec/support/models/address.rb +2 -1
- data/spec/support/models/address_custom_attr_names.rb +11 -0
- data/spec/support/models/address_with_code_only.rb +12 -0
- data/spec/support/models/address_with_name_only.rb +5 -0
- data/spec/support/models/address_with_separate_address_columns.rb +5 -0
- data/spec/support/models/user.rb +6 -1
- data/spec/support/schema.rb +22 -0
- metadata +17 -34
- data/lib/address_concern/address.rb +0 -306
@@ -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
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
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
|
-
(
|
49
|
-
|
70
|
+
(types || []).each do |type|
|
71
|
+
has_address(type)
|
50
72
|
end
|
51
|
-
|
73
|
+
create_addressable_association_on_address_if_needed
|
52
74
|
end
|
53
75
|
|
54
|
-
def
|
76
|
+
def create_addressable_association_on_address_if_needed(**options)
|
55
77
|
Address.class_eval do
|
56
|
-
|
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
|
@@ -1,9 +1,13 @@
|
|
1
|
-
|
2
|
-
require '
|
1
|
+
if false
|
2
|
+
require 'attribute_normalizer'
|
3
|
+
#require 'facets/string/cleanlines'
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
data/lib/address_concern.rb
CHANGED
@@ -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
|
-
|
11
|
-
|
9
|
+
require 'address_concern/engine'
|
10
|
+
|
11
|
+
require_relative '../app/models/concerns/address'
|
12
|
+
require_relative '../app/models/concerns/address_associations'
|
@@ -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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
t.string :
|
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
|
-
|
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
|
+
|