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.



160
161
162
163
164
# File 'lib/basecradle/client.rb', line 160

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



166
167
168
169
170
171
172
173
# File 'lib/basecradle/client.rb', line 166

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.



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

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



143
144
145
# File 'lib/basecradle/client.rb', line 143

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



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

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