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