smplkit 3.0.122 → 3.0.123

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.
Files changed (23) hide show
  1. checksums.yaml +4 -4
  2. data/lib/smplkit/_generated/audit/lib/smplkit_audit_client/models/forwarder.rb +5 -17
  3. data/lib/smplkit/_generated/audit/lib/smplkit_audit_client/models/{http_configuration.rb → forwarder_http_configuration.rb} +7 -7
  4. data/lib/smplkit/_generated/audit/lib/smplkit_audit_client/models/test_forwarder_request.rb +3 -3
  5. data/lib/smplkit/_generated/audit/lib/smplkit_audit_client.rb +1 -3
  6. data/lib/smplkit/_generated/audit/spec/models/{http_configuration_spec.rb → forwarder_http_configuration_spec.rb} +6 -6
  7. data/lib/smplkit/_generated/audit/spec/models/forwarder_spec.rb +0 -6
  8. data/lib/smplkit/_generated/jobs/lib/smplkit_jobs_client/models/job.rb +2 -2
  9. data/lib/smplkit/_generated/jobs/lib/smplkit_jobs_client/models/job_http_configuration.rb +4 -4
  10. data/lib/smplkit/_generated/jobs/lib/smplkit_jobs_client.rb +0 -2
  11. data/lib/smplkit/audit/forwarders.rb +18 -24
  12. data/lib/smplkit/audit/models.rb +175 -103
  13. data/lib/smplkit/jobs/client.rb +21 -26
  14. data/lib/smplkit/jobs/models.rb +233 -273
  15. metadata +3 -11
  16. data/lib/smplkit/_generated/audit/lib/smplkit_audit_client/models/forwarder_environment.rb +0 -162
  17. data/lib/smplkit/_generated/audit/lib/smplkit_audit_client/models/http_header.rb +0 -220
  18. data/lib/smplkit/_generated/audit/spec/models/forwarder_environment_spec.rb +0 -42
  19. data/lib/smplkit/_generated/audit/spec/models/http_header_spec.rb +0 -42
  20. data/lib/smplkit/_generated/jobs/lib/smplkit_jobs_client/models/http_header.rb +0 -220
  21. data/lib/smplkit/_generated/jobs/lib/smplkit_jobs_client/models/job_environment.rb +0 -206
  22. data/lib/smplkit/_generated/jobs/spec/models/http_header_spec.rb +0 -42
  23. data/lib/smplkit/_generated/jobs/spec/models/job_environment_spec.rb +0 -66
@@ -75,31 +75,19 @@ module Smplkit
75
75
 
76
76
  # Coerce a caller's +environments+ map to {JobEnvironment} instances.
77
77
  #
78
- # Accepts either {JobEnvironment} values or plain hashes
79
- # (+{ enabled: true, schedule: "0 3 * * *", configuration: HttpConfig.new(...) }+)
80
- # so callers can use the lightweight hash form without importing the model. A
81
- # dict-form +configuration+ override is coerced to an {HttpConfig} so it
82
- # serializes on save; optional +schedule+ cron, +timezone+, and
83
- # +retry_policy+ overrides pass through.
78
+ # Accepts either {JobEnvironment} values or plain hashes of its constructor
79
+ # kwargs (+{ enabled: true, schedule: "0 3 * * *", url: "https://prod/warm",
80
+ # headers: { "Authorization" => "Bearer prod" } }+) so callers can use the
81
+ # lightweight hash form without importing the model. Each hash is splatted
82
+ # into {JobEnvironment.new}; both symbol- and string-keyed hashes work.
84
83
  #
85
84
  # @api private
86
85
  def self.normalize_environments(environments)
87
86
  return {} if environments.nil? || environments.empty?
88
87
 
89
88
  environments.each_with_object({}) do |(env_key, value), out|
90
- out[env_key.to_s] = if value.is_a?(JobEnvironment)
91
- value
92
- else
93
- cfg = value[:configuration] || value["configuration"]
94
- cfg = HttpConfig.new(**cfg) if cfg.is_a?(Hash)
95
- JobEnvironment.new(
96
- enabled: value[:enabled] || value["enabled"] || false,
97
- schedule: value[:schedule] || value["schedule"],
98
- timezone: value[:timezone] || value["timezone"],
99
- retry_policy: value[:retry_policy] || value["retry_policy"],
100
- configuration: cfg
101
- )
102
- end
89
+ out[env_key.to_s] =
90
+ value.is_a?(JobEnvironment) ? value : JobEnvironment.new(**value.transform_keys(&:to_sym))
103
91
  end
104
92
  end
105
93
 
@@ -174,15 +162,6 @@ module Smplkit
174
162
  end
175
163
  end
