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



182
183
184
# File 'lib/basecamp/generated/services/base_service.rb', line 182

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



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

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



174
175
176
# File 'lib/basecamp/generated/services/base_service.rb', line 174

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



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

def http
  @client.http
end

#http_delete(path) ⇒ Object (protected)



200
201
202
203
204
# File 'lib/basecamp/generated/services/base_service.rb', line 200

%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:



200
201
202
203
204
# File 'lib/basecamp/generated/services/base_service.rb', line 200

%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:



200
201
202
203
204
# File 'lib/basecamp/generated/services/base_service.rb', line 200

%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)



200
201
202
203
204
# File 'lib/basecamp/generated/services/base_service.rb', line 200

%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:



200
201
202
203
204
# File 'lib/basecamp/generated/services/base_service.rb', line 200

%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



212
213
214
215
216
217
218
# File 'lib/basecamp/generated/services/base_service.rb', line 212

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.



147
148
149
150
151
# File 'lib/basecamp/generated/services/base_service.rb', line 147

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 paginated operation with hooks, preserving the pagination metadata carried by the inner ListEnumerator. Fires on_operation_start eagerly (paginate fetches page 1 inside the yield, so the start hook must precede it); on_operation_end fires via ensure when the first traversal completes, errors, or is cut short by break/take — exactly once, so re-enumerating never emits an on_operation_end without a matching start.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/basecamp/generated/services/base_service.rb', line 67

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
  )
  hooks = @hooks
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  safe_hook { hooks.on_operation_start(info) }

  begin
    enum = yield  # paginate 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

  ended = false
  ListEnumerator.new(enum.meta) do |yielder|
    error = nil
    begin
      enum.each { |item| yielder.yield(item) }
    rescue => e
      error = e
      raise
    ensure
      unless ended
        ended = true
        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
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), and on_operation_end exactly once, when the first traversal of the events Enumerator completes/errors/breaks.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/basecamp/generated/services/base_service.rb', line 106

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]
  ended = false
  wrapped_enum = ListEnumerator.new(inner_enum.meta) do |yielder|
    error = nil
    begin
      inner_enum.each { |item| yielder.yield(item) }
    rescue => e
      error = e
      raise
    ensure
      # Exactly once: on_operation_start fired once at call time, so
      # re-enumerating must not emit unmatched on_operation_end events.
      unless ended
        ended = true
        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

  result.merge(key => wrapped_enum)
end