sanger_warren 0.1.0 → 0.2.0.rc1

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.
data/bin/warren ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'warren/app/cli'
5
+
6
+ Warren::App::Cli.start(ARGV)
data/lefthook.yml ADDED
@@ -0,0 +1,53 @@
1
+ # https://github.com/Arkweid/lefthook
2
+ # Rubocop globs built based on https://github.com/rubocop-hq/rubocop/blob/master/config/default.yml With globbing support povided by
3
+ # https://pkg.go.dev/github.com/gobwas/glob#section-documentation
4
+ # Has the advantage of quicker commits when no rubocop files
5
+ pre-commit:
6
+ parallel: true
7
+ commands:
8
+ rubocop:
9
+ glob: '{*.{rb,arb,axlsx,builder,fcgi,gemfile,gemspec,god,jb,jbuilder,mspec,opal,pluginspec,podspec,rabl,rake,rbuild,rbw,rbx,ru,ruby,spec,thor,watchr},.irbrc,.pryrc,.simplecov,buildfile,Appraisals,Berksfile,Brewfile,Buildfile,Capfile,Cheffile,Dangerfile,Deliverfile,Fastfile,*Fastfile,Gemfile,Guardfile,Jarfile,Mavenfile,Podfile,Puppetfile,Rakefile,rakefile,Snapfile,Steepfile,Thorfile,Vagabondfile,Vagrantfile}'
10
+ run: rubocop --display-style-guide --extra-details --force-exclusion --parallel {staged_files} || (echo 'Run `lefthook run fix` to run autocrrect on staged files only'; exit 1)
11
+
12
+ fix:
13
+ parallel: true
14
+ commands:
15
+ rubocop:
16
+ glob: '{*.{rb,arb,axlsx,builder,fcgi,gemfile,gemspec,god,jb,jbuilder,mspec,opal,pluginspec,podspec,rabl,rake,rbuild,rbw,rbx,ru,ruby,spec,thor,watchr},.irbrc,.pryrc,.simplecov,buildfile,Appraisals,Berksfile,Brewfile,Buildfile,Capfile,Cheffile,Dangerfile,Deliverfile,Fastfile,*Fastfile,Gemfile,Guardfile,Jarfile,Mavenfile,Podfile,Puppetfile,Rakefile,rakefile,Snapfile,Steepfile,Thorfile,Vagabondfile,Vagrantfile}'
17
+ run: rubocop --display-style-guide --extra-details --auto-correct --force-exclusion {staged_files}
18
+
19
+
20
+ # EXAMPLE USAGE
21
+ # Refer for explanation to following link:
22
+ # https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md
23
+ #
24
+ # pre-push:
25
+ # commands:
26
+ # packages-audit:
27
+ # tags: frontend security
28
+ # run: yarn audit
29
+ # gems-audit:
30
+ # tags: backend security
31
+ # run: bundle audit
32
+ #
33
+ # pre-commit:
34
+ # parallel: true
35
+ # commands:
36
+ # eslint:
37
+ # glob: "*.{js,ts}"
38
+ # run: yarn eslint {staged_files}
39
+ # rubocop:
40
+ # tags: backend style
41
+ # glob: "*.rb"
42
+ # exclude: "application.rb|routes.rb"
43
+ # run: bundle exec rubocop --force-exclusion {all_files}
44
+ # govet:
45
+ # tags: backend style
46
+ # files: git ls-files -m
47
+ # glob: "*.go"
48
+ # run: go vet {files}
49
+ # scripts:
50
+ # "hello.js":
51
+ # runner: node
52
+ # "any.go":
53
+ # runner: go run
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # While the gem is sanger_warren we actually namespace under Warren.
4
+ # The gem 'warren' performs a very similar function, but hasn't been updated
5
+ # for ten years.
6
+ # We need to include this file to ensure bundler automatically requires warren,
7
+ # thereby triggering the railties
8
+ require 'warren'
data/lib/warren.rb CHANGED
@@ -4,26 +4,71 @@ require 'warren/version'
4
4
  require 'warren/callback'
5
5
  require 'warren/handler'
6
6
  require 'warren/message'
7
+ require 'warren/subscriber/base'
8
+
9
+ # Load railties if rails is available
10
+ require 'warren/railtie' if defined?(Rails::Railtie)
7
11
 
8
12
  #
9
13
  # Module Warren provides connection pooling for RabbitMQ Connections
10
14
  #
11
15
  module Warren
