chassis 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +362 -0
- data/Rakefile +33 -0
- data/chassis.gemspec +41 -0
- data/examples/repo.rb +40 -0
- data/lib/chassis.rb +81 -0
- data/lib/chassis/array_utils.rb +8 -0
- data/lib/chassis/circuit_panel.rb +22 -0
- data/lib/chassis/core_ext/array.rb +5 -0
- data/lib/chassis/core_ext/hash.rb +5 -0
- data/lib/chassis/core_ext/string.rb +13 -0
- data/lib/chassis/delegate.rb +29 -0
- data/lib/chassis/dirty_session.rb +105 -0
- data/lib/chassis/error.rb +7 -0
- data/lib/chassis/faraday.rb +226 -0
- data/lib/chassis/form.rb +56 -0
- data/lib/chassis/hash_utils.rb +16 -0
- data/lib/chassis/heroku.rb +5 -0
- data/lib/chassis/initializable.rb +11 -0
- data/lib/chassis/logger.rb +8 -0
- data/lib/chassis/observable.rb +19 -0
- data/lib/chassis/persistence.rb +49 -0
- data/lib/chassis/rack/bouncer.rb +33 -0
- data/lib/chassis/rack/builder_shim_patch.rb +7 -0
- data/lib/chassis/rack/health_check.rb +45 -0
- data/lib/chassis/rack/instrumentation.rb +20 -0
- data/lib/chassis/rack/json_body_parser.rb +20 -0
- data/lib/chassis/rack/no_robots.rb +24 -0
- data/lib/chassis/registry.rb +30 -0
- data/lib/chassis/repo.rb +73 -0
- data/lib/chassis/repo/base_repo.rb +99 -0
- data/lib/chassis/repo/delegation.rb +78 -0
- data/lib/chassis/repo/lazy_association.rb +57 -0
- data/lib/chassis/repo/memory_repo.rb +7 -0
- data/lib/chassis/repo/null_repo.rb +64 -0
- data/lib/chassis/repo/pstore_repo.rb +54 -0
- data/lib/chassis/repo/record_map.rb +44 -0
- data/lib/chassis/repo/redis_repo.rb +55 -0
- data/lib/chassis/serializable.rb +52 -0
- data/lib/chassis/string_utils.rb +50 -0
- data/lib/chassis/version.rb +3 -0
- data/lib/chassis/web_service.rb +61 -0
- data/test/array_utils_test.rb +23 -0
- data/test/chassis_test.rb +7 -0
- data/test/circuit_panel_test.rb +22 -0
- data/test/core_ext/array_test.rb +8 -0
- data/test/core_ext/hash_test.rb +8 -0
- data/test/core_ext/string_test.rb +16 -0
- data/test/delegate_test.rb +41 -0
- data/test/dirty_session_test.rb +138 -0
- data/test/error_test.rb +12 -0
- data/test/faraday_test.rb +749 -0
- data/test/form_test.rb +29 -0
- data/test/hash_utils_test.rb +17 -0
- data/test/initializable_test.rb +22 -0
- data/test/logger_test.rb +43 -0
- data/test/observable_test.rb +27 -0
- data/test/persistence_test.rb +112 -0
- data/test/prox_test.rb +7 -0
- data/test/rack/bouncer_test.rb +42 -0
- data/test/rack/builder_patch_test.rb +36 -0
- data/test/rack/health_check_test.rb +35 -0
- data/test/rack/instrumentation_test.rb +38 -0
- data/test/rack/json_body_parser_test.rb +38 -0
- data/test/rack/no_robots_test.rb +34 -0
- data/test/registry_test.rb +26 -0
- data/test/repo/delegation_test.rb +101 -0
- data/test/repo/lazy_association_test.rb +115 -0
- data/test/repo/memory_repo_test.rb +25 -0
- data/test/repo/null_repo_test.rb +48 -0
- data/test/repo/pstore_repo_test.rb +28 -0
- data/test/repo/redis_repo_test.rb +26 -0
- data/test/repo/repo_tests.rb +120 -0
- data/test/repo_test.rb +76 -0
- data/test/serializable_test.rb +77 -0
- data/test/string_utils_test.rb +21 -0
- data/test/test_helper.rb +10 -0
- data/test/web_service_test.rb +107 -0
- metadata +426 -0
data/lib/chassis/form.rb
ADDED
@@ -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,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,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
|
data/lib/chassis/repo.rb
ADDED
@@ -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
|