smplkit 3.0.96 → 3.0.97

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.
@@ -99,11 +99,21 @@ module Smplkit
99
99
  end
100
100
 
101
101
  # Read-only view of constrained values. +nil+ means unconstrained.
102
+ #
103
+ # Mutate via +add_value+ / +remove_value+ / +clear_values+.
104
+ #
105
+ # @return [Array<FlagValue>, nil] the constrained values, or +nil+ when
106
+ # the flag is unconstrained.
102
107
  def values
103
108
  @values&.dup
104
109
  end
105
110
 
106
111
  # Read-only view of per-environment configuration.
112
+ #
113
+ # Mutate via +add_rule+ / +enable_rules+ / +disable_rules+ /
114
+ # +set_default+ (with +environment:+) / +clear_rules+.
115
+ #
116
+ # @return [Hash{String => FlagEnvironment}] the per-environment configuration.
107
117
  def environments
108
118
  @environments.dup
109
119
  end
@@ -111,8 +121,10 @@ module Smplkit
111
121
  # Persist this flag to the server.
112
122
  #
113
123
  # Creates a new flag if unsaved, or updates the existing one. Requires a
114
- # management client (i.e. the flag was constructed via +mgmt.flags.new_*+
115
- # or returned from +mgmt.flags.get/list+).
124
+ # client (i.e. the flag was constructed via +client.flags.new_*+ or
125
+ # returned from +client.flags.get+ / +client.flags.list+).
126
+ #
127
+ # @return [self] this flag, with server-assigned fields applied.
116
128
  def save
117
129
  raise "Flag was constructed without a client; cannot save" if @client.nil?
118
130
 
@@ -127,6 +139,10 @@ module Smplkit
127
139
  end
128
140
  alias save! save
129
141
 
142
+ # Delete this flag from the server.
143
+ #
144
+ # @return [void]
145
+ # @raise [Smplkit::NotFoundError] no flag with this id exists for the account.
130
146
  def delete
131
147
  raise "Flag was constructed without a client or id; cannot delete" if @client.nil? || @id.nil?
132
148
 
@@ -138,6 +154,12 @@ module Smplkit
138
154
  #
139
155
  # The +built_rule+ Hash must include an +"environment"+ key.
140
156
  # Call +save+ to persist.
157
+ #
158
+ # @param built_rule [Hash] the Hash produced by
159
+ # +Smplkit::Rule.new(..., environment: ...).when(...).serve(...)+; must
160
+ # include an +"environment"+ key naming the target environment.
161
+ # @return [self] this flag, so calls can be chained.
162
+ # @raise [ArgumentError] the +built_rule+ Hash has no +"environment"+ key.
141
163
  def add_rule(built_rule)
142
164
  env_key = built_rule["environment"]
143
165
  if env_key.nil?
@@ -156,16 +178,37 @@ module Smplkit
156
178
  self
157
179
  end
158
180
 
181
+ # Enable rule evaluation. Call +save+ to persist.
182
+ #
183
+ # @param environment [String, nil] name of the environment to enable; when
184
+ # +nil+ (the default), enables rules in every environment configured on
185
+ # this flag.
186
+ # @return [self] this flag, so calls can be chained.
159
187
  def enable_rules(environment: nil)
160
188
  scoped(environment) { |k| @environments[k] = (@environments[k] || FlagEnvironment.new).with(enabled: true) }
161
189
  self
162
190
  end
163
191
 
192
+ # Disable rule evaluation (kill switch). Call +save+ to persist.
193
+ #
194
+ # When disabled, +get+ skips rules and returns the env-specific default
195
+ # (or the flag's base default).
196
+ #
197
+ # @param environment [String, nil] name of the environment to disable; when
198
+ # +nil+ (the default), disables rules in every environment configured on
199
+ # this flag.
200
+ # @return [self] this flag, so calls can be chained.
164
201
  def disable_rules(environment: nil)
165
202
  scoped(environment) { |k| @environments[k] = (@environments[k] || FlagEnvironment.new).with(enabled: false) }
166
203
  self
167
204
  end
168
205
 
206
+ # Remove rules. Call +save+ to persist.
207
+ #
208
+ # @param environment [String, nil] name of the environment whose rules to
209
+ # remove; when +nil+ (the default), removes rules from every environment
210
+ # configured on this flag.
211
+ # @return [self] this flag, so calls can be chained.
169
212
  def clear_rules(environment: nil)
