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,56 @@
1
+ module Chassis
2
+ UnknownFormFieldError = Chassis.error do |field|
3
+ "#{field} given but not allowed."
4
+ end
5
+
6
+ class FormModule < Module
7
+ module InstanceMethods
8
+ def initialize(hash = {})
9
+ assert_valid_keys! HashUtils.symbolize(hash)
10
+ super
11
+ yield self if block_given?
12
+ end
13
+
14
+ def values
15
+ dirty_attributes
16
+ end
17
+
18
+ private
19
+ def assert_valid_keys!(hash)
20
+ return if accepted_keys.empty?
21
+
22
+ hash.keys.each do |key|
23
+ raise UnknownFormFieldError, key unless accepted_keys.include? key
24
+ end
25
+ end
26
+
27
+ def accepted_keys
28
+ self.class.accepted_keys
29
+ end
30
+ end
31
+
32
+ module ClassMethods
33
+ def attribute(name, type, options = {})
34
+ accepted_keys << name.to_sym
35
+ super(name, type, options)
36
+ end
37
+
38
+ def accepted_keys
39
+ @accepted_keys ||= []
40
+ end
41
+ end
42
+
43
+ def included(base)
44
+ base.include Virtus.model
45
+ base.extend ClassMethods
46
+ base.include Virtus::DirtyAttribute
47
+ base.include InstanceMethods
48
+ end
49
+ end
50
+
51
+ class << self
52
+ def form
53
+ FormModule.new
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,16 @@
1
+ module Chassis
2
+ module HashUtils
3
+ def symbolize(hash)
4
+ hash.inject({}) do |memo, pair|
5
+ key, value = pair
6
+
7
+ if value.is_a? Hash
8
+ memo.merge! key.to_sym => symbolize(value)
9
+ else
10
+ memo.merge! key.to_sym => value
11
+ end
12
+ end
13
+ end
14
+ module_function :symbolize
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ # Whatever random things need to be done to make everything
2
+ # behave correctly on heroku.
3
+
4
+ $stdout.sync = true
5
+ $stderr.sync = true
@@ -0,0 +1,11 @@
1
+ module Chassis
2
+ module Initializable
3
+ def initialize(values = {})
4
+ super()
5
+ values.each_pair do |key, value|
6
+ send "#{key}=", value
7
+ end
8
+ yield self if block_given?
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ module Chassis
2
+ class Logger < ::Logger
3
+ def initialize(logdev = Chassis.stream, shift_age = 0, shift_size = 1048576)
4
+ super
5
+ self.level = ENV['LOG_LEVEL'].to_sym if ENV['LOG_LEVEL']
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ module Chassis
2
+ module Observable
3
+ def add_observer(observer)
4
+ @observers ||= []
5
+ @observers << observer
6
+ end
7
+
8
+ private
9
+ def notify_observers(event, *args)
10
+ return unless defined? @observers
11
+
12
+ @observers.each do |observer|
13
+ if observer.respond_to? event
14
+ observer.send event, *[self, args].flatten
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ module Chassis
2
+ module Persistence
3
+ module ClassMethods
4
+ def create(*args, &block)
5
+ record = new(*args, &block)
6
+ record.save
7
+ record
8
+ end
9
+
10
+ def repo
11
+ begin
12
+ @repo ||= StringUtils.constantize "#{name}Repo"
13
+ rescue NameError
14
+ fail "#{name}Repo not defined! Define this method to specify a different repo"
15
+ end
16
+ end
17
+ end
18
+
19
+ class << self
20
+ def included(base)
21
+ base.class_eval do
22
+ include Initializable
23
+ include Equalizer.new(:id)
24
+
25
+ attr_accessor :id
26
+ end
27
+
28
+ base.extend ClassMethods
29
+ end
30
+ end
31
+
32
+ def save
33
+ yield self if block_given?
34
+ repo.save self
35
+ end
36
+
37
+ def delete
38
+ repo.delete self
39
+ end
40
+
41
+ def new_record?
42
+ id.nil?
43
+ end
44
+
45
+ def repo
46
+ self.class.repo
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ module Chassis
2
+ module Rack
3
+ class Bouncer
4
+ BOUNCERS = [
5
+ lambda { |req| ; req.user_agent =~ /masscan/ }
6
+ ]
7
+
8
+ def initialize(app, &bouncer)
9
+ @app, @bouncer = app, bouncer
10
+ end
11
+
12
+ def call(env)
13
+ if bounce?(env)
14
+ [403, { }, [ ]]
15
+ else
16
+ @app.call env
17
+ end
18
+ end
19
+
20
+ def bounce?(env)
21
+ request = ::Rack::Request.new env
22
+
23
+ bouncers.any? do |bouncer|
24
+ bouncer.call request
25
+ end
26
+ end
27
+
28
+ def bouncers
29
+ [BOUNCERS, @bouncer].flatten.compact
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ module Rack
2
+ class Builder
3
+ def shim(middleware, *args, &block)
4
+ @use.unshift proc { |app| middleware.new(app, *args, &block) }
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,45 @@
1
+ module Chassis
2
+ HealthCheckError = Class.new RuntimeError
3
+
4
+ module Rack
5
+ class Ping
6
+ def initialize(app, &block)
7
+ @app, @block = app, block
8
+ end
9
+
10
+ def call(env)
11
+ if '/ping' == env.fetch('PATH_INFO')
12
+ if @block
13
+ begin
14
+ result = @block.call(::Rack::Request.new(env))
15
+ raise "health check did not return correctly" unless result
16
+ rescue => boom
17
+ raise HealthCheckError, boom.to_s
18
+ end
19
+ end
20
+
21
+ [200, {'Content-Type' => 'text/plain'}, ['pong']]
22
+ else
23
+ @app.call env
24
+ end
25
+ end
26
+ end
27
+
28
+ class HealthCheck
29
+ def initialize(app)
30
+ @app = app
31
+ end
32
+
33
+ def call(env)
34
+ begin
35
+ @app.call env
36
+ rescue HealthCheckError => ex
37
+ env['rack.errors'].write ex.to_s
38
+ env['rack.errors'].write ex.backtrace.join("\n")
39
+ env['rack.errors'].flush
40
+ exit!
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ require 'rack/runtime'
2
+
3
+ module Chassis
4
+ module Rack
5
+ class Instrumentation
6
+ def initialize(app, namespace = nil)
7
+ stack = ::Rack::Builder.new
8
+ stack.use ::Rack::Runtime
9
+ stack.use ::Harness::RackInstrumenter, namespace
10
+ stack.run app
11
+
12
+ @app = stack.to_app
13
+ end
14
+
15
+ def call(env)
16
+ @app.call env
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Chassis
2
+ module Rack
3
+ class JsonBodyParser < ::Rack::PostBodyContentTypeParser
4
+ def call(env)
5
+ body = env[POST_BODY].read
6
+
7
+ if body.strip.empty?
8
+ @app.call env
9
+ else
10
+ begin
11
+ env[POST_BODY].rewind
12
+ super
13
+ rescue JSON::ParserError => ex
14
+ [400, { 'Content-Type' => 'text/plain' }, [ex.to_s]]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module Chassis
2
+ module Rack
3
+ class NoRobots
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ if env['PATH_INFO'] == '/robots.txt'
10
+ [200, {'Content-Type' => 'text/plain'}, [robots_txt]]
11
+ else
12
+ @app.call env
13
+ end
14
+ end
15
+
16
+ def robots_txt
17
+ <<-txt
18
+ User Agent: *
19
+ Disallow: /
20
+ txt
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ module Chassis
2
+ UnregisteredError = Chassis.error do |key|
3
+ "#{key.inspect} is not registered!"
4
+ end
5
+
6
+ class Registry
7
+ def initialize
8
+ @map = { }
9
+ end
10
+
11
+ def []=(key, value)
12
+ map[key] = value
13
+ end
14
+
15
+ def fetch(key)
16
+ map.fetch key do
17
+ fail UnregisteredError, key
18
+ end
19
+ end
20
+
21
+ def clear
22
+ map.clear
23
+ end
24
+
25
+ private
26
+ def map
27
+ @map
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,73 @@
1
+ module Chassis
2
+ RecordNotFoundError = Chassis.error do |klass, id|
3
+ "Could not locate #{klass} with id #{id}"
4
+ end
5
+
6
+ QueryNotImplementedError = Chassis.error do |selector|
7
+ "Adapter does not support #{selector.class}!"
8
+ end
9
+
10
+ GraphQueryNotImplementedError = Chassis.error do |selector|
11
+ "Adapter does not know how to graph #{selector.class}!"
12
+ end
13
+
14
+ NoQueryResultError = Chassis.error do |selector|
15
+ "Query #{selector.class} must return results!"
16
+ end
17
+
18
+ class Repo
19
+ include Interchange.new(*[
20
+ :all, :find, :create, :update, :delete,
21
+ :first, :last, :query, :graph_query,
22
+ :sample, :empty?, :count, :clear,
23
+ :initialize_storage
24
+ ])
25
+
26
+ class << self
27
+ def default
28
+ @default ||= new
29
+ end
30
+ end
31
+
32
+ def find(klass, id)
33
+ raise ArgumentError, "id cannot be nil!" if id.nil?
34
+ super
35
+ end
36
+
37
+ def save(record)
38
+ if record.id
39
+ update record
40
+ else
41
+ create record
42
+ end
43
+ end
44
+
45
+ def query!(klass, selector)
46
+ result = query klass, selector
47
+ no_results = result.respond_to?(:empty?) ? result.empty? : result.nil?
48
+
49
+ if no_results && block_given?
50
+ yield klass, selector
51
+ elsif no_results
52
+ fail NoQueryResultError, selector
53
+ end
54
+
55
+ result
56
+ end
57
+ end
58
+
59
+ class << self
60
+ def repo
61
+ Repo.default
62
+ end
63
+ end
64
+ end
65
+
66
+ require_relative 'repo/delegation'
67
+ require_relative 'repo/lazy_association'
68
+ require_relative 'repo/record_map'
69
+ require_relative 'repo/base_repo'
70
+ require_relative 'repo/null_repo'
71
+ require_relative 'repo/memory_repo'
72
+ require_relative 'repo/redis_repo'
73
+ require_relative 'repo/pstore_repo'
@@ -0,0 +1,99 @@
1
+ module Chassis
2
+ class BaseRepo
3
+ def create(record)
4
+ record.id ||= next_id
5
+ map.set record
6
+ end
7
+
8
+ def update(record)
9
+ map.set record
10
+ end
11
+
12
+ def delete(record)
13
+ map.delete record
14
+ end
15
+
16
+ def clear
17
+ map.clear
18
+ end
19
+
20
+ def all(klass)
21
+ map.all klass
22
+ end
23
+
24
+ def find(klass, id)
25
+ map.get klass, id
26
+ end
27
+
28
+ def first(klass)
29
+ all(klass).first
30
+ end
31
+
32
+ def last(klass)
33
+ all(klass).last
34
+ end
35
+
36
+ def sample(klass)
37
+ all(klass).sample
38
+ end
39
+
40
+ def empty?(klass)
41
+ all(klass).empty?
42
+ end
43
+
44
+ def count(klass)
45
+ all(klass).count
46
+ end
47
+
48
+ def query(klass, selector)
49
+ if query_implemented? klass, selector
50
+ send query_method(klass, selector), klass, selector
51
+ else
52
+ raise QueryNotImplementedError, selector
53
+ end
54
+ end
55
+
56
+ def graph_query(klass, selector)
57
+ if graph_query_implemented? klass, selector
58
+ send graph_query_method(klass, selector), klass, selector
59
+ else
60
+ raise GraphQueryNotImplementedError, selector
61
+ end
62
+ end
63
+
64
+ def initialize_storage
65
+
66
+ end
67
+
68
+ private
69
+ def map
70
+ @map
71
+ end
72
+
73
+ def next_id
74
+ @counter ||= 0
75
+ @counter = @counter + 1
76
+ @counter
77
+ end
78
+
79
+ def query_method(klass, selector)
80
+ "query_#{selector_key(selector)}"
81
+ end
82
+
83
+ def query_implemented?(klass, selector)
84
+ respond_to? query_method(klass, selector)
85
+ end
86
+
87
+ def graph_query_method(klass, selector)
88
+ "graph_query_#{selector_key(selector)}"
89
+ end
90
+
91
+ def graph_query_implemented?(klass, selector)
92
+ respond_to? graph_query_method(klass, selector)
93
+ end
94
+
95
+ def selector_key(selector)
96
+ StringUtils.underscore(StringUtils.demodulize(selector.class.name))
97
+ end
98
+ end
99
+ end