lepus 0.0.1.beta2 → 0.1.0

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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/linter.yml +21 -0
  3. data/.github/workflows/specs.yml +93 -13
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +10 -0
  6. data/.tool-versions +1 -1
  7. data/Gemfile +7 -0
  8. data/Gemfile.lock +36 -9
  9. data/Makefile +19 -0
  10. data/README.md +562 -7
  11. data/bin/setup +5 -2
  12. data/config.ru +14 -0
  13. data/docker-compose.yml +5 -3
  14. data/docs/README.md +80 -0
  15. data/docs/cli.md +108 -0
  16. data/docs/configuration.md +171 -0
  17. data/docs/consumers.md +168 -0
  18. data/docs/getting-started.md +136 -0
  19. data/docs/images/lepus-web.png +0 -0
  20. data/docs/middleware.md +240 -0
  21. data/docs/producers.md +173 -0
  22. data/docs/prometheus.md +112 -0
  23. data/docs/rails.md +161 -0
  24. data/docs/supervisor.md +112 -0
  25. data/docs/testing.md +141 -0
  26. data/docs/web.md +85 -0
  27. data/examples/grafana-dashboard.json +450 -0
  28. data/gemfiles/Gemfile.rails-5.2 +7 -0
  29. data/gemfiles/{rails52.gemfile.lock → Gemfile.rails-5.2.lock} +102 -69
  30. data/gemfiles/Gemfile.rails-6.1 +7 -0
  31. data/gemfiles/{rails61.gemfile.lock → Gemfile.rails-6.1.lock} +113 -79
  32. data/gemfiles/{rails52.gemfile → Gemfile.rails-7.2} +1 -1
  33. data/gemfiles/Gemfile.rails-7.2.lock +321 -0
  34. data/gemfiles/{rails61.gemfile → Gemfile.rails-8.0} +1 -1
  35. data/gemfiles/Gemfile.rails-8.0.lock +322 -0
  36. data/lepus.gemspec +7 -1
  37. data/lib/lepus/cli.rb +35 -4
  38. data/lib/lepus/configuration.rb +107 -0
  39. data/lib/lepus/connection_pool.rb +135 -0
  40. data/lib/lepus/consumer.rb +59 -41
  41. data/lib/lepus/consumers/config.rb +183 -0
  42. data/lib/lepus/consumers/handler.rb +56 -0
  43. data/lib/lepus/consumers/middleware_chain.rb +22 -0
  44. data/lib/lepus/consumers/middlewares/exception_logger.rb +27 -0
  45. data/lib/lepus/consumers/middlewares/honeybadger.rb +33 -0
  46. data/lib/lepus/consumers/middlewares/json.rb +37 -0
  47. data/lib/lepus/consumers/middlewares/max_retry.rb +83 -0
  48. data/lib/lepus/consumers/middlewares/unique.rb +65 -0
  49. data/lib/lepus/consumers/stats.rb +70 -0
  50. data/lib/lepus/consumers/stats_registry.rb +29 -0
  51. data/lib/lepus/consumers/worker.rb +141 -0
  52. data/lib/lepus/consumers/worker_factory.rb +124 -0
  53. data/lib/lepus/consumers.rb +6 -0
  54. data/lib/lepus/message/delivery_info.rb +72 -0
  55. data/lib/lepus/message/metadata.rb +99 -0
  56. data/lib/lepus/message.rb +88 -5
  57. data/lib/lepus/middleware_chain.rb +83 -0
  58. data/lib/lepus/primitive/hash.rb +29 -0
  59. data/lib/lepus/process.rb +24 -24
  60. data/lib/lepus/process_registry/backend.rb +49 -0
  61. data/lib/lepus/process_registry/file_backend.rb +108 -0
  62. data/lib/lepus/process_registry/message_builder.rb +72 -0
  63. data/lib/lepus/process_registry/rabbitmq_backend.rb +153 -0
  64. data/lib/lepus/process_registry.rb +56 -23
  65. data/lib/lepus/processes/base.rb +0 -5
  66. data/lib/lepus/processes/callbacks.rb +3 -0
  67. data/lib/lepus/processes/interruptible.rb +4 -8
  68. data/lib/lepus/processes/procline.rb +1 -1
  69. data/lib/lepus/processes/registrable.rb +1 -1
  70. data/lib/lepus/processes/runnable.rb +1 -1
  71. data/lib/lepus/processes.rb +15 -0
  72. data/lib/lepus/producer.rb +141 -30
  73. data/lib/lepus/producers/config.rb +46 -0
  74. data/lib/lepus/producers/definition.rb +48 -0
  75. data/lib/lepus/producers/hooks.rb +170 -0
  76. data/lib/lepus/producers/middleware_chain.rb +22 -0
  77. data/lib/lepus/producers/middlewares/correlation_id.rb +37 -0
  78. data/lib/lepus/producers/middlewares/header.rb +47 -0
  79. data/lib/lepus/producers/middlewares/instrumentation.rb +30 -0
  80. data/lib/lepus/producers/middlewares/json.rb +47 -0
  81. data/lib/lepus/producers/middlewares/unique.rb +67 -0
  82. data/lib/lepus/producers.rb +7 -0
  83. data/lib/lepus/prometheus/collector.rb +149 -0
  84. data/lib/lepus/prometheus/instrumentation.rb +168 -0
  85. data/lib/lepus/prometheus.rb +48 -0
  86. data/lib/lepus/publisher.rb +67 -0
  87. data/lib/lepus/supervisor/children_pipes.rb +25 -0
  88. data/lib/lepus/supervisor/lifecycle_hooks.rb +50 -0
  89. data/lib/lepus/supervisor/pidfiled.rb +1 -1
  90. data/lib/lepus/supervisor/registry_cleaner.rb +22 -0
  91. data/lib/lepus/supervisor.rb +129 -25
  92. data/lib/lepus/testing/exchange.rb +95 -0
  93. data/lib/lepus/testing/message_builder.rb +177 -0
  94. data/lib/lepus/testing/rspec_matchers.rb +258 -0
  95. data/lib/lepus/testing.rb +210 -0
  96. data/lib/lepus/unique.rb +18 -0
  97. data/lib/lepus/version.rb +1 -1
  98. data/lib/lepus/web/aggregator.rb +154 -0
  99. data/lib/lepus/web/api.rb +132 -0
  100. data/lib/lepus/web/app.rb +37 -0
  101. data/lib/lepus/web/management_api.rb +192 -0
  102. data/lib/lepus/web/respond_with.rb +28 -0
  103. data/lib/lepus/web.rb +238 -0
  104. data/lib/lepus.rb +39 -28
  105. data/test_offline.html +189 -0
  106. data/web/assets/css/styles.css +635 -0
  107. data/web/assets/js/app.js +6 -0
  108. data/web/assets/js/bootstrap.js +20 -0
  109. data/web/assets/js/controllers/connection_controller.js +44 -0
  110. data/web/assets/js/controllers/dashboard_controller.js +499 -0
  111. data/web/assets/js/controllers/queue_controller.js +17 -0
  112. data/web/assets/js/controllers/theme_controller.js +31 -0
  113. data/web/assets/js/offline-manager.js +233 -0
  114. data/web/assets/js/service-worker-manager.js +65 -0
  115. data/web/index.html +159 -0
  116. data/web/sw.js +144 -0
  117. metadata +177 -18
  118. data/lib/lepus/consumer_config.rb +0 -149
  119. data/lib/lepus/consumer_wrapper.rb +0 -46
  120. data/lib/lepus/lifecycle_hooks.rb +0 -49
  121. data/lib/lepus/middlewares/honeybadger.rb +0 -23
  122. data/lib/lepus/middlewares/json.rb +0 -35
  123. data/lib/lepus/middlewares/max_retry.rb +0 -57
  124. data/lib/lepus/processes/consumer.rb +0 -113
  125. data/lib/lepus/supervisor/config.rb +0 -45
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "testing/exchange"
4
+ require_relative "testing/rspec_matchers"
5
+ require_relative "testing/message_builder"
6
+
7
+ module Lepus
8
+ module Testing
9
+ class << self
10
+ # Enable fake publishing mode and consumer error re-raising
11
+ def enable!
12
+ fake_publisher!
13
+ consumer_raise_errors!
14
+ end
15
+
16
+ # Disable fake publishing mode and consumer error re-raising
17
+ def disable!
18
+ fake_publisher_disable!
19
+ consumer_capture_errors!
20
+ end
21
+
22
+ # Enable fake publishing mode for testing
23
+ # When enabled, messages are stored in fake exchanges instead of being sent to RabbitMQ
24
+ def fake_publisher!
25
+ @fake_publisher_enabled = true
26
+ end
27
+
28
+ # Check if fake publishing is enabled
29
+ def fake_publisher_enabled?
30
+ @fake_publisher_enabled == true
31
+ end
32
+
33
+ # Disable fake publishing mode for testing
34
+ def fake_publisher_disable!
35
+ @fake_publisher_enabled = false
36
+ end
37
+
38
+ # When enabled, consumer exceptions are re-raised instead of being converted to :reject
39
+ # This is useful in specs, so RSpec can capture failures instead of false positives
40
+ def consumer_raise_errors!
41
+ @consumer_raise_errors_enabled = true
42
+ end
43
+
44
+ # Disable consumer error re-raising (default behavior converts to :reject)
45
+ def consumer_capture_errors!
46
+ @consumer_raise_errors_enabled = false
47
+ end
48
+
49
+ # Returns true if consumer errors should be re-raised in tests
50
+ def consumer_raise_errors?
51
+ @consumer_raise_errors_enabled == true
52
+ end
53
+
54
+ # Clear all messages from all fake exchanges
55
+ def clear_all_messages!
56
+ Exchange.clear_all_messages!
57
+ end
58
+
59
+ # Get all fake exchanges
60
+ def exchanges
61
+ Exchange.all
62
+ end
63
+
64
+ # Get a specific fake exchange by name
65
+ def exchange(name)
66
+ Exchange[name]
67
+ end
68
+
69
+ # Get messages for a specific producer class
70
+ def producer_messages(producer_class)
71
+ return [] unless producer_class.respond_to?(:definition)
72
+
73
+ begin
74
+ exchange_name = producer_class.definition.exchange_name
75
+ Exchange[exchange_name].messages
76
+ rescue
77
+ # If there's an error getting the exchange name, return empty array
78
+ []
79
+ end
80
+ end
81
+
82
+ # Test a consumer with a message
83
+ # @param consumer_class [Class] The consumer class to test
84
+ # @param message_or_payload [Lepus::Message, Hash, String] The message to process
85
+ # @return [Symbol] The result of the consumer's perform method (:ack, :reject, :requeue, :nack)
86
+ def consumer_perform(consumer_class, message_or_payload)
87
+ message = build_message(message_or_payload)
88
+ consumer = consumer_class.new
89
+ consumer.process_delivery(message.delivery_info, message.metadata, message.payload)
90
+ end
91
+
92
+ # Create a message builder for custom scenarios
93
+ # @return [MessageBuilder] A new message builder instance
94
+ def message_builder
95
+ MessageBuilder.new
96
+ end
97
+
98
+ private
99
+
100
+ # Build a message from various input types
101
+ def build_message(message_or_payload)
102
+ case message_or_payload
103
+ when Lepus::Message
104
+ message_or_payload
105
+ when Hash
106
+ if message_or_payload.key?(:payload)
107
+ # Hash with payload and other options
108
+ payload = message_or_payload.delete(:payload)
109
+ MessageBuilder.new
110
+ .with_payload(payload)
111
+ .tap { |builder| apply_options(builder, message_or_payload) }
112
+ .build
113
+ else
114
+ # Hash as payload - create message with Hash payload
115
+ MessageBuilder.new
116
+ .with_payload(message_or_payload)
117
+ .with_content_type("application/json")
118
+ .build
119
+ end
120
+ when String
121
+ MessageBuilder.new
122
+ .with_payload(message_or_payload)
123
+ .with_content_type("text/plain")
124
+ .build
125
+ else
126
+ raise ArgumentError, "Invalid message type: #{message_or_payload.class}"
127
+ end
128
+ end
129
+
130
+ # Apply options to a message builder
131
+ def apply_options(builder, options)
132
+ options.each do |key, value|
133
+ method_name = "with_#{key}"
134
+ if builder.respond_to?(method_name)
135
+ builder.send(method_name, value)
136
+ end
137
+ end
138
+ end
139
+
140
+ # Override Publisher methods when testing module is loaded
141
+ def setup_publisher_overrides!
142
+ return if @overrides_setup
143
+
144
+ # Add messages method to Producer class
145
+ Lepus::Producer.class_eval do
146
+ # Get messages published by this producer (only available in testing mode)
147
+ # @return [Array<Hash>] Array of published messages
148
+ def self.messages
149
+ Lepus::Testing.producer_messages(self)
150
+ end
151
+ end
152
+
153
+ # Override Publisher#publish
154
+ Lepus::Publisher.class_eval do
155
+ alias_method :original_publish, :publish
156
+
157
+ def publish(message, **options)
158
+ return unless Lepus::Producers.exchange_enabled?(exchange_name)
159
+
160
+ if Lepus::Testing.fake_publisher_enabled?
161
+ return fake_publish(message, **options)
162
+ end
163
+
164
+ original_publish(message, **options)
165
+ end
166
+
167
+ # Override Publisher#channel_publish
168
+ alias_method :original_channel_publish, :channel_publish
169
+
170
+ def channel_publish(channel, message, **options)
171
+ raise ArgumentError, "channel is required" unless channel
172
+ return unless Lepus::Producers.exchange_enabled?(exchange_name)
173
+
174
+ if Lepus::Testing.fake_publisher_enabled?
175
+ return fake_publish(message, **options)
176
+ end
177
+
178
+ original_channel_publish(channel, message, **options)
179
+ end
180
+
181
+ # Add fake_publish method
182
+ private
183
+
184
+ def fake_publish(message, **options)
185
+ opts = Lepus::Publisher::DEFAULT_PUBLISH_OPTIONS.merge(options)
186
+
187
+ fake_message = {
188
+ exchange: exchange_name,
189
+ payload: message, # Store the original message, not the JSON string
190
+ routing_key: opts[:routing_key],
191
+ headers: opts[:headers],
192
+ persistent: opts[:persistent],
193
+ mandatory: opts[:mandatory],
194
+ immediate: opts[:immediate],
195
+ content_type: message.is_a?(String) ? "text/plain" : "application/json",
196
+ timestamp: Time.now
197
+ }
198
+
199
+ Lepus::Testing::Exchange[exchange_name].add_message(fake_message)
200
+ end
201
+ end
202
+
203
+ @overrides_setup = true
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ # Automatically setup overrides when the testing module is required
210
+ Lepus::Testing.send(:setup_publisher_overrides!)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "de_dupe"
5
+ rescue LoadError
6
+ raise LoadError,
7
+ "The 'de-dupe' gem is required for Lepus unique middleware. " \
8
+ "Add `gem 'de-dupe'` to your Gemfile."
9
+ end
10
+
11
+ unless DeDupe.config.redis
12
+ raise Lepus::Error,
13
+ "DeDupe is not configured with Redis. " \
14
+ "Call DeDupe.configure { |c| c.redis = Redis.new } before requiring 'lepus/unique'."
15
+ end
16
+
17
+ require_relative "producers/middlewares/unique"
18
+ require_relative "consumers/middlewares/unique"
data/lib/lepus/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lepus
4
- VERSION = "0.0.1.beta2"
4
+ VERSION = "0.1.0"
5
5
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "concurrent"
5
+
6
+ module Lepus
7
+ module Web
8
+ # Aggregates process heartbeats from RabbitMQ into in-memory state.
9
+ # Subscribes to the lepus.heartbeat fanout exchange and maintains
10
+ # a thread-safe cache of all processes across connected Lepus apps.
11
+ class Aggregator
12
+ HEARTBEAT_EXCHANGE = ProcessRegistry::RabbitmqBackend::HEARTBEAT_EXCHANGE
13
+
14
+ attr_reader :stale_threshold
15
+
16
+ def initialize(stale_threshold: nil)
17
+ @stale_threshold = stale_threshold || Lepus.config.process_alive_threshold
18
+ @processes = Concurrent::Map.new
19
+ @connection = nil
20
+ @channel = nil
21
+ @consumer = nil
22
+ @running = Concurrent::AtomicBoolean.new(false)
23
+ @pruning_task = nil
24
+ @mutex = Mutex.new
25
+ end
26
+
27
+ def start
28
+ return if @running.true?
29
+
30
+ @mutex.synchronize do
31
+ return if @running.true?
32
+
33
+ @running.make_true
34
+ setup_subscription
35
+ start_pruning_task
36
+ end
37
+ rescue => e
38
+ Lepus.logger.error("[Web::Aggregator] Failed to start: #{e.message}")
39
+ @running.make_false
40
+ end
41
+
42
+ def stop
43
+ @mutex.synchronize do
44
+ @running.make_false
45
+ @pruning_task&.shutdown
46
+ @consumer&.cancel
47
+ @channel&.close if @channel&.open?
48
+ @connection&.close if @connection&.open?
49
+ end
50
+ rescue => e
51
+ Lepus.logger.warn("[Web::Aggregator] Error during shutdown: #{e.message}")
52
+ ensure
53
+ @pruning_task = nil
54
+ @consumer = nil
55
+ @channel = nil
56
+ @connection = nil
57
+ end
58
+
59
+ def running?
60
+ @running.true?
61
+ end
62
+
63
+ def all_processes
64
+ prune_stale_entries
65
+ @processes.values.map { |data| data[:process] }
66
+ end
67
+
68
+ def find(id)
69
+ @processes[id]&.dig(:process)
70
+ end
71
+
72
+ def count
73
+ @processes.size
74
+ end
75
+
76
+ def clear
77
+ @processes.clear
78
+ end
79
+
80
+ private
81
+
82
+ def setup_subscription
83
+ @connection = Lepus.config.create_connection(suffix: "(web-aggregator)")
84
+ @channel = @connection.create_channel
85
+
86
+ exchange = @channel.fanout(
87
+ HEARTBEAT_EXCHANGE,
88
+ durable: false,
89
+ auto_delete: false
90
+ )
91
+
92
+ queue = @channel.queue("", exclusive: true, auto_delete: true)
93
+ queue.bind(exchange)
94
+
95
+ @consumer = queue.subscribe do |_delivery_info, _metadata, payload|
96
+ handle_message(payload)
97
+ end
98
+ end
99
+
100
+ def handle_message(payload)
101
+ data = JSON.parse(payload, symbolize_names: true)
102
+
103
+ case data[:type]
104
+ when "heartbeat"
105
+ process_heartbeat(data)
106
+ when "deregister"
107
+ process_deregistration(data)
108
+ end
109
+ rescue => e
110
+ Lepus.logger.warn("[Web::Aggregator] Failed to handle message: #{e.message}")
111
+ end
112
+
113
+ def process_heartbeat(data)
114
+ process_data = data[:process]
115
+ return unless process_data && process_data[:id]
116
+
117
+ metrics = data[:metrics] || {}
118
+
119
+ flat_process = process_data.merge(
120
+ rss_memory: metrics[:rss_memory] || 0,
121
+ connections: metrics[:connections] || 0,
122
+ consumers: metrics[:consumers] || []
123
+ )
124
+
125
+ @processes[process_data[:id]] = {
126
+ process: flat_process,
127
+ received_at: Time.now
128
+ }
129
+ end
130
+
131
+ def process_deregistration(data)
132
+ process_id = data[:process_id]
133
+ @processes.delete(process_id) if process_id
134
+ end
135
+
136
+ def start_pruning_task
137
+ @pruning_task = Concurrent::TimerTask.new(
138
+ execution_interval: [@stale_threshold / 2, 30].min
139
+ ) do
140
+ prune_stale_entries
141
+ end
142
+
143
+ @pruning_task.execute
144
+ end
145
+
146
+ def prune_stale_entries
147
+ threshold = Time.now - @stale_threshold
148
+ @processes.each do |id, data|
149
+ @processes.delete(id) if data[:received_at] < threshold
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ module Web
5
+ class API
6
+ def initialize(aggregator: nil, management_api: nil)
7
+ @aggregator = aggregator
8
+ @management_api = management_api
9
+ end
10
+
11
+ def call(env)
12
+ req = Rack::Request.new(env)
13
+ case req.path_info
14
+ when "/health"
15
+ Web::RespondWith.json(template: :health)
16
+ when "/processes"
17
+ processes_data
18
+ when "/queues"
19
+ queues_data
20
+ when "/connections"
21
+ connections_data
22
+ when "/exchanges"
23
+ exchanges_data
24
+ else
25
+ Web::RespondWith.json(template: :not_found)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def aggregator
32
+ @aggregator || Web.aggregator
33
+ end
34
+
35
+ def management_api
36
+ @management_api || Web.management_api
37
+ end
38
+
39
+ def processes_data
40
+ if aggregator&.running?
41
+ payload = aggregator.all_processes
42
+ Web::RespondWith.json(template: :ok, body: payload)
43
+ else
44
+ Web::RespondWith.json(template: :ok, body: [])
45
+ end
46
+ end
47
+
48
+ def queues_data
49
+ if management_api
50
+ raw_queues = management_api.queues
51
+ payload = annotate_queues_with_apps(raw_queues)
52
+ Web::RespondWith.json(template: :ok, body: payload)
53
+ else
54
+ Web::RespondWith.json(template: :ok, body: [])
55
+ end
56
+ rescue => e
57
+ Lepus.logger.warn("[Web::API] Failed to fetch queues: #{e.message}")
58
+ Web::RespondWith.json(template: :ok, body: [])
59
+ end
60
+
61
+ def connections_data
62
+ if management_api
63
+ payload = management_api.connections
64
+ Web::RespondWith.json(template: :ok, body: payload)
65
+ else
66
+ Web::RespondWith.json(template: :ok, body: [])
67
+ end
68
+ rescue => e
69
+ Lepus.logger.warn("[Web::API] Failed to fetch connections: #{e.message}")
70
+ Web::RespondWith.json(template: :ok, body: [])
71
+ end
72
+
73
+ def exchanges_data
74
+ if management_api
75
+ raw_exchanges = management_api.exchanges
76
+ payload = filter_exchanges(raw_exchanges)
77
+ Web::RespondWith.json(template: :ok, body: payload)
78
+ else
79
+ Web::RespondWith.json(template: :ok, body: [])
80
+ end
81
+ rescue => e
82
+ Lepus.logger.warn("[Web::API] Failed to fetch exchanges: #{e.message}")
83
+ Web::RespondWith.json(template: :ok, body: [])
84
+ end
85
+
86
+ def annotate_queues_with_apps(queues)
87
+ return queues unless aggregator&.running?
88
+
89
+ queue_app_map = build_queue_app_map
90
+ return queues if queue_app_map.empty?
91
+
92
+ queues.map do |queue|
93
+ app = queue_app_map[queue[:name]]
94
+ app ? queue.merge(application: app) : queue
95
+ end
96
+ end
97
+
98
+ def filter_exchanges(exchanges)
99
+ return exchanges if Lepus.config.web_show_all_exchanges
100
+ return exchanges unless aggregator&.running?
101
+
102
+ known_exchanges = build_known_exchange_names
103
+ return exchanges if known_exchanges.empty?
104
+
105
+ exchanges.select { |e| known_exchanges.include?(e[:name]) }
106
+ end
107
+
108
+ def build_queue_app_map
109
+ map = {}
110
+ aggregator.all_processes.each do |process|
111
+ app_name = process[:application]
112
+ next unless app_name
113
+
114
+ (process[:consumers] || []).each do |consumer|
115
+ map[consumer[:queue]] = app_name if consumer[:queue]
116
+ end
117
+ end
118
+ map
119
+ end
120
+
121
+ def build_known_exchange_names
122
+ names = Set.new
123
+ aggregator.all_processes.each do |process|
124
+ (process[:consumers] || []).each do |consumer|
125
+ names << consumer[:exchange] if consumer[:exchange]
126
+ end
127
+ end
128
+ names
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lepus
4
+ module Web
5
+ class App
6
+ def self.build
7
+ root = Web.assets_path
8
+
9
+ Rack::Builder.new do
10
+ use Rack::Static,
11
+ urls: ["/assets", "/sw.js"],
12
+ root: root.to_s
13
+
14
+ map "/api" do
15
+ run Lepus::Web::API.new
16
+ end
17
+
18
+ run lambda { |env|
19
+ req = Rack::Request.new(env)
20
+ path = req.path_info
21
+
22
+ if path == "/" || path == "/index.html"
23
+ [200, {"content-type" => "text/html"}, [Web.render_index(env)]]
24
+ else
25
+ file_path = root.join(path.sub(%r{^/}, ""))
26
+ if File.file?(file_path)
27
+ [200, {"content-type" => Web.mime_for(file_path)}, [File.binread(file_path)]]
28
+ else
29
+ [200, {"content-type" => "text/html"}, [Web.render_index(env)]]
30
+ end
31
+ end
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end