opie 0.1.0 → 1.0.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/.rubocop.yml +6 -0
- data/CHANGELOG.md +28 -0
- data/README.md +208 -9
- data/lib/opie.rb +1 -0
- data/lib/opie/failure.rb +18 -0
- data/lib/opie/operation.rb +38 -32
- data/lib/opie/version.rb +1 -1
- data/opie.gemspec +2 -0
- metadata +32 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f3af112187a2390363fe9bcb389ed828eeaea29
|
4
|
+
data.tar.gz: e7f2c4d31314ba73317612f72d1dab3ecd234aa7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c7b9d9ce3250ec523633999c61340f3a44529ef1f2af5f5bec7048d68d428f166f242cfc32142fdf22b26c80ad5f64790453eafe67d6ba0240f7e472a72ed7b
|
7
|
+
data.tar.gz: 0bc5a6dbd668167fca6242b4d090214ec9d2dd8be77daae440b2a1dbb44b01abd2077e43df809d9b5c7dc63c7199a5fee6c490b1d9bd900bb9b68e2cb4959e1d
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# https://github.com/bbatsov/rubocop/blob/master/config/default.yml
|
2
2
|
|
3
3
|
AllCops:
|
4
|
+
Exclude:
|
5
|
+
- 'spec/**/*'
|
6
|
+
- '*.gemspec'
|
4
7
|
TargetRubyVersion: 2.3
|
5
8
|
|
6
9
|
Metrics/LineLength:
|
@@ -8,3 +11,6 @@ Metrics/LineLength:
|
|
8
11
|
|
9
12
|
Style/Documentation:
|
10
13
|
Enabled: false
|
14
|
+
|
15
|
+
Style/FrozenStringLiteralComment:
|
16
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
<a name="1.0.0"></a>
|
2
|
+
# [1.0.0](https://github.com/guzart/opie/compare/v0.1.0...v1.0.0) (2017-03-20)
|
3
|
+
|
4
|
+
|
5
|
+
### Bug Fixes
|
6
|
+
|
7
|
+
* reimplement step and call using new spec ([d765181](https://github.com/guzart/opie/commit/d765181))
|
8
|
+
|
9
|
+
|
10
|
+
### Features
|
11
|
+
|
12
|
+
* add #success? and #failure? ([29fb177](https://github.com/guzart/opie/commit/29fb177))
|
13
|
+
* **operation:** add #output method ([0fa85ae](https://github.com/guzart/opie/commit/0fa85ae))
|
14
|
+
* add failure as a value type ([729c429](https://github.com/guzart/opie/commit/729c429))
|
15
|
+
* add failure helper method ([6735d2b](https://github.com/guzart/opie/commit/6735d2b))
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
<a name="0.1.0"></a>
|
20
|
+
# [0.1.0](https://github.com/guzart/opie/compare/6c9db9d...v0.1.0) (2017-03-18)
|
21
|
+
|
22
|
+
|
23
|
+
### Features
|
24
|
+
|
25
|
+
* add operation with basic support for steps ([6c9db9d](https://github.com/guzart/opie/commit/6c9db9d))
|
26
|
+
|
27
|
+
|
28
|
+
|
data/README.md
CHANGED
@@ -1,8 +1,204 @@
|
|
1
1
|
# Opie
|
2
2
|
|
3
3
|
[](https://travis-ci.org/guzart/opie)
|
4
|
+
[](https://codecov.io/gh/guzart/opie)
|
5
|
+
[](https://codeclimate.com/github/guzart/opie)
|
6
|
+
[](https://badge.fury.io/rb/opie)
|
4
7
|
|
5
|
-
|
8
|
+
**Opie gives you a simple API for creating Operations using the
|
9
|
+
[Railsway oriented programming](https://vimeo.com/113707214) paradigm.**
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
**Simple Usage:**
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
# Create an Operation for completing a Todo
|
17
|
+
class Todos::CompleteTodo < Opie::Operation
|
18
|
+
step :find_todo
|
19
|
+
step :mark_as_complete
|
20
|
+
|
21
|
+
def find_todo(todo_id)
|
22
|
+
todo = Todo.find_by(id: todo_id)
|
23
|
+
return fail(:not_found, "Could not find the Todo using id: #{todo_id}") unless todo
|
24
|
+
todo
|
25
|
+
end
|
26
|
+
|
27
|
+
def mark_as_complete(todo)
|
28
|
+
success = todo.update(completed_at: Time.zone.now)
|
29
|
+
return fail(:update) unless success
|
30
|
+
todo
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class TodosController < ApplicationController
|
35
|
+
def complete
|
36
|
+
# invoke the operation
|
37
|
+
result = Todos::CompleteTodo.(params[:id])
|
38
|
+
if result.success? # if #success?
|
39
|
+
render status: :created, json: result.output # use output
|
40
|
+
else
|
41
|
+
render status: :bad_request, json: { error: error_message(result.failure) } # otherwise use #failure
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def error_message(failure)
|
48
|
+
case failure[:type]
|
49
|
+
when :not_found then failure[:data]
|
50
|
+
when :update then 'We were unable to make the changes to your todo'
|
51
|
+
else 'There was an unexpected error, sorry for the inconvenience'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
**Real world example:**
|
58
|
+
|
59
|
+
Imagine yourself in the context of a [habit tracker](https://github.com/isoron/uhabits), wanting to
|
60
|
+
add a new habit to track.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
# app/controllers/habits_controller.rb
|
64
|
+
|
65
|
+
class HabitsController < ApplicationController
|
66
|
+
# POST /habits
|
67
|
+
def create
|
68
|
+
# run the `operation` – since it's a modification we can call it a `command`
|
69
|
+
result = People::AddHabit.(habit_params)
|
70
|
+
|
71
|
+
# render response based on operation result
|
72
|
+
if result.success?
|
73
|
+
render status: :created, json: result.output
|
74
|
+
else
|
75
|
+
render status: error_http_status(result.failure[:type]), json: { errors: [result.failure] }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# the HTTP status depends on the error type, which separating the domain from the infrastructure
|
82
|
+
def error_http_status(error_type)
|
83
|
+
case(error_type)
|
84
|
+
when :validation then :unprocessable_entity
|
85
|
+
when :not_found then :not_found
|
86
|
+
else :server_error
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# simulate parameters came from a Http request
|
91
|
+
def habit_params
|
92
|
+
{
|
93
|
+
person_id: 2,
|
94
|
+
name: 'Excercise',
|
95
|
+
description: 'Did you excercise for at least 15 minutes today?',
|
96
|
+
frequency: :three_times_per_week,
|
97
|
+
color: 'DeepPink'
|
98
|
+
}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
And now the code that defines the operation
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
# application-wide dependencies container
|
107
|
+
class HabitTrackerContainer
|
108
|
+
extends Dry::Container::Mixin
|
109
|
+
|
110
|
+
register 'repositories.habit', HabitRepository.new
|
111
|
+
register 'repositories.person', PersonRepository.new
|
112
|
+
register 'service_bus', ServiceBus.new
|
113
|
+
end
|
114
|
+
|
115
|
+
# application-wide dependency injector
|
116
|
+
Import = Dry::AutoInject(HabitTrackerContainer.new)
|
117
|
+
|
118
|
+
module People
|
119
|
+
# we define a validation schema for our input
|
120
|
+
AddHabitSchema = Dry::Schema.Validation do
|
121
|
+
configure do
|
122
|
+
# custom predicate for frequency
|
123
|
+
def freq?(value)
|
124
|
+
[:weekly, :five_times_per_week, :four_times_per_week, :three_times_per_week].includes?(value)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
required(:person_id).filled(:int?, gt?: 0)
|
129
|
+
required(:name).filled(:str?)
|
130
|
+
required(:description).maybe(:str?)
|
131
|
+
required(:frequency).filled(:freq?)
|
132
|
+
required(:color).filled(:str?)
|
133
|
+
end
|
134
|
+
|
135
|
+
# the operation logic starts
|
136
|
+
class AddHabit < Opie::Operation
|
137
|
+
# inject dependencies, more flexible than ruby's global namespace
|
138
|
+
include Import[
|
139
|
+
habit_repo: 'repositories.habit',
|
140
|
+
person_repo: 'repositories.person',
|
141
|
+
service_bus: 'service_bus'
|
142
|
+
]
|
143
|
+
|
144
|
+
# first step receives ::call first argument, then the output of the step is the argument of the next step
|
145
|
+
step :validate
|
146
|
+
step :find_person
|
147
|
+
step :persist_habit
|
148
|
+
step :send_event
|
149
|
+
|
150
|
+
# receives the first input
|
151
|
+
def validate(params)
|
152
|
+
schema = AddHabitSchema.(params)
|
153
|
+
return fail(:validation, schema.errors) if schema.failure?
|
154
|
+
schema.output
|
155
|
+
end
|
156
|
+
|
157
|
+
# if it's valid then find the person (tenant)
|
158
|
+
def find_person(params)
|
159
|
+
person = person_repo.find(params[:person_id])
|
160
|
+
return fail(:repository, 'We could not find your account') unless person
|
161
|
+
params.merge(person: person)
|
162
|
+
end
|
163
|
+
|
164
|
+
# persist the new habit
|
165
|
+
def persist_habit(params)
|
166
|
+
new_habit = Entities::Habit.new(params)
|
167
|
+
habit_repo.create(new_habit)
|
168
|
+
rescue => error
|
169
|
+
fail(:persist_failed, error.message)
|
170
|
+
end
|
171
|
+
|
172
|
+
# notify the world
|
173
|
+
def send_event(habit)
|
174
|
+
event = Habits::CreatedEvent.new(habit.attributes)
|
175
|
+
service_bus.send(event)
|
176
|
+
rescue => error
|
177
|
+
fail(:event_failed, error)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
```
|
182
|
+
|
183
|
+
## API
|
184
|
+
|
185
|
+
The `Opie::Operation` API:
|
186
|
+
* `::step(Symbol) -> void` indicates a method that is executed in the operation sequence
|
187
|
+
* `#success? -> Boolean` indicates whether the operation was successful
|
188
|
+
* `#failure? -> Boolean` indicates whether the operation was a failure
|
189
|
+
* `#failure -> Hash | nil` the erorr if the operation is a `failure?`, nil when it's a success
|
190
|
+
* `#failures -> Array<Hash> | nil` an array with all errors
|
191
|
+
* `#output -> *` if succcessful, it returns the operation final output
|
192
|
+
validation error
|
193
|
+
|
194
|
+
Internal API:
|
195
|
+
* `#fail(error_type: Symbol, error_data: *) -> Hash`
|
196
|
+
|
197
|
+
_Tentative API_
|
198
|
+
|
199
|
+
* `::step(Array<Symbol>) -> void` a series of methods to be called in parallel
|
200
|
+
* `::step(Opie::Step) -> void` an enforcer of a step signature which helps to compose other steps
|
201
|
+
* `::failure(Symbol) -> void` indicates the method that handles failures
|
6
202
|
|
7
203
|
## Installation
|
8
204
|
|
@@ -14,21 +210,24 @@ gem 'opie'
|
|
14
210
|
|
15
211
|
And then execute:
|
16
212
|
|
17
|
-
|
213
|
+
```bash
|
214
|
+
$ bundle
|
215
|
+
```
|
18
216
|
|
19
217
|
Or install it yourself as:
|
20
218
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
TODO: Write usage instructions here
|
219
|
+
```bash
|
220
|
+
$ gem install opie
|
221
|
+
```
|
26
222
|
|
27
223
|
## Development
|
28
224
|
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
225
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
226
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
227
|
|
31
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update
|
228
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update
|
229
|
+
the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for
|
230
|
+
the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
231
|
|
33
232
|
## Contributing
|
34
233
|
|
data/lib/opie.rb
CHANGED
data/lib/opie/failure.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Opie
|
2
|
+
class Failure
|
3
|
+
attr_reader :data, :type
|
4
|
+
|
5
|
+
def initialize(type, data = nil)
|
6
|
+
@type = type
|
7
|
+
@data = data
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
type == other.type && data == other.data
|
12
|
+
end
|
13
|
+
|
14
|
+
def hash
|
15
|
+
[type, (data || '').to_sym].hash
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/opie/operation.rb
CHANGED
@@ -2,58 +2,64 @@ require 'dry-container'
|
|
2
2
|
|
3
3
|
module Opie
|
4
4
|
class Operation
|
5
|
-
|
5
|
+
FAIL = '__STEP_FAILED__'.freeze
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
attr_reader :failure, :output
|
8
|
+
|
9
|
+
def call(input = nil)
|
10
|
+
execute_steps(input)
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def failure?
|
15
|
+
!success?
|
16
|
+
end
|
17
|
+
|
18
|
+
def success?
|
19
|
+
failure.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
def failures
|
23
|
+
[failure].compact
|
11
24
|
end
|
12
25
|
|
13
26
|
class << self
|
14
|
-
def call(
|
15
|
-
|
16
|
-
instance.send(:execute_steps, step_list)
|
17
|
-
instance
|
27
|
+
def call(input = nil)
|
28
|
+
new.call(input)
|
18
29
|
end
|
19
30
|
|
20
|
-
def step(
|
21
|
-
add_step(
|
31
|
+
def step(name)
|
32
|
+
add_step(name)
|
22
33
|
end
|
23
34
|
|
24
|
-
|
25
|
-
|
26
|
-
def add_step(value)
|
35
|
+
def step_list
|
27
36
|
@steps ||= []
|
28
|
-
@steps << value
|
29
37
|
end
|
30
38
|
|
31
|
-
|
39
|
+
private
|
40
|
+
|
41
|
+
def add_step(name)
|
32
42
|
@steps ||= []
|
43
|
+
@steps << name
|
33
44
|
end
|
34
45
|
end
|
35
46
|
|
36
|
-
|
37
|
-
'fail'
|
38
|
-
end
|
47
|
+
private
|
39
48
|
|
40
|
-
|
41
|
-
|
42
|
-
# end
|
43
|
-
# result, container
|
49
|
+
def execute_steps(input)
|
50
|
+
step_list = self.class.step_list
|
44
51
|
|
45
|
-
|
46
|
-
|
47
|
-
|
52
|
+
next_input = input
|
53
|
+
step_list.find do |name|
|
54
|
+
next_input = public_send(name, next_input)
|
55
|
+
failure?
|
56
|
+
end
|
48
57
|
|
49
|
-
|
50
|
-
@failure
|
58
|
+
@output = next_input if success?
|
51
59
|
end
|
52
60
|
|
53
|
-
|
54
|
-
|
55
|
-
def execute_steps(steps)
|
56
|
-
@failure = steps.find { |m| send(m) == 'fail' }
|
61
|
+
def fail(type, data = nil)
|
62
|
+
@failure = Failure.new(type, data)
|
57
63
|
end
|
58
64
|
end
|
59
65
|
end
|
data/lib/opie/version.rb
CHANGED
data/opie.gemspec
CHANGED
@@ -27,6 +27,8 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_dependency 'dry-container', '~> 0.6'
|
28
28
|
|
29
29
|
spec.add_development_dependency 'awesome_print', '~> 1.7'
|
30
|
+
spec.add_development_dependency 'byebug', '~> 9.0'
|
31
|
+
spec.add_development_dependency 'codecov', '~> 0'
|
30
32
|
spec.add_development_dependency 'bundler', '~> 1.14'
|
31
33
|
spec.add_development_dependency 'guard', '~> 2.14'
|
32
34
|
spec.add_development_dependency 'guard-rspec', '~> 4.7'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: opie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arturo Guzman
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-03-
|
11
|
+
date: 2017-03-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-container
|
@@ -38,6 +38,34 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '9.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '9.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: codecov
|
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'
|
41
69
|
- !ruby/object:Gem::Dependency
|
42
70
|
name: bundler
|
43
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -138,6 +166,7 @@ files:
|
|
138
166
|
- ".rspec"
|
139
167
|
- ".rubocop.yml"
|
140
168
|
- ".travis.yml"
|
169
|
+
- CHANGELOG.md
|
141
170
|
- Gemfile
|
142
171
|
- Guardfile
|
143
172
|
- LICENSE.txt
|
@@ -149,6 +178,7 @@ files:
|
|
149
178
|
- bin/rspec
|
150
179
|
- bin/setup
|
151
180
|
- lib/opie.rb
|
181
|
+
- lib/opie/failure.rb
|
152
182
|
- lib/opie/operation.rb
|
153
183
|
- lib/opie/version.rb
|
154
184
|
- opie.gemspec
|