176
164
 
177
- # A single name/value HTTP header on the request a job performs.
178
- #
179
- # @!attribute [rw] name
180
- # @return [String] Header name (e.g. +"Authorization"+, +"Content-Type"+).
181
- # @!attribute [rw] value
182
- # @return [String] Header value. Returned in plaintext on reads, so a
183
- # get-mutate-put round-trip preserves it without re-entering secrets.
184
- HttpHeader = Struct.new(:name, :value, keyword_init: true)
185
-
186
165
  # The HTTP request a job performs when it fires (the +http+ configuration).
187
166
  #
188
167
  # Extends the shared forwarder shape with the two fields a scheduled job
@@ -193,9 +172,12 @@ module Smplkit
193
172
  # @!attribute [rw] url
194
173
  # @return [String] Destination URL the job requests on each run.
195
174
  # @!attribute [rw] headers
196
- # @return [Array<HttpHeader>] Headers attached to every request. Values
197
- # often carry credentials and are returned in plaintext on reads, so a
198
- # get-mutate-put round-trip preserves them without re-entering secrets.
175
+ # @return [Hash{String => String}] Headers attached to every request, as a
176
+ # name→value object (e.g. +{ "Authorization" => "Bearer s3cr3t" }+). Use
177
+ # {#set_header} / {#get_header} to read and write individual headers.
178
+ # Values often carry credentials and are returned in plaintext on reads,
179
+ # so a get-mutate-put round-trip preserves them without re-entering
180
+ # secrets.
199
181
  # @!attribute [rw] body
200
182
  # @return [String, nil] Request body sent on each run. +nil+ (the default)
201
183
  # sends an empty body, suitable for a connectivity ping. Sent verbatim —
@@ -229,13 +211,31 @@ module Smplkit
229
211
  success_status: "2xx", timeout: 30, tls_verify: true, ca_cert: nil
230
212
  )
231
213
  super(
232
- method: HttpMethod.coerce(method), url: url, headers: headers || [], body: body,
233
- success_status: success_status, timeout: timeout, tls_verify: tls_verify, ca_cert: ca_cert
214
+ method: HttpMethod.coerce(method), url: url, headers: (headers || {}).transform_keys(&:to_s),
215
+ body: body, success_status: success_status, timeout: timeout, tls_verify: tls_verify, ca_cert: ca_cert
234
216
  )
235
217
  end
236
218
 
219
+ # Set (or replace) a single request header by name.
220
+ #
221
+ # @param name [String] Header name.
222
+ # @param value [String] Header value.
223
+ def set_header(name, value)
224
+ self.headers ||= {}
225
+ headers[name.to_s] = value
226
+ end
227
+
228
+ # The value of header +name+, or +nil+ when it is not set.
229
+ #
230
+ # @param name [String] Header name.
231
+ # @return [String, nil]
232
+ def get_header(name)
233
+ (headers || {})[name.to_s]
234
+ end
235
+
237
236
  # @api private — Convert an {HttpConfig} (or a Hash with the same keys)
238
- # into the generated wire model the jobs service expects.
237
+ # into the generated wire model the jobs service expects. Headers travel
238
+ # as the generated name→value headers object.
239
239
  #
240
240
  # @param src [HttpConfig, Hash] The HTTP configuration to serialize.
241
241
  # @return [SmplkitGeneratedClient::Jobs::JobHttpConfiguration] The wire model.