170
213
  scoped(environment) do |k|
171
214
  @environments[k] = (@environments[k] || FlagEnvironment.new).with(rules: [].freeze)
@@ -173,22 +216,44 @@ module Smplkit
173
216
  self
174
217
  end
175
218
 
219
+ # Set the flag's per-environment default served value. Call +save+ to persist.
220
+ #
221
+ # @param value [Object] the default value to serve when no rule matches.
222
+ # @param environment [String] name of the environment whose default to set.
223
+ # @return [self] this flag, so calls can be chained.
176
224
  def set_default(value, environment:)
177
225
  @environments[environment] = (@environments[environment] || FlagEnvironment.new).with(default: value)
178
226
  self
179
227
  end
180
228
 
229
+ # Clear the per-environment default override on +environment+.
230
+ #
231
+ # After clearing, the environment falls back to the flag's base default
232
+ # when no rule matches. Call +save+ to persist.
233
+ #
234
+ # @param environment [String] name of the environment whose default
235
+ # override to clear.
236
+ # @return [self] this flag, so calls can be chained.
181
237
  def clear_default(environment:)
182
238
  @environments[environment] = (@environments[environment] || FlagEnvironment.new).with(default: nil)
183
239
  self
184
240
  end
185
241
 
242
+ # Append a constrained value to the flag's values list. Call +save+ to persist.
243
+ #
244
+ # @param flag_value [FlagValue] the value entry to allow the flag to serve.
245
+ # @return [self] this flag, so calls can be chained.
186
246
  def add_value(flag_value)
187
247
  @values ||= []
188
248
  @values << flag_value
189
249
  self
190
250
  end
191
251
 
252
+ # Remove the first values entry whose +name+ matches.
253
+ #
254
+ # @param name [String] the value-entry name to remove; the first match is
255
+ # removed and others are left in place.
256
+ # @return [self] this flag, so calls can be chained.
192
257
  def remove_value(name)
193
258
  return self unless @values
194
259
 
@@ -196,6 +261,9 @@ module Smplkit
196
261
  self
197
262
  end
198
263
 
264
+ # Clear the constrained values list (unconstrained). Call +save+ to persist.
265
+ #
266
+ # @return [self] this flag, so calls can be chained.
199
267
  def clear_values
200
268
  @values = []
201
269
  self
@@ -225,28 +293,62 @@ module Smplkit
225
293
  end
226
294
 
227
295
  class BooleanFlag < Flag
296
+ # Evaluate this flag and return its current boolean value.
297
+ #
298
+ # @param context [Array<Smplkit::Context>, nil] optional entities to
299
+ # evaluate targeting rules against; when omitted the ambient request
300
+ # context (if any) is used.
301
+ # @return [Boolean] the evaluated boolean value, or this flag's default
302
+ # when no environment override or rule applies (or the evaluated value
303
+ # is not a boolean).
228
304
  def get(context: nil)
229
- raw = @client._evaluate_handle(@id, @default, context)
230
- !!raw
305
+ value = @client._evaluate_handle(@id, @default, context)
306
+ [true, false].include?(value) ? value : @default
231
307
  end
232
308
  end
233
309
 
234
310
  class StringFlag < Flag
311
+ # Evaluate this flag and return its current string value.
312
+ #
313
+ # @param context [Array<Smplkit::Context>, nil] optional entities to
314
+ # evaluate targeting rules against; when omitted the ambient request
315
+ # context (if any) is used.
316
+ # @return [String] the evaluated string value, or this flag's default
317
+ # when no environment override or rule applies (or the evaluated value
318
+ # is not a String).
235
319
  def get(context: nil)
236
- raw = @client._evaluate_handle(@id, @default, context)
237
- raw.to_s
320
+ value = @client._evaluate_handle(@id, @default, context)
321
+ value.is_a?(String) ? value : @default
238
322
  end
239
323
  end
240
324
 
241
325
  class NumberFlag < Flag
326
+ # Evaluate this flag and return its current numeric value.
327
+ #
328
+ # @param context [Array<Smplkit::Context>, nil] optional entities to
329
+ # evaluate targeting rules against; when omitted the ambient request
330
+ # context (if any) is used.
331
+ # @return [Numeric] the evaluated numeric value, or this flag's default
332
+ # when no environment override or rule applies (or the evaluated value
333
+ # is not a Numeric).
242
334
  def get(context: nil)
