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