rails-contact 0.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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +12 -0
  3. data/.github/workflows/ci.yml +65 -0
  4. data/CHANGELOG.md +10 -0
  5. data/Gemfile +19 -0
  6. data/LICENSE.txt +21 -0
  7. data/MIT-LICENSE +21 -0
  8. data/README.md +71 -0
  9. data/Rakefile +3 -0
  10. data/app/assets/stylesheets/rails/contact/application.css +15 -0
  11. data/app/controllers/rails/contact/application_controller.rb +13 -0
  12. data/app/controllers/rails/contact/contacts_controller.rb +77 -0
  13. data/app/helpers/rails/contact/application_helper.rb +6 -0
  14. data/app/jobs/rails/contact/application_job.rb +6 -0
  15. data/app/jobs/rails/contact/google_sync_job.rb +13 -0
  16. data/app/jobs/rails/contact/index_contact_job.rb +18 -0
  17. data/app/mailers/rails/contact/application_mailer.rb +8 -0
  18. data/app/mailers/rails/contact/contact_mailer.rb +32 -0
  19. data/app/models/rails/contact/application_record.rb +7 -0
  20. data/app/models/rails/contact/contact.rb +32 -0
  21. data/app/models/rails/contact/contact_address.rb +9 -0
  22. data/app/models/rails/contact/contact_email.rb +24 -0
  23. data/app/models/rails/contact/contact_phone.rb +28 -0
  24. data/app/views/layouts/rails/contact/application.html.erb +17 -0
  25. data/app/views/rails/contact/contacts/_form.html.erb +61 -0
  26. data/app/views/rails/contact/contacts/edit.html.erb +3 -0
  27. data/app/views/rails/contact/contacts/index.html.erb +38 -0
  28. data/app/views/rails/contact/contacts/new.html.erb +3 -0
  29. data/app/views/rails/contact/contacts/show.html.erb +13 -0
  30. data/bin/rails +25 -0
  31. data/bin/rubocop +8 -0
  32. data/config/routes.rb +4 -0
  33. data/lib/generators/rails/contact/contact_generator.rb +29 -0
  34. data/lib/generators/rails/contact/install/install_generator.rb +19 -0
  35. data/lib/generators/rails/contact/install/templates/rails_contact.rb.tt +16 -0
  36. data/lib/generators/rails/contact/templates/create_rails_contact_contact_addresses.rb.tt +13 -0
  37. data/lib/generators/rails/contact/templates/create_rails_contact_contact_emails.rb.tt +15 -0
  38. data/lib/generators/rails/contact/templates/create_rails_contact_contact_phones.rb.tt +16 -0
  39. data/lib/generators/rails/contact/templates/create_rails_contact_contacts.rb.tt +24 -0
  40. data/lib/generators/rails/contact/views_generator.rb +15 -0
  41. data/lib/generators/rails_contact/rails_contact_generator.rb +7 -0
  42. data/lib/rails/contact/configuration.rb +25 -0
  43. data/lib/rails/contact/csv/import_service.rb +87 -0
  44. data/lib/rails/contact/engine.rb +21 -0
  45. data/lib/rails/contact/google/client.rb +55 -0
  46. data/lib/rails/contact/google/conflict_resolver.rb +20 -0
  47. data/lib/rails/contact/google/payload_mapper.rb +26 -0
  48. data/lib/rails/contact/google/sync_service.rb +34 -0
  49. data/lib/rails/contact/google/token_store.rb +31 -0
  50. data/lib/rails/contact/orm.rb +19 -0
  51. data/lib/rails/contact/routing.rb +11 -0
  52. data/lib/rails/contact/search/backends/database.rb +41 -0
  53. data/lib/rails/contact/search/backends/elasticsearch.rb +119 -0
  54. data/lib/rails/contact/search/query.rb +27 -0
  55. data/lib/rails/contact/version.rb +5 -0
  56. data/lib/rails/contact.rb +32 -0
  57. data/lib/tasks/rails/contact_tasks.rake +31 -0
  58. data/test/csv_import_service_test.rb +25 -0
  59. data/test/google_sync_service_test.rb +47 -0
  60. data/test/payload_mapper_test.rb +19 -0
  61. data/test/search_fallback_test.rb +25 -0
  62. data/test/test_helper.rb +79 -0
  63. metadata +192 -0