243
- @client._evaluate_handle(@id, @default, context)
335
+ value = @client._evaluate_handle(@id, @default, context)
336
+ value.is_a?(Numeric) ? value : @default
244
337
  end
245
338
  end
246
339
 
247
340
  class JsonFlag < Flag
341
+ # Evaluate this flag and return its current JSON value.
342
+ #
343
+ # @param context [Array<Smplkit::Context>, nil] optional entities to
344
+ # evaluate targeting rules against; when omitted the ambient request
345
+ # context (if any) is used.
346
+ # @return [Hash] the evaluated JSON object, or this flag's default when no
347
+ # environment override or rule applies (or the evaluated value is not a
348
+ # Hash).
248
349
  def get(context: nil)
249
- @client._evaluate_handle(@id, @default, context)
350
+ value = @client._evaluate_handle(@id, @default, context)
351
+ value.is_a?(Hash) ? value : @default
250
352
  end
251
353
  end
252
354
  end
@@ -67,8 +67,8 @@ module Smplkit
67
67
  @attributes = stringify_keys(new_attrs || {})
68
68
  end
69
69
 
70
- # Internal: associate a management client with this context so save/delete
71
- # can route through it.
70
+ # @api private — associate a client with this context so save/delete can
71
+ # route through it.
72
72
  def _bind_client(client)
73
73
  @client = client
74
74
  self
@@ -23,14 +23,17 @@ module Smplkit
23
23
  @api = api
24
24
  end
25
25
 
26
- # List runs for the authenticated account, newest first. Cursor paginated
27
- # (ADR-014): pass +page_size+ and the +after+ cursor from the prior page.
28
- # Pass +job+ to scope to a single job's history.
26
+ # List past runs, most recent first. Cursor paginated: pass +page_size+
27
+ # and the +after+ cursor from the prior page. Pass +job+ to scope to a
28
+ # single job's history.
29
29
  #
30
- # @param job [String, nil] Filter to a single job's run history, by job id.
31
- # @param page_size [Integer, nil] Items per page (cursor pagination).
32
- # @param after [String, nil] Opaque cursor token from a prior page.
33
- # @return [Array<Smplkit::Jobs::Run>]
30
+ # @param job [String, nil] Return only runs of the job with this id.
31
+ # +nil+ lists runs across all jobs in the account.
32
+ # @param page_size [Integer, nil] Maximum number of runs to return in this
33
+ # page. +nil+ uses the server default.
34
+ # @param after [String, nil] Opaque cursor from a previous page; returns
35
+ # the runs that follow it. +nil+ starts from the first page.
36
+ # @return [Array<Smplkit::Jobs::Run>] The runs in this page.
34
37
  def list(job: nil, page_size: nil, after: nil)
35
38
  opts = {}
36
39
  opts[:filter_job] = job unless job.nil?
@@ -41,28 +44,30 @@ module Smplkit
41
44
  (resp.data || []).map { |r| Run.from_resource(r) }
42
45
  end
43
46
 
44
- # Fetch a single run by id.
47
+ # Fetch a single run by its id.
45
48
  #
46
- # @param run_id [String]
47
- # @return [Smplkit::Jobs::Run]
49
+ # @param run_id [String] Identifier of the run to fetch.
50
+ # @return [Smplkit::Jobs::Run] The matching run.
51
+ # @raise [Smplkit::NotFoundError] when no run with this id exists.
48
52
  def get(run_id)
49
53
  resp = Jobs.call_api { @api.get_run(run_id) }
50
54
  Run.from_resource(resp.data)
51
55
  end
52
56
 
53
- # Cancel a pending run.
57
+ # Cancel a run that has not finished yet.
54
58
  #
55
- # @param run_id [String]
56
- # @return [Smplkit::Jobs::Run]
59
+ # @param run_id [String] Identifier of the run to cancel.
60
+ # @return [Smplkit::Jobs::Run] The updated run reflecting the cancellation.
57
61
  def cancel(run_id)
58
62
  resp = Jobs.call_api { @api.cancel_run(run_id) }
59
63
  Run.from_resource(resp.data)
60
64
  end
61
65
 
62
- # Re-run a prior run, spawning a new +RERUN+ run.
66
+ # Start a new run that repeats a previous one.
63
67
  #
