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