slimer 0.1.1

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.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "slimer"
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "slimer/version"
4
+
5
+ require_relative "slimer/database"
6
+ require_relative "slimer/group_configurator"
7
+
8
+ require "logger"
9
+
10
+ # @abstract
11
+ module Slimer
12
+ class Error < StandardError; end
13
+
14
+ DEFAULT_GROUP = "general"
15
+ DEFAULT_DATABASE_URL = "sqlite://./slimer.db"
16
+ DEFAULT_SIDEKIQ_QUEUE = "slimer"
17
+
18
+ DEFAULTS = {
19
+ groups: Set.new([DEFAULT_GROUP]),
20
+ database_url: DEFAULT_DATABASE_URL,
21
+ sidekiq_queue: DEFAULT_SIDEKIQ_QUEUE
22
+ }.freeze
23
+
24
+ def self.options
25
+ @options ||= DEFAULTS.dup
26
+ end
27
+
28
+ def self.options=(opts)
29
+ @options = DEFAULTS.merge(opts)
30
+ end
31
+
32
+ def self.db
33
+ database_url_from_env = ENV.delete("SLIMER_DATABASE_URL") || ENV.delete("DATABASE_URL")
34
+
35
+ if database_url_from_env
36
+ @options[:database_url] = database_url_from_env
37
+ return @db = Database.connection(
38
+ options[:database_url] || DEFAULT_DATABASE_URL
39
+ )
40
+ end
41
+
42
+ @db ||= Database.connection(
43
+ options[:database_url] || DEFAULT_DATABASE_URL
44
+ )
45
+ end
46
+
47
+ def self.groups(new_groups = nil)
48
+ self.groups = new_groups if new_groups
49
+ options[:groups]
50
+ end
51
+
52
+ def self.groups=(new_groups = Set.new(options[:groups].dup))
53
+ @options[:groups] = Set.new(new_groups.map(&:to_s))
54
+ end
55
+
56
+ def self.group(new_group, &block)
57
+ options[:groups] += GroupConfigurator.group(new_group, &block).all
58
+ end
59
+
60
+ def self.database_url(db_url = nil)
61
+ self.database_url = db_url if db_url
62
+ options[:database_url]
63
+ end
64
+
65
+ def self.database_url=(db_url)
66
+ @options[:database_url] = db_url
67
+ end
68
+
69
+ def self.sidekiq_queue(new_queue = nil)
70
+ self.sidekiq_queue = new_queue if new_queue
71
+ options[:sidekiq_queue]
72
+ end
73
+
74
+ def self.sidekiq_queue=(new_queue)
75
+ @options[:sidekiq_queue] = new_queue
76
+ end
77
+
78
+ def self.configure
79
+ yield self
80
+ end
81
+
82
+ def self.configure_sidekiq_client(&block)
83
+ Sidekiq.configure_client(&block)
84
+ end
85
+
86
+ def self.configure_sidekiq_server(&block)
87
+ Sidekiq.configure_server(&block)
88
+ end
89
+
90
+ def self.logger
91
+ @logger ||= Logger.new($stdout, level: Logger::INFO)
92
+ end
93
+
94
+ def self.logger=(logger)
95
+ if logger.nil?
96
+ self.logger.level = Logger::FATAL
97
+ return self.logger
98
+ end
99
+
100
+ @logger = logger
101
+ end
102
+
103
+ def self.reset!
104
+ @options = DEFAULTS.dup
105
+ truncate_tables!
106
+ @db = nil
107
+ end
108
+
109
+ def self.truncate_tables!
110
+ Slimer::ApiKey.truncate if defined? Slimer::ApiKey
111
+ Slimer::Substance.truncate if defined? Slimer::Substance
112
+ end
113
+ end
114
+
115
+ # These all depend on Slimer.options to exist, so we'll load hem after our
116
+ # initial module is setup.
117
+ require_relative "slimer/workers"
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Slimer
6
+ # A Sequel::Model wrapper around the api_keys table
7
+ class ApiKey < Sequel::Model
8
+ def self.generate(name)
9
+ new_token = loop do
10
+ generated_token = SecureRandom.urlsafe_base64
11
+ break generated_token unless token_exists?(generated_token)
12
+ end
13
+
14
+ create name: name, token: new_token
15
+ end
16
+
17
+ def self.token_exists?(token)
18
+ ApiKey.where(token: token).count.positive?
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "sequel/core"
5
+ require "sequel/model"
6
+ require "sequel/plugins/serialization"
7
+
8
+ module Slimer
9
+ # @abstract Wraps Sequel.connect to easily interface with the Slimer database
10
+ class Database
11
+ attr_reader :url
12
+
13
+ REQUIRED_TABLES = %i[api_keys substances].freeze
14
+
15
+ def initialize(url)
16
+ @url = url
17
+ add_supported_extensions
18
+ resolve_missing_tables!
19
+ connection.loggers << Slimer.logger if connection.loggers.empty?
20
+ end
21
+
22
+ def self.connection(url)
23
+ db = new(url)
24
+ # Sequel::Model requires a connection before you can subclass. Now that
25
+ # we have a connection, require the models.
26
+ connection = db.connection
27
+ db.load_models
28
+ connection
29
+ end
30
+
31
+ def connection
32
+ @connection ||= Sequel.connect(url)
33
+ end
34
+ alias connect connection
35
+
36
+ def resolve_missing_tables!
37
+ return if migrated?
38
+
39
+ create!
40
+ end
41
+
42
+ def create!
43
+ create_substances unless connection.table_exists?(:substances)
44
+ create_api_keys unless connection.table_exists?(:api_keys)
45
+ end
46
+
47
+ def migrated?
48
+ REQUIRED_TABLES.all? { |t| connection.table_exists?(t) }
49
+ end
50
+
51
+ def load_models
52
+ require_relative "api_key" unless defined?(Slimer::ApiKey)
53
+ require_relative "substance" unless defined?(Slimer::Substance)
54
+ end
55
+
56
+ private
57
+
58
+ def add_supported_extensions
59
+ return if connection.database_type != :postgres
60
+
61
+ connection.extension :pg_json
62
+ Sequel.extension :pg_json_ops
63
+ end
64
+
65
+ def create_substances
66
+ return create_substances_for_postgres if connection.database_type == :postgres
67
+
68
+ connection.create_table(:substances, ignore_index_errors: true) do
69
+ primary_key :id
70
+ String :uid
71
+ String :group, default: Slimer::DEFAULT_GROUP
72
+ String :payload, text: true
73
+ String :metadata, text: true
74
+ full_text_index :payload
75
+ full_text_index :metadata
76
+ end
77
+ end
78
+
79
+ def create_substances_for_postgres
80
+ connection.create_table(:substances, ignore_index_errors: true) do
81
+ String :uid
82
+ String :group, default: Slimer::DEFAULT_GROUP
83
+ JSONB :payload
84
+ JSONB :metadata
85
+ index :payload, type: :gin
86
+ index :metadata, type: :gin
87
+ end
88
+ end
89
+
90
+ def create_api_keys
91
+ connection.create_table(:api_keys, ignore_index_errors: true) do
92
+ primary_key :id
93
+ String :name
94
+ String :token
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A simple utility class to parse nested groups
4
+ class GroupConfigurator
5
+ attr_reader :groups
6
+
7
+ alias all groups
8
+
9
+ # Creates a flat set of groups from a string or block
10
+ #
11
+ # @examples
12
+ #
13
+ # GroupConfigurator.group("ruby/rails") => #<Set: {[ "ruby", "ruby/rails" ]} >
14
+ # GroupConfigurator.group(:ruby) do |config|
15
+ # config.group :rails
16
+ # end => #<Set: {[ "ruby", "ruby/rails" ]} >
17
+ def self.group(new_groups, &block)
18
+ new.group(new_groups, &block)
19
+ end
20
+
21
+ # Creates a flat set of groups from a string or block
22
+ #
23
+ # @examples
24
+ #
25
+ # GroupConfigurator.group("ruby/rails") => #<Set: {[ "ruby", "ruby/rails" ]} >
26
+ # GroupConfigurator.group(:ruby) do |config|
27
+ # config.group :rails
28
+ # end => #<Set: {[ "ruby", "ruby/rails" ]} >
29
+ def group(new_group)
30
+ @groups ||= Set.new
31
+ return group(group_from_string(new_group)) if new_group.to_s.include?("/")
32
+
33
+ group_with_level = [@level, new_group].compact.join("/")
34
+ @groups << group_with_level
35
+ return self unless block_given?
36
+
37
+ @level = group_with_level
38
+
39
+ yield(self)
40
+ self
41
+ end
42
+
43
+ private
44
+
45
+ # Extracts nested groups from a string and sets new level as parent group
46
+ # e.g. group_from_string("ruby/rails")
47
+ def group_from_string(new_group)
48
+ new_groups = new_group.split("/")
49
+ @groups << new_groups[0]
50
+ @level = new_groups.shift
51
+ new_groups
52
+ end
53
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimer
4
+ # A data object Slimer consumes
5
+ class Substance < Sequel::Model
6
+ plugin :serialization
7
+ serialize_attributes :json, :payload
8
+ serialize_attributes :json, :metadata
9
+
10
+ def self.consume(payload, options = {})
11
+ metadata = options.delete(:metadata)
12
+ group = options.delete(:group) || Slimer::DEFAULT_GROUP
13
+
14
+ Slimer::Workers::IngestSubstance.perform_async(
15
+ payload,
16
+ group,
17
+ metadata
18
+ )
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+
5
+ # Initialize the DB
6
+ Slimer.db
7
+ Dir.glob("#{__dir__}/tasks/*.rake") do |rake_file|
8
+ load rake_file
9
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :slimer do
4
+ namespace :api_keys do
5
+ desc "Generate a Slimer API key"
6
+ task :generate do
7
+ name = loop do
8
+ puts "Enter a name for this API key: "
9
+ new_name = $stdin.gets.chomp
10
+ break new_name unless new_name.to_s.empty?
11
+
12
+ puts "You must enter a name for this API key."
13
+ end
14
+
15
+ puts "Your new Slimer API key for \"#{name}\": #{Slimer::ApiKey.generate(name).token}"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slimer
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "slimer"
4
+ require "slimer/web/helpers"
5
+
6
+ require "slimer/web/router"
7
+ require "slimer/web/action"
8
+ require "slimer/web/application"
9
+ require "slimer/web/csfr_protection"
10
+
11
+ require "rack/builder"
12
+ require "rack/content_length"
13
+ require "rack/session/cookie"
14
+
15
+ # Slimer
16
+ module Slimer
17
+ # The Slimer web application on Rack
18
+ # Borrowed from Slidekiq:
19
+ # https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/web.rb
20
+ class Web
21
+ class << self
22
+ def settings
23
+ self
24
+ end
25
+
26
+ def middlewares
27
+ @middlewares ||= []
28
+ end
29
+
30
+ def enable(*opts)
31
+ opts.each { |key| set(key, true) }
32
+ end
33
+
34
+ def disable(*opts)
35
+ opts.each { |key| set(key, false) }
36
+ end
37
+
38
+ # Helper for the Sinatra syntax: Slimer::Web.set(:session_secret, Rails.application.secrets...)
39
+ def set(attribute, value)
40
+ send(:"#{attribute}=", value)
41
+ end
42
+
43
+ attr_accessor :app_url, :session_secret, :sessions
44
+ end
45
+
46
+ # rubocop:disable Lint/MissingSuper
47
+ def self.inherited(child)
48
+ child.app_url = app_url
49
+ child.session_secret = session_secret
50
+ child.redis_pool = redis_pool
51
+ child.sessions = sessions
52
+ end
53
+ # rubocop:enable Lint/MissingSuper
54
+
55
+ def settings
56
+ self.class.settings
57
+ end
58
+
59
+ def use(*middleware_args, &block)
60
+ middlewares << [middleware_args, block]
61
+ end
62
+
63
+ def middlewares
64
+ @middlewares ||= Web.middlewares.dup
65
+ end
66
+
67
+ def call(env)
68
+ app.call(env)
69
+ end
70
+
71
+ def self.call(env)
72
+ @app ||= new
73
+ @app.call(env)
74
+ end
75
+
76
+ def app
77
+ @app ||= build
78
+ end
79
+
80
+ def enable(*opts)
81
+ opts.each { |key| set(key, true) }
82
+ end
83
+
84
+ def disable(*opts)
85
+ opts.each { |key| set(key, false) }
86
+ end
87
+
88
+ def set(attribute, value)
89
+ send(:"#{attribute}=", value)
90
+ end
91
+
92
+ # Default values
93
+ set :sessions, true
94
+
95
+ attr_writer :sessions
96
+
97
+ def sessions
98
+ unless instance_variable_defined?("@sessions")
99
+ @sessions = self.class.sessions
100
+ @sessions = @sessions.to_hash.dup if @sessions.respond_to?(:to_hash)
101
+ end
102
+
103
+ @sessions
104
+ end
105
+
106
+ def self.register(extension)
107
+ extension.registered(WebApplication)
108
+ end
109
+
110
+ private
111
+
112
+ def using?(middleware)
113
+ middlewares.any? do |(m, _)|
114
+ m.is_a?(Array) && (m[0] == middleware || m[0].is_a?(middleware))
115
+ end
116
+ end
117
+
118
+ def build_sessions
119
+ middlewares = self.middlewares
120
+
121
+ s = sessions
122
+
123
+ # turn on CSRF protection if sessions are enabled and this is not the test env
124
+ middlewares.unshift [[CsrfProtection], nil] if s && !using?(CsrfProtection) && ENV["RACK_ENV"] != "test"
125
+
126
+ configure_cookie_middleware if s && !using?(::Rack::Session::Cookie)
127
+ middlewares.unshift [[::Rack::ContentLength], nil] unless using? ::Rack::ContentLength
128
+ end
129
+
130
+ def configure_cookie_middleware
131
+ unless (secret = Web.session_secret)
132
+ require "securerandom"
133
+ secret = SecureRandom.hex(64)
134
+ end
135
+
136
+ options = { secret: secret }
137
+ options = options.merge(sessions.to_hash) if sessions.respond_to? :to_hash
138
+
139
+ middlewares.unshift [[::Rack::Session::Cookie, options], nil]
140
+ end
141
+
142
+ def build
143
+ build_sessions
144
+
145
+ middlewares = self.middlewares
146
+ klass = self.class
147
+
148
+ ::Rack::Builder.new do
149
+ middlewares.each { |middleware, block| use(*middleware, &block) }
150
+
151
+ run WebApplication.new(klass)
152
+ end
153
+ end
154
+ end
155
+
156
+ Slimer::WebApplication.helpers WebHelpers
157
+ end