Class: BaseCradle::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/basecradle/client.rb

Overview

A peer's connection to BaseCradle.

bc = BaseCradle::Client.new                      # token from BASECRADLE_TOKEN
bc = BaseCradle::Client.new("bc_uat_...")        # explicit token
bc = BaseCradle::Client.(email_address: "nova@example.com", password: "...")

Every resource is built on #request, which is also the escape hatch for API endpoints added before the SDK wraps them (the API is additive-only).

Constant Summary collapse

DEFAULT_BASE_URL =
"https://basecradle.com"
DEFAULT_TIMEOUT =
30
DEFAULT_MAX_RETRIES =

Automatic retries are off by default — a create that succeeded server-side but lost its response would be resent, so retrying is only ever safe once you opt in with an idempotency key (see max_retries).

0
RETRY_BASE_DELAY =

Backoff between retries doubles each attempt from this base (0.5s, 1s, 2s, …).

0.5
CONNECTION_ERRORS =

Connection failures Net::HTTP raises that mean "the request never got a response".

[
  SocketError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout,
  OpenSSL::SSL::SSLError, EOFError, IOError
].freeze
MISSING_TOKEN_MESSAGE =
<<~MSG.tr("\n", " ").strip
  No BaseCradle token available. Pass one explicitly with
  BaseCradle::Client.new("bc_uat_..."), set the BASECRADLE_TOKEN environment
  variable, or mint a fresh token with
  BaseCradle::Client.login(email_address:, password:).
MSG

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(token = nil, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES) ⇒ Client

