Class: Basecamp::Services::BaseService

Inherits:
Object
  • Object
show all
Defined in:
lib/basecamp/generated/services/base_service.rb

Overview

Base service class for Basecamp API services.

Provides shared functionality for all service classes including:

  • HTTP method delegation (http_get, http_post, etc.)
  • Path building helpers
  • Pagination support

Examples:

class TodosService < BaseService
  def list(project_id:, todolist_id:)
    paginate(bucket_path(project_id, "/todolists/#{todolist_id}/todos.json"))
  end
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client) ⇒ BaseService

Returns a new instance of BaseService.

Parameters:

  • client (Object)

    the parent client (AccountClient or Client)



26
27
28
29
30
# File 'lib/basecamp/generated/services/base_service.rb', line 26

def initialize(client)
  @client = client
  @account_id = client.
  @hooks = client.hooks
end

Instance Attribute Details

#account_idString (readonly)

Returns the account ID for API requests.

Returns:

  • (String)

    the account ID for API requests



23
24
25
# File 'lib/basecamp/generated/services/base_service.rb', line 23

def 
  @account_id
end

Instance Method Details

#bucket_path(project_id, path) ⇒ String (protected)

Build a bucket (project) path.

Parameters:

  • project_id (Integer, String)

    the project/bucket ID

  • path (String)

    the path suffix

Returns:

  • (String)

    the full bucket path



161
162
163
# File 'lib/basecamp/generated/services/base_service.rb', line 161

def bucket_path(project_id, path)
  "/buckets/#{project_id}#{path}"
end

#compact_params(**kwargs) ⇒ Hash (protected)

Helper to remove nil values from a hash.

Parameters:

  • hash (Hash)

    the input hash

Returns:

  • (Hash)

    hash with nil values removed



140
141
142
# File 'lib/basecamp/generated/services/base_service.rb', line 140

def compact_params(**kwargs)
  kwargs.compact
end

#compact_query_params(**kwargs) ⇒ Hash (protected)

Like compact_params, but also drops empty arrays. For query filters an empty array means "no filter" and must be omitted entirely: Faraday's nested encoder would otherwise emit a bare bucket_ids[], which Rails normalizes into a bogus [0] filter instead of the unfiltered request the caller intended. Not used for request bodies, where an explicit empty array (e.g. clearing a list) can be meaningful.

Parameters:

  • hash (Hash)

    the input hash

Returns:

  • (Hash)

    hash with nil values and empty arrays removed



153
154
155
# File 'lib/basecamp/generated/services/base_service.rb', line 153

def compact_query_params(**kwargs)
  kwargs.reject { |_, value| value.nil? || value == [] }
end

#httpHTTP (protected)

Returns the HTTP client for direct access.

Returns:

  • (HTTP)

    the HTTP client for direct access



133
134
135
# File 'lib/basecamp/generated/services/base_service.rb', line 133

def http
  @client.http
end

#http_delete(path) ⇒ Object (protected)



179
180
181
182
183
# File 'lib/basecamp/generated/services/base_service.rb', line 179

%i[get post put delete post_raw put_raw].each do |method|
  define_method(:"http_#{method}") do |*args, **kwargs, &block|
    @client.public_send(method, *args, **kwargs, &block)
  end
end

#http_get(path, params: {}) ⇒ Object (protected)

See Also:



179
180
181
182
183
# File 'lib/basecamp/generated/services/base_service.rb', line 179

%i[get post put delete post_raw put_raw].each do |method|
  define_method(:"http_#{method}") do |*args, **kwargs, &block|
    @client.public_send(method, *args, **kwargs, &block)
  end
end

#http_post(path, body: nil) ⇒ Object (protected)

See Also:



179
180
181
182
183
# File 'lib/basecamp/generated/services/base_service.rb', line 179

%i[get post put delete post_raw put_raw].each do |method|
  define_method(:"http_#{method}") do |*args, **kwargs, &block|
    @client.public_send(method, *args, **kwargs, &block)
  end
end

#http_post_raw(path, body:, content_type:) ⇒ Object (protected)



179
180
181
182
183
# File 'lib/basecamp/generated/services/base_service.rb', line 179

%i[get post put delete post_raw put_raw].each do |method|
  define_method(:"http_#{method}") do |*args, **kwargs, &block|
    @client.public_send(method, *args, **kwargs, &block)
  end
end

#http_put(path, body: nil) ⇒ Object (protected)

See Also:



179
180
181
182
183
# File 'lib/basecamp/generated/services/base_service.rb', line 179

%i[get post put delete post_raw put_raw].each do |method|
  define_method(:"http_#{method}") do |*args, **kwargs, &block|
    @client.public_send(method, *args, **kwargs, &block)
  end
end

#http_put_multipart(path, io:, filename:, content_type:, field: "file") ⇒ Object (protected)

Upload a file as multipart/form-data.

