pico_phone-rails 0.2.1 → 0.3.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 +130 -1
- data/lib/generators/pico_phone/rails/extracted_phone_numbers/extracted_phone_numbers_generator.rb +33 -0
- data/lib/generators/pico_phone/rails/extracted_phone_numbers/templates/create_pico_phone_rails_extracted_phone_numbers.rb.tt +24 -0
- data/lib/generators/pico_phone/rails/phone_number/phone_number_generator.rb +48 -0
- data/lib/generators/pico_phone/rails/phone_number/templates/create_table.rb.tt +17 -0
- data/lib/generators/pico_phone/rails/phone_number/templates/model.rb.tt +11 -0
- data/lib/pico_phone/rails/extracted_phone_number.rb +18 -0
- data/lib/pico_phone/rails/extraction.rb +245 -0
- data/lib/pico_phone/rails/phone_search_index.rb +189 -0
- data/lib/pico_phone/rails/railtie.rb +13 -0
- data/lib/pico_phone/rails/region_resolution.rb +21 -0
- data/lib/pico_phone/rails/version.rb +1 -1
- data/lib/pico_phone/rails.rb +8 -4
- metadata +16 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0524cbb733d2edc10a4a6f0fcb13197f2853d58f437286c7aea7bf4c30d53092
|
|
4
|
+
data.tar.gz: b29da13e7d7308a7e4e60fda8be21fbd467a5e138e6d6509ce97a719cac77afd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c82086362cc5c8dcf692ba712c7a961b076c2d87becfd5d3ff9ccece17d7a1da095558d76748b83ecdee46fc6067c884671d6f681e2b037cf14fb06ffccc9e05
|
|
7
|
+
data.tar.gz: 0cc7b9175ebdbef5c3cddbabae8fbb8b8705b5cd7a8ffbce7077b4ef46510f895401bd6699105601d8ce13730692f09c513e4503568fe3dd2e17c4850a80bd8f
|
data/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Rails integration for [`pico_phone`](https://github.com/gjack/pico_phone): an
|
|
4
4
|
ActiveRecord attribute type, an ActiveModel validator, a before-validation
|
|
5
|
-
normalizer,
|
|
5
|
+
normalizer, free-text phone number extraction, a same-table phone search
|
|
6
|
+
index, and an ActiveJob serializer for phone numbers.
|
|
6
7
|
|
|
7
8
|
## Installation
|
|
8
9
|
|
|
@@ -52,6 +53,134 @@ A `before_validation` callback that rewrites the column to E.164 when it parses
|
|
|
52
53
|
as valid, so formatting noise ("(510) 274-5656") is stripped before the
|
|
53
54
|
validator runs. Pairs naturally with the validator above.
|
|
54
55
|
|
|
56
|
+
### Extraction
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class Note < ApplicationRecord
|
|
60
|
+
extract_phone_numbers_from :body, region: "US"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
note.extracted_phone_numbers # => [#<PicoPhone::PhoneNumberMatch ...>, ...]
|
|
64
|
+
note.body_with_phones_redacted # => "Call me at [PHONE] or [PHONE]"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Scans a free-text column (notes, support tickets, chat logs) for phone
|
|
68
|
+
numbers of any format. Both methods re-scan the column live -- nothing is
|
|
69
|
+
persisted. Works without a model too:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
PicoPhone::Rails.extract_phone_numbers(text, region: "US")
|
|
73
|
+
PicoPhone::Rails.redact_phone_numbers(text, region: "US", replacement: "[PHONE]")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### Persisting matches for cross-record search
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
rails generate pico_phone:rails:extracted_phone_numbers
|
|
80
|
+
rails db:migrate
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class Note < ApplicationRecord
|
|
85
|
+
extract_phone_numbers_from :body, region: "US", persist: true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Note.containing_phone_number("(510) 274-5656") # matches regardless of stored format
|
|
89
|
+
Note.phone_number_starting_with("(510)") # matches a locally-formatted prefix
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`persist: true` keeps a `pico_phone_rails_extracted_phone_numbers` row in sync
|
|
93
|
+
with the column on every save (only when it actually changes), so
|
|
94
|
+
`containing_phone_number` can find records by E.164 instead of grepping raw
|
|
95
|
+
text -- `"(510) 274-5656"`, `"5102745656"`, and `"+15102745656"` all match the
|
|
96
|
+
same row. `phone_number_starting_with` matches the digits of the number's
|
|
97
|
+
*displayed* national format, so a prefix like `"(510)"` matches the way a
|
|
98
|
+
viewer actually sees/types it. Calling either without having run the
|
|
99
|
+
generator and passed `persist: true` raises
|
|
100
|
+
`PicoPhone::Rails::PersistenceNotEnabled` with instructions, rather than a
|
|
101
|
+
bare "no such table" error.
|
|
102
|
+
|
|
103
|
+
`region:` also accepts a Symbol (called as an instance method on the record)
|
|
104
|
+
or a Proc (called with the record), for multi-tenant/multi-region apps where
|
|
105
|
+
the correct region varies per record instead of being fixed for the whole
|
|
106
|
+
model:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
extract_phone_numbers_from :body, region: ->(note) { note.campus.region }, persist: true
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Since there's no single record to resolve a dynamic region against for a
|
|
113
|
+
class-level search, `containing_phone_number` requires an explicit `region:`
|
|
114
|
+
in that case (raising `PicoPhone::Rails::RegionRequired` otherwise):
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
Note.containing_phone_number("01 23 45 67 89", region: current_organization.region)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Phone search index
|
|
121
|
+
|
|
122
|
+
For a table that's already one-row-per-phone-number (rather than free text
|
|
123
|
+
that might mention one), `maintain_phone_search_index` keeps configurable
|
|
124
|
+
sibling search columns in sync via `before_save` -- no child table required.
|
|
125
|
+
|
|
126
|
+
Starting from scratch, with no phone-number table yet:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
rails generate pico_phone:rails:phone_number PhoneNumber
|
|
130
|
+
rails db:migrate
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This creates a `phone_numbers` table (raw `number` column plus all four
|
|
134
|
+
search-index columns, indexed) and an `app/models/phone_number.rb` with
|
|
135
|
+
`maintain_phone_search_index` already wired up -- just fill in `region:` for
|
|
136
|
+
your app. If you already have a table you want to add search columns to
|
|
137
|
+
instead, skip the generator and call `maintain_phone_search_index` directly:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
class PhoneNumber < ApplicationRecord
|
|
141
|
+
maintain_phone_search_index :number,
|
|
142
|
+
region: "US",
|
|
143
|
+
columns: { e164: :e164, national_digits: :national_digits, reversed_digits: :reverse_index }
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Each `columns:` entry is independently optional -- omit any you don't want
|
|
148
|
+
maintained. `reversed_digits` reverses the digit string so "ends with"/last-N
|
|
149
|
+
searches can use a leftmost-prefix index lookup. Unparseable input (which the
|
|
150
|
+
tracked column may deliberately allow) leaves every configured column `nil`
|
|
151
|
+
rather than failing the save.
|
|
152
|
+
|
|
153
|
+
To backfill rows that existed before you added the search index, use
|
|
154
|
+
`sync_phone_search_index!` -- the `before_save` callback only fires when the
|
|
155
|
+
tracked column *changes*, so re-saving an untouched row won't populate it:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
PhoneNumber.find_each(&:sync_phone_search_index!)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
It writes immediately via `update_columns` (no callbacks, no validations),
|
|
162
|
+
same graceful `nil`-on-unparseable behavior as the regular sync.
|
|
163
|
+
|
|
164
|
+
Search against whichever columns you configured:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
PhoneNumber.phone_number_index_matching("(510) 274-5656") # exact match via e164
|
|
168
|
+
PhoneNumber.phone_number_index_starting_with("(510)") # prefix, the way a viewer types it
|
|
169
|
+
PhoneNumber.phone_number_index_ending_with("5656") # last-N-digit suffix search
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Each raises `PicoPhone::Rails::SearchColumnNotConfigured` if the column it
|
|
173
|
+
needs wasn't included in `columns:`. `phone_number_index_matching` also
|
|
174
|
+
accepts `region:` for interpreting an ambiguous query term, required
|
|
175
|
+
explicitly (raising `PicoPhone::Rails::RegionRequired` otherwise) when the
|
|
176
|
+
model's own `region:` is a Symbol/Proc rather than a fixed String -- same
|
|
177
|
+
reasoning as `containing_phone_number` above.
|
|
178
|
+
|
|
179
|
+
(These are named distinctly from Extraction's `containing_phone_number`/
|
|
180
|
+
`phone_number_starting_with` on purpose -- both concerns get included onto
|
|
181
|
+
every model, so identical names would collide and one would silently shadow
|
|
182
|
+
the other.)
|
|
183
|
+
|
|
55
184
|
### ActiveJob serializer
|
|
56
185
|
|
|
57
186
|
```ruby
|
data/lib/generators/pico_phone/rails/extracted_phone_numbers/extracted_phone_numbers_generator.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
require "rails/generators/active_record"
|
|
6
|
+
|
|
7
|
+
module PicoPhone
|
|
8
|
+
module Rails
|
|
9
|
+
# Invoked as `rails generate pico_phone:rails:extracted_phone_numbers`.
|
|
10
|
+
# Creates the migration for the table `extract_phone_numbers_from ...,
|
|
11
|
+
# persist: true` needs -- this is never run automatically, since a
|
|
12
|
+
# gem shouldn't silently add tables to an app's schema.
|
|
13
|
+
class ExtractedPhoneNumbersGenerator < ::Rails::Generators::Base
|
|
14
|
+
include ::Rails::Generators::Migration
|
|
15
|
+
|
|
16
|
+
source_root File.expand_path("templates", __dir__)
|
|
17
|
+
|
|
18
|
+
# @param dirname [String]
|
|
19
|
+
# @return [String]
|
|
20
|
+
def self.next_migration_number(dirname)
|
|
21
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [void]
|
|
25
|
+
def create_migration_file
|
|
26
|
+
migration_template(
|
|
27
|
+
"create_pico_phone_rails_extracted_phone_numbers.rb.tt",
|
|
28
|
+
"db/migrate/create_pico_phone_rails_extracted_phone_numbers.rb"
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class CreatePicoPhoneRailsExtractedPhoneNumbers < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :pico_phone_rails_extracted_phone_numbers do |t|
|
|
4
|
+
t.references :extractable, polymorphic: true, null: false
|
|
5
|
+
t.string :source_attribute, null: false
|
|
6
|
+
t.string :region, null: false
|
|
7
|
+
t.string :e164, null: false
|
|
8
|
+
t.string :national_digits, null: false
|
|
9
|
+
t.string :raw_string, null: false
|
|
10
|
+
t.integer :start_offset, null: false
|
|
11
|
+
t.integer :end_offset, null: false
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Explicit short names: the table name is long enough that the default
|
|
17
|
+
# generated name for the national_digits index exceeds the 64-character
|
|
18
|
+
# limit some databases (MySQL) and Rails versions enforce.
|
|
19
|
+
add_index :pico_phone_rails_extracted_phone_numbers, :e164,
|
|
20
|
+
name: "index_pico_phone_extracted_phone_numbers_on_e164"
|
|
21
|
+
add_index :pico_phone_rails_extracted_phone_numbers, :national_digits,
|
|
22
|
+
name: "index_pico_phone_extracted_phone_numbers_on_national_digits"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
require "rails/generators/active_record"
|
|
6
|
+
|
|
7
|
+
module PicoPhone
|
|
8
|
+
module Rails
|
|
9
|
+
# Invoked as `rails generate pico_phone:rails:phone_number PhoneNumber`.
|
|
10
|
+
# For apps starting a phone-number table from scratch -- unlike
|
|
11
|
+
# `maintain_phone_search_index` itself, which has no generator on
|
|
12
|
+
# purpose, since it needs to map onto whatever columns an *existing*
|
|
13
|
+
# table already has.
|
|
14
|
+
#
|
|
15
|
+
# Generates a migration for a table with a raw `number` column plus all
|
|
16
|
+
# four search-index columns (e164, national_digits, reversed_digits,
|
|
17
|
+
# region), and a starter model with `maintain_phone_search_index`
|
|
18
|
+
# already wired up.
|
|
19
|
+
class PhoneNumberGenerator < ::Rails::Generators::NamedBase
|
|
20
|
+
include ::Rails::Generators::Migration
|
|
21
|
+
|
|
22
|
+
source_root File.expand_path("templates", __dir__)
|
|
23
|
+
|
|
24
|
+
# @param dirname [String]
|
|
25
|
+
# @return [String]
|
|
26
|
+
def self.next_migration_number(dirname)
|
|
27
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [void]
|
|
31
|
+
def create_migration_file
|
|
32
|
+
migration_template "create_table.rb.tt", "db/migrate/create_#{table_name}.rb"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [void]
|
|
36
|
+
def create_model_file
|
|
37
|
+
template "model.rb.tt", "app/models/#{file_path}.rb"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# @return [String]
|
|
43
|
+
def migration_class_name
|
|
44
|
+
"Create#{table_name.camelize}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :<%= table_name %> do |t|
|
|
4
|
+
t.string :number
|
|
5
|
+
t.string :e164
|
|
6
|
+
t.string :national_digits
|
|
7
|
+
t.string :reversed_digits
|
|
8
|
+
t.string :region
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :<%= table_name %>, :e164
|
|
14
|
+
add_index :<%= table_name %>, :national_digits
|
|
15
|
+
add_index :<%= table_name %>, :reversed_digits
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class <%= class_name %> < ApplicationRecord
|
|
2
|
+
maintain_phone_search_index :number,
|
|
3
|
+
region: "US", # TODO: set your app's region -- a String, a Symbol
|
|
4
|
+
# (instance method name), or a Proc, see the README
|
|
5
|
+
columns: {
|
|
6
|
+
e164: :e164,
|
|
7
|
+
national_digits: :national_digits,
|
|
8
|
+
reversed_digits: :reversed_digits,
|
|
9
|
+
region: :region
|
|
10
|
+
}
|
|
11
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PicoPhone
|
|
4
|
+
module Rails
|
|
5
|
+
# A single phone number match persisted from a free-text column by
|
|
6
|
+
# `extract_phone_numbers_from ..., persist: true`. Required only once
|
|
7
|
+
# ActiveRecord::Base exists -- see the railtie -- since it inherits from
|
|
8
|
+
# it directly.
|
|
9
|
+
#
|
|
10
|
+
# Requires the table created by the `pico_phone:rails:extracted_phone_numbers`
|
|
11
|
+
# generator.
|
|
12
|
+
class ExtractedPhoneNumber < ActiveRecord::Base
|
|
13
|
+
self.table_name = "pico_phone_rails_extracted_phone_numbers"
|
|
14
|
+
|
|
15
|
+
belongs_to :extractable, polymorphic: true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module PicoPhone
|
|
6
|
+
module Rails
|
|
7
|
+
# Scans arbitrary text for phone numbers. Works on any string -- no
|
|
8
|
+
# ActiveRecord model required.
|
|
9
|
+
#
|
|
10
|
+
# @param text [String, nil]
|
|
11
|
+
# @param region [String] ISO 3166-1 alpha-2 default region for numbers found without a country code
|
|
12
|
+
# @return [Array<PicoPhone::PhoneNumberMatch>]
|
|
13
|
+
def self.extract_phone_numbers(text, region:)
|
|
14
|
+
return [] if text.nil? || text.empty?
|
|
15
|
+
|
|
16
|
+
PicoPhone.find_numbers(text, region)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Replaces every phone number found in +text+ with +replacement+,
|
|
20
|
+
# preserving everything else in the string untouched.
|
|
21
|
+
#
|
|
22
|
+
# @param text [String, nil]
|
|
23
|
+
# @param region [String] ISO 3166-1 alpha-2 default region for numbers found without a country code
|
|
24
|
+
# @param replacement [String]
|
|
25
|
+
# @return [String]
|
|
26
|
+
def self.redact_phone_numbers(text, region:, replacement: "[PHONE]")
|
|
27
|
+
return text.to_s if text.nil? || text.empty?
|
|
28
|
+
|
|
29
|
+
matches = extract_phone_numbers(text, region: region)
|
|
30
|
+
return text.dup if matches.empty?
|
|
31
|
+
|
|
32
|
+
splice_redactions(text, matches, replacement)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param text [String]
|
|
36
|
+
# @param matches [Array<PicoPhone::PhoneNumberMatch>]
|
|
37
|
+
# @param replacement [String]
|
|
38
|
+
# @return [String]
|
|
39
|
+
def self.splice_redactions(text, matches, replacement)
|
|
40
|
+
segments = []
|
|
41
|
+
cursor = 0
|
|
42
|
+
matches.each do |match|
|
|
43
|
+
segments << text.byteslice(cursor, match.start - cursor)
|
|
44
|
+
segments << replacement
|
|
45
|
+
cursor = match.end_index
|
|
46
|
+
end
|
|
47
|
+
segments << text.byteslice(cursor, text.bytesize - cursor)
|
|
48
|
+
segments.join
|
|
49
|
+
end
|
|
50
|
+
private_class_method :splice_redactions
|
|
51
|
+
|
|
52
|
+
# Raised by `containing_phone_number` when the including model never
|
|
53
|
+
# opted into `persist: true` (or its migration hasn't been run), instead
|
|
54
|
+
# of an obscure ActiveRecord::StatementInvalid: no such table.
|
|
55
|
+
class PersistenceNotEnabled < Error; end
|
|
56
|
+
|
|
57
|
+
# Raised by `containing_phone_number` when the model's `region:` is
|
|
58
|
+
# resolved per record (a Symbol or Proc, for multi-tenant/multi-region
|
|
59
|
+
# apps) and the caller didn't pass an explicit `region:` -- there's no
|
|
60
|
+
# single record to resolve it against at the class level.
|
|
61
|
+
class RegionRequired < Error; end
|
|
62
|
+
|
|
63
|
+
# Included into ActiveRecord::Base by the railtie. Adds an
|
|
64
|
+
# `extract_phone_numbers_from` class macro that wraps a free-text column
|
|
65
|
+
# with helpers for pulling out and redacting the phone numbers it
|
|
66
|
+
# mentions -- useful for notes, support tickets, and chat logs where a
|
|
67
|
+
# phone number might appear anywhere in the text, in any format.
|
|
68
|
+
#
|
|
69
|
+
# @example
|
|
70
|
+
# class Note < ApplicationRecord
|
|
71
|
+
# extract_phone_numbers_from :body, region: "US"
|
|
72
|
+
# end
|
|
73
|
+
#
|
|
74
|
+
# note.extracted_phone_numbers # => [#<PicoPhone::PhoneNumberMatch ...>, ...]
|
|
75
|
+
# note.body_with_phones_redacted # => "Call me at [PHONE] or [PHONE]"
|
|
76
|
+
#
|
|
77
|
+
# @example Persisting matches for cross-record search
|
|
78
|
+
# class Note < ApplicationRecord
|
|
79
|
+
# extract_phone_numbers_from :body, region: "US", persist: true
|
|
80
|
+
# end
|
|
81
|
+
#
|
|
82
|
+
# Note.containing_phone_number("(510) 274-5656") # matches regardless of stored format
|
|
83
|
+
module Extraction
|
|
84
|
+
extend ActiveSupport::Concern
|
|
85
|
+
|
|
86
|
+
included do
|
|
87
|
+
class_attribute :pico_phone_rails_persisted, instance_accessor: false, default: false
|
|
88
|
+
class_attribute :pico_phone_rails_extraction_region, instance_accessor: false, default: nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class_methods do
|
|
92
|
+
# @!method extract_phone_numbers_from(attribute, region:, persist: false)
|
|
93
|
+
# Defines +#extracted_phone_numbers+ and +#<attribute>_with_phones_redacted+
|
|
94
|
+
# on the including model, both backed by a live re-scan of +attribute+ --
|
|
95
|
+
# nothing is persisted or cached by default.
|
|
96
|
+
#
|
|
97
|
+
# When +persist: true+, also declares a +has_many :extracted_phone_number_records+
|
|
98
|
+
# association and an +after_save+ callback that keeps it in sync with
|
|
99
|
+
# +attribute+, so {.containing_phone_number} can find records by phone
|
|
100
|
+
# number regardless of how it was formatted in the source text. Requires
|
|
101
|
+
# the table created by the `pico_phone:rails:extracted_phone_numbers`
|
|
102
|
+
# generator.
|
|
103
|
+
# @param attribute [Symbol] the text attribute to scan
|
|
104
|
+
# @param region [String, Symbol, Proc] ISO 3166-1 alpha-2 default region for numbers found without a
|
|
105
|
+
# country code. A String is used as-is; a Symbol is called as an instance method on the record; a
|
|
106
|
+
# Proc is called with the record. Either lets the region vary per record, e.g.
|
|
107
|
+
# `region: ->(note) { note.campus.region }`, for multi-tenant/multi-region apps, rather than being
|
|
108
|
+
# fixed once for the whole model.
|
|
109
|
+
# @param persist [Boolean] persist matches for cross-record search via {.containing_phone_number}
|
|
110
|
+
# @return [void]
|
|
111
|
+
def extract_phone_numbers_from(attribute, region:, persist: false)
|
|
112
|
+
define_method(:extracted_phone_numbers) do
|
|
113
|
+
resolved_region = PicoPhone::Rails.resolve_region(region, self)
|
|
114
|
+
PicoPhone::Rails.extract_phone_numbers(public_send(attribute).to_s, region: resolved_region)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
define_method("#{attribute}_with_phones_redacted") do
|
|
118
|
+
resolved_region = PicoPhone::Rails.resolve_region(region, self)
|
|
119
|
+
PicoPhone::Rails.redact_phone_numbers(public_send(attribute).to_s, region: resolved_region)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
return unless persist
|
|
123
|
+
|
|
124
|
+
persist_extracted_phone_numbers_from(attribute, region)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class_methods do
|
|
129
|
+
# @param term [String] a phone number in any format
|
|
130
|
+
# @param region [String, nil] ISO 3166-1 alpha-2 region for interpreting +term+ -- required if the
|
|
131
|
+
# model's own region is resolved per record (a Symbol or Proc), since there's no record to resolve
|
|
132
|
+
# it against here; pass the searching user's own region, e.g. `region: current_organization.region`
|
|
133
|
+
# @raise [PicoPhone::Rails::PersistenceNotEnabled] if no attribute was registered with +persist: true+
|
|
134
|
+
# @raise [PicoPhone::Rails::RegionRequired] if +region+ is omitted and the model's region is per-record
|
|
135
|
+
# @return [ActiveRecord::Relation]
|
|
136
|
+
def containing_phone_number(term, region: nil)
|
|
137
|
+
ensure_pico_phone_rails_persisted!
|
|
138
|
+
resolved_region = region || pico_phone_rails_static_extraction_region!
|
|
139
|
+
|
|
140
|
+
e164 = PicoPhone.parse(term.to_s, resolved_region).e164
|
|
141
|
+
joins(:extracted_phone_number_records)
|
|
142
|
+
.where(PicoPhone::Rails::ExtractedPhoneNumber.table_name => { e164: e164 })
|
|
143
|
+
.distinct
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Matches against the digits of the number's *displayed* national
|
|
147
|
+
# format (e.g. "5102745656", or "0123456789" for a French number
|
|
148
|
+
# whose national format shows a leading trunk "0") -- what a viewer
|
|
149
|
+
# would actually type while looking at the number, rather than the
|
|
150
|
+
# E.164 "national significant number" (which drops that trunk digit).
|
|
151
|
+
#
|
|
152
|
+
# @param prefix [String] a phone number prefix in any format, e.g. "(510)"
|
|
153
|
+
# @raise [PicoPhone::Rails::PersistenceNotEnabled] if no attribute was registered with +persist: true+
|
|
154
|
+
# @return [ActiveRecord::Relation]
|
|
155
|
+
def phone_number_starting_with(prefix)
|
|
156
|
+
ensure_pico_phone_rails_persisted!
|
|
157
|
+
|
|
158
|
+
digits = prefix.to_s.gsub(/\D/, "")
|
|
159
|
+
joins(:extracted_phone_number_records)
|
|
160
|
+
.where("#{PicoPhone::Rails::ExtractedPhoneNumber.table_name}.national_digits LIKE ?", "#{digits}%")
|
|
161
|
+
.distinct
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class_methods do
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
# @raise [PicoPhone::Rails::PersistenceNotEnabled]
|
|
169
|
+
# @return [void]
|
|
170
|
+
def ensure_pico_phone_rails_persisted!
|
|
171
|
+
return if pico_phone_rails_persisted
|
|
172
|
+
|
|
173
|
+
raise PersistenceNotEnabled,
|
|
174
|
+
"#{name} has no attribute registered with `extract_phone_numbers_from ..., persist: true` -- " \
|
|
175
|
+
"add persist: true and run the pico_phone:rails:extracted_phone_numbers generator"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @raise [PicoPhone::Rails::RegionRequired] if the configured region isn't a plain String
|
|
179
|
+
# @return [String, nil]
|
|
180
|
+
def pico_phone_rails_static_extraction_region!
|
|
181
|
+
region = pico_phone_rails_extraction_region
|
|
182
|
+
return region if region.nil? || region.is_a?(String)
|
|
183
|
+
|
|
184
|
+
raise RegionRequired,
|
|
185
|
+
"#{name}'s region is resolved per record (#{region.inspect}), so it can't be inferred for a " \
|
|
186
|
+
"class-level search -- pass region: explicitly, e.g. " \
|
|
187
|
+
"#{name}.containing_phone_number(term, region: current_organization.region)"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
class_methods do
|
|
192
|
+
# @param attribute [Symbol]
|
|
193
|
+
# @param region [String]
|
|
194
|
+
# @return [void]
|
|
195
|
+
private def persist_extracted_phone_numbers_from(attribute, region)
|
|
196
|
+
self.pico_phone_rails_persisted = true
|
|
197
|
+
self.pico_phone_rails_extraction_region = region
|
|
198
|
+
|
|
199
|
+
has_many :extracted_phone_number_records,
|
|
200
|
+
class_name: "PicoPhone::Rails::ExtractedPhoneNumber",
|
|
201
|
+
as: :extractable,
|
|
202
|
+
dependent: :delete_all
|
|
203
|
+
|
|
204
|
+
after_save(if: -> { public_send(:"saved_change_to_#{attribute}?") }) do
|
|
205
|
+
pico_phone_rails_sync_extracted_phone_numbers(attribute, region)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# @param attribute [Symbol]
|
|
211
|
+
# @param region [String, Symbol, Proc]
|
|
212
|
+
# @return [void]
|
|
213
|
+
def pico_phone_rails_sync_extracted_phone_numbers(attribute, region)
|
|
214
|
+
resolved_region = PicoPhone::Rails.resolve_region(region, self)
|
|
215
|
+
matches = PicoPhone::Rails.extract_phone_numbers(public_send(attribute).to_s, region: resolved_region)
|
|
216
|
+
|
|
217
|
+
extracted_phone_number_records.where(source_attribute: attribute.to_s).delete_all
|
|
218
|
+
return if matches.empty?
|
|
219
|
+
|
|
220
|
+
rows = matches.map do |match|
|
|
221
|
+
pico_phone_rails_extracted_phone_number_attributes(match, attribute, resolved_region)
|
|
222
|
+
end
|
|
223
|
+
extracted_phone_number_records.insert_all(rows)
|
|
224
|
+
end
|
|
225
|
+
private :pico_phone_rails_sync_extracted_phone_numbers
|
|
226
|
+
|
|
227
|
+
# @param match [PicoPhone::PhoneNumberMatch]
|
|
228
|
+
# @param attribute [Symbol]
|
|
229
|
+
# @param region [String] the region actually resolved for this record, persisted for auditability
|
|
230
|
+
# @return [Hash]
|
|
231
|
+
def pico_phone_rails_extracted_phone_number_attributes(match, attribute, region)
|
|
232
|
+
{
|
|
233
|
+
source_attribute: attribute.to_s,
|
|
234
|
+
region: region,
|
|
235
|
+
e164: match.number.e164,
|
|
236
|
+
national_digits: match.number.national.gsub(/\D/, ""),
|
|
237
|
+
raw_string: match.raw_string,
|
|
238
|
+
start_offset: match.start,
|
|
239
|
+
end_offset: match.end_index
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
private :pico_phone_rails_extracted_phone_number_attributes
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module PicoPhone
|
|
6
|
+
module Rails
|
|
7
|
+
# Raised by the {PhoneSearchIndex} class-level search methods when the
|
|
8
|
+
# column they need wasn't configured in `columns:`.
|
|
9
|
+
class SearchColumnNotConfigured < Error; end
|
|
10
|
+
|
|
11
|
+
# Included into ActiveRecord::Base by the railtie. Adds a
|
|
12
|
+
# `maintain_phone_search_index` class macro for a table that's already
|
|
13
|
+
# one-row-per-phone-number (unlike Extraction, which is for a free-text
|
|
14
|
+
# column that might mention a number anywhere in it). Keeps configurable
|
|
15
|
+
# sibling columns in sync via `before_save` -- no child table, no
|
|
16
|
+
# polymorphic association.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# class PhoneNumber < ApplicationRecord
|
|
20
|
+
# maintain_phone_search_index :number,
|
|
21
|
+
# region: "US",
|
|
22
|
+
# columns: { e164: :e164, national_digits: :national_digits, reversed_digits: :reverse_index }
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# PhoneNumber.phone_number_index_matching("(510) 274-5656")
|
|
26
|
+
# PhoneNumber.phone_number_index_starting_with("(510)")
|
|
27
|
+
# PhoneNumber.phone_number_index_ending_with("5656")
|
|
28
|
+
module PhoneSearchIndex
|
|
29
|
+
extend ActiveSupport::Concern
|
|
30
|
+
|
|
31
|
+
included do
|
|
32
|
+
class_attribute :pico_phone_rails_phone_search_columns, instance_accessor: false, default: {}
|
|
33
|
+
class_attribute :pico_phone_rails_phone_search_region, instance_accessor: false, default: nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class_methods do
|
|
37
|
+
# @!method maintain_phone_search_index(attribute, region:, columns: {})
|
|
38
|
+
# Registers a +before_save+ callback (only when +attribute+ actually
|
|
39
|
+
# changes) that parses +attribute+ and writes the configured derived
|
|
40
|
+
# columns. Each entry in +columns:+ is independently optional -- omit
|
|
41
|
+
# any key you don't want maintained, e.g. to keep an existing
|
|
42
|
+
# hand-rolled column untouched.
|
|
43
|
+
#
|
|
44
|
+
# Unparseable input (which +attribute+ may deliberately allow) leaves
|
|
45
|
+
# every configured column +nil+ rather than failing the save, since
|
|
46
|
+
# +PhoneNumber#e164+/+#national+ raise on input that can't be
|
|
47
|
+
# interpreted as a phone number attempt at all.
|
|
48
|
+
# @param attribute [Symbol] the column holding the raw phone number
|
|
49
|
+
# @param region [String, Symbol, Proc] see {Extraction.extract_phone_numbers_from} for resolution rules
|
|
50
|
+
# @param columns [Hash{Symbol => Symbol}] maps :e164, :national_digits, :reversed_digits, and/or :region
|
|
51
|
+
# (search-index concepts) to the actual column names to write them to
|
|
52
|
+
# @return [void]
|
|
53
|
+
def maintain_phone_search_index(attribute, region:, columns: {})
|
|
54
|
+
self.pico_phone_rails_phone_search_columns = columns
|
|
55
|
+
self.pico_phone_rails_phone_search_region = region
|
|
56
|
+
|
|
57
|
+
before_save(if: -> { public_send(:"#{attribute}_changed?") }) do
|
|
58
|
+
pico_phone_rails_stage_phone_search_index(attribute, region, columns)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @!method sync_phone_search_index!
|
|
62
|
+
# Recomputes and immediately persists the configured columns via
|
|
63
|
+
# +update_columns+ (no callbacks, no validations), regardless of
|
|
64
|
+
# whether +attribute+ has changed -- unlike the +before_save+
|
|
65
|
+
# above, which only fires on an actual change. Meant for
|
|
66
|
+
# backfilling existing rows, since dirty-tracking has no way to
|
|
67
|
+
# know an unmodified row was never synced in the first place:
|
|
68
|
+
# PhoneNumber.find_each(&:sync_phone_search_index!)
|
|
69
|
+
# @return [void]
|
|
70
|
+
define_method(:sync_phone_search_index!) do
|
|
71
|
+
resolved_region = PicoPhone::Rails.resolve_region(region, self)
|
|
72
|
+
phone_number = PicoPhone.parse(public_send(attribute).to_s, resolved_region)
|
|
73
|
+
values = pico_phone_rails_phone_search_index_values(phone_number, resolved_region, columns)
|
|
74
|
+
update_columns(values)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class_methods do
|
|
80
|
+
# @param term [String] a phone number in any format
|
|
81
|
+
# @param region [String, nil] region for interpreting +term+ -- required if the model's own
|
|
82
|
+
# region is resolved per record (a Symbol or Proc); pass the searching user's own region
|
|
83
|
+
# @raise [PicoPhone::Rails::SearchColumnNotConfigured] if +columns:+ didn't configure +:e164+
|
|
84
|
+
# @raise [PicoPhone::Rails::RegionRequired] if +region+ is omitted and the model's region is per-record
|
|
85
|
+
# @return [ActiveRecord::Relation]
|
|
86
|
+
def phone_number_index_matching(term, region: nil)
|
|
87
|
+
column = pico_phone_rails_phone_search_column!(:e164)
|
|
88
|
+
resolved_region = region || pico_phone_rails_static_phone_search_region!
|
|
89
|
+
|
|
90
|
+
e164 = PicoPhone.parse(term.to_s, resolved_region).e164
|
|
91
|
+
where(column => e164)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Matches against the digits of the number's *displayed* national format -- see
|
|
95
|
+
# {Extraction::ClassMethods#phone_number_starting_with} for the "why" (national vs
|
|
96
|
+
# raw_national digits), and the same caveat applies here.
|
|
97
|
+
# @param prefix [String] a phone number prefix in any format, e.g. "(510)"
|
|
98
|
+
# @raise [PicoPhone::Rails::SearchColumnNotConfigured] if +columns:+ didn't configure +:national_digits+
|
|
99
|
+
# @return [ActiveRecord::Relation]
|
|
100
|
+
def phone_number_index_starting_with(prefix)
|
|
101
|
+
column = pico_phone_rails_phone_search_column!(:national_digits)
|
|
102
|
+
digits = prefix.to_s.gsub(/\D/, "")
|
|
103
|
+
where("#{column} LIKE ?", "#{digits}%")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Matches against the reversed digits of the number's displayed national format, so a
|
|
107
|
+
# last-N-digit search can use a leftmost-prefix index lookup instead of a full scan.
|
|
108
|
+
# @param suffix [String] a phone number suffix in any format, e.g. "5656"
|
|
109
|
+
# @raise [PicoPhone::Rails::SearchColumnNotConfigured] if +columns:+ didn't configure +:reversed_digits+
|
|
110
|
+
# @return [ActiveRecord::Relation]
|
|
111
|
+
def phone_number_index_ending_with(suffix)
|
|
112
|
+
column = pico_phone_rails_phone_search_column!(:reversed_digits)
|
|
113
|
+
digits = suffix.to_s.gsub(/\D/, "")
|
|
114
|
+
where("#{column} LIKE ?", "#{digits.reverse}%")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
class_methods do
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# @param key [Symbol]
|
|
122
|
+
# @raise [PicoPhone::Rails::SearchColumnNotConfigured]
|
|
123
|
+
# @return [Symbol]
|
|
124
|
+
def pico_phone_rails_phone_search_column!(key)
|
|
125
|
+
column = pico_phone_rails_phone_search_columns[key]
|
|
126
|
+
return column if column
|
|
127
|
+
|
|
128
|
+
raise SearchColumnNotConfigured,
|
|
129
|
+
"#{name} didn't configure a :#{key} column -- add it to `columns:` in maintain_phone_search_index"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @raise [PicoPhone::Rails::RegionRequired] if the configured region isn't a plain String
|
|
133
|
+
# @return [String, nil]
|
|
134
|
+
def pico_phone_rails_static_phone_search_region!
|
|
135
|
+
region = pico_phone_rails_phone_search_region
|
|
136
|
+
return region if region.nil? || region.is_a?(String)
|
|
137
|
+
|
|
138
|
+
raise RegionRequired,
|
|
139
|
+
"#{name}'s region is resolved per record (#{region.inspect}), so it can't be inferred for a " \
|
|
140
|
+
"class-level search -- pass region: explicitly, e.g. " \
|
|
141
|
+
"#{name}.phone_number_index_matching(term, region: current_organization.region)"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @param attribute [Symbol]
|
|
146
|
+
# @param region [String, Symbol, Proc]
|
|
147
|
+
# @param columns [Hash{Symbol => Symbol}]
|
|
148
|
+
# @return [void]
|
|
149
|
+
def pico_phone_rails_stage_phone_search_index(attribute, region, columns)
|
|
150
|
+
resolved_region = PicoPhone::Rails.resolve_region(region, self)
|
|
151
|
+
phone_number = PicoPhone.parse(public_send(attribute).to_s, resolved_region)
|
|
152
|
+
|
|
153
|
+
pico_phone_rails_phone_search_index_values(phone_number, resolved_region, columns).each do |column, value|
|
|
154
|
+
public_send("#{column}=", value)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
private :pico_phone_rails_stage_phone_search_index
|
|
158
|
+
|
|
159
|
+
# @param phone_number [PicoPhone::PhoneNumber]
|
|
160
|
+
# @param resolved_region [String]
|
|
161
|
+
# @param columns [Hash{Symbol => Symbol}]
|
|
162
|
+
# @return [Hash{Symbol => String, nil}] actual column name => value to write
|
|
163
|
+
def pico_phone_rails_phone_search_index_values(phone_number, resolved_region, columns)
|
|
164
|
+
e164 = pico_phone_rails_safe_phone_call(phone_number, :e164)
|
|
165
|
+
national_digits = pico_phone_rails_safe_phone_call(phone_number, :national)&.gsub(/\D/, "")
|
|
166
|
+
|
|
167
|
+
values = {
|
|
168
|
+
e164: e164,
|
|
169
|
+
national_digits: national_digits,
|
|
170
|
+
reversed_digits: national_digits&.reverse,
|
|
171
|
+
region: (resolved_region if e164)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
columns.each_with_object({}) { |(key, column), result| result[column] = values[key] }
|
|
175
|
+
end
|
|
176
|
+
private :pico_phone_rails_phone_search_index_values
|
|
177
|
+
|
|
178
|
+
# @param phone_number [PicoPhone::PhoneNumber]
|
|
179
|
+
# @param method_name [Symbol]
|
|
180
|
+
# @return [String, nil] +nil+ if +method_name+ raises on unparseable input
|
|
181
|
+
def pico_phone_rails_safe_phone_call(phone_number, method_name)
|
|
182
|
+
phone_number.public_send(method_name)
|
|
183
|
+
rescue TypeError
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
private :pico_phone_rails_safe_phone_call
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -17,6 +17,19 @@ module PicoPhone
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
initializer "pico_phone_rails.extraction" do
|
|
21
|
+
ActiveSupport.on_load(:active_record) do
|
|
22
|
+
require "pico_phone/rails/extracted_phone_number"
|
|
23
|
+
include PicoPhone::Rails::Extraction
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
initializer "pico_phone_rails.phone_search_index" do
|
|
28
|
+
ActiveSupport.on_load(:active_record) do
|
|
29
|
+
include PicoPhone::Rails::PhoneSearchIndex
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
20
33
|
initializer "pico_phone_rails.active_job_serializer" do
|
|
21
34
|
ActiveSupport.on_load(:active_job) do
|
|
22
35
|
require "pico_phone/rails/serializers/phone_number_serializer"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PicoPhone
|
|
4
|
+
module Rails
|
|
5
|
+
# Resolves a `region:` option that may be a plain region code, or --
|
|
6
|
+
# for apps where the correct region varies per record (multi-tenant,
|
|
7
|
+
# multi-region) -- a Symbol naming an instance method, or a Proc called
|
|
8
|
+
# with the record. Shared by {Extraction} and {PhoneSearchIndex}.
|
|
9
|
+
#
|
|
10
|
+
# @param region [String, Symbol, Proc]
|
|
11
|
+
# @param record [Object]
|
|
12
|
+
# @return [String, nil]
|
|
13
|
+
def self.resolve_region(region, record)
|
|
14
|
+
case region
|
|
15
|
+
when Symbol then record.public_send(region)
|
|
16
|
+
when Proc then region.call(record)
|
|
17
|
+
else region
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/pico_phone/rails.rb
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
require "pico_phone"
|
|
4
4
|
require "pico_phone/rails/version"
|
|
5
|
-
require "phone_validator"
|
|
6
|
-
require "pico_phone/rails/type"
|
|
7
|
-
require "pico_phone/rails/normalizer"
|
|
8
|
-
require "pico_phone/rails/railtie" if defined?(Rails::Railtie)
|
|
9
5
|
|
|
10
6
|
module PicoPhone
|
|
11
7
|
module Rails
|
|
12
8
|
class Error < StandardError; end
|
|
13
9
|
end
|
|
14
10
|
end
|
|
11
|
+
|
|
12
|
+
require "phone_validator"
|
|
13
|
+
require "pico_phone/rails/region_resolution"
|
|
14
|
+
require "pico_phone/rails/type"
|
|
15
|
+
require "pico_phone/rails/normalizer"
|
|
16
|
+
require "pico_phone/rails/extraction"
|
|
17
|
+
require "pico_phone/rails/phone_search_index"
|
|
18
|
+
require "pico_phone/rails/railtie" if defined?(Rails::Railtie)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pico_phone-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabi Jack
|
|
@@ -53,8 +53,11 @@ dependencies:
|
|
|
53
53
|
version: '7.0'
|
|
54
54
|
description: 'pico_phone-rails wires the pico_phone gem into Rails: a :phone_number
|
|
55
55
|
ActiveRecord attribute type, a PhoneValidator for ActiveModel validations, a normalize_phone
|
|
56
|
-
class macro that rewrites phone attributes to E.164 before validation,
|
|
57
|
-
|
|
56
|
+
class macro that rewrites phone attributes to E.164 before validation, extract_phone_numbers_from
|
|
57
|
+
for pulling phone numbers out of free text (with an optional persisted backend for
|
|
58
|
+
cross-record search), maintain_phone_search_index for keeping search columns in
|
|
59
|
+
sync on an existing phone-number table, and an ActiveJob serializer so a PhoneNumber
|
|
60
|
+
survives being passed as a job argument.'
|
|
58
61
|
email:
|
|
59
62
|
- gabi@gabijack.com
|
|
60
63
|
executables: []
|
|
@@ -63,11 +66,20 @@ extra_rdoc_files: []
|
|
|
63
66
|
files:
|
|
64
67
|
- LICENSE.txt
|
|
65
68
|
- README.md
|
|
69
|
+
- lib/generators/pico_phone/rails/extracted_phone_numbers/extracted_phone_numbers_generator.rb
|
|
70
|
+
- lib/generators/pico_phone/rails/extracted_phone_numbers/templates/create_pico_phone_rails_extracted_phone_numbers.rb.tt
|
|
71
|
+
- lib/generators/pico_phone/rails/phone_number/phone_number_generator.rb
|
|
72
|
+
- lib/generators/pico_phone/rails/phone_number/templates/create_table.rb.tt
|
|
73
|
+
- lib/generators/pico_phone/rails/phone_number/templates/model.rb.tt
|
|
66
74
|
- lib/phone_validator.rb
|
|
67
75
|
- lib/pico_phone/rails.rb
|
|
76
|
+
- lib/pico_phone/rails/extracted_phone_number.rb
|
|
77
|
+
- lib/pico_phone/rails/extraction.rb
|
|
68
78
|
- lib/pico_phone/rails/locale/en.yml
|
|
69
79
|
- lib/pico_phone/rails/normalizer.rb
|
|
80
|
+
- lib/pico_phone/rails/phone_search_index.rb
|
|
70
81
|
- lib/pico_phone/rails/railtie.rb
|
|
82
|
+
- lib/pico_phone/rails/region_resolution.rb
|
|
71
83
|
- lib/pico_phone/rails/serializers/phone_number_serializer.rb
|
|
72
84
|
- lib/pico_phone/rails/type.rb
|
|
73
85
|
- lib/pico_phone/rails/version.rb
|
|
@@ -98,5 +110,5 @@ requirements: []
|
|
|
98
110
|
rubygems_version: 3.6.9
|
|
99
111
|
specification_version: 4
|
|
100
112
|
summary: 'Rails integration for pico_phone: attribute type, validator, normalizer,
|
|
101
|
-
and ActiveJob serializer'
|
|
113
|
+
free-text extraction, phone search index, and ActiveJob serializer'
|
|
102
114
|
test_files: []
|