aldous 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (212) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.irbrc +3 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +591 -0
  9. data/Rakefile +1 -0
  10. data/aldous.gemspec +24 -0
  11. data/examples/basic_todo/.foreman +1 -0
  12. data/examples/basic_todo/.gitignore +16 -0
  13. data/examples/basic_todo/.rspec +3 -0
  14. data/examples/basic_todo/.ruby-version +2 -0
  15. data/examples/basic_todo/Gemfile +52 -0
  16. data/examples/basic_todo/Procfile +1 -0
  17. data/examples/basic_todo/README.rdoc +28 -0
  18. data/examples/basic_todo/Rakefile +6 -0
  19. data/examples/basic_todo/app/assets/images/.keep +0 -0
  20. data/examples/basic_todo/app/assets/javascripts/application.js +13 -0
  21. data/examples/basic_todo/app/assets/stylesheets/application.css +15 -0
  22. data/examples/basic_todo/app/controller_actions/base_action.rb +24 -0
  23. data/examples/basic_todo/app/controller_actions/base_precondition.rb +2 -0
  24. data/examples/basic_todo/app/controller_actions/home_controller/show.rb +8 -0
  25. data/examples/basic_todo/app/controller_actions/shared/ensure_user_not_disabled_precondition.rb +9 -0
  26. data/examples/basic_todo/app/controller_actions/sign_ins_controller/create.rb +24 -0
  27. data/examples/basic_todo/app/controller_actions/sign_ins_controller/destroy.rb +9 -0
  28. data/examples/basic_todo/app/controller_actions/sign_ins_controller/new.rb +7 -0
  29. data/examples/basic_todo/app/controller_actions/sign_ins_controller/user_params.rb +9 -0
  30. data/examples/basic_todo/app/controller_actions/sign_ups_controller/create.rb +23 -0
  31. data/examples/basic_todo/app/controller_actions/sign_ups_controller/new.rb +7 -0
  32. data/examples/basic_todo/app/controller_actions/sign_ups_controller/user_params.rb +9 -0
  33. data/examples/basic_todo/app/controller_actions/todos/all_completed_controller/destroy.rb +17 -0
  34. data/examples/basic_todo/app/controller_actions/todos/completed_controller/create.rb +29 -0
  35. data/examples/basic_todo/app/controller_actions/todos_controller/create.rb +26 -0
  36. data/examples/basic_todo/app/controller_actions/todos_controller/destroy.rb +21 -0
  37. data/examples/basic_todo/app/controller_actions/todos_controller/edit.rb +19 -0
  38. data/examples/basic_todo/app/controller_actions/todos_controller/index.rb +19 -0
  39. data/examples/basic_todo/app/controller_actions/todos_controller/new.rb +17 -0
  40. data/examples/basic_todo/app/controller_actions/todos_controller/todo_params.rb +9 -0
  41. data/examples/basic_todo/app/controller_actions/todos_controller/update.rb +28 -0
  42. data/examples/basic_todo/app/controller_actions/users_controller/index.rb +19 -0
  43. data/examples/basic_todo/app/controllers/application_controller.rb +9 -0
  44. data/examples/basic_todo/app/controllers/home_controller.rb +5 -0
  45. data/examples/basic_todo/app/controllers/sign_ins_controller.rb +5 -0
  46. data/examples/basic_todo/app/controllers/sign_ups_controller.rb +5 -0
  47. data/examples/basic_todo/app/controllers/todos/all_completed_controller.rb +5 -0
  48. data/examples/basic_todo/app/controllers/todos/completed_controller.rb +5 -0
  49. data/examples/basic_todo/app/controllers/todos_controller.rb +5 -0
  50. data/examples/basic_todo/app/controllers/users_controller.rb +5 -0
  51. data/examples/basic_todo/app/helpers/application_helper.rb +2 -0
  52. data/examples/basic_todo/app/mailers/.keep +0 -0
  53. data/examples/basic_todo/app/models/ability.rb +27 -0
  54. data/examples/basic_todo/app/models/role.rb +5 -0
  55. data/examples/basic_todo/app/models/todo.rb +5 -0
  56. data/examples/basic_todo/app/models/user.rb +12 -0
  57. data/examples/basic_todo/app/models/user_role.rb +5 -0
  58. data/examples/basic_todo/app/services/create_user_service.rb +26 -0
  59. data/examples/basic_todo/app/services/find_current_user_service.rb +29 -0
  60. data/examples/basic_todo/app/services/sign_in_service.rb +13 -0
  61. data/examples/basic_todo/app/services/sign_out_service.rb +12 -0
  62. data/examples/basic_todo/app/views/base_view.rb +18 -0
  63. data/examples/basic_todo/app/views/defaults/bad_request.html.slim +12 -0
  64. data/examples/basic_todo/app/views/defaults/bad_request_view.rb +15 -0
  65. data/examples/basic_todo/app/views/defaults/forbidden.html.slim +6 -0
  66. data/examples/basic_todo/app/views/defaults/forbidden_view.rb +14 -0
  67. data/examples/basic_todo/app/views/defaults/server_error.html.slim +12 -0
  68. data/examples/basic_todo/app/views/defaults/server_error_view.rb +14 -0
  69. data/examples/basic_todo/app/views/home/show.html.slim +5 -0
  70. data/examples/basic_todo/app/views/home/show_redirect.rb +5 -0
  71. data/examples/basic_todo/app/views/home/show_view.rb +7 -0
  72. data/examples/basic_todo/app/views/layouts/application.html.slim +18 -0
  73. data/examples/basic_todo/app/views/modules/_header.html.slim +13 -0
  74. data/examples/basic_todo/app/views/modules/header_view.rb +7 -0
  75. data/examples/basic_todo/app/views/sign_ins/new.html.slim +14 -0
  76. data/examples/basic_todo/app/views/sign_ins/new_view.rb +10 -0
  77. data/examples/basic_todo/app/views/sign_ups/new.html.slim +13 -0
  78. data/examples/basic_todo/app/views/sign_ups/new_view.rb +10 -0
  79. data/examples/basic_todo/app/views/todos/edit.html.slim +14 -0
  80. data/examples/basic_todo/app/views/todos/edit_view.rb +10 -0
  81. data/examples/basic_todo/app/views/todos/index.html.slim +12 -0
  82. data/examples/basic_todo/app/views/todos/index_redirect.rb +5 -0
  83. data/examples/basic_todo/app/views/todos/index_view/_todo.html.slim +8 -0
  84. data/examples/basic_todo/app/views/todos/index_view/todo_view.rb +28 -0
  85. data/examples/basic_todo/app/views/todos/index_view.rb +26 -0
  86. data/examples/basic_todo/app/views/todos/new.html.slim +14 -0
  87. data/examples/basic_todo/app/views/todos/new_view.rb +10 -0
  88. data/examples/basic_todo/app/views/todos/not_found.html.slim +6 -0
  89. data/examples/basic_todo/app/views/todos/not_found_view.rb +15 -0
  90. data/examples/basic_todo/app/views/users/index.html.slim +7 -0
  91. data/examples/basic_todo/app/views/users/index_redirect.rb +5 -0
  92. data/examples/basic_todo/app/views/users/index_view/_user.html.slim +2 -0
  93. data/examples/basic_todo/app/views/users/index_view/user_view.rb +22 -0
  94. data/examples/basic_todo/app/views/users/index_view.rb +26 -0
  95. data/examples/basic_todo/bin/bundle +3 -0
  96. data/examples/basic_todo/bin/rails +8 -0
  97. data/examples/basic_todo/bin/rake +8 -0
  98. data/examples/basic_todo/bin/rspec +7 -0
  99. data/examples/basic_todo/bin/spring +15 -0
  100. data/examples/basic_todo/config/application.rb +41 -0
  101. data/examples/basic_todo/config/boot.rb +4 -0
  102. data/examples/basic_todo/config/database.yml +25 -0
  103. data/examples/basic_todo/config/environment.rb +5 -0
  104. data/examples/basic_todo/config/environments/development.rb +37 -0
  105. data/examples/basic_todo/config/environments/production.rb +78 -0
  106. data/examples/basic_todo/config/environments/test.rb +39 -0
  107. data/examples/basic_todo/config/initializers/aldous.rb +3 -0
  108. data/examples/basic_todo/config/initializers/assets.rb +8 -0
  109. data/examples/basic_todo/config/initializers/backtrace_silencers.rb +7 -0
  110. data/examples/basic_todo/config/initializers/cookies_serializer.rb +3 -0
  111. data/examples/basic_todo/config/initializers/filter_parameter_logging.rb +4 -0
  112. data/examples/basic_todo/config/initializers/inflections.rb +16 -0
  113. data/examples/basic_todo/config/initializers/mime_types.rb +4 -0
  114. data/examples/basic_todo/config/initializers/session_store.rb +3 -0
  115. data/examples/basic_todo/config/initializers/wrap_parameters.rb +14 -0
  116. data/examples/basic_todo/config/locales/en.yml +23 -0
  117. data/examples/basic_todo/config/routes.rb +18 -0
  118. data/examples/basic_todo/config/secrets.yml +22 -0
  119. data/examples/basic_todo/config.ru +4 -0
  120. data/examples/basic_todo/db/migrate/20150226035524_create_user.rb +10 -0
  121. data/examples/basic_todo/db/migrate/20150227004411_create_todo.rb +11 -0
  122. data/examples/basic_todo/db/migrate/20150301110126_roles.rb +22 -0
  123. data/examples/basic_todo/db/migrate/20150301121923_add_user_disabled_column.rb +5 -0
  124. data/examples/basic_todo/db/schema.rb +45 -0
  125. data/examples/basic_todo/db/seeds.rb +7 -0
  126. data/examples/basic_todo/lib/assets/.keep +0 -0
  127. data/examples/basic_todo/lib/tasks/.keep +0 -0
  128. data/examples/basic_todo/log/.keep +0 -0
  129. data/examples/basic_todo/public/404.html +67 -0
  130. data/examples/basic_todo/public/422.html +67 -0
  131. data/examples/basic_todo/public/500.html +66 -0
  132. data/examples/basic_todo/public/favicon.ico +0 -0
  133. data/examples/basic_todo/public/robots.txt +5 -0
  134. data/examples/basic_todo/test/controllers/.keep +0 -0
  135. data/examples/basic_todo/test/fixtures/.keep +0 -0
  136. data/examples/basic_todo/test/helpers/.keep +0 -0
  137. data/examples/basic_todo/test/integration/.keep +0 -0
  138. data/examples/basic_todo/test/mailers/.keep +0 -0
  139. data/examples/basic_todo/test/models/.keep +0 -0
  140. data/examples/basic_todo/test/test_helper.rb +10 -0
  141. data/examples/basic_todo/vendor/assets/javascripts/.keep +0 -0
  142. data/examples/basic_todo/vendor/assets/stylesheets/.keep +0 -0
  143. data/lib/aldous/build_respondable_service.rb +23 -0
  144. data/lib/aldous/configuration.rb +18 -0
  145. data/lib/aldous/controller/action/precondition/wrapper.rb +32 -0
  146. data/lib/aldous/controller/action/precondition.rb +52 -0
  147. data/lib/aldous/controller/action/result_execution_service.rb +27 -0
  148. data/lib/aldous/controller/action/wrapper.rb +34 -0
  149. data/lib/aldous/controller/action_execution_service.rb +42 -0
  150. data/lib/aldous/controller/preconditions_execution_service.rb +32 -0
  151. data/lib/aldous/controller.rb +21 -0
  152. data/lib/aldous/controller_action.rb +63 -0
  153. data/lib/aldous/dummy_error_reporter.rb +9 -0
  154. data/lib/aldous/dummy_logger.rb +8 -0
  155. data/lib/aldous/errors/user_error.rb +6 -0
  156. data/lib/aldous/logging_wrapper.rb +16 -0
  157. data/lib/aldous/params.rb +34 -0
  158. data/lib/aldous/respondable/base.rb +32 -0
  159. data/lib/aldous/respondable/headable.rb +30 -0
  160. data/lib/aldous/respondable/redirectable.rb +38 -0
  161. data/lib/aldous/respondable/renderable.rb +50 -0
  162. data/lib/aldous/respondable/request_http_basic_authentication.rb +23 -0
  163. data/lib/aldous/respondable/send_data.rb +36 -0
  164. data/lib/aldous/respondable/shared/flash.rb +24 -0
  165. data/lib/aldous/service/result/base/predicate_methods_for_inheritance.rb +44 -0
  166. data/lib/aldous/service/result/base.rb +13 -0
  167. data/lib/aldous/service/result/failure.rb +11 -0
  168. data/lib/aldous/service/result/success.rb +11 -0
  169. data/lib/aldous/service/wrapper.rb +48 -0
  170. data/lib/aldous/service.rb +34 -0
  171. data/lib/aldous/simple_dto.rb +47 -0
  172. data/lib/aldous/stdout_logger.rb +9 -0
  173. data/lib/aldous/version.rb +3 -0
  174. data/lib/aldous/view/blank/atom_view.rb +12 -0
  175. data/lib/aldous/view/blank/html_view.rb +16 -0
  176. data/lib/aldous/view/blank/json_view.rb +16 -0
  177. data/lib/aldous.rb +40 -0
  178. data/spec/aldous/build_respondable_service_spec.rb +48 -0
  179. data/spec/aldous/configuration_spec.rb +15 -0
  180. data/spec/aldous/controller/action/precondition/wrapper_spec.rb +48 -0
  181. data/spec/aldous/controller/action/precondition_spec.rb +81 -0
  182. data/spec/aldous/controller/action/result_execution_service_spec.rb +43 -0
  183. data/spec/aldous/controller/action/wrapper_spec.rb +46 -0
  184. data/spec/aldous/controller/action_execution_service_spec.rb +79 -0
  185. data/spec/aldous/controller/preconditions_execution_service_spec.rb +45 -0
  186. data/spec/aldous/controller_action_spec.rb +97 -0
  187. data/spec/aldous/controller_spec.rb +25 -0
  188. data/spec/aldous/dummy_error_reporter_spec.rb +10 -0
  189. data/spec/aldous/dummy_logger_spec.rb +7 -0
  190. data/spec/aldous/logging_wrapper_spec.rb +55 -0
  191. data/spec/aldous/params_spec.rb +39 -0
  192. data/spec/aldous/respondable/base_spec.rb +11 -0
  193. data/spec/aldous/respondable/headable/head_action_spec.rb +17 -0
  194. data/spec/aldous/respondable/headable_spec.rb +20 -0
  195. data/spec/aldous/respondable/redirectable/redirect_action_spec.rb +34 -0
  196. data/spec/aldous/respondable/redirectable_spec.rb +26 -0
  197. data/spec/aldous/respondable/renderable/render_action_spec.rb +34 -0
  198. data/spec/aldous/respondable/renderable_spec.rb +46 -0
  199. data/spec/aldous/respondable/request_http_basic_authentication_spec.rb +0 -0
  200. data/spec/aldous/respondable/send_data/send_data_action_spec.rb +15 -0
  201. data/spec/aldous/respondable/send_data_spec.rb +30 -0
  202. data/spec/aldous/respondable/shared/flash_spec.rb +30 -0
  203. data/spec/aldous/service/result/failure_spec.rb +11 -0
  204. data/spec/aldous/service/result/success_spec.rb +11 -0
  205. data/spec/aldous/service/wrapper_spec.rb +110 -0
  206. data/spec/aldous/service_spec.rb +101 -0
  207. data/spec/aldous/simple_dto_spec.rb +40 -0
  208. data/spec/aldous/view/blank/atom_view_spec.rb +15 -0
  209. data/spec/aldous/view/blank/html_view_spec.rb +15 -0
  210. data/spec/aldous/view/blank/json_view_spec.rb +15 -0
  211. data/spec/spec_helper.rb +26 -0
  212. metadata +330 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cbd9c9ba816e2ef37e0528487e6f203a2f30c821
