opie 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/guzart/opie.svg?branch=master)](https://travis-ci.org/guzart/opie)
|
4
|
+
[![codecov](https://codecov.io/gh/guzart/opie/branch/master/graph/badge.svg)](https://codecov.io/gh/guzart/opie)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/guzart/opie/badges/gpa.svg)](https://codeclimate.com/github/guzart/opie)
|
6
|
+
[![Gem Version](https://badge.fury.io/rb/opie.svg)](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
|