startback-websocket 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/README.md +13 -0
- data/Rakefile +18 -0
- data/lib/startback/audit/prometheus.rb +87 -0
- data/lib/startback/audit/shared.rb +17 -0
- data/lib/startback/audit/trailer.rb +129 -0
- data/lib/startback/audit.rb +3 -0
- data/lib/startback/caching/entity_cache.rb +157 -0
- data/lib/startback/caching/no_store.rb +28 -0
- data/lib/startback/caching/store.rb +34 -0
- data/lib/startback/context/h_factory.rb +43 -0
- data/lib/startback/context/middleware.rb +53 -0
- data/lib/startback/context.rb +122 -0
- data/lib/startback/errors.rb +197 -0
- data/lib/startback/event/agent.rb +84 -0
- data/lib/startback/event/bus/bunny/async.rb +162 -0
- data/lib/startback/event/bus/bunny.rb +1 -0
- data/lib/startback/event/bus/memory/async.rb +45 -0
- data/lib/startback/event/bus/memory/sync.rb +35 -0
- data/lib/startback/event/bus/memory.rb +2 -0
- data/lib/startback/event/bus.rb +100 -0
- data/lib/startback/event/engine.rb +94 -0
- data/lib/startback/event/ext/context.rb +5 -0
- data/lib/startback/event/ext/operation.rb +13 -0
- data/lib/startback/event.rb +47 -0
- data/lib/startback/ext/date_time.rb +9 -0
- data/lib/startback/ext/time.rb +9 -0
- data/lib/startback/ext.rb +2 -0
- data/lib/startback/model.rb +6 -0
- data/lib/startback/operation/error_operation.rb +19 -0
- data/lib/startback/operation/multi_operation.rb +28 -0
- data/lib/startback/operation.rb +78 -0
- data/lib/startback/services.rb +11 -0
- data/lib/startback/support/data_object.rb +71 -0
- data/lib/startback/support/env.rb +41 -0
- data/lib/startback/support/fake_logger.rb +18 -0
- data/lib/startback/support/hooks.rb +48 -0
- data/lib/startback/support/log_formatter.rb +34 -0
- data/lib/startback/support/logger.rb +34 -0
- data/lib/startback/support/operation_runner.rb +150 -0
- data/lib/startback/support/robustness.rb +157 -0
- data/lib/startback/support/transaction_manager.rb +25 -0
- data/lib/startback/support/transaction_policy.rb +33 -0
- data/lib/startback/support/world.rb +54 -0
- data/lib/startback/support.rb +26 -0
- data/lib/startback/version.rb +8 -0
- data/lib/startback/web/api.rb +99 -0
- data/lib/startback/web/auto_caching.rb +85 -0
- data/lib/startback/web/catch_all.rb +52 -0
- data/lib/startback/web/cors_headers.rb +80 -0
- data/lib/startback/web/health_check.rb +49 -0
- data/lib/startback/web/magic_assets/ng_html_transformer.rb +80 -0
- data/lib/startback/web/magic_assets/rake_tasks.rb +64 -0
- data/lib/startback/web/magic_assets.rb +98 -0
- data/lib/startback/web/middleware.rb +13 -0
- data/lib/startback/web/prometheus.rb +16 -0
- data/lib/startback/web/shield.rb +58 -0
- data/lib/startback.rb +43 -0
- data/spec/spec_helper.rb +49 -0
- data/spec/unit/audit/test_prometheus.rb +72 -0
- data/spec/unit/audit/test_trailer.rb +105 -0
- data/spec/unit/caching/test_entity_cache.rb +136 -0
- data/spec/unit/context/test_abstraction_factory.rb +64 -0
- data/spec/unit/context/test_dup.rb +42 -0
- data/spec/unit/context/test_fork.rb +37 -0
- data/spec/unit/context/test_h_factory.rb +31 -0
- data/spec/unit/context/test_middleware.rb +45 -0
- data/spec/unit/context/test_with_world.rb +20 -0
- data/spec/unit/context/test_world.rb +17 -0
- data/spec/unit/event/bus/memory/test_async.rb +43 -0
- data/spec/unit/event/bus/memory/test_sync.rb +43 -0
- data/spec/unit/support/hooks/test_after_hook.rb +54 -0
- data/spec/unit/support/hooks/test_before_hook.rb +54 -0
- data/spec/unit/support/operation_runner/test_around_run.rb +156 -0
- data/spec/unit/support/operation_runner/test_before_after_call.rb +48 -0
- data/spec/unit/support/test_data_object.rb +156 -0
- data/spec/unit/support/test_env.rb +75 -0
- data/spec/unit/support/test_robusteness.rb +229 -0
- data/spec/unit/support/test_transaction_manager.rb +64 -0
- data/spec/unit/support/test_world.rb +72 -0
- data/spec/unit/test_event.rb +62 -0
- data/spec/unit/test_operation.rb +55 -0
- data/spec/unit/test_support.rb +40 -0
- data/spec/unit/web/fixtures/assets/app/hello.es6 +4 -0
- data/spec/unit/web/fixtures/assets/app/hello.html +1 -0
- data/spec/unit/web/fixtures/assets/index.es6 +1 -0
- data/spec/unit/web/test_api.rb +82 -0
- data/spec/unit/web/test_auto_caching.rb +81 -0
- data/spec/unit/web/test_catch_all.rb +77 -0
- data/spec/unit/web/test_cors_headers.rb +88 -0
- data/spec/unit/web/test_healthcheck.rb +59 -0
- data/spec/unit/web/test_magic_assets.rb +82 -0
- data/tasks/test.rake +14 -0
- metadata +237 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 296fd63b702924e7a5952b60a67bbb90e89b99a415be62e52a4f7f63e5638753
|
4
|
+
data.tar.gz: 8234e5f95a05e91f6698da787bc8479ce924a2bfc537866ea68d188f89df7b54
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 48169cfbda94301c4a8ebbfea5f0ba93bedba26f59860a4b8d5376d51ec19cf0f23dcbbda9d7c8c366d483f1bd22d9d36dec2ce90ec362870adc731e7c28a7f6
|
7
|
+
data.tar.gz: e304a39fb8f4ce6a3c3df896c014f8b60eda3a58b18b4e9f52c7de1eeee67b3f7fd3b12e9652d7e50ccc7344377a9c289fd9c0aa14d4a6c13dd9ee75fcf39fab
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Startback - Got Your Ruby Back
|
2
|
+
|
3
|
+
Yet another ruby framework, I'm afraid. Here, we srongly seperate between:
|
4
|
+
|
5
|
+
1. the web layer, in charge of a quality HTTP handling
|
6
|
+
2. the operations layer, in charge of the high-level software operations
|
7
|
+
3. the database layer, abstracted using the Relations As First Class Citizen pattern
|
8
|
+
|
9
|
+
Currently,
|
10
|
+
|
11
|
+
1. is handled using extra support on top of Sinatra
|
12
|
+
2. is handled using Startback specific classes
|
13
|
+
3. is handled using Bmg
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', __FILE__)
|
2
|
+
|
3
|
+
def shell(*cmds)
|
4
|
+
cmd = cmds.join("\n")
|
5
|
+
puts cmd
|
6
|
+
system cmd
|
7
|
+
end
|
8
|
+
|
9
|
+
#
|
10
|
+
# Install all tasks found in tasks folder
|
11
|
+
#
|
12
|
+
# See .rake files there for complete documentation.
|
13
|
+
#
|
14
|
+
Dir["tasks/*.rake"].each do |taskfile|
|
15
|
+
load taskfile
|
16
|
+
end
|
17
|
+
|
18
|
+
task :default => :test
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative 'shared'
|
2
|
+
require 'prometheus/client'
|
3
|
+
|
4
|
+
module Startback
|
5
|
+
module Audit
|
6
|
+
#
|
7
|
+
# Prometheus exporter abstraction, that can be registered as an around
|
8
|
+
# hook on OperationRunner and as a prometheus client on Context instances.
|
9
|
+
#
|
10
|
+
# The exporter uses the ruby client for prometheus to expose metrics regarding Operation runs.
|
11
|
+
#
|
12
|
+
# The following metrics are exported:
|
13
|
+
#
|
14
|
+
# A counter 'operation_errors' (failed runs)
|
15
|
+
# A histogram 'operation_calls'
|
16
|
+
#
|
17
|
+
# All these metrics use the following labels
|
18
|
+
# - operation : class name of the operation executed
|
19
|
+
#
|
20
|
+
# Given that this Exporter is intended to be used as around hook on an
|
21
|
+
# `OperationRunner`, operations that fail at construction time will not be
|
22
|
+
# exported at all, since they can't be ran in the first place. This may lead
|
23
|
+
# to metrics not containing important errors cases if operations check their
|
24
|
+
# input at construction time.
|
25
|
+
#
|
26
|
+
class Prometheus
|
27
|
+
include Shared
|
28
|
+
|
29
|
+
def initialize(options = {})
|
30
|
+
@prefix = options[:prefix] || "startback"
|
31
|
+
@options = options
|
32
|
+
@registry = ::Prometheus::Client.registry
|
33
|
+
all_labels = [:operation, :startback_version] + option_labels.keys
|
34
|
+
@errors = @registry.counter(
|
35
|
+
:"#{prefix}_operation_errors",
|
36
|
+
docstring: 'A counter of operation errors',
|
37
|
+
labels: all_labels)
|
38
|
+
@calls = @registry.histogram(
|
39
|
+
:"#{prefix}_operation_calls",
|
40
|
+
docstring: 'A histogram of operation latency',
|
41
|
+
labels: all_labels)
|
42
|
+
end
|
43
|
+
attr_reader :registry, :calls, :errors, :options, :prefix
|
44
|
+
|
45
|
+
def call(runner, op)
|
46
|
+
name = op_name(op)
|
47
|
+
result = nil
|
48
|
+
time = Benchmark.realtime{
|
49
|
+
result = yield
|
50
|
+
}
|
51
|
+
ignore_safely {
|
52
|
+
@calls.observe(time, labels: get_labels(name))
|
53
|
+
}
|
54
|
+
result
|
55
|
+
rescue => ex
|
56
|
+
ignore_safely {
|
57
|
+
@errors.increment(labels: get_labels(name))
|
58
|
+
}
|
59
|
+
raise
|
60
|
+
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
64
|
+
def ignore_safely
|
65
|
+
yield
|
66
|
+
rescue => ex
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_labels(op_name)
|
71
|
+
option_labels.merge({
|
72
|
+
operation: op_name,
|
73
|
+
startback_version: version
|
74
|
+
})
|
75
|
+
end
|
76
|
+
|
77
|
+
def option_labels
|
78
|
+
@options[:labels] || {}
|
79
|
+
end
|
80
|
+
|
81
|
+
def version
|
82
|
+
Startback::VERSION
|
83
|
+
end
|
84
|
+
|
85
|
+
end # class Prometheus
|
86
|
+
end # module Audit
|
87
|
+
end # module Startback
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Startback
|
2
|
+
module Audit
|
3
|
+
module Shared
|
4
|
+
|
5
|
+
def op_name(op)
|
6
|
+
return op.op_name if op.respond_to?(:op_name)
|
7
|
+
|
8
|
+
case op
|
9
|
+
when String then op
|
10
|
+
when Class then op.name
|
11
|
+
else op.class.name
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end # module Shared
|
16
|
+
end # module Audit
|
17
|
+
end # module Startback
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require_relative 'shared'
|
2
|
+
require 'forwardable'
|
3
|
+
module Startback
|
4
|
+
module Audit
|
5
|
+
#
|
6
|
+
# Log & Audit trail abstraction, that can be registered as an around
|
7
|
+
# hook on OperationRunner and as an actual logger on Context instances.
|
8
|
+
#
|
9
|
+
# The trail is outputted as JSON lines, using a Logger on the "device"
|
10
|
+
# passed at construction. The following JSON entries are dumped:
|
11
|
+
#
|
12
|
+
# - severity : INFO or ERROR
|
13
|
+
# - time : ISO8601 Datetime of operation execution
|
14
|
+
# - op : class name of the operation executed
|
15
|
+
# - op_took : Execution duration of the operation
|
16
|
+
# - op_data : Dump of operation input data
|
17
|
+
# - context : Execution context, through its `h` information contract (IC)
|
18
|
+
#
|
19
|
+
# Dumping of operation data follows the following duck typing conventions:
|
20
|
+
#
|
21
|
+
# - If the operation instance responds to `to_trail`, this data is taken
|
22
|
+
# - If the operation instance responds to `input`, this data is taken
|
23
|
+
# - If the operation instance responds to `request`, this data is taken
|
24
|
+
# - Otherwise op_data is a JSON null
|
25
|
+
#
|
26
|
+
# By contributing to the Context's `h` IC, users can easily dump information that
|
27
|
+
# makes sense (such as the operation execution requester).
|
28
|
+
#
|
29
|
+
# The class implements a sanitization process when dumping the context and
|
30
|
+
# operation data. Blacklisted words taken in construction options are used to
|
31
|
+
# prevent dumping hash keys that match them (insentively). Default stop words
|
32
|
+
# are equivalent to:
|
33
|
+
#
|
34
|
+
# Trailer.new("/var/log/trail.log", {
|
35
|
+
# blacklist: "token password secret credential"
|
36
|
+
# })
|
37
|
+
#
|
38
|
+
# Please note that the sanitization process does not apply recursively if
|
39
|
+
# the operation data is hierarchic. It only applies to the top object of
|
40
|
+
# Hash and [Hash]. Use `Operation#to_trail` to fine-tune your audit trail.
|
41
|
+
#
|
42
|
+
# Given that this Trailer is intended to be used as around hook on an
|
43
|
+
# `OperationRunner`, operations that fail at construction time will not be
|
44
|
+
# trailed at all, since they can't be ran in the first place. This may lead
|
45
|
+
# to trails not containing important errors cases if operations check their
|
46
|
+
# input at construction time.
|
47
|
+
#
|
48
|
+
class Trailer
|
49
|
+
include Shared
|
50
|
+
extend Forwardable
|
51
|
+
def_delegators :@logger, :debug, :info, :warn, :error, :fatal
|
52
|
+
|
53
|
+
DEFAULT_OPTIONS = {
|
54
|
+
|
55
|
+
# Words used to stop dumping for, e.g., security reasons
|
56
|
+
blacklist: "token password secret credential"
|
57
|
+
|
58
|
+
}
|
59
|
+
|
60
|
+
def initialize(device, options = {})
|
61
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
62
|
+
@logger = ::Logger.new(device, 'daily')
|
63
|
+
@logger.formatter = Support::LogFormatter.new
|
64
|
+
end
|
65
|
+
attr_reader :logger, :options
|
66
|
+
|
67
|
+
def call(runner, op)
|
68
|
+
result = nil
|
69
|
+
time = Benchmark.realtime{ result = yield }
|
70
|
+
logger.info(op_to_trail(op, time))
|
71
|
+
result
|
72
|
+
rescue => ex
|
73
|
+
logger.error(op_to_trail(op, time, ex))
|
74
|
+
raise
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def op_to_trail(op, time = nil, ex = nil)
|
80
|
+
log_msg = {
|
81
|
+
op_took: time ? time.round(8) : nil,
|
82
|
+
op: op_name(op),
|
83
|
+
context: op_context(op),
|
84
|
+
op_data: op_data(op)
|
85
|
+
}.compact
|
86
|
+
log_msg[:error] = ex if ex
|
87
|
+
log_msg
|
88
|
+
end
|
89
|
+
|
90
|
+
def op_context(op)
|
91
|
+
sanitize(op.respond_to?(:context, false) ? op.context.to_h : {})
|
92
|
+
end
|
93
|
+
|
94
|
+
def op_data(op)
|
95
|
+
data = if op.respond_to?(:op_data, false)
|
96
|
+
op.op_data
|
97
|
+
elsif op.respond_to?(:to_trail, false)
|
98
|
+
op.to_trail
|
99
|
+
elsif op.respond_to?(:input, false)
|
100
|
+
op.input
|
101
|
+
elsif op.respond_to?(:request, false)
|
102
|
+
op.request
|
103
|
+
elsif op.is_a?(Operation::MultiOperation)
|
104
|
+
op.ops.map{ |sub_op| op_to_trail(sub_op) }
|
105
|
+
end
|
106
|
+
sanitize(data)
|
107
|
+
end
|
108
|
+
|
109
|
+
def sanitize(data)
|
110
|
+
case data
|
111
|
+
when Hash, OpenStruct
|
112
|
+
data.dup.delete_if{|k| k.to_s =~ blacklist_rx }
|
113
|
+
when Enumerable
|
114
|
+
data.map{|elm| sanitize(elm) }.compact
|
115
|
+
else
|
116
|
+
data
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def blacklist_rx
|
121
|
+
@blacklist_rx ||= Regexp.new(
|
122
|
+
options[:blacklist].split(/\s+/).join("|"),
|
123
|
+
Regexp::IGNORECASE
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
end # class Trailer
|
128
|
+
end # module Audit
|
129
|
+
end # module Startback
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Startback
|
2
|
+
module Caching
|
3
|
+
#
|
4
|
+
# A overriable caching abstraction aiming at making Entity-based caching easy.
|
5
|
+
#
|
6
|
+
# This class MUST be overriden:
|
7
|
+
#
|
8
|
+
# * the `load_entity` protected method MUST be implemented to load data from
|
9
|
+
# a primary & context unaware key.
|
10
|
+
#
|
11
|
+
# * the `primary_key` protected method MAY be implemented to convert candidate
|
12
|
+
# keys (received from ultimate callers) to primary keys. The method is also
|
13
|
+
# a good place to check and/or log the keys actually used by callers.
|
14
|
+
#
|
15
|
+
# * the `context_free_key` protected method MAY be overriden to provide
|
16
|
+
# domain unrelated caching keys from primary keys, e.g. by encoding the
|
17
|
+
# context into the caching key itself, if needed.
|
18
|
+
#
|
19
|
+
# * the `valid?` protected method MAY be overriden to check validity of data
|
20
|
+
# extracted from the cache and force a refresh even if found.
|
21
|
+
#
|
22
|
+
# An EntityCache takes an actual store at construction. The object must meet the
|
23
|
+
# specification writtern in Store. The 'cache' ruby gem can be used in practice.
|
24
|
+
#
|
25
|
+
# Cache hits, outdated and miss are logged in debug, info, and info severity.
|
26
|
+
# The `cache_hit`, `cache_outdated`, `cache_miss` protected methods MAY be
|
27
|
+
# overriden to change that behavior.
|
28
|
+
#
|
29
|
+
class EntityCache
|
30
|
+
include Support::Robustness
|
31
|
+
|
32
|
+
class << self
|
33
|
+
|
34
|
+
# Default time to live, in seconds
|
35
|
+
attr_writer :default_ttl
|
36
|
+
|
37
|
+
def default_ttl
|
38
|
+
@default_ttl || (superclass.respond_to?(:default_ttl, true) && superclass.default_ttl) || 3600
|
39
|
+
end
|
40
|
+
|
41
|
+
end # class DSL
|
42
|
+
|
43
|
+
def initialize(store, context = nil)
|
44
|
+
@store = store
|
45
|
+
@context = context
|
46
|
+
end
|
47
|
+
attr_reader :store, :context
|
48
|
+
|
49
|
+
# Returns the entity corresponding to a given key.
|
50
|
+
#
|
51
|
+
# If the entity is not in cache, loads it and puts it in cache using
|
52
|
+
# the caching options passed as second parameter.
|
53
|
+
def get(candidate_key, caching_options = default_caching_options)
|
54
|
+
pkey = primary_key(candidate_key)
|
55
|
+
cache_key = encode_key(context_free_key(pkey))
|
56
|
+
if store.exist?(cache_key)
|
57
|
+
cached = store.get(cache_key)
|
58
|
+
if valid?(pkey, cached)
|
59
|
+
cache_hit(pkey, cached)
|
60
|
+
return cached
|
61
|
+
else
|
62
|
+
cache_outdated(pkey, cached)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
cache_miss(pkey)
|
66
|
+
load_entity(pkey).tap{|to_cache|
|
67
|
+
store.set(cache_key, to_cache, caching_options)
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
# Invalidates the cache under a given key.
|
72
|
+
def invalidate(candidate_key)
|
73
|
+
pkey = primary_key(candidate_key)
|
74
|
+
cache_key = encode_key(context_free_key(pkey))
|
75
|
+
store.delete(cache_key)
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
|
80
|
+
def cache_hit(pkey, cached)
|
81
|
+
log(:debug, self, "cache_hit", context, op_data: pkey)
|
82
|
+
end
|
83
|
+
|
84
|
+
def cache_outdated(pkey, cached)
|
85
|
+
log(:info, self, "cache_outdated", context, op_data: pkey)
|
86
|
+
end
|
87
|
+
|
88
|
+
def cache_miss(pkey)
|
89
|
+
log(:info, self, "cache_miss", context, op_data: pkey)
|
90
|
+
end
|
91
|
+
|
92
|
+
def default_caching_options
|
93
|
+
{ ttl: self.class.default_ttl }
|
94
|
+
end
|
95
|
+
|
96
|
+
# Converts a candidate key to a primary key, so as to prevent
|
97
|
+
# cache duplicates if callers are allowed to request an entity
|
98
|
+
# through various keys.
|
99
|
+
#
|
100
|
+
# The default implementation returns the candidate key and MAY
|
101
|
+
# be overriden.
|
102
|
+
def primary_key(candidate_key)
|
103
|
+
candidate_key
|
104
|
+
end
|
105
|
+
|
106
|
+
# Encodes a context free key to an actual cache key.
|
107
|
+
#
|
108
|
+
# Default implementation uses JSON.fast_generate but MAY be
|
109
|
+
# overriden.
|
110
|
+
def encode_key(context_free_key)
|
111
|
+
JSON.fast_generate(context_free_key)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns whether `cached` entity seems fresh enough to
|
115
|
+
# be returned as a cache hit.
|
116
|
+
#
|
117
|
+
# This method provides a way to check freshness using, e.g.
|
118
|
+
# `updated_at` or `etag` kind of entity fields. The default
|
119
|
+
# implementation returns true and MAY be overriden.
|
120
|
+
def valid?(primary_key, cached)
|
121
|
+
true
|
122
|
+
end
|
123
|
+
|
124
|
+
# Converts a primary_key to a context_free_key, using the
|
125
|
+
# context (instance variable) to encode the context itself
|
126
|
+
# into the actual cache key.
|
127
|
+
#
|
128
|
+
# The default implementation simply returns the primary key
|
129
|
+
# and MAY be overriden.
|
130
|
+
def context_free_key(primary_key)
|
131
|
+
full_key(primary_key)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Deprecated, will be removed in 0.6.0. Use context_free_key
|
135
|
+
# instead.
|
136
|
+
def full_key(primary_key)
|
137
|
+
primary_key
|
138
|
+
end
|
139
|
+
|
140
|
+
# Actually loads the entity using the given primary key, and
|
141
|
+
# possibly the cache context.
|
142
|
+
#
|
143
|
+
# This method MUST be implemented and raises a NotImplementedError
|
144
|
+
# by default.
|
145
|
+
def load_entity(primary_key)
|
146
|
+
load_raw_data(primary_key)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Deprecated, will be removed in 0.6.0. Use load_entity
|
150
|
+
# instead.
|
151
|
+
def load_raw_data(*args, &bl)
|
152
|
+
raise NotImplementedError, "#{self.class.name}#load_entity"
|
153
|
+
end
|
154
|
+
|
155
|
+
end # class EntityCache
|
156
|
+
end # module Caching
|
157
|
+
end # module Startback
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Startback
|
2
|
+
module Caching
|
3
|
+
#
|
4
|
+
# Caching store implementation that caches nothing at all.
|
5
|
+
#
|
6
|
+
class NoStore
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
end
|
10
|
+
|
11
|
+
def exist?(key)
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(key)
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def set(key, value, ttl)
|
20
|
+
value
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete(key)
|
24
|
+
end
|
25
|
+
|
26
|
+
end # class NoStore
|
27
|
+
end # module Caching
|
28
|
+
end # module Startback
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Startback
|
2
|
+
module Caching
|
3
|
+
#
|
4
|
+
# Caching store specification & dummy implementation.
|
5
|
+
#
|
6
|
+
# This class should not be used in real project, as it implements
|
7
|
+
# See the 'cache' gem that provides conforming implementations.
|
8
|
+
#
|
9
|
+
class Store
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@saved = {}
|
13
|
+
end
|
14
|
+
attr_reader :saved
|
15
|
+
|
16
|
+
def exist?(key)
|
17
|
+
saved.has_key?(key)
|
18
|
+
end
|
19
|
+
|
20
|
+
def get(key)
|
21
|
+
saved[key]
|
22
|
+
end
|
23
|
+
|
24
|
+
def set(key, value, ttl)
|
25
|
+
saved[key] = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(key)
|
29
|
+
saved.delete(key)
|
30
|
+
end
|
31
|
+
|
32
|
+
end # class Store
|
33
|
+
end # module Caching
|
34
|
+
end # module Startback
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Startback
|
2
|
+
class Context
|
3
|
+
module HFactory
|
4
|
+
|
5
|
+
def h(hash)
|
6
|
+
h_factor!(self.new, hash)
|
7
|
+
end
|
8
|
+
|
9
|
+
def h_factor!(context, hash)
|
10
|
+
h_factories.each do |f|
|
11
|
+
f.call(context, hash)
|
12
|
+
end
|
13
|
+
context
|
14
|
+
end
|
15
|
+
|
16
|
+
def h_factories
|
17
|
+
@h_factories ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
def h_factory(&factory)
|
21
|
+
h_factories << factory
|
22
|
+
end
|
23
|
+
|
24
|
+
###
|
25
|
+
|
26
|
+
def h_dump!(context, hash = {})
|
27
|
+
h_dumpers.each do |d|
|
28
|
+
context.instance_exec(hash, &d)
|
29
|
+
end
|
30
|
+
hash
|
31
|
+
end
|
32
|
+
|
33
|
+
def h_dumpers
|
34
|
+
@h_dumpers ||= []
|
35
|
+
end
|
36
|
+
|
37
|
+
def h_dump(&dumper)
|
38
|
+
h_dumpers << dumper
|
39
|
+
end
|
40
|
+
|
41
|
+
end # module HFactory
|
42
|
+
end # class Context
|
43
|
+
end # module Startback
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Startback
|
2
|
+
class Context
|
3
|
+
#
|
4
|
+
# Rack middleware that installs a particular context instance
|
5
|
+
# on the Rack environment.
|
6
|
+
#
|
7
|
+
# Examples:
|
8
|
+
#
|
9
|
+
# # Use the default context class
|
10
|
+
# Rack::Builder.new do
|
11
|
+
# use Startback::Context::Middleware
|
12
|
+
#
|
13
|
+
# run ->(env){
|
14
|
+
# ctx = env[Startback::Context::Middleware::RACK_ENV_KEY]
|
15
|
+
# ctx.is_a?(Startback::Context) # => true
|
16
|
+
# }
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # Use a user defined context class
|
20
|
+
# Rack::Builder.new do
|
21
|
+
# use Startback::Context::Middleware, MyContextClass.new
|
22
|
+
#
|
23
|
+
# run ->(env){
|
24
|
+
# ctx = env[Startback::Context::Middleware::RACK_ENV_KEY]
|
25
|
+
# ctx.is_a?(MyContextClass) # => true (your subclass)
|
26
|
+
# ctx.is_a?(Startback::Context) # => true (required!)
|
27
|
+
# }
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
class Middleware
|
31
|
+
|
32
|
+
RACK_ENV_KEY = 'SAMBACK_CONTEXT'
|
33
|
+
|
34
|
+
def initialize(app, context = Context.new)
|
35
|
+
@app = app
|
36
|
+
@context = context
|
37
|
+
end
|
38
|
+
attr_reader :context
|
39
|
+
|
40
|
+
def call(env)
|
41
|
+
env[RACK_ENV_KEY] ||= context.dup.tap{|c|
|
42
|
+
c.original_rack_env = env.dup
|
43
|
+
}
|
44
|
+
@app.call(env)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.context(env)
|
48
|
+
env[RACK_ENV_KEY]
|
49
|
+
end
|
50
|
+
|
51
|
+
end # class Middleware
|
52
|
+
end # class Context
|
53
|
+
end # module Startback
|