trailblazer 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -1
  3. data/.travis.yml +5 -0
  4. data/CHANGES.md +3 -0
  5. data/Gemfile +4 -0
  6. data/README.md +417 -16
  7. data/Rakefile +14 -0
  8. data/THOUGHTS +12 -0
  9. data/TODO.md +6 -0
  10. data/doc/Trb-The-Stack.png +0 -0
  11. data/doc/trb.jpg +0 -0
  12. data/gemfiles/Gemfile.rails +7 -0
  13. data/gemfiles/Gemfile.rails.lock +99 -0
  14. data/lib/trailblazer.rb +2 -0
  15. data/lib/trailblazer/autoloading.rb +5 -0
  16. data/lib/trailblazer/operation.rb +124 -0
  17. data/lib/trailblazer/operation/controller.rb +76 -0
  18. data/lib/trailblazer/operation/crud.rb +61 -0
  19. data/lib/trailblazer/operation/representer.rb +18 -0
  20. data/lib/trailblazer/operation/responder.rb +24 -0
  21. data/lib/trailblazer/operation/uploaded_file.rb +77 -0
  22. data/lib/trailblazer/operation/worker.rb +96 -0
  23. data/lib/trailblazer/version.rb +1 -1
  24. data/test/crud_test.rb +115 -0
  25. data/test/fixtures/apotomo.png +0 -0
  26. data/test/fixtures/cells.png +0 -0
  27. data/test/operation_test.rb +334 -0
  28. data/test/rails/controller_test.rb +175 -0
  29. data/test/rails/fake_app/app-cells/.gitkeep +0 -0
  30. data/test/rails/fake_app/cells.rb +21 -0
  31. data/test/rails/fake_app/config.rb +3 -0
  32. data/test/rails/fake_app/controllers.rb +101 -0
  33. data/test/rails/fake_app/models.rb +13 -0
  34. data/test/rails/fake_app/rails_app.rb +57 -0
  35. data/test/rails/fake_app/song/operations.rb +63 -0
  36. data/test/rails/fake_app/views/bands/show.html.erb +1 -0
  37. data/test/rails/fake_app/views/songs/new.html.erb +1 -0
  38. data/test/rails/test_helper.rb +4 -0
  39. data/test/responder_test.rb +77 -0
  40. data/test/test_helper.rb +15 -0
  41. data/test/uploaded_file_test.rb +85 -0
  42. data/test/worker_test.rb +116 -0
  43. data/trailblazer.gemspec +10 -2
  44. metadata +160 -23
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f0ed3011dfc0a91641be3c62c80a9fdf8f68d9b2
4
+ data.tar.gz: 3a219f4bb0f1873088249b39d5a51d097c4ea919
5
+ SHA512:
6
+ metadata.gz: 9d8d68ac22880d537923009618bd71ba6463632e5f9a453e4c1d6d8e03f892ff5ee8cf2d240678549d035acbd5d84c6649c426b10eb311c765d1e713fcc83b4e
7
+ data.tar.gz: 556f3942a0e00e5a83a112f88ea1962b0e28a34698acf2e5cbc2b9103c66a14b142a57cf10d8ce623d183858c729d9e9c516c127869d4fe141a78fe7e79aee94
data/.gitignore CHANGED
@@ -7,7 +7,6 @@ Gemfile.lock
7
7
  InstalledFiles
8
8
  _yardoc
9
9
  coverage
10
- doc/
11
10
  lib/bundler/man
12
11
  pkg
13
12
  rdoc
@@ -15,3 +14,4 @@ spec/reports
15
14
  test/tmp
16
15
  test/version_tmp
17
16
  tmp
17
+ /**/*.log
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ - 2.0.0
5
+ - 1.9.3
data/CHANGES.md ADDED
@@ -0,0 +1,3 @@
1
+ # 0.1.0
2
+
3
+ * First stable release after almost 6 months of blood, sweat and tears. I know, this is a ridiculously brief codebase but it was a hell of a job to structure everything the way it is now. Enjoy!
data/Gemfile CHANGED
@@ -2,3 +2,7 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in trailblazer.gemspec
4
4
  gemspec
5
+
6
+ # gem "representable", path: "../representable"
7
+ # gem "reform", path: "../reform"
8
+ # gem "reform", git: "https://github.com/apotonick/reform.git"
data/README.md CHANGED
@@ -1,29 +1,430 @@
1
1
  # Trailblazer
