action_operation 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +261 -0
- data/Rakefile +12 -0
- data/lib/action_operation.rb +123 -0
- data/lib/action_operation/error.rb +7 -0
- data/lib/action_operation/error/missing_error.rb +9 -0
- data/lib/action_operation/error/missing_schema_for_task.rb +9 -0
- data/lib/action_operation/error/missing_task.rb +9 -0
- data/lib/action_operation/types.rb +5 -0
- data/lib/action_operation/version.rb +3 -0
- data/lib/action_operation/version_spec.rb +7 -0
- data/lib/action_operation_spec.rb +50 -0
- metadata +171 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ac4f5bc88c4689c01c68d6761631feb15ff1b17e54274be64223b3e9d6190f50
|
4
|
+
data.tar.gz: ccced431a6f77242476bc749a94c5d3a742f8b91cb24ced140543c14f9cff55f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1d93d27a24c101660023688e238d7d84743c5586b901dc73942524422f7317587ac9b38925967dbbefd0ad1193a4dc9b39fd295d4b32a1ac280dfceabcca6507
|
7
|
+
data.tar.gz: 5f9f49b17ca20437080e52466d07195f19fe752adb52237a196b3e6011d39d3ca950b4bd749640be5f62c0eb47db32d419d54dfd5b132efb70d872f8ad0f6b9d
|
data/README.md
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
# action_operation
|
2
|
+
|
3
|
+
- [![Build](http://img.shields.io/travis-ci/krainboltgreene/action_operation.rb.svg?style=flat-square)](https://travis-ci.org/krainboltgreene/action_operation.rb)
|
4
|
+
- [![Downloads](http://img.shields.io/gem/dtv/action_operation.svg?style=flat-square)](https://rubygems.org/gems/action_operation)
|
5
|
+
- [![Version](http://img.shields.io/gem/v/action_operation.svg?style=flat-square)](https://rubygems.org/gems/action_operation)
|
6
|
+
|
7
|
+
|
8
|
+
A simple set of right-to-left operations, similar to many other gems out there.
|
9
|
+
|
10
|
+
|
11
|
+
## Using
|
12
|
+
|
13
|
+
Alright, so you have some business logic you'd like to control in your application. You've found that putting in the controllers sucks, because an application is more than it's HTTP requests. You've found that putting in the model sucks, because there's not enough context and does too many things. You've found that "service classes" have no form or shape and get way too out of hand.
|
14
|
+
|
15
|
+
ActionOperation is here to help! This, like many others before and after, gives a concise way to describe a series of business requirements. It has as much context as you give it and only does the thing you need it to do. It can be used anywhere and everywhere.
|
16
|
+
|
17
|
+
First let's make our operation:
|
18
|
+
|
19
|
+
``` ruby
|
20
|
+
class AddToCartOperation
|
21
|
+
include ActionOperation
|
22
|
+
|
23
|
+
task :check_for_missing_product
|
24
|
+
task :carbon_copy_cart_item
|
25
|
+
task :lock
|
26
|
+
task :persist
|
27
|
+
task :publish
|
28
|
+
error :notify, catch: ProductMissingFromCartItemError
|
29
|
+
error :reraise
|
30
|
+
|
31
|
+
state :check_for_missing_product do
|
32
|
+
field :cart_item, type: Types.Instance(CartItem)
|
33
|
+
end
|
34
|
+
step :check_for_missing_product do |state|
|
35
|
+
raise ProductMissingFromCartItemError if state.cart_item.product.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
state :carbon_copy_cart_item do
|
39
|
+
field :cart_item, type: Types.Instance(CartItem)
|
40
|
+
end
|
41
|
+
step :carbon_copy_cart_item do |state|
|
42
|
+
state.cart_item.carbon_copy
|
43
|
+
end
|
44
|
+
|
45
|
+
state :lock do
|
46
|
+
field :cart_item, type: Types.Instance(CartItem)
|
47
|
+
end
|
48
|
+
step :lock do |state|
|
49
|
+
GlobalLock.(state.cart_item.owner, state.cart_item, expires_in: 15.minutes)
|
50
|
+
end
|
51
|
+
|
52
|
+
state :persist do
|
53
|
+
field :cart_item, type: Types.Instance(CartItem)
|
54
|
+
end
|
55
|
+
step :persist do |state|
|
56
|
+
CartItem.transaction do
|
57
|
+
state.cart_item.save!
|
58
|
+
end
|
59
|
+
|
60
|
+
fresh(current_account: state.cart_item.owner, cart_item: state.cart_item)
|
61
|
+
end
|
62
|
+
|
63
|
+
state :publish do
|
64
|
+
field :cart_item, type: Types.Instance(CartItem)
|
65
|
+
field :current_account, type: Types.Instance(Account)
|
66
|
+
end
|
67
|
+
step :publish do |state|
|
68
|
+
CartItemPickedMessage.(subject: state.cart_item, to: state.current_account).via_pubsub.deliver_later!
|
69
|
+
end
|
70
|
+
|
71
|
+
step :notify do |exception|
|
72
|
+
Bugsnag.notify(exception)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
There's a lot to take in here, so lets go through each point:
|
78
|
+
|
79
|
+
``` ruby
|
80
|
+
class AddToCartOperation
|
81
|
+
# ...
|
82
|
+
|
83
|
+
task :check_for_missing_product
|
84
|
+
task :carbon_copy_cart_item
|
85
|
+
task :lock
|
86
|
+
task :persist
|
87
|
+
task :publish
|
88
|
+
error :notify, catch: ProductMissingFromCartItemError
|
89
|
+
error :reraise
|
90
|
+
|
91
|
+
# ...
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
These are the steps our process will take. Each `task` call is *in the order it is listed*, which means that `check_for_missing_product` will happen before `carbon_copy_cart_item`. Each `error` is also *in the order it is listed*, but they only trigger when one of the `task` raises an exception. In this case, we only want to `notify` when there's something seriously wrong!
|
96
|
+
|
97
|
+
Finally, before we leave, notice the `reraise` error step. This is built in to the operation layer so that you can easily pass the buck to whomever owns the action currently.
|
98
|
+
|
99
|
+
Okay, so on to our first step:
|
100
|
+
|
101
|
+
``` ruby
|
102
|
+
class AddToCartOperation
|
103
|
+
# ...
|
104
|
+
|
105
|
+
state :check_for_missing_product do
|
106
|
+
field :cart_item, type: Types.Instance(CartItem)
|
107
|
+
end
|
108
|
+
step :check_for_missing_product do |state|
|
109
|
+
raise ProductMissingFromCartItemError if state.cart_item.product.nil?
|
110
|
+
end
|
111
|
+
|
112
|
+
# ...
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
There's two things we want to talk about there and the first is `state`. It defines the shape of the *immutable* state that the step will receive. We use [smart_params](https://github.com/krainboltgreene/smart_params.rb) which means each field is typed with [dry-types](https://github.com/dry-rb/dry-types). Read up on both of those libraries for more fine grained control over your data.
|
117
|
+
|
118
|
+
Second is the `step` definition itself which provides a `state` object that is based on the schema defined above. You have four choices on what you can do in a `step`. You can:
|
119
|
+
|
120
|
+
- Return any value, which will simply proceed to the next step.
|
121
|
+
- Raise an exception, which will move you into the left track (that uses `error` steps)
|
122
|
+
- Return a fresh state, which will be described below
|
123
|
+
- Return a drift instruction, which will be described below
|
124
|
+
|
125
|
+
### Fresh State
|
126
|
+
|
127
|
+
Sometimes you want to change the data that is passed around after a step is completed. To achieve this functionality we provide the `fresh()` function:
|
128
|
+
|
129
|
+
``` ruby
|
130
|
+
class AddToCartOperation
|
131
|
+
# ...
|
132
|
+
|
133
|
+
step :persist do |state|
|
134
|
+
CartItem.transaction do
|
135
|
+
state.cart_item.save!
|
136
|
+
end
|
137
|
+
|
138
|
+
fresh(current_account: state.cart_item.owner, cart_item: state.cart_item)
|
139
|
+
end
|
140
|
+
|
141
|
+
# ...
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
This is the only way to "change" the shape of the state.
|
146
|
+
|
147
|
+
|
148
|
+
### Receivers
|
149
|
+
|
150
|
+
Sometimes you need to share functionality across multiple operations. You can do this via modules and inheritance like normal or you can use our specialized interface:
|
151
|
+
|
152
|
+
``` ruby
|
153
|
+
class DocumentUploadOperation
|
154
|
+
include ActionOperation
|
155
|
+
|
156
|
+
task :upload_to_s3, receiver: S3UploadOperation
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
This will give the `DocumentUploadOperation` a task that is on another operation! Sometimes that other task has a different name, so we also provide aliasing:
|
161
|
+
|
162
|
+
``` ruby
|
163
|
+
class DocumentUploadOperation
|
164
|
+
include ActionOperation
|
165
|
+
|
166
|
+
task :upload_to_s3, receiver: S3UploadOperation, :upload
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
So when `DocumentUploadOperation` finally gets to the `upload_to_s3` task it's actually calling the `S3UploadOperation` task called `upload`. More on why this is useful in the next section.
|
171
|
+
|
172
|
+
|
173
|
+
### Drifting
|
174
|
+
|
175
|
+
Alright, so lets say you have a business requirement to upload important documents to the cloud. You have multiple providers (S3, Azure, and DigitalOcean Spaces) and you want to make sure it gets pushed to at least one. Here's how you would write this:
|
176
|
+
|
177
|
+
``` ruby
|
178
|
+
class S3UploadOperation
|
179
|
+
include ActionOperation
|
180
|
+
|
181
|
+
task :upload
|
182
|
+
|
183
|
+
state :upload do
|
184
|
+
field :document, type: Types.Instance(Document)
|
185
|
+
end
|
186
|
+
step :upload do |state|
|
187
|
+
fresh(document: state.document, location: S3.push(state.document))
|
188
|
+
rescue StandardError => exception
|
189
|
+
raise FailedUploadError
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
```
|
194
|
+
|
195
|
+
``` ruby
|
196
|
+
class DocumentUploadOperation
|
197
|
+
include ActionOperation
|
198
|
+
|
199
|
+
task :upload_to_s3, receiver: S3UploadOperation, as: :upload
|
200
|
+
task :upload_to_azure, receiver: AzureUploadOperation, as: :upload, required: false
|
201
|
+
task :upload_to_spaces, receiver: SpacesUploadOperation, as: :upload, required: false
|
202
|
+
task :publish
|
203
|
+
error :retry, catch: FailedUploadError
|
204
|
+
error :reraise
|
205
|
+
|
206
|
+
step :retry do |exception, _, step|
|
207
|
+
case step
|
208
|
+
when :upload_to_s3 then drift(to: :upload_to_azure)
|
209
|
+
when :upload_to_azure then drift(to: :upload_to_spaces)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
state :publish do
|
214
|
+
field :document, type: Types.Instance(Document)
|
215
|
+
field :location, type: Types::Strict::String
|
216
|
+
end
|
217
|
+
step :publish do |state|
|
218
|
+
DocumentSuccessfullyUploadedMessage.(owner: state.document.owner, location: state.location).via_pubsub.deliver_later!
|
219
|
+
end
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
So here's how this works:
|
224
|
+
|
225
|
+
1. First we call `upload_to_s3`, which actually talks to `S3UploadOperation/upload`, but for some reason this fails and gets caught by `failed_upload`, which bubbles up a specific exception that we catch with `DocumentUploadOperation/retry`
|
226
|
+
2. `retry` looks at the last known step and then drifts to `upload_to_azure`, which functions just like above.
|
227
|
+
3. Then somehow we fail to upload to Azure, so we repeat and retry with DigitalOcean Spaces.
|
228
|
+
4. We fail to even upload that, which means the next error step gets called (`reraise`) giving control back to the owner of the operation
|
229
|
+
|
230
|
+
|
231
|
+
However, if it finishes successfully we get to push a notification to the document owner in `finish`.
|
232
|
+
|
233
|
+
|
234
|
+
### Understanding the design
|
235
|
+
|
236
|
+
Each task is a map function wrapped in a HOC for handling the return data. The annotation of each task is `state -> mixed | state` and the HOC is `state -> (state -> mixed | state) -> state`. `error` is like a task, but instead: `exception -> mixed` wrapped in a HOC that matches `exception -> (exception -> mixed) -> exception`.
|
237
|
+
|
238
|
+
|
239
|
+
## Installing
|
240
|
+
|
241
|
+
Add this line to your application's Gemfile:
|
242
|
+
|
243
|
+
gem "action_operation", "1.0.0"
|
244
|
+
|
245
|
+
And then execute:
|
246
|
+
|
247
|
+
$ bundle
|
248
|
+
|
249
|
+
Or install it yourself with:
|
250
|
+
|
251
|
+
$ gem install action_operation
|
252
|
+
|
253
|
+
|
254
|
+
## Contributing
|
255
|
+
|
256
|
+
1. Read the [Code of Conduct](/CONDUCT.md)
|
257
|
+
2. Fork it
|
258
|
+
3. Create your feature branch (`git checkout -b my-new-feature`)
|
259
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
260
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
261
|
+
6. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
desc "Run all the tests in spec"
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |let|
|
8
|
+
let.pattern = "lib/**{,/*/**}/*_spec.rb"
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "Default: run tests"
|
12
|
+
task default: :spec
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
require "active_support/core_ext/array"
|
3
|
+
require "smart_params"
|
4
|
+
|
5
|
+
module ActionOperation
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
require_relative "action_operation/version"
|
9
|
+
require_relative "action_operation/types"
|
10
|
+
require_relative "action_operation/error"
|
11
|
+
|
12
|
+
State = Struct.new(:raw)
|
13
|
+
Drift = Struct.new(:to)
|
14
|
+
|
15
|
+
attr_reader :raw
|
16
|
+
attr_reader :state
|
17
|
+
attr_reader :step
|
18
|
+
|
19
|
+
def initialize(raw:)
|
20
|
+
@raw = raw
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(forced: nil)
|
24
|
+
right.from(forced || 0).reduce(state || raw) do |state, function|
|
25
|
+
next state unless function.required || (forced && right.at(forced) == function)
|
26
|
+
|
27
|
+
# NOTE: We store this so we can go drift back if an error tells us to
|
28
|
+
@state = state
|
29
|
+
|
30
|
+
# NOTE: We store this so an error step can ask for the last ran step
|
31
|
+
@step = function.name
|
32
|
+
|
33
|
+
raise Error::MissingTask, function unless function.receiver.steps.key?(function.as)
|
34
|
+
raise Error::MissingSchemaForTask, function unless function.receiver.schemas.key?(function.as)
|
35
|
+
|
36
|
+
value = instance_exec(function.receiver.schemas.fetch(function.as).new(state), &function.receiver.steps.fetch(function.as))
|
37
|
+
|
38
|
+
case value
|
39
|
+
when State then value.raw
|
40
|
+
when Drift then break call(forced: right.find_index { |step| step.name == value.to })
|
41
|
+
else state
|
42
|
+
end
|
43
|
+
end
|
44
|
+
rescue *left.select(&:catch).map(&:catch).uniq => handled_exception
|
45
|
+
left.select do |failure|
|
46
|
+
failure.catch === handled_exception
|
47
|
+
end.reduce(handled_exception) do |exception, function|
|
48
|
+
raise Error::MissingError, function unless function.receiver.steps.key?(function.as)
|
49
|
+
|
50
|
+
value = instance_exec(exception, @state, @step, &function.receiver.steps.fetch(function.as))
|
51
|
+
|
52
|
+
if value.kind_of?(Drift)
|
53
|
+
break call(forced: right.find_index { |step| step.name == value.to })
|
54
|
+
else
|
55
|
+
exception
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def fresh(raw)
|
61
|
+
State.new(raw)
|
62
|
+
end
|
63
|
+
|
64
|
+
def drift(to:)
|
65
|
+
Drift.new(to)
|
66
|
+
end
|
67
|
+
|
68
|
+
private def left
|
69
|
+
self.class.left
|
70
|
+
end
|
71
|
+
|
72
|
+
private def right
|
73
|
+
self.class.right
|
74
|
+
end
|
75
|
+
|
76
|
+
included do
|
77
|
+
step :reraise do |exception|
|
78
|
+
raise exception
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class_methods do
|
83
|
+
def state(name, &structure)
|
84
|
+
schemas[name] = Class.new do
|
85
|
+
include(SmartParams)
|
86
|
+
|
87
|
+
schema type: SmartParams::Strict::Hash, &structure
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def task(name, receiver: self, as: name, required: true)
|
92
|
+
right.<<(OpenStruct.new({name: name, as: as, receiver: receiver || self, required: required}))
|
93
|
+
end
|
94
|
+
|
95
|
+
def error(name, receiver: self, catch: StandardError, as: name)
|
96
|
+
left.<<(OpenStruct.new({name: name, as: as, receiver: receiver || self, catch: catch || StandardError}))
|
97
|
+
end
|
98
|
+
|
99
|
+
def step(name, &process)
|
100
|
+
steps[name] = process
|
101
|
+
end
|
102
|
+
|
103
|
+
def call(raw = {})
|
104
|
+
new(raw: raw).call
|
105
|
+
end
|
106
|
+
|
107
|
+
def right
|
108
|
+
@right ||= Array.new
|
109
|
+
end
|
110
|
+
|
111
|
+
def left
|
112
|
+
@left ||= Array.new
|
113
|
+
end
|
114
|
+
|
115
|
+
def schemas
|
116
|
+
@schemas ||= Hash.new
|
117
|
+
end
|
118
|
+
|
119
|
+
def steps
|
120
|
+
@steps ||= Hash.new
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe ActionOperation do
|
4
|
+
let(:operation) { DocumentUploadOperation }
|
5
|
+
|
6
|
+
describe "#call" do
|
7
|
+
subject { operation.call(arguments) }
|
8
|
+
|
9
|
+
context "with the right arguments" do
|
10
|
+
let(:arguments) {{document: Document.new}}
|
11
|
+
|
12
|
+
it "works" do
|
13
|
+
expect(subject).to match(hash_including(document: a_kind_of(Document), location: "some.s3"))
|
14
|
+
end
|
15
|
+
|
16
|
+
context "drifting from s3" do
|
17
|
+
before do
|
18
|
+
allow(S3).to receive(:push).and_raise(StandardError, 'something')
|
19
|
+
end
|
20
|
+
|
21
|
+
it "works" do
|
22
|
+
expect(subject).to match(hash_including(document: a_kind_of(Document), location: "some.azure"))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "drifting from s3 and azure" do
|
27
|
+
before do
|
28
|
+
allow(S3).to receive(:push).and_raise(StandardError, 'something')
|
29
|
+
allow(Azure).to receive(:push).and_raise(StandardError, 'something')
|
30
|
+
end
|
31
|
+
|
32
|
+
it "works" do
|
33
|
+
expect(subject).to match(hash_including(document: a_kind_of(Document), location: "some.spaces"))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "drifting from s3 and azure and spacs" do
|
38
|
+
before do
|
39
|
+
allow(S3).to receive(:push).and_raise(StandardError, 'something')
|
40
|
+
allow(Azure).to receive(:push).and_raise(StandardError, 'something')
|
41
|
+
allow(Spaces).to receive(:push).and_raise(StandardError, 'something')
|
42
|
+
end
|
43
|
+
|
44
|
+
it "works" do
|
45
|
+
expect{subject}.to raise_exception(FailedUploadError)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: action_operation
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kurtis Rainbolt-Greene
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-05-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '12.2'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '12.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.11'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.11'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry-doc
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.11'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.11'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activesupport
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 4.0.0
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '4.1'
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 5.0.0
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '5.1'
|
99
|
+
type: :runtime
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: 4.0.0
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '4.1'
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: 5.0.0
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '5.1'
|
115
|
+
- !ruby/object:Gem::Dependency
|
116
|
+
name: smart_params
|
117
|
+
requirement: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '2.0'
|
122
|
+
type: :runtime
|
123
|
+
prerelease: false
|
124
|
+
version_requirements: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '2.0'
|
129
|
+
description: A set of BPMN style operation logic
|
130
|
+
email:
|
131
|
+
- kurtis@rainbolt-greene.online
|
132
|
+
executables: []
|
133
|
+
extensions: []
|
134
|
+
extra_rdoc_files: []
|
135
|
+
files:
|
136
|
+
- README.md
|
137
|
+
- Rakefile
|
138
|
+
- lib/action_operation.rb
|
139
|
+
- lib/action_operation/error.rb
|
140
|
+
- lib/action_operation/error/missing_error.rb
|
141
|
+
- lib/action_operation/error/missing_schema_for_task.rb
|
142
|
+
- lib/action_operation/error/missing_task.rb
|
143
|
+
- lib/action_operation/types.rb
|
144
|
+
- lib/action_operation/version.rb
|
145
|
+
- lib/action_operation/version_spec.rb
|
146
|
+
- lib/action_operation_spec.rb
|
147
|
+
homepage: http://krainboltgreene.github.io/action_operation
|
148
|
+
licenses:
|
149
|
+
- ISC
|
150
|
+
metadata: {}
|
151
|
+
post_install_message:
|
152
|
+
rdoc_options: []
|
153
|
+
require_paths:
|
154
|
+
- lib
|
155
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '0'
|
165
|
+
requirements: []
|
166
|
+
rubyforge_project:
|
167
|
+
rubygems_version: 2.7.3
|
168
|
+
signing_key:
|
169
|
+
specification_version: 4
|
170
|
+
summary: A set of BPMN style operation logic
|
171
|
+
test_files: []
|