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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: '045387aa207b16be2233d147879ce84a4aa4f582'
4
- data.tar.gz: 721d6f47366e454c71d48d89cb1b52c0c667dee3
3
+ metadata.gz: 1f3af112187a2390363fe9bcb389ed828eeaea29
4
+ data.tar.gz: e7f2c4d31314ba73317612f72d1dab3ecd234aa7
5
5
  SHA512:
6
- metadata.gz: 92a9ea5e58135b1ceb1f652c495b7518de7cc633bf8d83e063e87dad2351aa0bd4c8fb0255882ba52da7ef11bd42e5430c1595d6c194e4d7b6843cfdd7f1a170
7
- data.tar.gz: 73a7952a92f88fe0b3434480d7367c03fe99e61809975703f1559da882bbbae51c10f41da3b6d53d77a6e323c4e99274413737e44a2a0de22126eeb452f3de77
6
+ metadata.gz: 3c7b9d9ce3250ec523633999c61340f3a44529ef1f2af5f5bec7048d68d428f166f242cfc32142fdf22b26c80ad5f64790453eafe67d6ba0240f7e472a72ed7b
7
+ data.tar.gz: 0bc5a6dbd668167fca6242b4d090214ec9d2dd8be77daae440b2a1dbb44b01abd2077e43df809d9b5c7dc63c7199a5fee6c490b1d9bd900bb9b68e2cb4959e1d
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  /.bundle/
2
+ /.byebug*
2
3
  /.yardoc
3
4
  /Gemfile.lock
4
5
  /_yardoc/
@@ -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
@@ -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
- TODO: describe your gem
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
- $ bundle
213
+ ```bash
214
+ $ bundle
215
+ ```
18
216
 
19
217
  Or install it yourself as:
20
218
 
21
- $ gem install opie
22
-
23
- ## Usage
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. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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 the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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
 
@@ -1,4 +1,5 @@
1
1
  module Opie
2
2
  require 'opie/version'
3
+ require 'opie/failure'
3
4
  require 'opie/operation'
4
5
  end
@@ -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
@@ -2,58 +2,64 @@ require 'dry-container'
2
2
 
3
3
  module Opie
4
4
  class Operation
5
- attr_reader :container
5
+ FAIL = '__STEP_FAILED__'.freeze
6
6
 
7
- def initialize(params = {}, dependencies = {})
8
- @container = Dry::Container.new
9
- @container.register('params', params)
10
- dependencies.each { |k, v| @container.register(k, v) }
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(*args)
15
- instance = self.new(*args)
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(step)
21
- add_step(step)
31
+ def step(name)
32
+ add_step(name)
22
33
  end
23
34
 
24
- private
25
-
26
- def add_step(value)
35
+ def step_list
27
36
  @steps ||= []
28
- @steps << value
29
37
  end
30
38
 
31
- def step_list
39
+ private
40
+
41
+ def add_step(name)
32
42
  @steps ||= []
43
+ @steps << name
33
44
  end
34
45
  end
35
46
 
36
- def fail
37
- 'fail'
38
- end
47
+ private
39
48
 
40
- # def []=(key, value)
41
- # container.register(key, value)
42
- # end
43
- # result, container
49
+ def execute_steps(input)
50
+ step_list = self.class.step_list
44
51
 
45
- def success?
46
- !@failure
47
- end
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
- def failure?
50
- @failure
58
+ @output = next_input if success?
51
59
  end
52
60
 
53
- private
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
@@ -1,3 +1,3 @@
1
1
  module Opie
2
- VERSION = '0.1.0'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
@@ -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: 0.1.0
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-18 00:00:00.000000000 Z
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