rodbot 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +1 -0
  3. data/CHANGELOG.md +14 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +653 -0
  6. data/exe/rodbot +7 -0
  7. data/lib/roda/plugins/rodbot.rb +36 -0
  8. data/lib/rodbot/async.rb +45 -0
  9. data/lib/rodbot/cli/command.rb +25 -0
  10. data/lib/rodbot/cli/commands/console.rb +23 -0
  11. data/lib/rodbot/cli/commands/credentials.rb +19 -0
  12. data/lib/rodbot/cli/commands/deploy.rb +20 -0
  13. data/lib/rodbot/cli/commands/new.rb +21 -0
  14. data/lib/rodbot/cli/commands/simulator.rb +17 -0
  15. data/lib/rodbot/cli/commands/start.rb +26 -0
  16. data/lib/rodbot/cli/commands/stop.rb +15 -0
  17. data/lib/rodbot/cli/commands/version.rb +15 -0
  18. data/lib/rodbot/cli/commands.rb +18 -0
  19. data/lib/rodbot/cli.rb +9 -0
  20. data/lib/rodbot/config.rb +157 -0
  21. data/lib/rodbot/constants.rb +13 -0
  22. data/lib/rodbot/db/hash.rb +71 -0
  23. data/lib/rodbot/db/redis.rb +61 -0
  24. data/lib/rodbot/db.rb +91 -0
  25. data/lib/rodbot/dispatcher.rb +125 -0
  26. data/lib/rodbot/env.rb +48 -0
  27. data/lib/rodbot/error.rb +19 -0
  28. data/lib/rodbot/generator.rb +108 -0
  29. data/lib/rodbot/log.rb +67 -0
  30. data/lib/rodbot/memoize.rb +86 -0
  31. data/lib/rodbot/plugins/github_webhook/README.github_webhook.md +42 -0
  32. data/lib/rodbot/plugins/github_webhook/app.rb +46 -0
  33. data/lib/rodbot/plugins/gitlab_webhook/README.gitlab_webhook.md +40 -0
  34. data/lib/rodbot/plugins/gitlab_webhook/app.rb +40 -0
  35. data/lib/rodbot/plugins/hal/README.hal.md +15 -0
  36. data/lib/rodbot/plugins/hal/app.rb +22 -0
  37. data/lib/rodbot/plugins/matrix/README.matrix.md +42 -0
  38. data/lib/rodbot/plugins/matrix/relay.rb +113 -0
  39. data/lib/rodbot/plugins/otp/README.otp.md +82 -0
  40. data/lib/rodbot/plugins/otp/app.rb +47 -0
  41. data/lib/rodbot/plugins/word_of_the_day/README.word_of_the_day.md +13 -0
  42. data/lib/rodbot/plugins/word_of_the_day/schedule.rb +51 -0
  43. data/lib/rodbot/plugins.rb +81 -0
  44. data/lib/rodbot/rack.rb +50 -0
  45. data/lib/rodbot/refinements.rb +118 -0
  46. data/lib/rodbot/relay.rb +104 -0
  47. data/lib/rodbot/services/app.rb +72 -0
  48. data/lib/rodbot/services/relay.rb +37 -0
  49. data/lib/rodbot/services/schedule.rb +29 -0
  50. data/lib/rodbot/services.rb +32 -0
  51. data/lib/rodbot/simulator.rb +60 -0
  52. data/lib/rodbot/version.rb +5 -0
  53. data/lib/rodbot.rb +60 -0
  54. data/lib/templates/deploy/docker/compose.yaml.gerb +37 -0
  55. data/lib/templates/deploy/docker-split/compose.yaml.gerb +52 -0
  56. data/lib/templates/deploy/procfile/Procfile.gerb +1 -0
  57. data/lib/templates/deploy/procfile-split/Procfile.gerb +5 -0
  58. data/lib/templates/deploy/render/render.yaml.gerb +0 -0
  59. data/lib/templates/deploy/render-split/render.yaml.gerb +0 -0
  60. data/lib/templates/new/LICENSE.txt +22 -0
  61. data/lib/templates/new/README.md +4 -0
  62. data/lib/templates/new/app/app.rb +11 -0
  63. data/lib/templates/new/app/routes/help.rb +19 -0
  64. data/lib/templates/new/app/views/layout.erb +12 -0
  65. data/lib/templates/new/app/views/root.erb +5 -0
  66. data/lib/templates/new/config/rodbot.rb +8 -0
  67. data/lib/templates/new/config/schedule.rb +5 -0
  68. data/lib/templates/new/config.ru +3 -0
  69. data/lib/templates/new/gems.locked +104 -0
  70. data/lib/templates/new/gems.rb +15 -0
  71. data/lib/templates/new/guardfile.rb +9 -0
  72. data/lib/templates/new/public/assets/images/rodbot.avif +0 -0
  73. data/lib/templates/new/public/assets/stylesheets/base.css +18 -0
  74. data/lib/templates/new/rakefile.rb +28 -0
  75. data.tar.gz.sig +0 -0
  76. metadata +510 -0
  77. metadata.gz.sig +3 -0
@@ -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,15 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodbot
4
+ class CLI
5
+ module Commands
6
+ class Stop < Rodbot::CLI::Command
7
+ desc 'Stop Rodbot'
8
+
9
+ def rescued_call(**)
10
+ Rodbot::Services.new.interrupt
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodbot
4
+ class CLI
5
+ module Commands
6
+ class Version < Rodbot::CLI::Command
7
+ desc "Print version"
8
+
9
+ def rescued_call(**)
10
+ puts Rodbot::VERSION
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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,9 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'dry/cli'
4
+
5
+ module Rodbot
6
+ class CLI < Dry::CLI
7
+ end
8
+ end
9
+
@@ -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,13 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodbot
4
+ module Constants
5
+
6
+ # Service types
7
+ SERVICES = %i(app relay schedule).freeze
8
+
9
+ # Hosting types
10
+ HOSTINGS = %i(procfile docker render).freeze
11
+
12
+ end
13
+ 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