slimer 0.1.1

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