chassis 0.1.0

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.
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