startback-websocket 0.14.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/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
|