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.
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