shifty 0.5.0 → 0.6.0
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +79 -0
- data/README.md +79 -35
- data/benchmark/RESULTS.md +69 -0
- data/benchmark/handoff_policies.rb +82 -0
- data/docs/planning/artifacts/.discovery-notes.md +40 -0
- data/docs/planning/artifacts/.round1-specialist-outputs.md +196 -0
- data/docs/planning/artifacts/implementation-decision-log.md +231 -0
- data/docs/planning/artifacts/implementation-iteration-history.md +73 -0
- data/docs/planning/feature-implementation-plan.md +191 -0
- data/docs/planning/handoff-immutability-policies.md +590 -0
- data/docs/shifty/worker.md +8 -19
- data/docs/use_cases.md +59 -0
- data/docs/wiki/Coding-Idioms-Under-Frozen.md +74 -0
- data/docs/wiki/Handoff-Policies.md +167 -0
- data/docs/wiki/Home.md +59 -0
- data/docs/wiki/Migration-Guide-0.6.md +96 -0
- data/docs/wiki/Performance.md +59 -0
- data/docs/wiki/Testing-Workers.md +127 -0
- data/docs/wiki/Worker-Types.md +171 -0
- data/docs/wiki/_Sidebar.md +13 -0
- data/lib/shifty/configuration.rb +27 -0
- data/lib/shifty/dsl.rb +35 -14
- data/lib/shifty/errors.rb +121 -0
- data/lib/shifty/gang.rb +25 -2
- data/lib/shifty/policy.rb +124 -0
- data/lib/shifty/rspec.rb +37 -0
- data/lib/shifty/testing.rb +82 -0
- data/lib/shifty/version.rb +1 -1
- data/lib/shifty/worker.rb +108 -5
- data/lib/shifty.rb +3 -0
- data/shifty.gemspec +6 -1
- metadata +56 -8
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
module Shifty
|
|
2
|
+
# Shared by Worker and Gang: declares a pipeline-level policy default on
|
|
3
|
+
# every node reachable upstream through the supply chain. A worker's own
|
|
4
|
+
# policy declaration (its contract) always wins over the pipeline default.
|
|
5
|
+
module PolicyDeclarable
|
|
6
|
+
def with_policy(policy_name)
|
|
7
|
+
policy_name = Policy.validate!(policy_name)
|
|
8
|
+
each_upstream_node { |node| node.pipeline_policy = policy_name }
|
|
9
|
+
self
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Locks the assembled topology: "the pipeline you composed is the
|
|
13
|
+
# pipeline that runs" becomes a guarantee. Rewiring (supply=, Gang
|
|
14
|
+
# append/roster mutation) raises FrozenError afterward. Worker
|
|
15
|
+
# closure/context state stays mutable — only the topology freezes.
|
|
16
|
+
#
|
|
17
|
+
# Call this on the pipeline's TAIL: like with_policy, it walks the
|
|
18
|
+
# supply chain upstream, so freezing a mid-chain node leaves
|
|
19
|
+
# everything downstream of it mutable. And use this, not the bare
|
|
20
|
+
# Object#freeze — freeze! first materializes each node's lazy task
|
|
21
|
+
# and Fiber; a bare freeze skips that and the first shift would
|
|
22
|
+
# raise FrozenError from deep inside the worker.
|
|
23
|
+
def freeze!
|
|
24
|
+
each_upstream_node { |node| node.freeze_topology_node! }
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def each_upstream_node
|
|
31
|
+
node = self
|
|
32
|
+
seen = {}.compare_by_identity
|
|
33
|
+
while node.respond_to?(:pipeline_policy=) && !seen[node]
|
|
34
|
+
seen[node] = true
|
|
35
|
+
yield node
|
|
36
|
+
node = node.supply
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Handoff policies govern how a value crosses a worker boundary.
|
|
42
|
+
# Each policy responds to #call(value, worker:) and returns the value
|
|
43
|
+
# the worker's task will receive.
|
|
44
|
+
module Policy
|
|
45
|
+
# Deeply freezes the value in place (zero copies) so any mutation,
|
|
46
|
+
# anywhere in the pipeline, raises at the worker that attempted it.
|
|
47
|
+
# IO-like values are rejected proactively: Ractor.make_shareable would
|
|
48
|
+
# otherwise freeze a live handle in place — a process-wide side effect
|
|
49
|
+
# on shared resources like loggers or $stdout.
|
|
50
|
+
Frozen = lambda do |value, worker:|
|
|
51
|
+
if value.is_a?(IO)
|
|
52
|
+
raise UnshareableValue.new(worker: worker, policy: :frozen, value: value)
|
|
53
|
+
end
|
|
54
|
+
begin
|
|
55
|
+
Ractor.make_shareable(value)
|
|
56
|
+
rescue Ractor::Error => e
|
|
57
|
+
raise UnshareableValue.new(worker: worker, policy: :frozen, value: value, cause: e)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Hands the task a private, mutable deep copy. Marshal is the mechanism
|
|
62
|
+
# because Ractor.make_shareable(copy: true) returns a *frozen* copy,
|
|
63
|
+
# which cannot satisfy the :isolated contract of a mutable scratch value.
|
|
64
|
+
Isolated = lambda do |value, worker:|
|
|
65
|
+
Marshal.load(Marshal.dump(value))
|
|
66
|
+
rescue TypeError => e
|
|
67
|
+
raise UnshareableValue.new(worker: worker, policy: :isolated, value: value, cause: e)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The escape hatch: the raw reference passes through untouched.
|
|
71
|
+
Shared = ->(value, worker:) { value }
|
|
72
|
+
|
|
73
|
+
TABLE = {
|
|
74
|
+
frozen: Frozen,
|
|
75
|
+
isolated: Isolated,
|
|
76
|
+
shared: Shared
|
|
77
|
+
}.freeze
|
|
78
|
+
|
|
79
|
+
ALIASES = {hardened: :isolated}.freeze
|
|
80
|
+
|
|
81
|
+
class << self
|
|
82
|
+
def resolve(name)
|
|
83
|
+
TABLE.fetch(canonical(name)) do
|
|
84
|
+
raise ArgumentError, "unknown policy #{name.inspect}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def canonical(name)
|
|
89
|
+
if ALIASES.key?(name)
|
|
90
|
+
replacement = ALIASES[name]
|
|
91
|
+
warn "[shifty] policy :#{name} is deprecated and will be " \
|
|
92
|
+
"removed in 1.0.0; use :#{replacement} instead."
|
|
93
|
+
replacement
|
|
94
|
+
else
|
|
95
|
+
name
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Canonicalizes and validates a policy name at declaration time, so
|
|
100
|
+
# a typo fails where it was written rather than at first shift.
|
|
101
|
+
def validate!(name)
|
|
102
|
+
canonical(name).tap do |canonical_name|
|
|
103
|
+
unless TABLE.key?(canonical_name)
|
|
104
|
+
raise ArgumentError, "unknown policy #{name.inspect}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Wraps a worker's supply so that values the task pulls directly
|
|
111
|
+
# (e.g. filter/batch/trailing workers calling supply.shift mid-task)
|
|
112
|
+
# cross the boundary under the same policy as the primary intake.
|
|
113
|
+
class Supply
|
|
114
|
+
def initialize(supply, worker)
|
|
115
|
+
@supply = supply
|
|
116
|
+
@worker = worker
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def shift
|
|
120
|
+
@worker.intake(@supply.shift)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/shifty/rspec.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "shifty/testing"
|
|
2
|
+
|
|
3
|
+
# Opt-in RSpec sugar for testing Shifty workers under handoff policies.
|
|
4
|
+
# `require "shifty/rspec"` from your spec_helper; assumes RSpec is loaded.
|
|
5
|
+
|
|
6
|
+
RSpec::Matchers.define :mutate_input do |input|
|
|
7
|
+
match do |worker|
|
|
8
|
+
Shifty::Testing.mutates_input?(worker, input)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
failure_message do |worker|
|
|
12
|
+
"expected the worker's task to mutate its input #{input.inspect}, but it did not"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
failure_message_when_negated do |worker|
|
|
16
|
+
"expected the worker's task not to mutate its input, but it mutated " \
|
|
17
|
+
"#{input.inspect}. It is only correct under policy :isolated (mutation " \
|
|
18
|
+
"stays local) or :shared (mutation is intentional); it will raise under :frozen."
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Runs the worker through the framework against deeply frozen input —
|
|
23
|
+
# the strictest policy — proving the task is non-destructive and therefore
|
|
24
|
+
# correct under every policy (§9.4, "test at the ceiling").
|
|
25
|
+
#
|
|
26
|
+
# Expects `worker` and `safe_input` to be defined with `let`.
|
|
27
|
+
RSpec.shared_examples "a policy-safe worker" do
|
|
28
|
+
it "processes a deeply frozen input without raising" do
|
|
29
|
+
expect {
|
|
30
|
+
Shifty::Testing.run(worker, inputs: [safe_input], policy: :frozen)
|
|
31
|
+
}.not_to raise_error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "does not mutate its input" do
|
|
35
|
+
expect(worker).not_to mutate_input(safe_input)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require "shifty"
|
|
2
|
+
|
|
3
|
+
module Shifty
|
|
4
|
+
# Test harness: runs a worker through the framework so unit tests
|
|
5
|
+
# exercise the same handoff policy the production pipeline will.
|
|
6
|
+
# Deliberately not loaded by `require "shifty"` — opt in with
|
|
7
|
+
# `require "shifty/testing"`.
|
|
8
|
+
module Testing
|
|
9
|
+
class << self
|
|
10
|
+
# Feeds the inputs to the worker via a source and collects its
|
|
11
|
+
# outputs until the end-of-stream sentinel (nil). The worker's
|
|
12
|
+
# declared/effective policy governs each handoff, exactly as in
|
|
13
|
+
# production; pass policy: to override it for policy-matrix tests.
|
|
14
|
+
def run(worker, inputs:, policy: nil, max_shifts: 10_000)
|
|
15
|
+
harness(worker, policy) do |subject|
|
|
16
|
+
subject.supply = source_for(inputs)
|
|
17
|
+
outputs = []
|
|
18
|
+
shifts = 0
|
|
19
|
+
until (value = subject.shift).nil?
|
|
20
|
+
outputs << value
|
|
21
|
+
if (shifts += 1) > max_shifts
|
|
22
|
+
raise Error, "Shifty::Testing.run exceeded #{max_shifts} shifts " \
|
|
23
|
+
"without seeing the nil end-of-stream sentinel. The worker's " \
|
|
24
|
+
"task probably converts nil into a non-nil value; let nil " \
|
|
25
|
+
"pass through (e.g. `value && ...`), or raise max_shifts:."
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
outputs
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The mutation detector (§6.4): hands the task a private mutable
|
|
33
|
+
# deep copy and reports whether the task changed it — surfacing
|
|
34
|
+
# mutation even when the current policy permits or hides it.
|
|
35
|
+
def mutates_input?(worker, input)
|
|
36
|
+
copy = begin
|
|
37
|
+
Marshal.load(Marshal.dump(input))
|
|
38
|
+
rescue TypeError => e
|
|
39
|
+
raise Error, "Shifty::Testing.mutates_input? needs a deep-copyable " \
|
|
40
|
+
"(Marshal-dumpable) input, but got an instance of #{input.class} " \
|
|
41
|
+
"(#{e.message})."
|
|
42
|
+
end
|
|
43
|
+
baseline = Marshal.dump(copy)
|
|
44
|
+
harness(worker, :shared) do |subject|
|
|
45
|
+
subject.supply = source_for([copy])
|
|
46
|
+
subject.shift
|
|
47
|
+
end
|
|
48
|
+
Marshal.dump(copy) != baseline
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Temporarily rewires the caller's actual worker (a dup would defeat
|
|
54
|
+
# task closures that reference their own worker, e.g. side_worker's
|
|
55
|
+
# policy check) and restores its policy, supply, and Fiber afterward,
|
|
56
|
+
# so the harness never leaves a mark on the worker under test.
|
|
57
|
+
def harness(worker, policy)
|
|
58
|
+
saved = {
|
|
59
|
+
policy: worker.instance_variable_get(:@policy),
|
|
60
|
+
supply: worker.instance_variable_get(:@supply),
|
|
61
|
+
fiber: worker.instance_variable_get(:@my_little_machine)
|
|
62
|
+
}
|
|
63
|
+
worker.instance_variable_set(:@policy, Policy.validate!(policy)) if policy
|
|
64
|
+
worker.instance_variable_set(:@my_little_machine, nil)
|
|
65
|
+
worker.instance_variable_set(:@supply, nil)
|
|
66
|
+
yield worker
|
|
67
|
+
ensure
|
|
68
|
+
worker.instance_variable_set(:@policy, saved[:policy])
|
|
69
|
+
worker.instance_variable_set(:@supply, saved[:supply])
|
|
70
|
+
worker.instance_variable_set(:@my_little_machine, saved[:fiber])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def source_for(inputs)
|
|
74
|
+
series = inputs.dup
|
|
75
|
+
Worker.new(tags: [:source]) do
|
|
76
|
+
series.each { |input| Fiber.yield input }
|
|
77
|
+
loop { Fiber.yield nil }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/shifty/version.rb
CHANGED
data/lib/shifty/worker.rb
CHANGED
|
@@ -2,22 +2,60 @@ require "ostruct"
|
|
|
2
2
|
require "shifty/taggable"
|
|
3
3
|
|
|
4
4
|
module Shifty
|
|
5
|
+
# Shifty::Worker uses Ruby Fibers for cooperative multitasking.
|
|
6
|
+
# This results in a single-threaded execution model where workers
|
|
7
|
+
# explicitly yield control to one another. This model is chosen for its
|
|
8
|
+
# simplicity and suitability for creating chainable data processing
|
|
9
|
+
# pipelines, where each worker performs a specific task and passes
|
|
10
|
+
# its output to the next worker in the chain.
|
|
5
11
|
class Worker
|
|
6
|
-
attr_reader :supply, :tags
|
|
12
|
+
attr_reader :supply, :tags, :name
|
|
13
|
+
attr_accessor :pipeline_policy
|
|
7
14
|
|
|
8
15
|
include Shifty::Taggable
|
|
16
|
+
include Shifty::PolicyDeclarable
|
|
9
17
|
|
|
10
18
|
def initialize(p = {}, &block)
|
|
11
19
|
@supply = p[:supply]
|
|
12
20
|
@task = block || p[:task]
|
|
13
21
|
@context = p[:context] || OpenStruct.new
|
|
22
|
+
@policy = Policy.validate!(p[:policy]) if p[:policy]
|
|
23
|
+
@name = p[:name]
|
|
14
24
|
self.criteria = p[:criteria]
|
|
15
25
|
self.tags = p[:tags]
|
|
16
26
|
end
|
|
17
27
|
|
|
28
|
+
def effective_policy
|
|
29
|
+
@policy || pipeline_policy || Shifty.config.default_policy
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Applies this worker's effective policy to a value crossing its
|
|
33
|
+
# boundary. Public because Policy::Supply routes a task's own
|
|
34
|
+
# supply.shift calls back through it.
|
|
35
|
+
def intake(value)
|
|
36
|
+
Policy.resolve(effective_policy).call(value, worker: self)
|
|
37
|
+
end
|
|
38
|
+
|
|
18
39
|
def shift
|
|
19
40
|
ensure_ready_to_work!
|
|
20
41
|
workflow.resume
|
|
42
|
+
rescue PolicyError
|
|
43
|
+
# The raising Fiber is terminated and can never be resumed; discard
|
|
44
|
+
# it so a caller that rescues the violation can keep shifting.
|
|
45
|
+
# Closure and context state survive — only the loop restarts.
|
|
46
|
+
# (A #freeze!-d worker can't rebuild its Fiber, so a violation
|
|
47
|
+
# there ends the pipeline — the topology guarantee wins.)
|
|
48
|
+
@my_little_machine = nil unless frozen?
|
|
49
|
+
raise
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Materializes lazy state (default task, the Fiber) so that freezing
|
|
53
|
+
# this instance cannot surprise it later with a FrozenError on ivar
|
|
54
|
+
# assignment mid-shift, then freezes it.
|
|
55
|
+
def freeze_topology_node!
|
|
56
|
+
ensure_ready_to_work!
|
|
57
|
+
workflow
|
|
58
|
+
freeze
|
|
21
59
|
end
|
|
22
60
|
|
|
23
61
|
def ready_to_work?
|
|
@@ -50,11 +88,18 @@ module Shifty
|
|
|
50
88
|
end
|
|
51
89
|
|
|
52
90
|
def workflow
|
|
91
|
+
# This is the core of the worker's execution, managed by a Fiber.
|
|
92
|
+
# The Fiber allows the worker to pause its execution (yield) and
|
|
93
|
+
# be resumed later, enabling cooperative multitasking.
|
|
94
|
+
#
|
|
95
|
+
# Handoff policy is applied at intake — the moment this worker pulls
|
|
96
|
+
# a value across its boundary — which is the single seam every value
|
|
97
|
+
# crosses regardless of how many times an upstream task yielded.
|
|
53
98
|
@my_little_machine ||= Fiber.new {
|
|
54
99
|
loop do
|
|
55
|
-
value = supply&.shift
|
|
100
|
+
value = intake(supply&.shift)
|
|
56
101
|
if criteria_passes?
|
|
57
|
-
Fiber.yield
|
|
102
|
+
Fiber.yield perform_task(value)
|
|
58
103
|
else
|
|
59
104
|
Fiber.yield value
|
|
60
105
|
end
|
|
@@ -62,6 +107,22 @@ module Shifty
|
|
|
62
107
|
}
|
|
63
108
|
end
|
|
64
109
|
|
|
110
|
+
def perform_task(value)
|
|
111
|
+
@task.call(value, policy_supply, @context)
|
|
112
|
+
rescue FrozenError => e
|
|
113
|
+
raise PolicyViolation.new(
|
|
114
|
+
worker: self,
|
|
115
|
+
policy: effective_policy,
|
|
116
|
+
receiver: e.receiver,
|
|
117
|
+
value: value,
|
|
118
|
+
cause: e
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def policy_supply
|
|
123
|
+
supply && Policy::Supply.new(supply, self)
|
|
124
|
+
end
|
|
125
|
+
|
|
65
126
|
def default_task
|
|
66
127
|
proc { |value| value }
|
|
67
128
|
end
|
|
@@ -77,7 +138,49 @@ module Shifty
|
|
|
77
138
|
def task_method_accepts_a_value?
|
|
78
139
|
method(:task).arity > 0
|
|
79
140
|
end
|
|
80
|
-
end
|
|
81
141
|
|
|
82
|
-
|
|
142
|
+
# ## Concurrency and Thread Safety
|
|
143
|
+
#
|
|
144
|
+
# ### Concurrency Model
|
|
145
|
+
# Shifty utilizes Ruby Fibers to achieve cooperative multitasking. This means
|
|
146
|
+
# that workers voluntarily yield control, allowing other workers to execute.
|
|
147
|
+
# The entire processing pipeline runs within a single thread, which removes
|
|
148
|
+
# an entire class of *preemptive* concurrency hazards (races on shared
|
|
149
|
+
# objects, the need for mutexes around Shifty's own state).
|
|
150
|
+
#
|
|
151
|
+
# Note that single-threading does NOT make the data flowing between workers
|
|
152
|
+
# safe on its own: because each value passes through every worker before the
|
|
153
|
+
# next value begins, a worker that mutates a handed-off value could silently
|
|
154
|
+
# corrupt what downstream workers observe. That hazard is orthogonal to
|
|
155
|
+
# threading and is governed by handoff immutability policies: values are
|
|
156
|
+
# deeply frozen at intake by default (:frozen), with :isolated and :shared
|
|
157
|
+
# opt-outs per worker/pipeline/global (see lib/shifty/policy.rb and the
|
|
158
|
+
# project wiki's Handoff-Policies page).
|
|
159
|
+
#
|
|
160
|
+
# ### Alternatives (Threads/Ractors)
|
|
161
|
+
# Threads and Ractors are not used for parallelism *yet*. Shifty's current
|
|
162
|
+
# goal is an easy-to-use framework for sequential data pipelines, and a
|
|
163
|
+
# single-threaded Fiber model serves that directly without the overhead of
|
|
164
|
+
# mutexes (Threads) or the sharing restrictions of Ractors. This is a
|
|
165
|
+
# scoping decision, not a rejection: the default :frozen policy's deeply
|
|
166
|
+
# frozen, shareable handoff values are deliberately Ractor-compatible and
|
|
167
|
+
# lay the groundwork for a future Ractor-backed worker type. (Fibers cannot
|
|
168
|
+
# cross Ractor boundaries, so such a worker would be a distinct type, not a
|
|
169
|
+
# retrofit of this one.)
|
|
170
|
+
#
|
|
171
|
+
# ### Thread Safety
|
|
172
|
+
# `Shifty::Worker` instances, and by extension `Shifty::Gang` or
|
|
173
|
+
# `Shifty::Roster` instances that manage these workers, are **not**
|
|
174
|
+
# inherently thread-safe if they are shared and modified across multiple
|
|
175
|
+
# user-created native threads. If users choose to integrate Shifty components
|
|
176
|
+
# into a multi-threaded application (e.g., processing multiple independent
|
|
177
|
+
# Shifty pipelines in parallel using separate Threads), they are responsible
|
|
178
|
+
# for implementing appropriate synchronization mechanisms (like mutexes)
|
|
179
|
+
# to protect shared Shifty objects from concurrent access and modification.
|
|
180
|
+
# (Handoff immutability policies govern only the *values* passed between
|
|
181
|
+
# workers, not the worker/pipeline objects themselves or their closure
|
|
182
|
+
# state — those remain the user's responsibility across threads.)
|
|
183
|
+
# For typical use cases where a Shifty pipeline is built and run, no
|
|
184
|
+
# external threading is usually involved by the user.
|
|
185
|
+
end
|
|
83
186
|
end
|
data/lib/shifty.rb
CHANGED
data/shifty.gemspec
CHANGED
|
@@ -20,10 +20,15 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
21
21
|
spec.require_paths = ["lib"]
|
|
22
22
|
|
|
23
|
+
spec.required_ruby_version = ">= 3.2"
|
|
24
|
+
|
|
25
|
+
spec.add_dependency "ostruct"
|
|
26
|
+
|
|
23
27
|
spec.add_development_dependency "rake"
|
|
24
28
|
spec.add_development_dependency "rspec", "~> 3.9"
|
|
25
29
|
spec.add_development_dependency "rspec-given", "~> 3.8"
|
|
26
30
|
spec.add_development_dependency "pry"
|
|
27
31
|
spec.add_development_dependency "standard", ">= 1.28.2"
|
|
28
|
-
spec.add_development_dependency "
|
|
32
|
+
spec.add_development_dependency "simplecov"
|
|
33
|
+
spec.add_development_dependency "benchmark-ips"
|
|
29
34
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shifty
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joel Helbling
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ostruct
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
13
26
|
- !ruby/object:Gem::Dependency
|
|
14
27
|
name: rake
|
|
15
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -81,7 +94,21 @@ dependencies:
|
|
|
81
94
|
- !ruby/object:Gem::Version
|
|
82
95
|
version: 1.28.2
|
|
83
96
|
- !ruby/object:Gem::Dependency
|
|
84
|
-
name:
|
|
97
|
+
name: simplecov
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: benchmark-ips
|
|
85
112
|
requirement: !ruby/object:Gem::Requirement
|
|
86
113
|
requirements:
|
|
87
114
|
- - ">="
|
|
@@ -106,20 +133,43 @@ files:
|
|
|
106
133
|
- ".gitignore"
|
|
107
134
|
- ".rspec"
|
|
108
135
|
- ".standard.yml"
|
|
136
|
+
- CHANGELOG.md
|
|
109
137
|
- Gemfile
|
|
110
138
|
- Guardfile
|
|
111
139
|
- LICENSE.txt
|
|
112
140
|
- README.md
|
|
113
141
|
- Rakefile
|
|
114
142
|
- _config.yml
|
|
143
|
+
- benchmark/RESULTS.md
|
|
144
|
+
- benchmark/handoff_policies.rb
|
|
115
145
|
- bin/console
|
|
116
146
|
- bin/setup
|
|
147
|
+
- docs/planning/artifacts/.discovery-notes.md
|
|
148
|
+
- docs/planning/artifacts/.round1-specialist-outputs.md
|
|
149
|
+
- docs/planning/artifacts/implementation-decision-log.md
|
|
150
|
+
- docs/planning/artifacts/implementation-iteration-history.md
|
|
151
|
+
- docs/planning/feature-implementation-plan.md
|
|
152
|
+
- docs/planning/handoff-immutability-policies.md
|
|
117
153
|
- docs/shifty/worker.md
|
|
154
|
+
- docs/use_cases.md
|
|
155
|
+
- docs/wiki/Coding-Idioms-Under-Frozen.md
|
|
156
|
+
- docs/wiki/Handoff-Policies.md
|
|
157
|
+
- docs/wiki/Home.md
|
|
158
|
+
- docs/wiki/Migration-Guide-0.6.md
|
|
159
|
+
- docs/wiki/Performance.md
|
|
160
|
+
- docs/wiki/Testing-Workers.md
|
|
161
|
+
- docs/wiki/Worker-Types.md
|
|
162
|
+
- docs/wiki/_Sidebar.md
|
|
118
163
|
- lib/shifty.rb
|
|
164
|
+
- lib/shifty/configuration.rb
|
|
119
165
|
- lib/shifty/dsl.rb
|
|
166
|
+
- lib/shifty/errors.rb
|
|
120
167
|
- lib/shifty/gang.rb
|
|
168
|
+
- lib/shifty/policy.rb
|
|
121
169
|
- lib/shifty/roster.rb
|
|
170
|
+
- lib/shifty/rspec.rb
|
|
122
171
|
- lib/shifty/taggable.rb
|
|
172
|
+
- lib/shifty/testing.rb
|
|
123
173
|
- lib/shifty/version.rb
|
|
124
174
|
- lib/shifty/worker.rb
|
|
125
175
|
- pkg/.gitkeep
|
|
@@ -128,7 +178,6 @@ homepage: https://github.com/joelhelbling/shifty
|
|
|
128
178
|
licenses:
|
|
129
179
|
- MIT
|
|
130
180
|
metadata: {}
|
|
131
|
-
post_install_message:
|
|
132
181
|
rdoc_options: []
|
|
133
182
|
require_paths:
|
|
134
183
|
- lib
|
|
@@ -136,15 +185,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
136
185
|
requirements:
|
|
137
186
|
- - ">="
|
|
138
187
|
- !ruby/object:Gem::Version
|
|
139
|
-
version: '
|
|
188
|
+
version: '3.2'
|
|
140
189
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
190
|
requirements:
|
|
142
191
|
- - ">="
|
|
143
192
|
- !ruby/object:Gem::Version
|
|
144
193
|
version: '0'
|
|
145
194
|
requirements: []
|
|
146
|
-
rubygems_version:
|
|
147
|
-
signing_key:
|
|
195
|
+
rubygems_version: 4.0.15
|
|
148
196
|
specification_version: 4
|
|
149
197
|
summary: A functional framework aimed at extremely low coupling
|
|
150
198
|
test_files: []
|