lepus 0.0.1.beta2

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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/specs.yml +44 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +35 -0
  6. data/.tool-versions +1 -0
  7. data/CHANGELOG.md +10 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +120 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +213 -0
  12. data/Rakefile +4 -0
  13. data/bin/console +9 -0
  14. data/bin/setup +7 -0
  15. data/docker-compose.yml +8 -0
  16. data/exec/lepus +9 -0
  17. data/gemfiles/rails52.gemfile +5 -0
  18. data/gemfiles/rails52.gemfile.lock +242 -0
  19. data/gemfiles/rails61.gemfile +5 -0
  20. data/gemfiles/rails61.gemfile.lock +260 -0
  21. data/lepus.gemspec +53 -0
  22. data/lib/lepus/app_executor.rb +19 -0
  23. data/lib/lepus/cli.rb +27 -0
  24. data/lib/lepus/configuration.rb +90 -0
  25. data/lib/lepus/consumer.rb +177 -0
  26. data/lib/lepus/consumer_config.rb +149 -0
  27. data/lib/lepus/consumer_wrapper.rb +46 -0
  28. data/lib/lepus/lifecycle_hooks.rb +49 -0
  29. data/lib/lepus/message.rb +37 -0
  30. data/lib/lepus/middleware.rb +18 -0
  31. data/lib/lepus/middlewares/honeybadger.rb +23 -0
  32. data/lib/lepus/middlewares/json.rb +35 -0
  33. data/lib/lepus/middlewares/max_retry.rb +57 -0
  34. data/lib/lepus/primitive/string.rb +55 -0
  35. data/lib/lepus/process.rb +136 -0
  36. data/lib/lepus/process_registry.rb +37 -0
  37. data/lib/lepus/processes/base.rb +50 -0
  38. data/lib/lepus/processes/callbacks.rb +72 -0
  39. data/lib/lepus/processes/consumer.rb +113 -0
  40. data/lib/lepus/processes/interruptible.rb +38 -0
  41. data/lib/lepus/processes/procline.rb +11 -0
  42. data/lib/lepus/processes/registrable.rb +67 -0
  43. data/lib/lepus/processes/runnable.rb +102 -0
  44. data/lib/lepus/processes/supervised.rb +44 -0
  45. data/lib/lepus/processes.rb +6 -0
  46. data/lib/lepus/producer.rb +42 -0
  47. data/lib/lepus/rails/log_subscriber.rb +120 -0
  48. data/lib/lepus/rails/railtie.rb +31 -0
  49. data/lib/lepus/rails.rb +7 -0
  50. data/lib/lepus/supervisor/config.rb +45 -0
  51. data/lib/lepus/supervisor/maintenance.rb +35 -0
  52. data/lib/lepus/supervisor/pidfile.rb +61 -0
  53. data/lib/lepus/supervisor/pidfiled.rb +29 -0
  54. data/lib/lepus/supervisor/signals.rb +71 -0
  55. data/lib/lepus/supervisor.rb +204 -0
  56. data/lib/lepus/timer.rb +29 -0
  57. data/lib/lepus/version.rb +5 -0
  58. data/lib/lepus.rb +95 -0
  59. data/lib/puma/plugin/lepus.rb +74 -0
  60. metadata +290 -0
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ # The abstract base class for consumers processing messages from queues.
5
+ # @abstract Subclass and override {#work} to implement.
6
+ class Consumer
7
+ class << self
8
+ def abstract_class?
9
+ return @abstract_class == true if defined?(@abstract_class)
10
+
11
+ instance_variable_get(:@config).nil?
12
+ end
13
+
14
+ def abstract_class=(value)
15
+ @config = nil
16
+ @abstract_class = value
17
+ end
18
+
19
+ def inherited(subclass)
20
+ super
21
+ subclass.abstract_class = false
22
+ end
23
+
24
+ def config
25
+ return if @abstract_class == true
26
+ return @config if defined?(@config)
27
+
28
+ name = Primitive::String.new(to_s).underscore.split("/").last
29
+ @config = ConsumerConfig.new(queue: name, exchange: name)
30
+ end
31
+
32
+ # List of registered middlewares. Register new middlewares with {.use}.
33
+ # @return [Array<Lepus::Middleware>]
34
+ def middlewares
35
+ @middlewares ||= []
36
+ end
37
+
38
+ # Registers a new middleware by instantiating +middleware+ and passing it +opts+.
39
+ #
40
+ # @param [Symbol, Class<Lepus::Middleware>] middleware The middleware class to instantiate and register.
41
+ # @param [Hash] opts The options for instantiating the middleware.
42
+ def use(middleware, opts = {})
43
+ if middleware.is_a?(Symbol) || middleware.is_a?(String)
44
+ begin
45
+ require_relative "middlewares/#{middleware}"
46
+ class_name = Primitive::String.new(middleware.to_s).classify
47
+ class_name = "JSON" if class_name == "Json"
48
+ middleware = Lepus::Middlewares.const_get(class_name)
49
+ rescue LoadError, NameError
50
+ raise ArgumentError, "Middleware #{middleware} not found"
51
+ end
52
+ end
53
+
54
+ middlewares << middleware.new(**opts)
55
+ end
56
+
57
+ # Configures the consumer, setting queue, exchange and other options to be used by
58
+ # the add_consumer method.
59
+ #
60
+ # @param [Hash] opts The options to configure the consumer with.
61
+ # @option opts [String, Hash] :queue The name of the queue to consume from.
62
+ # @option opts [String, Hash] :exchange The name of the exchange the queue should be bound to.
63
+ # @option opts [Array] :routing_key The routing keys used for the queue binding.
64
+ # @option opts [Boolean, Hash] :retry_queue (false) Whether a retry queue should be provided.
65
+ # @option opts [Boolean, Hash] :error_queue (false) Whether an error queue should be provided.
66
+ def configure(opts = {})
67
+ @config = ConsumerConfig.new(opts)
68
+ yield(@config) if block_given?
69
+ @config
70
+ end
71
+
72
+ def descendants # :nodoc:
73
+ descendants = []
74
+ ObjectSpace.each_object(singleton_class) do |k|
75
+ descendants.unshift k unless k == self
76
+ end
77
+ descendants.uniq
78
+ end
79
+ end
80
+
81
+ # The method that is called when a message from the queue is received.
82
+ # Keep in mind that the parameters received can be altered by middlewares!
83
+ #
84
+ # @param [Leupus::Message] message The message to process.
85
+ #
86
+ # @return [:ack, :reject, :requeue] A symbol denoting what should be done with the message.
87
+ def perform(message)
88
+ raise NotImplementedError
89
+ end
90
+
91
+ # Wraps #perform to add middlewares. This is being called by Lepus when a message is received for the consumer.
92
+ #
93
+ # @param [Bunny::DeliveryInfo] delivery_info The delivery info of the received message.
94
+ # @param [Bunny::MessageProperties] metadata The metadata of the received message.
95
+ # @param [String] payload The payload of the received message.
96
+ # @raise [InvalidConsumerReturnError] if you return something other than +:ack+, +:reject+ or +:requeue+ from {#perform}.
97
+ def process_delivery(delivery_info, metadata, payload)
98
+ message = Message.new(delivery_info, metadata, payload)
99
+ self
100
+ .class
101
+ .middlewares
102
+ .reverse
103
+ .reduce(work_proc) do |next_middleware, middleware|
104
+ nest_middleware(middleware, next_middleware)
105
+ end
106
+ .call(message)
107
+ rescue Lepus::InvalidConsumerReturnError
108
+ raise
109
+ rescue Exception => ex # rubocop:disable Lint/RescueException
110
+ # @TODO: add error handling
111
+ logger.error(ex)
112
+
113
+ reject!
114
+ end
115
+
116
+ protected
117
+
118
+ def logger
119
+ Lepus.logger
120
+ end
121
+
122
+ # Helper method to ack a message.
123
+ #
124
+ # @return [:ack]
125
+ def ack!
126
+ :ack
127
+ end
128
+ alias_method :ack, :ack!
129
+
130
+ # Helper method to reject a message.
131
+ #
132
+ # @return [:reject]
133
+ #
134
+ def reject!
135
+ :reject
136
+ end
137
+ alias_method :reject, :reject!
138
+
139
+ # Helper method to requeue a message.
140
+ #
141
+ # @return [:requeue]
142
+ def requeue!
143
+ :requeue
144
+ end
145
+ alias_method :requeue, :requeue!
146
+
147
+ # Helper method to nack a message.
148
+ #
149
+ # @return [:nack]
150
+ def nack!
151
+ :nack
152
+ end
153
+ alias_method :nack, :nack!
154
+
155
+ private
156
+
157
+ def work_proc
158
+ ->(message) do
159
+ perform(message).tap do |result|
160
+ verify_result(result)
161
+ end
162
+ end
163
+ end
164
+
165
+ def nest_middleware(middleware, next_middleware)
166
+ ->(message) do
167
+ middleware.call(message, next_middleware)
168
+ end
169
+ end
170
+
171
+ def verify_result(result)
172
+ return if %i[ack reject requeue nack].include?(result)
173
+
174
+ raise InvalidConsumerReturnError, result
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,149 @@
1
+ require "bunny"
2
+
3
+ module Lepus
4
+ # Parse the list of options for the consumer.
5
+ class ConsumerConfig
6
+ DEFAULT_EXCHANGE_OPTIONS = {
7
+ name: nil,
8
+ type: :topic, # The type of the exchange (:direct, :fanout, :topic or :headers).
9
+ durable: true
10
+ }.freeze
11
+
12
+ DEFAULT_QUEUE_OPTIONS = {
13
+ name: nil,
14
+ durable: true
15
+ }.freeze
16
+
17
+ DEFAULT_RETRY_QUEUE_OPTIONS = {
18
+ name: nil,
19
+ durable: true,
20
+ delay: 5000,
21
+ arguments: {}
22
+ }
23
+
24
+ DEFAULT_ERROR_QUEUE_OPTIONS = DEFAULT_QUEUE_OPTIONS
25
+
26
+ attr_reader :options
27
+
28
+ def initialize(options = {})
29
+ opts = HashUtil.deep_symbolize_keys(options)
30
+
31
+ @exchange_opts = DEFAULT_EXCHANGE_OPTIONS.merge(
32
+ declaration_config(opts.delete(:exchange))
33
+ )
34
+ @queue_opts = DEFAULT_QUEUE_OPTIONS.merge(
35
+ declaration_config(opts.delete(:queue))
36
+ )
37
+ if (value = opts.delete(:retry_queue))
38
+ @retry_queue_opts = DEFAULT_RETRY_QUEUE_OPTIONS.merge(
39
+ declaration_config(value)
40
+ )
41
+ end
42
+ if (value = opts.delete(:error_queue))
43
+ @error_queue_opts = DEFAULT_ERROR_QUEUE_OPTIONS.merge(
44
+ declaration_config(value)
45
+ )
46
+ end
47
+ @bind_opts = opts.delete(:bind) || {}
48
+ if (routing_key = opts.delete(:routing_key))
49
+ @bind_opts[:routing_key] ||= routing_key
50
+ end
51
+ @options = opts
52
+ end
53
+
54
+ def channel_args
55
+ @channel_opts.values_at(
56
+ :consumer_pool_size,
57
+ :consumer_pool_abort_on_exception,
58
+ :consumer_pool_shutdown_timeout
59
+ )
60
+ end
61
+
62
+ def exchange_args
63
+ [exchange_name, @exchange_opts.reject { |k, v| k == :name }]
64
+ end
65
+
66
+ def consumer_queue_args
67
+ opts = @queue_opts.reject { |k, v| k == :name }
68
+ return [queue_name, opts] unless retry_queue_args
69
+
70
+ opts[:arguments] ||= {}
71
+ opts[:arguments]["x-dead-letter-exchange"] = ""
72
+ opts[:arguments]["x-dead-letter-routing-key"] = retry_queue_name
73
+
74
+ [queue_name, opts]
75
+ end
76
+
77
+ def retry_queue_args
78
+ return unless @retry_queue_opts
79
+
80
+ delay = @retry_queue_opts[:delay]
81
+ args = (@retry_queue_opts[:arguments] || {}).merge(
82
+ "x-dead-letter-exchange" => "",
83
+ "x-dead-letter-routing-key" => queue_name,
84
+ "x-message-ttl" => delay
85
+ )
86
+ extra_keys = %i[name delay]
87
+ opts = @retry_queue_opts.reject { |k, v| extra_keys.include?(k) }
88
+ [retry_queue_name, opts.merge(arguments: args)]
89
+ end
90
+
91
+ def error_queue_args
92
+ return unless @error_queue_opts
93
+
94
+ name = @error_queue_opts[:name]
95
+ name ||= "#{queue_name}.error"
96
+ [name, @error_queue_opts.reject { |k, v| k == :name }]
97
+ end
98
+
99
+ def binds_args
100
+ arguments = @bind_opts.fetch(:arguments, {}).transform_keys(&:to_s)
101
+ opts = {}
102
+ opts[:arguments] = arguments unless arguments.empty?
103
+ if (routing_keys = @bind_opts[:routing_key]).is_a?(Array)
104
+ routing_keys.map { |key| opts.merge(routing_key: key) }
105
+ elsif (routing_key = @bind_opts[:routing_key])
106
+ [opts.merge(routing_key: routing_key)]
107
+ else
108
+ [opts]
109
+ end
110
+ end
111
+
112
+ protected
113
+
114
+ def exchange_name
115
+ @exchange_opts[:name] || raise(InvalidConsumerConfigError, "Exchange name is required")
116
+ end
117
+
118
+ def queue_name
119
+ @queue_opts[:name] || raise(InvalidConsumerConfigError, "Queue name is required")
120
+ end
121
+
122
+ def retry_queue_name
123
+ name = @retry_queue_opts[:name]
124
+ name ||= "#{queue_name}.retry"
125
+ name
126
+ end
127
+
128
+ # Normalizes a declaration config (for exchanges and queues) into a configuration Hash.
129
+ #
130
+ # If the given `value` is a String, convert it to a Hash with the key `:name` and the value.
131
+ # If the given `value` is a Hash, leave it as is.
132
+ def declaration_config(value)
133
+ case value
134
+ when Hash then value
135
+ when String then {name: value}
136
+ when NilClass then {}
137
+ when TrueClass then {}
138
+ end
139
+ end
140
+
141
+ class HashUtil
142
+ def self.deep_symbolize_keys(hash)
143
+ hash.each_with_object({}) do |(k, v), memo|
144
+ memo[k.to_sym] = v.is_a?(Hash) ? deep_symbolize_keys(v) : v
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,46 @@
1
+ require "bunny"
2
+
3
+ module Lepus
4
+ # Wraps the user-defined consumer to provide the expected interface to Bunny.
5
+ class ConsumerWrapper < Bunny::Consumer
6
+ # @param [Lepus::Consumer] consumer The user-defined consumer implementation derived from {Lepus::Consumer}.
7
+ # @param [Bunny::Channel] channel The channel used for the consumer.
8
+ # @param [Bunny::Queue] queue The queue the consumer is subscribed to.
9
+ # @param [String] consumer_tag A string identifying the consumer instance.
10
+ # @param [Hash] arguments Arguments that are passed on to +Bunny::Consumer.new+.
11
+ def initialize(consumer, channel, queue, consumer_tag, arguments = {})
12
+ @consumer = consumer
13
+ super(channel, queue, consumer_tag, false, false, arguments)
14
+ end
15
+
16
+ # Called when a message is received from the subscribed queue.
17
+ #
18
+ # @param [Bunny::DeliveryInfo] delivery_info The delivery info of the received message.
19
+ # @param [Bunny::MessageProperties] metadata The metadata of the received message.
20
+ # @param [String] payload The payload of the received message.
21
+ def process_delivery(delivery_info, metadata, payload)
22
+ consumer
23
+ .process_delivery(delivery_info, metadata, payload)
24
+ .tap { |result| process_result(result, delivery_info.delivery_tag) }
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :consumer
30
+
31
+ def process_result(result, delivery_tag)
32
+ case result
33
+ when :ack
34
+ channel.ack(delivery_tag, false)
35
+ when :reject
36
+ channel.reject(delivery_tag)
37
+ when :requeue
38
+ channel.reject(delivery_tag, true)
39
+ when :nack
40
+ channel.nack(delivery_tag, false, true)
41
+ else
42
+ raise Lepus::InvalidConsumerReturnError, result
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ # @TODO: Move after/before fork hooks to this module
5
+ module LifecycleHooks
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ base.send :include, InstanceMethods
9
+ base.instance_variable_set(:@lifecycle_hooks, {start: [], stop: []})
10
+ end
11
+
12
+ module ClassMethods
13
+ attr_reader :lifecycle_hooks
14
+
15
+ def on_start(&block)
16
+ lifecycle_hooks[:start] << block
17
+ end
18
+
19
+ def on_stop(&block)
20
+ lifecycle_hooks[:stop] << block
21
+ end
22
+
23
+ def clear_hooks
24
+ lifecycle_hooks[:start] = []
25
+ lifecycle_hooks[:stop] = []
26
+ end
27
+ end
28
+
29
+ module InstanceMethods
30
+ private
31
+
32
+ def run_start_hooks
33
+ run_hooks_for :start
34
+ end
35
+
36
+ def run_stop_hooks
37
+ run_hooks_for :stop
38
+ end
39
+
40
+ def run_hooks_for(event)
41
+ self.class.lifecycle_hooks.fetch(event, []).each do |block|
42
+ block.call
43
+ rescue Exception => exception # rubocop:disable Lint/RescueException
44
+ handle_thread_error(exception)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ class Message
5
+ attr_reader :delivery_info, :metadata, :payload
6
+
7
+ def initialize(delivery_info, metadata, payload)
8
+ @delivery_info = delivery_info
9
+ @metadata = metadata
10
+ @payload = payload
11
+ end
12
+
13
+ def mutate(payload: nil, metadata: nil, delivery_info: nil)
14
+ self.class.new(
15
+ delivery_info || @delivery_info,
16
+ metadata || @metadata,
17
+ payload || @payload
18
+ )
19
+ end
20
+
21
+ def to_h
22
+ {
23
+ delivery: delivery_info&.to_h,
24
+ metadata: metadata&.to_h,
25
+ payload: payload
26
+ }
27
+ end
28
+
29
+ def eql?(other)
30
+ other.is_a?(self.class) &&
31
+ delivery_info == other.delivery_info &&
32
+ metadata == other.metadata &&
33
+ payload == other.payload
34
+ end
35
+ alias_method :==, :eql?
36
+ end
37
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ # The abstract base class for middlewares.
5
+ # @abstract Subclass and override {#call} (and maybe +#initialize+) to implement.
6
+ class Middleware
7
+ def initialize(**)
8
+ end
9
+
10
+ # Invokes the middleware.
11
+ #
12
+ # @param [Lepus::Message] message The message to process.
13
+ # @param app The next middleware to call or the actual consumer instance.
14
+ def call(message, app)
15
+ raise NotImplementedError
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ module Middlewares
5
+ # A middleware that automatically wraps {Lepus::Consumer#perform]} in an Honeybadger transaction.
6
+ class Honeybadger < Lepus::Middleware
7
+ # @param [Hash] opts The options for the middleware.
8
+ # @option opts [String] :class_name The name of the class you want to monitor.
9
+ def initialize(class_name:, **)
10
+ super
11
+
12
+ @class_name = class_name
13
+ end
14
+
15
+ def call(message, app)
16
+ app.call(message)
17
+ rescue => err
18
+ ::Honeybadger.notify(err, context: {class_name: @class_name})
19
+ raise err
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "multi_json"
4
+
5
+ module Lepus
6
+ module Middlewares
7
+ # A middleware that automatically parses your JSON payload.
8
+ class JSON < Lepus::Middleware
9
+ # @param [Hash] opts The options for the middleware.
10
+ # @option opts [Proc] :on_error (Proc.new { :reject }) A Proc to be called when an error occurs during processing.
11
+ # @option opts [Boolean] :symbolize_keys (false) Whether to symbolize the keys of your payload.
12
+ def initialize(**opts)
13
+ super
14
+
15
+ @on_error = opts.fetch(:on_error, proc { :reject })
16
+ @symbolize_keys = opts.fetch(:symbolize_keys, false)
17
+ end
18
+
19
+ def call(message, app)
20
+ begin
21
+ parsed_payload =
22
+ MultiJson.load(message.payload, symbolize_keys: symbolize_keys)
23
+ rescue => e
24
+ return on_error.call(e)
25
+ end
26
+
27
+ app.call(message.mutate(payload: parsed_payload))
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :symbolize_keys, :on_error
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,57 @@
1
+ module Lepus
2
+ module Middlewares
3
+ # A middleware that automatically puts messages on an error queue when the specified number of retries are exceeded.
4
+ class MaxRetry < Lepus::Middleware
5
+ include Lepus::AppExecutor
6
+
7
+ # @param [Hash] opts The options for the middleware.
8
+ # @option opts [Integer] :retries The number of retries before the message is sent to the error queue.
9
+ # @option opts [String] :error_queue The name of the queue where messages should be sent to when the max retries are reached.
10
+ def initialize(retries:, error_queue:)
11
+ super
12
+
13
+ @retries = retries
14
+ @error_queue = error_queue
15
+ end
16
+
17
+ def call(message, app)
18
+ return handle_exceeded(message) if retries_exceeded?(message.metadata)
19
+
20
+ app.call(message)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :retries, :error_queue
26
+
27
+ def handle_exceeded(message)
28
+ payload = message.payload
29
+ payload = MultiJson.dump(payload) if payload.is_a?(Hash)
30
+ ::Bunny::Exchange.default(message.delivery_info.channel).publish(
31
+ payload,
32
+ routing_key: error_queue
33
+ )
34
+ :ack
35
+ rescue => err
36
+ handle_thread_error(err)
37
+ end
38
+
39
+ def retries_exceeded?(metadata)
40
+ return false if metadata.headers.nil?
41
+
42
+ rejected_deaths =
43
+ metadata
44
+ .headers
45
+ .fetch("x-death", [])
46
+ .select { |death| death["reason"] == "rejected" }
47
+
48
+ return false unless rejected_deaths.any?
49
+
50
+ retry_count = rejected_deaths.map { |death| death["count"] }.compact.max
51
+ return false unless retry_count
52
+
53
+ retry_count > @retries
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "dry/inflector"
5
+ rescue LoadError
6
+ # noop
7
+ end
8
+
9
+ begin
10
+ require "active_support/inflector"
11
+ rescue LoadError
12
+ # noop
13
+ end
14
+
15
+ module Lepus::Primitive
16
+ class String < ::String
17
+ def classify
18
+ new_str = if defined?(Dry::Inflector)
19
+ Dry::Inflector.new.classify(self)
20
+ elsif defined?(ActiveSupport::Inflector)
21
+ ActiveSupport::Inflector.classify(self)
22
+ else
23
+ split("/").collect do |c|
24
+ c.split("_").collect(&:capitalize).join
25
+ end.join("::")
26
+ end
27
+
28
+ self.class.new(new_str)
29
+ end
30
+
31
+ def constantize
32
+ if defined?(Dry::Inflector)
33
+ Dry::Inflector.new.constantize(self)
34
+ elsif defined?(ActiveSupport::Inflector)
35
+ ActiveSupport::Inflector.constantize(self)
36
+ else
37
+ Object.const_get(self)
38
+ end
39
+ end
40
+
41
+ def underscore
42
+ new_str = sub(/^::/, "")
43
+ .gsub("::", "/")
44
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
45
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
46
+ .tr("-", "_")
47
+ .tr(".", "_")
48
+ .gsub(/\s/, "_")
49
+ .gsub(/__+/, "_")
50
+ .downcase
51
+
52
+ self.class.new(new_str)
53
+ end
54
+ end
55
+ end