smplkit 3.0.109 → 3.0.111

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.
@@ -8,7 +8,13 @@ module Smplkit
8
8
  # single client. A {Job} is an active record: build it with
9
9
  # +client.jobs.new(...)+, set fields, and call {Job#save} (create when new,
10
10
  # full-replace update when it already exists) or {Job#delete}. Runs are
11
- # read-only views; run actions live on +client.jobs.runs+.
11
+ # read-only views with +rerun+ / +cancel+ actions; run history lives on
12
+ # +client.jobs.runs+.
13
+ #
14
+ # A job is enabled per environment: a recurring (cron) job may run in several
15
+ # environments at once, a one-off (+now+ / future datetime) job runs a single
16
+ # time in the environment it was created in. Base +enabled+ is a read-only,
17
+ # server-derived roll-up (+true+ when enabled in at least one environment).
12
18
  module Jobs
13
19
  # Wrap a generated-jobs-API call and translate +ApiError+ into the
14
20
  # +Smplkit::Error+ hierarchy. Connection-level failures (no response
@@ -28,6 +34,68 @@ module Smplkit
28
34
  raise
29
35
  end
30
36
 
37
+ # Coerce a caller-supplied +environments+ value into the comma-separated
38
+ # string the +GET /api/v1/runs+ endpoint expects for +filter[environment]+,
39
+ # or +nil+ when no filter should be sent.
40
+ #
41
+ # +nil+ or an empty array (or one whose entries are all blank) returns
42
+ # +nil+ so the caller omits the query param entirely and the read covers
43
+ # every environment the credential can access.
44
+ #
45
+ # @api private
46
+ def self.join_environments(environments)
47
+ return nil if environments.nil?
48
+
49
+ values = Array(environments).map { |e| e.to_s.strip }.reject(&:empty?)
50
+ values.empty? ? nil : values.join(",")
51
+ end
52
+
53
+ # Resolve the +filter[environment]+ value for the runs listing.
54
+ #
55
+ # An explicit, non-empty +environments+ list always wins and is comma-joined
56
+ # via {join_environments}. Otherwise the client's configured +default+
57
+ # environment scopes the read. A client with no configured environment and
58
+ # no explicit list returns +nil+ so the caller omits the query param and the
59
+ # credential's own scoping applies server-side.
60
+ #
61
+ # @param environments [Array<String>, String, nil] Explicit per-call
62
+ # environment filter; an empty/blank value falls through to +default+.
63
+ # @param default [String, nil] The client's configured environment.
64
+ # @return [String, nil] The +filter[environment]+ value, or +nil+ to omit it.
65
+ # @api private
66
+ def self.resolve_environment_filter(environments, default)
67
+ joined = join_environments(environments)
68
+ return joined unless joined.nil?
69
+
70
+ default
71
+ end
72
+
73
+ # Coerce a caller's +environments+ map to {JobEnvironment} instances.
74
+ #
75
+ # Accepts either {JobEnvironment} values or plain hashes
76
+ # (+{ enabled: true, configuration: HttpConfig.new(...) }+) so callers can
77
+ # use the lightweight hash form without importing the model. A dict-form
78
+ # +configuration+ override is coerced to an {HttpConfig} so it serializes on
79
+ # save.
80
+ #
81
+ # @api private
82
+ def self.normalize_environments(environments)
83
+ return {} if environments.nil? || environments.empty?
84
+
85
+ environments.each_with_object({}) do |(env_key, value), out|
86
+ out[env_key.to_s] = if value.is_a?(JobEnvironment)
87
+ value
88
+ else
89
+ cfg = value[:configuration] || value["configuration"]
90
+ cfg = HttpConfig.new(**cfg) if cfg.is_a?(Hash)
91
+ JobEnvironment.new(
92
+ enabled: value[:enabled] || value["enabled"] || false,
93
+ configuration: cfg
94
+ )
95
+ end
96
+ end
97
+ end
98
+
31
99
  # HTTP verb a job uses when it fires.
32
100
  #
33
101
  # Mirrors the jobs spec's method enum so a job's
@@ -109,7 +177,7 @@ module Smplkit
109
177
  keyword_init: true