16
+ # Environmental variables
17
+ WARREN_TYPE = 'WARREN_TYPE'
18
+
19
+ #
20
+ # Construct a {Warren::Handler::Base} of the type `type`.
21
+ # For Rails apps this is usually handled automatically by the initializer.
22
+ #
23
+ # @param type ['test','log','broadcast'] The type of warren handler to construct
24
+ # @param config [Hash] A configuration hash object
25
+ # @option config [Hash] :server Bunny connection parameters
26
+ # http://rubybunny.info/articles/connecting.html#using_a_map_of_parameters
27
+ # @option config [String] :exchange The default exchange to receive published messaged
28
+ # @option config [String] :routing_key_prefix A prefix to apply to all routing keys (Such as the environment)
29
+ #
30
+ # @return [Warren::Handler::Base] Exact class determined by the type passed in
31
+ #
12
32
  def self.construct(type:, config: {})
13
- case type
33
+ warren_type = ENV.fetch(WARREN_TYPE, type)
34
+ case warren_type
14
35
  when 'test' then Warren::Handler::Test.new
15
36
  when 'log' then Warren::Handler::Log.new(logger: config.fetch(:logger) { Rails.logger })
16
- when 'broadcast' then Warren::Handler::Broadcast.new(config)
17
- else raise StandardError, "Unknown type warren: #{type}"
37
+ when 'broadcast' then Warren::Handler::Broadcast.new(**config)
38
+ else raise StandardError, "Unknown type warren: #{warren_type}"
18
39
  end
19
40
  end
20
41
 
42
+ # Constructs a Warren::Handler of the specified type and sets it as the global handler.
21
43
  def self.setup(opts, logger: Rails.logger)
22
44
  logger.warn 'Recreating Warren handler when one already exists' if handler.present?
23
- @handler = construct(opts.symbolize_keys)
45
+ @handler = construct(**opts.symbolize_keys)
24
46
  end
25
47
 
48
+ #
49
+ # Returns the global Warren handler
50
+ #
51
+ # @return [Warren::Handler::Base] A warren handler for broadcasting messages
52
+ #
26
53
  def self.handler
27
54
  @handler
28
55
  end
56
+
57
+ # When we invoke the warren consumer, we end up loading warren before
58
+ # rails is loaded, so don't invoke the railtie, and don't get a change to do
59
+ # so until after the Rails has initialized, and thus run its ties.
60
+ # I'm sure there is a proper way of handling this, but want to move on for now.
61
+ def self.load_configuration
62
+ config = begin
63
+ Rails.application.config_for(:warren)
64
+ rescue RuntimeError => e
65
+ warn <<~WARN
66
+ 🐇 WARREN CONFIGURATION ERROR
67
+ #{e.message}
68
+ Use `warren config` to generate a basic configuration file
69
+ WARN
70
+ exit 1
71
+ end
72
+ Warren.setup(config.deep_symbolize_keys.slice(:type, :config))
73
+ end
29
74
  end
