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,41 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'chassis/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "chassis"
8
+ spec.version = Chassis::VERSION
9
+ spec.authors = ["ahawkins"]
10
+ spec.email = ["adam@hawkins.io"]
11
+ spec.description = %q{A collection of modules and helpers for building mantainable Ruby applications}
12
+ spec.summary = %q{}
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_dependency "sinatra"
22
+ spec.add_dependency "sinatra-contrib"
23
+ spec.add_dependency "rack-contrib"
24
+ spec.add_dependency "manifold"
25
+ spec.add_dependency "prox"
26
+ spec.add_dependency "harness"
27
+ spec.add_dependency "harness-rack"
28
+ spec.add_dependency "virtus"
29
+ spec.add_dependency "virtus-dirty_attribute"
30
+ spec.add_dependency "faraday", "~> 0.9.0"
31
+ spec.add_dependency "logger-better"
32
+ spec.add_dependency "breaker"
33
+ spec.add_dependency "interchange"
34
+ spec.add_dependency "tnt"
35
+
36
+ spec.add_development_dependency "bundler", "~> 1.3"
37
+ spec.add_development_dependency "rake"
38
+ spec.add_development_dependency "rack-test"
39
+ spec.add_development_dependency "mocha"
40
+ spec.add_development_dependency "redis"
41
+ end
@@ -0,0 +1,40 @@
1
+ require 'chassis'
2
+
3
+ class Post
4
+ attr_accessor :id, :title, :text
5
+ end
6
+
7
+ repo = Chassis::Repo.default
8
+
9
+ puts repo.empty? Post #=> true
10
+
11
+ post = Post.new
12
+ post.title = 'Such Repos'
13
+ post.text = 'Very wow. Much design.'
14
+
15
+ repo.save post
16
+
17
+ puts post.id #=> 1
18
+
19
+ found_post = repo.find Post, post.id
20
+ found_post == post #=> true (no difference between objects in memory)
21
+
22
+ post.title = 'Such updates'
23
+ post.text = 'Very easy. Wow'
24
+
25
+ repo.save post
26
+
27
+ repo.find(Post, post.id).text #=> 'Very easy. Wow.'
28
+
29
+ class PostRepo
30
+ extend Chassis::Repo::Delegation
31
+ end
32
+
33
+ post = PostRepo.find post.id
34
+ post.text #=> 'Very Easy. Wow'
35
+
36
+ PostRepo.all #=> [post]
37
+ # etc
38
+
39
+ PostRepo.delete post
40
+ PostRepo.empty? #=> true
@@ -0,0 +1,81 @@
1
+ require "chassis/version"
2
+
3
+ require 'logger-better'
4
+
5
+ require 'sinatra'
6
+ require 'manifold'
7
+ require 'rack/contrib/bounce_favicon'
8
+ require 'rack/contrib/post_body_content_type_parser'
9
+
10
+ require 'harness'
11
+ require 'harness/rack'
12
+
13
+ require 'virtus'
14
+ require 'virtus/dirty_attribute'
15
+
16
+ require 'prox'
17
+
18
+ require 'faraday'
19
+
20
+ require 'breaker'
21
+
22
+ require 'interchange'
23
+
24
+ require 'tnt'
25
+
26
+ module Chassis
27
+ Proxy = Prox
28
+
29
+ class << self
30
+ def stream
31
+ @stream
32
+ end
33
+
34
+ def stream=(stream)
35
+ @stream = stream
36
+ end
37
+ end
38
+ end
39
+
40
+ require_relative 'chassis/hash_utils'
41
+ require_relative 'chassis/string_utils'
42
+ require_relative 'chassis/array_utils'
43
+
44
+ require_relative 'chassis/error'
45
+
46
+ require_relative 'chassis/logger'
47
+
48
+ require_relative 'chassis/faraday'
49
+
50
+ require_relative 'chassis/form'
51
+
52
+ require_relative 'chassis/persistence'
53
+
54
+ require_relative 'chassis/initializable'
55
+
56
+ require_relative 'chassis/serializable'
57
+
58
+ require_relative 'chassis/observable'
59
+
60
+ require_relative 'chassis/dirty_session'
61
+
62
+ require_relative 'chassis/circuit_panel'
63
+
64
+ require_relative 'chassis/registry'
65
+
66
+ require_relative 'chassis/repo'
67
+
68
+ require_relative 'chassis/delegate'
69
+
70
+ require_relative 'chassis/rack/bouncer'
71
+ require_relative 'chassis/rack/builder_shim_patch'
72
+ require_relative 'chassis/rack/health_check'
73
+ require_relative 'chassis/rack/instrumentation'
74
+ require_relative 'chassis/rack/no_robots'
75
+ require_relative 'chassis/rack/json_body_parser'
76
+
77
+ require_relative 'chassis/web_service'
78
+
79
+ Chassis.repo.register :memory, Chassis::MemoryRepo.new
80
+ Chassis.repo.register :null, Chassis::NullRepo.new
81
+ Chassis.repo.use :memory
@@ -0,0 +1,8 @@
1
+ module Chassis
2
+ module ArrayUtils
3
+ def extract_options!(array)
4
+ array.last.is_a?(Hash) ? array.pop : { }
5
+ end
6
+ module_function :extract_options!
7
+ end
8
+ end
@@ -0,0 +1,22 @@
1
+ module Chassis
2
+ class CircuitPanel
3
+ class << self
4
+ def build(&block)
5
+ raise ArgumentError, "block required" unless block
6
+ Class.new self, &block
7
+ end
8
+
9
+ def circuit(name, options = {})
10
+ define_method name do
11
+ Breaker.circuit name, options
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ class << self
18
+ def circuit_panel(&block)
19
+ CircuitPanel.build &block
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ class Array
2
+ def extract_options!
3
+ Chassis::ArrayUtils.extract_options! self
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Hash
2
+ def symbolize
3
+ Chassis::HashUtils.symbolize self
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ class String
2
+ def constantize
3
+ Chassis::StringUtils.constantize self
4
+ end
5
+
6
+ def demodulize
7
+ Chassis::StringUtils.demodulize self
8
+ end
9
+
10
+ def underscore
11
+ Chassis::StringUtils.underscore self
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ module Chassis
2
+ DelegationError = Chassis.error do |method, delegate|
3
+ "Cannot delegate #{method} without #{delegate}"
4
+ end
5
+
6
+ class Delegation < Module
7
+ def initialize(*methods)
8
+ options = methods.last.is_a?(Hash) ? methods.pop : { }
9
+
10
+ delegate = options.fetch :to do
11
+ fail ArgumentError, ":to not given"
12
+ end
13
+
14
+ methods.each do |method|
15
+ define_method method do |*args, &block|
16
+ object = send delegate
17
+ fail DelegationError.new method, delegate unless object
18
+ object.send(method, *args, &block)
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ class << self
25
+ def delegate(*methods)
26
+ Delegation.new *methods
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,105 @@
1
+ require 'set'
2
+
3
+ module Chassis
4
+ class DirtySession < Proxy
5
+ def initialize(*args)
6
+ super
7
+ @original_values = { }
8
+ @new_values = { }
9
+ end
10
+
11
+ def clean?
12
+ new_values.empty?
13
+ end
14
+
15
+ def dirty?
16
+ !clean?
17
+ end
18
+
19
+ def original_values
20
+ @original_values
21
+ end
22
+
23
+ def new_values
24
+ @new_values
25
+ end
26
+
27
+ def changes
28
+ Set.new original_values.keys
29
+ end
30
+
31
+ def reset!
32
+ original_values.clear
33
+ new_values.clear
34
+ end
35
+
36
+ def method_missing(name, *args, &block)
37
+ raise MissingObject, name unless __getobj__
38
+
39
+ if writer_method?(name)
40
+ handle_writer_method name, *args, &block
41
+ elsif changed_method?(name)
42
+ handle_changed_method name
43
+ elsif original_method?(name)
44
+ handle_original_method name
45
+ else
46
+ __getobj__.send name, *args, &block
47
+ end
48
+ end
49
+
50
+ def respond_to_missing?(name, include_private = false)
51
+ if changed_method?(name) || original_method?(name)
52
+ __getobj__.respond_to? reader_method(name)
53
+ else
54
+ super
55
+ end
56
+ end
57
+
58
+ private
59
+ def handle_writer_method(name, *args, &block)
60
+ method_key = reader_method name
61
+
62
+ original_value = __getobj__.send method_key
63
+ new_value = __getobj__.send name, *args, &block
64
+
65
+ if new_value != original_value
66
+ original_values[method_key] = original_value unless original_values.key? method_key
67
+ new_values[method_key] = new_value
68
+ end
69
+
70
+ new_value
71
+ end
72
+
73
+ def writer_method?(name)
74
+ name =~ /=$/
75
+ end
76
+
77
+ def reader_method(name)
78
+ method_name = name.to_s
79
+
80
+ if writer_method? method_name
81
+ method_name.match(/(.+)=$/)[1].to_sym
82
+ elsif changed_method? method_name
83
+ method_name.match(/(.+)_changed\?$/)[1].to_sym
84
+ elsif original_method? method_name
85
+ method_name.match(/original_(.+)$/)[1].to_sym
86
+ end
87
+ end
88
+
89
+ def original_method?(name)
90
+ name =~ /original_.+$/
91
+ end
92
+
93
+ def changed_method?(name)
94
+ name =~ /_changed\?$/
95
+ end
96
+
97
+ def handle_changed_method(name)
98
+ original_values.key? reader_method(name)
99
+ end
100
+
101
+ def handle_original_method(name)
102
+ original_values[reader_method(name)]
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,7 @@
1
+ module Chassis
2
+ class << self
3
+ def error(*args, &block)
4
+ Tnt.boom *args, &block
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,226 @@
1
+ module Chassis
2
+ class HttpRequestFailedError < StandardError
3
+ def initialize(env)
4
+ @env = env
5
+ end
6
+
7
+ def to_s
8
+ "#{status} - #{description} : #{method} #{url}"
9
+ end
10
+
11
+ def method
12
+ env.fetch(:method).upcase
13
+ end
14
+
15
+ def body
16
+ env.fetch :body
17
+ end
18
+
19
+ def url
20
+ env.fetch(:url)
21
+ end
22
+
23
+ def status
24
+ env.fetch :status
25
+ end
26
+
27
+ def description
28
+ ::Rack::Utils::HTTP_STATUS_CODES.fetch status, 'UNKOWN'
29
+ end
30
+
31
+ private
32
+ def env
33
+ @env
34
+ end
35
+ end
36
+
37
+ # 4xx errors
38
+ HttpClientError = Class.new HttpRequestFailedError
39
+ HttpBadRequestError = Class.new HttpClientError
40
+ HttpUnauthorizedError = Class.new HttpClientError
41
+ HttpPaymentRequiredError = Class.new HttpClientError
42
+ HttpForbiddenError = Class.new HttpClientError
43
+ HttpNotFoundError = Class.new HttpClientError
44
+ HttpMethodNotAllowedError = Class.new HttpClientError
45
+ HttpNotAcceptableError = Class.new HttpClientError
46
+ HttpProxyAuthenticationRequiredError = Class.new HttpClientError
47
+ HttpRequestTimeoutError = Class.new HttpClientError
48
+ HttpConflictError = Class.new HttpClientError
49
+ HttpGoneError = Class.new HttpClientError
50
+ HttpLengthRequiredError = Class.new HttpClientError
51
+ HttpPreconditionFailedError = Class.new HttpClientError
52
+ HttpRequestEntityTooLargeError = Class.new HttpClientError
53
+ HttpRequestUriTooLongError = Class.new HttpClientError
54
+ HttpUnsupportedMediaTypeError = Class.new HttpClientError
55
+ HttpRequestRangeNotSatisfiableError = Class.new HttpClientError
56
+ HttpExpectationFailedError = Class.new HttpClientError
57
+ HttpUnprocessableEntityError = Class.new HttpClientError
58
+ HttpLockedError = Class.new HttpClientError
59
+ HttpFailedDependencyError = Class.new HttpClientError
60
+ HttpUpgradeRequiredError = Class.new HttpClientError
61
+
62
+ # 5xx errors
63
+ HttpServerError = Class.new HttpRequestFailedError
64
+ HttpInternalServerError = Class.new HttpServerError
65
+ HttpNotImplementedError = Class.new HttpServerError
66
+ HttpBadGatewayError = Class.new HttpServerError
67
+ HttpServiceUnavailableError = Class.new HttpServerError
68
+ HttpGatewayTimeoutError = Class.new HttpServerError
69
+ HttpVersionNotSupportedError = Class.new HttpServerError
70
+ HttpInsufficientStorageError = Class.new HttpServerError
71
+ HttpNotExtendedError = Class.new HttpServerError
72
+
73
+ HTTP_STATUS_CODE_ERROR_MAP = {
74
+ 400 => HttpBadRequestError,
75
+ 401 => HttpUnauthorizedError,
76
+ 402 => HttpPaymentRequiredError,
77
+ 403 => HttpForbiddenError,
78
+ 404 => HttpNotFoundError,
79
+ 405 => HttpMethodNotAllowedError,
80
+ 406 => HttpNotAcceptableError,
81
+ 407 => HttpProxyAuthenticationRequiredError,
82
+ 408 => HttpRequestTimeoutError,
83
+ 409 => HttpConflictError,
84
+ 410 => HttpGoneError,
85
+ 411 => HttpLengthRequiredError,
86
+ 412 => HttpPreconditionFailedError,
87
+ 413 => HttpRequestEntityTooLargeError,
88
+ 414 => HttpRequestUriTooLongError,
89
+ 415 => HttpUnsupportedMediaTypeError,
90
+ 416 => HttpRequestRangeNotSatisfiableError,
91
+ 417 => HttpExpectationFailedError,
92
+ 422 => HttpUnprocessableEntityError,
93
+ 423 => HttpLockedError,
94
+ 424 => HttpFailedDependencyError,
95
+ 426 => HttpUpgradeRequiredError,
96
+
97
+ # 5xx errors
98
+ 500 => HttpInternalServerError,
99
+ 501 => HttpNotImplementedError,
100
+ 502 => HttpBadGatewayError,
101
+ 503 => HttpServiceUnavailableError,
102
+ 504 => HttpGatewayTimeoutError,
103
+ 505 => HttpVersionNotSupportedError,
104
+ 507 => HttpInsufficientStorageError,
105
+ 510 => HttpNotExtendedError
106
+ }
107
+
108
+ class ServerErrorHandler < ::Faraday::Response::Middleware
109
+ def on_complete(env)
110
+ status = env.fetch :status
111
+ return unless (400..600).include? status
112
+ raise HTTP_STATUS_CODE_ERROR_MAP.fetch(status), env
113
+ end
114
+ end
115
+
116
+ class ParseJson < ::Faraday::Response::Middleware
117
+ def on_complete(env)
118
+ return if [204, 304].include? env.fetch(:status)
119
+
120
+ content_type = env.fetch(:response_headers).fetch('Content-Type', nil)
121
+
122
+ return unless content_type
123
+ return unless content_type =~ /json/
124
+
125
+ body = env.body
126
+
127
+ return unless body.respond_to? :to_str
128
+
129
+ content = body.to_str.strip
130
+
131
+ return if content.empty?
132
+
133
+ env[:body] = JSON.load content
134
+ end
135
+ end
136
+
137
+ class Instrumentation < ::Faraday::Middleware
138
+ include Harness::Instrumentation
139
+
140
+ def initialize(app, progname = 'faraday')
141
+ @app, @progname = app, progname
142
+ end
143
+
144
+ def call(env)
145
+ time "#{@progname}.#{env.method}" do
146
+ @app.call env
147
+ end
148
+ end
149
+ end
150
+
151
+ class EncodeJson < ::Faraday::Middleware
152
+ def initialize(app)
153
+ @app = app
154
+ end
155
+
156
+ def call(env)
157
+ if env.body
158
+ env[:request_headers]['Content-Type'] = 'application/json'
159
+ env.body = JSON.dump env.body
160
+ end
161
+
162
+ @app.call env
163
+ end
164
+ end
165
+
166
+ class Logging < ::Faraday::Response::Middleware
167
+ def initialize(app, logger)
168
+ @app, @logger = app, logger
169
+ end
170
+
171
+ def call(env)
172
+ dump_request env
173
+ super
174
+ end
175
+
176
+ def on_complete(env)
177
+ dump_response env
178
+ end
179
+
180
+ private
181
+ def dump_request(env)
182
+ request_line = "> #{env.fetch(:method).to_s.upcase} #{env.fetch(:url)}"
183
+ headers = env.fetch(:request_headers).map do |name, value|
184
+ "> #{name}: #{value}"
185
+ end.join("\n")
186
+
187
+ @logger.debug [request_line, headers, env.body].compact.join("\n")
188
+ end
189
+
190
+ def dump_response(env)
191
+ status = env.fetch :status
192
+ status_text = ::Rack::Utils::HTTP_STATUS_CODES.fetch status, 'Unknown'
193
+ request_line = "> #{status.to_s.upcase} #{status_text}"
194
+ headers = env.fetch(:response_headers).map do |name, value|
195
+ "> #{name}: #{value}"
196
+ end.join("\n")
197
+
198
+ @logger.debug [request_line, headers, env.body].compact.join("\n")
199
+ end
200
+ end
201
+
202
+ ::Faraday::Request.register_middleware instrumentation: Instrumentation
203
+ ::Faraday::Request.register_middleware encode_json: EncodeJson
204
+
205
+ ::Faraday::Response.register_middleware parse_json: ParseJson
206
+ ::Faraday::Response.register_middleware server_error_handler: ServerErrorHandler
207
+ ::Faraday::Response.register_middleware logging: Logging
208
+
209
+ class << self
210
+ def faraday(host, options = {})
211
+ namespace = options.delete :namespace
212
+ logger = options.delete(:logger) || Logger.new.tap { |l| l.progname = 'faraday' }
213
+
214
+ Faraday.new host, options do |conn|
215
+ conn.request :instrumentation, namespace
216
+ conn.request :encode_json
217
+
218
+ conn.response :parse_json
219
+ conn.response :server_error_handler
220
+ conn.response :logging, logger
221
+
222
+ yield conn if block_given?
223
+ end
224
+ end
225
+ end
226
+ end