110
178
  ) do
111
179
  def initialize(
112
- url: "", method: HttpMethod::POST, headers: nil, body: nil,
180
+ url:, method: HttpMethod::POST, headers: nil, body: nil,
113
181
  success_status: "2xx", timeout: 30, tls_verify: true, ca_cert: nil
114
182
  )
115
183
  super(
@@ -152,7 +220,7 @@ module Smplkit
152
220
  # wire model, or +nil+ for an empty configuration.
153
221
  # @return [HttpConfig] The wrapper-side configuration.
154
222
  def self.from_wire(src)
155
- return new if src.nil?
223
+ return new(url: "") if src.nil?
156
224
 
157
225
  # +url+, +success_status+, and +timeout+ are required non-nil on the
158
226
  # generated jobs config, so they pass straight through. +method+,
@@ -176,6 +244,42 @@ module Smplkit
176
244
  end
177
245
  # rubocop:enable Lint/StructNewOverride
178
246
 
247
+ # Per-environment enablement and optional configuration override for a job.
248
+ #
249
+ # A recurring job fires in a given environment only when that environment
250
+ # has an entry in {Job#environments} with +enabled: true+; an environment
251
+ # with no entry (or +enabled: false+) does not fire there.
252
+ #
253
+ # @!attribute [rw] enabled
254
+ # @return [Boolean] Whether the job fires in this environment. Defaults to
255
+ # +false+.
256
+ # @!attribute [rw] configuration
257
+ # @return [HttpConfig, nil] Optional per-environment request configuration
258
+ # that fully replaces the job's base {Job#configuration} for this
259
+ # environment. +nil+ (the default) inherits the base configuration. As
260
+ # with the base configuration, header values are returned in plaintext on
261
+ # reads, so a get-mutate-put round-trip preserves them.
262
+ JobEnvironment = Struct.new(:enabled, :configuration, keyword_init: true) do
263
+ def initialize(enabled: false, configuration: nil)
264
+ super
265
+ end
266
+
267
+ # @api private — Build a {JobEnvironment} from the generated wire model.
268
+ #
269
+ # @param src [SmplkitGeneratedClient::Jobs::JobEnvironment, nil] The wire
270
+ # model, or +nil+ for a disabled environment with no override.
271
+ # @return [JobEnvironment]
272
+ def self.from_wire(src)
273
+ return new if src.nil?
274
+
275
+ cfg = src.configuration
276
+ new(
277
+ enabled: src.enabled.nil? ? false : src.enabled,
278
+ configuration: cfg.nil? ? nil : HttpConfig.from_wire(cfg)
279
+ )
280
+ end
281
+ end
282
+
179
283
  # A scheduled unit of work: an HTTP request run on a schedule.
180
284
  #
181
285
  # Active-record style: instantiate via +client.jobs.new(...)+, mutate fields
@@ -183,6 +287,11 @@ module Smplkit
183
287
  # values in +configuration.headers+ are returned in plaintext on reads, so
184
288
  # fetching a job, mutating it, and calling {#save} preserves its header
185
289
  # values without re-entering secrets.
290
+ #
291
+ # Enablement is per environment, set via {#set_enabled} (and read via
292
+ # {#is_enabled}); base {#enabled} is a read-only roll-up. The schedule is
293
+ # environment-agnostic — one cron / datetime / +now+ shared across every
294
+ # environment the job runs in.
186
295
  class Job
187
296
  # @return [String] Caller-supplied unique identifier for the job (the
188
297
  # resource +id+). Unique within the account and immutable; the service
@@ -195,20 +304,37 @@ module Smplkit
195
304
  # @return [String, nil] Free-text description. +nil+ when unset.
196
305
  attr_accessor :description
197
306
 
198
- # @return [Boolean] Whether the job is scheduling runs. +false+ pauses
199
- # without deleting.
307
+ # @return [Boolean] Read-only, server-derived roll-up: +true+ when the job
308
+ # is enabled in at least one environment. Set enablement per environment
309
+ # via {#set_enabled} / {#environments}; mutating this field directly has
310
+ # no effect on the server (it is never written).
200
311
  attr_accessor :enabled
201
312
 
313
+ # @return [Hash{String => JobEnvironment}] Per-environment overrides keyed
314
+ # by environment key (e.g. +"production"+). The writable surface for
315
+ # enablement: a recurring job fires in an environment only when
316
+ # +environments[env].enabled+ is +true+. Each entry may carry an optional
317
+ # {HttpConfig} override; omit it to inherit the base {#configuration}.
318
+ # Every referenced environment must exist and be managed for the account.
319
+ attr_accessor :environments
320
+
321
+ # @return [Boolean, nil] Read-only. +true+ for a recurring (cron) schedule,
322
+ # +false+ for a one-off datetime / +now+ schedule. Derived from
323
+ # {#schedule} by the server.
324
+ attr_accessor :recurring
325
+
202
326
  # @return [String] Job type. Only +"http"+ is supported today.
203
327
  attr_accessor :type
204
328
 
205
329
  # @return [String] When the job runs: an ISO-8601 datetime (a one-off
206
330
  # run), a 5-field cron expression evaluated in UTC (recurring), or the
207
331
  # literal +"now"+ (run once, as soon as possible). A datetime or +"now"+
208
- # job disables itself after it fires.
332
+ # job disables itself after it fires. The schedule is environment-agnostic
333
+ # — set it with {#set_schedule}.
209
334
  attr_accessor :schedule
210
335
 
211
- # @return [HttpConfig] The HTTP request to perform when the job fires.
336
+ # @return [HttpConfig] The base HTTP request to perform when the job fires.
337
+ # Per-environment overrides live in {#environments}.
212
338
  attr_accessor :configuration
213
339
 
214
340
  # @return [String] How overlapping runs are handled. +"ALLOW"+ (the only
@@ -234,19 +360,28 @@ module Smplkit
234
360
  # server-side write.
235
361
  attr_accessor :version
236
362
 
363
+ # @api private — For a one-off job, the environment it is born in, sent as
364
+ # the +X-Smplkit-Environment+ header by {JobsClient#_create_job}. Ignored
365
+ # for a recurring job, whose environments come from {#environments}.
366
+ attr_accessor :birth_environment
367
+
237
368
  def initialize(client = nil, id:, name:, schedule:, configuration:,
238
- description: nil, enabled: true, type: "http",
239
- concurrency_policy: "ALLOW", next_run_at: nil,
240
- created_at: nil, updated_at: nil, deleted_at: nil, version: nil)
369
+ description: nil, environments: nil, enabled: false,
370
+ recurring: nil, type: "http", concurrency_policy: "ALLOW",
371
+ birth_environment: nil, next_run_at: nil, created_at: nil,
372
+ updated_at: nil, deleted_at: nil, version: nil)
241
373
  @client = client
