rodbot 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|