lontara_utilities 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a5cf89378d3ae64ad5517aa214ff99ba9aa73c419266d8392e7291e8f13bd47e
4
+ data.tar.gz: 1b1ec1916807d80d5fcf69c13139647f2bb34475dacc4abe036a4207e3812d1a
5
+ SHA512:
6
+ metadata.gz: ca4ceeba469609e6ebc470ccae64869a45bae24b0cc822aec801f017f953db6320587fcc8017e6fbf7e707e946504f69db4af97806bd6fc15dbfe5432b3bb422
7
+ data.tar.gz: 93aaa2a7abfc9733c83b0f1be0b6e6473f1ac6104afe51c92a212f7f7afc6aa483e4c4b446f191253a88294d4107ae11e34334fd241c72740956f9f4e535cfaa
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ lontara.gem
2
+ recovery_codes
3
+ build.sh
data/.rubocop.yml ADDED
@@ -0,0 +1,17 @@
1
+ # The behavior of RuboCop can be controlled via the .rubocop.yml
2
+ # configuration file. It makes it possible to enable/disable
3
+ # certain cops (checks) and to alter their behavior if they accept
4
+ # any parameters. The file can be placed either in your home
5
+ # directory or in some project directory.
6
+ #
7
+ # RuboCop will start looking for the configuration file in the directory
8
+ # where the inspected file is and continue its way up to the root directory.
9
+ #
10
+ # See https://docs.rubocop.org/rubocop/configuration
11
+
12
+ AllCops:
13
+ TargetRubyVersion: 3.1
14
+ NewCops: enable
15
+
16
+ Style/FetchEnvVar:
17
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,70 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ lontara_utilities (3.0.0)
5
+ bunny (~> 2.20)
6
+ connection_pool (~> 2.3)
7
+ faraday (~> 2.7)
8
+ json (~> 2.6)
9
+ securerandom (~> 0.2)
10
+ uri (~> 0.12)
11
+
12
+ GEM
13
+ remote: https://rubygems.org/
14
+ specs:
15
+ addressable (2.8.1)
16
+ public_suffix (>= 2.0.2, < 6.0)
17
+ amq-protocol (2.3.2)
18
+ bunny (2.20.3)
19
+ amq-protocol (~> 2.3, >= 2.3.1)
20
+ sorted_set (~> 1, >= 1.0.2)
21
+ byebug (11.1.3)
22
+ connection_pool (2.3.0)
23
+ crack (0.4.5)
24
+ rexml
25
+ diff-lcs (1.5.0)
26
+ faraday (2.7.4)
27
+ faraday-net_http (>= 2.0, < 3.1)
28
+ ruby2_keywords (>= 0.0.4)
29
+ faraday-net_http (3.0.2)
30
+ hashdiff (1.0.1)
31
+ json (2.6.3)
32
+ public_suffix (5.0.1)
33
+ rbtree (0.4.6)
34
+ rexml (3.2.5)
35
+ rspec (3.12.0)
36
+ rspec-core (~> 3.12.0)
37
+ rspec-expectations (~> 3.12.0)
38
+ rspec-mocks (~> 3.12.0)
39
+ rspec-core (3.12.1)
40
+ rspec-support (~> 3.12.0)
41
+ rspec-expectations (3.12.2)
42
+ diff-lcs (>= 1.2.0, < 2.0)
43
+ rspec-support (~> 3.12.0)
44
+ rspec-mocks (3.12.3)
45
+ diff-lcs (>= 1.2.0, < 2.0)
46
+ rspec-support (~> 3.12.0)
47
+ rspec-support (3.12.0)
48
+ ruby2_keywords (0.0.5)
49
+ securerandom (0.2.2)
50
+ set (1.0.3)
51
+ sorted_set (1.0.3)
52
+ rbtree
53
+ set (~> 1.0)
54
+ uri (0.12.0)
55
+ webmock (3.18.1)
56
+ addressable (>= 2.8.0)
57
+ crack (>= 0.3.2)
58
+ hashdiff (>= 0.4.0, < 2.0.0)
59
+
60
+ PLATFORMS
61
+ x86_64-linux
62
+
63
+ DEPENDENCIES
64
+ byebug (~> 11.1)
65
+ lontara_utilities!
66
+ rspec (~> 3.12)
67
+ webmock (~> 3.18)
68
+
69
+ BUNDLED WITH
70
+ 2.3.26
data/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # Lontara Utilities
2
+
3
+ > Build Version: 3.0.0
4
+
5
+ This is a collection of utilities for your Ruby project provided by Lontara. These are:
6
+ - `LontaraUtilities` - a collection of utilities
7
+ - `HTTPClient` - Client for HTTP Connection
8
+ - `RMQ::Client` - Client for RabbitMQ Connection
9
+ - `RMQ::Server` - Server for RabbitMQ Connection
10
+ - `Git` - An interface to Git Branch and Release
11
+ - `BaseError` - Custom Error inherited from StandardError with some features
12
+
13
+ ## Installation
14
+ Open your terminal and type:
15
+ ```bash
16
+ gem install lontara_utilities
17
+ ```
18
+
19
+ or add this line to your application's Gemfile:
20
+ ```ruby
21
+ gem 'lontara_utilities'
22
+ ```
23
+
24
+ If you wanna use the `git` method, you need to install `git` first, and then initialize it in your project directory.
25
+
26
+ For Rails project, you can use initializer to run RMQ Server. Just add `rmq.rb` file in `config/initializers` directory, and add this code:
27
+
28
+ > Note: Currently we only support `RPCConsumer` (from RPC Pattern) or `Subscriber` (from Pub/Sub Messaging Pattern) server.
29
+
30
+ ```ruby
31
+ require 'lontara_utilities/rmq'
32
+
33
+ LontaraUtilities::RMQ.start(
34
+ server: 'RPCConsumer',
35
+ url: ENV.fetch('RABBITMQ_URL', 'amqp://guest:guest@rmqserver:5672'),
36
+ queue: ENV.fetch('RABBITMQ_QUEUE_VOUCHER', 'lontara-dev.voucher')
37
+ )
38
+ ```
39
+
40
+ ## Usage
41
+ You can require this utilities by adding `require 'lontara_utilities'`, or you can require each utilities separately.
42
+
43
+ ```ruby
44
+ require 'lontara_utilities/http_client'
45
+
46
+ LontaraUtilities::HTTPClient.get('http://example.com')
47
+ ```
48
+
49
+ But, if `lontara_utilities` or it's alias: `lontara` is required, you can call these methods using method format.
50
+
51
+ ```ruby
52
+ require 'lontara_utilities' # or require 'lontara'
53
+
54
+ Lontara.http_client.get(url: 'http://example.com')
55
+ ```
56
+
57
+ For usage of each utilities, please refer to the documentation: [Lontara Utilities](https://www.rubydoc.info/gems/lontara-utilities)
58
+
59
+ ## Contributing
60
+ Bug reports and pull requests are welcome on GitHub at [Lontara Utilities](https://github.com/lontara-app/lontara-utilities).
data/lib/lontara.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Lontara is an alias for LontaraUtilities.
4
+ Lontara = LontaraUtilities
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ # This Base Error gives you a standard way to raise & handle errors with some additional features.
5
+ #
6
+ # Use this class directly:
7
+ #
8
+ # begin
9
+ # raise LontaraUtilities::BaseError, 'This is a test'
10
+ # rescue LontaraUtilities::BaseError => e
11
+ # puts e.message
12
+ # end
13
+ #
14
+ # or inherited by other classes:
15
+ #
16
+ # class RMQ::ConnectionError < LontaraUtilities::BaseError
17
+ # def initialize(message = "Can't established connection") = super
18
+ # end
19
+ #
20
+ # Note: You must call `super` in the initialize method of the inherited class.
21
+ #
22
+ # Parameter `handler` is a Proc or Any. If it is a Proc, it will be called.
23
+ # If it is not a Proc, it will be assigned to the `handler` attribute.
24
+ #
25
+ # class RMQ::QueueNotDefined < LontaraUtilities::BaseError
26
+ # def initialize(message = 'Queue not defined', handler: -> { logger }) = super
27
+ #
28
+ # # Write logs to 'rmq.log'
29
+ # def logger
30
+ # File.open(File.join(__dir__, 'log', 'rmq.log'), 'a') do |f|
31
+ # f.puts "#{timestamp} | #{code} | #{message}"
32
+ # end
33
+ # end
34
+ # end
35
+ #
36
+ # You also can pass a Backtrace after the message in raise method.
37
+ #
38
+ # raise LontaraUtilities::BaseError, 'This is a test', caller
39
+ #
40
+ # **Important**:
41
+ # You cannot pass other arguments except `message`, and `backtrace`
42
+ # to the raise method. `backtrace` only callable from rescue block also.
43
+ class BaseError < StandardError
44
+ # @param message [String] Error message. Default is 'LontaraUtilities::BaseError'.
45
+ # @param code [String] Error code. Default is LontaraUtilities::BaseError, or the class name of the inherited class.
46
+ # @param timestamp [Time] Error timestamp. Default is Time.now.
47
+ # @param handler [Proc | Any] Error han dler. Default is nil.
48
+ def initialize(message = 'BaseError', code: self.class.name, timestamp: Time.now, handler: nil)
49
+ @message = message
50
+ @code = code.to_s.split('::').last
51
+ @timestamp = timestamp
52
+ @handler = handler.is_a?(Proc) ? handler.call : handler
53
+
54
+ super(message)
55
+ end
56
+
57
+ attr_reader :message, :code, :timestamp, :handler
58
+ end
59
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ module Git
5
+ # Class Branch used to show all git branches in your project.
6
+ class Branch
7
+ # Show current running branch in your project.
8
+ def self.current_branch
9
+ `git rev-parse --abbrev-ref HEAD`.strip
10
+ end
11
+
12
+ # Show all branches in your project.
13
+ def self.all
14
+ `git branch -a`.split("\n").map { |branch| branch.gsub('*', '').strip }
15
+ end
16
+
17
+ # Show all remote branches in your project.
18
+ def self.remote
19
+ all.select { |branch| branch.start_with?('remotes') }
20
+ end
21
+
22
+ # Show all local branches in your project.
23
+ def self.local
24
+ all.reject { |branch| branch.start_with?('remotes') }
25
+ end
26
+
27
+ # Use metaprogramming to define methods
28
+ # for checking current branch using method_missing and plain ruby method.
29
+ def self.method_missing(method_name, *args, &)
30
+ if method_name.to_s.end_with?('?')
31
+ current_branch == method_name.to_s.gsub('?', '')
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def self.respond_to_missing?(method_name, include_private = false)
38
+ method_name.to_s.end_with?('?') || super
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ module Git
5
+ # Class Release used to show all release tag in your project.
6
+ class Release
7
+ # Show current running release in your project.
8
+ def self.current_release
9
+ `git describe --tags --abbrev=0`.strip
10
+ end
11
+
12
+ # Show all release in your project.
13
+ def self.all
14
+ `git tag`.split("\n").map(&:strip)
15
+ end
16
+
17
+ # Show latest release in your project.
18
+ def self.latest
19
+ all.last
20
+ end
21
+
22
+ # Check if current release is latest release.
23
+ def self.latest?(release)
24
+ release = release.to_s.downcase.gsub('v', '')
25
+ latest == release
26
+ end
27
+
28
+ # Check if release is current release.
29
+ def self.current?(release)
30
+ release = release.to_s.downcase.gsub('v', '')
31
+ current_release == release
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'git/branch'
4
+ require_relative 'git/release'
5
+
6
+ module LontaraUtilities
7
+ # Module Git responsible for handling git Branch and Release.
8
+ module Git
9
+ module_function
10
+
11
+ # Module function of Git Branch.
12
+ # Call this function like this: `LontaraUtilities::Git.branch#method_name`
13
+ def branch
14
+ Branch
15
+ end
16
+
17
+ # Module function of Git Release.
18
+ # Call this function like this: `LontaraUtilities::Git.release#method_name`
19
+ def release
20
+ Release
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ module HTTPClient
5
+ # Body Parser responsible for parsing body based on content type.
6
+ class BodyParser
7
+ def initialize(content_type:, body:)
8
+ @content_type = content_type
9
+ @body = body
10
+ end
11
+
12
+ # Parse body based on content type.
13
+ def parse
14
+ case content_type
15
+ when 'application/json'
16
+ body.to_json
17
+ when 'application/x-www-form-urlencoded'
18
+ URI.encode_www_form(body)
19
+ else
20
+ body
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :content_type, :body
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ module HTTPClient
5
+ # Request class responsible for handling HTTP request.
6
+ class Request
7
+ def initialize(method, url:, headers: {}, body: nil, params: nil)
8
+ @method = method
9
+ @url = url
10
+ @headers = headers
11
+ @body = body
12
+ @params = params
13
+
14
+ headers.merge!(user_agent:) unless headers.key?(:user_agent)
15
+ end
16
+
17
+ # Perform HTTP request.
18
+ def perform
19
+ call
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :method, :url, :headers, :body, :params
25
+
26
+ def call
27
+ connection.send(method, url) do |req|
28
+ req.body = parsed_body if body
29
+ req.params = params if params
30
+ req.options.timeout = 10
31
+ req.options.open_timeout = 10
32
+ end
33
+ end
34
+
35
+ def connection
36
+ Faraday.new do |faraday|
37
+ faraday.headers = headers
38
+ faraday.adapter Faraday.default_adapter
39
+ end
40
+ end
41
+
42
+ def parsed_body
43
+ BodyParser.new(**headers.slice(:content_type), body:).parse
44
+ end
45
+
46
+ def app_name
47
+ # Check if Rails is defined
48
+ return 'Lontara HTTPClient' unless defined?(Rails)
49
+
50
+ Rails.application.class.module_parent.name
51
+ end
52
+
53
+ def app_version
54
+ release = Git::Release.current_release
55
+ return VERSION if release.empty?
56
+
57
+ release
58
+ end
59
+
60
+ def user_agent
61
+ "#{app_name}/#{app_version}"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'uri'
5
+ require 'faraday'
6
+
7
+ require_relative 'git'
8
+ require_relative 'http_client/body_parser'
9
+ require_relative 'http_client/request'
10
+
11
+ module LontaraUtilities
12
+ # Lontara HTTP Client
13
+ #
14
+ # Request dapat menggunakan method `new` disertai HTTP method sebagai parameter,
15
+ # atau menggunakan method predifined `.get`, `.post`, `.put`, `.delete`, `.patch`.
16
+ #
17
+ # Parameter yang dibutuhkan:
18
+ # - method. Contoh: `:get`, `:post`, `:put`, `:delete`. Dapat menerima dalam bentuk String.
19
+ # - :url. Contoh: `http://localhost:4000/book/category`
20
+ # - :params. Contoh: `params: { sort: "createDate,desc" }`
21
+ # - :body. Contoh: `body: { username: admin, password: admin }`
22
+ # - :headers. Headers dapat diisi dengan berbagai key-value dalam format lowercase.
23
+ # Pisahkan dengan underscore jika key lebih dari satu kata.
24
+ # Contoh: `headers: { authorization: "Bearer hnjuvdiwv67wwqvn....", content_type: "application/json" }`
25
+ #
26
+ # Contoh:
27
+ #
28
+ # Lontara::HTTPClient.new(:get,
29
+ # url: 'http://localhost:4000/book/category',
30
+ # params: { sort: 'createDate,desc' }
31
+ # )
32
+ #
33
+ # atau
34
+ #
35
+ # request = Lontara::HTTPClient.get(
36
+ # url: 'http://localhost:4000/user',
37
+ # params: { id: "123GHANIY" },
38
+ # headers: {
39
+ # authorization: "Bearer #{token}",
40
+ # content_type: 'application/json'
41
+ # }
42
+ # )
43
+ #
44
+ # JSON.parse(request.body)
45
+ #
46
+ module HTTPClient
47
+ def self.new(method, url:, **options)
48
+ Request.new(method, url:, **options).perform
49
+ end
50
+
51
+ # Use predifined method to make HTTP request.
52
+ %i[get post put delete patch].each do |method|
53
+ define_singleton_method(method) do |url:, **options|
54
+ Request.new(method, url:, **options).perform
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ module RMQ
5
+ module Client
6
+ # Client for AMQ on Pub/Sub Pattern
7
+ class Publisher
8
+ def initialize(connection, queue:, exchange: nil, exchange_type: :direct)
9
+ @channel = connection.channel
10
+
11
+ # Use `default_exchange` if exchange is not declared in parameter
12
+ # nor use the specified exchange from the connection.
13
+ x = -> { exchange.nil? ? channel.default_exchange : channel.exchange(exchange, type: exchange_type) }
14
+ @exchange = connection.exchange.nil? ? x.call : connection.exchange
15
+
16
+ @queue = channel.queue(queue, durable: true)
17
+ end
18
+
19
+ # Publish message to the queue.
20
+ #
21
+ # This method will yield the block to get the message.
22
+ # Message published to NestJS Service must contain `id` key.
23
+ #
24
+ # Example:
25
+ #
26
+ # ```
27
+ # client.publish do
28
+ # {
29
+ # id: 'message_1',
30
+ # pattern: 'voucher.voucher.find_one',
31
+ # data: { id: 1 }
32
+ # }
33
+ # end
34
+ # ```
35
+ def publish(&block)
36
+ @request = block.call
37
+
38
+ exchange.publish(
39
+ request.to_json,
40
+ routing_key: queue.name
41
+ )
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :channel, :exchange, :queue, :request_id, :request
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ module RMQ
5
+ module Client
6
+ # Client for AMQ on RPC Pattern
7
+ class RPCProducer
8
+ def initialize(connection, queue:, reply_queue: 'amq.rabbitmq.reply-to')
9
+ @channel = connection.channel
10
+ @exchange = connection.exchange
11
+ @queue = channel.queue(queue, durable: true)
12
+ @reply_queue = channel.queue(reply_queue, durable: true)
13
+
14
+ # Consumer must be initialized to Reply Queue
15
+ # before publishing the message.
16
+ consume_reply
17
+ end
18
+
19
+ # Publish message to the queue.
20
+ #
21
+ # This method will yield the block to get the message.
22
+ # Message published to NestJS Service must contain `id` key.
23
+ #
24
+ # Example:
25
+ #
26
+ # ```
27
+ # client.publish do
28
+ # {
29
+ # id: 'message_1',
30
+ # pattern: 'voucher.voucher.find_one',
31
+ # data: { id: 1 }
32
+ # }
33
+ # end
34
+ # ```
35
+ def publish(&block) # rubocop:disable Metrics/AbcSize
36
+ @request = block.call
37
+ @request_id = SecureRandom.uuid
38
+
39
+ exchange.publish(
40
+ request.to_json,
41
+ routing_key: queue.name,
42
+ correlation_id: request_id,
43
+ reply_to: reply_queue.name
44
+ )
45
+
46
+ # waits for the signal from #consume_reply
47
+ lock.synchronize { condition.wait(lock) }
48
+
49
+ response
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :channel,
55
+ :exchange,
56
+ :queue,
57
+ :reply_queue,
58
+ :request_id,
59
+ :lock,
60
+ :condition,
61
+ :request,
62
+ :response
63
+
64
+ def consume_reply
65
+ @lock = Mutex.new
66
+ @condition = ConditionVariable.new
67
+
68
+ reply_queue.subscribe do |_delivery_info, properties, payload|
69
+ if properties[:correlation_id] == request_id
70
+ @response = payload
71
+
72
+ # sends the signal to continue the execution of #call
73
+ lock.synchronize { condition.signal }
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ module RMQ
5
+ # Base module for RMQ client.
6
+ module Client
7
+ class << self
8
+ # Client interface for RMQ Publisher or RPC Publisher.
9
+ #
10
+ # Start client by giving url and queue name.
11
+ # Parameter client must be `RPCProducer` or `Publisher`.
12
+ # If parameter not defined, default is `RPCProducer`.
13
+ #
14
+ # **Use `Publisher` if no need to listen the request's responses.**
15
+ #
16
+ # Options params are:
17
+ # - `default_exchange: (default: true)`
18
+ #
19
+ # That option is changing the way Connection object is created.
20
+ # If set to `false`, connection object didn't create exchange, also connection.exchange property will be `nil`.
21
+ #
22
+ # - `queue:`,
23
+ # - `reply_queue: (default: 'amq.rabbitmq.reply-to')`
24
+ # (Reply queue is only applicable if `client` is `RPCProducer`).
25
+ #
26
+ # These options only applicable if `default_exchange` is `false`, and `client` is `Publisher` (exchange created inside consumer).
27
+ # - `exchange:`
28
+ # - `exchange_type: (default: :direct)`
29
+ def start(url: ENV['RABBITMQ_URL'], client: 'RPCProducer', **options)
30
+ parameter_validator(client, options)
31
+
32
+ client_opts = client_opts_assigner(client, options)
33
+ conn_opts = %i[exchange exchange_type]
34
+ client = Object.const_get("LontaraUtilities::RMQ::Client::#{client}")
35
+
36
+ @connection = Connection.new(url:, **options.slice(*conn_opts))
37
+
38
+ @client = client.new(@connection, **client_opts)
39
+
40
+ self
41
+ end
42
+
43
+ def publish(request)
44
+ @client.publish { request }
45
+ end
46
+
47
+ def stop
48
+ @connection.close
49
+ end
50
+
51
+ private
52
+
53
+ def client_opts_assigner(client, options)
54
+ pubsub_opts = %i[queue exchange exchange_type]
55
+ rpc_opts = %i[queue reply_queue]
56
+
57
+ return options.slice(*pubsub_opts) if client == 'Publisher'
58
+ return options.slice(*rpc_opts) if client == 'RPCProducer'
59
+ end
60
+
61
+ def parameter_validator(client, options)
62
+ raise Errors::ClientParameterRequired unless %w[RPCProducer Publisher].include?(client)
63
+
64
+ return unless options[:default_exchange] == false
65
+ raise Errors::DefaultExchangeParameterRequired if client == 'Publisher'
66
+
67
+ return unless options[:exchange].nil?
68
+
69
+ raise Errors::ExchangeParameterRequired
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ module RMQ
5
+ # Initializing the connection to RabbitMQ.
6
+ class Connection
7
+ # **Connection can be initialized within a block.**
8
+ #
9
+ # If block is given, connection object will be yielded.
10
+ # You can use the object to define the `exchange`, `queue`, or `reply queue`.
11
+ #
12
+ # Example:
13
+ #
14
+ # def initialize(url, queue, reply_queue)
15
+ # Connection.new(url:) do |conn|
16
+ # @exchange = conn.channel.default_exchange
17
+ # @queue = conn.channel.queue(queue)
18
+ # @reply_queue = conn.channel.queue(reply_queue, exclusive: true)
19
+ # end
20
+ # end
21
+ # # ... your code goes here
22
+ def initialize(url: ENV['RABBITMQ_URL'], default_exchange: true)
23
+ @connection = Bunny.new(url)
24
+ @connection.start
25
+
26
+ @channel = channel_pool
27
+ @exchange = channel.default_exchange if default_exchange
28
+ end
29
+
30
+ def close
31
+ @connection.close
32
+ end
33
+
34
+ attr_reader :connection, :channel, :exchange
35
+
36
+ private
37
+
38
+ def channel_pool
39
+ @channel_pool ||= ConnectionPool.new { @connection }.with(&:create_channel)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_error'
4
+
5
+ module LontaraUtilities
6
+ module RMQ
7
+ module Errors
8
+ class ClientParameterRequired < BaseError # rubocop:disable Style/Documentation
9
+ def initialize(message = 'Client parameter required, and must be RPCProducer or Publisher.') = super
10
+ end
11
+
12
+ class DefaultExchangeParameterRequired < BaseError # rubocop:disable Style/Documentation
13
+ def initialize(message = "Default exchange parameter required, and shouldn't be false.") = super
14
+ end
15
+
16
+ class ExchangeParameterRequired < BaseError # rubocop:disable Style/Documentation
17
+ def initialize(message = 'Exchange parameter required unless default_exchange declared.') = super
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ module RMQ
5
+ # Listener for RabbitMQ.
6
+ #
7
+ # This class used to define the listener for each request type.
8
+ # Same as routes.rb in Rails, that define the routes for each request type
9
+ # and the method to be called.
10
+ class Listener
11
+ def self.listen(request)
12
+ new(request).start
13
+ end
14
+
15
+ def initialize(request)
16
+ @id = request[:id] if request[:id].present?
17
+ @pattern = request[:pattern]
18
+ @data = request[:data]
19
+ end
20
+
21
+ # Start listening the request and process it to the defined listener.
22
+ def start
23
+ # Merge message ID if present.
24
+ id.present? ? { id: }.merge!(listener_response) : listener_response
25
+ rescue StandardError => e
26
+ id.present? ? { id: }.merge!(error_response(e)) : error_response(e)
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :id, :pattern, :data
32
+
33
+ def listener_response
34
+ RMQRoutes.draw(with: pattern, data:)
35
+ end
36
+
37
+ def error_response(error)
38
+ {
39
+ type: :error,
40
+ timestamp: Time.now.to_i,
41
+ data: {
42
+ message: error.message,
43
+ code: error.class.name.split('::').last,
44
+ stacktrace: error.backtrace
45
+ }
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ module RMQ
5
+ module Server
6
+ # Server for AMQ on RPC Pattern,m
7
+ class RPCConsumer
8
+ def initialize(connection, queue:)
9
+ @channel = connection.channel
10
+ @exchange = connection.exchange
11
+ @queue = channel.queue(queue, durable: true)
12
+ end
13
+
14
+ # Start consuming the queue, process the request, and publish the response.
15
+ def start
16
+ queue.subscribe do |_, properties, payload|
17
+ @request = JSON.parse(payload, symbolize_names: true)
18
+
19
+ publish_response(properties.reply_to, properties.correlation_id)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :channel, :exchange, :queue, :request, :response
26
+
27
+ def publish_response(reply_queue, correlation_id)
28
+ process_request
29
+
30
+ exchange.publish(response.to_json, routing_key: reply_queue, correlation_id:)
31
+ end
32
+
33
+ def process_request
34
+ @response = Listener.listen request
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ module RMQ
5
+ module Server
6
+ # Server for AMQ on Direct Pattern
7
+ class Subscriber
8
+ def initialize(connection, queue:)
9
+ @channel = connection.channel
10
+ @queue = channel.queue(queue, durable: true)
11
+ end
12
+
13
+ # Start consuming the queue and process the request.
14
+ def start
15
+ queue.subscribe(manual_ack: true) do |delivery_info, _, body|
16
+ @request = JSON.parse(body, symbolize_names: true)
17
+
18
+ channel.ack(delivery_info.delivery_tag)
19
+
20
+ process_request
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :channel, :queue, :request
27
+
28
+ def process_request
29
+ Listener.listen request
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ module RMQ
5
+ # Base module for RMQ server.
6
+ module Server
7
+ class << self
8
+ # Server interface for RMQ Publisher or RPC Publisher.
9
+ #
10
+ # Start server by giving url and queue name.
11
+ # Parameter server must be `RPCConsumer` or `Subscriber`.
12
+ # If parameter not defined, default is `RPCConsumer`.
13
+ #
14
+ # **Use `Subscriber` if no need to reply the request's responses.**
15
+ #
16
+ # Options params is: `default_exchange: (default: true)`
17
+ #
18
+ # That option is changing the way Connection object is created.
19
+ # If set to `false`, connection object didn't create exchange, also connection.exchange property will be `nil`.
20
+ #
21
+ # These options only applicable if `default_exchange` is `false`, and `server` is `RPCConsumer` (exchange created inside consumer).
22
+ # - `exchange:`
23
+ # - `exchange_type: (default: :direct)`
24
+ #
25
+ # Be aware of this conditions:
26
+ # - Don't leave `exchange` where `default_exchange` is `false`, or Server object will raise error.
27
+ def start(url:, queue:, server: 'RPCConsumer', **options)
28
+ parameter_validator(server, options)
29
+
30
+ server = Object.const_get("LontaraUtilities::RMQ::Server::#{server}")
31
+ server_opts = %i[exchange exchange_type]
32
+
33
+ connection = Connection.new(url:, **options.except(*server_opts))
34
+
35
+ server.new(connection, queue:, **options.slice(*server_opts)).start
36
+ end
37
+
38
+ private
39
+
40
+ def parameter_validator(server, options)
41
+ raise ArgumentError, 'server parameter is required, and must be RPCConsumer or Subscriber' if server.nil?
42
+
43
+ return unless options[:default_exchange] == false
44
+ raise ArgumentError, "default_exchange shouldn't be false if server is RPCConsumer" if server == 'RPCConsumer'
45
+
46
+ return unless options[:exchange].nil?
47
+
48
+ raise ArgumentError, 'exchange parameter is required if default_exchange is false'
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'bunny'
5
+ require 'connection_pool'
6
+ require 'securerandom'
7
+
8
+ require_relative 'rmq/server/rpc_consumer'
9
+ require_relative 'rmq/client/rpc_producer'
10
+ require_relative 'rmq/server/subscriber'
11
+ require_relative 'rmq/client/publisher'
12
+ require_relative 'rmq/connection'
13
+ require_relative 'rmq/listener'
14
+ require_relative 'rmq/server'
15
+ require_relative 'rmq/client'
16
+ require_relative 'rmq/errors'
17
+
18
+ module LontaraUtilities
19
+ # Lontara RMQ
20
+ #
21
+ # RMQ module responsible for handling AMQP connection
22
+ # between services.
23
+ module RMQ
24
+ module_function
25
+
26
+ # Instantiating RMQ connection.
27
+ def connection(url:, **options)
28
+ Connection.new(url:, **options)
29
+ end
30
+
31
+ # RPC Consumer server.
32
+ # Options parameter are: `exchange:` and `exchange_type:`
33
+ def rpc_consumer(connection, queue:, **options)
34
+ Server::RPCConsumer.new(connection, queue:, **options)
35
+ end
36
+
37
+ # Publisher server.
38
+ def subscriber(connection, queue:)
39
+ Server::Subscriber.new(connection, queue:)
40
+ end
41
+
42
+ # RPC Producer client.
43
+ # Options parameter are: `exchange:` and `exchange_type:`
44
+ def rpc_producer(connection, queue:, reply_queue:, **options)
45
+ Client::RPCProducer.new(connection, queue:, reply_queue:, **options)
46
+ end
47
+
48
+ # Publisher client.
49
+ # Options parameter are: `exchange:` and `exchange_type:`
50
+ def publisher(connection, queue:, **options)
51
+ Client::Publisher.new(connection, queue:, **options)
52
+ end
53
+
54
+ def client
55
+ Client
56
+ end
57
+
58
+ def server
59
+ Server
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LontaraUtilities
4
+ VERSION = '3.0.0'
5
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lontara_utilities/base_error'
4
+ require_relative 'lontara_utilities/git'
5
+ require_relative 'lontara_utilities/http_client'
6
+ require_relative 'lontara_utilities/rmq'
7
+ require_relative 'lontara_utilities/version'
8
+ require_relative 'lontara'
9
+
10
+ # Base module for Lontara Utilities
11
+ module LontaraUtilities
12
+ module_function
13
+
14
+ def git
15
+ Git
16
+ end
17
+
18
+ def rmq
19
+ RMQ
20
+ end
21
+
22
+ def http_client
23
+ HTTPClient
24
+ end
25
+
26
+ def version
27
+ VERSION
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'json'
7
+ require 'lontara_utilities/version'
8
+
9
+ # Load JSON File for Gem metadata
10
+ metadata = JSON.parse(File.read('metadata.json')).transform_keys!(&:to_sym)
11
+
12
+ Gem::Specification.new do |spec|
13
+ spec.version = LontaraUtilities::VERSION
14
+
15
+ metadata.each do |key, value|
16
+ # Create keys as methods
17
+ spec.send("#{key}=", value)
18
+ end
19
+
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ end
23
+ spec.require_paths = ['lib']
24
+ spec.required_ruby_version = '>= 3.1.0'
25
+
26
+ spec.add_dependency 'bunny', '~> 2.20'
27
+ spec.add_dependency 'connection_pool', '~> 2.3'
28
+ spec.add_dependency 'faraday', '~> 2.7'
29
+ spec.add_dependency 'json', '~> 2.6'
30
+ spec.add_dependency 'securerandom', '~> 0.2'
31
+ spec.add_dependency 'uri', '~> 0.12'
32
+
33
+ spec.add_development_dependency 'byebug', '~> 11.1'
34
+ spec.add_development_dependency 'rspec', '~> 3.12'
35
+ spec.add_development_dependency 'webmock', '~> 3.18'
36
+ spec.metadata['rubygems_mfa_required'] = 'true'
37
+ end
data/metadata.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "lontara_utilities",
3
+ "authors": ["Abdul Hakim Ghaniy"],
4
+ "email": "abdulhakimghaniy@outlook.co.id",
5
+ "homepage": "https://github.com/lontara-app/lontara-utilities",
6
+ "summary": "Lontara Utilities provided by Lontara.app",
7
+ "description": "Lontara Utilities provides some utils for your Ruby's project, like RabbitMQ Connection for Pub/Sub Messaging and RPC, HTTP Client, Git Branch or Release, and Base Error with some features. This utils used by our Lontara.app's services and we want to share it with you. Built with Faraday, Bunny, SecureRandom, and ConnectionPool. Enjoy!",
8
+ "license": "GPL-3.0"
9
+ }
metadata ADDED
@@ -0,0 +1,200 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lontara_utilities
3
+ version: !ruby/object:Gem::Version
4
+ version: 3.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Abdul Hakim Ghaniy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bunny
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.20'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.20'
27
+ - !ruby/object:Gem::Dependency
28
+ name: connection_pool
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.7'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: json
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.6'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: securerandom
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: uri
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.12'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.12'
97
+ - !ruby/object:Gem::Dependency
98
+ name: byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '11.1'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '11.1'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.12'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.12'
125
+ - !ruby/object:Gem::Dependency
126
+ name: webmock
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.18'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.18'
139
+ description: Lontara Utilities provides some utils for your Ruby's project, like RabbitMQ
140
+ Connection for Pub/Sub Messaging and RPC, HTTP Client, Git Branch or Release, and
141
+ Base Error with some features. This utils used by our Lontara.app's services and
142
+ we want to share it with you. Built with Faraday, Bunny, SecureRandom, and ConnectionPool.
143
+ Enjoy!
144
+ email: abdulhakimghaniy@outlook.co.id
145
+ executables: []
146
+ extensions: []
147
+ extra_rdoc_files: []
148
+ files:
149
+ - ".gitignore"
150
+ - ".rubocop.yml"
151
+ - Gemfile
152
+ - Gemfile.lock
153
+ - README.md
154
+ - lib/lontara.rb
155
+ - lib/lontara_utilities.rb
156
+ - lib/lontara_utilities/base_error.rb
157
+ - lib/lontara_utilities/git.rb
158
+ - lib/lontara_utilities/git/branch.rb
159
+ - lib/lontara_utilities/git/release.rb
160
+ - lib/lontara_utilities/http_client.rb
161
+ - lib/lontara_utilities/http_client/body_parser.rb
162
+ - lib/lontara_utilities/http_client/request.rb
163
+ - lib/lontara_utilities/rmq.rb
164
+ - lib/lontara_utilities/rmq/client.rb
165
+ - lib/lontara_utilities/rmq/client/publisher.rb
166
+ - lib/lontara_utilities/rmq/client/rpc_producer.rb
167
+ - lib/lontara_utilities/rmq/connection.rb
168
+ - lib/lontara_utilities/rmq/errors.rb
169
+ - lib/lontara_utilities/rmq/listener.rb
170
+ - lib/lontara_utilities/rmq/server.rb
171
+ - lib/lontara_utilities/rmq/server/rpc_consumer.rb
172
+ - lib/lontara_utilities/rmq/server/subscriber.rb
173
+ - lib/lontara_utilities/version.rb
174
+ - lontara-utilities.gemspec
175
+ - metadata.json
176
+ homepage: https://github.com/lontara-app/lontara-utilities
177
+ licenses:
178
+ - GPL-3.0
179
+ metadata:
180
+ rubygems_mfa_required: 'true'
181
+ post_install_message:
182
+ rdoc_options: []
183
+ require_paths:
184
+ - lib
185
+ required_ruby_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: 3.1.0
190
+ required_rubygems_version: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ requirements: []
196
+ rubygems_version: 3.3.26
197
+ signing_key:
198
+ specification_version: 4
199
+ summary: Lontara Utilities provided by Lontara.app
200
+ test_files: []