242
374
  @id = id
243
375
  @name = name
244
376
  @description = description
377
+ @environments = environments || {}
245
378
  @enabled = enabled
379
+ @recurring = recurring
246
380
  @type = type
247
381
  @schedule = schedule
248
382
  @configuration = configuration
249
383
  @concurrency_policy = concurrency_policy
384
+ @birth_environment = birth_environment
250
385
  @next_run_at = next_run_at
251
386
  @created_at = created_at
252
387
  @updated_at = updated_at
@@ -259,7 +394,8 @@ module Smplkit
259
394
  # Upsert behavior is driven by {#created_at}: a job with no +created_at+
260
395
  # is created (POST); otherwise it's full-replace updated (PUT). After the
261
396
  # call, every field is refreshed from the server response (including
262
- # newly-assigned +created_at+, +version+, +next_run_at+).
397
+ # newly-assigned +created_at+, +version+, +next_run_at+, and the derived
398
+ # +enabled+ roll-up).
263
399
  #
264
400
  # @return [self]
265
401
  def save
@@ -281,12 +417,139 @@ module Smplkit
281
417
  end
282
418
  alias delete! delete
283
419
 
420
+ # Enable or disable the job in a single environment.
421
+ #
422
+ # Sets the per-environment override's +enabled+ on {#environments},
423
+ # creating the override entry if it doesn't exist yet (preserving any
424
+ # already-set +configuration+ on it). Call {#save} to persist.
425
+ #
426
+ # @param enabled [Boolean] Whether the job should fire in this environment.
427
+ # @param environment [String] The environment key to enable / disable.
428
+ def set_enabled(enabled, environment:)
429
+ _environment_override(environment).enabled = enabled
430
+ end
431
+
432
+ # Whether the job is enabled.
433
+ #
434
+ # With +environment+ omitted (the default), returns the roll-up — +true+
435
+ # when the job is enabled in at least one environment. With an
436
+ # +environment+, returns whether the job is enabled in that specific
437
+ # environment.
438
+ #
439
+ # @param environment [String, nil] An environment key, or +nil+ for the
440
+ # roll-up across every environment.
441
+ # @return [Boolean]
442
+ def is_enabled(environment: nil)
443
+ return @enabled if environment.nil?
444
+
445
+ override = @environments[environment]
446
+ return false if override.nil?
447
+
448
+ override.enabled
449
+ end
450
+
451
+ # Set the job's configuration — base (+environment+ omitted) or
452
+ # per-environment.
453
+ #
454
+ # With +environment+ given, sets the per-environment override's
455
+ # configuration on {#environments}, creating the override entry if it
456
+ # doesn't exist yet (preserving any already-set +enabled+ on it). Call
457
+ # {#save} to persist.
458
+ #
459
+ # @param configuration [HttpConfig] The HTTP request configuration.
460
+ # @param environment [String, nil] An environment key for a per-environment
461
+ # override, or +nil+ to set the base configuration.
462
+ def set_configuration(configuration, environment: nil)
463
+ if environment.nil?
464
+ @configuration = configuration
465
+ else
466
+ _environment_override(environment).configuration = configuration
467
+ end
468
+ end
469
+
470
+ # The job's effective configuration.
471
+ #
472
+ # With +environment+ omitted (the default), returns the base
473
+ # configuration. With an +environment+, returns that environment's
474
+ # configuration override when it has one, else the base configuration —
475
+ # the request the job actually sends when it fires in that environment.
476
+ #
477
+ # @param environment [String, nil] An environment key, or +nil+ for the
478
+ # base configuration.
479
+ # @return [HttpConfig]
480
+ def get_configuration(environment: nil)
481
+ unless environment.nil?
482
+ override = @environments[environment]
483
+ return override.configuration if override && !override.configuration.nil?
484
+ end
485
+ @configuration
486
+ end
487
+
488
+ # Set the job's schedule.
489
+ #
490
+ # The schedule is environment-agnostic — a job has a single cron /
491
+ # datetime / +"now"+ schedule shared across every environment it runs in
492
+ # (each enabled environment fires on the same cadence). There is no
493
+ # per-environment schedule, so this setter takes no +environment+.
494
+ #
495
+ # @param schedule [String] An ISO-8601 datetime, a 5-field UTC cron
496
+ # expression, or the literal +"now"+.
497
+ def set_schedule(schedule)
498
+ @schedule = schedule
499
+ end
500
+
501
+ # Trigger one immediate, manual run of this job (a +MANUAL+ run).
502
+ #
503
+ # @param environment [String, nil] Environment the run executes in.
504
+ # Defaults to the client's configured environment; when the job is
505
+ # enabled in exactly one environment that environment is used.
506
+ # @return [Smplkit::Jobs::Run] The run that was started.
507
+ # @raise [RuntimeError] when this job has no bound client.
508
+ def trigger(environment: nil)
509
+ raise "Job was constructed without a client; cannot trigger a run" if @client.nil?
510
+
511
+ @client.run(@id, environment: environment)
512
+ end
513
+
514
+ # List this job's run history, most recent first.
515
+ #
516
+ # @param environment [String, nil] Restrict to runs stamped with this
517
+ # environment. +nil+ covers every environment you can access.
518
+ # @param page_size [Integer, nil] Maximum number of runs to return in this
519
+ # page.
520
+ # @param after [String, nil] Opaque cursor from a previous page.
521
+ # @return [Array<Smplkit::Jobs::Run>] The runs in this page.
522
+ # @raise [RuntimeError] when this job has no bound client.
523
+ def list_runs(environment: nil, page_size: nil, after: nil)
524
+ raise "Job was constructed without a client; cannot list runs" if @client.nil?
525
+
526
+ @client.runs.list(
527
+ job: @id,
528
+ environments: environment.nil? ? nil : [environment],
529
+ page_size: page_size,
530
+ after: after
531
+ )
532
+ end
533
+
534
+ # Return the override for +environment+, creating an empty one if absent.
535
+ #
536
+ # The per-environment mutators reach through here so an existing override's
537
+ # other field is preserved when only one of +enabled+ / +configuration+ is
538
+ # being set.
539
+ #
540
+ # @api private
541
+ def _environment_override(environment)
542
+ @environments[environment] ||= JobEnvironment.new
543
+ end
544
+
284
545
  # @api private
