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 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