attio-rails 0.1.1 → 0.2.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.
data/CONCEPTS.md ADDED
@@ -0,0 +1,448 @@
1
+ # Attio Rails - Concepts & Architecture
2
+
3
+ ## Table of Contents
4
+ - [Core Concepts](#core-concepts)
5
+ - [Architecture Overview](#architecture-overview)
6
+ - [Sync Flow](#sync-flow)
7
+ - [Batch Processing](#batch-processing)
8
+ - [Error Handling & Retry Strategy](#error-handling--retry-strategy)
9
+ - [Testing Strategy](#testing-strategy)
10
+
11
+ ## Core Concepts
12
+
13
+ ### 1. ActiveRecord Integration Pattern
14
+
15
+ The gem uses the **Concern pattern** to mix functionality into ActiveRecord models. This provides a clean, Rails-idiomatic interface:
16
+
17
+ ```mermaid
18
+ graph TD
19
+ A[ActiveRecord Model] -->|includes| B[Attio::Rails::Concerns::Syncable]
20
+ B -->|provides| C[Sync Methods]
21
+ B -->|provides| D[Callbacks]
22
+ B -->|provides| E[Attribute Mapping]
23
+
24
+ C --> F[sync_to_attio_now]
25
+ C --> G[sync_to_attio_later]
26
+ C --> H[remove_from_attio]
27
+
28
+ D --> I[before_attio_sync]
29
+ D --> J[after_attio_sync]
30
+ D --> K[ActiveRecord Callbacks]
31
+ ```
32
+
33
+ ### 2. Attribute Mapping System
34
+
35
+ The attribute mapping system supports multiple mapping strategies:
36
+
37
+ ```mermaid
38
+ graph LR
39
+ A[Model Attributes] --> B{Mapping Type}
40
+ B -->|Symbol| C[Method Call]
41
+ B -->|String| D[Method Call]
42
+ B -->|Proc/Lambda| E[Dynamic Evaluation]
43
+ B -->|Static Value| F[Direct Value]
44
+
45
+ C --> G[Attio Attributes]
46
+ D --> G
47
+ E --> G
48
+ F --> G
49
+ ```
50
+
51
+ **Example mappings:**
52
+ ```ruby
53
+ {
54
+ email: :email_address, # Symbol -> calls model.email_address
55
+ name: "full_name", # String -> calls model.full_name
56
+ type: "customer", # Static -> always "customer"
57
+ count: ->(m) { m.items.count } # Lambda -> evaluated dynamically
58
+ }
59
+ ```
60
+
61
+ ## Architecture Overview
62
+
63
+ ### System Components
64
+
65
+ ```mermaid
66
+ graph TB
67
+ subgraph "Rails Application"
68
+ A[ActiveRecord Model]
69
+ B[ActiveJob Queue]
70
+ end
71
+
72
+ subgraph "Attio Rails Gem"
73
+ C[Syncable Concern]
74
+ D[AttioSyncJob]
75
+ E[BatchSync]
76
+ F[Configuration]
77
+ end
78
+
79
+ subgraph "External"
80
+ G[Attio API]
81
+ end
82
+
83
+ A -->|includes| C
84
+ C -->|enqueues| D
85
+ C -->|uses| E
86
+ D -->|processes| B
87
+ E -->|bulk operations| G
88
+ D -->|sync/delete| G
89
+ C -->|immediate sync| G
90
+ F -->|configures| C
91
+ F -->|configures| D
92
+ ```
93
+
94
+ ## Sync Flow
95
+
96
+ ### Automatic Sync Lifecycle
97
+
98
+ ```mermaid
99
+ sequenceDiagram
100
+ participant User
101
+ participant Model
102
+ participant Syncable
103
+ participant AttioSyncJob
104
+ participant AttioAPI
105
+
106
+ User->>Model: create/update/destroy
107
+ Model->>Syncable: after_commit callback
108
+
109
+ alt Sync Enabled & Conditions Met
110
+ alt Background Sync
111
+ Syncable->>AttioSyncJob: enqueue job
112
+ AttioSyncJob-->>AttioSyncJob: process async
113
+ AttioSyncJob->>AttioAPI: sync data
114
+ AttioAPI-->>AttioSyncJob: response
115
+ AttioSyncJob->>Model: update attio_record_id
116
+ else Immediate Sync
117
+ Syncable->>AttioAPI: sync data
118
+ AttioAPI-->>Syncable: response
119
+ Syncable->>Model: update attio_record_id
120
+ end
121
+ else Sync Disabled or Conditions Not Met
122
+ Syncable-->>Model: skip sync
123
+ end
124
+ ```
125
+
126
+ ### Manual Sync Options
127
+
128
+ ```mermaid
129
+ graph TD
130
+ A[Manual Sync Trigger] --> B{Sync Method}
131
+
132
+ B -->|sync_to_attio_now| C[Immediate Sync]
133
+ B -->|sync_to_attio_later| D[Background Job]
134
+ B -->|sync_to_attio| E{Config Check}
135
+
136
+ E -->|background_sync=true| D
137
+ E -->|background_sync=false| C
138
+
139
+ C --> F[Direct API Call]
140
+ D --> G[Enqueue AttioSyncJob]
141
+
142
+ F --> H[Attio API]
143
+ G --> I[Job Queue]
144
+ I --> H
145
+ ```
146
+
147
+ ## Batch Processing
148
+
149
+ ### BatchSync Flow
150
+
151
+ ```mermaid
152
+ flowchart TD
153
+ A[BatchSync.perform] --> B[Initialize Results Hash]
154
+ B --> C[Process in Batches]
155
+
156
+ C --> D{Async Mode?}
157
+
158
+ D -->|Yes| E[Enqueue Batch]
159
+ D -->|No| F[Sync Batch]
160
+
161
+ E --> G[For Each Record]
162
+ G --> H[Enqueue AttioSyncJob]
163
+
164
+ F --> I[For Each Record]
165
+ I --> J{Has attio_record_id?}
166
+
167
+ J -->|Yes| K[Update Record]
168
+ J -->|No| L[Create Record]
169
+
170
+ K --> M[API Call]
171
+ L --> M
172
+
173
+ M --> N{Success?}
174
+ N -->|Yes| O[Add to success array]
175
+ N -->|No| P[Add to failed array]
176
+
177
+ H --> Q[Add to success array]
178
+
179
+ O --> R[Return Results]
180
+ P --> R
181
+ Q --> R
182
+ ```
183
+
184
+ ### Batch Processing Strategies
185
+
186
+ ```mermaid
187
+ graph LR
188
+ A[Large Dataset] --> B[find_in_batches]
189
+ B --> C[Batch 1]
190
+ B --> D[Batch 2]
191
+ B --> E[Batch N]
192
+
193
+ C --> F{Processing Mode}
194
+ D --> F
195
+ E --> F
196
+
197
+ F -->|Async| G[Job Queue]
198
+ F -->|Sync| H[Direct Processing]
199
+
200
+ G --> I[Parallel Processing]
201
+ H --> J[Sequential Processing]
202
+ ```
203
+
204
+ ## Error Handling & Retry Strategy
205
+
206
+ ### Error Flow
207
+
208
+ ```mermaid
209
+ flowchart TD
210
+ A[Sync Operation] --> B{Success?}
211
+
212
+ B -->|Yes| C[Update Local Record]
213
+ B -->|No| D[Error Occurred]
214
+
215
+ D --> E{Has Error Handler?}
216
+
217
+ E -->|Yes| F[Call Error Handler]
218
+ E -->|No| G{Environment?}
219
+
220
+ F --> H[Custom Logic]
221
+
222
+ G -->|Development| I[Raise Error]
223
+ G -->|Production| J[Log Error]
224
+
225
+ H --> K{In Background Job?}
226
+ K -->|Yes| L[Retry Logic]
227
+ K -->|No| M[Complete]
228
+
229
+ L --> N{Retry Attempts < 3?}
230
+ N -->|Yes| O[Wait & Retry]
231
+ N -->|No| P[Dead Letter Queue]
232
+
233
+ O --> A
234
+ ```
235
+
236
+ ### Retry Strategy with ActiveJob
237
+
238
+ ```mermaid
239
+ graph TD
240
+ A[Job Fails] --> B[Retry Mechanism]
241
+ B --> C{Attempt 1}
242
+ C -->|Fails| D[Wait 3 seconds]
243
+ D --> E{Attempt 2}
244
+ E -->|Fails| F[Wait 18 seconds]
245
+ F --> G{Attempt 3}
246
+ G -->|Fails| H[Job Failed]
247
+ G -->|Success| I[Complete]
248
+ E -->|Success| I
249
+ C -->|Success| I
250
+
251
+ style H fill:#f96
252
+ style I fill:#9f6
253
+ ```
254
+
255
+ ## Testing Strategy
256
+
257
+ ### Test Double Architecture
258
+
259
+ ```mermaid
260
+ graph TD
261
+ A[RSpec Test] --> B[Test Helpers]
262
+
263
+ B --> C[stub_attio_client]
264
+ C --> D[Mock Client]
265
+ C --> E[Mock Records API]
266
+
267
+ B --> F[expect_attio_sync]
268
+ F --> G[Expectation Setup]
269
+
270
+ B --> H[with_attio_sync_disabled]
271
+ H --> I[Temporary Config Change]
272
+
273
+ D --> J[No Real API Calls]
274
+ E --> J
275
+
276
+ J --> K[Fast Tests]
277
+ J --> L[Predictable Results]
278
+ ```
279
+
280
+ ### Testing Layers
281
+
282
+ ```mermaid
283
+ graph TB
284
+ subgraph "Unit Tests"
285
+ A[Model Tests]
286
+ B[Concern Tests]
287
+ C[Job Tests]
288
+ end
289
+
290
+ subgraph "Integration Tests"
291
+ D[Sync Flow Tests]
292
+ E[Batch Processing Tests]
293
+ end
294
+
295
+ subgraph "Test Helpers"
296
+ F[Stubbed Client]
297
+ G[Job Assertions]
298
+ H[Config Helpers]
299
+ end
300
+
301
+ A --> F
302
+ B --> F
303
+ C --> G
304
+ D --> F
305
+ D --> G
306
+ E --> F
307
+ E --> H
308
+ ```
309
+
310
+ ## Configuration Cascade
311
+
312
+ ### Configuration Priority
313
+
314
+ ```mermaid
315
+ graph TD
316
+ A[Configuration Sources] --> B[Environment Variables]
317
+ A --> C[Initializer Config]
318
+ A --> D[Model-level Options]
319
+ A --> E[Method-level Options]
320
+
321
+ B --> F{Priority}
322
+ C --> F
323
+ D --> F
324
+ E --> F
325
+
326
+ F --> G[Final Configuration]
327
+
328
+ style E fill:#9f6
329
+ style D fill:#af9
330
+ style C fill:#cf9
331
+ style B fill:#ff9
332
+ ```
333
+
334
+ Priority order (highest to lowest):
335
+ 1. Method-level options (e.g., `sync_to_attio_now(force: true)`)
336
+ 2. Model-level options (e.g., `syncs_with_attio 'people', if: :active?`)
337
+ 3. Initializer configuration (e.g., `config.background_sync = true`)
338
+ 4. Environment variables (e.g., `ATTIO_API_KEY`)
339
+
340
+ ## Data Flow Transformations
341
+
342
+ ### Transform Pipeline
343
+
344
+ ```mermaid
345
+ graph LR
346
+ A[Raw Model Data] --> B[Attribute Mapping]
347
+ B --> C[Transform Function]
348
+ C --> D[Validated Data]
349
+ D --> E[API Payload]
350
+
351
+ B -.->|Example| B1[email: :work_email]
352
+ C -.->|Example| C1[Add computed fields]
353
+ D -.->|Example| D1[Remove nil values]
354
+ E -.->|Example| E1[JSON structure]
355
+ ```
356
+
357
+ ### Callback Chain
358
+
359
+ ```mermaid
360
+ sequenceDiagram
361
+ participant Model
362
+ participant Callbacks
363
+ participant Sync
364
+ participant API
365
+
366
+ Model->>Callbacks: before_attio_sync
367
+ Callbacks->>Callbacks: prepare_data
368
+ Callbacks->>Sync: proceed with sync
369
+ Sync->>Sync: transform_attributes
370
+ Sync->>API: send data
371
+ API-->>Sync: response
372
+ Sync->>Callbacks: after_attio_sync
373
+ Callbacks->>Callbacks: log_sync
374
+ Callbacks->>Model: complete
375
+ ```
376
+
377
+ ## Performance Considerations
378
+
379
+ ### Optimization Strategies
380
+
381
+ ```mermaid
382
+ graph TD
383
+ A[Performance Optimizations] --> B[Batch Processing]
384
+ A --> C[Background Jobs]
385
+ A --> D[Connection Pooling]
386
+ A --> E[Smart Retries]
387
+
388
+ B --> F[Reduce API Calls]
389
+ C --> G[Non-blocking Operations]
390
+ D --> H[Reuse Connections]
391
+ E --> I[Exponential Backoff]
392
+
393
+ F --> J[Better Performance]
394
+ G --> J
395
+ H --> J
396
+ I --> J
397
+ ```
398
+
399
+ ### Load Distribution
400
+
401
+ ```mermaid
402
+ graph LR
403
+ A[100 Records to Sync] --> B[BatchSync]
404
+ B --> C[Batch 1: Records 1-25]
405
+ B --> D[Batch 2: Records 26-50]
406
+ B --> E[Batch 3: Records 51-75]
407
+ B --> F[Batch 4: Records 76-100]
408
+
409
+ C --> G[Job Queue]
410
+ D --> G
411
+ E --> G
412
+ F --> G
413
+
414
+ G --> H[Worker 1]
415
+ G --> I[Worker 2]
416
+ G --> J[Worker N]
417
+
418
+ style G fill:#9cf
419
+ ```
420
+
421
+ ## Best Practices
422
+
423
+ ### Recommended Patterns
424
+
425
+ 1. **Use Background Sync for Production**
426
+ - Prevents blocking web requests
427
+ - Provides automatic retry on failure
428
+ - Better user experience
429
+
430
+ 2. **Implement Error Handlers**
431
+ - Log errors to monitoring services
432
+ - Gracefully handle API downtime
433
+ - Notify administrators of issues
434
+
435
+ 3. **Optimize Attribute Mapping**
436
+ - Only sync necessary fields
437
+ - Use transforms to reduce payload size
438
+ - Cache computed values when possible
439
+
440
+ 4. **Test Thoroughly**
441
+ - Use provided test helpers
442
+ - Mock external API calls
443
+ - Test error scenarios
444
+
445
+ 5. **Monitor Performance**
446
+ - Track sync success rates
447
+ - Monitor job queue depth
448
+ - Alert on repeated failures
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  # Specify your gem's dependencies in attio-rails.gemspec
@@ -6,15 +8,19 @@ gemspec
6
8
  gem "rake", "~> 13.0"
7
9
 
8
10
  group :development, :test do
9
- gem "rspec", "~> 3.12"
10
- gem "rspec-rails", "~> 6.0"
11
+ gem "bundler-audit", "~> 0.9"
12
+ gem "danger", "~> 9.4"
13
+ gem "pry", "~> 0.14"
11
14
  gem "rails", "~> 7.0"
12
- gem "sqlite3", "~> 1.4"
13
- gem "simplecov", "~> 0.22"
14
- gem "simplecov-console", "~> 0.9"
15
+ gem "redcarpet", "~> 3.5"
16
+ gem "rspec", "~> 3.13"
17
+ gem "rspec-rails", "~> 6.0"
15
18
  gem "rubocop", "~> 1.50"
16
19
  gem "rubocop-rails", "~> 2.19"
17
- gem "rubocop-rspec", "~> 2.19"
18
- gem "pry", "~> 0.14"
19
- gem "webmock", "~> 3.18"
20
+ gem "rubocop-rspec", "~> 3.6"
21
+ gem "simplecov", "~> 0.22"
22
+ gem "simplecov-console", "~> 0.9"
23
+ gem "sqlite3", "~> 1.4"
24
+ gem "webmock", "~> 3.19"
25
+ gem "yard", "~> 0.9"
20
26
  end
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rspec/core/rake_task"
3
5
  require "rubocop/rake_task"
@@ -7,9 +9,9 @@ RSpec::Core::RakeTask.new(:spec)
7
9
  RuboCop::RakeTask.new
8
10
 
9
11
  YARD::Rake::YardocTask.new do |t|
10
- t.files = ['lib/**/*.rb']
11
- t.options = ['--markup-provider=redcarpet', '--markup=markdown', '--protected', '--private']
12
- t.stats_options = ['--list-undoc']
12
+ t.files = ["lib/**/*.rb"]
13
+ t.options = ["--markup-provider=redcarpet", "--markup=markdown", "--protected", "--private"]
14
+ t.stats_options = ["--list-undoc"]
13
15
  end
14
16
 
15
17
  namespace :yard do
@@ -17,13 +19,13 @@ namespace :yard do
17
19
  task :server do
18
20
  sh "bundle exec yard server --reload"
19
21
  end
20
-
22
+
21
23
  desc "Generate documentation for GitHub Pages"
22
24
  task :gh_pages do
23
25
  sh "bundle exec yard --output-dir docs"
24
26
  # Create .nojekyll file for GitHub Pages
25
- File.open("docs/.nojekyll", "w") {}
27
+ File.write("docs/.nojekyll", "")
26
28
  end
27
29
  end
28
30
 
29
- task :default => [:spec, :rubocop]
31
+ task default: %i[spec rubocop]
data/attio-rails.gemspec CHANGED
@@ -1,4 +1,6 @@
1
- require_relative 'lib/attio/rails/version'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/attio/rails/version"
2
4
 
3
5
  Gem::Specification.new do |spec|
4
6
  spec.name = "attio-rails"
@@ -6,8 +8,8 @@ Gem::Specification.new do |spec|
6
8
  spec.authors = ["Ernest Sim"]
7
9
  spec.email = ["ernest.codes@gmail.com"]
8
10
 
9
- spec.summary = %q{Rails integration for the Attio API client}
10
- spec.description = %q{Rails-specific features and integrations for the Attio Ruby client, including model concerns and generators}
11
+ spec.summary = "Rails integration for the Attio API client"
12
+ spec.description = "Rails-specific features and integrations for the Attio Ruby client, including model concerns and generators"
11
13
  spec.homepage = "https://github.com/idl3/attio-rails"
12
14
  spec.license = "MIT"
13
15
  spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
@@ -21,26 +23,13 @@ Gem::Specification.new do |spec|
21
23
 
22
24
  # Specify which files should be added to the gem when it is released.
23
25
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
27
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
28
  end
27
29
  spec.bindir = "exe"
28
30
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
31
  spec.require_paths = ["lib"]
30
32
 
31
- spec.add_dependency "attio", "~> 0.1", ">= 0.1.1"
33
+ spec.add_dependency "attio", "~> 0.1", ">= 0.1.3"
32
34
  spec.add_dependency "rails", ">= 6.1", "< 9.0"
33
-
34
- spec.add_development_dependency "yard", "~> 0.9"
35
- spec.add_development_dependency "redcarpet", "~> 3.5"
36
- spec.add_development_dependency "rspec", "~> 3.13"
37
- spec.add_development_dependency "rubocop", "~> 1.50"
38
- spec.add_development_dependency "simplecov", "~> 0.22"
39
- spec.add_development_dependency "simplecov-console", "~> 0.9"
40
- spec.add_development_dependency "webmock", "~> 3.19"
41
- spec.add_development_dependency "pry", "~> 0.14"
42
- spec.add_development_dependency "sqlite3", "~> 1.4"
43
- spec.add_development_dependency "rake", "~> 13.0"
44
- spec.add_development_dependency "bundler-audit", "~> 0.9"
45
- spec.add_development_dependency "danger", "~> 9.4"
46
35
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attio
2
4
  module Rails
3
5
  module Concerns
@@ -5,7 +7,7 @@ module Attio
5
7
  extend ActiveSupport::Concern
6
8
 
7
9
  included do
8
- after_commit :sync_to_attio, on: [:create, :update], if: :should_sync_to_attio?
10
+ after_commit :sync_to_attio, on: %i[create update], if: :should_sync_to_attio?
9
11
  after_commit :remove_from_attio, on: :destroy, if: :should_remove_from_attio?
10
12
 
11
13
  class_attribute :attio_object_type
@@ -18,7 +20,7 @@ module Attio
18
20
  def syncs_with_attio(object_type, mapping = {}, options = {})
19
21
  self.attio_object_type = object_type
20
22
  self.attio_attribute_mapping = mapping
21
- self.attio_sync_conditions = options[:if] || -> { true }
23
+ self.attio_sync_conditions = options[:if]
22
24
  self.attio_identifier_attribute = options[:identifier] || :id
23
25
  end
24
26
 
@@ -46,7 +48,7 @@ module Attio
46
48
  def sync_to_attio_now
47
49
  client = Attio::Rails.client
48
50
  attributes = attio_attributes
49
-
51
+
50
52
  if attio_record_id.present?
51
53
  client.records.update(
52
54
  object: attio_object_type,
@@ -58,7 +60,7 @@ module Attio
58
60
  object: attio_object_type,
59
61
  data: { values: attributes }
60
62
  )
61
- update_column(:attio_record_id, response['data']['id']) if respond_to?(:attio_record_id=)
63
+ update_column(:attio_record_id, response["data"]["id"]) if respond_to?(:attio_record_id=)
62
64
  end
63
65
  end
64
66
 
@@ -80,7 +82,7 @@ module Attio
80
82
 
81
83
  def remove_from_attio_now
82
84
  return unless attio_record_id.present?
83
-
85
+
84
86
  client = Attio::Rails.client
85
87
  client.records.delete(
86
88
  object: attio_object_type,
@@ -92,23 +94,19 @@ module Attio
92
94
  return {} unless attio_attribute_mapping
93
95
 
94
96
  attio_attribute_mapping.each_with_object({}) do |(attio_key, local_key), hash|
95
- value = case local_key
96
- when Proc
97
- local_key.call(self)
98
- when Symbol, String
99
- send(local_key)
100
- else
101
- local_key
102
- end
97
+ value = attribute_value_for(local_key)
103
98
  hash[attio_key] = value unless value.nil?
104
99
  end
105
100
  end
106
101
 
107
102
  def should_sync_to_attio?
108
103
  return false unless Attio::Rails.sync_enabled?
109
- return false unless attio_object_type.present? && attio_attribute_mapping.present?
110
-
104
+ return false unless attio_object_type.present?
105
+ return false unless attio_attribute_mapping.present?
106
+
111
107
  condition = attio_sync_conditions
108
+ return true if condition.nil?
109
+
112
110
  case condition
113
111
  when Proc
114
112
  instance_exec(&condition)
@@ -126,7 +124,20 @@ module Attio
126
124
  def attio_identifier
127
125
  send(attio_identifier_attribute)
128
126
  end
127
+
128
+ private def attribute_value_for(local_key)
129
+ case local_key
130
+ when Proc
131
+ local_key.call(self)
132
+ when Symbol
133
+ send(local_key)
134
+ when String
135
+ respond_to?(local_key) ? send(local_key) : local_key
136
+ else
137
+ local_key
138
+ end
139
+ end
129
140
  end
130
141
  end
131
142
  end
132
- end
143
+ end
@@ -1,12 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attio
2
4
  module Rails
3
5
  class Configuration
4
6
  attr_accessor :api_key, :default_workspace_id, :logger, :sync_enabled, :background_sync
5
7
 
6
8
  def initialize
7
- @api_key = ENV['ATTIO_API_KEY']
9
+ @api_key = ENV.fetch("ATTIO_API_KEY", nil)
8
10
  @default_workspace_id = nil
9
- @logger = defined?(::Rails) ? ::Rails.logger : Logger.new(STDOUT)
11
+ @logger = if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
12
+ ::Rails.logger
13
+ else
14
+ ::Logger.new($stdout)
15
+ end
10
16
  @sync_enabled = true
11
17
  @background_sync = true
12
18
  end
@@ -30,6 +36,7 @@ module Attio
30
36
 
31
37
  def client
32
38
  raise ConfigurationError, "Attio API key not configured" unless configuration.valid?
39
+
33
40
  @client ||= ::Attio.client(api_key: configuration.api_key)
34
41
  end
35
42
 
@@ -52,4 +59,4 @@ module Attio
52
59
 
53
60
  class ConfigurationError < StandardError; end
54
61
  end
55
- end
62
+ end