285
546
  def _apply(other)
286
547
  @id = other.id
287
548
  @name = other.name
288
549
  @description = other.description
289
550
  @enabled = other.enabled
551
+ @environments = other.environments
552
+ @recurring = other.recurring
290
553
  @type = other.type
291
554
  @schedule = other.schedule
292
555
  @configuration = other.configuration
@@ -306,12 +569,19 @@ module Smplkit
306
569
  # @return [Job] The hydrated job.
307
570
  def self.from_resource(resource, client: nil)
308
571
  a = resource.attributes
572
+ environments = (a.environments || {}).each_with_object({}) do |(env_key, env_raw), out|
573
+ out[env_key.to_s] = JobEnvironment.from_wire(env_raw)
574
+ end
309
575
  new(
310
576
  client,
311
577
  id: resource.id,
312
578
  name: a.name,
313
579
  description: a.description,
314
- enabled: a.enabled.nil? || a.enabled,
580
+ # The base +enabled+ is a server-derived roll-up; round-trip whatever
581
+ # the server returned without assuming a default of true.
582
+ enabled: a.enabled.nil? ? false : a.enabled,
583
+ environments: environments,
584
+ recurring: a.recurring,
315
585
  type: a.type || "http",
316
586
  schedule: a.schedule,
317
587
  configuration: HttpConfig.from_wire(a.configuration),
@@ -325,11 +595,12 @@ module Smplkit
325
595
  end
326
596
  end
327
597
 
328
- # A single execution of a job (read-only).
598
+ # A single execution of a job (read-only) with +rerun+ / +cancel+ actions.
329
599
  #
330
600
  # Runs are created and mutated by the jobs service, not by clients; clients
331
- # influence runs only through the +run+ / +cancel+ / +rerun+ actions on
332
- # +client.jobs+.
601
+ # influence runs only through {#rerun} / {#cancel} (and the +run+ action on
602
+ # +client.jobs+). A run returned from the SDK holds a backref to the runs
603
+ # client so {#rerun} / {#cancel} work without re-deriving the client.
333
604
  #
334
605
  # @!attribute [rw] id
335
606
  # @return [String] Server-assigned UUID for this run.
@@ -337,6 +608,10 @@ module Smplkit
337
608
  # @return [String] The id of the job this run belongs to.
338
609
  # @!attribute [rw] job_version
339
610
  # @return [Integer, nil] The job's version at the time the run executed.
611
+ # @!attribute [rw] environment
612
+ # @return [String] The environment this run executed in. A scheduled run
613
+ # inherits the firing job-environment; a manual run uses the environment
614
+ # named on the run-now request; a rerun copies its source run's environment.
340
615
  # @!attribute [rw] trigger
341
616
  # @return [String] Why the run exists: +SCHEDULE+, +MANUAL+ (run now), or +RERUN+.
342
617
  # @!attribute [rw] rerun_of
@@ -367,22 +642,24 @@ module Smplkit
367
642
  # @!attribute [rw] created_at
368
643
  # @return [String, nil] When the run was enqueued (became +PENDING+).
369
644
  Run = Struct.new(
370
- :id, :job, :job_version, :trigger, :rerun_of, :scheduled_for, :status,
371
- :started_at, :finished_at, :pending_duration_ms, :run_duration_ms,
645
+ :id, :job, :job_version, :environment, :trigger, :rerun_of, :scheduled_for,
646
+ :status, :started_at, :finished_at, :pending_duration_ms, :run_duration_ms,
372
647
  :total_duration_ms, :failure_reason, :error, :request, :result, :created_at,
373
648
  keyword_init: true
374
649
  ) do
375
650
  # @api private — Build a {Run} from a JSON:API resource returned by the
376
- # jobs service.
651
+ # jobs service, binding it to +runs+ so {#rerun} / {#cancel} work.
377
652
  #
378
653
  # @param resource [Object] The JSON:API resource (id + attributes).
654
+ # @param runs [RunsClient, nil] Runs client to bind the run to.
379
655
  # @return [Run] The hydrated run.
380
- def self.from_resource(resource)
656
+ def self.from_resource(resource, runs: nil)
381
657
  a = resource.attributes
382
658
  new(
383
659
  id: resource.id,
384
660
  job: a.job,
385
661
  job_version: a.job_version,
662
+ environment: a.environment,
386
663
  trigger: a.trigger,
387
664
  rerun_of: a.rerun_of,
388
665
  scheduled_for: a.scheduled_for,
@@ -397,7 +674,29 @@ module Smplkit
397
674
  request: a.request.nil? ? nil : Smplkit::Helpers.deep_stringify_keys(a.request),
398
675
  result: a.result.nil? ? nil : Smplkit::Helpers.deep_stringify_keys(a.result),
399
676
  created_at: a.created_at
400
- )
677
+ ).tap { |run| run.instance_variable_set(:@runs, runs) }
678
+ end
679
+
680
+ # Start a new run that repeats this one (a +RERUN+), in the same
681
+ # environment.
682
+ #
683
+ # @return [Smplkit::Jobs::Run] The new run, with +rerun_of+ set to this
684
+ # run's id.
685
+ # @raise [RuntimeError] when this run has no bound runs client.
686
+ def rerun
687
+ raise "Run was constructed without a client; cannot rerun" if @runs.nil?
688
+
689
+ @runs.rerun(id)
690
+ end
691
+
692
+ # Cancel this run if it has not finished yet.
693
+ #
694
+ # @return [Smplkit::Jobs::Run] The updated run reflecting the cancellation.
695
+ # @raise [RuntimeError] when this run has no bound runs client.
696
+ def cancel
697
+ raise "Run was constructed without a client; cannot cancel" if @runs.nil?
698
+
699
+ @runs.cancel(id)
401
700
  end
