chassis 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +362 -0
  6. data/Rakefile +33 -0
  7. data/chassis.gemspec +41 -0
  8. data/examples/repo.rb +40 -0
  9. data/lib/chassis.rb +81 -0
  10. data/lib/chassis/array_utils.rb +8 -0
  11. data/lib/chassis/circuit_panel.rb +22 -0
  12. data/lib/chassis/core_ext/array.rb +5 -0
  13. data/lib/chassis/core_ext/hash.rb +5 -0
  14. data/lib/chassis/core_ext/string.rb +13 -0
  15. data/lib/chassis/delegate.rb +29 -0
  16. data/lib/chassis/dirty_session.rb +105 -0
  17. data/lib/chassis/error.rb +7 -0
  18. data/lib/chassis/faraday.rb +226 -0
  19. data/lib/chassis/form.rb +56 -0
  20. data/lib/chassis/hash_utils.rb +16 -0
  21. data/lib/chassis/heroku.rb +5 -0
  22. data/lib/chassis/initializable.rb +11 -0
  23. data/lib/chassis/logger.rb +8 -0
  24. data/lib/chassis/observable.rb +19 -0
  25. data/lib/chassis/persistence.rb +49 -0
  26. data/lib/chassis/rack/bouncer.rb +33 -0
  27. data/lib/chassis/rack/builder_shim_patch.rb +7 -0
  28. data/lib/chassis/rack/health_check.rb +45 -0
  29. data/lib/chassis/rack/instrumentation.rb +20 -0
  30. data/lib/chassis/rack/json_body_parser.rb +20 -0
  31. data/lib/chassis/rack/no_robots.rb +24 -0
  32. data/lib/chassis/registry.rb +30 -0
  33. data/lib/chassis/repo.rb +73 -0
  34. data/lib/chassis/repo/base_repo.rb +99 -0
  35. data/lib/chassis/repo/delegation.rb +78 -0
  36. data/lib/chassis/repo/lazy_association.rb +57 -0
  37. data/lib/chassis/repo/memory_repo.rb +7 -0
  38. data/lib/chassis/repo/null_repo.rb +64 -0
  39. data/lib/chassis/repo/pstore_repo.rb +54 -0
  40. data/lib/chassis/repo/record_map.rb +44 -0
  41. data/lib/chassis/repo/redis_repo.rb +55 -0
  42. data/lib/chassis/serializable.rb +52 -0
  43. data/lib/chassis/string_utils.rb +50 -0
  44. data/lib/chassis/version.rb +3 -0
  45. data/lib/chassis/web_service.rb +61 -0
  46. data/test/array_utils_test.rb +23 -0
  47. data/test/chassis_test.rb +7 -0
  48. data/test/circuit_panel_test.rb +22 -0
  49. data/test/core_ext/array_test.rb +8 -0
  50. data/test/core_ext/hash_test.rb +8 -0
  51. data/test/core_ext/string_test.rb +16 -0
  52. data/test/delegate_test.rb +41 -0
  53. data/test/dirty_session_test.rb +138 -0
  54. data/test/error_test.rb +12 -0
  55. data/test/faraday_test.rb +749 -0
  56. data/test/form_test.rb +29 -0
  57. data/test/hash_utils_test.rb +17 -0
  58. data/test/initializable_test.rb +22 -0
  59. data/test/logger_test.rb +43 -0
  60. data/test/observable_test.rb +27 -0
  61. data/test/persistence_test.rb +112 -0
  62. data/test/prox_test.rb +7 -0
  63. data/test/rack/bouncer_test.rb +42 -0
  64. data/test/rack/builder_patch_test.rb +36 -0
  65. data/test/rack/health_check_test.rb +35 -0
  66. data/test/rack/instrumentation_test.rb +38 -0
  67. data/test/rack/json_body_parser_test.rb +38 -0
  68. data/test/rack/no_robots_test.rb +34 -0
  69. data/test/registry_test.rb +26 -0
  70. data/test/repo/delegation_test.rb +101 -0
  71. data/test/repo/lazy_association_test.rb +115 -0
  72. data/test/repo/memory_repo_test.rb +25 -0
  73. data/test/repo/null_repo_test.rb +48 -0
  74. data/test/repo/pstore_repo_test.rb +28 -0
  75. data/test/repo/redis_repo_test.rb +26 -0
  76. data/test/repo/repo_tests.rb +120 -0
  77. data/test/repo_test.rb +76 -0
  78. data/test/serializable_test.rb +77 -0
  79. data/test/string_utils_test.rb +21 -0
  80. data/test/test_helper.rb +10 -0
  81. data/test/web_service_test.rb +107 -0
  82. metadata +426 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 16a3807f8f99f5ef57d7c1b994468d6f2235a7c4