2
2
 
3
- TODO: Write a gem description
3
+ _Trailblazer is a thin layer on top of Rails. It gently enforces encapsulation, an intuitive code structure and gives you an object-oriented architecture._
4
4
 
5
- ## Installation
5
+ In a nutshell: Trailblazer makes you write **logicless models** that purely act as data objects, don't contain callbacks, nested attributes, validations or domain logic. It **removes bulky controllers** and strong_parameters by supplying additional layers to hold that code and **completely replaces helpers**.
6
6
 
7
- Add this line to your application's Gemfile:
7
+ ![](https://raw.githubusercontent.com/apotonick/trailblazer/master/doc/trb.jpg)
8
8
 
9
- gem 'trailblazer'
9
+ Please sign up for my upcoming book [Trailblazer - A new architecture for Rails](https://leanpub.com/trailblazer), check out the free sample chapter and [let me know](http://twitter.com/apotonick) what you think!
10
10
 
11
- And then execute:
11
+ The [demo application](https://github.com/apotonick/gemgem-trbrb) implements what we discuss in the book.
12
12
 
13
- $ bundle
13
+ ## Mission
14
14
 
15
- Or install it yourself as:
15
+ While _Trailblazer_ offers you abstraction layers for all aspects of Ruby On Rails, it does _not_ missionize you. Wherever you want, you may fall back to the "Rails Way" with fat models, monolithic controllers, global helpers, etc. This is not a bad thing, but allows you to step-wise introduce Trailblazer's encapsulation in your app without having to rewrite it.
16
16
 
17
- $ gem install trailblazer
17
+ Trailblazer is all about structure. It helps re-organize existing code into smaller components where different concerns are handled in separated classes. Forms go into form objects, views are object-oriented MVC controllers, the business logic happens in dedicated domain objects backed by completely decoupled persistence objects.
18
18
 
19
- ## Usage
19
+ Again, you can pick which layers you want. Trailblazer doesn't impose technical implementations, it offers mature solutions for recurring problems in all types of Rails applications.
20
20
 
21
- TODO: Write usage instructions here
21
+ Trailblazer is no "complex web of objects and indirection". It solves many problems that have been around for years with a cleanly layered architecture. Only use what you like. And that's the bottom line.
22
22
 
23
- ## Contributing
24
23
 
25
- 1. Fork it
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create new Pull Request
24
+ ## A Concept-Driven OOP Framework
25
+
26
+ Trailblazer offers you a new, more intuitive file layout in Rails apps where you structure files by *concepts*.
27
+
28
+ ```
29
+ app
30
+ ├── concepts
31
+ │ ├── comment
32
+ │ │ ├── cell.rb
33
+ │ │ ├── views
34
+ │ │ │ ├── show.haml
35
+ │ │ │ ├── list.haml
36
+ │ │ ├── assets
37
+ │ │ │ ├── comment.css.sass
38
+ │ │ ├── operation.rb
39
+ │ │ ├── twin.rb
40
+ ```
41
+
42
+ Files, classes and views that logically belong to one _concept_ are kept in one place. You are free to use additional namespaces within a concept. Trailblazer tries to keep it as simple as possible, though.
43
+
44
+ ## Architecture
45
+
46
+ Trailblazer extends the conventional MVC stack in Rails. Keep in mind that adding layers doesn't necessarily mean adding more code and complexity.
47
+
48
+ The opposite is the case: Controller, view and model become lean endpoints for HTTP, rendering and persistence. Redundant code gets eliminated by putting very little application code into the right layer.
49
+
50
+ ![The Trailblazer stack.](https://raw.github.com/apotonick/trailblazer/master/doc/Trb-The-Stack.png)
51
+
52
+ ## Routing
53
+
54
+ Trailblazer uses Rails routing to map URLs to controllers (we will add simplifications to routing soon).
55
+
56
+ ## Controllers
57
+
58
+ Controllers are lean endpoints for HTTP. They differentiate between request formats like HTML or JSON and immediately dispatch to an operation. Controllers do not contain any business logic.
59
+
60
+ Trailblazer provides four methods to present and invoke operations. But before that, you need to include the `Controller` module.
61
+
62
+ ```ruby
63
+ class CommentsController < ApplicationController
64
+ include Trailblazer::Operation::Controller
65
+
66
+ ```
67
+
68
+ ### Rendering the form object
69
+
70
+ Operations can populate and present their form object so it can be used with `simple_form` and other form helpers.
71
+
72
+ ```ruby
73
+
74
+ def new
75
+ form Comment::Create
76
+ end
77
+ ```
78
+
79
+ This will run the operation but _not_ its `validate` code. It then sets the `@form` instance variable in the controller so it can be rendered.
80
+
81
+ ```haml
82
+ = form_for @form do |f|
83
+ = f.input f.body
84
+ ```
85
+
86
+ `#form` is meant for HTML actions like `#new` and `#edit`, only.
87
+
88
+ ### Running an operation
89
+
90
+ If you do not intend to maintain different request formats, the easiest is to use `#run` to process incoming data using an operation.
91
+
92
+ ```ruby
93
+ def create
94
+ run Comment::Create
95
+ end
96
+ ```
97
+
98
+ This will simply run `Comment::Create[params]`.
99
+
100
+ You can pass your own params, too.
101
+
102
+ ```ruby
103
+ def create
104
+ run Comment::Create, params.merge({current_user: current_user})
105
+ end
106
+ ```
107
+
108
+ An additional block will be executed _only if_ the operation result is valid.
109
+
110
+ ```ruby
111
+ def create
112
+ run Comment::Create do |op|
113
+ return redirect_to(comments_path, notice: op.message)
114
+ end
115
+ end
116
+ ```
117
+
118
+ Note that the operation instance is yield to the block.
119
+
120
+ The case of an invalid response can be handled after the block.
121
+
122
+ ```ruby
123
+ def create
124
+ run Comment::Create do |op|
125
+ # valid code..
126
+ return
127
+ end
128
+
129
+ render action: :new
130
+ end
131
+ ```
132
+
133
+ Don't forget to `return` from the valid block, otherwise both the valid block _and_ the invalid calls after it will be invoked.
134
+
135
+ ### Responding
136
+
137
+ Alternatively, you can use Rails' excellent `#respond_with` to let a responder take care of what to render. Operations can be passed into `respond_with`. This happens automatically in `#respond`, the third way to let Trailblazer invoke an operation.
138
+
139
+ ```ruby
140
+ def create
141
+ respond Comment::Create
142
+ end
143
+ ```
144
+
145
+ This will simply run the operation and chuck the instance into the responder letting the latter sort out what to render or where to redirect. The operation delegates respective calls to its internal `model`.
146
+
147
+ You can also handle different formats in that block. It is totally fine to do that in the controller as this is _endpoint_ logic that is HTTP-specific and not business.
148
+
149
+ ```ruby
150
+ def create
151
+ respond Comment::Create do |op, formats|
152
+ formats.html { redirect_to(op.model, :notice => op.valid? ? "All good!" : "Fail!") }
153
+ formats.json { render nothing: true }
154
+ end
155
+ end
156
+ ```
157
+
158
+ The block passed to `#respond` is _always_ executed, regardless of the operation's validity result. Goal is to let the responder handle the validity of the operation.
159
+
160
+ The `formats` object is simply passed on to `#respond_with`.
161
+
162
+ ### Presenting
163
+
164
+ For `#show` actions that simply present the model using a HTML page or a JSON or XML document the `#present` method comes in handy.
165
+
166
+ ```ruby
167
+ def show
168
+ present Comment::Create
169
+ end
170
+ ```
171
+
172
+ Again, this will only run the operation's setup and provide the model in `@model`. You can then use a cell or controller view for HTML to present the model.
173
+
174
+ For document-based APIs and request types that are not HTTP the operation will be advised to render the JSON or XML document using the operation's representer.
175
+
176
+ Note that `#present` will also work instead of `#form` (allowing it to be used in `#new` and `#edit`, too) as the responder will _not_ trigger any rendering in those actions.
177
+
178
+ ### Controller API
179
+
180
+ In all three cases the following instance variables are assigned: `@operation`, `@form`, `@model`.
181
+
182
+ ## Operation
183
+
184
+ Operations encapsulate business logic. One operation per high-level domain _function_ is used. Different formats or environments are handled in subclasses. Operations don't know about HTTP.
185
+
186
+ ```ruby
187
+ class Comment < ActiveRecord::Base
188
+ class Create < Trailblazer::Operation
189
+ def process(params)
190
+ # do whatever you feel like.
191
+ self
192
+ end
193
+ end
194
+ end
195
+ ```
196
+
197
+ Operations only need to implement `#process` which receives the params from the caller.
198
+
199
+ ### Call style
200
+
201
+ The simplest way of running an operation is the _call style_.
202
+
203
+ ```ruby
204
+ op = Comment::Create[params]
205
+ ```
206
+
207
+ Using `Operation#[]` will return the operation instance. In case of an invalid operation, this will raise an exception.
208
+
209
+ Note how this can easily be used for test factories.
210
+
211
+ ```ruby
212
+ let(:comment) { Comment::Create[valid_comment_params].model }
213
+ ```
214
+
215
+ Using operations as test factories is a fundamental concept of Trailblazer to remove buggy redundancy in tests and manual factories.
216
+
217
+ ### Run style
218
+
219
+ You can run an operation manually and use the same block semantics as found in the controller.
220
+
221
+ ```ruby
222
+ Comment::Create.run(params) do |op|
223
+ # only run when valid.
224
+ end
225
+ ```
226
+
227
+ Of course, this does _not_ throw an exception but simply skips the block when the operation is invalid.
228
+
229
+ ## Validations
230
+
231
+ Operations usually have a form object which is simply a `Reform::Form` class. All the [API documented in Reform](https://github.com/apotonick/reform) can be applied and used.
232
+
233
+ The operation makes use of the form object using the `#validate` method.
234
+
235
+ ```ruby
236
+ class Comment < ActiveRecord::Base
237
+ class Create < Trailblazer::Operation
238
+ contract do
239
+ property :body, validates: {presence: true}
240
+ end
241
+
242
+ def process(params)
243
+ @model = Comment.new
244
+
245
+ validate(params[:comment], @model) do |f|
246
+ f.save
247
+ end
248
+ end
249
+ end
250
+ end
251
+ ```
252
+
253
+ The contract (aka _form_) is defined in the `::contract` block. You can implement nested forms, default values, validations, and everything else Reform provides.
254
+
255
+ In case of a valid form the block for `#validate` is invoked. It receives the populated form object. You can use the form to save data or write your own logic.
256
+
257
+ Technically speaking, what really happens in `Operation#validate` is the following.
258
+
259
+ ```ruby
260
+ contract_class.new(@model).validate(params[:comment])
261
+ ```
262
+
263
+ This is a familiar work-flow from Reform. Validation does _not_ touch the model.
264
+
265
+
266
+ ## Models
267
+
268
+ Models for persistence can be implemented using any ORM you fancy, for instance [ActiveRecord](https://github.com/rails/rails/tree/master/activerecord#active-record--object-relational-mapping-in-rails) or [Datamapper](http://datamapper.org/).
269
+
270
+ In Trailblazer, models are completely empty and solely configure database-relevant directives and associations. No business logic is allowed in models. Only operations, views and cells can access models directly.
271
+
272
+ ## Views
273
+
274
+ View rendering can happen using the controller as known from Rails. This is absolutely fine for simple views.
275
+
276
+ More complex UI logic happens in _View Models_ as found in [Cells](https://github.com/apotonick/cells). View models also replace helpers.
277
+
278
+
279
+
280
+
281
+ 8. **HTTP API** Consuming and rendering API documents (e.g. JSON or XML) is done via [roar](https://github.com/apotonick/roar) and [representable](https://github.com/apotonick/representable). They usually inherit the schema from <em>Contract</em>s .
282
+
283
+ 10. **Tests** Subject to tests are mainly <em>Operation</em>s and <em>View Model</em>s, as they encapsulate endpoint behaviour of your app. As a nice side effect, factories are replaced by simple _Operation_ calls.
284
+
285
+ Trailblazer is basically a mash-up of mature gems that have been developed over the past 10 years and are used in hundreds and thousands of production apps.
286
+
287
+
288
+ ## Controller API
289
+
290
+ ### Normalizing params
291
+
292
+ Override `#process_params!` to add or remove values to `params` before the operation is run. This is called in `#run`, `#respond` and `#present`.
293
+
294
+ ```ruby
295
+ class CommentsController < ApplicationController
296
+ # ..
297
+
298
+ private
299
+ def process_params!(params)
300
+ params.merge!(current_user: current_user)
301
+ end
302
+ end
303
+ ```
304
+
305
+ This centralizes params normalization and doesn't require you to do that in every action manually.
306
+
307
+
308
+ ### Different Request Formats
309
+
310
+ The controller helpers `#present` and `#respond` automatically pass the request body into the operation via the `params` hash. It's up to the operation's builder to decide which class to instantiate.
311
+
312
+ ```ruby
313
+ class Create < Trailblazer::Operation
314
+ builds do |params|
315
+ JSON if params[:format] == "json"
316
+ end
317
+ end
318
+ ```
319
+
320
+ [Note that this will soon be provided with a module.]
321
+
322
+
323
+ ## Operation API
324
+
325
+ ### CRUD Semantics
326
+
327
+ You can make Trailblazer find and create models for you using the `CRUD` module.
328
+
329
+ ```ruby
330
+ require 'trailblazer/operation/crud'
331
+
332
+ class Comment < ActiveRecord::Base
333
+ class Create < Trailblazer::Operation
334
+ include CRUD
335
+ model Comment, :create
336
+
337
+ contract do
338
+ # ..
339
+ end
340
+
341
+ def process(params)
342
+ validate(params[:comment]) do |f|
343
+ f.save
344
+ end
345
+ end
346
+ end
347
+ end
348
+ ```
349
+
350
+ You have to tell `CRUD` the model class and what action to implement using `::model`.
351
+
352
+ Note how you do not have to pass the `@model` to validate anymore. Also, the `@model` gets created automatically and is accessable using `Operation#model`.
353
+
354
+ In inherited operations, you can override the action, only, using `::action`.
355
+
356
+ ```ruby
357
+ class Update < Create
358
+ action :update
359
+ end
360
+ ```
361
+
362
+ Another action is `:find` (which is currently doing the same as `:update`) to find a model by using `params[:id]`.
363
+
364
+ ### Background Processing
365
+
366
+ To run an operation in Sidekiq (ActiveJob-support coming!) all you need to do is include the `Worker` module.
367
+
368
+ ```ruby
369
+ require 'trailblazer/operation/worker'
370
+
371
+ class Comment::Image::Crop < Trailblazer::Operation
372
+ include Worker
373
+
374
+ def process(params)
375
+ # will be run asynchronous.
376
+ end
377
+ end
378
+ ```
379
+
380
+ ### Rendering Operation's Form
381
+
382
+ You have access to an operation's form using `::contract`.
383
+
384
+ ```ruby
385
+ Comment::Create.contract(params)
386
+ ```
387
+
388
+ This will run the operation's `#process` method _without_ the validate block and return the contract.
389
+
390
+ ### Marking Operation as Invalid
391
+
392
+ Sometimes you don't need a form object but still want the validity behavior of an operation.
393
+
394
+ ```ruby
395
+ def process(params)
396
+ return invalid! unless params[:id]
397
+
398
+ Comment.find(params[:id]).destroy
399
+ self
400
+ end
401
+ ```
402
+
403
+ `#invalid!` returns `self` per default but accepts any result.
404
+
405
+
406
+ ### Worker::FileMarshaller: needs representable 2.1.1 (.schema)
407
+
408
+
409
+ ### Testing Operations
410
+
411
+ ## Autoloading
412
+
413
+ Use our autoloading if you dislike explicit requires.
414
+
415
+ You can just add
416
+
417
+ ```ruby
418
+ require 'trailblazer/autoloading'
419
+ ```
420
+
421
+ to `config/initializers/trailblazer.rb` and files will be "automatically" loaded.
422
+
423
+
424
+
425
+ ## Why?
426
+
427
+ * Grouping code, views and assets by concepts increases the **maintainability** of your apps. Developers will find their way faster into your structure as the file layout is more intuitive.
428
+ * Finding bugs gets less frustrating as encapsulated layers allow **testing components** in total isolation. Once you know your form and your view are ok, it must be the parsing code.
429
+ * The reusability of code increases drastically as Trailblazer gently pushes you towards encapsulation. No more redundant helpers but clean inheritance.
430
+ * No more surprises from ActiveRecord's massive API. The separation between persistence and domain automatically results in smaller, less destructive APIs for your models.