402
701
  end
403
702
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smplkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.109
4
+ version: 3.0.111
5
5
  platform: ruby
6
6
  authors:
7
7
  - Smpl Solutions LLC
@@ -767,6 +767,7 @@ files:
767
767
  - lib/smplkit/_generated/jobs/lib/smplkit_jobs_client/models/job.rb
768
768
  - lib/smplkit/_generated/jobs/lib/smplkit_jobs_client/models/job_create_request.rb
769
769
  - lib/smplkit/_generated/jobs/lib/smplkit_jobs_client/models/job_create_resource.rb
770
+ - lib/smplkit/_generated/jobs/lib/smplkit_jobs_client/models/job_environment.rb
770
771
  - lib/smplkit/_generated/jobs/lib/smplkit_jobs_client/models/job_http_configuration.rb
771
772
  - lib/smplkit/_generated/jobs/lib/smplkit_jobs_client/models/job_list_response.rb
772
773
  - lib/smplkit/_generated/jobs/lib/smplkit_jobs_client/models/job_request.rb
@@ -790,6 +791,7 @@ files:
790
791
  - lib/smplkit/_generated/jobs/spec/models/http_header_spec.rb
791
792
  - lib/smplkit/_generated/jobs/spec/models/job_create_request_spec.rb
792
793
  - lib/smplkit/_generated/jobs/spec/models/job_create_resource_spec.rb
794
+ - lib/smplkit/_generated/jobs/spec/models/job_environment_spec.rb
793
795
  - lib/smplkit/_generated/jobs/spec/models/job_http_configuration_spec.rb
794
796
  - lib/smplkit/_generated/jobs/spec/models/job_list_response_spec.rb
795
797
  - lib/smplkit/_generated/jobs/spec/models/job_request_spec.rb