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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +39 -0
- data/.rubocop.yml +11 -5
- data/CHANGELOG.md +9 -1
- data/Gemfile +6 -1
- data/Gemfile.lock +68 -36
- data/README.md +105 -44
- data/bin/console +3 -6
- data/bin/warren +6 -0
- data/lefthook.yml +53 -0
- data/lib/sanger_warren.rb +8 -0
- data/lib/warren.rb +49 -4
- data/lib/warren/app.rb +9 -0
- data/lib/warren/app/cli.rb +34 -0
- data/lib/warren/app/config.rb +100 -0
- data/lib/warren/app/consumer.rb +53 -0
- data/lib/warren/app/consumer_add.rb +122 -0
- data/lib/warren/app/consumer_start.rb +25 -0
- data/lib/warren/app/exchange_config.rb +138 -0
- data/lib/warren/app/templates/subscriber.tt +32 -0
- data/lib/warren/callback.rb +2 -7
- data/lib/warren/client.rb +111 -0
- data/lib/warren/config/consumers.rb +101 -0
- data/lib/warren/den.rb +77 -0
- data/lib/warren/exceptions.rb +15 -0
- data/lib/warren/fox.rb +161 -0
- data/lib/warren/framework_adaptor/rails_adaptor.rb +83 -0
- data/lib/warren/handler/base.rb +20 -0
- data/lib/warren/handler/broadcast.rb +30 -16
- data/lib/warren/handler/log.rb +42 -10
- data/lib/warren/handler/test.rb +102 -14
- data/lib/warren/helpers/state_machine.rb +55 -0
- data/lib/warren/log_tagger.rb +58 -0
- data/lib/warren/message.rb +5 -5
- data/lib/warren/message/short.rb +41 -4
- data/lib/warren/railtie.rb +12 -0
- data/lib/warren/subscriber/base.rb +123 -0
- data/lib/warren/subscription.rb +71 -0
- data/lib/warren/version.rb +2 -1
- data/sanger-warren.gemspec +5 -4
- metadata +48 -8
- data/.travis.yml +0 -6
data/bin/warren
ADDED
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
|
-
|
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: #{
|
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,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
|