64
- # @param run_id [String]
65
- # @return [Smplkit::Jobs::Run]
68
+ # @param run_id [String] Identifier of the run to repeat.
69
+ # @return [Smplkit::Jobs::Run] The new run, with +rerun_of+ set to the
70
+ # source +run_id+.
66
71
  def rerun(run_id)
67
72
  resp = Jobs.call_api { @api.rerun_run(run_id) }
68
73
  Run.from_resource(resp.data)
@@ -88,13 +93,13 @@ module Smplkit
88
93
  # exactly like the top-level clients do. Smpl Jobs is JSON:API, so the
89
94
  # transport carries the +application/vnd.api+json+ Accept header.
90
95
  def self.jobs_transport(api_key:, profile:, base_domain:, scheme:, debug:, extra_headers:)
91
- cfg = ConfigResolution.resolve_management_config(
96
+ cfg = ConfigResolution.resolve_client_config(
92
97
  profile: profile, api_key: api_key, base_domain: base_domain, scheme: scheme, debug: debug
93
98
  )
94
99
  merged = {}
95
100
  merged.merge!(cfg.extra_headers || {})
96
101
  merged.merge!(extra_headers || {})
97
- tcfg = ConfigResolution::ResolvedManagementConfig.new(
102
+ tcfg = ConfigResolution::ResolvedClientConfig.new(
98
103
  api_key: cfg.api_key, base_domain: cfg.base_domain, scheme: cfg.scheme,
99
104
  debug: cfg.debug, extra_headers: merged
100
105
  )
@@ -111,6 +116,17 @@ module Smplkit
111
116
  # @return [RunsClient] Run history and run actions (+client.jobs.runs+).
112
117
  attr_reader :runs
113
118
 
119
+ # @param api_key [String, nil] API key. When omitted, resolved from
120
+ # +SMPLKIT_API_KEY+ or +~/.smplkit+.
121
+ # @param profile [String, nil] Named +~/.smplkit+ profile section.
122
+ # @param base_domain [String, nil] Base domain for API requests
123
+ # (default +"smplkit.com"+).
124
+ # @param scheme [String, nil] URL scheme (default +"https"+).
125
+ # @param debug [Boolean, nil] Enable SDK debug logging.
126
+ # @param extra_headers [Hash, nil] Extra headers attached to every request.
127
+ # @param auth_client [Object, nil] Internal — a pre-built transport
128
+ # supplied by a top-level client so the jobs surface shares one
129
+ # connection pool. Not for direct use.
114
130
  def initialize(api_key = nil, profile: nil, base_domain: nil, scheme: nil,
115
131
  debug: nil, extra_headers: nil, auth_client: nil)
116
132
  auth = auth_client || Jobs.jobs_transport(
@@ -194,19 +210,24 @@ module Smplkit
194
210
  Job.from_resource(resp.data, client: self)
195
211
  end
196
212
 
197
- # Soft-delete a job.
213
+ # Delete a job by its id.
198
214
  #
199
- # @param id [String]
215
+ # @param id [String] Identifier of the job to delete.
200
216
  # @return [nil]
201
217
  def delete(id)
202
218
  Jobs.call_api { @api.delete_job(id) }
203
219
  nil
204
220
  end
205
221
 
206
- # Trigger one immediate +MANUAL+ run of the job.
222
+ # Trigger one immediate, manual run of a job, ignoring its schedule.
207
223
  #
208
- # @param id [String]
209
- # @return [Smplkit::Jobs::Run]
224
+ # This starts an ad-hoc run right now in addition to any scheduled runs;
225
+ # it does not alter the job's schedule. To read or act on existing runs,
226
+ # use +client.jobs.runs+.
227
+ #
228
+ # @param id [String] Identifier of the job to run.
229
+ # @return [Smplkit::Jobs::Run] The run that was started, with +trigger+
230
+ # set to +MANUAL+.
210
231
  def run(id)
211
232
  resp = Jobs.call_api { @api.run_job_now(id) }
212
233
  Run.from_resource(resp.data)
@@ -233,9 +254,9 @@ module Smplkit
233
254
  # @api private — Full-replace PUT for an existing job. Called by
234
255
  # {Smplkit::Jobs::Job#save} on instances with +created_at+.
235
256
  #
236
- # Header values must be re-supplied as plaintext; the GET path redacts
237
- # them, so a PUT body containing the redacted placeholder would persist
238
- # that literal. Track real header values client-side and round-trip them.
257
+ # Header values come back in plaintext on the GET path, so a fetched job
258
+ # round-trips through this full-replace PUT with its header values intact
259
+ # no need to re-enter secrets.
239
260
  def _update_job(job)
240
261
  raise ArgumentError, "cannot update a Job with no id" if job.id.nil?
241
262
 
@@ -1,15 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Smplkit
4
- # Smpl Jobs surface — exposed through +mgmt.jobs.*+.
4
+ # Smpl Jobs surface — exposed through +client.jobs.*+.
5
5
  #
6
6
  # Unlike Config/Flags/Logging, Jobs has no live "phone-home" agent — no
7
- # environment registration, no WebSocket — so its entire surface lives on
8
- # the management client rather than a runtime client. A {Job} is an active
9
- # record: build it with +mgmt.jobs.new(...)+, set fields, and call {Job#save}
10
- # (create when new, full-replace update when it already exists) or
11
- # {Job#delete}. Runs are read-only views; run actions live on
12
- # +mgmt.jobs.runs+.
7
+ # environment registration, no WebSocket — so its entire surface lives on a
8
+ # single client. A {Job} is an active record: build it with
9
+ # +client.jobs.new(...)+, set fields, and call {Job#save} (create when new,
10
+ # full-replace update when it already exists) or {Job#delete}. Runs are
11
+ # read-only views; run actions live on +client.jobs.runs+.
13
12
  module Jobs
14
13
  # Wrap a generated-jobs-API call and translate +ApiError+ into the
15
14
  # +Smplkit::Error+ hierarchy. Connection-level failures (no response
@@ -29,7 +28,7 @@ module Smplkit
29
28
  raise
30
29
  end
31
30
 
32
- # HTTP verb a job uses when it fires (ADR-049).
31
+ # HTTP verb a job uses when it fires.
33
32
  #
34
33
  # Mirrors the jobs spec's method enum so a job's
35
34
  # +configuration.method+ field is constrained to a known value instead
@@ -64,8 +63,8 @@ module Smplkit
64
63
  # @!attribute [rw] name
65
64
  # @return [String] Header name (e.g. +"Authorization"+, +"Content-Type"+).
66
65
  # @!attribute [rw] value
67
- # @return [String] Header value, plaintext on writes. The jobs service
68
- # encrypts values at rest; reads return them redacted.
66
+ # @return [String] Header value. Returned in plaintext on reads, so a
67
+ # get-mutate-put round-trip preserves it without re-entering secrets.
69
68
  HttpHeader = Struct.new(:name, :value, keyword_init: true)
70
69
 
71
70
  # The HTTP request a job performs when it fires (the +http+ configuration).
@@ -79,7 +78,8 @@ module Smplkit
79
78
  # @return [String] Destination URL the job requests on each run.
80
79
  # @!attribute [rw] headers
81
80
  # @return [Array<HttpHeader>] Headers attached to every request. Values
82
- # are redacted on reads.
81
+ # often carry credentials and are returned in plaintext on reads, so a
82
+ # get-mutate-put round-trip preserves them without re-entering secrets.
83
83
  # @!attribute [rw] body
84
84
  # @return [String, nil] Request body sent on each run. +nil+ (the default)
85
85
  # sends an empty body, suitable for a connectivity ping. Sent verbatim —
@@ -118,6 +118,11 @@ module Smplkit
118
118
  )
119
119
  end
120
120
 
121
+ # @api private — Convert an {HttpConfig} (or a Hash with the same keys)
122
+ # into the generated wire model the jobs service expects.
123
+ #
124
+ # @param src [HttpConfig, Hash] The HTTP configuration to serialize.
125
+ # @return [SmplkitGeneratedClient::Jobs::JobHttpConfiguration] The wire model.
121
126
  def self.to_wire(src)
122
127
  h = src.is_a?(Hash) ? new(**src) : src
123
128
  SmplkitGeneratedClient::Jobs::JobHttpConfiguration.new(
@@ -139,6 +144,13 @@ module Smplkit
139
144
  )
140
145
  end
141
146
 
147
+ # @api private — Build an {HttpConfig} from the generated wire model
148
+ # returned by the jobs service. Header values arrive in plaintext, so a
149
+ # round-trip back through {to_wire} preserves them.
150
+ #
151
+ # @param src [SmplkitGeneratedClient::Jobs::JobHttpConfiguration, nil] The
152
+ # wire model, or +nil+ for an empty configuration.
153
+ # @return [HttpConfig] The wrapper-side configuration.
142
154
  def self.from_wire(src)
143
155
  return new if src.nil?
144
156
 
@@ -166,11 +178,11 @@ module Smplkit
166
178
 
167
179
  # A scheduled unit of work: an HTTP request run on a schedule.
168
180
  #
169
- # Active-record style: instantiate via +mgmt.jobs.new(...)+, mutate fields
181
+ # Active-record style: instantiate via +client.jobs.new(...)+, mutate fields
170
182
  # directly, and call {#save} to persist or {#delete} to remove. Header
171
- # values in +configuration.headers+ are returned redacted on reads
172
- # re-supply the real values before calling {#save}; the SDK does not cache
173
- # them client-side.
183
+ # values in +configuration.headers+ are returned in plaintext on reads, so
184
+ # fetching a job, mutating it, and calling {#save} preserves its header
185
+ # values without re-entering secrets.
174
186
  class Job
175
187
  # @return [String] Caller-supplied unique identifier for the job (the
176
188
  # resource +id+). Unique within the account and immutable; the service
@@ -214,7 +226,8 @@ module Smplkit
214
226
  # @return [String, nil] ISO-8601 timestamp of the most recent mutation.
215
227
  attr_accessor :updated_at
216
228
 
217
- # @return [String, nil] Soft-delete timestamp. +nil+ for live jobs.
229
+ # @return [String, nil] Timestamp when the job was deleted; +nil+ for live
230
+ # jobs.
218
231
  attr_accessor :deleted_at
219
232
 
220
233
  # @return [Integer, nil] Monotonic version counter, bumped on every
@@ -258,7 +271,7 @@ module Smplkit
258
271
  end
259
272
  alias save! save
260
273
 
261
- # Soft-delete this job on the server.
274
+ # Delete this job on the server.
262
275
  #
263
276
  # @return [nil]
264
277
  def delete
@@ -285,6 +298,12 @@ module Smplkit
285
298
  @version = other.version
286
299
  end
287
300
 
301
+ # @api private — Build a {Job} from a JSON:API resource returned by the
302
+ # jobs service, binding it to +client+ so {#save} and {#delete} work.
303
+ #
304
+ # @param resource [Object] The JSON:API resource (id + attributes).
305
+ # @param client [JobsClient, nil] Client to bind the job to.
306
+ # @return [Job] The hydrated job.
288
307
  def self.from_resource(resource, client: nil)
289
308
  a = resource.attributes
290
309
  new(
@@ -310,7 +329,7 @@ module Smplkit
310
329
  #
311
330
  # Runs are created and mutated by the jobs service, not by clients; clients
312
331
  # influence runs only through the +run+ / +cancel+ / +rerun+ actions on
313
- # +mgmt.jobs+.
332
+ # +client.jobs+.
314
333
  #
315
334
  # @!attribute [rw] id
316
335
  # @return [String] Server-assigned UUID for this run.
@@ -353,6 +372,11 @@ module Smplkit
353
372
  :total_duration_ms, :failure_reason, :error, :request, :result, :created_at,
354
373
  keyword_init: true
355
374
  ) do
375
+ # @api private — Build a {Run} from a JSON:API resource returned by the
376
+ # jobs service.
377
+ #
378
+ # @param resource [Object] The JSON:API resource (id + attributes).
379
+ # @return [Run] The hydrated run.
356
380
  def self.from_resource(resource)
357
381
  a = resource.attributes
358
382
  new(
@@ -393,6 +417,11 @@ module Smplkit
393
417
  :period, :runs_used, :runs_included, :active_jobs, :active_jobs_limit,
394
418
  keyword_init: true
395
419
  ) do
420
+ # @api private — Build a {Usage} snapshot from a JSON:API resource
421
+ # returned by the jobs service.
422
+ #
423
+ # @param resource [Object] The JSON:API resource (attributes).
424
+ # @return [Usage] The usage snapshot.
396
425
  def self.from_resource(resource)
397
426
  a = resource.attributes
398
427
  new(