data/lib/warren/app.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warren
4
+ # The command line application can be triggered by `bundle exec warren`.
5
+ # It provides tools for configuring your warren set-up, as well as starting
6
+ # consumers.
7
+ module App
8
+ end
9
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative 'config'
5
+ require_relative 'consumer'
6
+
7
+ module Warren
8
+ module App
9
+ # Warren Thor CLI application used to:
10
+ # - Generate the configuration
11
+ # - Update the configuration with new consumers
12
+ # - Start and stop consumers
13
+ # @see http://whatisthor.com
14
+ class Cli < Thor
15
+ # Ensure we exit with an error in the event of failure
16
+ def self.exit_on_failure?
17
+ true
18
+ end
19
+
20
+ desc 'config', 'generate a basic warren config file'
21
+ option :path, type: :string,
22
+ default: 'config/warren.yml',
23
+ desc: 'The path to the configuration file to generate'
24
+ option :exchange, type: :string,
25
+ desc: 'The RabbitMQ exchange to connect to'
26
+ def config
27
+ Warren::App::Config.invoke(self, path: options['path'], exchange: options['exchange'])
28
+ end
29
+
30
+ desc 'consumer {add|start}', 'add and start queue consumers'
31
+ subcommand 'consumer', Consumer
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warren
4
+ module App
5
+ # Handles the initial creation of the configuration object
6
+ class Config
7
+ # We keep the template as plain text as it allows us to add comments
8
+ TEMPLATE = <<~TEMPLATE
9
+ # By default the development environment just logs the message and
10
+ # payload. If you wish to enable broadcasting in development mode,
11
+ # the easiest way to do so is to set the ENV WARREN_TYPE.
12
+ # For example
13
+ # `WARREN_TYPE=broadcast bundle exec rails s`
14
+ # This will override the setting in warren.yml
15
+ development:
16
+ type: log
17
+ # Log mode does not actually use this configuration, but
18
+ # it is provided for convenience when broadcast mode is enabled.
19
+ # The provided settings are the default options of RabbitMQ
20
+ # DO NOT commit sensitive information in this file. Instead you may
21
+ # use the WARREN_CONNECTION_URI environmental variable
22
+ config:
23
+ server:
24
+ host: localhost
25
+ port: 5672
26
+ username: guest
27
+ password: guest
28
+ vhost: %<vhost>s
29
+ frame_max: 0
30
+ heartbeat: 30
31
+ exchange: %<exchange>s
32
+ routing_key_prefix: development
33
+ # The test environment sets up a test message handler, which lets
34
+ # you make assertions about which messages have been sent.
35
+ # See: https://rubydoc.info/gems/sanger_warren/Warren/Handler/Test
36
+ test:
37
+ type: test
38
+ config:
39
+ routing_key_prefix: test
40
+ # You are encouraged to use the WARREN_CONNECTION_URI environmental
41
+ # variable to configure your production environment. Under no
42
+ # circumstances should you commit sensitive information in the file.
43
+ TEMPLATE
44
+
45
+ def self.invoke(shell, path:, exchange: nil)
46
+ new(shell, path: path, exchange: exchange).invoke
47
+ end
48
+
49
+ #
50
+ # Generates a new warren.yml file at {#path}. Usually invoked via the
51
+ # `warren config` cli command
52
+ #
53
+ # @param shell [Thor::Shell::Basic] Thor shell instance for user interaction
54
+ # @param path [String] The path to the warren.yml file.
55
+ # @param exchange [String] The exchange to connect to
56
+ #
57
+ def initialize(shell, path:, exchange: nil)
58
+ @shell = shell
59
+ @path = path
60
+ @exchange = exchange
61
+ end
62
+
63
+ #
64
+ # Create a new configuration yaml file at {#path} using sensible defaults
65
+ # and the provided {#exchange}. If {#exchange} is nil, prompts the user
66
+ #
67
+ # @return [Void]
68
+ #
69
+ def invoke
70
+ return unless check_file?
71
+
72
+ @exchange ||= ask_exchange # Update our exchange before we do anything
73
+ File.open(@path, 'w') do |file|
74
+ file.write payload
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ # The path to the config file
81
+ attr_reader :path
82
+ # The exchange to connect to
83
+ attr_reader :exchange
84
+
85
+ def payload
86
+ format(TEMPLATE, exchange: @exchange, vhost: '/')
87
+ end
88
+
89
+ def check_file?
90
+ return true unless File.exist?(@path)
91
+
92
+ @shell.yes? "#{@path} exists. Overwrite (Y/N)? "
93
+ end
94
+
95
+ def ask_exchange
96
+ @shell.ask 'Specify an exchange: '
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative 'consumer_add'
5
+ require_relative 'consumer_start'
6
+ require 'warren/config/consumers'
7
+
8
+ module Warren
9
+ module App
10
+ # Warren Thor CLI subcommand used to:
11
+ # - Add new consumer configurations
12
+ # - Start consumers
13
+ # @see http://whatisthor.com
14
+ class Consumer < Thor
15
+ include Thor::Actions
16
+
17
+ source_root("#{File.dirname(__FILE__)}/templates")
18
+
19
+ # Ensure we exit with an error in the event of failure
20
+ def self.exit_on_failure?
21
+ true
22
+ end
23
+
24
+ desc 'add NAME', 'generate a new warren consumer'
25
+ option :desc, type: :string,
26
+ desc: 'Brief description of consumer'
27
+ option :queue, type: :string,
28
+ desc: 'The RabbitMQ queue to create / connect to'
29
+ option :bindings, type: :array,
30
+ desc: 'bindings between the queue and exchange',
31
+ banner: '{direct|fanout|topic|headers}:EXCHANGE[:ROUTING_KEY_A[,ROUTING_KEY_B]]'
32
+ option :path, type: :string,
33
+ default: Warren::Config::Consumers::DEFAULT_PATH,
34
+ desc: 'The path to the consumer configuration file to generate'
35
+ def add(name = nil)
36
+ say 'Adding a consumer'
37
+ Warren::App::ConsumerAdd.invoke(self, name, options)
38
+ end
39
+
40
+ desc 'start', 'start registered consumers'
41
+ option :path, type: :string,
42
+ default: Warren::Config::Consumers::DEFAULT_PATH,
43
+ desc: 'The path to the consumer configuration file to use'
44
+ option :consumers, type: :array,
45
+ desc: 'The consumers to start. Defaults to all consumers',
46
+ banner: 'consumer_name other_consumer'
47
+ def start
48
+ say 'Starting consumers'
49
+ Warren::App::ConsumerStart.invoke(self, options)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'exchange_config'
4
+ require 'warren/config/consumers'
5
+
6
+ module Warren
7
+ module App
8
+ # Handles the initial creation of the configuration object
9
+ class ConsumerAdd
10
+ SUBSCRIBER_NAMESPACE = 'Warren::Subscriber::'
11
+
12
+ attr_reader :name, :desc, :queue
13
+
14
+ #
15
+ # Add a consumer to the configuration file located at `options.path`
16
+ # Will prompt the user for input on the `shell` if information not
17
+ # provided upfront
18
+ #
19
+ # @param shell [Thor::Shell::Basic] Thor shell instance for feedback
20
+ # @param name [String] The name of the consumer
21
+ # @param options [Hash] Hash of command line arguments from Thor
22
+ # @option options [String] :desc Short description of consumer (for documentation)
23
+ # @option options [String] :queue Then name of the queue to bind to
24
+ # @option options [Array<String>] :bindings Array of binding in the format
25
+ # '<exchange_type>:<exchange_name>:<outing_key>,<routing_key>'
26
+ #
27
+ # @return [ConsumerAdd] The ConsumerAdd
28
+ #
29
+ def self.invoke(shell, name, options)
30
+ new(shell, name, options).invoke
31
+ end
32
+
33
+ # Create a consumer configuration object. Use {#invoke} to gather information and
34
+ # generate the config
35
+ #
36
+ # @param shell [Thor::Shell::Basic] Thor shell instance for feedback
37
+ # @param name [String] The name of the consumer
38
+ # @param options [Hash] Hash of command line arguments from Thor
39
+ # @option options [String] :desc Short description of consumer (for documentation)
40
+ # @option options [String] :queue Then name of the queue to bind to
41
+ # @option options [Array<String>] :bindings Array of binding in the format
42
+ # '<exchange_type>:<exchange_name>:<outing_key>,<routing_key>'
43
+ #
44
+ def initialize(shell, name, options)
45
+ @shell = shell
46
+ @name = name
47
+ @desc = options[:desc]
48
+ @queue = options[:queue]
49
+ @config = Warren::Config::Consumers.new(options[:path])
50
+ @bindings = Warren::App::ExchangeConfig.parse(shell, options[:bindings])
51
+ end
52
+
53
+ #
54
+ # Create a new configuration yaml file at `@path` using sensible defaults
55
+ # and the provided exchange. If exchange is nil, prompts the user
56
+ #
57
+ # @return [Void]
58
+ #
59
+ def invoke
60
+ check_name if @name # Check name before we gather facts, as its better to know we
61
+ # might have an issue early.
62
+ gather_facts
63
+ write_configuration
64
+ write_subscriber
65
+ end
66
+
67
+ private
68
+
69
+ def subscribed_class
70
+ class_name = name.split(/[\s\-_]/).map(&:capitalize).join
71
+
72
+ "#{SUBSCRIBER_NAMESPACE}#{class_name}"
73
+ end
74
+
75
+ def check_name
76
+ while @config.consumer_exist?(@name)
77
+ @name = @shell.ask(
78
+ "Consumer named '#{@name}' already exists. Specify a alternative " \
79
+ 'consumer name: '
80
+ )
81
+ end
82
+ end
83
+
84
+ #
85
+ # Loads the configuration, should be a hash
86
+ #
87
+ # @return [Hash] A hash of consumer configurations indexed by name
88
+ #
89
+ def load_config
90
+ YAML.load_file(@path)
91
+ rescue Errno::ENOENT
92
+ {}
93
+ end
94
+
95
+ def gather_facts
96
+ @name ||= @shell.ask 'Specify a consumer name: '
97
+ check_name
98
+ @desc ||= @shell.ask 'Provide an optional description: '
99
+ @queue ||= @shell.ask 'Provide the name of the queue to connect to: '
100
+ @bindings ||= gather_bindings
101
+ nil
102
+ end
103
+
104
+ def gather_bindings
105
+ Warren::App::ExchangeConfig.ask(@shell)
106
+ end
107
+
108
+ def write_configuration
109
+ @config.add_consumer(@name, desc: @desc, queue: @queue, bindings: @bindings, subscribed_class: subscribed_class)
110
+ @config.save
111
+ end
112
+
113
+ def write_subscriber
114
+ @shell.template('subscriber.tt', consumer_path, context: binding)
115
+ end
116
+
117
+ def consumer_path
118
+ "app/warren/subscribers/#{@name.tr(' -', '_')}.rb"
119
+ end
120
+ end
121
+ end
122
+ end