4
+ data.tar.gz: 703e7280297e814881ef6761476809bea61445c7
5
+ SHA512:
6
+ metadata.gz: 868c6a7634af6ffd555f05e575b80794bd3782fe52b1cb30b35c774eec5366404ca359ac05f60b027d1870206ae9820e2c21c9415903412a073d0d732d778445
7
+ data.tar.gz: 080c73bc4cc4e721a17ccee6b02447a9c34f60fd7eac80b5c407512ff807d0dd205df49bf9fff647a583a54df174e4a5fdfa798005841db897fd04b651746dc4
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in chassis.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 ahawkins
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,362 @@
1
+ # Chassis
2
+
3
+ Chassis is a collection of new classes and enhancements to existing
4
+ projects for building maintainable applications. I choose the name
5
+ "chassis" because I'm a car guy. A chassis is a car's foundation.
6
+ Every car has key components: there is an engine, transmission,
7
+ differential, suspension, electrical system, and a bunch of other
8
+ things. They fit together on the chassis in a certain way, there are
9
+ guidelines but no one is going to stop you from building a custom
10
+ front suspension on a typical chassis. And that's the point. The
11
+ chassis is there to build on. It does not make decisions for you.
12
+ There are also kit cars and longblock engines. Kit cars come with some
13
+ components and rely on you to assemble them. Longblocks are halfway
14
+ complete engines. The engine block and valve train are predecided. You
15
+ must decide which fuel delivery and exhaust system to use. Then you
16
+ mount it in the chassis. In all things there is a line between
17
+ prepackaged DIY and turn-key solutions. This project is a combination
18
+ of a chassis and long block. Some things have been predecided and
19
+ others are left to you. In that sense this project is a utility belt.
20
+ All the components are there, you just need to figure out how to put
21
+ them together.
22
+
23
+ This project chooses an ideal gem stack for building web applications
24
+ and enhancements to existing projects. It's just a enough structure to
25
+ build an application. It is the chassis you build your application on.
26
+
27
+ Here's an [example](https://github.com/ahawkins/chassis-example) I put together.
28
+
29
+ ## Installation
30
+
31
+ Add this line to your application's Gemfile:
32
+
33
+ gem 'chassis'
34
+
35
+ And then execute:
36
+
37
+ $ bundle
38
+
39
+ Or install it yourself as:
40
+
41
+ $ gem install chassis
42
+
43
+ ## Rack & Sinatra
44
+
45
+ Right off the bat, chassis is for building web applications. It
46
+ depends on other gems to make that happen. Chassis fully endorses rack
47
+ & Sinatra as the best way to do this. So it contains enhancements and
48
+ middleware to make that so.
49
+
50
+ * `Chassis::Rack::Bouncer` - takes a block. Used to bounce spam or
51
+ other undesirable requests.
52
+ * `Chassis::Rack::HealthCheck` - for load balanced applications. Takes
53
+ a block to test if the applications is ready. Failures terminate the
54
+ process.
55
+ * `Chassis::Rack::Instrumentation` - use harness to instrument all
56
+ request timings
57
+ * `Chassis::Rack::NoRobots` - blocks all crawlers and bots.
58
+
59
+ `Chassis::WebService` includes some of these middleware as well as
60
+ other customizations.
61
+
62
+ * requires `sinatra/json` for JSON response generation
63
+ * requires `rack/contrib/bounce_favicton` because ain't no body got
64
+ time for that
65
+ * uses `Chassis::Rack::Bouncer`
66
+ * uses `Chassis::Rack::NoRobots`
67
+ * uses `Rack::Deflator` to gzip everything
68
+ * uses `Rack::PostBodyContentTypeParser` to parse incoming JSON bodies
69
+ * `enable :cors` to enable CORS with manifold.
70
+ * registers error handlers for unknown exceptions coming from other
71
+ chassis components.
72
+ * other misc helpers for generating JSON and handling errors.
73
+
74
+ ## Data Access
75
+
76
+ Chassis includes a
77
+ [repository](http://martinfowler.com/eaaCatalog/repository.html) using
78
+ the query pattern as well. The repository pattern is perfect because
79
+ it does not require knowledge about your persistence layer. It is the
80
+ access layer. A null, in-memory, and Redis adapter are included. You
81
+ can subclass these adapters to make your own.
82
+ `Chassis::Repo::Delegation` can be included in other classes to
83
+ delegate to the repository.
84
+
85
+ Here's an example:
86
+
87
+ ```ruby
88
+ class CustomerRepo
89
+ extend Chassis::Repo::Delegation
90
+ end
91
+ ```
92
+
93
+ Now there are CRUD methods available on `CustomerRepo` that delegate
94
+ to the repository for `Customer` objects. `Chassis::Persistence` can
95
+ be included in any object. It will make the object compatible with
96
+ the matching repo.
97
+
98
+ ```ruby
99
+ class Customer
100
+ include Chassis::Persistence
101
+ end
102
+ ```
103
+
104
+ Now `Customer` responds to `id`, `save`, and `repo`. `repo` looks for
105
+ a repository class matching the class name (e.g. `CustomerRepo`).
106
+ Override as you see if.
107
+
108
+ More on my blog
109
+ [here](http://hawkins.io/2014/01/pesistence_with_repository_and_query_patterns/).
110
+
111
+ ## Chassis::Form
112
+
113
+ `Virtus` and `virtus-dirty_attribute` are used to create
114
+ `Chassis::Form`. It includes a few minor enhancements. All assignments
115
+ go through dirty tracking to support the partial update use case.
116
+ `Chassis::Form#values` will return a hash of everything that's been
117
+ assigned. `Chassi::Form#attributes` returns a hash for all the
118
+ declared attributes. `initialize` has been modified as well. Trying to
119
+ set an unknown attributes will raise
120
+ `Chassis::Form::UnknownFieldError` instead of `NoMethodError`.
121
+ `Chassis::WebService` registers an error handler and returns a `400
122
+ Bad Request` in this case.
123
+
124
+ Create a new form by including `Chassis.form`
125
+
126
+ ```ruby
127
+ class SignupForm
128
+ include Chassis.form
129
+ end
130
+ ```
131
+
132
+ ## Outgoing HTTP with Faraday
133
+
134
+ Chassis uses Faraday because it's the best god damn HTTP client in
135
+ ruby. Chassis includes a bunch of middleware to make it even better.
136
+
137
+ ```ruby
138
+ Farday.new 'http://foo.com', do |builder|
139
+ # Every request is timed with Harness into a namespaced key.
140
+ # You can pass a namespace as the second argument: IE "twilio",
141
+ # or "sendgrid"
142
+ faraday.request :instrumentation
143
+
144
+ # Send requests with `content-type: application/json` and use
145
+ # the standard library JSON to encode the body
146
+ faraday.request :encode_json
147
+
148
+ # Parse a JSON response into a hash
149
+ faraday.request :parse_json
150
+
151
+ # This is the most important one IMO. All requests 4xx and 5xx
152
+ # requests will raise a useful error with the response body
153
+ # and status code. This is much more useful than the bundled
154
+ # implementation. A 403 response will raise a HttpForbiddenError.
155
+ # This middleware also captures timeouts.
156
+ # Useful for catching failure conditions.
157
+ faraday.request :server_error_handler
158
+
159
+ # Log all requests and responses. Useful when debugging running
160
+ # applications
161
+ faraday.response :logging
162
+ end
163
+ ```
164
+
165
+ There is also a faraday factory that will build new connections using
166
+ this middleware stack.
167
+
168
+ ```ruby
169
+ # Just like normal, but the aforementioned middleware included.
170
+ # Any middleware you insert will come after the chassis ones.
171
+
172
+ Chassis.faraday 'http://foo.com' do |builder|
173
+ # your stuff here
174
+ end
175
+ ```
176
+
177
+ ## Circuit Breakers with Breaker
178
+
179
+ [Breaker](https://github.com/ahawkins/breaker) provides the low level
180
+ implementation. `Chassis::CircuitPanel` is a class for unifying
181
+ access to all the different circuits in the application. This is
182
+ useful because other parts of the code don't need to know about how
183
+ the circuit is implemented. `Chassis.circuit_panel` behaves like
184
+ `Struct.new`. It returns a new class.
185
+
186
+ ```ruby
187
+ CircuitPanel = Chassis.circuit_panel do
188
+ circuit :test, timeout: 10, retry_threshold: 6
189
+ end
190
+
191
+ panel = CircuitPanel.new
192
+
193
+ circuit = panel.test
194
+ circuit.class # => Breaker::Circuit
195
+
196
+ circuit.run do
197
+ # do your stuff here
198
+ end
199
+ ```
200
+
201
+ Since `Chassis.circuit_panel` returns a class, you can do anything you
202
+ want. Don't like to have to instantiate a new instance every time? Use
203
+ a singleton and assign that to a constant.
204
+
205
+ ```ruby
206
+ require 'singleton'
207
+
208
+ CircuitPanel = Chassis.circuit_panel do
209
+ include Singleton
210
+
211
+ circuit :test, timeout: 10, retry_threshold: 6
212
+ end.instance
213
+
214
+ CircuitPanel.test.run do
215
+ # your stuff here
216
+ end
217
+ ```
218
+
219
+ ## Chassis::Strategy
220
+
221
+ `Chassis::Strategy` is a way to define boundary objects. The class
222
+ defines the all required methods, then delegates the work to an
223
+ implementation. Implementations are be registered and used. A null
224
+ object implementation is automatically generated and set as the
225
+ default implementation. Here are some examples.
226
+
227
+ ```ruby
228
+ class Mailer
229
+ include Chassis.strategy(:deliver, :deliveries)
230
+ end
231
+
232
+ class SMTPDelivery
233
+ def deliver(mail)
234
+ # send w/SMTP
235
+ end
236
+
237
+ def deliveries
238
+ # check the email account
239
+ end
240
+ end
241
+
242
+ class SnailMail
243
+ def deliver(mail)
244
+ # print the mail and go to the post office
245
+ end
246
+
247
+ def deliveries
248
+ # go outside and check the mailbox
249
+ end
250
+ end
251
+
252
+ mailer = Mailer.new
253
+ mailer.register :smtp, SMTPDelivery.new
254
+ mailer.register :snail_mail, SnailMail.new
255
+
256
+ mail.use :smtp
257
+ mail.deliver some_message
258
+
259
+ mail.use :null # switch back to the null implementation.
260
+ ```
261
+
262
+ These objects are very useful when you have an interaction that needs
263
+ to happen but implementations can vary widely. You can also use this
264
+ as class if you don't like the instance flavor.
265
+
266
+ ```ruby
267
+ class Mailer
268
+ extend Chassis.strategy(:foo, :bar, :bar)
269
+ end
270
+
271
+ Mailer.register, :smtp, SomeSmtpClass
272
+ ```
273
+
274
+ Since `Chassis.strategy` returns a new module, you can call define
275
+ methods and call `super` just like normal.
276
+
277
+ ```ruby
278
+ class Mailer
279
+ include Chassis.strategy(:deliver)
280
+
281
+ def deliver(mail)
282
+ raise "No address" unless mail.to
283
+ super
284
+ end
285
+ end
286
+ ```
287
+
288
+ This is great when you have some shared logic at the boundary but not
289
+ across implementations.
290
+
291
+ ## Chassis::DirtySession
292
+
293
+ A proxy object used to track assignments. Wrap an object in a dirty
294
+ session to see what changed and what it changed to.
295
+
296
+ ```ruby
297
+ Person = Struct.new :name
298
+
299
+ adam = Person.new 'adam'
300
+
301
+ session = Chassis::DirtySession.new adam
302
+ session.clean? # => true
303
+ session.dirty? # => false
304
+
305
+ session.name = 'Adman'
306
+
307
+ session.dirty? # => true
308
+ session.clean? # => false
309
+
310
+ session.named_changed? # => true
311
+ session.changed # => set of values changed
312
+ session.new_values # => { name: 'Adman' }
313
+ session.original_values # => { name: 'adam' }
314
+
315
+ session.reset! # reset everything back to normal
316
+ ```
317
+
318
+ ## Chassis::Logger
319
+
320
+ Chassis includes the `logger-better` gem to refine the standard
321
+ library logger. `Chassis::Logger` default the `logdev` argument to
322
+ `Chassis.stream`. This gives a unified place to assign all output.
323
+ The log level can also be controlled by the `LOG_LEVEL` environment
324
+ variable. This makes it possible to restart/boot the application with
325
+ a new log level without redeploying code.
326
+
327
+ ## Chassis::Observable
328
+
329
+ A very simple implementation of the observer pattern. It is different
330
+ from the standard library implementation for two reasons:
331
+
332
+ * you don't need to call `changed` for `notify_observers` to work.
333
+ * `notify_obsevers` includes `self` as first argument to all observers
334
+ * there is only the `add_observer` method.
335
+
336
+ ## Chassis::Initializable
337
+
338
+ Encapsulate the common pattern of passing a hash for assignments to
339
+ `initialize`. A block can be given as well.
340
+
341
+
342
+ ```ruby
343
+ class Person
344
+ include Chassis::Initializable
345
+
346
+ attr_accessor :name, :email
347
+ end
348
+
349
+ Person.new name: 'adam', email: 'example@example.com'
350
+
351
+ Person.new name: 'adam' do |adam|
352
+ adam.email = 'example@example.com'
353
+ end
354
+ ```
355
+
356
+ ## Contributing
357
+
358
+ 1. Fork it
359
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
360
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
361
+ 4. Push to the branch (`git push origin my-new-feature`)
362
+ 5. Create new Pull Request
@@ -0,0 +1,33 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+ require 'stringio'
5
+
6
+ def capture_stdout
7
+ out = StringIO.new
8
+ $stdout = out
9
+ yield
10
+ return out
11
+ ensure
12
+ $stdout = STDOUT
13
+ end
14
+
15
+ desc 'Run examples'
16
+ task :examples do
17
+ root = File.dirname __FILE__
18
+ Dir["#{root}/examples/*.rb"].each do |example|
19
+ capture_stdout do
20
+ require example
21
+ end
22
+ end
23
+ end
24
+
25
+ namespace :test do
26
+ Rake::TestTask.new(:all) do |t|
27
+ t.pattern = 'test/**/*_test.rb'
28
+ end
29
+ end
30
+
31
+ task test: ['test:all', 'examples']
32
+
33
+ task default: :test