porch 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +66 -0
- data/LICENSE.md +21 -0
- data/README.md +286 -0
- data/Rakefile +6 -0
- data/lib/porch/context.rb +71 -0
- data/lib/porch/core_ext/string.rb +55 -0
- data/lib/porch/core_ext.rb +1 -0
- data/lib/porch/errors/invalid_step_type_error.rb +10 -0
- data/lib/porch/errors.rb +1 -0
- data/lib/porch/executable_step_decorator.rb +35 -0
- data/lib/porch/guard_rail/guard.rb +18 -0
- data/lib/porch/guard_rail.rb +9 -0
- data/lib/porch/human_error.rb +43 -0
- data/lib/porch/organizer.rb +14 -0
- data/lib/porch/step_chain.rb +31 -0
- data/lib/porch/step_decorators/class_step_decorator.rb +19 -0
- data/lib/porch/step_decorators/method_step_decorator.rb +20 -0
- data/lib/porch/step_decorators/proc_step_decorator.rb +19 -0
- data/lib/porch/version.rb +3 -0
- data/lib/porch.rb +12 -0
- data/porch.gemspec +27 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a4465a58cd1f427a90f98c0fe4287da4a95a591c
|
4
|
+
data.tar.gz: 3c9117fc73b0f17b88e1a6d6a44a3c549269886a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 283ed8cbacd23252b660d4f64cd538851b1f15463f3b3e84534cc8eb260b721018a1cc6a9c6c0eec0816763811c6ce8dfbfbafe64a10c5fa1404b283d0fc2686
|
7
|
+
data.tar.gz: 0809d2abafa5631ac04752b8dcc0c21864e364b38be31fe1068a4a33a7a44d2513beb491ce0422bea66325644f882092028c2ac4d6b75111c944a1b8f5b188a2
|
data/.rspec
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include:
|
14
|
+
|
15
|
+
* The use of sexualized language or imagery
|
16
|
+
* Personal attacks
|
17
|
+
* Trolling or insulting/derogatory comments
|
18
|
+
* Public or private harassment
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
20
|
+
addresses, without explicit permission
|
21
|
+
* Other unethical or unprofessional conduct
|
22
|
+
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
27
|
+
threatening, offensive, or harmful.
|
28
|
+
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
32
|
+
Conduct may be permanently removed from the project team.
|
33
|
+
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
35
|
+
when an individual is representing the project or its community.
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
38
|
+
reported by contacting a project maintainer at jamie@brilliantfantastic.com. All
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
42
|
+
incident.
|
43
|
+
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
45
|
+
version 1.3.0, available at
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
47
|
+
|
48
|
+
[homepage]: http://contributor-covenant.org
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
porch (0.1.0)
|
5
|
+
dry-validation (~> 0.10.4)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
concurrent-ruby (1.0.4)
|
11
|
+
diff-lcs (1.2.5)
|
12
|
+
dry-configurable (0.5.0)
|
13
|
+
concurrent-ruby (~> 1.0)
|
14
|
+
dry-container (0.6.0)
|
15
|
+
concurrent-ruby (~> 1.0)
|
16
|
+
dry-configurable (~> 0.1, >= 0.1.3)
|
17
|
+
dry-core (0.2.3)
|
18
|
+
concurrent-ruby (~> 1.0)
|
19
|
+
dry-equalizer (0.2.0)
|
20
|
+
dry-logic (0.4.0)
|
21
|
+
dry-container (~> 0.2, >= 0.2.6)
|
22
|
+
dry-core (~> 0.1)
|
23
|
+
dry-equalizer (~> 0.2)
|
24
|
+
dry-types (0.9.3)
|
25
|
+
concurrent-ruby (~> 1.0)
|
26
|
+
dry-configurable (~> 0.1)
|
27
|
+
dry-container (~> 0.3)
|
28
|
+
dry-core (~> 0.2, >= 0.2.1)
|
29
|
+
dry-equalizer (~> 0.2)
|
30
|
+
dry-logic (~> 0.4, >= 0.4.0)
|
31
|
+
inflecto (~> 0.0.0, >= 0.0.2)
|
32
|
+
dry-validation (0.10.4)
|
33
|
+
concurrent-ruby (~> 1.0)
|
34
|
+
dry-configurable (~> 0.1, >= 0.1.3)
|
35
|
+
dry-container (~> 0.2, >= 0.2.8)
|
36
|
+
dry-core (~> 0.2, >= 0.2.1)
|
37
|
+
dry-equalizer (~> 0.2)
|
38
|
+
dry-logic (~> 0.4, >= 0.4.0)
|
39
|
+
dry-types (~> 0.9, >= 0.9.0)
|
40
|
+
inflecto (0.0.2)
|
41
|
+
rake (10.0.4)
|
42
|
+
rspec (3.5.0)
|
43
|
+
rspec-core (~> 3.5.0)
|
44
|
+
rspec-expectations (~> 3.5.0)
|
45
|
+
rspec-mocks (~> 3.5.0)
|
46
|
+
rspec-core (3.5.4)
|
47
|
+
rspec-support (~> 3.5.0)
|
48
|
+
rspec-expectations (3.5.0)
|
49
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
50
|
+
rspec-support (~> 3.5.0)
|
51
|
+
rspec-mocks (3.5.0)
|
52
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
53
|
+
rspec-support (~> 3.5.0)
|
54
|
+
rspec-support (3.5.0)
|
55
|
+
|
56
|
+
PLATFORMS
|
57
|
+
ruby
|
58
|
+
|
59
|
+
DEPENDENCIES
|
60
|
+
bundler (~> 1.12.0)
|
61
|
+
porch!
|
62
|
+
rake (~> 10.0.0)
|
63
|
+
rspec (~> 3.5.0)
|
64
|
+
|
65
|
+
BUNDLED WITH
|
66
|
+
1.12.5
|
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2016 Jamie Wright
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,286 @@
|
|
1
|
+
Porch
|
2
|
+
======
|
3
|
+
|
4
|
+
[ ![Codeship Status for jwright/porch](https://app.codeship.com/projects/8780ad70-b5ae-0134-fb0d-62cbe9f5cc84/status?branch=master)](https://app.codeship.com/projects/194128)
|
5
|
+
|
6
|
+
A simple service layer pattern for plain old Ruby objects.
|
7
|
+
|
8
|
+
## DESCRIPTION
|
9
|
+
|
10
|
+
Yeah, yeah, yeah. Keep yer controllers skinny they always tell us. This is a lot easier to say than do in many cases. Our controllers are supposed to create that user, send them an invitation email, block them from access until they authenticate their email address, etc.
|
11
|
+
|
12
|
+
Porch allows you to move the code into a series of steps that execute simple methods on itself or within simple PORO objects.
|
13
|
+
|
14
|
+
This was inspired by [LightService](https://github.com/adomokos/light-service) and the middleware chain by [Sidekiq](https://github.com/mperham/sidekiq).
|
15
|
+
|
16
|
+
## USAGE
|
17
|
+
|
18
|
+
### Installation
|
19
|
+
|
20
|
+
```
|
21
|
+
gem install porch
|
22
|
+
```
|
23
|
+
|
24
|
+
### Getting started
|
25
|
+
|
26
|
+
Your service object is simply a series of steps.
|
27
|
+
|
28
|
+
```
|
29
|
+
# app/services/registers_user.rb
|
30
|
+
|
31
|
+
require "porch"
|
32
|
+
|
33
|
+
module Services
|
34
|
+
class RegistersUser
|
35
|
+
include Porch::Organizer
|
36
|
+
|
37
|
+
attr_reader :attributes
|
38
|
+
|
39
|
+
def initialize(attributes)
|
40
|
+
@attributes = attributes
|
41
|
+
end
|
42
|
+
|
43
|
+
def register
|
44
|
+
with(attributes) do |chain|
|
45
|
+
chain.add CreateUser
|
46
|
+
chain.add SendWelcomeEmail
|
47
|
+
chain.add CreateBillingCustomer
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
You can then use your service in your controllers with the result that it returns from the chain.
|
55
|
+
|
56
|
+
```
|
57
|
+
# app/controllers/users_controller.rb
|
58
|
+
|
59
|
+
class UsersController < ApplicationController
|
60
|
+
def create
|
61
|
+
result = Services::RegistersUser.new(params[:user]).register
|
62
|
+
|
63
|
+
if result.success?
|
64
|
+
redirect_to dashboard_path(result.user), notice: "Welcome #{result.user.name}."
|
65
|
+
else
|
66
|
+
flash[:error] = result.message
|
67
|
+
render :new
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
### Defining steps or actions
|
74
|
+
|
75
|
+
You can define steps as classes and include some nice helper methods. (COMING SOON)
|
76
|
+
|
77
|
+
```
|
78
|
+
# app/services/steps/create_user.rb
|
79
|
+
|
80
|
+
require "porch"
|
81
|
+
|
82
|
+
class CreateUser
|
83
|
+
include Porch::Step
|
84
|
+
|
85
|
+
params do
|
86
|
+
required(:email).filled(type?: :str, format?: RegEx.email_address)
|
87
|
+
required(:password).filled(type?: :str, min_size?: 8)
|
88
|
+
end
|
89
|
+
|
90
|
+
def call(context)
|
91
|
+
context.user = User.create email: context.email, password: context.password
|
92
|
+
context.fail! context.user.errors unless context.user.valid?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
You can define steps as PORO classes that respond to a call method.
|
98
|
+
|
99
|
+
```
|
100
|
+
# app/services/create_billing_customer.rb
|
101
|
+
|
102
|
+
require "stripe"
|
103
|
+
|
104
|
+
class CreateBillingCustomer
|
105
|
+
def call(context)
|
106
|
+
customer = Stripe::Customer.create email: context.email
|
107
|
+
User.find_by_email(context.email).update_attributes(billing_id: customer.id)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
You can define steps as blocks on the organizer
|
113
|
+
|
114
|
+
```
|
115
|
+
# app/services/registers_user.rb
|
116
|
+
|
117
|
+
require "porch"
|
118
|
+
|
119
|
+
module Services
|
120
|
+
class RegistersUser
|
121
|
+
include Porch::Organizer
|
122
|
+
|
123
|
+
# ...
|
124
|
+
|
125
|
+
def register
|
126
|
+
with(attributes) do |chain|
|
127
|
+
# ...
|
128
|
+
chain.add :send_welcome_email do |context|
|
129
|
+
UserMailer.welcome(context.user).deliver_later
|
130
|
+
end
|
131
|
+
# ...
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
You can define steps as methods on the organizer.
|
139
|
+
|
140
|
+
```
|
141
|
+
# app/services/registers_user.rb
|
142
|
+
|
143
|
+
require "porch"
|
144
|
+
|
145
|
+
module Services
|
146
|
+
class RegistersUser
|
147
|
+
include Porch::Organizer
|
148
|
+
|
149
|
+
# ...
|
150
|
+
|
151
|
+
def register
|
152
|
+
with(attributes) do |chain|
|
153
|
+
# ...
|
154
|
+
chain.add :send_welcome_email
|
155
|
+
# ...
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def send_welcome_email(context)
|
162
|
+
UserMailer.welcome(context.user).deliver_later
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
### Failing the chain
|
169
|
+
|
170
|
+
At any step, you can set the `Porch::Context` as a failure which will stop processing the remaining steps and set the `Context` as a failed context with an optional message.
|
171
|
+
|
172
|
+
```
|
173
|
+
class RegistersUser
|
174
|
+
include Porch::Organizer
|
175
|
+
|
176
|
+
# ...
|
177
|
+
|
178
|
+
def register
|
179
|
+
with(attributes) do |chain|
|
180
|
+
# ...
|
181
|
+
chain.add :send_welcome_email
|
182
|
+
# ...
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def send_welcome_email(context)
|
189
|
+
context.fail! "Better luck next time!" if some_failure_condition?
|
190
|
+
UserMailer.welcome(context.user).deliver_later
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
result = RegistersUser.new(email: "test@example.com").register
|
195
|
+
if result.failure?
|
196
|
+
puts result.message # => "Better luck next time!"
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
### Skipping steps
|
201
|
+
|
202
|
+
At any step, you can skip the remaining actions in the organizer. This stops the running of the remaining actions but the `Porch::Context` will still return a successful `Porch::Context`.
|
203
|
+
|
204
|
+
```
|
205
|
+
class RegistersUser
|
206
|
+
include Porch::Organizer
|
207
|
+
|
208
|
+
# ...
|
209
|
+
|
210
|
+
def register
|
211
|
+
with(attributes) do |chain|
|
212
|
+
# ...
|
213
|
+
chain.add :save_user
|
214
|
+
# ...
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def save_user(context)
|
221
|
+
User.create context
|
222
|
+
context.skip_remaining! if sending_emails_disabled?
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
result = RegistersUser.new(email: "test@example.com").register
|
227
|
+
result.success? # => true
|
228
|
+
```
|
229
|
+
|
230
|
+
### Validating the context
|
231
|
+
|
232
|
+
Porch comes with multiple ways that you can validate each step is setup correctly. It uses the DSL provided by [dry-validation](http://dry-rb.org/gems/dry-validation/).
|
233
|
+
|
234
|
+
Several helper methods are included in the `Porch::Context` to guard against an invalid `Porch::Context`.
|
235
|
+
|
236
|
+
`Porch::Context#guard` can be used and if the validation fails, the `Context` will skip the remaining actions.
|
237
|
+
|
238
|
+
```
|
239
|
+
class SomeStep
|
240
|
+
def call(context)
|
241
|
+
context.guard { required(:email) }
|
242
|
+
# The rest of the action will not be performed and the rest of the actions will be
|
243
|
+
# skipped if the guard fails
|
244
|
+
end
|
245
|
+
end
|
246
|
+
```
|
247
|
+
|
248
|
+
`Porch::Context#guard!` (with a bang(!)) can be used and if the validation fails, the `Context` will be marked as a failure. The failure message for the `Context` will be set to be a comma-seperated list of the context errors that failed.
|
249
|
+
|
250
|
+
```
|
251
|
+
class SomeStep
|
252
|
+
def call(context)
|
253
|
+
context.guard! { required(:email) }
|
254
|
+
# The rest of the action will not be performed and the rest of the actions will be
|
255
|
+
# skipped and the action will be marked as failed if the guard fails
|
256
|
+
end
|
257
|
+
end
|
258
|
+
```
|
259
|
+
|
260
|
+
At any point, you can use the `Porch::GuardRail::Guard` helper method to validate any hash (including the `Porch::Context`).
|
261
|
+
|
262
|
+
```
|
263
|
+
hash = { email: "test@example" }
|
264
|
+
result = Porch::GuardRail::Guard.new(hash).against { required(:email).value(format?: RegEx::Email) }
|
265
|
+
result.success? # => false
|
266
|
+
result.errors # => { email: ["is invalid"] }
|
267
|
+
```
|
268
|
+
|
269
|
+
## CONTRIBUTING
|
270
|
+
|
271
|
+
1. Clone the repository `git clone https://github.com/jwright/porch`
|
272
|
+
1. Create a feature branch `git checkout -b my-awesome-feature`
|
273
|
+
1. Codez!
|
274
|
+
1. Commit your changes (small commits please)
|
275
|
+
1. Push your new branch `git push origin my-awesome-feature`
|
276
|
+
1. Create a pull request `hub pull-request -b jwright:master -h jwright:my-awesome-feature`
|
277
|
+
|
278
|
+
## RELEASING A NEW GEM
|
279
|
+
|
280
|
+
1. Bump the VERSION in `lib/porch/version.rb`
|
281
|
+
1. Commit changes and push to GitHub
|
282
|
+
1. run `bundle exec rake release`
|
283
|
+
|
284
|
+
## LICENSE
|
285
|
+
|
286
|
+
This project is licensed under the [MIT License](LICENSE.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require "porch/guard_rail"
|
2
|
+
|
3
|
+
module Porch
|
4
|
+
class Context < Hash
|
5
|
+
include Porch::GuardRail
|
6
|
+
|
7
|
+
attr_reader :message
|
8
|
+
|
9
|
+
def initialize(context={}, success=true)
|
10
|
+
@message = nil
|
11
|
+
@success = success
|
12
|
+
@skip_remaining = false
|
13
|
+
deep_duplicate(context)
|
14
|
+
end
|
15
|
+
|
16
|
+
def deep_dup
|
17
|
+
self.class.new(self, self.success?)
|
18
|
+
end
|
19
|
+
|
20
|
+
def fail!(message="")
|
21
|
+
@message = message
|
22
|
+
@success = false
|
23
|
+
throw :stop_current_step_execution, self
|
24
|
+
end
|
25
|
+
|
26
|
+
def failure?
|
27
|
+
!success?
|
28
|
+
end
|
29
|
+
|
30
|
+
def guard(&block)
|
31
|
+
result = super
|
32
|
+
skip_remaining! if result.failure?
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
def guard!(&block)
|
37
|
+
result = guard &block
|
38
|
+
fail!(HumanError.new(result.errors).message) if result.failure?
|
39
|
+
result
|
40
|
+
end
|
41
|
+
|
42
|
+
def method_missing(name, *args, &block)
|
43
|
+
return fetch(name) if key?(name)
|
44
|
+
super
|
45
|
+
end
|
46
|
+
|
47
|
+
def skip_remaining?
|
48
|
+
!!@skip_remaining
|
49
|
+
end
|
50
|
+
|
51
|
+
def skip_remaining!(skip_current=false)
|
52
|
+
@skip_remaining = true
|
53
|
+
throw :stop_current_step_execution, self if skip_current
|
54
|
+
end
|
55
|
+
|
56
|
+
def success?
|
57
|
+
@success
|
58
|
+
end
|
59
|
+
|
60
|
+
def stop_processing?
|
61
|
+
failure? || skip_remaining?
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def deep_duplicate(context)
|
67
|
+
(context || {}).to_hash.each { |k, v| self[k] = v }
|
68
|
+
self
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Porch
|
2
|
+
module CoreExt
|
3
|
+
class String
|
4
|
+
CAPITALIZE_SEPARATOR = ' '.freeze
|
5
|
+
CLASSIFY_SEPARATOR = '_'.freeze
|
6
|
+
NAMESPACE_SEPARATOR = '::'.freeze
|
7
|
+
UNDERSCORE_DIVISION_TARGET = '\1_\2'.freeze
|
8
|
+
UNDERSCORE_SEPARATOR = '/'.freeze
|
9
|
+
|
10
|
+
def initialize(string)
|
11
|
+
@string = string.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def capitalize
|
15
|
+
head, *tail = underscore.split(CLASSIFY_SEPARATOR)
|
16
|
+
|
17
|
+
self.class.new tail.unshift(head.capitalize).join(CAPITALIZE_SEPARATOR)
|
18
|
+
end
|
19
|
+
|
20
|
+
def gsub(pattern, replacement=nil, &blk)
|
21
|
+
if block_given?
|
22
|
+
string.gsub(pattern, &blk)
|
23
|
+
else
|
24
|
+
string.gsub(pattern, replacement)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def split(pattern, limit=0)
|
29
|
+
string.split(pattern, limit)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
string
|
34
|
+
end
|
35
|
+
|
36
|
+
def underscore
|
37
|
+
new_string = gsub(NAMESPACE_SEPARATOR, UNDERSCORE_SEPARATOR)
|
38
|
+
new_string.gsub!(/([A-Z\d]+)([A-Z][a-z])/, UNDERSCORE_DIVISION_TARGET)
|
39
|
+
new_string.gsub!(/([a-z\d])([A-Z])/, UNDERSCORE_DIVISION_TARGET)
|
40
|
+
new_string.gsub!(/[[:space:]]|\-/, UNDERSCORE_DIVISION_TARGET)
|
41
|
+
new_string.downcase!
|
42
|
+
self.class.new new_string
|
43
|
+
end
|
44
|
+
|
45
|
+
def ==(other)
|
46
|
+
to_s == other
|
47
|
+
end
|
48
|
+
alias eql? ==
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
attr_reader :string
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "core_ext/string"
|
data/lib/porch/errors.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "errors/invalid_step_type_error"
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative "step_decorators/class_step_decorator"
|
2
|
+
require_relative "step_decorators/method_step_decorator"
|
3
|
+
require_relative "step_decorators/proc_step_decorator"
|
4
|
+
|
5
|
+
module Porch
|
6
|
+
class ExecutableStepDecorator
|
7
|
+
attr_reader :decorated_step
|
8
|
+
|
9
|
+
def step
|
10
|
+
decorated_step.step
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(step, organizer)
|
14
|
+
@decorated_step = decorate step, organizer
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute(context)
|
18
|
+
catch :stop_current_step_execution do
|
19
|
+
decorated_step.execute context
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.registered_decorators
|
24
|
+
[ClassStepDecorator, MethodStepDecorator, ProcStepDecorator].freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def decorate(step, organizer)
|
30
|
+
decorator = self.class.registered_decorators.find { |d| d.decorates?(step) }
|
31
|
+
raise InvalidStepTypeError.new(step) if decorator.nil?
|
32
|
+
decorator.new(step, organizer)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "dry-validation"
|
2
|
+
|
3
|
+
module Porch
|
4
|
+
module GuardRail
|
5
|
+
class Guard
|
6
|
+
attr_reader :guarded_object
|
7
|
+
|
8
|
+
def initialize(guarded_object)
|
9
|
+
@guarded_object = guarded_object
|
10
|
+
end
|
11
|
+
|
12
|
+
def against(&block)
|
13
|
+
schema = Dry::Validation.Schema &block
|
14
|
+
schema.call guarded_object
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Porch
|
2
|
+
class HumanError
|
3
|
+
attr_accessor :seperator
|
4
|
+
attr_reader :errors
|
5
|
+
|
6
|
+
def initialize(errors, seperator=", ")
|
7
|
+
@errors = errors
|
8
|
+
@seperator = seperator
|
9
|
+
end
|
10
|
+
|
11
|
+
def message
|
12
|
+
collect_messages(errors).join(seperator)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def collect_messages(error_hash, attribute_prefix=nil)
|
18
|
+
error_hash.collect do |attribute, descriptions|
|
19
|
+
if descriptions.is_a? Hash
|
20
|
+
collect_messages descriptions, attribute
|
21
|
+
else
|
22
|
+
append_message attribute_prefix, attribute, descriptions
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def append_message(attribute_prefix, attribute, descriptions)
|
28
|
+
append_descriptions(humanize(attribute_prefix, attribute), descriptions)
|
29
|
+
end
|
30
|
+
|
31
|
+
def append_descriptions(attribute, descriptions)
|
32
|
+
descriptions.collect { |description| "#{attribute} #{description}" }.join(seperator)
|
33
|
+
end
|
34
|
+
|
35
|
+
def humanize(attribute_prefix, attribute)
|
36
|
+
capitalize("#{attribute_prefix} #{attribute}".lstrip)
|
37
|
+
end
|
38
|
+
|
39
|
+
def capitalize(string)
|
40
|
+
CoreExt::String.new(string).capitalize
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Porch
|
2
|
+
class StepChain
|
3
|
+
attr_reader :organizer, :steps
|
4
|
+
|
5
|
+
def initialize(organizer)
|
6
|
+
@organizer = organizer
|
7
|
+
@steps = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(step=nil, &block)
|
11
|
+
step = block if block_given?
|
12
|
+
steps << ExecutableStepDecorator.new(step, organizer)
|
13
|
+
end
|
14
|
+
|
15
|
+
def insert(index, step=nil, &block)
|
16
|
+
step = block if block_given?
|
17
|
+
steps.insert index, ExecutableStepDecorator.new(step, organizer)
|
18
|
+
end
|
19
|
+
|
20
|
+
def remove(step)
|
21
|
+
@steps.delete_if { |decorated_step| decorated_step.step == step }
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute(context)
|
25
|
+
ctx = Context.new context
|
26
|
+
steps.map do |step|
|
27
|
+
ctx = step.execute ctx unless ctx.stop_processing?
|
28
|
+
end.last || ctx
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Porch
|
2
|
+
class ClassStepDecorator
|
3
|
+
attr_reader :step
|
4
|
+
|
5
|
+
def initialize(step, _organizer)
|
6
|
+
@step = step
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute(context)
|
10
|
+
ctx = Context.new(context)
|
11
|
+
step.new.call ctx
|
12
|
+
ctx
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.decorates?(step)
|
16
|
+
step.is_a? Class
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Porch
|
2
|
+
class MethodStepDecorator
|
3
|
+
attr_reader :organizer, :step
|
4
|
+
|
5
|
+
def initialize(step, organizer)
|
6
|
+
@step = step
|
7
|
+
@organizer = organizer
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute(context)
|
11
|
+
ctx = Context.new(context)
|
12
|
+
organizer.send step, ctx
|
13
|
+
ctx
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.decorates?(step)
|
17
|
+
step.is_a?(Symbol) || step.is_a?(String)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Porch
|
2
|
+
class ProcStepDecorator
|
3
|
+
attr_reader :step
|
4
|
+
|
5
|
+
def initialize(step, _organizer)
|
6
|
+
@step = step
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute(context)
|
10
|
+
ctx = Context.new(context)
|
11
|
+
step.call ctx
|
12
|
+
ctx
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.decorates?(step)
|
16
|
+
step.is_a? Proc
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/porch.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "porch/context"
|
2
|
+
require "porch/core_ext"
|
3
|
+
require "porch/errors"
|
4
|
+
require "porch/executable_step_decorator"
|
5
|
+
require "porch/human_error"
|
6
|
+
require "porch/guard_rail"
|
7
|
+
require "porch/organizer"
|
8
|
+
require "porch/step_chain"
|
9
|
+
require "porch/version"
|
10
|
+
|
11
|
+
module Porch
|
12
|
+
end
|
data/porch.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "porch/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "porch"
|
8
|
+
spec.version = Porch::VERSION
|
9
|
+
spec.authors = ["Jamie Wright"]
|
10
|
+
spec.email = ["jamie@brilliantfantastic.com"]
|
11
|
+
|
12
|
+
spec.summary = "A simple service layer pattern for plain old Ruby objects."
|
13
|
+
spec.description = %q{Porch allows you to move the code into a series of steps that execute simple methods on itself or within simple PORO objects.}
|
14
|
+
spec.homepage = "https://github.com/jwright/porch"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "dry-validation", "~> 0.10.4"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.12.0"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0.0"
|
26
|
+
spec.add_development_dependency "rspec", "~> 3.5.0"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: porch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jamie Wright
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-01-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-validation
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.10.4
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.10.4
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.12.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.12.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 10.0.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 10.0.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.5.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 3.5.0
|
69
|
+
description: Porch allows you to move the code into a series of steps that execute
|
70
|
+
simple methods on itself or within simple PORO objects.
|
71
|
+
email:
|
72
|
+
- jamie@brilliantfantastic.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".rspec"
|
78
|
+
- CODE_OF_CONDUCT.md
|
79
|
+
- Gemfile
|
80
|
+
- Gemfile.lock
|
81
|
+
- LICENSE.md
|
82
|
+
- README.md
|
83
|
+
- Rakefile
|
84
|
+
- lib/porch.rb
|
85
|
+
- lib/porch/context.rb
|
86
|
+
- lib/porch/core_ext.rb
|
87
|
+
- lib/porch/core_ext/string.rb
|
88
|
+
- lib/porch/errors.rb
|
89
|
+
- lib/porch/errors/invalid_step_type_error.rb
|
90
|
+
- lib/porch/executable_step_decorator.rb
|
91
|
+
- lib/porch/guard_rail.rb
|
92
|
+
- lib/porch/guard_rail/guard.rb
|
93
|
+
- lib/porch/human_error.rb
|
94
|
+
- lib/porch/organizer.rb
|
95
|
+
- lib/porch/step_chain.rb
|
96
|
+
- lib/porch/step_decorators/class_step_decorator.rb
|
97
|
+
- lib/porch/step_decorators/method_step_decorator.rb
|
98
|
+
- lib/porch/step_decorators/proc_step_decorator.rb
|
99
|
+
- lib/porch/version.rb
|
100
|
+
- porch.gemspec
|
101
|
+
homepage: https://github.com/jwright/porch
|
102
|
+
licenses:
|
103
|
+
- MIT
|
104
|
+
metadata: {}
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 2.5.1
|
122
|
+
signing_key:
|
123
|
+
specification_version: 4
|
124
|
+
summary: A simple service layer pattern for plain old Ruby objects.
|
125
|
+
test_files: []
|