rodbot 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
- checksums.yaml.gz.sig +1 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE.txt +22 -0
- data/README.md +653 -0
- data/exe/rodbot +7 -0
- data/lib/roda/plugins/rodbot.rb +36 -0
- data/lib/rodbot/async.rb +45 -0
- data/lib/rodbot/cli/command.rb +25 -0
- data/lib/rodbot/cli/commands/console.rb +23 -0
- data/lib/rodbot/cli/commands/credentials.rb +19 -0
- data/lib/rodbot/cli/commands/deploy.rb +20 -0
- data/lib/rodbot/cli/commands/new.rb +21 -0
- data/lib/rodbot/cli/commands/simulator.rb +17 -0
- data/lib/rodbot/cli/commands/start.rb +26 -0
- data/lib/rodbot/cli/commands/stop.rb +15 -0
- data/lib/rodbot/cli/commands/version.rb +15 -0
- data/lib/rodbot/cli/commands.rb +18 -0
- data/lib/rodbot/cli.rb +9 -0
- data/lib/rodbot/config.rb +157 -0
- data/lib/rodbot/constants.rb +13 -0
- data/lib/rodbot/db/hash.rb +71 -0
- data/lib/rodbot/db/redis.rb +61 -0
- data/lib/rodbot/db.rb +91 -0
- data/lib/rodbot/dispatcher.rb +125 -0
- data/lib/rodbot/env.rb +48 -0
- data/lib/rodbot/error.rb +19 -0
- data/lib/rodbot/generator.rb +108 -0
- data/lib/rodbot/log.rb +67 -0
- data/lib/rodbot/memoize.rb +86 -0
- data/lib/rodbot/plugins/github_webhook/README.github_webhook.md +42 -0
- data/lib/rodbot/plugins/github_webhook/app.rb +46 -0
- data/lib/rodbot/plugins/gitlab_webhook/README.gitlab_webhook.md +40 -0
- data/lib/rodbot/plugins/gitlab_webhook/app.rb +40 -0
- data/lib/rodbot/plugins/hal/README.hal.md +15 -0
- data/lib/rodbot/plugins/hal/app.rb +22 -0
- data/lib/rodbot/plugins/matrix/README.matrix.md +42 -0
- data/lib/rodbot/plugins/matrix/relay.rb +113 -0
- data/lib/rodbot/plugins/otp/README.otp.md +82 -0
- data/lib/rodbot/plugins/otp/app.rb +47 -0
- data/lib/rodbot/plugins/word_of_the_day/README.word_of_the_day.md +13 -0
- data/lib/rodbot/plugins/word_of_the_day/schedule.rb +51 -0
- data/lib/rodbot/plugins.rb +81 -0
- data/lib/rodbot/rack.rb +50 -0
- data/lib/rodbot/refinements.rb +118 -0
- data/lib/rodbot/relay.rb +104 -0
- data/lib/rodbot/services/app.rb +72 -0
- data/lib/rodbot/services/relay.rb +37 -0
- data/lib/rodbot/services/schedule.rb +29 -0
- data/lib/rodbot/services.rb +32 -0
- data/lib/rodbot/simulator.rb +60 -0
- data/lib/rodbot/version.rb +5 -0
- data/lib/rodbot.rb +60 -0
- data/lib/templates/deploy/docker/compose.yaml.gerb +37 -0
- data/lib/templates/deploy/docker-split/compose.yaml.gerb +52 -0
- data/lib/templates/deploy/procfile/Procfile.gerb +1 -0
- data/lib/templates/deploy/procfile-split/Procfile.gerb +5 -0
- data/lib/templates/deploy/render/render.yaml.gerb +0 -0
- data/lib/templates/deploy/render-split/render.yaml.gerb +0 -0
- data/lib/templates/new/LICENSE.txt +22 -0
- data/lib/templates/new/README.md +4 -0
- data/lib/templates/new/app/app.rb +11 -0
- data/lib/templates/new/app/routes/help.rb +19 -0
- data/lib/templates/new/app/views/layout.erb +12 -0
- data/lib/templates/new/app/views/root.erb +5 -0
- data/lib/templates/new/config/rodbot.rb +8 -0
- data/lib/templates/new/config/schedule.rb +5 -0
- data/lib/templates/new/config.ru +3 -0
- data/lib/templates/new/gems.locked +104 -0
- data/lib/templates/new/gems.rb +15 -0
- data/lib/templates/new/guardfile.rb +9 -0
- data/lib/templates/new/public/assets/images/rodbot.avif +0 -0
- data/lib/templates/new/public/assets/stylesheets/base.css +18 -0
- data/lib/templates/new/rakefile.rb +28 -0
- data.tar.gz.sig +0 -0
- metadata +510 -0
- metadata.gz.sig +3 -0
data/lib/rodbot/async.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require 'sucker_punch'
|
4
|
+
|
5
|
+
module Rodbot
|
6
|
+
module Async
|
7
|
+
extend self
|
8
|
+
|
9
|
+
SuckerPunch.logger = Rodbot::Log.logger('async')
|
10
|
+
|
11
|
+
# Perform code asynchronously
|
12
|
+
#
|
13
|
+
# In order not to interfere with tests, the code is performed synchronously
|
14
|
+
# in case the current env is "test"!
|
15
|
+
#
|
16
|
+
# @example with block
|
17
|
+
# Environment.async do
|
18
|
+
# some_heavy_number_crunching
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# @example with proc
|
22
|
+
# Environment.async(-> { some_heavy_number_crunching })
|
23
|
+
#
|
24
|
+
# @param proc [Proc] either pass a proc to perform...
|
25
|
+
# @yield ...or yield the code to perform (ignored if a proc is given)
|
26
|
+
def perform(&block)
|
27
|
+
if Rodbot.env.test?
|
28
|
+
block.call
|
29
|
+
else
|
30
|
+
Job.perform_async(block)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Job
|
35
|
+
include SuckerPunch::Job
|
36
|
+
|
37
|
+
# Generic job which simply calls a proc
|
38
|
+
#
|
39
|
+
# @param proc [Proc] proc to call
|
40
|
+
def perform(proc)
|
41
|
+
proc.call
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodbot
|
4
|
+
class CLI
|
5
|
+
class Command < Dry::CLI::Command
|
6
|
+
option :backtrace, type: :boolean, default: false, desc: "Dump backtrace on errors"
|
7
|
+
|
8
|
+
def call(backtrace:, **args)
|
9
|
+
rescued_call(**args)
|
10
|
+
rescue => error
|
11
|
+
error(error.message) do
|
12
|
+
raise error if backtrace
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def error(message)
|
19
|
+
STDERR.puts "ERROR: command failed: #{message}"
|
20
|
+
yield if block_given?
|
21
|
+
exit 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
using Rodbot::Refinements
|
4
|
+
|
5
|
+
module Rodbot
|
6
|
+
class CLI
|
7
|
+
module Commands
|
8
|
+
class Console < Rodbot::CLI::Command
|
9
|
+
desc 'Start the Rodbot console'
|
10
|
+
|
11
|
+
def rescued_call(**)
|
12
|
+
Rodbot.boot
|
13
|
+
Rodbot::SERVICES.each { "rodbot/services/#{_1}".constantize }
|
14
|
+
require 'irb'
|
15
|
+
IRB.setup nil
|
16
|
+
IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
|
17
|
+
require 'irb/ext/multi-irb'
|
18
|
+
IRB.irb nil, Rodbot
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodbot
|
4
|
+
class CLI
|
5
|
+
module Commands
|
6
|
+
class Credentials < Rodbot::CLI::Command
|
7
|
+
desc 'Edit the credentials for ENVIRONMENT'
|
8
|
+
argument :environment, values: Rodbot::Env::ENVS, desc: 'Which environment to edit', required: true
|
9
|
+
example [
|
10
|
+
'development'
|
11
|
+
]
|
12
|
+
|
13
|
+
def rescued_call(environment:, **)
|
14
|
+
Rodbot.credentials.edit! environment
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodbot
|
4
|
+
class CLI
|
5
|
+
module Commands
|
6
|
+
class Deploy < Rodbot::CLI::Command
|
7
|
+
desc 'Print the deploy configuration'
|
8
|
+
argument :hosting, values: Rodbot::HOSTINGS, required: true, desc: 'Which hosting to use'
|
9
|
+
option :split, type: :boolean, default: false, desc: "Whether to split into individual services"
|
10
|
+
|
11
|
+
def rescued_call(hosting:, split:, **)
|
12
|
+
dir = [hosting, ('split' if split)].compact.join('-')
|
13
|
+
Rodbot::Generator
|
14
|
+
.new(Rodbot.env.gem.join('lib', 'templates', 'deploy', dir))
|
15
|
+
.display
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodbot
|
4
|
+
class CLI
|
5
|
+
module Commands
|
6
|
+
class New < Rodbot::CLI::Command
|
7
|
+
desc 'Create a new Rodbot scaffold in PATH'
|
8
|
+
argument :path, required: true, desc: 'Root directory of the new bot'
|
9
|
+
example [
|
10
|
+
'my_awesome_bot'
|
11
|
+
]
|
12
|
+
|
13
|
+
def rescued_call(path:, **)
|
14
|
+
Rodbot::Generator
|
15
|
+
.new(Rodbot.env.gem.join('lib', 'templates', 'new'))
|
16
|
+
.write(Pathname(path))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodbot
|
4
|
+
class CLI
|
5
|
+
module Commands
|
6
|
+
class Simulator < Rodbot::CLI::Command
|
7
|
+
desc 'Launch the chat simulator'
|
8
|
+
option :sender, default: 'simulator', desc: "Sender to mimick"
|
9
|
+
option :raw, type: :boolean, default: false, desc: "Whether to display raw Markdown"
|
10
|
+
|
11
|
+
def rescued_call(sender:, raw:, **)
|
12
|
+
Rodbot::Simulator.new(sender, raw: raw).run
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodbot
|
4
|
+
class CLI
|
5
|
+
module Commands
|
6
|
+
class Start < Rodbot::CLI::Command
|
7
|
+
desc 'Start Rodbot or parts of it'
|
8
|
+
argument :service, values: Rodbot::SERVICES, desc: 'Which service to start or all by default'
|
9
|
+
argument :extension, desc: 'Which service extension to start or all by default (only if SERVICE is relay)'
|
10
|
+
option :daemonize, type: :boolean, desc: "Whether to daemonize and supervise processes, default: true (all services) or false (one service)"
|
11
|
+
option :debugger, type: :boolean, default: false, desc: "Whether to load the debugger"
|
12
|
+
|
13
|
+
def rescued_call(service: nil, extension: nil, daemonize: false, debugger: false, **)
|
14
|
+
require 'debug' if debugger
|
15
|
+
daemonize = true unless service
|
16
|
+
Rodbot::Services.new.then do |services|
|
17
|
+
(service ? [service] : Rodbot::SERVICES).each do |service|
|
18
|
+
services.register(service, extension: extension)
|
19
|
+
end
|
20
|
+
services.run(daemonize: daemonize)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodbot
|
4
|
+
class CLI
|
5
|
+
module Commands
|
6
|
+
extend Dry::CLI::Registry
|
7
|
+
|
8
|
+
register 'credentials', Credentials, aliases: %w(cred)
|
9
|
+
register 'deploy', Deploy
|
10
|
+
register 'new', New
|
11
|
+
register 'simulator', Simulator, aliases: %w(sim)
|
12
|
+
register 'start', Start, aliases: %w(up)
|
13
|
+
register 'stop', Stop, aliases: %w(down)
|
14
|
+
register 'console', Console, aliases: %w(c)
|
15
|
+
register 'version', Version, aliases: %w(-v --version)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/rodbot/cli.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodbot
|
4
|
+
|
5
|
+
# Simple yet flexible configuration module
|
6
|
+
#
|
7
|
+
# The configuration is defined in Ruby as follows:
|
8
|
+
#
|
9
|
+
# name 'Bot'
|
10
|
+
# country 'Sweden'
|
11
|
+
# country nil
|
12
|
+
# log do
|
13
|
+
# level 3
|
14
|
+
# end
|
15
|
+
# plugin :matrix do
|
16
|
+
# version 1
|
17
|
+
# ssl true
|
18
|
+
# end
|
19
|
+
# plugin :slack do
|
20
|
+
# version 2
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# Within Rodbot, you should use the +Rodbot.config+ shortcut to access the
|
24
|
+
# configuration.
|
25
|
+
#
|
26
|
+
# file = File.new('config/rodbot.rb')
|
27
|
+
# rc = Rodbot::Config.new(file.read)
|
28
|
+
# rc.config(:name) # => 'Bot'
|
29
|
+
# rc.config(:country) # => nil
|
30
|
+
# rc.config(:undefined) # => nil
|
31
|
+
# rc.config(:log) # => { level: 3 }
|
32
|
+
# rc.config(:plugin, :matrix, :version)
|
33
|
+
# # => 1
|
34
|
+
# rc.config(:plugin, :matrix)
|
35
|
+
# # => { version: 1, ssl: true }
|
36
|
+
# rc.config(:plugin)
|
37
|
+
# # => { matrix: { version: 1, ssl: true }, slack: { version: 2 } }
|
38
|
+
# rc.config
|
39
|
+
# # => { name: 'Bot', country: nil, plugin: { matrix: { version: 1, ssl: true }, slack: { version: 2 } } }
|
40
|
+
#
|
41
|
+
# There are two types configuration items:
|
42
|
+
#
|
43
|
+
# 1. Object values without block like +name 'Bot'+:<br>The config key +:name+
|
44
|
+
# gets the object +'Bot'+ assigned. Subsequent assignments with the same
|
45
|
+
# config key overwrite previous assignments.
|
46
|
+
# 2. Unspecified value with a block like +log do+:<br>The config key +:log+ is
|
47
|
+
# assigned a hash defined by the block. Subsequent assignments with the
|
48
|
+
# same config key are merged into the hash.
|
49
|
+
# 3. Object values with a block like +plugin :matrix do+:<br>The config key
|
50
|
+
# +:plugin+ is assigned an empty hash which is then populated with the
|
51
|
+
# object `:matrix` (usually a Symbol) as key and the subtree defined by the
|
52
|
+
# block. Subsequent assignments with the same config key add more keys to
|
53
|
+
# this hash.
|
54
|
+
#
|
55
|
+
# Please note: You can force a config key to always be treated as if it had
|
56
|
+
# a block (type 3) by adding it to the +KEYS_WITH_IMPLICIT_BLOCK+ array.
|
57
|
+
#
|
58
|
+
# Defaults set by the +DEFAULTS+ constant are read first and therefore may be
|
59
|
+
# overwritten or extend as mentioned above.
|
60
|
+
class Config
|
61
|
+
|
62
|
+
# Keys which are always treated as if they had a block even if they don't
|
63
|
+
KEYS_WITH_IMPLICIT_BLOCK = %i(plugin).freeze
|
64
|
+
|
65
|
+
# Default configuration
|
66
|
+
DEFAULTS = <<~END
|
67
|
+
name 'Rodbot'
|
68
|
+
port 7200
|
69
|
+
timezone 'Etc/UTC'
|
70
|
+
db 'hash'
|
71
|
+
app do
|
72
|
+
threads Rodbot.env.development? ? (1..1) : (2..4)
|
73
|
+
end
|
74
|
+
log do
|
75
|
+
to STDOUT
|
76
|
+
level Rodbot.env.development? ? Logger::INFO : Logger::ERROR
|
77
|
+
end
|
78
|
+
END
|
79
|
+
|
80
|
+
# Read configuration from file
|
81
|
+
#
|
82
|
+
# @param source [String] config source e.g. read from +config/rodbot.rb+
|
83
|
+
# @param defaults [Boolean] whether to load the defaults or not
|
84
|
+
# @return [self]
|
85
|
+
def initialize(source, defaults: true)
|
86
|
+
@config = Reader.new.eval_strings((DEFAULTS if defaults), source).to_h
|
87
|
+
end
|
88
|
+
|
89
|
+
# Get config values and subtrees
|
90
|
+
#
|
91
|
+
# @note Use the +Rodbot.config+ shortcut to access this method!
|
92
|
+
#
|
93
|
+
# @param keys [Array] key path to config subtree or value
|
94
|
+
# @return [Object] config subtree or value
|
95
|
+
def config(*keys)
|
96
|
+
return @config if keys.none?
|
97
|
+
value = @config.dig(*keys)
|
98
|
+
if value.instance_of?(Array) && value.count == 1
|
99
|
+
value.first
|
100
|
+
else
|
101
|
+
value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class Reader
|
106
|
+
def initialize
|
107
|
+
@hash = {}
|
108
|
+
end
|
109
|
+
|
110
|
+
# Eval configuration from strings
|
111
|
+
#
|
112
|
+
# @param strings [String, nil] one or more strings to evaluate
|
113
|
+
# @return [self]
|
114
|
+
def eval_strings(*strings)
|
115
|
+
instance_eval(strings.compact.join("\n"))
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
# Eval configuration from block
|
120
|
+
#
|
121
|
+
# @yield block to evaluate
|
122
|
+
# @return [self]
|
123
|
+
def eval_block(&block)
|
124
|
+
instance_eval(&block) if block
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
# Set an config value
|
129
|
+
#
|
130
|
+
# @param key [Symbol] config key
|
131
|
+
# @param value [Object, nil] config value
|
132
|
+
# @yield optional block containing nested config
|
133
|
+
# @return [self]
|
134
|
+
def method_missing(key, value=nil, *, &block)
|
135
|
+
case
|
136
|
+
when block && value.nil?
|
137
|
+
@hash[key] ||= {}
|
138
|
+
@hash[key].merge! self.class.new.eval_block(&block).to_h
|
139
|
+
when block || KEYS_WITH_IMPLICIT_BLOCK.include?(key)
|
140
|
+
@hash[key] ||= {}
|
141
|
+
@hash[key][value] = self.class.new.eval_block(&block).to_h
|
142
|
+
else
|
143
|
+
@hash[key] = value
|
144
|
+
end
|
145
|
+
self
|
146
|
+
end
|
147
|
+
|
148
|
+
# Config hash
|
149
|
+
#
|
150
|
+
# @return [Hash]
|
151
|
+
def to_h
|
152
|
+
@hash
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodbot
|
4
|
+
class Db
|
5
|
+
|
6
|
+
# Database adapter for Hash
|
7
|
+
#
|
8
|
+
# @example Enable in config/rodbot.rb
|
9
|
+
# db 'hash'
|
10
|
+
#
|
11
|
+
# This database is for development and testing only, it is not thread-safe
|
12
|
+
# and therefore should not be used in production.
|
13
|
+
module Hash
|
14
|
+
PRUNE_THRESHOLD = 100
|
15
|
+
|
16
|
+
def set(*key, expires_in: nil, &block)
|
17
|
+
prune
|
18
|
+
block.call((get(*key) unless block.arity.zero?)).tap do |value|
|
19
|
+
db[key.join(':')] = [
|
20
|
+
serialize(value),
|
21
|
+
(epoch + expires_in if expires_in)
|
22
|
+
]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def get(*key)
|
27
|
+
value, expires_at = db[skey(*key)]
|
28
|
+
deserialize(value) if value && (!expires_at || epoch < expires_at)
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete(*key)
|
32
|
+
value, expires_at = db.delete(skey(*key))
|
33
|
+
deserialize(value) if value && (!expires_at || epoch < expires_at)
|
34
|
+
end
|
35
|
+
|
36
|
+
def scan(*key)
|
37
|
+
re = /\A#{skey(*key).sub(/\*\z/, '')}/
|
38
|
+
db.keys.select { _1.match? re }
|
39
|
+
end
|
40
|
+
|
41
|
+
def flush
|
42
|
+
@db = {}
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def db
|
49
|
+
@db ||= {}
|
50
|
+
end
|
51
|
+
|
52
|
+
def skey(*key)
|
53
|
+
key.join(':')
|
54
|
+
end
|
55
|
+
|
56
|
+
def epoch
|
57
|
+
Time.now.to_f
|
58
|
+
end
|
59
|
+
|
60
|
+
def prune
|
61
|
+
@counter ||= 0
|
62
|
+
if (@counter += 1) > PRUNE_THRESHOLD
|
63
|
+
cached_epoch = epoch
|
64
|
+
db.delete_if { _2.last < cached_epoch }
|
65
|
+
@counter = 1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodbot
|
4
|
+
class Db
|
5
|
+
|
6
|
+
# Database adapter for Redis
|
7
|
+
#
|
8
|
+
# All keys are implicitly nested inside the "rodbot:..." namespace to allow
|
9
|
+
# using one and the same Redis db for more than just Rodbot.
|
10
|
+
#
|
11
|
+
# @example Enable in config/rodbot.rb
|
12
|
+
# db 'redis://localhost:6379/10'
|
13
|
+
module Redis
|
14
|
+
include Rodbot::Memoize
|
15
|
+
|
16
|
+
def self.extended(*)
|
17
|
+
require 'redis'
|
18
|
+
end
|
19
|
+
|
20
|
+
def set(*key, expires_in: nil, &block)
|
21
|
+
block.call((get(*key) unless block.arity.zero?)).tap do |value|
|
22
|
+
db.set(skey(*key), serialize(value), ex: expires_in)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def get(*key)
|
27
|
+
deserialize(db.get(skey(*key)))
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete(*key)
|
31
|
+
get(*key).tap do
|
32
|
+
db.del(skey(*key))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def scan(*key)
|
37
|
+
cursor, result = 0, []
|
38
|
+
loop do
|
39
|
+
cursor, keys = db.scan(cursor, match: skey(*key))
|
40
|
+
result.append(*keys)
|
41
|
+
break result if cursor == '0'
|
42
|
+
end.map { _1[7..] }
|
43
|
+
end
|
44
|
+
|
45
|
+
def flush
|
46
|
+
db.flushdb
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
memoize def db
|
53
|
+
::Redis.new(url: url)
|
54
|
+
end
|
55
|
+
|
56
|
+
def skey(*key)
|
57
|
+
key.prepend('rodbot').join(':')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/rodbot/db.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
using Rodbot::Refinements
|
6
|
+
|
7
|
+
module Rodbot
|
8
|
+
|
9
|
+
# Uniform key/value database interface for various backends
|
10
|
+
#
|
11
|
+
# Keys can be namespaced using the colon as separator:
|
12
|
+
#
|
13
|
+
# * 'color' - color key without namespace
|
14
|
+
# * 'bike:color' - color key within the bike namespace
|
15
|
+
# * 'vehicle:bike:color' - color key within... you get the idea
|
16
|
+
#
|
17
|
+
# Furthermore, keys can be entered either as colon separated keys or as
|
18
|
+
# list of symbols. The following are therefore equivalent:
|
19
|
+
#
|
20
|
+
# Rodbot.db.get('vehicle:bike:color')
|
21
|
+
# Rodbot.db.get(:vehicle, :bike, :color)
|
22
|
+
#
|
23
|
+
# Same goes for the star wildcard which is only allowed last:
|
24
|
+
#
|
25
|
+
# Rodbot.db.scan('vehicle:*')
|
26
|
+
# Rodbot.db.scan(:vehicle, :*)
|
27
|
+
#
|
28
|
+
# The interface is simple and straightforward:
|
29
|
+
#
|
30
|
+
# @example Set a value
|
31
|
+
# Rodbot.db.set(:name) { 'John' }
|
32
|
+
#
|
33
|
+
# @example Set a value which expires after 30 seconds
|
34
|
+
# Rodbot.db.set(:name, expires_in: 30) { 'John' }
|
35
|
+
#
|
36
|
+
# @example Replace an existing value
|
37
|
+
# Rodbot.db.set(:name) { 'John' }
|
38
|
+
# Rodbot.db.set(:name) { 'Bob' } # replaces John with Bob
|
39
|
+
#
|
40
|
+
# @example Update an existing value
|
41
|
+
# Rodbot.db.set(:name) { 'John' }
|
42
|
+
# Rodbot.db.set(:name) { |n| "#{n} Doe" } # replaces John with John Doe
|
43
|
+
#
|
44
|
+
# @example Get a value
|
45
|
+
# Rodbot.db.set(:name) { 'John' }
|
46
|
+
# Rodbot.db.get(:name) # => 'John'
|
47
|
+
#
|
48
|
+
# @example Delete a value
|
49
|
+
# Rodbot.db.set(:name) { 'John' }
|
50
|
+
# Rodbot.db.delete(:name) # => 'John'
|
51
|
+
# Rodbot.db.get(:name) # => nil
|
52
|
+
#
|
53
|
+
# @example Scan for keys
|
54
|
+
# Rodbot.db.set(:first_name) { 'John' }
|
55
|
+
# Rodbot.db.set(:last_name) { 'Doe' }
|
56
|
+
# Rodbot.db.scan(:*) # => [:first_name, :last_name]
|
57
|
+
#
|
58
|
+
# @example Delete all keys
|
59
|
+
# Rodbot.db.flush
|
60
|
+
#
|
61
|
+
# Please note that {JSON} is used to serialize which has an influence on
|
62
|
+
# what you get back after deserialization:
|
63
|
+
#
|
64
|
+
# * Primitive types such as String, Integer or TrueClass are preserved.
|
65
|
+
# * Any other object is converted to String, incuding...
|
66
|
+
# * Symbol values are converted to String
|
67
|
+
# * Symbol elements in an array are converted to String
|
68
|
+
# * Symbol values in a hash are converted to String
|
69
|
+
#
|
70
|
+
# However, hash keys are always converted to Symbol - mainly because Rubyland
|
71
|
+
# favours Symbol keys over String keys for visual reasons.
|
72
|
+
class Db
|
73
|
+
attr_reader :url
|
74
|
+
|
75
|
+
# @param url [String] connection URL of the backend
|
76
|
+
def initialize(url)
|
77
|
+
@url = url
|
78
|
+
extend "rodbot/db/#{url.split('://').first}".constantize
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def serialize(object)
|
84
|
+
object.to_json
|
85
|
+
end
|
86
|
+
|
87
|
+
def deserialize(string)
|
88
|
+
JSON.parse(string, symbolize_names: true) if string
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|