hexagonal 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0361915ddc3f4bcb10417c531a291b3f8857c164
4
+ data.tar.gz: 1389a24a48796d96a529b2fe84fbf17c7ccbc834
5
+ SHA512:
6
+ metadata.gz: 883666b54d38b6033bede7a5baf99f5603e9e2b5d482d819edc92a635b0ece9e9f8a505d719268bae96b02d91b5df8e6ae7e4eca0cf23a09df995246efb51c3b
7
+ data.tar.gz: 1ba10dcff07734983eebc13dcfd566f00e2d7ba0a691cf5eaed332c9032490628bf7d764e1449dc14f5935a1cdf4d250494f26faf8b50b1252ce06a823abb3d8
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hexagonal.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Matt Beedle
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,367 @@
1
+ # Hexagonal
2
+
3
+ A simple gem to provide structure and guidance for writing hexagonal ruby
4
+ applications.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'hexagonal'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install hexagonal
21
+
22
+ ## Why?!?!
23
+
24
+ Rails applications are usually really fast to build at the beginning, but due to
25
+ high coupling, as they mature they begin to calcify. Eventually adding any new
26
+ features and fixing bugs becomes a pain. Developers get caught in callback hell.
27
+ Nothing can be tested in isolation. Test suites take >10 minutes to run. This is
28
+ also sometimes true of non-Rails Ruby apps. Some people will suggest that Rails
29
+ Engines are a better solution for breaking up complexity. I say that engines and
30
+ Hexagonal can be used together. Engines don't solve the problem of domain
31
+ objects being tightly coupled to the database and often to each other through
32
+ callbacks for example.
33
+
34
+ Hexagonal is an abstraction of the way that I've been building my latest Ruby
35
+ applications. It's inspired by [Matt Wynne](http://www.confreaks.com/videos/977-goruco2012-hexagonal-rails),
36
+ [Brandur](https://brandur.org/mediator), [grouper](http://eng.joingrouper.com/blog/2014/03/03/rails-the-missing-parts-interactors),
37
+ [agileplanner](https://www.agileplannerapp.com/blog/building-agile-planner/refactoring-with-hexagonal-rails),
38
+ [Victor Savkins](http://victorsavkin.com/post/42542190528/hexagonal-architecture-for-rails-developers)
39
+ and many discussions with [@soulim](https://github.com/soulim).
40
+ Hexagonal provides base classes for everything required to build a small
41
+ modular, Ruby application. See Structure section below.
42
+
43
+ ## Usage
44
+
45
+ When using Rails, the following generators are available. When not using Rails,
46
+ please see the examples folder for how to extend the provided classes
47
+ correctly.
48
+
49
+ ### Generate a resource
50
+ This will generate a repository, policy, mediators and runners for all CRUD
51
+ actions for a specified resource.
52
+ ```
53
+ rails generate hexagonal:resource [RESOURCE_NAME]
54
+ ```
55
+
56
+ ### Generate a repository
57
+ ```
58
+ rails generate hexagonal:repository [REPOSITORY_NAME]
59
+ ```
60
+
61
+ ### Generate a runner
62
+ ```
63
+ rails generate hexagonal:runner [RUNNER_NAME]
64
+ ```
65
+
66
+ ### Generate a mediator
67
+ ```
68
+ rails generate hexagonal:mediator [MEDIATOR_NAME]
69
+ ```
70
+
71
+ ### Generate a policy
72
+ ```
73
+ rails generate hexaganal:policy [POLICY_NAME]
74
+ ```
75
+
76
+ ### Generate a worker
77
+ ```
78
+ rails generate hexagonal:worker [WORKER_NAME]
79
+ ```
80
+
81
+ ### Generate a job
82
+ ```
83
+ rails generate hexagonal:job [JOB_NAME]
84
+ ```
85
+
86
+ ### Generate a decorator
87
+ ```
88
+ rails generate hexagonal:decorator [MODEL_NAME]
89
+ ```
90
+
91
+ ## Structure
92
+
93
+ Here is the basic app structure along with some implementation examples.
94
+ Not all of these objects need to inherited/extended from Hexagonal. Services,
95
+ Jobs and Workers are not planned to be part of the gem.
96
+
97
+ ### Runners (app/runners)
98
+ These are my own creation. They are responsible for model materialization,
99
+ authorization (authentication still happens in the controller) and running
100
+ parameter validation
101
+
102
+ ```ruby
103
+ class CreateJobRunner < Hexagonal::Runners::CreateRunner
104
+ private
105
+
106
+ def form
107
+ @form ||= JobForm.new(attributes)
108
+ end
109
+
110
+ def mediator
111
+ @mediator ||= CreateJobMediator.new(user, form.attributes)
112
+ end
113
+ end
114
+ ```
115
+
116
+ ### Mediators (app/mediators)
117
+ A [Mediator](http://en.wikipedia.org/wiki/Mediator_pattern) is a design
118
+ pattern encapsulating how a set of objects interact
119
+ The mediators take care of saving/updating/deleting/etc and calling out
120
+ to workers (for longer jobs, like looking up social media data)
121
+ or jobs (for shorter jobs, like sending email)
122
+
123
+ ```ruby
124
+ class CreateJobMediator < Hexagonal::Mediators::CreateMediator
125
+ def target
126
+ @target ||= Job.new(attributes)
127
+ end
128
+
129
+ private
130
+
131
+ def default_attributes
132
+ { created_by_id: user.id, account_id: user.account_id }
133
+ end
134
+
135
+ def repository
136
+ @repository ||= JobRepository.new
137
+ end
138
+ end
139
+ ```
140
+
141
+ ### Forms (app/forms)
142
+ These contain parameter validation logic.
143
+
144
+ ```ruby
145
+ class JobForm
146
+ include Hexagonal::Form
147
+
148
+ attribute :title, String
149
+ attribute :remote_working_allowed, Boolean, default: true
150
+
151
+ validates :title, presence: true
152
+ end
153
+ ```
154
+
155
+ ### Decorators (app/decorators)
156
+ These are used to add an object-oriented presentation layer. Decorators use the
157
+ [draper gem](https://github.com/drapergem/draper).
158
+
159
+ ```ruby
160
+ class JobDecorator < Draper::Decorator
161
+ decorates :job
162
+
163
+ delegate_all
164
+
165
+ def address
166
+ [street, city, country].compact.join(', ')
167
+ end
168
+ end
169
+ ```
170
+
171
+ ### Workers (app/workers)
172
+ Sidekiq workers to handle longer running tasks (to avoid slow requests).
173
+ The workers themselves have barely any code inside. They just materialize any
174
+ models required and then call the required service.
175
+
176
+ ```ruby
177
+ class ContactImportWorker
178
+ include Sidekiq::Worker
179
+
180
+ def perform(user_id)
181
+ User.find(user_id).tap do |user|
182
+ ContactImportService.new(user).call
183
+ end
184
+ end
185
+ end
186
+ ```
187
+
188
+ ### Jobs (app/jobs)
189
+ Sucker Punch jobs. Sucker punch handles background tasks in a single process
190
+ using asynchronous Ruby. It's good for keeping costs down on heroku.
191
+ I find sidekiq to [generally be overkill](http://brandonhilkert.com/blog/why-i-wrote-the-sucker-punch-gem/)
192
+ for most tasks (email sending for example). Sucker Punch workers are the same as
193
+ Sidekiq workers. Just materialize models and call the correct service
194
+
195
+ ```ruby
196
+ class SignupConfirmationJob
197
+ include SuckerPunch::Job
198
+
199
+ def perform(user)
200
+ UserMailer.signup_confirmation(user).deliver
201
+ end
202
+ end
203
+ ```
204
+
205
+ ### Services (app/services)
206
+ When the app needs to interact with any third party service then a service
207
+ object is used. They are called either from workers or mediators. They handle
208
+ the details of things like email sending, lookup up social media data,
209
+ importing/syncing contacts, polling IMAP, etc.
210
+
211
+ ```ruby
212
+ class ContactImportService
213
+ def initialize(user)
214
+ @user = user
215
+ end
216
+
217
+ def call
218
+ # some complex logic to pull contacts from social media
219
+ end
220
+ end
221
+ ```
222
+
223
+ ### Responses (app/responses)
224
+ These handle responding to the client. They are almost all just simple
225
+ delegators that help me to avoid duplicating code in controllers.
226
+
227
+ ```ruby
228
+ class CreateResponse < SimpleDelegator
229
+ def created_successfully(object)
230
+ respond_with object
231
+ end
232
+
233
+ def creation_failed(object)
234
+ render :errors, object.errors.as_json
235
+ end
236
+ end
237
+ ```
238
+
239
+ ### Repositories (app/repositories)
240
+ Used to access the database. I'm trying to gradually decouple the app completely
241
+ from ActiveRecord. It keeps the queries private instead of leaking storage API
242
+ details into the app.
243
+
244
+ ```ruby
245
+ class JobRepository
246
+ include Hexagonal::Repository
247
+
248
+ def find_by_creator_id(creator_id)
249
+ adapter.where(creator_id: creator_id)
250
+ end
251
+ end
252
+ ```
253
+
254
+ ### Adapters (app/adapters)
255
+ Adapters communicate between specific storage implementations and repositories.
256
+ So far there is only an ActiveRecordAdapter. When I comes time to switch to
257
+ something else, perhaps sequel, then I will just need to define a new adapter
258
+ and plug it into the base repository. Adapters also need to define a Unit Of
259
+ Work in order to be able to roll back groups of changes. With SQL this is just a
260
+ wrapper around a Transaction.
261
+
262
+ ### Errors (app/errors)
263
+ These define business specific errors rather than just using the standard ones.
264
+ Also map database specific errors to business ones so that the database can be
265
+ switched out easily.
266
+
267
+ ### Policies (app/policies)
268
+ These handle authorization.
269
+
270
+ ```ruby
271
+ class JobPolicy
272
+ def initialize(user, job)
273
+ @user = user
274
+ @job = job
275
+ end
276
+
277
+ def delete?
278
+ job.created_by == user
279
+ end
280
+
281
+ private
282
+
283
+ attr_reader :user, :job
284
+ end
285
+ ```
286
+
287
+ ## Example
288
+
289
+ Here is an example Rails API controller using hexagonal
290
+ ```ruby
291
+ class JobsController < ApplicationController::Base
292
+ before_filter :authenticate_user!
293
+
294
+ def index
295
+ filter_runner.run
296
+ end
297
+
298
+ def show
299
+ find_runner.run
300
+ end
301
+
302
+ def create
303
+ create_runner.run
304
+ end
305
+
306
+ def update
307
+ update_runner.run
308
+ end
309
+
310
+ def destroy
311
+ delete_runner.run
312
+ end
313
+
314
+ private
315
+
316
+ def find_runner
317
+ FindJobRunner.new(find_one_response, current_user, params[:id])
318
+ end
319
+
320
+ def find_one_response
321
+ FindOneResponse.new(self)
322
+ end
323
+
324
+ def filter_runner
325
+ FilterJobsRunner.new(find_all_response, current_user, params)
326
+ end
327
+
328
+ def create_runner
329
+ CreateJobRunner.new(create_response, current_user, params[:job])
330
+ end
331
+
332
+ def create_response
333
+ CreateResponse.new(self)
334
+ end
335
+
336
+ def update_runner
337
+ UpdateJobRunner
338
+ .new(update_response, current_user, params[:id], params[:job])
339
+ end
340
+
341
+ def update_response
342
+ UpdateResponse.new(self)
343
+ end
344
+
345
+ def delete_runner
346
+ DeleteRunner.new(delete_response, current_user, params[:id])
347
+ end
348
+
349
+ def delete_response
350
+ DeleteResponse.new(self)
351
+ end
352
+ end
353
+ ```
354
+
355
+ ## Supported Rubies
356
+ 2.0.x, 2.1.x, JRuby 1.7.x
357
+
358
+ ## Contributing
359
+
360
+ 1. Fork it ( https://github.com/[my-github-username]/hexagonal/fork )
361
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
362
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
363
+ 4. Push to the branch (`git push origin my-new-feature`)
364
+ 5. Create a new Pull Request
365
+
366
+ ## Alternatives
367
+ - [hexx](https://github.com/nepalez/hexx)
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -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 'hexagonal/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hexagonal"
8
+ spec.version = Hexagonal::VERSION
9
+ spec.authors = ["Matt Beedle"]
10
+ spec.email = ["mattbeedle@googlemail.com"]
11
+ spec.summary = %q{A simple gem for building hexagonal Ruby applications}
12
+ spec.description = %q{A simple gem for building hexagonal Ruby applications}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency 'activerecord'
22
+ spec.add_runtime_dependency 'activesupport'
23
+
24
+ spec.add_development_dependency 'bogus'
25
+ spec.add_development_dependency "bundler", "~> 1.7"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ end
@@ -0,0 +1,11 @@
1
+ require 'hexagonal/version'
2
+
3
+ require 'hexagonal/adapters'
4
+ require 'hexagonal/errors'
5
+ require 'hexagonal/mediators'
6
+ require 'hexagonal/repository'
7
+ require 'hexagonal/runners'
8
+
9
+ module Hexagonal
10
+ # Your code goes here...
11
+ end
@@ -0,0 +1 @@
1
+ require_relative 'adapters/active_record_adapter'
@@ -0,0 +1,65 @@
1
+ module Hexagonal
2
+ module Adapters
3
+ class ActiveRecordAdapter
4
+ include ActiveSupport::Rescuable
5
+
6
+ pattr_initialize :persistence
7
+
8
+ rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
9
+ rescue_from ActiveRecord::StatementInvalid, with: :statement_invalid
10
+
11
+ def find(id)
12
+ persistence.find id
13
+ rescue ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid => e
14
+ rescue_with_handler(e)
15
+ end
16
+
17
+ def save(object)
18
+ object.save
19
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::StatementInvalid => e
20
+ rescue_with_handler(e)
21
+ end
22
+
23
+ def save!(object)
24
+ object.save!
25
+ object
26
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::StatementInvalid => e
27
+ rescue_with_handler(e)
28
+ end
29
+
30
+ def destroy(object)
31
+ object.destroy
32
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::StatementInvalid => e
33
+ rescue_with_handler(e)
34
+ end
35
+
36
+ def query(&block)
37
+ yield(persistence)
38
+ rescue ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid => e
39
+ rescue_with_handler(e)
40
+ end
41
+
42
+ def unit_of_work
43
+ @unit_of_work ||=
44
+ Hexagonal::Adapters::ActiveRecordAdapter::UnitOfWork.new
45
+ end
46
+
47
+ def method_missing(method_sym, *arguments, &block)
48
+ persistence.send(method_sym, *arguments, &block)
49
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::StatementInvalid => e
50
+ rescue_with_handler(e)
51
+ end
52
+
53
+ private
54
+
55
+ def record_not_found(exception)
56
+ fail Hexagonal::Errors::RecordNotFoundException, exception, caller
57
+ end
58
+
59
+ def statement_invalid(exception)
60
+ fail Hexagonal::Errors::StatementInvalidException, exception, caller
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,11 @@
1
+ module Hexagonal
2
+ module Adapters
3
+ module ActiveRecordAdapter
4
+ class UnitOfWork
5
+ def run(&block)
6
+ ActiveRecord::Base.transaction { yield }
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'errors/record_invalid_exception'
2
+ require_relative 'errors/unauthorized_exception'
@@ -0,0 +1,6 @@
1
+ module Hexagonal
2
+ module Errors
3
+ class RecordInvalidException < ActiveRecord::RecordInvalid
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Hexagonal
2
+ module Errors
3
+ class UnauthorizedException < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'mediators/create_mediator'
2
+ require_relative 'mediators/delete_mediator'
@@ -0,0 +1,26 @@
1
+ module Hexagonal
2
+ module Mediators
3
+ class CreateMediator
4
+ pattr_initialize :user, :attributes
5
+
6
+ attr_writer :repository
7
+
8
+ def initialize(user, attributes)
9
+ @user = user
10
+ @attributes = attributes.merge(default_attributes)
11
+ end
12
+
13
+ def call
14
+ repository.save! target
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :attributes, :repository
20
+
21
+ def default_attributes
22
+ {}
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ module Hexagonal
2
+ module Mediators
3
+ class DeleteMediator
4
+ pattr_initialize :user, :target
5
+
6
+ attr_writer :repository
7
+
8
+ def call
9
+ repository.destroy target
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :repository
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ module Hexagonal
2
+ class Repository
3
+ attr_writer :database_klass, :adapter
4
+
5
+ def all
6
+ adapter.all
7
+ end
8
+
9
+ def find(id)
10
+ adapter.find id
11
+ end
12
+
13
+ def save(object)
14
+ adapter.save(object)
15
+ end
16
+
17
+ def save!(object)
18
+ adapter.save!(object)
19
+ end
20
+
21
+ def destroy(object)
22
+ adapter.destroy(object)
23
+ end
24
+
25
+ def unit_of_work
26
+ adapter.unit_of_work
27
+ end
28
+
29
+ private
30
+
31
+ def adapter
32
+ @adapter ||= Hexagonal::Adapters::ActiveRecordAdapter.new(database_klass)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'responses/create_response'
2
+ require_relative 'responses/delete_response'
3
+ require_relative 'responses/find_all_response'
@@ -0,0 +1,13 @@
1
+ module Hexagonal
2
+ module Responses
3
+ class CreateResponse
4
+ def created_successfully(object)
5
+ present object.class.to_s.underscore.to_sym, object
6
+ end
7
+
8
+ def creation_failed(exception)
9
+ error!({ errors: exception.record.errors }, 422)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Hexagonal
2
+ module Responses
3
+ class DeleteResponse < SimpleDelegator
4
+ def deleted_successfully(object)
5
+ status 204
6
+ end
7
+
8
+ def unauthorized(exception)
9
+ error!({ message: exception.message }, 401)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ module Hexagonal
2
+ module Responses
3
+ class FindAllResponse
4
+ delegate :error!, :garner, :pagination, :present, to: :target
5
+
6
+ def initialize(target, key, paginated: false, cache_method: nil)
7
+ @target = target
8
+ @key = key
9
+ @paginated = paginated
10
+ @cache_method = cache_method
11
+ end
12
+
13
+ def found(objects)
14
+ garner.bind(cache_key(objects)) do
15
+ present(key, objects).as_json
16
+ present(:meta, pagination(objects)).as_json if paginated
17
+ end
18
+ end
19
+
20
+ def invalid(exception)
21
+ error!({ errors: exception.record.errors }, 422)
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :cache_method, :key, :paginated, :target
27
+
28
+ def cache_key(objects)
29
+ cache_method ? objects.send(cache_method) : objects
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'runners/create_runner'
2
+ require_relative 'runners/delete_runner'
3
+ require_relative 'runners/filter_runner'
@@ -0,0 +1,32 @@
1
+ module Hexagonal
2
+ module Runners
3
+ class Hexagonal::Runners::CreateRunner
4
+ pattr_initialize :listener, :user, :attributes
5
+
6
+ delegate :created_successfully, :creation_failed, to: :listener
7
+ delegate :target, to: :mediator
8
+
9
+ attr_writer :form
10
+
11
+ def run
12
+ validate!
13
+ create!
14
+ rescue Hexagonal::Errors::RecordInvalidException => ex
15
+ creation_failed(ex)
16
+ end
17
+
18
+ private
19
+
20
+ def create!
21
+ mediator.call
22
+ created_successfully(target)
23
+ end
24
+
25
+ def validate!
26
+ unless form.valid?
27
+ fail Hexagonal::Errors::RecordInvalidException, form, caller
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ module Hexagonal
2
+ module Runners
3
+ class DeleteRunner
4
+ pattr_initialize :listener, :user, :id
5
+
6
+ delegate :unauthorized, :deleted_successfully, to: :listener
7
+
8
+ attr_writer :policy, :repository
9
+
10
+ def run
11
+ authorize!
12
+ delete!
13
+ deleted_successfully target
14
+ rescue Hexagonal::Errors::UnauthorizedException => ex
15
+ unauthorized(ex)
16
+ end
17
+
18
+ private
19
+
20
+ def authorize!
21
+ fail! unless policy.delete?
22
+ end
23
+
24
+ def fail!
25
+ fail Hexagonal::Errors::UnauthorizedException, 'Unauthorized', caller
26
+ end
27
+
28
+ def delete!
29
+ mediator.call
30
+ end
31
+
32
+ def target
33
+ @target ||= repository.find id
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ module Hexagonal
2
+ module Runners
3
+ class FilterRunner
4
+ attr_writer :form, :repository
5
+
6
+ delegate :found, :invalid, to: :listener
7
+
8
+ def initialize(listener, user, attributes = nil)
9
+ @listener = listener
10
+ @user = user
11
+ @attributes = attributes
12
+ end
13
+
14
+ def run
15
+ validate! if attributes
16
+ found items
17
+ rescue Hexagonal::Errors::RecordInvalidException => ex
18
+ invalid ex
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :listener, :user, :attributes
24
+
25
+ def items
26
+ @items ||= repository.filter_for_user(user, form.attributes)
27
+ end
28
+
29
+ def validate!
30
+ unless form.valid?
31
+ fail Hexagonal::Errors::RecordInvalidException, form, caller
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module Hexagonal
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hexagonal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Matt Beedle
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-09-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bogus
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.7'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ description: A simple gem for building hexagonal Ruby applications
84
+ email:
85
+ - mattbeedle@googlemail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - Gemfile
92
+ - LICENSE.txt
93
+ - README.md
94
+ - Rakefile
95
+ - hexagonal.gemspec
96
+ - lib/hexagonal.rb
97
+ - lib/hexagonal/adapters.rb
98
+ - lib/hexagonal/adapters/active_record_adapter.rb
99
+ - lib/hexagonal/adapters/active_record_adapter/unit_of_work.rb
100
+ - lib/hexagonal/errors.rb
101
+ - lib/hexagonal/errors/record_invalid_exception.rb
102
+ - lib/hexagonal/errors/unauthorized_exception.rb
103
+ - lib/hexagonal/mediators.rb
104
+ - lib/hexagonal/mediators/create_mediator.rb
105
+ - lib/hexagonal/mediators/delete_mediator.rb
106
+ - lib/hexagonal/repository.rb
107
+ - lib/hexagonal/responses.rb
108
+ - lib/hexagonal/responses/create_response.rb
109
+ - lib/hexagonal/responses/delete_response.rb
110
+ - lib/hexagonal/responses/find_all_response.rb
111
+ - lib/hexagonal/runners.rb
112
+ - lib/hexagonal/runners/create_runner.rb
113
+ - lib/hexagonal/runners/delete_runner.rb
114
+ - lib/hexagonal/runners/filter_runner.rb
115
+ - lib/hexagonal/version.rb
116
+ homepage: ''
117
+ licenses:
118
+ - MIT
119
+ metadata: {}
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubyforge_project:
136
+ rubygems_version: 2.2.2
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: A simple gem for building hexagonal Ruby applications
140
+ test_files: []