@@ -0,0 +1,119 @@
1
+ module Rails
2
+ module Contact
3
+ module Search
4
+ module Backends
5
+ class Elasticsearch
6
+ INDEX = "rails_contact_contacts".freeze
7
+
8
+ def initialize(client: nil)
9
+ @client = client || ::Elasticsearch::Client.new(url: Rails::Contact.configuration.elasticsearch_url)
10
+ end
11
+
12
+ def search(query, filters)
13
+ response = @client.search(index: INDEX, body: search_body(query, filters))
14
+ ids = response.fetch("hits", {}).fetch("hits", []).map { |doc| doc["_id"] }
15
+ records_by_id = Contact.where(id: ids).index_by { |record| record.id.to_s }
16
+ ids.filter_map { |id| records_by_id[id.to_s] }
17
+ rescue StandardError
18
+ Search::Backends::Database.new.search(query, filters)
19
+ end
20
+
21
+ def upsert(contact)
22
+ @client.index(index: INDEX, id: contact.id, body: document_for(contact))
23
+ end
24
+
25
+ def remove(contact_id)
26
+ @client.delete(index: INDEX, id: contact_id)
27
+ rescue StandardError
28
+ nil
29
+ end
30
+
31
+ def create_index!
32
+ return if @client.indices.exists?(index: INDEX)
33
+
34
+ @client.indices.create(index: INDEX, body: index_mapping)
35
+ end
36
+
37
+ private
38
+
39
+ def index_mapping
40
+ {
41
+ settings: {
42
+ analysis: {
43
+ analyzer: {
44
+ folding_analyzer: {
45
+ tokenizer: "standard",
46
+ filter: [ "lowercase", "asciifolding" ]
47
+ }
48
+ }
49
+ }
50
+ },
51
+ mappings: {
52
+ properties: {
53
+ full_name: { type: "text", analyzer: "folding_analyzer" },
54
+ emails: { type: "keyword" },
55
+ phones_e164: { type: "keyword" },
56
+ region_name: { type: "keyword" },
57
+ current_city: { type: "keyword" },
58
+ departure_city: { type: "keyword" },
59
+ sync_eligible: { type: "boolean" },
60
+ updated_at: { type: "date" }
61
+ }
62
+ }
63
+ }
64
+ end
65
+
66
+ def search_body(query, filters)
67
+ {
68
+ size: Rails::Contact.configuration.default_per_page,
69
+ sort: [ { updated_at: { order: "desc" } }, { id: { order: "desc" } } ],
70
+ query: {
71
+ bool: {
72
+ must: search_clause(query),
73
+ filter: filter_clauses(filters)
74
+ }
75
+ }
76
+ }
77
+ end
78
+
79
+ def search_clause(query)
80
+ return [ { match_all: {} } ] if query.blank?
81
+
82
+ [ {
83
+ multi_match: {
84
+ query: query,
85
+ fields: [ "full_name^3", "emails^2", "phones_e164" ],
86
+ fuzziness: "AUTO"
87
+ }
88
+ } ]
89
+ end
90
+
91
+ def filter_clauses(filters)
92
+ clauses = []
93
+ clauses << { term: { current_city: filters["city"] } } if filters["city"].present?
94
+ clauses << { term: { region_name: filters["region"] } } if filters["region"].present?
95
+ unless filters["sync_eligible"].nil?
96
+ value = ActiveModel::Type::Boolean.new.cast(filters["sync_eligible"])
97
+ clauses << { term: { sync_eligible: value } }
98
+ end
99
+ clauses
100
+ end
101
+
102
+ def document_for(contact)
103
+ {
104
+ id: contact.id,
105
+ full_name: contact.full_name,
106
+ emails: contact.emails.map(&:value),
107
+ phones_e164: contact.phones.map(&:e164),
108
+ region_name: contact.region_name,
109
+ current_city: contact.current_city,
110
+ departure_city: contact.departure_city,
111
+ sync_eligible: contact.sync_eligible,
112
+ updated_at: contact.updated_at
113
+ }
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,27 @@
1
+ module Rails
2
+ module Contact
3
+ module Search
4
+ class Query
5
+ def initialize(query, filters: {})
6
+ @query = query
7
+ @filters = filters.to_h.compact_blank
8
+ end
9
+
10
+ def call
11
+ backend.search(@query, @filters)
12
+ end
13
+
14
+ private
15
+
16
+ def backend
17
+ case Rails::Contact.configuration.search_backend.to_sym
18
+ when :elasticsearch
19
+ Search::Backends::Elasticsearch.new
20
+ else
21
+ Search::Backends::Database.new
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ module Rails
2
+ module Contact
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,32 @@
1
+ require "rails/contact/version"
2
+ require "elasticsearch"
3
+ require "rails/contact/engine"
4
+ require "rails/contact/configuration"
5
+ require "rails/contact/orm"
6
+ require "rails/contact/routing"
7
+ require "rails/contact/search/query"
8
+ require "rails/contact/search/backends/database"
9
+ require "rails/contact/search/backends/elasticsearch"
10
+ require "rails/contact/csv/import_service"
11
+ require "rails/contact/google/client"
12
+ require "rails/contact/google/token_store"
13
+ require "rails/contact/google/payload_mapper"
14
+ require "rails/contact/google/sync_service"
15
+
16
+ module Rails
17
+ module Contact
18
+ class Error < StandardError; end
19
+
20
+ class << self
21
+ attr_writer :configuration
22
+
23
+ def configuration
24
+ @configuration ||= Configuration.new
25
+ end
26
+
27
+ def configure
28
+ yield(configuration)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ namespace :rails_contact do
2
+ desc "Install initializer and mount routes"
3
+ task install: :environment do
4
+ puts "Run: rails generate rails:contact:install"
5
+ end
6
+
7
+ desc "Create Elasticsearch index and reindex all contacts"
8
+ task reindex: :environment do
9
+ backend = Rails::Contact::Search::Backends::Elasticsearch.new
10
+ backend.create_index!
11
+ Rails::Contact::Contact.includes(:emails, :phones).find_each do |contact|
12
+ backend.upsert(contact)
13
+ end
14
+ puts "Indexed contacts into Elasticsearch"
15
+ end
16
+
17
+ desc "Run Google two-way sync for rolling window"
18
+ task sync_google: :environment do
19
+ Rails::Contact::GoogleSyncJob.perform_now
20
+ puts "Google sync completed"
21
+ end
22
+
23
+ desc "Import contacts from CSV path (CSV_PATH=/path/file.csv)"
24
+ task import_csv: :environment do
25
+ path = ENV["CSV_PATH"]
26
+ raise "Set CSV_PATH env var" if path.blank?
27
+
28
+ count = Rails::Contact::Csv::ImportService.new(path: path).import!
29
+ puts "Imported #{count} rows"
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ require_relative "test_helper"
2
+ require "tempfile"
3
+
4
+ module Rails
5
+ module Contact
6
+ class CsvImportServiceTest < Minitest::Test
7
+ def test_imports_and_dedupes_by_email
8
+ file = Tempfile.new([ "contacts", ".csv" ])
9
+ file.write("Enquirer Email,Enquirer First Name,Enquirer Last Name,Enquirer Phone,Enquirer Phone Country Code,Current City,Departure City,Region Name\n")
10
+ file.write("john@example.com,John,Doe,9998887777,+91,Delhi,Mumbai,UK Tours\n")
11
+ file.write("john@example.com,John,Doe,9998887777,+91,Delhi,Mumbai,UK Tours\n")
12
+ file.rewind
13
+
14
+ count = Csv::ImportService.new(path: file.path).import!
15
+
16
+ assert_equal 2, count
17
+ assert_equal 1, Contact.count
18
+ assert_equal 1, Contact.first.emails.count
19
+ ensure
20
+ file.close
21
+ file.unlink
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,47 @@
1
+ require_relative "test_helper"
2
+
3
+ module Rails
4
+ module Contact
5
+ class GoogleSyncServiceTest < Minitest::Test
6
+ FakeTokenStore = Struct.new(:access_token)
7
+
8
+ class FakeClient
9
+ attr_reader :created
10
+
11
+ def initialize
12
+ @created = []
13
+ end
14
+
15
+ def create_contact(payload)
16
+ @created << payload
17
+ { "resourceName" => "people/123", "etag" => "abc" }
18
+ end
19
+
20
+ def update_contact(_resource_name, payload)
21
+ @created << payload
22
+ { "resourceName" => "people/123", "etag" => "def" }
23
+ end
24
+ end
25
+
26
+ def setup
27
+ Rails::Contact.configuration.google_max_contacts = 1
28
+ end
29
+
30
+ def test_syncs_only_within_rolling_window
31
+ older = Contact.create!(given_name: "Old", updated_at: 2.days.ago, created_at: 2.days.ago)
32
+ newer = Contact.create!(given_name: "New", updated_at: Time.current, created_at: Time.current)
33
+ [ older, newer ].each do |contact|
34
+ contact.emails.create!(value: "#{contact.given_name.downcase}@example.com", primary: true)
35
+ contact.phones.create!(value: "+911234567890", e164: "+911234567890", primary: true)
36
+ end
37
+
38
+ client = FakeClient.new
39
+ Google::SyncService.new(token_store: FakeTokenStore.new("token"), client: client).sync!
40
+
41
+ assert_equal 1, client.created.size
42
+ assert_nil older.reload.google_resource_name
43
+ refute_nil newer.reload.google_resource_name
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "test_helper"
2
+
3
+ module Rails
4
+ module Contact
5
+ class PayloadMapperTest < Minitest::Test
6
+ def test_maps_contact_to_people_payload
7
+ contact = Contact.create!(given_name: "A", family_name: "B", current_city: "Delhi", departure_city: "Pune", region_name: "UK Tours")
8
+ contact.emails.create!(value: "ab@example.com", primary: true, label: "work")
9
+ contact.phones.create!(value: "+919999999999", e164: "+919999999999", primary: true, label: "mobile")
10
+ contact.addresses.create!(city: "Delhi", departure_city: "Pune", formatted_value: "Current city: Delhi\nDeparture city: Pune")
11
+
12
+ payload = Google::PayloadMapper.new(contact).to_people_payload
13
+ assert_equal "A", payload[:names][0][:givenName]
14
+ assert_equal "ab@example.com", payload[:emailAddresses][0][:value]
15
+ assert_equal "+919999999999", payload[:phoneNumbers][0][:value]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ require_relative "test_helper"
2
+
3
+ module Rails
4
+ module Contact
5
+ class SearchFallbackTest < Minitest::Test
6
+ class FailingClient
7
+ def search(*)
8
+ raise StandardError, "boom"
9
+ end
10
+ end
11
+
12
+ def test_falls_back_to_database_when_elasticsearch_fails
13
+ contact = Contact.create!(given_name: "Aruna", family_name: "D", current_city: "Pune")
14
+ contact.emails.create!(value: "aruna@example.com", primary: true)
15
+ contact.phones.create!(value: "+919999999999", e164: "+919999999999", primary: true)
16
+
17
+ backend = Search::Backends::Elasticsearch.new(client: FailingClient.new)
18
+ results = backend.search("Aruna", {})
19
+
20
+ assert_equal 1, results.size
21
+ assert_equal "Aruna", results.first.given_name
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,79 @@
1
+ ENV["RAILS_ENV"] ||= "test"
2
+
3
+ require "bundler/setup"
4
+ require "rails"
5
+ require "active_record"
6
+ require "active_job"
7
+ require "minitest/autorun"
8
+ require "minitest/reporters"
9
+ require "webmock/minitest"
10
+ require "mocha/minitest"
11
+ require "rails/contact"
12
+ require_relative "../app/models/rails/contact/application_record"
13
+ require_relative "../app/models/rails/contact/contact"
14
+ require_relative "../app/models/rails/contact/contact_email"
15
+ require_relative "../app/models/rails/contact/contact_phone"
16
+ require_relative "../app/models/rails/contact/contact_address"
17
+ require_relative "../app/jobs/rails/contact/application_job"
18
+ require_relative "../app/jobs/rails/contact/index_contact_job"
19
+
20
+ Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
21
+
22
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
23
+
24
+ ActiveRecord::Schema.define do
25
+ create_table :rails_contact_contacts, force: true do |t|
26
+ t.string :given_name, null: false
27
+ t.string :family_name
28
+ t.string :region_name
29
+ t.string :departure_city
30
+ t.string :current_city
31
+ t.string :google_resource_name
32
+ t.string :google_etag
33
+ t.datetime :google_last_modified_at
34
+ t.datetime :last_synced_at
35
+ t.boolean :sync_eligible, default: true
36
+ t.text :biography
37
+ t.text :metadata
38
+ t.timestamps
39
+ end
40
+
41
+ create_table :rails_contact_contact_emails, force: true do |t|
42
+ t.integer :contact_id, null: false
43
+ t.string :value, null: false
44
+ t.string :label
45
+ t.boolean :primary, default: false
46
+ t.timestamps
47
+ end
48
+
49
+ create_table :rails_contact_contact_phones, force: true do |t|
50
+ t.integer :contact_id, null: false
51
+ t.string :value, null: false
52
+ t.string :e164
53
+ t.string :label
54
+ t.boolean :primary, default: false
55
+ t.timestamps
56
+ end
57
+
58
+ create_table :rails_contact_contact_addresses, force: true do |t|
59
+ t.integer :contact_id, null: false
60
+ t.string :city
61
+ t.string :departure_city
62
+ t.text :formatted_value
63
+ t.string :label
64
+ t.timestamps
65
+ end
66
+ end
67
+
68
+ ActiveJob::Base.queue_adapter = :test
69
+
70
+ class Minitest::Test
71
+ def setup
72
+ Rails::Contact::ContactAddress.delete_all
73
+ Rails::Contact::ContactPhone.delete_all
74
+ Rails::Contact::ContactEmail.delete_all
75
+ Rails::Contact::Contact.delete_all
76
+ ActiveJob::Base.queue_adapter.enqueued_jobs.clear
77
+ ActiveJob::Base.queue_adapter.performed_jobs.clear
78
+ end
79
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-contact
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kshitiz Sinha
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: httparty
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0.22'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.22'
40
+ - !ruby/object:Gem::Dependency
41
+ name: phonelib
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0.8'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0.8'
54
+ - !ruby/object:Gem::Dependency
55
+ name: faraday
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '2.9'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '2.9'
68
+ - !ruby/object:Gem::Dependency
69
+ name: faraday-retry
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '2.2'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '2.2'
82
+ - !ruby/object:Gem::Dependency
83
+ name: elasticsearch
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '8.13'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '8.13'
96
+ description: Mountable Rails engine offering contact CRUD, CSV import, Elasticsearch-backed
97
+ search, and optional capped Google Contacts two-way sync.
98
+ email:
99
+ - kshtzkr@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".github/dependabot.yml"
105
+ - ".github/workflows/ci.yml"
106
+ - CHANGELOG.md
107
+ - Gemfile
108
+ - LICENSE.txt
109
+ - MIT-LICENSE
110
+ - README.md
111
+ - Rakefile
112
+ - app/assets/stylesheets/rails/contact/application.css
113
+ - app/controllers/rails/contact/application_controller.rb
114
+ - app/controllers/rails/contact/contacts_controller.rb
115
+ - app/helpers/rails/contact/application_helper.rb
116
+ - app/jobs/rails/contact/application_job.rb
117
+ - app/jobs/rails/contact/google_sync_job.rb
118
+ - app/jobs/rails/contact/index_contact_job.rb
119
+ - app/mailers/rails/contact/application_mailer.rb
120
+ - app/mailers/rails/contact/contact_mailer.rb
121
+ - app/models/rails/contact/application_record.rb
122
+ - app/models/rails/contact/contact.rb
123
+ - app/models/rails/contact/contact_address.rb
124
+ - app/models/rails/contact/contact_email.rb
125
+ - app/models/rails/contact/contact_phone.rb
126
+ - app/views/layouts/rails/contact/application.html.erb
127
+ - app/views/rails/contact/contacts/_form.html.erb
128
+ - app/views/rails/contact/contacts/edit.html.erb
129
+ - app/views/rails/contact/contacts/index.html.erb
130
+ - app/views/rails/contact/contacts/new.html.erb
131
+ - app/views/rails/contact/contacts/show.html.erb
132
+ - bin/rails
133
+ - bin/rubocop
134
+ - config/routes.rb
135
+ - lib/generators/rails/contact/contact_generator.rb
136
+ - lib/generators/rails/contact/install/install_generator.rb
137
+ - lib/generators/rails/contact/install/templates/rails_contact.rb.tt
138
+ - lib/generators/rails/contact/templates/create_rails_contact_contact_addresses.rb.tt
139
+ - lib/generators/rails/contact/templates/create_rails_contact_contact_emails.rb.tt
140
+ - lib/generators/rails/contact/templates/create_rails_contact_contact_phones.rb.tt
141
+ - lib/generators/rails/contact/templates/create_rails_contact_contacts.rb.tt
142
+ - lib/generators/rails/contact/views_generator.rb
143
+ - lib/generators/rails_contact/rails_contact_generator.rb
144
+ - lib/rails/contact.rb
145
+ - lib/rails/contact/configuration.rb
146
+ - lib/rails/contact/csv/import_service.rb
147
+ - lib/rails/contact/engine.rb
148
+ - lib/rails/contact/google/client.rb
149
+ - lib/rails/contact/google/conflict_resolver.rb
150
+ - lib/rails/contact/google/payload_mapper.rb
151
+ - lib/rails/contact/google/sync_service.rb
152
+ - lib/rails/contact/google/token_store.rb
153
+ - lib/rails/contact/orm.rb
154
+ - lib/rails/contact/routing.rb
155
+ - lib/rails/contact/search/backends/database.rb
156
+ - lib/rails/contact/search/backends/elasticsearch.rb
157
+ - lib/rails/contact/search/query.rb
158
+ - lib/rails/contact/version.rb
159
+ - lib/tasks/rails/contact_tasks.rake
160
+ - test/csv_import_service_test.rb
161
+ - test/google_sync_service_test.rb
162
+ - test/payload_mapper_test.rb
163
+ - test/search_fallback_test.rb
164
+ - test/test_helper.rb
165
+ homepage: https://github.com/kshtzkr/rails-contact
166
+ licenses:
167
+ - MIT
168
+ metadata:
169
+ allowed_push_host: https://rubygems.org
170
+ homepage_uri: https://github.com/kshtzkr/rails-contact
171
+ source_code_uri: https://github.com/kshtzkr/rails-contact/tree/main
172
+ changelog_uri: https://github.com/kshtzkr/rails-contact/releases
173
+ bug_tracker_uri: https://github.com/kshtzkr/rails-contact/issues
174
+ rubygems_mfa_required: 'true'
175
+ rdoc_options: []
176
+ require_paths:
177
+ - lib
178
+ required_ruby_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '3.1'
183
+ required_rubygems_version: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ requirements: []
189
+ rubygems_version: 4.0.3
190
+ specification_version: 4
191
+ summary: Google-shaped contacts for Rails with search and sync.
192
+ test_files: []