job_contracts 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ccfdc1005a713277b03897aaf58f87db30e04d9697e92f462c8712eeb4faf8d
4
- data.tar.gz: 3f3a4d679a61701c986cd858ca259b1208a2f3d82bd94681b96e884252fb4403
3
+ metadata.gz: 3409ea614e4698241974af1291cac6b6e2fc0cf75c45af5d7d5ea61cde545998
4
+ data.tar.gz: 7a59f4f8408f9d0c7963578b411bc5425da56e7138cb35e7cd0ee938f342dc58
5
5
  SHA512:
6
- metadata.gz: 41d343fa07382f35997fe9a6a75410fcfaf8f599b42cf9421349ae4084ed49c5b5467579b83fd326992c27bf4d3ec64bbb38882ee370e297d8688dcd571131c4
7
- data.tar.gz: 95336d3dd5464cbb7731ce25c946f5502a7cbc2158a7ea73ef727726b9226e7d660a735351bc5260f783cf6e3ab1a6bc93adeac7c15464a81288863cd70896f9
6
+ metadata.gz: 6e3329c2fb4ff9a4a898c44453504259b211389f8f2d57a6a6ba1a0722da414ad843030e8c1991e79ba28dfe71abedad9ef016f130ba469a5dc37c23c441572a
7
+ data.tar.gz: 40be8734e8791531b7b03fde19f89697a10744ec0f77e688a091090df9a998bffd8aa3d70c95bd1e31aba024f10338a4aa7fdea712cef9f8eeaefb4beb2fbc45
data/README.md CHANGED
@@ -1,12 +1,265 @@
1
+ [![Lines of Code](http://img.shields.io/badge/lines_of_code-240-brightgreen.svg?style=flat)](http://blog.codinghorror.com/the-best-code-is-no-code-at-all/)
2
+ [![Code Quality](https://app.codacy.com/project/badge/Grade/f604d4bc6db0474c802ef51182732488)](https://www.codacy.com/gh/hopsoft/job_contracts/dashboard?utm_source=github.com&utm_medium=referral&utm_content=hopsoft/job_contracts&utm_campaign=Badge_Grade)
3
+ ![tests](https://github.com/hopsoft/job_contracts/actions/workflows/test.yml/badge.svg)
4
+
1
5
  # Job Contracts
2
6
 
3
- ## Enforceable contracts for background jobs
7
+ ## Test-like assurances for jobs
8
+
9
+ Have you ever wanted to prevent a background job from writing to the database or perhaps ensure that it completes within a fixed amount of time?
10
+
11
+ Contracts allow you to easily enforce guarantees like this.
12
+
13
+ <!-- Tocer[start]: Auto-generated, don't remove. -->
14
+
15
+ ## Table of Contents
16
+
17
+ - [Why use Contracts?](#why-use-contracts)
18
+ - [Quick Start](#quick-start)
19
+ - [Contracts](#contracts)
20
+ - [Breach of Contract](#breach-of-contract)
21
+ - [Anatomy of a Contract](#anatomy-of-a-contract)
22
+ - [Defining a Contract](#defining-a-contract)
23
+ - [Using a Contract](#using-a-contract)
24
+ - [Worker Formation/Topology](#worker-formationtopology)
25
+ - [Advanced Usage](#advanced-usage)
26
+ - [Sidekiq](#sidekiq)
27
+ - [Todo](#todo)
28
+ - [License](#license)
29
+ - [Sponsors](#sponsors)
30
+
31
+ <!-- Tocer[finish]: Auto-generated, don't remove. -->
32
+
33
+ ## Why use Contracts?
34
+
35
+ - Organize your code for better reuse, consistency, and maintainability
36
+ - Refine your telemetry and instrumentation efforts
37
+ - Improve job performance via enforced *(SLAs/SLOs/SLIs)*
38
+ - Monitor and manage job queue backpressure
39
+ - Improve your worker formation/topology to support high throughput
40
+
41
+ ## Quick Start
42
+
43
+ Imagine you want to ensure a specific job completes within 5 seconds of being enqueued.
44
+
45
+ ```ruby
46
+ class ImportantJob < ApplicationJob
47
+ include JobContracts::Contractable
48
+
49
+ queue_as :default
50
+
51
+ add_contract JobContracts::DurationContract.new(max: 5.seconds)
52
+
53
+ def perform
54
+ # logic...
55
+ end
56
+
57
+ # default callback that's invoked if the contract is breached
58
+ def contract_breached!(contract)
59
+ # handle breach...
60
+ end
61
+ end
62
+ ```
63
+
64
+ *How to handle a [__breach of contract__](#breach-of-contract).*
65
+
66
+ ## Contracts
67
+
68
+ A contract is an agreement that a job should fulfill.
69
+ Failing to satisfy the contract is considered a __breach of contract__.
70
+
71
+ Contracts help you track `actual` results and compare them to `expected` outcomes.
72
+ For example, this project has a default set of contracts that verify the following:
73
+
74
+ - That a job will [execute within a set amount of time](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/contracts/duration_contract.rb)
75
+ - That a job is only [performed on a specific queue](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/contracts/queue_name_contract.rb)
76
+ - That a job [does not write to the database](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/contracts/read_only_contract.rb)
77
+
78
+ ### Breach of Contract
79
+
80
+ A __breach of contract__ is similar to a test failure; however, the breach can be handled in many different ways.
81
+
82
+ - Log and instrument the breach and continue
83
+ - Halt processing of the job and all other contracts and raise an exception
84
+ - Move the job to a queue where the contract will not be enforced
85
+ - etc...
86
+
87
+ *Mix and match any combination of these options to support your requirements.*
88
+
89
+ ### Anatomy of a Contract
90
+
91
+ Contracts support the following constructor arguments.
92
+
93
+ - __`trigger`__ `[Symbol] (:before, *:after)` - when contract enforcement takes place, *before or after perform*
94
+ - __`halt`__ `[Boolean] (true, *false)` - indicates whether or not to stop processing when the contract is breached
95
+ - __`queues`__ `[Array<String,Symbol>]` - a list of queue names where this contract will be enforced _(defaults to the configured queue, or `*` if the queue has not beeen configured)_
96
+ - __`expected`__ `[Hash]` - a dictionary of contract expectations
97
+
98
+ ### Defining a Contract
99
+
100
+ Here's a contrived but simple example that ensures the first argument to perform fits within a specific range of values.
101
+
102
+ ```ruby
103
+ # app/contracts/argument_contract.rb
104
+ class ArgumentContract < JobContracts::Contract
105
+ def initialize(range:)
106
+ # enforced on all queues
107
+ super queues: ["*"], expected: {range: range}
108
+ end
109
+
110
+ def enforce!(contractable)
111
+ actual[:argument] = contractable.arguments.first
112
+ self.satisfied = expected[:range].cover?(actual[:argument])
113
+ super
114
+ end
115
+ end
116
+ ```
117
+
118
+ ### Using a Contract
119
+
120
+ Here's how to use the `ArgumentContract` in a job.
121
+
122
+ ```ruby
123
+ # app/jobs/argument_example_job.rb
124
+ class ArgumentExampleJob < ApplicationJob
125
+ include JobContracts::Contractable
126
+
127
+ queue_as :default
128
+ add_contract ArgumentContract.new(range: (1..10))
4
129
 
5
- ## Todos
130
+ def perform(arg)
131
+ # logic...
132
+ end
6
133
 
7
- - [ ] Add documentation
8
- - [ ] Add automated tests
134
+ # default callback that's invoked if the contract is breached
135
+ def contract_breached!(contract)
136
+ # handle breach...
137
+ end
138
+ end
139
+ ```
140
+
141
+ This job will help ensure that the argument passed to perform is between 1 and 10.
142
+ *It's up to you to determine how to handle a breach of contract.*
143
+
144
+ ## Worker Formation/Topology
145
+
146
+ Thoughtful Rails applications often use specialized worker formations.
147
+
148
+ A simple formation might be to use two sets of workers.
149
+ One set dedicated to fast low-latency jobs with plenty of dedicated compute resources *(CPUs, processes, threads, etc...)*,
150
+ with another set dedicated to slower jobs that uses fewer compute resources.
151
+
152
+ <img width="593" alt="Untitled 2 2022-04-29 15-06-13" src="https://user-images.githubusercontent.com/32920/166069103-e316dcc7-e601-43d0-90df-ad0eda20409b.png">
153
+
154
+ Say we determine that fast low-latency jobs should __not__ write to the database.
155
+
156
+ We can use a [`ReadOnlyContract`](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/contracts/read_only_contract.rb)
157
+ to enforce this decision. If the contract is breached, we can notify our apm/monitoring service and re-enqueue the job to a slower queue *(worker set)* where database writes are permitted.
158
+ This will ensure that our fast low-latency queue doesn't get clogged with slow-running jobs.
159
+
160
+ Here's an example job implementation that accomplishes this.
161
+
162
+ ```ruby
163
+ class FastJob < ApplicationJob
164
+ include JobContracts::Contractable
165
+
166
+ # Configure the queue before adding contracts
167
+ # It will be used as the default enforcement queue for contracts
168
+ queue_as :critical
169
+
170
+ # Only enforces on the critical queue
171
+ # This allows us to halt job execution and reenqueue the job to a different queue
172
+ # where the contract will not be enforced
173
+ #
174
+ # NOTE: the arg `queues: [:critical]` is default behavior in this example
175
+ # we're setting it explicitly here for illustration purposes
176
+ add_contract JobContracts::ReadOnlyContract.new(queues: [:critical])
177
+
178
+ def perform
179
+ # logic that shouldn't write to the database,
180
+ # but might accidentally due to complex or opaque internals
181
+ end
182
+
183
+ def contract_breached!(contract)
184
+ # log and notify apm/monitoring service
185
+
186
+ # re-enqueue to a different queue
187
+ # where the database write will be permitted
188
+ # i.e. where the contract will not be enforced
189
+ enqueue queue: :default
190
+ end
191
+ end
192
+ ```
193
+
194
+ *Worker formations can be designed in countless ways to handle incredibly sophisticated requirements and operational constraints.
195
+ The only real limitation is your creativity.*
196
+
197
+ ## Advanced Usage
198
+
199
+ It's possible to override the default callback method that handles contract breaches.
200
+
201
+ ```ruby
202
+ class ImportantJob < ApplicationJob
203
+ include JobContracts::Contractable
204
+
205
+ queue_as :default
206
+
207
+ on_contract_breach :take_action
208
+ add_contract JobContracts::DurationContract.new(max: 5.seconds)
209
+
210
+ def perform
211
+ # logic...
212
+ end
213
+
214
+ def take_action(contract)
215
+ # handle breach...
216
+ end
217
+ end
218
+ ```
219
+
220
+ ```ruby
221
+ class ImportantJob < ApplicationJob
222
+ include JobContracts::Contractable
223
+
224
+ queue_as :default
225
+
226
+ on_contract_breach -> (contract) {
227
+ # take action...
228
+ }
229
+
230
+ add_contract JobContracts::DurationContract.new(max: 5.seconds)
231
+
232
+ def perform
233
+ # logic...
234
+ end
235
+ end
236
+ ```
237
+
238
+ ## Sidekiq
239
+
240
+ Sidekiq jobs/workers are supported.
241
+ Unfortunately this support comes with a performance penalty *(i.e. additional latency)* because executing
242
+ Sidekiq jobs don't have access to their own metadata. To get around this, we wait to find job metadata in the active
243
+ [`WorkSet`](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/concerns/sidekiq_contractable.rb#L23-L25)
244
+ which is only updated [every 5 seconds](https://github.com/mperham/sidekiq/wiki/API#workers).
245
+
246
+ ## Todo
247
+
248
+ - [ ] Sidekiq tests
9
249
 
10
250
  ## License
11
251
 
12
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
252
+ The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
253
+
254
+ ## Sponsors
255
+
256
+ This project is sponsored by [Orbit.love](https://orbit.love/?utm_source=github&utm_medium=repo&utm_campaign=hopsoft&utm_content=job_contracts) *(mission control for your community)*.
257
+
258
+ <a href="https://orbit.love/?utm_source=github&utm_medium=repo&utm_campaign=hopsoft&utm_content=job_contracts">
259
+ <img height="50" src="https://user-images.githubusercontent.com/32920/166343064-55f92cdb-c81b-4f85-80a8-167bfda73c85.png"></img>
260
+ </a>
261
+
262
+ ---
263
+
264
+ This effort was partly inspired by a presentation at [Sin City Ruby](https://www.sincityruby.com/) from our friends on the platform team at [Gusto](https://gusto.com/).
265
+ Their presentation validated some of my prior solutions aimed at accomplishing similar goals and motivated me to extract that work into a GEM.
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/setup"
2
4
 
3
5
  require "bundler/gem_tasks"
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
1
5
  module JobContracts
2
6
  # Universal mixin for jobs/workers
3
7
  module Contractable
@@ -5,33 +9,43 @@ module JobContracts
5
9
 
6
10
  module Prepends
7
11
  extend ActiveSupport::Concern
12
+ include MonitorMixin
8
13
 
9
14
  def perform(*args)
10
- should_perform = true
11
- self.class.contracts_to_enforce_before_perform.each do |contract|
12
- contract.enforce! self
13
- should_perform = false if contract.breached? && contract.halt?
15
+ # fetch sidekiq job/worker metadata on main thread
16
+ try :sidekiq_job_metadata
17
+
18
+ halted = false
19
+ contracts.select(&:before?).each do |contract|
20
+ contract.enforce! self unless halted
21
+ halted = true if contract.breached? && contract.halt?
14
22
  end
15
- super if should_perform
16
- self.class.contracts_to_enforce_after_perform.each do |contract|
17
- contract.enforce! self
23
+ super unless halted
24
+ ensure
25
+ # enforce after contracts in a separate thread to ensure that any perform related behavior
26
+ # defined in ContractablePrepends will finish executing before we invoke contract.enforce!
27
+ Thread.new do
28
+ sleep 0
29
+ synchronize do
30
+ contracts.select(&:after?).each do |contract|
31
+ contract.enforce! self unless halted
32
+ end
33
+ end
18
34
  end
19
35
  end
20
36
  end
21
37
 
22
38
  module ClassMethods
23
- attr_reader :after_contract_breach_callback
24
-
25
- def contracts_to_enforce_before_perform
26
- @contracts_to_enforce_before_perform ||= Set.new
39
+ def contracts
40
+ @contracts ||= Set.new
27
41
  end
28
42
 
29
- def contracts_to_enforce_after_perform
30
- @contracts_to_enforce_after_perform ||= Set.new
43
+ def on_contract_breach(value = nil, &block)
44
+ @on_contract_breach_callback = value || block
31
45
  end
32
46
 
33
- def after_contract_breach(value = nil, &block)
34
- @after_contract_breach_callback = value || block
47
+ def on_contract_breach_callback
48
+ @on_contract_breach_callback ||= :contract_breached!
35
49
  end
36
50
 
37
51
  def add_contract(contract)
@@ -45,21 +59,27 @@ module JobContracts
45
59
 
46
60
  prepend JobContracts::Contractable::Prepends
47
61
 
48
- if contract.trigger == :before
49
- contracts_to_enforce_before_perform << contract
50
- else
51
- contracts_to_enforce_after_perform << contract
52
- end
62
+ contract.queues << queue_name.to_s if contract.queues.blank? && queue_name.present?
63
+ contract.queues << "*" if contract.queues.blank?
64
+ contracts << contract
53
65
  end
54
66
  end
55
67
 
56
- def after_contract_breach(contract)
57
- method = self.class.after_contract_breach_callback
58
- case method
59
- when Proc then method.call(contract)
60
- when String, Symbol then send(method, contract)
61
- else raise NotImplementedError
62
- end
68
+ delegate :contracts, to: "self.class"
69
+
70
+ def breached_contracts
71
+ @breached_contracts ||= Set.new
72
+ end
73
+
74
+ # Default callback
75
+ def contract_breached!
76
+ # noop / override in job subclasses
77
+ end
78
+
79
+ def on_contract_breach(contract)
80
+ breached_contracts << contract
81
+ method = self.class.on_contract_breach_callback
82
+ method.is_a?(Proc) ? method.call(contract) : send(method.to_s, contract)
63
83
  end
64
84
  end
65
85
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "contractable"
2
4
 
3
5
  module JobContracts
@@ -6,41 +8,54 @@ module JobContracts
6
8
  extend ActiveSupport::Concern
7
9
  include Contractable
8
10
 
9
- class MetadataNotFoundError < StandardError; end
11
+ class SidekiqJobMetadataNotFoundError < StandardError; end
10
12
 
11
- def metadata
12
- hit = nil
13
- begin
14
- attempts ||= 1
15
- hit = Sidekiq::WorkSet.new.find do |_pid, _tid, work|
16
- work.dig("payload", "jid") == jid
17
- end
18
- raise MetadataNotFoundError if hit.blank?
19
- rescue MetadataNotFoundError
20
- # The WorkSet only updates every 5 seconds
21
- # SEE: https://github.com/mperham/sidekiq/wiki/API#workers
22
- # Re-attempt up to 10 times with a simple backoff strategy (up to 5.5 seconds)
23
- # TODO: Is there a faster and more reliable way to fetch the job's metadata after perform has begun?
24
- # May need to query Redis directly if the data is still in there at this point
25
- attempts += 1
26
- if attempts <= 10
27
- sleep 0.1 * attempts
28
- retry
29
- end
13
+ module ClassMethods
14
+ def queue_name
15
+ sidekiq_options_hash["queue"]
30
16
  end
17
+ end
18
+
19
+ def sidekiq_job_metadata
20
+ @sidekiq_job_metadata ||= begin
21
+ hit = nil
22
+ begin
23
+ attempts ||= 1
24
+ hit = Sidekiq::Workers.new.find do |_process_id, _thread_id, work|
25
+ work.dig("payload", "jid") == jid
26
+ end
27
+ raise SidekiqJobMetadataNotFoundError if hit.blank?
28
+ rescue SidekiqJobMetadataNotFoundError
29
+ # The WorkSet only updates every 5 seconds
30
+ # SEE: https://github.com/mperham/sidekiq/wiki/API#workers
31
+ # Re-attempt up to 10 times with a simple backoff strategy (up to 5.5 seconds)
32
+ # TODO: Is there a faster and more reliable way to fetch the job's metadata after perform has begun?
33
+ # May need to query Redis directly if the data is still in there at this point
34
+ attempts += 1
35
+ if attempts <= 10
36
+ sleep 0.1 * attempts
37
+ retry
38
+ end
39
+ end
31
40
 
32
- hit&.last || {}
41
+ hit&.last || {}
42
+ end
33
43
  end
34
44
 
35
45
  # Matches the ActiveJob API
36
46
  def queue_name
37
- metadata["queue"]
47
+ sidekiq_job_metadata["queue"]
38
48
  end
39
49
 
40
50
  # Matches the ActiveJob API
41
51
  def enqueued_at
42
- seconds = metadata.dig("payload", "enqueued_at")
52
+ seconds = sidekiq_job_metadata.dig("payload", "enqueued_at")
43
53
  (seconds ? Time.at(seconds) : nil)&.iso8601.to_s
44
54
  end
55
+
56
+ # Matches the ActiveJob API
57
+ def arguments
58
+ sidekiq_job_metadata.dig("payload", "args") || []
59
+ end
45
60
  end
46
61
  end
@@ -1,29 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "observer"
2
4
 
3
5
  module JobContracts
4
6
  class Contract
5
7
  include Observable
6
8
 
7
- attr_reader :trigger
9
+ attr_reader :trigger, :queues
8
10
 
9
- def initialize(trigger: :after, halt: false, **kwargs)
11
+ def initialize(trigger: :after, halt: false, queues: [], expected: {})
10
12
  @trigger = trigger.to_sym
11
13
  @halt = halt
12
- expect.merge! kwargs
14
+ @queues = Set.new(queues.map(&:to_s))
15
+ self.expected.merge! expected
13
16
  end
14
17
 
15
- def expect
16
- @expect ||= HashWithIndifferentAccess.new
18
+ def expected
19
+ @expected ||= HashWithIndifferentAccess.new
17
20
  end
18
21
 
19
22
  def actual
20
23
  @actual ||= HashWithIndifferentAccess.new
21
24
  end
22
25
 
26
+ def should_enforce?(contractable)
27
+ return true if queues.include?("*")
28
+ queues.include? contractable.queue_name.to_s
29
+ end
30
+
23
31
  # Method to be implemented by subclasses
24
32
  # NOTE: subclasses should update `actual`, set `satisfied`, and call `super`
25
33
  def enforce!(contractable)
26
- add_observer contractable, :after_contract_breach
34
+ return unless should_enforce?(contractable)
35
+ add_observer contractable, :on_contract_breach
27
36
  changed if breached?
28
37
  notify_observers self
29
38
  ensure
@@ -42,6 +51,25 @@ module JobContracts
42
51
  !!@halt
43
52
  end
44
53
 
54
+ def before?
55
+ trigger == :before
56
+ end
57
+
58
+ def after?
59
+ trigger == :after
60
+ end
61
+
62
+ def to_h
63
+ HashWithIndifferentAccess.new(
64
+ name: self.class.name,
65
+ trigger: trigger,
66
+ halt: halt?,
67
+ queues: queues.to_a,
68
+ expected: expected,
69
+ actual: actual
70
+ )
71
+ end
72
+
45
73
  protected
46
74
 
47
75
  attr_accessor :satisfied
@@ -1,14 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "contract"
2
4
 
3
5
  module JobContracts
4
6
  class DurationContract < Contract
5
- def initialize(duration:)
6
- super
7
+ def initialize(max:, queues: ["*"])
8
+ super queues: queues, expected: {max: max}
7
9
  end
8
10
 
9
11
  def enforce!(contractable)
10
12
  actual[:duration] = (Time.current - Time.parse(contractable.enqueued_at)).seconds
11
- self.satisfied = actual[:duration] < expect[:duration].seconds
13
+ self.satisfied = actual[:duration] < expected[:max].seconds
12
14
  super
13
15
  end
14
16
  end
@@ -1,15 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "contract"
2
4
 
3
5
  module JobContracts
4
6
  class QueueNameContract < Contract
5
7
  def initialize(queue_name:)
6
- super trigger: :before, halt: true, queue_name: queue_name
8
+ super(
9
+ trigger: :before,
10
+ halt: true,
11
+ queues: ["*"],
12
+ expected: {queue_name: queue_name.to_s}
13
+ )
7
14
  end
8
15
 
9
16
  def enforce!(contractable)
10
- queue_name = contractable.queue_name
11
- actual[:queue_name] = queue_name
12
- self.satisfied = queue_name.to_s == expect[:queue_name].to_s
17
+ actual[:queue_name] = contractable.queue_name.to_s
18
+ self.satisfied = contractable.queue_name.to_s == expected[:queue_name]
13
19
  super
14
20
  end
15
21
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "contract"
2
4
 
3
5
  module JobContracts
@@ -6,7 +8,12 @@ module JobContracts
6
8
  extend ActiveSupport::Concern
7
9
 
8
10
  def perform(*args)
9
- ActiveRecord::Base.while_preventing_writes do
11
+ contract = contracts.find { |c| c.is_a?(ReadOnlyContract) }
12
+ if contract.should_enforce?(self)
13
+ ActiveRecord::Base.while_preventing_writes do
14
+ super
15
+ end
16
+ else
10
17
  super
11
18
  end
12
19
  rescue ActiveRecord::ReadOnlyError => error
@@ -21,11 +28,8 @@ module JobContracts
21
28
  end
22
29
  end
23
30
 
24
- # def initialize
25
- # super trigger: :before
26
- # end
27
-
28
31
  def enforce!(contractable)
32
+ self.satisfied = true
29
33
  if contractable.read_only_error.present?
30
34
  actual[:error] = contractable.read_only_error.message
31
35
  self.satisfied = false
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JobContracts
2
4
  class Railtie < ::Rails::Railtie
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JobContracts
2
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
3
5
  end
data/lib/job_contracts.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "job_contracts/version"
2
4
  require "job_contracts/railtie"
3
5
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: job_contracts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Hopkins
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-22 00:00:00.000000000 Z
11
+ date: 2022-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -52,7 +52,63 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 1.10.0
55
- description: Enforceable contracts for background jobs
55
+ - !ruby/object:Gem::Dependency
56
+ name: magic_frozen_string_literal
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-doc
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: tocer
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Enforceable contracts for jobs
56
112
  email:
57
113
  - natehop@gmail.com
58
114
  executables: []
@@ -93,8 +149,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
149
  - !ruby/object:Gem::Version
94
150
  version: '0'
95
151
  requirements: []
96
- rubygems_version: 3.3.7
152
+ rubygems_version: 3.3.3
97
153
  signing_key:
98
154
  specification_version: 4
99
- summary: Enforceable contracts for background jobs
155
+ summary: Enforceable contracts for jobs
100
156
  test_files: []