@@ -244,14 +244,7 @@ module Smplkit
244
244
  SmplkitGeneratedClient::Jobs::JobHttpConfiguration.new(
245
245
  method: HttpMethod.coerce(h.method),
246
246
  url: h.url,
247
- headers: (h.headers || []).map do |hdr|
248
- name, value = if hdr.is_a?(Hash)
249
- [hdr[:name] || hdr["name"], hdr[:value] || hdr["value"]]
250
- else
251
- [hdr.name, hdr.value]
252
- end
253
- SmplkitGeneratedClient::Jobs::HttpHeader.new(name: name, value: value)
254
- end,
247
+ headers: (h.headers || {}).transform_keys(&:to_s),
255
248
  body: h.body,
256
249
  success_status: h.success_status,
257
250
  timeout: h.timeout,
@@ -273,11 +266,13 @@ module Smplkit
273
266
  # +url+, +success_status+, and +timeout+ are required non-nil on the
274
267
  # generated jobs config, so they pass straight through. +method+,
275
268
  # +tls_verify+, +headers+, +body+, and +ca_cert+ are nullable and get
276
- # wrapper-side defaults when the wire omits them.
269
+ # wrapper-side defaults when the wire omits them. Header keys arrive as
270
+ # symbols (the generated client symbolizes JSON) — stringify them so
271
+ # {#get_header} and round-trips behave by name.
277
272
  new(
278
273
  method: src.method || HttpMethod::POST,
279
274
  url: src.url,
280
- headers: (src.headers || []).map { |h| HttpHeader.new(name: h.name, value: h.value) },
275
+ headers: (src.headers || {}).transform_keys(&:to_s),
281
276
  body: src.body,
282
277
  success_status: src.success_status,
283
278
  timeout: src.timeout,
@@ -292,70 +287,169 @@ module Smplkit
292
287
  end
293
288
  # rubocop:enable Lint/StructNewOverride
294
289
 
295
- # Per-environment enablement, schedule, and configuration override for a job.
290
+ # The per-environment scalar override leaves (everything except +enabled+ and
291
+ # +headers+, which are addressed individually as +headers.<name>+). These map
292
+ # 1:1 onto {JobEnvironment} fields and onto the flat overlay's leaf paths, in
293
+ # payload order.
294
+ #
295
+ # @api private
296
+ JOB_ENV_SCALAR_LEAVES =
297
+ %i[schedule timezone retry_policy url method timeout body success_status tls_verify ca_cert].freeze
298
+ # The same leaves as wire-key strings, for parsing the flat overlay.
299
+ #
300
+ # @api private
301
+ JOB_ENV_SCALAR_LEAF_NAMES = JOB_ENV_SCALAR_LEAVES.map(&:to_s).freeze
302
+
303
+ # One environment's *sparse override* for a job (ADR-056).
304
+ #
305
+ # A job's {Job#environments} map holds one of these per environment. Only the
306
+ # leaves you set are sent on save; everything you leave unset is inherited
307
+ # from the job's base definition, and the server resolves base ⊕ overrides
308
+ # when the job fires. The base definition is disabled everywhere, so a job
309
+ # runs in an environment only when that environment's override sets
310
+ # +enabled: true+.
296
311
  #
297
- # A job runs in a given environment only when that environment has an entry
298
- # in {Job#environments} with +enabled: true+ (scheduled there for a
299
- # recurring job, triggerable there for a manual one); an environment with no
300
- # entry (or +enabled: false+) is disabled there.
312
+ # Reach one through {Job#environment}, e.g.
313
+ # +job.environment("production").url = "https://prod.example.com/warm"+.
314
+ #
315
+ # *Reading a leaf returns this environment's override, or +nil+ when it does
316
+ # not override that leaf* — the SDK does not merge in the base value (jobs
317
+ # resolve server-side). To see a base value, read the job's base definition
318
+ # ({Job#configuration}, {Job#schedule}, …).
301
319
  #
302
320
  # @!attribute [rw] enabled
303
- # @return [Boolean] Whether the job is enabled in this environment.
304
- # Defaults to +false+.
321
+ # @return [Boolean] Whether the job runs in this environment. Defaults to +false+.
305
322
  # @!attribute [rw] schedule
306
- # @return [String, nil] Optional per-environment cron schedule override
307
- # that varies the cadence in this environment. +nil+ (the default)
308
- # inherits the job's base {Job#schedule}. When present, it must be a
309
- # 5-field UTC cron expression and is only meaningful on a recurring job —
310
- # it cannot turn a one-off job recurring or vice-versa.
323
+ # @return [String, nil] Per-environment cron override (recurring jobs only).
324
+ # +nil+ inherits the base {Job#schedule}.
311
325
  # @!attribute [rw] timezone
312
- # @return [String, nil] Optional per-environment IANA timezone override for
313
- # evaluating this environment's cron {#schedule} (recurring jobs only).
314
- # +nil+ (the default) inherits the base {Job#timezone}, else UTC. When
315
- # present, it must be a valid IANA zone key (e.g. +"America/New_York"+);
316
- # it may be set on an environment that inherits the base schedule (it
317
- # need not also override {#schedule}). Sent on writes only when present.
326
+ # @return [String, nil] Per-environment IANA timezone override (recurring
327
+ # jobs only). +nil+ inherits the base {Job#timezone}, else UTC.
318
328
  # @!attribute [rw] retry_policy
319
- # @return [String, nil] Optional per-environment retry-policy override — the
320
- # id of a {RetryPolicy} (or +"Default"+). +nil+ (the default) inherits the
321
- # job's base {Job#retry_policy}. Sent on writes only when present.
322
- # @!attribute [rw] configuration
323
- # @return [HttpConfig, nil] Optional per-environment request configuration
324
- # that fully replaces the job's base {Job#configuration} for this
325
- # environment. +nil+ (the default) inherits the base configuration. As
326
- # with the base configuration, header values are returned in plaintext on
327
- # reads, so a get-mutate-put round-trip preserves them.
329
+ # @return [String, nil] Per-environment retry-policy override — a policy id,
330
+ # a {RetryPolicy} (coerced to its id), or +"Default"+. +nil+ inherits the
331
+ # base {Job#retry_policy}.
332
+ # @!attribute [rw] url
333
+ # @return [String, nil] Per-environment URL override. +nil+ inherits the base.
334
+ # @!attribute [rw] method
335
+ # @return [String, nil] Per-environment HTTP-method override. +nil+ inherits the base.
336
+ # @!attribute [rw] timeout
337
+ # @return [Integer, nil] Per-environment timeout override. +nil+ inherits the base.
338
+ # @!attribute [rw] body
339
+ # @return [String, nil] Per-environment body override. +nil+ inherits the base.
340
+ # @!attribute [rw] success_status
341
+ # @return [String, nil] Per-environment success-status override. +nil+ inherits the base.
342
+ # @!attribute [rw] tls_verify
343
+ # @return [Boolean, nil] Per-environment TLS-verify override. +nil+ inherits the base.
344
+ # @!attribute [rw] ca_cert
345
+ # @return [String, nil] Per-environment CA-cert override. +nil+ inherits the base.
346
+ # @!attribute [rw] headers
347
+ # @return [Hash{String => String}] Per-environment header overrides, as a
348
+ # name→value object. Each entry overrides (or adds) that one header by name
349
+ # on top of the base headers, leaving the rest inherited. Use {#set_header}
350
+ # / {#get_header}.
328
351
  # @!attribute [rw] next_run_at
329
- # @return [String, nil] Read-only. The next scheduled fire time in this
330
- # environment. +nil+ when the environment is not enabled, or once a
331
- # one-off run has fired. Never written back on save.
352
+ # @return [String, nil] Read-only next scheduled fire time in this
353
+ # environment, or +nil+ when not enabled / once a one-off has fired. Never
354
+ # sent on save.
355
+ #
356
+ # rubocop:disable Lint/StructNewOverride -- ``:method`` matches the
357
+ # API attribute and shadowing Struct#method is the expected ergonomics.
332
358
  JobEnvironment = Struct.new(
333
- :enabled, :schedule, :timezone, :retry_policy, :configuration, :next_run_at, keyword_init: true
359
+ :enabled, :schedule, :timezone, :retry_policy, :url, :method, :timeout,
360
+ :body, :success_status, :tls_verify, :ca_cert, :headers, :next_run_at,
361
+ keyword_init: true
334
362
  ) do
335
363
  def initialize(enabled: false, schedule: nil, timezone: nil, retry_policy: nil,
336
- configuration: nil, next_run_at: nil)
337
- super
364
+ url: nil, method: nil, timeout: nil, body: nil, success_status: nil,
365
+ tls_verify: nil, ca_cert: nil, headers: nil, next_run_at: nil)
366
+ super(
367
+ enabled: enabled, schedule: schedule, timezone: timezone,
368
+ retry_policy: (retry_policy.is_a?(RetryPolicy) ? retry_policy.id : retry_policy),
369
+ url: url, method: method, timeout: timeout, body: body,
370
+ success_status: success_status, tls_verify: tls_verify, ca_cert: ca_cert,
371
+ headers: (headers || {}).transform_keys(&:to_s), next_run_at: next_run_at
372
+ )
373
+ end
374
+
375
+ # Coerce a retry-policy reference to its id on assignment, so both
376
+ # +env.retry_policy = policy+ and +env.retry_policy = "retry-on-5xx"+ work.
377
+ def retry_policy=(value)
378
+ self[:retry_policy] = value.is_a?(RetryPolicy) ? value.id : value
338
379
  end
339
380
 
340
- # @api private Build a {JobEnvironment} from the generated wire model.
381
+ # Override (or add) a single header by name in this environment.
341
382
  #
342
- # @param src [SmplkitGeneratedClient::Jobs::JobEnvironment, nil] The wire
343
- # model, or +nil+ for a disabled environment with no override.
344
- # @return [JobEnvironment]
345
- def self.from_wire(src)
346
- return new if src.nil?
383
+ # @param name [String] Header name.
384
+ # @param value [String] Header value.
385
+ def set_header(name, value)
386
+ self.headers ||= {}
387
+ headers[name.to_s] = value
388
+ end
389
+
390
+ # This environment's override for header +name+, or +nil+ when it does not
391
+ # override that header.
392
+ #
393
+ # @param name [String] Header name.
394
+ # @return [String, nil]
395
+ def get_header(name)
396
+ (headers || {})[name.to_s]
397
+ end
347
398
 
348
- cfg = src.configuration
399
+ # @api private — Emit the flat sparse leaf-path overlay (ADR-056): +enabled+
400
+ # plus only the leaves this environment overrides, with each header as a
401
+ # +headers.<name>+ leaf. +next_run_at+ is read-only and never emitted.
402
+ #
403
+ # @return [Hash{String => Object}]
404
+ def to_payload
405
+ payload = { "enabled" => enabled }
406
+ JOB_ENV_SCALAR_LEAVES.each do |leaf|
407
+ value = self[leaf]
408
+ payload[leaf.to_s] = value unless value.nil?
409
+ end
410
+ (headers || {}).each { |name, value| payload["headers.#{name}"] = value }
411
+ payload
412
+ end
413
+
414
+ # @api private — Parse the flat leaf-path overlay the server returns
415
+ # (ADR-056). Header leaves arrive as +headers.<name>+ (split on the first
416
+ # dot, so a dotted header name like +X-Foo.Bar+ is preserved); every other
417
+ # leaf is a single top-level key. Unknown leaves are ignored for forward
418
+ # compatibility. Keys may be symbols or strings.
419
+ #
420
+ # @param raw [Hash, nil] The flat overlay hash, or +nil+ for an empty
421
+ # override.
422
+ # @return [JobEnvironment]
423
+ def self.from_flat(raw)
424
+ return new if raw.nil?
425
+
426
+ headers = {}
427
+ scalars = {}
428
+ next_run_at = nil
429
+ raw.each do |key, value|
430
+ key = key.to_s
431
+ if key == "next_run_at"
432
+ next_run_at = value
433
+ next
434
+ end
435
+ group, _dot, name = key.partition(".")
436
+ if group == "headers" && !name.empty?
437
+ headers[name] = value
438
+ elsif JOB_ENV_SCALAR_LEAF_NAMES.include?(key) || key == "enabled"
439
+ scalars[key] = value
440
+ end
441
+ end
349
442
  new(
350
- enabled: src.enabled.nil? ? false : src.enabled,
351
- schedule: src.schedule,
352
- timezone: src.timezone,
353
- retry_policy: src.retry_policy,
354
- configuration: cfg.nil? ? nil : HttpConfig.from_wire(cfg),
355
- next_run_at: src.next_run_at
443
+ enabled: scalars["enabled"] ? true : false,
444
+ schedule: scalars["schedule"], timezone: scalars["timezone"],
445
+ retry_policy: scalars["retry_policy"], url: scalars["url"], method: scalars["method"],
446
+ timeout: scalars["timeout"], body: scalars["body"], success_status: scalars["success_status"],
447
+ tls_verify: scalars["tls_verify"], ca_cert: scalars["ca_cert"],
448
+ headers: headers, next_run_at: next_run_at
356
449
  )
357
450
  end
358
451
  end
452
+ # rubocop:enable Lint/StructNewOverride
359
453
 
360
454
  # A unit of work: an HTTP request, run on a schedule or triggered on demand.
361
455
  #
@@ -368,11 +462,13 @@ module Smplkit
368
462
  #
369
463
  # A job's {#kind} follows from its {#schedule}: a recurring (cron) job, a
370
464
  # manual job (no schedule, runs only when triggered), or a one-off (+now+ /
371
- # datetime) job that runs a single time. Enablement is per environment, set
372
- # via {#set_enabled} (and read via {#is_enabled}); base {#enabled} is a
373
- # derived roll-up over {#environments}. The base schedule is
374
- # environment-agnostic, while each environment may carry its own cron
375
- # {#set_schedule} override.
465
+ # datetime) job that runs a single time. Enablement and every other override
466
+ # is per environment: reach an environment's sparse override via
467
+ # {#environment} and set its +enabled+ / leaf fields
468
+ # (+job.environment("production").enabled = true+). Base {#enabled} is a
469
+ # read-only roll-up over {#environments} (+true+ when enabled in at least one
470
+ # environment). Base fields ({#schedule}, {#timezone}, {#retry_policy},
471
+ # {#configuration}) are set by direct assignment.
376
472
  class Job
377
473
  # @return [String] Caller-supplied unique identifier for the job (the
378
474
  # resource +id+). Unique within the account and immutable; the service
@@ -385,20 +481,20 @@ module Smplkit
385
481
  # @return [String, nil] Free-text description. +nil+ when unset.
386
482
  attr_accessor :description
387
483
 
388
- # @return [Boolean] Derived roll-up: +true+ when the job is enabled in at
484
+ # @return [Boolean] Read-only roll-up: +true+ when the job is enabled in at
389
485
  # least one environment. Computed from {#environments} rather than read
390
- # from the wire — the API no longer carries a top-level +enabled+. Set
391
- # enablement per environment via {#set_enabled} / {#environments}.
486
+ # from the wire — the API has no top-level +enabled+. Enable per
487
+ # environment via +job.environment(env).enabled = true+.
392
488
  def enabled
393
489
  (@environments || {}).each_value.any?(&:enabled)
394
490
  end
395
491
 
396
- # @return [Hash{String => JobEnvironment}] Per-environment overrides keyed
397
- # by environment key (e.g. +"production"+). The writable surface for
398
- # enablement: a recurring job fires in an environment only when
399
- # +environments[env].enabled+ is +true+. Each entry may carry an optional
400
- # {HttpConfig} override; omit it to inherit the base {#configuration}.
401
- # Every referenced environment must exist and be managed for the account.
492
+ # @return [Hash{String => JobEnvironment}] Per-environment sparse overrides
493
+ # keyed by environment key (e.g. +"production"+). A job runs in an
494
+ # environment only when +environments[env].enabled+ is +true+. Each entry
495
+ # overrides only the leaves it sets; omitted leaves inherit the base
496
+ # definition. Reach one via {#environment}. Every referenced environment
497
+ # must exist and be managed for the account.
402
498
  attr_accessor :environments
403
499
 
404
500
  # @return [String, nil] Read-only. How the job runs, derived from its
@@ -415,8 +511,9 @@ module Smplkit
415
511
  # evaluated in UTC for a recurring job; an ISO-8601 datetime for a
416
512
  # one-off run at that instant; or the literal +"now"+ for a one-off run
417
513
  # as soon as possible. A datetime or +"now"+ job disables itself after it
418
- # fires. The schedule is environment-agnostic — set it with
419
- # {#set_schedule}.
514
+ # fires. The schedule is environment-agnostic — set it by direct
515
+ # assignment; per-environment cron overrides live on
516
+ # +job.environment(env).schedule+.
420
517
  attr_accessor :schedule
421
518
 
422
519
  # @return [String, nil] The base IANA timezone the cron {#schedule} is
@@ -425,16 +522,22 @@ module Smplkit
425
522
  # {JobEnvironment#timezone}. The cron fires on this zone's wall clock
426
523
  # (DST-aware) while +next_run_at+ is still reported as a UTC instant.
427
524
  # Only valid on a recurring (cron) job — leave +nil+ for a manual or
428
- # one-off job. Set it with {#set_timezone}; sent on writes only when
429
- # present.
525
+ # one-off job. Sent on writes only when present.
430
526
  attr_accessor :timezone
431
527
 
432
528
  # @return [String, nil] The base retry policy for failed runs — the id of a
433
529
  # {RetryPolicy}, overridable per environment via
434
530
  # {JobEnvironment#retry_policy}. +nil+ (the default, omitted on the wire)
435
- # uses the built-in +"Default"+ policy, which never retries. Set it with
436
- # {#set_retry_policy}; sent on writes only when present.
437
- attr_accessor :retry_policy
531
+ # uses the built-in +"Default"+ policy, which never retries. Assigning
532
+ # accepts a {RetryPolicy} (its id is used) or a policy id string; sent on
533
+ # writes only when present.
534
+ attr_reader :retry_policy
535
+
536
+ # Set the base retry policy, coercing a {RetryPolicy} to its id so both
537
+ # +job.retry_policy = policy+ and +job.retry_policy = "retry-on-5xx"+ work.
538
+ def retry_policy=(value)
539
+ @retry_policy = value.is_a?(RetryPolicy) ? value.id : value
540
+ end
438
541
 
439
542
  # @return [HttpConfig] The base HTTP request to perform when the job fires.
440
543
  # Per-environment overrides live in {#environments}.
@@ -479,7 +582,7 @@ module Smplkit
479
582
  @type = type
480
583
  @schedule = schedule
481
584
  @timezone = timezone
482
- @retry_policy = retry_policy
585
+ self.retry_policy = retry_policy
483
586
  @configuration = configuration
484
587
  @concurrency_policy = concurrency_policy
485
588
  @birth_environment = birth_environment
@@ -517,35 +620,24 @@ module Smplkit
517
620
  end
518
621
  alias delete! delete
519
622
 
520
- # Enable or disable the job in a single environment.
623
+ # The per-environment override for +environment+ the single place to read
624
+ # or set what this job overrides there (ADR-056).
521
625
  #
522
- # Sets the per-environment override's +enabled+ on {#environments},
523
- # creating the override entry if it doesn't exist yet (preserving any
524
- # already-set +configuration+ on it). Call {#save} to persist.
626
+ # Returns the {JobEnvironment} for +environment+, creating an empty one (and
627
+ # inserting it into {#environments}) on first access, so you can set
628
+ # overrides directly:
525
629
  #
526
- # @param enabled [Boolean] Whether the job should fire in this environment.
527
- # @param environment [String] The environment key to enable / disable.
528
- def set_enabled(enabled, environment:)
529
- _environment_override(environment).enabled = enabled
530
- end
531
-
532
- # Whether the job is enabled.
630
+ # job.environment("production").enabled = true
631
+ # job.environment("production").url = "https://prod.example.com/warm"
632
+ # job.environment("production").set_header("Authorization", "Bearer prod")
533
633
  #
534
- # With +environment+ omitted (the default), returns the roll-up +true+
535
- # when the job is enabled in at least one environment. With an
536
- # +environment+, returns whether the job is enabled in that specific
537
- # environment.
634
+ # Only the leaves you set are sent on save; everything else inherits the
635
+ # base definition (the server resolves base overrides when the job fires).
538
636
  #
539
- # @param environment [String, nil] An environment key, or +nil+ for the
540
- # roll-up across every environment.
541
- # @return [Boolean]
542
- def is_enabled(environment: nil)
543
- return enabled if environment.nil?
544
-
545
- override = @environments[environment]
546
- return false if override.nil?
547
-
548
- override.enabled
637
+ # @param environment [String] The environment key.
638
+ # @return [JobEnvironment]
639
+ def environment(environment)
640
+ @environments[environment] ||= JobEnvironment.new
549
641
  end
550
642
 
551
643
  # Whether this is a recurring (cron-scheduled) job.
@@ -569,127 +661,6 @@ module Smplkit
569
661
  @kind == JobKind::ONE_OFF
570
662
  end
571
663
 
572
- # Set the job's configuration — base (+environment+ omitted) or
573
- # per-environment.
574
- #
575
- # With +environment+ given, sets the per-environment override's
576
- # configuration on {#environments}, creating the override entry if it
577
- # doesn't exist yet (preserving any already-set +enabled+ on it). Call
578
- # {#save} to persist.
579
- #
580
- # @param configuration [HttpConfig] The HTTP request configuration.
581
- # @param environment [String, nil] An environment key for a per-environment
582
- # override, or +nil+ to set the base configuration.
583
- def set_configuration(configuration, environment: nil)
584
- if environment.nil?
585
- @configuration = configuration
586
- else
587
- _environment_override(environment).configuration = configuration
588
- end
589
- end
590
-
591
- # The job's effective configuration.
592
- #
593
- # With +environment+ omitted (the default), returns the base
594
- # configuration. With an +environment+, returns that environment's
595
- # configuration override when it has one, else the base configuration —
596
- # the request the job actually sends when it fires in that environment.
597
- #
598
- # @param environment [String, nil] An environment key, or +nil+ for the
599
- # base configuration.
600
- # @return [HttpConfig]
601
- def get_configuration(environment: nil)
602
- unless environment.nil?
603
- override = @environments[environment]
604
- return override.configuration if override && !override.configuration.nil?
605
- end
606
- @configuration
607
- end
608
-
609
- # Set the job's schedule — base (+environment+ omitted) or per-environment.
610
- #
611
- # With +environment+ omitted (the default), sets the base {#schedule} —
612
- # an ISO-8601 datetime, a 5-field UTC cron expression, or the literal
613
- # +"now"+ — which every environment inherits unless it overrides it.
614
- #
615
- # With +environment+ given, sets that environment's per-environment cron
616
- # +schedule+ override on {#environments}, creating the override entry if it
617
- # doesn't exist yet (preserving any already-set +enabled+ / +configuration+
618
- # on it). A per-environment override is a cron expression only and varies
619
- # the cadence within that environment; it cannot turn a one-off job
620
- # recurring or vice-versa. Call {#save} to persist.
621
- #
622
- # Because the timezone is an integral part of a cron cadence, a +timezone+
623
- # may be supplied alongside the schedule; when given it sets the same
624
- # scope's timezone too (equivalent to a follow-up {#set_timezone}). Omit it
625
- # to leave the timezone untouched. For a timezone-only change, use
626
- # {#set_timezone}.
627
- #
628
- # @param schedule [String] An ISO-8601 datetime, a 5-field UTC cron
629
- # expression, or the literal +"now"+ (base); a 5-field UTC cron
630
- # expression (per-environment).
631
- # @param timezone [String, nil] An optional IANA timezone to set on the
632
- # same scope (recurring jobs only). +nil+ (the default) leaves the
633
- # timezone untouched.
634
- # @param environment [String, nil] An environment key for a per-environment
635
- # override, or +nil+ to set the base schedule.
636
- def set_schedule(schedule, timezone: nil, environment: nil)
637
- if environment.nil?
638
- @schedule = schedule
639
- else
640
- _environment_override(environment).schedule = schedule
641
- end
642
- set_timezone(timezone, environment: environment) unless timezone.nil?
643
- end
644
-
645
- # Set the IANA timezone the cron schedule is evaluated in — base
646
- # (+environment+ omitted) or per-environment.
647
- #
648
- # With +environment+ omitted (the default), sets the base {#timezone}
649
- # every environment inherits unless it overrides it. With an +environment+
650
- # given, sets that environment's per-environment +timezone+ override on
651
- # {#environments}, creating the override entry if it doesn't exist yet
652
- # (preserving any already-set +enabled+ / +schedule+ / +configuration+ on
653
- # it). A timezone is only valid on a recurring (cron) job; +nil+ means UTC
654
- # (base) or "inherit the base" (per-environment). Call {#save} to persist.
655
- #
656
- # @param timezone [String, nil] A valid IANA timezone key (e.g.
657
- # +"America/New_York"+), or +nil+ for UTC / inherit.
658
- # @param environment [String, nil] An environment key for a per-environment
659
- # override, or +nil+ to set the base timezone.
660
- def set_timezone(timezone, environment: nil)
661
- if environment.nil?
662
- @timezone = timezone
663
- else
664
- _environment_override(environment).timezone = timezone
665
- end
666
- end
667
-
668
- # Set the retry policy for failed runs — base (+environment+ omitted) or
669
- # per-environment.
670
- #
671
- # With +environment+ omitted (the default), sets the base {#retry_policy}
672
- # every environment inherits unless it overrides it. With an +environment+
673
- # given, sets that environment's per-environment override on
674
- # {#environments}, creating the override entry if it doesn't exist yet
675
- # (preserving any already-set +enabled+ / +schedule+ / +timezone+ /
676
- # +configuration+ on it). Call {#save} to persist.
677
- #
678
- # Accepts either a {RetryPolicy} instance (its id is used) or a policy id
679
- # string — pass +"Default"+ for the built-in never-retry policy.
680
- #
681
- # @param retry_policy [RetryPolicy, String] A {RetryPolicy} or a policy id.
682
- # @param environment [String, nil] An environment key for a per-environment
683
- # override, or +nil+ to set the base retry policy.
684
- def set_retry_policy(retry_policy, environment: nil)
685
- policy_id = retry_policy.is_a?(RetryPolicy) ? retry_policy.id : retry_policy
686
- if environment.nil?
687
- @retry_policy = policy_id
688
- else
689
- _environment_override(environment).retry_policy = policy_id
690
- end
691
- end
692
-
693
664
  # Trigger one immediate, manual run of this job (a +MANUAL+ run).
694
665
  #
695
666
  # @param environment [String, nil] Environment the run executes in.
@@ -731,17 +702,6 @@ module Smplkit
731
702
  )
