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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +367 -0
- data/Rakefile +2 -0
- data/hexagonal.gemspec +27 -0
- data/lib/hexagonal.rb +11 -0
- data/lib/hexagonal/adapters.rb +1 -0
- data/lib/hexagonal/adapters/active_record_adapter.rb +65 -0
- data/lib/hexagonal/adapters/active_record_adapter/unit_of_work.rb +11 -0
- data/lib/hexagonal/errors.rb +2 -0
- data/lib/hexagonal/errors/record_invalid_exception.rb +6 -0
- data/lib/hexagonal/errors/unauthorized_exception.rb +6 -0
- data/lib/hexagonal/mediators.rb +2 -0
- data/lib/hexagonal/mediators/create_mediator.rb +26 -0
- data/lib/hexagonal/mediators/delete_mediator.rb +17 -0
- data/lib/hexagonal/repository.rb +35 -0
- data/lib/hexagonal/responses.rb +3 -0
- data/lib/hexagonal/responses/create_response.rb +13 -0
- data/lib/hexagonal/responses/delete_response.rb +13 -0
- data/lib/hexagonal/responses/find_all_response.rb +33 -0
- data/lib/hexagonal/runners.rb +3 -0
- data/lib/hexagonal/runners/create_runner.rb +32 -0
- data/lib/hexagonal/runners/delete_runner.rb +37 -0
- data/lib/hexagonal/runners/filter_runner.rb +36 -0
- data/lib/hexagonal/version.rb +3 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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)
|
data/Rakefile
ADDED
data/hexagonal.gemspec
ADDED
@@ -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
|
data/lib/hexagonal.rb
ADDED
@@ -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,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,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,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,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,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
|
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: []
|