Parameters:

  • path (String)

    API path

  • io (IO, String)

    file data

  • filename (String)

    display filename

  • content_type (String)

    MIME type

  • field (String) (defaults to: "file")

    form field name



191
192
193
194
195
196
197
# File 'lib/basecamp/generated/services/base_service.rb', line 191

def http_put_multipart(path, io:, filename:, content_type:, field: "file")
  boundary = "BasecampSDK#{SecureRandom.hex(16)}"
  body = build_multipart_body(boundary: boundary, field: field, io: io, \
    filename: filename, content_type: content_type)
  http_put_raw(path, body: body, content_type: "multipart/form-data; boundary=#{boundary}")
  nil
end

#safe_hookObject (protected)

Invoke a hook callback, swallowing exceptions so hooks never break SDK behavior.



126
127
128
129
130
# File 'lib/basecamp/generated/services/base_service.rb', line 126

def safe_hook
  yield
rescue => e
  warn "Basecamp hook error: #{e.class}: #{e.message}"
end

#with_operation(service:, operation:, resource_type: nil, is_mutation: false, project_id: nil, resource_id: nil) { ... } ⇒ Object (protected)

Wraps a service operation with hooks for observability.

Parameters:

  • service (String)

    service name (e.g., "projects")

  • operation (String)

    operation name (e.g., "list")

  • resource_type (String, nil) (defaults to: nil)

    resource type (e.g., "Project")

  • is_mutation (Boolean) (defaults to: false)

    whether this is a write operation

  • project_id (Integer, String, nil) (defaults to: nil)

    project/bucket ID

  • resource_id (Integer, String, nil) (defaults to: nil)

    resource ID

Yields:

  • the operation to execute

Returns:

  • the result of the block



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/basecamp/generated/services/base_service.rb', line 43

def with_operation(service:, operation:, resource_type: nil, is_mutation: false, project_id: nil, resource_id: nil)
  info = OperationInfo.new(
    service: service, operation: operation, resource_type: resource_type,
    is_mutation: is_mutation, project_id: project_id, resource_id: resource_id
  )
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  safe_hook { @hooks.on_operation_start(info) }
  result = yield
  duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
  safe_hook { @hooks.on_operation_end(info, OperationResult.new(duration_ms: duration, error: nil)) }
  result
rescue => e
  duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
  safe_hook { @hooks.on_operation_end(info, OperationResult.new(duration_ms: duration, error: e)) }
  raise
end

#wrap_paginated(service:, operation:, is_mutation: false, project_id: nil, resource_id: nil) ⇒ Object (protected)

Wraps a lazy Enumerator so operation hooks fire around actual iteration, not at enumerator creation time. Hooks fire when the consumer begins iterating (.each, .to_a, .first, etc.) and end fires via ensure when iteration completes, errors, or is cut short by break/take.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/basecamp/generated/services/base_service.rb', line 64

def wrap_paginated(service:, operation:, is_mutation: false, project_id: nil, resource_id: nil)
  info = OperationInfo.new(
    service: service, operation: operation,
    is_mutation: is_mutation, project_id: project_id, resource_id: resource_id
  )
  enum = yield

  hooks = @hooks
  Enumerator.new do |yielder|
    start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    error = nil
    begin
      safe_hook { hooks.on_operation_start(info) }
      enum.each { |item| yielder.yield(item) }
    rescue => e
      error = e
      raise
    ensure
      duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
      safe_hook { hooks.on_operation_end(info, OperationResult.new(duration_ms: duration, error: error)) }
    end
  end
end

#wrap_paginated_wrapped(key:, service:, operation:, is_mutation: false, project_id: nil, resource_id: nil) ⇒ Object (protected)

Wraps a wrapped-paginated operation with hooks. Fires on_operation_start eagerly (before page 1 fetch), on_operation_end when the events Enumerator completes/errors/breaks.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/basecamp/generated/services/base_service.rb', line 91

def wrap_paginated_wrapped(key:, service:, operation:, is_mutation: false, project_id: nil, resource_id: nil)
  info = OperationInfo.new(
    service: service, operation: operation,
    is_mutation: is_mutation, project_id: project_id, resource_id: resource_id
  )
  hooks = @hooks
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  safe_hook { hooks.on_operation_start(info) }

  begin
    result = yield  # paginate_wrapped fetches page 1 here
  rescue => e
    duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
    safe_hook { hooks.on_operation_end(info, OperationResult.new(duration_ms: duration, error: e)) }
    raise
  end

  inner_enum = result[key]
  wrapped_enum = Enumerator.new do |yielder|
    error = nil
    begin
      inner_enum.each { |item| yielder.yield(item) }
    rescue => e
      error = e
      raise
    ensure
      duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
      safe_hook { hooks.on_operation_end(info, OperationResult.new(duration_ms: duration, error: error)) }
    end
  end

  result.merge(key => wrapped_enum)
end