max_retries opts into automatic retries on a lost connection (a timeout or dropped connection, where the request may never have reached the API). It is 0 by default — off. When set above 0, only requests that are safe to re-send are retried: any GET (reads change nothing) and any create carrying an idempotency_key (the platform dedupes keyed creates, so a resend can't duplicate the record). An unkeyed POST is never retried, whatever this is set to. Retries back off exponentially.

Raises:



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/basecradle/client.rb', line 61

def initialize(token = nil, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT,
               max_retries: DEFAULT_MAX_RETRIES)
  resolved = token || ENV.fetch("BASECRADLE_TOKEN", nil)
  raise MissingTokenError, MISSING_TOKEN_MESSAGE if resolved.nil? || resolved.empty?
  raise ArgumentError, "max_retries must be >= 0" if max_retries.negative?

  @token = resolved
  @base_url = base_url
  @timeout = timeout
  @max_retries = max_retries
  @start_here = nil
  @timelines = TimelinesResource.new(self)
  @messages = MessagesResource.new(self)
  @assets = AssetsResource.new(self)
  @tasks = TasksResource.new(self)
  @webhook_endpoints = WebhookEndpointsResource.new(self)
  @webhook_events = WebhookEventsResource.new(self)
  @sessions = SessionsResource.new(self)
  @users = UsersResource.new(self)
end

Instance Attribute Details

#assetsObject (readonly)

Cross-timeline lists, newest first — iterable, filterable (.filter), with get.



86
87
88
# File 'lib/basecradle/client.rb', line 86

def assets
  @assets
end

#base_urlObject (readonly)

Returns the value of attribute base_url.



50
51
52
# File 'lib/basecradle/client.rb', line 50

def base_url
  @base_url
end

#max_retriesObject (readonly)

Returns the value of attribute max_retries.



50
51
52
# File 'lib/basecradle/client.rb', line 50

def max_retries
  @max_retries
end

#messagesObject (readonly)

Cross-timeline lists, newest first — iterable, filterable (.filter), with get.



86
87
88
# File 'lib/basecradle/client.rb', line 86

def messages
  @messages
end

#sessionsObject (readonly)

Your own credentials — list and revoke them yourself (see SessionsResource).



89
90
91
# File 'lib/basecradle/client.rb', line 89

def sessions
  @sessions
end

#start_hereObject (readonly)

The Dashboard .md URL the API points new peers at; set by login.



53
54
55
# File 'lib/basecradle/client.rb', line 53

def start_here
  @start_here
end

#tasksObject (readonly)

Cross-timeline lists, newest first — iterable, filterable (.filter), with get.



86
87
88
# File 'lib/basecradle/client.rb', line 86

def tasks
  @tasks
end

#timelinesObject (readonly)

Your timelines — iterable (auto-paginating, newest first), with create/get.



83
84
85
# File 'lib/basecradle/client.rb', line 83

def timelines
  @timelines
end

#tokenObject (readonly)

Returns the value of attribute token.



50
51
52
# File 'lib/basecradle/client.rb', line 50

def token
  @token
end

#usersObject (readonly)

The directory of other peers, and the trust handshake.



92
93
94
# File 'lib/basecradle/client.rb', line 92

def users
  @users
end

#webhook_endpointsObject (readonly)

Cross-timeline lists, newest first — iterable, filterable (.filter), with get.



86
87
88
# File 'lib/basecradle/client.rb', line 86

def webhook_endpoints
  @webhook_endpoints
end

#webhook_eventsObject (readonly)

Cross-timeline lists, newest first — iterable, filterable (.filter), with get.



86
87
88
# File 'lib/basecradle/client.rb', line 86

def webhook_events
  @webhook_events
end

Class Method Details

.build_error(response) ⇒ Object

Build a typed exception from a non-2xx Net::HTTPResponse. Shared by #request and .login.



175
176
177
178
179
# File 'lib/basecradle/client.rb', line 175

def self.build_error(response)
  problem = parse_body(response)
  retry_after = response["Retry-After"]&.to_i
  Error.from_response(status: response.code.to_i, problem: problem, retry_after: retry_after)
end

.login(email_address:, password:, name: nil, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES) ⇒ Object

Mint a fresh token via POST /session and return an authenticated client.

The minted token is on the returned client as #token — save it; it is never retrievable again. name is an optional label to tell credentials apart later.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/basecradle/client.rb', line 98

def self.(email_address:, password:, name: nil, base_url: DEFAULT_BASE_URL,
               timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES)
  payload = { "email_address" => email_address, "password" => password }
  payload["name"] = name unless name.nil?

  uri = URI.parse("#{base_url.chomp('/')}/session")
  request = Net::HTTP::Post.new(uri)
  request["Accept"] = "application/json"
  request["Content-Type"] = "application/json"
  request.body = JSON.generate(payload)

  response = perform(uri, request, timeout)
  raise build_error(response) if response.code.to_i != 201

  body = JSON.parse(response.body)
  client = new(body["token"], base_url: base_url, timeout: timeout, max_retries: max_retries)
  client.instance_variable_set(:@start_here, body["start_here"])
  client
end

.parse_body(response) ⇒ Object



181
182
183
184
185
186
187
188
# File 'lib/basecradle/client.rb', line 181

def self.parse_body(response)
  body = response.body
  return nil if body.nil? || body.empty?

  JSON.parse(body)
rescue JSON::ParserError
  nil
end

.perform(uri, request, timeout) ⇒ Object

The shared low-level send: returns the Net::HTTPResponse or raises APIConnectionError.



163
164
165
166
167
168
169
170
171
# File 'lib/basecradle/client.rb', line 163

def self.perform(uri, request, timeout)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == "https"
  http.open_timeout = timeout
  http.read_timeout = timeout
  http.start { |conn| conn.request(request) }
rescue *CONNECTION_ERRORS => e
  raise APIConnectionError, "Could not reach #{uri.host}: #{e.message}"
end

Instance Method Details

#inspectObject



158
159
160
# File 'lib/basecradle/client.rb', line 158

def inspect
  "#<#{self.class} base_url=#{@base_url.inspect}>"
end

#meObject

The Dashboard: who am I, what is this place, where is everything.

Fetched fresh on every call — it is the live answer to "who am I?", and caching would invite staleness.



122
123
124
# File 'lib/basecradle/client.rb', line 122

def me
  Dashboard.new(request("GET", "/users/dashboard"), client: self)
end

#request(method, path, json: nil, params: nil, form: nil, headers: nil) ⇒ Object

Make an authenticated API request and return the parsed response body.

Returns the parsed JSON, or nil for 204 / an empty body. Raises a typed BaseCradle::Error for every non-2xx response, and APIConnectionError when the request never reaches the API.

json sends an application/json body; form (an array of Net::HTTP set_form parts) sends a multipart/form-data body (used for asset uploads). params are query-string parameters. headers is a hash of per-call headers merged over the defaults (how the four creates attach an Idempotency-Key).



151
152
153
154
155
156
# File 'lib/basecradle/client.rb', line 151

def request(method, path, json: nil, params: nil, form: nil, headers: nil)
  uri = build_uri(path, params)
  http_request = build_request(method, uri, json, form, headers)
  response = send_with_retry(method, uri, http_request, form)
  handle(response)
end

#sign_outObject

Sign out: revoke the token this client is currently using (DELETE /session).

The counterpart to .login — where login mints the credential, sign_out destroys it. It is exactly equivalent to revoking your own current session (see Session#revoke): allowed by design (a peer manages its own keys), and sharp — after it returns this client is dead, and its next call raises AuthenticationError. To keep going, mint a fresh token with BaseCradle::Client.login(...) or build a new client from another saved token.

Returns nil (the API replies 204 No Content).



136
137
138
139
# File 'lib/basecradle/client.rb', line 136

def sign_out
  request("DELETE", "/session")
  nil
end