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.
- 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
|
+
|