happn 1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 06b2f20b019373ac23f020f1b5c060f5f5185dddba551ace75ef4fc9a129d8ec
4
+ data.tar.gz: b7b767f5e7abb62eb33661afaea52b7a2695674ddd2b18255002c18ad4923fed
5
+ SHA512:
6
+ metadata.gz: 94cf0746e1fac94dfde7da681c1abe4c4632411d79d46123d546b41bc84a87ece483e5a9e5b0094fba1e98518fc15d9aa202ea65cb8e05accce59176122059a0
7
+ data.tar.gz: c63c774536d302c301b33ba1d37e828abddf2eae9e9783b8e60ae44ea45601a3f221df5ecc13265381c00ac3ae5b25078e54e1782855b604889a06f2d38515ca
data/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ### [1.1.0] - 2026-07-30
9
+
10
+ * Remove the `activesupport` dependency: `Happn::Configuration` no longer relies on `ActiveSupport::Configurable`, and `Happn::Event` no longer relies on `String#to_datetime` / `Object#try`
11
+ * Relax the `bundler` development dependency from a pinned `1.12.5` to `>=1.17`
12
+
13
+ ### [1.0.3] - 2024-12-04
14
+
15
+ * Replace the use of `to_time` to `to_datetime`
16
+
17
+ ### [1.0.2] - 2021-11-06
18
+
19
+ * Breaking change : force every `port` to be an integer
20
+ * Add `management_options`
21
+
22
+ ### [1.0.0] - 2021-11-05
23
+
24
+ * Add `bunny_options`
25
+ * Add `rabbitmq_management_scheme`
26
+ * MIT License
27
+
28
+ ### [0.1.4]
29
+
30
+ * Add convenient method `Event.request_metadata`
31
+
32
+ ### [0.1.3]
33
+
34
+ * Use of `rabbitmq_http_api_client >= 2.0.0` and `bunny >= 2.19.0`
35
+
36
+ ### [0.1.2]
37
+
38
+ * Use of `rabbitmq_http_api_client:1.14.0`, which supports `faraday >= 1`
39
+
40
+ ### [0.1.1]
41
+
42
+ * Happn now raise an exception instead of exiting the process when the consumption of an event fails, allowing the gem user to decide how to handle it.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # Gem for happn
2
+
3
+ Happn connects a RabbitMQ exchange and listens for CREPE events (possibly generated using `flu-rails`) sequentially.
4
+ Happn helps developers to create _"Projectors"_ that define how to match and consume events.
5
+
6
+ This gem connects a single RabbitMQ queue and bind it automatically to its exchange. These bindings are defined by developers through "matchers" when loading projectors.
7
+
8
+ ## Requirements
9
+
10
+ * Ruby 2.2
11
+ * Tested with RabbitMQ 3.5.8
12
+ * `happn-ruby` works with or without Rails (tested with Rails 4 and 5).
13
+
14
+ ## Installation
15
+
16
+ Add the gem to your project's Gemfile:
17
+
18
+ ```ruby
19
+ gem "happn", git: "https://github.com/crepesourcing/happn-ruby.git"
20
+ ```
21
+
22
+ Then, configure `Happn`. If you use Rails, you can create an initializer into your Rails app (`config/initializers/happn.rb`). This code can be called anywhere before starting `Happn.init`.
23
+ ```ruby
24
+ Happn.configure do |config|
25
+ config.logger = Rails.logger
26
+ config.rabbitmq_host = ENV["RABBITMQ_HOST"]
27
+ config.rabbitmq_port = ENV["RABBITMQ_PORT"]
28
+ config.rabbitmq_management_port = ENV["RABBITMQ_MANAGEMENT_PORT"]
29
+ config.rabbitmq_user = ENV["RABBITMQ_USER"]
30
+ config.rabbitmq_password = ENV["RABBITMQ_PASSWORD"]
31
+ config.rabbitmq_queue_name = ENV["CONSUMER_QUEUE_NAME"]
32
+ config.rabbitmq_exchange_name = ENV["RABBITMQ_EXCHANGE_NAME"]
33
+ config.rabbitmq_exchange_durable = ENV["RABBITMQ_EXCHANGE_DURABLE"] == "true"
34
+ config.projector_classes = [LoggerProjector]
35
+ config.bunny_options = {}
36
+ end
37
+ ```
38
+
39
+ Each configuration is detailed below.
40
+
41
+ ## About queues
42
+
43
+ * `Happn` consumes a single queue through the RabbitMQ's [Topic Exchange Model](https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchange-topic).
44
+ * If the queue does not exist when `Happn` starts, it is created automatically.
45
+ * When connecting a queue, please be careful that each connection parameter must match the existing queue's parameters. For instance, the value of `x-queue-mode` must match to avoid a `PRECONDITION FAILED` error.
46
+ * All bindings between queues and their exchange are reset when starting `Happn`. Based on all the projectors that have been registered (option `projector_classes`), `Happn` detects which events must be consumed and binds its queue to the exchange depending on these event matchers.
47
+
48
+ ## About projectors
49
+
50
+ A projector:
51
+ * defines one or multiple matchers to detect which events must be consumed. Matchers can be declared using 4 event properties
52
+ * `emitter`: _e.g._ `Facebook` or `MyInternalApi` (`:all` or no value mean "all emitters")
53
+ * `kind`: _e.g._ `entity_change` or `kind` (`:all` or no value mean "all kinds")
54
+ * `name`: _e.g._ `create country` or `request to destroy bunnies` (`:all` or no value mean "all names")
55
+ * `status`: _e.g._ `:new` or `:replayed` (`:all` or no value mean "all statuses")
56
+ * defines how to consume these events.
57
+
58
+ When a projector raises an Error, `Happn` stops.
59
+
60
+ ## Usage
61
+
62
+ ### Start Up
63
+
64
+ Starting `Happn` consumes events sequentially. For instance, it can be started from a `Rake` tasks:
65
+ ```ruby
66
+ namespace :events do
67
+ desc "Listen all events and consume them."
68
+ task consume: :environment do
69
+ Happn.init
70
+ Happn.start
71
+ end
72
+ end
73
+ ```
74
+
75
+ ### Define a Projector
76
+
77
+ A projector is a class that defines how to consume one or multiple types of events. This class must:
78
+
79
+ * extend `Happn::Projector`.
80
+ * use its `on` method to declare _which_ events to match and _how_ to consume them. This must be done in a `define_handlers` method.
81
+
82
+ ```ruby
83
+ class LoggerProjector < Happn::Projector
84
+ def define_handlers
85
+ on emitter: "MyApplication", name: "create country" do |event|
86
+ Rails.logger("A country has been created and generated an event with id #{event.id}")
87
+ end
88
+
89
+ on kind: "request", status: :new do |event|
90
+ Rails.logger("This is a new request to the controller: #{event.data["controller_name"]}")
91
+ end
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### Registering all projectors automatically
97
+
98
+ If all subclass of `Happn::Projector` should be registered seamlessly, the configuration can declare the following code (using Rails):
99
+ ```ruby
100
+ Happn.configure do |config|
101
+ Rails.application.eager_load!
102
+ config.projector_classes = Happn::Projector.descendants
103
+ end
104
+ ```
105
+
106
+ When Zeitwerk is defined and when `Rails.application.eager_load!` returns `false`, you should call `Zeitwerk::Loader.eager_load_all`.
107
+
108
+ ## Overall configuration options
109
+
110
+ All options have a default value. However, all of them can be changed in your `Happn.configure` block.
111
+
112
+ | Option | Default Value | Type | Required? | Description | Example |
113
+ | ---- | ----- | ------ | ----- | ------ | ----- |
114
+ | `logger` | `Logger.new(STDOUT)`| Logger | Optional | The logger used by `happn` | `Rails.logger` |
115
+ | `rabbitmq_host` | `"localhost"` | String | Required | RabbitMQ exchange's host. | `"192.168.42.42"` |
116
+ | `rabbitmq_port` | `5672` | Integer | Required | RabbitMQ exchange's port. | `1234` |
117
+ | `rabbitmq_user` | `""` | String | Required | RabbitMQ exchange's username. | `"root"` |
118
+ | `rabbitmq_password` | `""` | String | Required | RabbitMQ exchange's password. | `"pouet"` |
119
+ | `rabbitmq_exchange_name` | `"events"` | String | Required | RabbitMQ exchange's name. | `"myproject"` |
120
+ | `rabbitmq_management_scheme` | `"http"` | String | Required | RabbitMQ exchange's management scheme. This scheme is used when `happn` must access metadata information about queues, messages, etc. This port is used to create/delete bindings between the queue and its exchange. | `"https"` |
121
+ | `rabbitmq_management_port` | `15672` | Integer | Required | RabbitMQ exchange's management port. This port is used when `happn` must access metadata information about queues, messages, etc. This port is used to create/delete bindings between the queue and its exchange. | `15671` |
122
+ | `rabbitmq_queue_name` | `"happn-queue"` | String | Required | The RabbitMQ queue to create, bind and consume. If the queue does not exist, it will be created at startup. | `"my-queue"` |
123
+ | `rabbitmq_exchange_durable` | `true` | Boolean | Optional | Make the RabbitMQ's exchange durable or not. From RabbitMQ's [documentation](https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchanges): _"Durable exchanges survive broker restart whereas transient exchanges do not (they have to be redeclared when broker comes back online)."_ | `false` |
124
+ | `rabbitmq_queue_mode` | `nil` | String | Optional | When creating the queue, this option can be passed to set `x-queue-mode`. For instance, a queue can be made _"lazy"_ by passing `"lazy"` as a value. See [RabbitMQ's documentation](https://www.rabbitmq.com/lazy-queues.html) for more details. | `lazy` |
125
+ | `rabbitmq_prefetch_size` | `10` | Integer | Optional | Also known as RabbitMQ's QOS. From the [RabbitMQ's documentation](http://www.rabbitmq.com/consumer-prefetch.html): _"AMQP specifies the basic.qos method to allow you to limit the number of unacknowledged messages on a channel (or connection) when consuming (aka "prefetch count")."_ | `1000` |
126
+ | `projector_classes` | `[]` | Array of constants | Required | All Projector classes to register. This value can be generated by reading all descendant classes from `Happn::Projector`. | `[MyProjector]` |
127
+ | `bunny_options` | `{}` | Hash of symbols | Optional | Additional options to add when connecting the RabbitMQ broker. This overrides the existing options with the same name. | `{ verify_peer: true }` |
128
+ | `management_options` | `{}` | Hash of symbols | Optional | Additional options to add when accessing the RabbitMQ Managmement. This overrides the existing options with the same name. The options are defined at https://github.com/ruby-amqp/rabbitmq_http_api_client | `{ verify: false }` |
129
+ | `on_error` | `nil` | `block` with an argument `exception` | false | When the consumption of an event raises an Error, the consumption exits. However, this block can be called before exiting the consumption execution. | `lambda { |exception| Raven.capture_exception(exception) }` (see Sentry's [documentation](https://github.com/getsentry/raven-ruby) |
data/happn.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "happn/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "happn"
8
+ spec.version = Happn::VERSION
9
+ spec.authors = ["Commuty"]
10
+ spec.email = ["support@commuty.net"]
11
+ spec.summary = "Gem to connect a RabbitMQ exchange and listen for events."
12
+ spec.description = "Gem to connect a RabbitMQ exchange and listen for events."
13
+ spec.homepage = "https://gitlab.spin42.me/commuty/happn"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", ">=1.17"
22
+ spec.add_development_dependency "rake", ">=12.0"
23
+ spec.add_development_dependency "rspec", "~>3.0"
24
+ spec.add_dependency "bunny", ">=2.19.0"
25
+ spec.add_dependency "rabbitmq_http_api_client", ">=2.0.0"
26
+ end
@@ -0,0 +1,20 @@
1
+ module Happn
2
+ class Configuration
3
+ attr_accessor :logger,
4
+ :rabbitmq_host,
5
+ :rabbitmq_port,
6
+ :rabbitmq_management_port,
7
+ :rabbitmq_management_scheme,
8
+ :rabbitmq_user,
9
+ :rabbitmq_password,
10
+ :rabbitmq_queue_name,
11
+ :rabbitmq_exchange_name,
12
+ :rabbitmq_exchange_durable,
13
+ :rabbitmq_queue_mode,
14
+ :rabbitmq_prefetch_size,
15
+ :projector_classes,
16
+ :bunny_options,
17
+ :management_options,
18
+ :on_error
19
+ end
20
+ end
@@ -0,0 +1,105 @@
1
+ require "date"
2
+
3
+ module Happn
4
+ class Event
5
+
6
+ def initialize(args)
7
+ @meta = deep_underscore_keys(args.fetch("meta"))
8
+ @data = deep_underscore_keys(args.fetch("data"))
9
+ end
10
+
11
+ def data
12
+ @data
13
+ end
14
+
15
+ def user_metadata
16
+ @data[:user_metadata]
17
+ end
18
+
19
+ def request_metadata
20
+ @data[:request_metadata]
21
+ end
22
+
23
+ def changes
24
+ @data[:changes]
25
+ end
26
+
27
+ def changes=(new_changes)
28
+ @data[:changes] = new_changes
29
+ end
30
+
31
+ def add_change(name, value)
32
+ new_value = value == "" ? nil : value
33
+ changes[name.to_sym] = [nil, new_value]
34
+ end
35
+
36
+ def associations
37
+ @data[:associations]
38
+ end
39
+
40
+ def timestamp
41
+ DateTime.parse(@meta[:timestamp])
42
+ end
43
+
44
+ def id
45
+ @meta[:id]
46
+ end
47
+
48
+ def name
49
+ @meta[:name]
50
+ end
51
+
52
+ def status
53
+ @meta[:status]
54
+ end
55
+
56
+ def kind
57
+ @meta[:kind]
58
+ end
59
+
60
+ def emitter
61
+ @meta[:emitter]
62
+ end
63
+
64
+ def change_after(attribute_name)
65
+ changes[attribute_name.to_sym]&.last
66
+ end
67
+
68
+ def change_before(attribute_name)
69
+ changes[attribute_name.to_sym]&.first
70
+ end
71
+
72
+ def has_change?(attribute_name)
73
+ !changes[attribute_name].nil?
74
+ end
75
+
76
+ def delete_change(attribute_name)
77
+ changes.delete(attribute_name)
78
+ end
79
+
80
+ private
81
+
82
+ def underscore_key(key)
83
+ underscore(key).to_sym
84
+ end
85
+
86
+ def deep_underscore_keys(value)
87
+ case value
88
+ when Array
89
+ value.map(&method(:deep_underscore_keys))
90
+ when Hash
91
+ Hash[value.map { |key, value| [underscore_key(key), deep_underscore_keys(value)] }]
92
+ else
93
+ value
94
+ end
95
+ end
96
+
97
+ def underscore(camel_cased_word)
98
+ camel_cased_word.gsub(/::/, '/').
99
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
100
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
101
+ tr("-", "_").
102
+ downcase
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,129 @@
1
+ require "bunny"
2
+ require "rabbitmq/http/client"
3
+
4
+ module Happn
5
+ class EventConsumer
6
+ def initialize(logger, configuration, subscription_repository)
7
+ @configuration = configuration
8
+ @logger = logger
9
+ @subscription_repository = subscription_repository
10
+ @queue_name = @configuration.rabbitmq_queue_name
11
+ options = {
12
+ host: @configuration.rabbitmq_host,
13
+ port: @configuration.rabbitmq_port&.to_i,
14
+ user: @configuration.rabbitmq_user,
15
+ password: @configuration.rabbitmq_password,
16
+ automatically_recover: true
17
+ }.merge(@configuration.bunny_options || {})
18
+ @connection = Bunny.new(options)
19
+
20
+ management_options = {
21
+ username: @configuration.rabbitmq_user,
22
+ password: @configuration.rabbitmq_password
23
+ }.merge(@configuration.management_options || {})
24
+ @management_client = RabbitMQ::HTTP::Client.new("#{@configuration.rabbitmq_management_scheme || "http"}://#{@configuration.rabbitmq_host}:#{@configuration.rabbitmq_management_port}/", management_options)
25
+ end
26
+
27
+ def wait_until_connected
28
+ connected = false
29
+ while !connected
30
+ begin
31
+ connect
32
+ connected = true
33
+ rescue Bunny::TCPConnectionFailedForAllHosts
34
+ @logger.warn("RabbitMQ connection failed, try again in 1 second.")
35
+ sleep 2
36
+ end
37
+ end
38
+ end
39
+
40
+ def start
41
+ wait_until_connected
42
+ consume
43
+ end
44
+
45
+ private
46
+
47
+ def connect
48
+ @connection.start
49
+ channel_number = nil
50
+ consumer_pool_size = 1
51
+ consumer_pool_abort_on_exception = true
52
+ @channel = @connection.create_channel(channel_number, consumer_pool_size, consumer_pool_abort_on_exception)
53
+ @channel.on_uncaught_exception do |exception|
54
+ @configuration.on_error&.call(exception)
55
+ @logger.error("An error occurred. Exiting events consumption.")
56
+ exit(1)
57
+ end
58
+ @channel.basic_qos(@configuration.rabbitmq_prefetch_size)
59
+ arguments = {}
60
+ arguments["x-queue-mode"] = @configuration.rabbitmq_queue_mode unless @configuration.rabbitmq_queue_mode.nil?
61
+ @queue = @channel.queue(@queue_name, durable: true, arguments: arguments)
62
+ exchange = @channel.send(:topic,
63
+ @configuration.rabbitmq_exchange_name,
64
+ durable: @configuration.rabbitmq_exchange_durable)
65
+
66
+ routing_keys = @subscription_repository.find_all.map do | subscription |
67
+ subscription.query.to_routing_key
68
+ end
69
+ routing_keys.uniq.each do | routing_key |
70
+ @logger.info("Bind exchange to queue with routing key : #{routing_key}")
71
+ @queue.bind(exchange, routing_key: routing_key)
72
+ end
73
+
74
+ unbind_useless_routing_keys(@queue, exchange, routing_keys)
75
+
76
+ @logger.info("Ready!")
77
+ end
78
+
79
+ def consume
80
+ options = {
81
+ manual_ack: true,
82
+ block: true
83
+ }
84
+ @queue.subscribe(options) do | delivery_info, _properties, event |
85
+ begin
86
+ handle_message(event, delivery_info)
87
+ rescue => exception
88
+ handle_exception(exception, delivery_info)
89
+ end
90
+ end
91
+ end
92
+
93
+ def handle_message(message, delivery_info)
94
+ event = Event.new(JSON.parse(message))
95
+ subscriptions = @subscription_repository.find_subscriptions_for(event)
96
+
97
+ @logger.info("Executing #{subscriptions.size} handlers for event '#{event.name}' with id: #{event.id}.")
98
+ subscriptions.each do | subscription |
99
+ projector = subscription.projector
100
+ handler = subscription.handler
101
+ projector.instance_exec(event, &handler)
102
+ end
103
+ @channel.acknowledge(delivery_info.delivery_tag, false)
104
+ end
105
+
106
+ def handle_exception(exception, delivery_info)
107
+ @logger.error(exception)
108
+ @channel.reject(delivery_info.delivery_tag, true)
109
+ @logger.fatal("Can't handle event, exit.")
110
+ raise exception
111
+ end
112
+
113
+ private
114
+
115
+ def unbind_useless_routing_keys(queue, exchange, useful_routing_keys)
116
+ all_routing_keys = find_all_routing_keys_of(queue)
117
+ keys_to_remove = all_routing_keys - useful_routing_keys
118
+ keys_to_remove.each do | routing_key |
119
+ @queue.unbind(exchange, routing_key: routing_key)
120
+ end
121
+ end
122
+
123
+ def find_all_routing_keys_of(queue)
124
+ @management_client.list_queue_bindings("/", queue.name).map do | binding |
125
+ binding.routing_key
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,17 @@
1
+ module Happn
2
+ class Projector
3
+
4
+ def initialize(logger, subscription_repository)
5
+ @logger = logger
6
+ @subscription_repository = subscription_repository
7
+ end
8
+
9
+ def define_handlers
10
+ end
11
+
12
+ def on(emitter: :all, kind: :all, name: :all, status: :all, &block)
13
+ query = Query.new(emitter, kind, name, status)
14
+ @subscription_repository.register(query, self, &block)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ module Happn
2
+ class Query
3
+
4
+ attr_reader :emitter, :kind, :name, :status
5
+
6
+ def initialize(emitter, kind, name, status)
7
+ if emitter.to_s.include?(".") || kind.to_s.include?(".") || name.to_s.include?(".") || status.to_s.include?(".")
8
+ raise "'Dot' is not a valid character"
9
+ end
10
+
11
+ @emitter = emitter
12
+ @kind = kind
13
+ @name = name
14
+ @status = status
15
+ end
16
+
17
+ def to_routing_key
18
+ "#{to_expression(@status)}.#{to_expression(@emitter)}.#{to_expression(@kind)}.#{to_expression(@name)}"
19
+ end
20
+
21
+ private
22
+
23
+ def to_expression(query_expression)
24
+ query_expression == :all ? "*" : query_expression
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ module Happn
2
+ class Subscription
3
+
4
+ attr_reader :handler, :projector, :query
5
+
6
+ def initialize(query, projector, &handler)
7
+ @query = query
8
+ @projector = projector
9
+ @handler = handler
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,57 @@
1
+ module Happn
2
+ class SubscriptionRepository
3
+
4
+ def initialize(logger)
5
+ @subscriptions = {}
6
+ @logger = logger
7
+ end
8
+
9
+ def register(query, projector, &block)
10
+ subscription = Subscription.new(query, projector, &block)
11
+ emitter = query.emitter.to_s
12
+ kind = query.kind.to_s
13
+ name = query.name.to_s
14
+ status = query.status.to_s
15
+ @subscriptions[status] ||= {}
16
+ @subscriptions[status][emitter] ||= {}
17
+ @subscriptions[status][emitter][kind] ||= {}
18
+ @subscriptions[status][emitter][kind][name] ||= []
19
+ @subscriptions[status][emitter][kind][name].push(subscription)
20
+ @logger.info("Subscribe projector '#{projector.class}' to query : [#{status}][#{emitter}][#{kind}][#{name}]")
21
+ end
22
+
23
+ def find_all
24
+ flatten(@subscriptions)
25
+ end
26
+
27
+ def find_subscriptions_for(event)
28
+ possible_event_statuses = ["all", event.status.to_s]
29
+ possible_event_emitters = ["all", event.emitter.to_s]
30
+ possible_event_names = ["all", event.name.to_s]
31
+ possible_event_kinds = ["all", event.kind.to_s]
32
+ subscriptions = []
33
+ possible_event_statuses.each do | status |
34
+ possible_event_emitters.each do | emitter |
35
+ possible_event_kinds.each do | kind |
36
+ possible_event_names.each do | name |
37
+ subscriptions += @subscriptions.dig(status, emitter, kind, name) || []
38
+ end
39
+ end
40
+ end
41
+ end
42
+ subscriptions
43
+ end
44
+
45
+ private
46
+
47
+ def flatten(item)
48
+ if item.instance_of?(Hash)
49
+ item.values.inject([]) do | result, value |
50
+ result += flatten(value)
51
+ end
52
+ else
53
+ item
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module Happn
2
+ VERSION = "1.1.0"
3
+ end
data/lib/happn.rb ADDED
@@ -0,0 +1,71 @@
1
+ require_relative "happn/event"
2
+ require_relative "happn/query"
3
+ require_relative "happn/version"
4
+ require_relative "happn/configuration"
5
+ require_relative "happn/event_consumer"
6
+ require_relative "happn/subscription"
7
+ require_relative "happn/subscription_repository"
8
+ require_relative "happn/projector"
9
+ require "logger"
10
+
11
+ module Happn
12
+ def self.configure
13
+ yield @configuration ||= Happn::Configuration.new
14
+ end
15
+
16
+ def self.config
17
+ @configuration
18
+ end
19
+
20
+ def self.logger
21
+ @logger
22
+ end
23
+
24
+ def self.init
25
+ @logger = @configuration.logger || Logger.new(STDOUT)
26
+ subscription_repository = SubscriptionRepository.new(@logger)
27
+ Happn::register(@configuration.projector_classes, subscription_repository)
28
+ @event_consumer = EventConsumer.new(@logger, @configuration, subscription_repository)
29
+ end
30
+
31
+ def self.start
32
+ @event_consumer.start
33
+ end
34
+
35
+ def self.create_queue_only
36
+ Happn.init
37
+ @event_consumer.wait_until_connected
38
+ end
39
+
40
+ configure do |config|
41
+ config.logger = nil
42
+ config.rabbitmq_host = "localhost"
43
+ config.rabbitmq_port = 5672
44
+ config.rabbitmq_management_scheme = "http"
45
+ config.rabbitmq_management_port = 15672
46
+ config.rabbitmq_user = ""
47
+ config.rabbitmq_password = ""
48
+ config.rabbitmq_queue_name = "happn-queue"
49
+ config.rabbitmq_exchange_name = "events"
50
+ config.rabbitmq_exchange_durable = true
51
+ config.rabbitmq_queue_mode = nil
52
+ config.rabbitmq_prefetch_size = 10
53
+ config.projector_classes = []
54
+ config.on_error = nil
55
+ config.bunny_options = {}
56
+ config.management_options = {}
57
+ config.on_error = nil
58
+ end
59
+
60
+ private
61
+
62
+ def self.register(projector_classes, subscription_repository)
63
+ @logger.info("#{projector_classes.size} projector are going to be registered...")
64
+ projector_classes.each do | projector_class |
65
+ projector = projector_class.new(@logger, subscription_repository)
66
+ projector.define_handlers
67
+ @logger.info("Projector '#{projector_class}' registered")
68
+ end
69
+ end
70
+
71
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: happn
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Commuty
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.17'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.17'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '12.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '12.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: bunny
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 2.19.0
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 2.19.0
68
+ - !ruby/object:Gem::Dependency
69
+ name: rabbitmq_http_api_client
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 2.0.0
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 2.0.0
82
+ description: Gem to connect a RabbitMQ exchange and listen for events.
83
+ email:
84
+ - support@commuty.net
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - CHANGELOG.md
90
+ - Gemfile
91
+ - MIT-LICENSE
92
+ - README.md
93
+ - happn.gemspec
94
+ - lib/happn.rb
95
+ - lib/happn/configuration.rb
96
+ - lib/happn/event.rb
97
+ - lib/happn/event_consumer.rb
98
+ - lib/happn/projector.rb
99
+ - lib/happn/query.rb
100
+ - lib/happn/subscription.rb
101
+ - lib/happn/subscription_repository.rb
102
+ - lib/happn/version.rb
103
+ homepage: https://gitlab.spin42.me/commuty/happn
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubygems_version: 4.0.17
122
+ specification_version: 4
123
+ summary: Gem to connect a RabbitMQ exchange and listen for events.
124
+ test_files: []