732
703
  end
733
704
 
734
- # Return the override for +environment+, creating an empty one if absent.
735
- #
736
- # The per-environment mutators reach through here so an existing override's
737
- # other field is preserved when only one of +enabled+ / +configuration+ is
738
- # being set.
739
- #
740
- # @api private
741
- def _environment_override(environment)
742
- @environments[environment] ||= JobEnvironment.new
743
- end
744
-
745
705
  # @api private
746
706
  def _apply(other)
747
707
  @id = other.id
@@ -770,7 +730,7 @@ module Smplkit
770
730
  def self.from_resource(resource, client: nil)
771
731
  a = resource.attributes
772
732
  environments = (a.environments || {}).each_with_object({}) do |(env_key, env_raw), out|
773
- out[env_key.to_s] = JobEnvironment.from_wire(env_raw)
733
+ out[env_key.to_s] = JobEnvironment.from_flat(env_raw)
774
734
  end
775
735
  new(
776
736
  client,
@@ -958,9 +918,9 @@ module Smplkit
958
918
  #
959
919
  # Active-record style: instantiate via +client.jobs.retry_policies.new(...)+,
960
920
  # mutate fields directly, and call {#save} to persist or {#delete} to remove.
961
- # Reference a saved policy from a job's {Job#retry_policy} (see
962
- # {JobsClient#new_recurring_job} and {Job#set_retry_policy}). Retry policies
963
- # are account-global — never environment-scoped.
921
+ # Reference a saved policy from a job's {Job#retry_policy} (base) or
922
+ # {JobEnvironment#retry_policy} (per environment, via {Job#environment}).
923
+ # Retry policies are account-global — never environment-scoped.
964
924
  #
965
925
  # A policy decides whether and how a failed run is retried. A job that
966
926
  # references nothing uses the built-in +"Default"+ policy, which never