4
+ data.tar.gz: 8b1ef09ccbd2e525f814255bcbe61425056edae2
5
+ SHA512:
6
+ metadata.gz: 5ceae3ac65b7eb859192b9311c7ad8502426f59c25d5a2dfdabf0d7029d992c1525c785b94f61cc100f4492b4ccc1dee217b5052ce653a40ae34d8f8fecb83ca
7
+ data.tar.gz: 759d8191ad6b4c4d0e663f5dd268040924481419f93dbaf1d40dae727fbb3f77f13fa39ad8fdab32376670c55738b56f1bed964ca3e7fcce3d225f83995b7958
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .idea
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ tags
data/.irbrc ADDED
@@ -0,0 +1,3 @@
1
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), 'lib'))
2
+
3
+ require './lib/aldous'
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format doc
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.1.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in aldous.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Alan Skorkin
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,591 @@
1
+ # Aldous
2
+
3
+ The basic idea behind this gem is that [Rails](https://github.com/rails/rails) is missing a few key things that make our lives especially painful when our applications start gettging bigger and more complex. With Aldous we attempt to address some of these shortcomings, namely:
4
+
5
+ 1) Bloated models that trample all over the single responsibility principle. God models that tend to appear despite our best intentions. Not to mention the complexity of testing objects that are so big an unwieldy.
6
+
7
+ Aldous attempts to address this, by introducing a light convention for creating service objects that conform to the same interface. See below, for the features that Aldous service objects have and how to use them in your app.
8
+
9
+ 2) Big and bloated controllers (despite our best intentions) that contain business logic and are hard to test. Not to mention the logic that is spread among the various `before_actions` requiring mental compilation to understand a controller action fully. Lastly the instance variables spread all over the controller that get inherited by the view templates as global variables.
10
+
11
+ Aldous addresses this by introducing the concept of a controller action object. The Rails controllers still exist by they contain no logic in them. All the logic moves to the action classes with one action per class. Instance variables from these don't automatically get inherited by the view templates and the job of `before_actions` is taken over by precondition objects. See below for a more detailed overview of Aldous controller actions.
12
+
13
+ 3) Views that are not really views, but are instead view templates that inherit instance variables from controllers as globals and leave no good place for view-specific logic making us resort to hacky solutions like view helpers.
14
+
15
+ Aldous addresses this by introducing a concept of view objects which are actual ruby objects, these live alongside the standard Rails views (which we try to call templates from now on). These view objects provide a good place for view specific logic, which lets us remove much of the logic that Rails templates tend to accumulate. And being Ruby objects this logic can be tested like it should. See below for more details about Aldous view objects.
16
+
17
+ The key concepts that motivate Aldous are:
18
+
19
+ - a greater number of light and small objects which are easy to understand and test
20
+ - looser coupling between all parts of a larger application
21
+
22
+ No matter how big our applications get we want to maintain the rapid speed of feature development we usually only have at the start of a project. The two ideas above greatly facilitate this. As a side-effect we also achieve looser coupling with the web framework and more clearly defined business logic.
23
+
24
+ ## Installation
25
+
26
+ Add this line to your application's Gemfile:
27
+
28
+ gem 'aldous'
29
+
30
+ And then execute:
31
+
32
+ $ bundle
33
+
34
+ Or install it yourself as:
35
+
36
+ $ gem install aldous
37
+
38
+ ## Blog Posts About Aldous
39
+
40
+ It is difficult to explain all the motivations and thoughts and philosophy that have gone into this gem without bloating the Readme tremendously. So this Readme will remain focused on usage. Any extra stuff, such as how it all hangs together and why things are done a certain way will be delegated to blog posts which will be linked from here.
41
+
42
+ ## Usage
43
+
44
+ All the concepts in Aldous are designed to work well when used together, but we also don't want to be too prescriptive, so you can easily use just some of the concepts without using others. You can take advantage of the service objects without worrying about controller action or view objects or if you prefer to just use the controller action you can do that too. Here is an in-depth explanation for how to hook everything up one concept at a time (with recommendations for how to achieve the best results).
45
+
46
+ ### Service Objects
47
+
48
+ The first thing to do is to configure a folder for the services to live in, `app/services` is a good candidate. So go into your `app/config/application.rb` and add the following:
49
+
50
+ ```ruby
51
+ config.autoload_paths += %W(
52
+ #{config.root}/app/services
53
+ )
54
+
55
+ config.eager_load_paths += %W(
56
+ #{config.root}/app/services
57
+ )
58
+ ```
59
+
60
+ The next thing to think about is how you will name your service objects. I prefer a `*_service` convention (`create_user_service.rb`), that way you can always tell you're looking at a service object regardless of where it is being used. Also remember it's a service object, so name it like you would a method instead of like you would a class (e.g. it's not `UserCreatorService`, it's `CreateUserService`).
61
+
62
+ A typical Aldous service object might look something like this:
63
+
64
+ ```ruby
65
+ class CreateUserService < Aldous::Service
66
+ attr_reader :user_data_hash
67
+
68
+ def initialize(user_data_hash)
69
+ @user_data_hash = user_data_hash
70
+ end
71
+
72
+ def raisable_error
73
+ MyApplication::Errors::UserError
74
+ end
75
+
76
+ def default_result_data
77
+ {user: nil}
78
+ end
79
+
80
+ def perform
81
+ user = User.new(user_data_hash)
82
+ user.roles << Role.where(name: "account_holder").first
83
+
84
+ if user.save
85
+ Result::Success.new(user: user)
86
+ else
87
+ Result::Failure.new
88
+ end
89
+ end
90
+ end
91
+ ```
92
+
93
+ A couple of things of note:
94
+
95
+ - it inherits from `Aldous::Service`, this is important as it gives it some nice behaviour.
96
+ - the `raisable_error`, `default_result_data` and `perform` methods are part of the public interface that is inherited from `Aldous::Service`
97
+
98
+ The only method is strictly *have* to override is `perform`, but I recommend you do all 3 for extra goodness that is explained below.
99
+
100
+ Here is how you would call it:
101
+
102
+ ```ruby
103
+ hash = {}
104
+ result = CreateUserService.perform(hash)
105
+ if result.success?
106
+ # do success stuff
107
+ else #result.failure?
108
+ # do failure stuff
109
+ end
110
+ ```
111
+
112
+ or
113
+
114
+ ```ruby
115
+ hash = {}
116
+ result = CreateUserService.perform!(hash)
117
+ begin
118
+ if result.success?
119
+ # do success stuff
120
+ else #result.failure?
121
+ # do failure stuff
122
+ end
123
+ rescue MyApplication::Errors::UserError => e
124
+ end
125
+ ```
126
+
127
+ or
128
+
129
+ ```ruby
130
+ hash = {}
131
+ service = CreateUserService.build(hash)
132
+ result = service.perform
133
+ if result.success?
134
+ # do success stuff
135
+ else #result.failure?
136
+ # do failure stuff
137
+ end
138
+ ```
139
+
140
+ As you can see, you don't use the constructor to build these objects, use `build` or call `perform` directly (otherwise you'll be missing all the nice features).
141
+
142
+ An Aldous service object can be either error free or error raising, here is what that means. If you construct the service object correctly via the factory method and/or call `perform`. The method call is guaranteed to never raise an error. When implementing the `perform` method you have to remember to return `Result::Success` for any success cases and return `Result::Failure` for any failure cases. If the code you have writted raises an error, it will be automatically caught and a `Result::Failure` will be returned. The idea is that a service either succeeds or fails and if an error occurs that's just a kind of failure.
143
+
144
+ However there are situations where you *want* the service to raise an error, for example if you're using it in a background job and an error means that the job will be retried (e.g. like `sidekiq` would do). For this purpose Aldous service objects automatically gain a `perform!` method. This will use the funtionality of your `perform` method, but if an error occurs it will be caught and then re-raised as the error type you specify in the `raisable_error` method. This way you always know that your services can return `Result::Success`, `Result::Failure` or raise the error type you specify depending on how you call them.
145
+
146
+ Service objects are immutable once they are constructed their state doens't change, but we may still want to return data to the user of the service. This is why `Result::Success` and `Result::Failure` are not just are not just semantic types they are also DTO (data transfer) objects. When you construct a `Result::Success` for example, it will automatically have reader methods defined on it with the names of the keys in `default_result_data` hash. An example to demonstrate:
147
+
148
+ ```ruby
149
+ class CreateUserService < Aldous::Service
150
+ def default_result_data
151
+ {user: nil}
152
+ end
153
+
154
+ def perform
155
+ Result::Success.new
156
+ end
157
+ end
158
+
159
+ result = CreateUserService.perform
160
+ result.user # will be equal to nil
161
+ ```
162
+
163
+ or
164
+
165
+ ```ruby
166
+ class CreateUserService < Aldous::Service
167
+ def default_result_data
168
+ {user: nil}
169
+ end
170
+
171
+ def perform
172
+ Result::Success.new(user: user)
173
+ end
174
+
175
+ private
176
+
177
+ def user
178
+ User.new
179
+ end
180
+ end
181
+
182
+ result = CreateUserService.perform
183
+ result.user # will be a User instance
184
+ ```
185
+
186
+ Hopefully you can see how this hangs together, the `default_result_data` method defines a hash of data with default values. This data will magically end up on the `Result::Success` and `Result::Failure` objects you construct in your perform method. If you pass a hash of data when you construct those object, that data will override the default values defined in the `default_result_data` method.
187
+
188
+ ### Controller Actions
189
+
190
+ The first thing to do is to configure a folder for the controller actions to live in, `app/controller_actions` is a good candidate. So go into your `app/config/application.rb` and add the following:
191
+
192
+ ```ruby
193
+ config.autoload_paths += %W(
194
+ #{config.root}/app/controller_action
195
+ )
196
+
197
+ config.eager_load_paths += %W(
198
+ #{config.root}/app/controller_action
199
+ )
200
+ ```
201
+
202
+ Let's say we're creating a controller called `TodosController`, here is what it would look like now:
203
+
204
+ ```ruby
205
+ class TodosController < ApplicationController
206
+ include Aldous::Controller
207
+
208
+ controller_actions :index, :new, :create, :edit, :update, :destroy
209
+ end
210
+ ```
211
+
212
+ After you've done this, the code for the `index` action should live in, `app/controller_actions/todos_controller/index.rb` - pretty intuitive.
213
+
214
+ A controller action might look like this:
215
+
216
+ ```ruby
217
+ class TodosController::Index < BaseAction
218
+ def default_view_data
219
+ super.merge({todos: todos})
220
+ end
221
+
222
+ def perform
223
+ return build_view(Home::ShowRedirect) unless current_user
224
+
225
+ build_view(Todos::IndexView)
226
+ end
227
+
228
+ private
229
+
230
+ def todos
231
+ Todo.where(user_id: current_user.id)
232
+ end
233
+ end
234
+ ```
235
+
236
+ The action above is also using Aldous view objects.
237
+
238
+ As you can see, the controller action lives under the namespace of the controller. I also recommend creating a `BaseAction` and inheriting from that for most of your actions. The `BaseAction` might look like this:
239
+
240
+ ```ruby
241
+ class BaseAction < ::Aldous::ControllerAction
242
+ def default_view_data
243
+ {
244
+ current_user: current_user,
245
+ current_ability: current_ability,
246
+ }
247
+ end
248
+
249
+ def preconditions
250
+ [Shared::EnsureUserNotDisabledPrecondition]
251
+ end
252
+
253
+ def default_error_respondable
254
+ Defaults::ServerErrorView
255
+ end
256
+
257
+ def current_user
258
+ @current_user ||= FindCurrentUserService.perform(session).user
259
+ end
260
+
261
+ def current_ability
262
+ @current_ability ||= Ability.new(current_user)
263
+ end
264
+ end
265
+ ```
266
+
267
+ As you can see if inherits from `Aldous::ControllerAction`. The methods you should override are:
268
+
269
+ - `default_view_data` - this hash of data will be available to all the aldous view objects
270
+ - `preconditions` - this is used as a replacement for `before_actions`
271
+ - `default_error_respondable` - this view will be rendered if an unhandled error gets raised in the action code
272
+
273
+ Similar to services, for actions you implement the `perform` method (might as well keep things consistent). All action classes have the same constructor signature, they take a controller object and you have access to this controller object in your action classes. Also a few methods from the controller get exposed directly to your action (params, session, cookies, request, response) for convenience. You can expose others via configuration in an initializer, or you can grab them from the controller instance you have access to.
274
+
275
+ Controller action are automatically error free, so any error that gets raised in the perform method, automatically get caught and a view you specify in `default_error_respondable` will get rendered to handle that error.
276
+
277
+ Lets look at a slightly bigger controller action:
278
+
279
+ ```ruby
280
+ class TodosController::Update < BaseAction
281
+ def default_view_data
282
+ super.merge({todo: todo})
283
+ end
284
+
285
+ def preconditions
286
+ super.reject{|klass| klass == Shared::EnsureUserNotDisabledPrecondition}
287
+ end
288
+
289
+ def perform
290
+ return build_view(Home::ShowRedirect) unless current_user
291
+ return build_view(Defaults::BadRequestView, errors: [todo_params.error_message]) unless todo_params.fetch
292
+ return build_view(Todos::NotFoundView, todo_id: params[:id]) unless todo
293
+ return build_view(Defaults::ForbiddenView) unless current_ability.can?(:update, todo)
294
+
295
+ if todo.update_attributes(todo_params.fetch)
296
+ build_view(Todos::IndexRedirect)
297
+ else
298
+ build_view(Todos::EditView)
299
+ end
300
+ end
301
+
302
+ private
303
+
304
+ def todo
305
+ @todo ||= Todo.where(id: params[:id]).first
306
+ end
307
+
308
+ def todo_params
309
+ TodosController::TodoParams.build(params)
310
+ end
311
+ end
312
+ ```
313
+
314
+ Let's start with the `perform` method. As you can see it's fattish, but no fatter than regular controller methods if you take into account all the `before_actions`. The idea is to handle the special cases first and then perform the logic which will either succeed or fail. In our case we want to redirect to home if there is no `current_user`. The actual `current_user` method, comes from our `BaseAction`. If we can't fetch strong params we render a bad request view. If we couldn't find a `todo` we render a not found view. And if the current user isn't allowed to update the todo, we render a forbidden view. All of those are things we should think about and handle before we try to perform the logic of our update action. And all those things should be close to the logic of the action, so we can grok the full scope of the action easily without mental compilation. We then perform the logic and render or redirect based on the success or failure of that logic. As you might imagine, if the logic was more complex you would push it into a service object e.g.:
315
+
316
+ ```ruby
317
+ class SignUpsController::Create < BaseAction
318
+ def perform
319
+ return build_view(Todos::IndexRedirect) if current_user
320
+ return build_view(Defaults::BadRequestView, errors: [user_params.error_message]) unless user_params.fetch
321
+
322
+ if create_user_result.success?
323
+ SignInService.perform!(session, create_user_result.user)
324
+ build_view(Todos::IndexRedirect)
325
+ else
326
+ build_view(SignUps::NewView)
327
+ end
328
+ end
329
+
330
+ private
331
+
332
+ def create_user_result
333
+ @create_user_result ||= CreateUserService.perform(user_params.fetch)
334
+ end
335
+
336
+ def user_params
337
+ @user_params ||= ::SignUpsController::UserParams.build(params)
338
+ end
339
+ end
340
+ ```
341
+
342
+ Let's talk about `default_view_data`. We first define this method in our `BaseAction` we them augment it in our actual actions. Eventually that whole hash of data will get bundles up into a DTO object and be available in whichever view object we end up rendering. You always know which data your view object will have access to and you directly control it with pure ruby and very limited magic.
343
+
344
+ Let's talk about the `preconditions` method. We usually want to define it in our `BaseAction` and only override it in our actual actions if we want to get rid of a particular precondition, which should happen infrequently. This method defines an array of classes, this classes will get instantiated and executed in the order they are defined before the actual action is executed. In most respects a precondition class looks and behaves exactly like an action class. The precondition also has access to the action instance:
345
+
346
+ ```ruby
347
+ class Shared::EnsureUserNotDisabledPrecondition < BasePrecondition
348
+ delegate :current_user, :current_ability, to: :action
349
+
350
+ def perform
351
+ if current_user && current_user.disabled && !current_ability.can?(:manage, :all)
352
+ return build_view(Defaults::ForbiddenView, errors: ['Your account has been disabled'])
353
+ end
354
+ end
355
+ end
356
+ ```
357
+
358
+ I recommend having a base class for preconditions:
359
+
360
+ ```ruby
361
+ class BasePrecondition < ::Aldous::Controller::Action::Precondition
362
+ end
363
+ ```
364
+
365
+ Preconditions should be applicable to all or most actions, and if you need to switch a precondition off for a particular action, you can do so using ruby collection methods (e.g. `reject`).
366
+
367
+ Aldous provides a small helper for bundling strong params logic into an object:
368
+
369
+ ```ruby
370
+ class SignUpsController::UserParams < Aldous::Params
371
+ def permitted_params
372
+ params.require(:user).permit(:email, :password)
373
+ end
374
+
375
+ def error_message
376
+ 'Missing param :user'
377
+ end
378
+ end
379
+
380
+ ::SignUpsController::UserParams.build(params)
381
+ ```
382
+
383
+ When you have an instance of an `Aldous::Params` object, you just call `fetch` on it and it will either return the params hash you were after, or nil if something went wrong. You can put params object along side your actions, or if they are applicable across controllers, put it into `app/controller_actions/shared` just like you would with precondition objects.
384
+
385
+ ### View Objects
386
+
387
+ The first thing to do is to configure a folder for the view objects to live in, we already have `app/views` for our view templates, so lets just put our view objects right there next to the templates. So go into your `app/config/application.rb` and add the following:
388
+
389
+ ```ruby
390
+ config.autoload_paths += %W(
391
+ #{config.root}/app/views
392
+ )
393
+
394
+ config.eager_load_paths += %W(
395
+ #{config.root}/app/views
396
+ )
397
+ ```
398
+
399
+ We have to understand that when we're talking about view objects, we're not necessarily talking about views as Rails would understand them. We're talking about any kind of output you would normally produce from a Rails controller (e.g. render view, redirect to location, send data, head etc.). In Aldous this concept is called - respondables and we have a different type of object to represent each one of these. There are `Renderable` object for standard view, `Redirectable` objects for redirects etc. The key thing is that a controller action always produces a respondable object and it's the responsibility of that object to do the right thing to produce the output that we expect. Example time, here is what a redirect object might look like:
400
+
401
+ ```ruby
402
+ class Home::ShowRedirect < Aldous::Respondable::Redirectable
403
+ def location
404
+ view_context.root_path
405
+ end
406
+ end
407
+ ```
408
+
409
+ As you can see it inherits from `Aldous::Respondable::Redirectable` and all you have to do is to provide the location method. Here is what a renderable object (for views) might look like:
410
+
411
+ ```ruby
412
+ class Home::ShowView < BaseView
413
+ def template_data
414
+ {
415
+ template: 'home/show',
416
+ locals: {}
417
+ }
418
+ end
419
+ end
420
+ ```
421
+
422
+ with `BaseView` being:
423
+
424
+ ```ruby
425
+ class BaseView < ::Aldous::Respondable::Renderable
426
+ def default_template_locals
427
+ {
428
+ current_user: current_user,
429
+ header_view: header_view,
430
+ }
431
+ end
432
+
433
+ def current_user
434
+ view_data.current_user
435
+ end
436
+
437
+ private
438
+
439
+ def header_view
440
+ build_view(Modules::HeaderView)
441
+ end
442
+ end
443
+ ```
444
+
445
+ As you can see a view object ultimately inherits from `Aldous::Respondable::Renderable` and the key method to override is `template_data`. This method needs to return a hash with template or partial that we want to render as well as the locals that we want to supply. If we have a `BaseView` which I recommend, then we can also override the `default_template_locals` method. When you do this, some Aldous magic will happen and all the things in that hash will be available in all the templates as locals which is very handy for data that's common across all or most templates. The key things here is that you control it.
446
+
447
+ You construct view objects either in your controller actions or in other view objects, to do this you use the `build_view` method which is available in both of those places. The only place where you may have to construct view objects directly is in tests. The constructor signature of a view object is:
448
+
449
+ ```ruby
450
+ def initialize(status, view_data, view_context)
451
+ @status = status
452
+ @view_data = view_data
453
+ @view_context = view_context
454
+ end
455
+ ```
456
+
457
+ The first variable is self explanatory, use it if you want to override the status for a particular view object. The second parameter is a DTO which contains all the data that comes from the `default_view_data` in your controller action. The last parameter is the `view_context` that you normally get from the controller. This is the context in which the view templates get executed in vanilla Rails.
458
+
459
+ You should only override the `default_template_locals` in a `BaseView` object. In the actual view object just provide the locals directly in the `template_data` method. Everything will get merged correctly by Aldous. You can also provide a default status for any view by overriding the `default_status` method.
460
+
461
+ Lets look at a more complex view object and the correspoding templates:
462
+
463
+ ```ruby
464
+ class Todos::IndexView < BaseView
465
+ def template_data
466
+ {
467
+ template: 'todos/index',
468
+ locals: {
469
+ todo_views: todo_views,
470
+ }
471
+ }
472
+ end
473
+
474
+ private
475
+
476
+ def todos
477
+ view_data.todos
478
+ end
479
+
480
+ def todo_views
481
+ todos.map do |todo|
482
+ todo_view(todo)
483
+ end
484
+ end
485
+
486
+ def todo_view(todo)
487
+ build_view(Todos::IndexView::TodoView, todo: todo)
488
+ end
489
+ end
490
+ ```
491
+
492
+ our `BaseView` is:
493
+
494
+ ```ruby
495
+ class BaseView < ::Aldous::Respondable::Renderable
496
+ def default_template_locals
497
+ {
498
+ current_user: current_user,
499
+ header_view: header_view,
500
+ }
501
+ end
502
+
503
+ def current_user
504
+ view_data.current_user
505
+ end
506
+
507
+ private
508
+
509
+ def header_view
510
+ build_view(Modules::HeaderView)
511
+ end
512
+ end
513
+ ```
514
+
515
+ so our template `index.html.slim` has access to `current_user`, `header_view`, and `todo_views` as locals. Let's have a look at the template:
516
+
517
+ ```ruby
518
+ - provide :title, "View Todos"
519
+
520
+ = render header_view.template
521
+
522
+ h1 Your Todos
523
+
524
+ = link_to 'Create New Todo', new_todo_path
525
+ = button_to 'Delete Completed Todos', all_completed_todos_path, method: :delete
526
+
527
+ - todo_views.each do |todo_view|
528
+ = render todo_view.template
529
+ ```
530
+
531
+ You can see how we render partials inside a template:
532
+
533
+ ```ruby
534
+ = render header_view.template
535
+ ```
536
+
537
+ If we needed to pass in extra locals for the partial (e.g. a form object), we could do:
538
+
539
+ ```ruby
540
+ = render header_view.template(f: f)
541
+ ```
542
+
543
+ In our case `header_view` will be available to all templates, which includes the layout, so we can pull it out of this template and add it to the layout instead.
544
+
545
+ The idea here is for templates to have very little logic besides boilerplate stuff like the `- todo_views.each` above. There should also be very few (or none at all) demeter violations in the template, as this stuff can be pushed up into the view objects now and handled more robustly, tested etc.
546
+
547
+ ### Configuring Aldous
548
+
549
+ You can create an initializer for Aldous it would look like this:
550
+
551
+ ```ruby
552
+ Aldous.configuration do |aldous|
553
+ aldous.logger = Rails.logger
554
+ aldous.error_reporter = MyApplication::ErrorReporter
555
+ aldous.controller_methods_exposed_to_action += [:current_user]
556
+ end
557
+ ```
558
+
559
+ The most interesting config option initially is the ability to supply the logger so that you can see relevant things in your log files and also pick up on any issues with Aldous itself. The `error_reporter` is an object that responds to `report`:
560
+
561
+ ```ruby
562
+ module Aldous
563
+ class DummyErrorReporter
564
+ class << self
565
+ def report(e, data = {})
566
+ nil
567
+ end
568
+ end
569
+ end
570
+ end
571
+ ```
572
+
573
+ This can be used for things like sending errors to an error collection service or whatever else you want to do when exceptions occur.
574
+
575
+ You can also expose more methods from the Rails controller to the controller action by using `controller_methods_exposed_to_action`.
576
+
577
+ ## Why is it called Aldous?
578
+
579
+ When we initially started using some of the things that would later become Aldous, we called it Brave New World (BNW). So when a lot of that functionality migrated into a gem we called it [Aldous](http://en.wikipedia.org/wiki/Aldous_Huxley) cause [Brave New World](http://en.wikipedia.org/wiki/Brave_New_World).
580
+
581
+ ## Examples
582
+
583
+ There is a basic [example Rails app](examples/basic_todo) that lives in this repo. Run it up, and have a look at the code. Also one of the goals of Aldous was to make its own source code easy to understand read and modify for other people, to facilitate understading and cooperation. So, feel free to read the code itself to figure out how things hang together, start with the Aldous objects that you would inherit from e.g. `Aldous::ControllerAction`, `Aldous::Service`.
584
+
585
+ ## Contributing
586
+
587
+ 1. Fork it
588
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
589
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
590
+ 4. Push to the branch (`git push origin my-new-feature`)
591
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/aldous.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'aldous/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "aldous"
8
+ spec.version = Aldous::VERSION
9
+ spec.authors = ["Alan Skorkin"]
10
+ spec.email = ["alan@skorks.com"]
11
+ spec.description = %q{Rails brave new world}
12
+ spec.summary = %q{Rails brave new world}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
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_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency 'rspec'
24
+ end
@@ -0,0 +1 @@
1
+ port: 9090