umbrellio-utils 0.1.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: 7a8a024f0f4fc2df65b3ec0a9e8bfafc71122522cc9e633dec609ed7f03f1a59
4
+ data.tar.gz: 5e66a79927988a0a2232308315d8e0e07ccb3fcb40c9c770963babd81f77e47e
5
+ SHA512:
6
+ metadata.gz: d8a5425810747fb97922019b388e54358623b39d119ea0482592d39f3ffda8efa54768b20d896ebcac3b2877059da24212ff2811c255eb29506108946309d8c4
7
+ data.tar.gz: e918d6b8324560d613fef02b68625373b6285b8a9791bbf5072c1a1bb0117b73f9699edc508f2456d88b7a3307cbc3ba42a86103f554518685b539efb3fd1728
@@ -0,0 +1,60 @@
1
+ name: Test
2
+
3
+ on: [push, pull_request]
4
+
5
+ env:
6
+ FULL_COVERAGE_CHECK: false # Sometimes, sometimes...
7
+
8
+ jobs:
9
+ full-check:
10
+ runs-on: ubuntu-latest
11
+
12
+ # We want to run on external PRs, but not on our own internal PRs as they'll be run on push event
13
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'umbrellio/utils'
14
+
15
+ steps:
16
+ - uses: actions/checkout@v2
17
+ - uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: 3
20
+ bundler-cache: true
21
+ - name: Run Linter
22
+ run: bundle exec ci-helper RubocopLint
23
+ - name: Check missed spec suffixes
24
+ run: bundle exec ci-helper CheckSpecSuffixes --extra-paths spec/*.rb --ignored-paths spec/*_helper.rb
25
+ - name: Run specs
26
+ run: bundle exec ci-helper RunSpecs
27
+ - name: Audit
28
+ run: bundle exec ci-helper BundlerAudit
29
+ - name: Coveralls
30
+ uses: coverallsapp/github-action@master
31
+ with:
32
+ github-token: ${{ secrets.GITHUB_TOKEN }}
33
+ specs:
34
+ runs-on: ubuntu-latest
35
+ continue-on-error: ${{ matrix.experimental }}
36
+
37
+ env:
38
+ FULL_TEST_COVERAGE_CHECK: false
39
+
40
+ # We want to run on external PRs, but not on our own internal PRs as they'll be run on push event
41
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'umbrellio/utils'
42
+
43
+ strategy:
44
+ fail-fast: false
45
+ matrix:
46
+ ruby: [2.6, 2.7]
47
+ experimental: [false]
48
+ include:
49
+ - ruby: head
50
+ experimental: true
51
+
52
+
53
+ steps:
54
+ - uses: actions/checkout@v2
55
+ - uses: ruby/setup-ruby@v1
56
+ with:
57
+ ruby-version: ${{ matrix.ruby }}
58
+ bundler-cache: true
59
+ - name: Run specs
60
+ run: bundle exec rspec
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,19 @@
1
+ inherit_gem:
2
+ rubocop-config-umbrellio: lib/rubocop.yml
3
+
4
+ AllCops:
5
+ DisplayCopNames: true
6
+ TargetRubyVersion: 2.6
7
+
8
+ Naming/MethodParameterName:
9
+ AllowedNames: ["x", "y", "z"]
10
+
11
+ RSpec/EmptyLineAfterHook:
12
+ Enabled: false
13
+
14
+ Naming/FileName:
15
+ Exclude:
16
+ - lib/umbrellio-utils.rb
17
+
18
+ Naming/RescuedExceptionsVariableName:
19
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in umbrellio_utils.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 JustAnotherDude
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # Umbrellio Utils
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem "umbrellio-utils"
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ ```bash
14
+ $ bundle install
15
+ ```
16
+
17
+ Or install it yourself as:
18
+
19
+ ```bash
20
+ $ gem install umbrellio-utils
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Quick Start
26
+
27
+ You can use modules and classes directly by accessing modules and classes
28
+ under namespace `UmbrellioUtils`. Or you can `include UmbrellioUtils` to other
29
+ module with name you like.
30
+
31
+ ```ruby
32
+ # Direct using
33
+ UmbrellioUtils::Constants.get_class!(:object) #=> Object
34
+
35
+ # Aliasing to shorter name.
36
+
37
+ module Utils
38
+ include UmbrellioUtils
39
+ end
40
+
41
+ Utils::Constants.get_class!(:object) #=> Object
42
+ Utils::Constants #=> UmbrellioUtils::Constants
43
+ ```
44
+
45
+ ### Configuration
46
+
47
+ Some modules and classes are configurable. Here's the full list of settings and what they do:
48
+
49
+ - `store_table_name` — table which is used by `UmbrellioUtils::Store` module.
50
+ Defaults to `:store`
51
+ - `http_client_name` — fiber-local variable name for http client instance in
52
+ `UmbrellioUtils::HTTPClient`. Defaults to `:application_httpclient`
53
+
54
+ You can change config in two ways. Firstly, you can change values by accessing configuration
55
+ directly. Secondly, you can use `UmbrellioUtils::configure` method, which accepts a block.
56
+
57
+ ```ruby
58
+
59
+ # First method
60
+
61
+ UmbrellioUtils.config.store_table_name = :cool_name
62
+
63
+ # Second method
64
+
65
+ module Utils
66
+ include UmbrellioUtils
67
+
68
+ configure do |config|
69
+ config.store_table_name = :cool_name
70
+ end
71
+ end
72
+ ```
73
+
74
+ Keep in mind that the config is common to all modules: if you use multiple modules that include
75
+ `UmbrellioUtils`, then all modules will use the same configuration object.
76
+
77
+ ### Extension
78
+
79
+ You can extend module with you own project specific methods
80
+ via `UmbrellioUtils::extend_util!`.
81
+
82
+ ```ruby
83
+ module Utils
84
+ include UmbrellioUtils
85
+
86
+ configure do |config|
87
+ config.store_table_name = :cool_name
88
+ end
89
+
90
+ extend_util!(:Constants) do
91
+ def useful_method
92
+ "Just string"
93
+ end
94
+ end
95
+ end
96
+
97
+ Utils::Constants.useful_method #=> "Just string"
98
+ ```
99
+
100
+ Or you can define methods in your module and then extend the desired module.
101
+
102
+ ```ruby
103
+ module MyHelpers
104
+ def useful_method
105
+ "Just string"
106
+ end
107
+ end
108
+
109
+ module Utils
110
+ include UmbrellioUtils
111
+
112
+ extend_util!(:Constants) { extend MyHelpers }
113
+ end
114
+
115
+ Utils::Constants.useful_method #=> "Just string"
116
+ ```
117
+
118
+ ## Contributing
119
+
120
+ Bug reports and pull requests are welcome on GitHub at https://github.com/umbrellio/utils.
121
+
122
+ ## License
123
+
124
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
125
+
126
+ ## Authors
127
+
128
+ Created by Umbrellio's Ruby developers
129
+
130
+ <a href="https://github.com/umbrellio/">
131
+ <img style="float: left;" src="https://umbrellio.github.io/Umbrellio/supported_by_umbrellio.svg" alt="Supported by Umbrellio" width="439" height="72">
132
+ </a>
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "umbrellio_utils"
5
+ require "pry"
6
+
7
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "umbrellio_utils"
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "memery"
4
+
5
+ module UmbrellioUtils
6
+ CONFIG_SET_MUTEX = Mutex.new
7
+ CONFIG_MUTEX = Mutex.new
8
+ EXTENSION_MUTEX = Mutex.new
9
+
10
+ Dir["#{__dir__}/*/*.rb"].each { |file_path| require_relative(file_path) }
11
+
12
+ extend self
13
+
14
+ def included(othermod)
15
+ super
16
+ othermod.extend(self)
17
+ end
18
+
19
+ # rubocop:disable Style/ClassVars
20
+ def config
21
+ CONFIG_SET_MUTEX.synchronize do
22
+ @@config ||= Struct
23
+ .new(:store_table_name, :http_client_name, keyword_init: true)
24
+ .new(**default_settings)
25
+ end
26
+ end
27
+
28
+ # rubocop:enable Style/ClassVars
29
+
30
+ def configure
31
+ CONFIG_MUTEX.synchronize { yield config }
32
+ end
33
+
34
+ def extend_util!(module_name, &block)
35
+ const = UmbrellioUtils.const_get(module_name)
36
+ EXTENSION_MUTEX.synchronize { const.class_eval(&block) }
37
+ end
38
+
39
+ private
40
+
41
+ def default_settings
42
+ {
43
+ store_table_name: :store,
44
+ http_client_name: :application_httpclient,
45
+ }
46
+ end
47
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Cards
5
+ extend self
6
+
7
+ InvalidExpiryDateString = Class.new(StandardError)
8
+
9
+ def parse_expiry_date!(string, **options)
10
+ result = parse_expiry_date(string, **options)
11
+
12
+ unless result
13
+ raise InvalidExpiryDateString, "Failed to parse expiry date: #{string.inspect}"
14
+ end
15
+
16
+ result
17
+ end
18
+
19
+ def parse_expiry_date(string)
20
+ month, year = string.split("/", 2).map(&:to_i)
21
+ return unless month && year
22
+ year += 2000 if year < 100
23
+ time = suppress(ArgumentError) { Time.zone.local(year, month) }
24
+ time + 1.month - 1.second if time
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Checks
5
+ extend self
6
+
7
+ VOWELS_REGEX = /[AEIOUY]/.freeze
8
+ CONSONANTS_REGEX = /[BCDFGHJKLMNPQRSTVXZW]/.freeze
9
+ EMAIL_REGEXP = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i.freeze
10
+
11
+ def secure_compare(src, dest)
12
+ ActiveSupport::SecurityUtils.secure_compare(
13
+ ::Digest::SHA256.hexdigest(src),
14
+ ::Digest::SHA256.hexdigest(dest),
15
+ )
16
+ end
17
+
18
+ def valid_card?(number)
19
+ numbers = number.to_s.chars.map(&:to_i)
20
+
21
+ modified_numbers = numbers.reverse.map.with_index do |number, index|
22
+ if index.odd?
23
+ number *= 2
24
+ number -= 9 if number > 9
25
+ end
26
+
27
+ number
28
+ end
29
+
30
+ (modified_numbers.sum % 10).zero?
31
+ end
32
+
33
+ def valid_email?(email)
34
+ email.to_s =~ EMAIL_REGEXP
35
+ end
36
+
37
+ def valid_card_holder?(holder)
38
+ words = holder.to_s.split
39
+ return if words.count != 2
40
+ return if words.any? { |x| x.match?(/(.+)(\1)(\1)/) }
41
+ return unless words.all? { |x| x.size >= 2 }
42
+ return unless words.all? { |x| x.match?(/\A[A-Z]+\z/) }
43
+ return unless words.all? { |x| x.match?(VOWELS_REGEX) && x.match?(CONSONANTS_REGEX) }
44
+
45
+ true
46
+ end
47
+
48
+ def valid_card_cvv?(cvv)
49
+ cvv = cvv.to_s.scan(/\d/).join
50
+ cvv.size.between?(3, 4)
51
+ end
52
+
53
+ def valid_phone?(phone)
54
+ Phonelib.valid?(phone)
55
+ end
56
+
57
+ def between?(checked_value, boundary_values, convert_sym: :to_f)
58
+ checked_value.public_send(convert_sym).between?(*boundary_values.first(2).map(&convert_sym))
59
+ end
60
+
61
+ def int_array?(value, size_range = 1..Float::INFINITY)
62
+ value.all? { |value| value.to_i.positive? } && value.size.in?(size_range)
63
+ end
64
+
65
+ def valid_limit?(limit)
66
+ int_array?(limit, 2..2) && limit.reduce(:<=)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Constants
5
+ extend self
6
+
7
+ def get_class(*name_parts)
8
+ safe_constantize(name_parts.join("/").underscore.camelize)
9
+ end
10
+
11
+ def get_class!(*args)
12
+ get_class(*args) or raise "Failed to get class for #{args.inspect}"
13
+ end
14
+
15
+ def safe_constantize(constant_name)
16
+ constant = suppress(NameError) { Object.const_get(constant_name, false) }
17
+ constant if constant && constant.name == constant_name
18
+ end
19
+
20
+ def match_by_class!(**kwargs)
21
+ name, instance = kwargs.shift
22
+ result = kwargs.find { |klass, _| instance.is_a?(klass) }&.last
23
+ raise "Unsupported #{name} type: #{instance.inspect}" if result.nil?
24
+
25
+ result.is_a?(Proc) ? result.call : result
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Control
5
+ extend self
6
+
7
+ class UniqueConstraintViolation < StandardError; end
8
+
9
+ def run_in_interval(interval, key:)
10
+ previous_string = Store[key]
11
+ previous = previous_string ? Time.zone.parse(previous_string) : Time.utc(0)
12
+
13
+ return if previous + interval > Time.current
14
+ Store[key] = Time.current
15
+
16
+ yield
17
+ ensure
18
+ Store.delete(key) rescue nil
19
+ end
20
+
21
+ def retry_on_unique_violation(
22
+ times: Float::INFINITY, retry_on_all_constraints: false, checked_constraints: [], &block
23
+ )
24
+ retry_on(Sequel::UniqueConstraintViolation, times: times) do
25
+ DB.transaction(savepoint: true, &block)
26
+ rescue Sequel::UniqueConstraintViolation => e
27
+ constraint_name = Database.get_violated_constraint_name(e)
28
+
29
+ if retry_on_all_constraints || checked_constraints.include?(constraint_name)
30
+ raise e
31
+ else
32
+ raise UniqueConstraintViolation, e.message
33
+ end
34
+ end
35
+ end
36
+
37
+ def run_non_critical(rescue_all: false, in_transaction: false, &block)
38
+ in_transaction ? DB.transaction(savepoint: true, &block) : yield
39
+ rescue (rescue_all ? Exception : StandardError) => e
40
+ Exceptions.notify!(e)
41
+ nil
42
+ end
43
+
44
+ def retry_on(exception, times: Float::INFINITY, wait: 0)
45
+ retries = 0
46
+
47
+ begin
48
+ yield
49
+ rescue exception
50
+ retries += 1
51
+ raise if retries > times
52
+ sleep(wait)
53
+ retry
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Database
5
+ extend self
6
+
7
+ HandledConstaintError = Class.new(StandardError)
8
+
9
+ def handle_constraint_error(constraint_name, &block)
10
+ DB.transaction(savepoint: true, &block)
11
+ rescue Sequel::UniqueConstraintViolation => e
12
+ if constraint_name.to_s == get_violated_constraint_name(e)
13
+ raise HandledConstaintError
14
+ else
15
+ raise e
16
+ end
17
+ end
18
+
19
+ def get_violated_constraint_name(exception)
20
+ error = exception.wrapped_exception
21
+ error.result.error_field(PG::Result::PG_DIAG_CONSTRAINT_NAME)
22
+ end
23
+
24
+ def each_record(dataset, **options, &block)
25
+ primary_key = primary_key_from(options)
26
+ with_temp_table(dataset, **options) do |ids|
27
+ dataset.model.where(primary_key => ids).each(&block)
28
+ end
29
+ end
30
+
31
+ def with_temp_table(dataset, **options)
32
+ primary_key = primary_key_from(options)
33
+ page_size = options.fetch(:page_size, 1_000)
34
+ do_sleep = options.fetch(:sleep, Rails.env.production?)
35
+
36
+ temp_table_name = create_temp_table(dataset, primary_key: primary_key)
37
+
38
+ pk_set = []
39
+
40
+ loop do
41
+ DB.transaction do
42
+ pk_expr = DB[temp_table_name].select(primary_key).reverse(primary_key).limit(page_size)
43
+
44
+ deleted_items = DB[temp_table_name].where(primary_key => pk_expr).returning.delete
45
+ pk_set = deleted_items.map { |item| item[primary_key] }
46
+
47
+ yield(pk_set) if pk_set.any?
48
+ end
49
+
50
+ break if pk_set.empty?
51
+
52
+ sleep(1) if do_sleep
53
+ clear_lamian_logs!
54
+ end
55
+ ensure
56
+ DB.drop_table(temp_table_name)
57
+ end
58
+
59
+ def clear_lamian_logs!
60
+ Lamian.logger.send(:logdevs).each { |x| x.truncate(0) && x.rewind }
61
+ end
62
+
63
+ def create_temp_table(dataset, primary_key:)
64
+ model = dataset.model
65
+ time = Time.current
66
+ temp_table_name = "temp_#{model.table_name}_#{time.to_i}#{time.nsec}".to_sym
67
+ type = model.db_schema[primary_key][:db_type]
68
+
69
+ DB.drop_table?(temp_table_name)
70
+ DB.create_table(temp_table_name) { column primary_key, type, primary_key: true }
71
+
72
+ insert_ds = dataset.select(Sequel[model.table_name][primary_key])
73
+ DB[temp_table_name].insert(insert_ds)
74
+ temp_table_name
75
+ end
76
+
77
+ private
78
+
79
+ def primary_key_from(options)
80
+ options.fetch(:primary_key, :id)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Formatting
5
+ extend self
6
+
7
+ def pluralize(symbol)
8
+ symbol.to_s.pluralize.to_sym
9
+ end
10
+
11
+ def merge_query_into_url(url, query)
12
+ uri = Addressable::URI.parse(url)
13
+ url = uri.omit(:query)
14
+ original_query = uri.query_values || {}
15
+ to_url(url, **original_query, **query.stringify_keys)
16
+ end
17
+
18
+ def to_url(*parts)
19
+ params = parts.select { |x| x.is_a?(Hash) }
20
+ parts -= params
21
+ params = params.reduce(&:merge)
22
+ query = to_query(params).presence if params.present?
23
+ [File.join(*parts), query].compact.join("?")
24
+ end
25
+
26
+ def to_query(hash, namespace = nil)
27
+ pairs = hash.map do |key, value|
28
+ key = CGI.escape(key.to_s)
29
+ ns = namespace ? "#{namespace}[#{key}]" : key
30
+ value.is_a?(Hash) ? to_query(value, ns) : "#{CGI.escape(ns)}=#{CGI.escape(value.to_s)}"
31
+ end
32
+
33
+ pairs.join("&")
34
+ end
35
+
36
+ def uncapitalize_string(string)
37
+ string = string.dup
38
+ string[0] = string[0].downcase
39
+ string
40
+ end
41
+
42
+ def cache_key(*parts)
43
+ parts.flatten.compact.join("-")
44
+ end
45
+
46
+ def render_money(money)
47
+ "#{money.round} #{money.currency}"
48
+ end
49
+
50
+ def match_or_nil(str, regex)
51
+ return if str.blank?
52
+ return unless str.match?(regex)
53
+ str
54
+ end
55
+
56
+ def encode_key(key)
57
+ Base64.strict_encode64(key.to_der)
58
+ end
59
+
60
+ def to_date_part_string(part)
61
+ format("%<part>02d", part: part)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module UmbrellioUtils
6
+ class HTTPClient
7
+ include Singleton
8
+
9
+ def perform(*args, **kwargs)
10
+ client.perform(*args, **kwargs)
11
+ end
12
+
13
+ def perform!(*args, **kwargs)
14
+ client.perform!(*args, **kwargs)
15
+ end
16
+
17
+ def request(*args, **kwargs)
18
+ client.request(*args, **kwargs)
19
+ end
20
+
21
+ private
22
+
23
+ def client
24
+ Thread.current[UmbrellioUtils.config.http_client_name] ||= EzClient.new(**ezclient_options)
25
+ end
26
+
27
+ def ezclient_options
28
+ { keep_alive: 30, on_retry: method(:on_retry), timeout: 15 }
29
+ end
30
+
31
+ def on_retry(_request, error, _metadata)
32
+ log!("Retrying on error: #{error.class}: #{error.message}")
33
+ end
34
+
35
+ def log!(message)
36
+ Rails.logger.info "[httpclient] #{message}"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Misc
5
+ extend self
6
+
7
+ def table_sync(scope, delay: 1, routing_key: nil)
8
+ scope.in_batches do |batch|
9
+ batch.each do |model|
10
+ next if model.try(:skip_table_sync?)
11
+
12
+ values = [model.class.name, model.values]
13
+ publisher = TableSync::Publishing::Publisher.new(*values, confirm: false)
14
+ publisher.routing_key = routing_key if routing_key
15
+ publisher.publish_now
16
+ end
17
+
18
+ sleep delay
19
+ end
20
+ end
21
+
22
+ # Ranges go from high to low priority
23
+ def merge_ranges(*ranges)
24
+ ranges = ranges.map { |x| x.present? && x.size == 2 ? x : [nil, nil] }
25
+ ranges.first.zip(*ranges[1..]).map { |x| x.find(&:present?) }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Parsing
5
+ extend self
6
+
7
+ RFC_AUTH_HEADERS = %w[
8
+ HTTP_AUTHORIZATION
9
+ HTTP_X_HTTP_AUTHORIZATION
10
+ HTTP_REDIRECT_X_HTTP_AUTHORIZATION
11
+ ].freeze
12
+ CARD_TRUNCATED_PAN_REGEX = /\A(\d{6}).*(\d{4})\z/.freeze
13
+
14
+ def try_to_parse_as_json(data)
15
+ JSON.parse(data) rescue data
16
+ end
17
+
18
+ def parse_xml(xml, remove_attributes: true, snakecase: true)
19
+ xml = Nokogiri::XML(xml)
20
+ xml.remove_namespaces!
21
+ xml.xpath("//@*").remove if remove_attributes
22
+
23
+ tags_converter = snakecase ? -> (tag) { tag.snakecase.to_sym } : -> (tag) { tag.to_sym }
24
+ nori = Nori.new(convert_tags_to: tags_converter)
25
+ nori.parse(xml.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::NO_DECLARATION))
26
+ end
27
+
28
+ def card_truncated_pan(string)
29
+ string.gsub(CARD_TRUNCATED_PAN_REGEX, "\\1...\\2")
30
+ end
31
+
32
+ def card_expiry_time(string, year_format: "%y")
33
+ format_string = "%m/#{year_format}"
34
+ time = suppress(ArgumentError) { Time.zone.strptime(string, format_string) }
35
+ time + 1.month - 1.second if time
36
+ end
37
+
38
+ def extract_host(string)
39
+ URI(string).host
40
+ end
41
+
42
+ def parse_basic_auth(headers)
43
+ auth_header = headers.values_at(*RFC_AUTH_HEADERS).compact.first or return
44
+ credentials_b64 = auth_header[/\ABasic (.*)/, 1] or return
45
+ joined_credentials = Base64.strict_decode64(credentials_b64) rescue return
46
+
47
+ joined_credentials.split(":")
48
+ end
49
+
50
+ def safely_parse_base64(string)
51
+ Base64.strict_decode64(string)
52
+ rescue ArgumentError
53
+ nil
54
+ end
55
+
56
+ def safely_parse_json(string)
57
+ JSON.parse(string)
58
+ rescue JSON::ParserError
59
+ {}
60
+ end
61
+
62
+ def parse_datetime(timestamp, timezone: "UTC", format: nil)
63
+ return if timestamp.blank?
64
+ tz = ActiveSupport::TimeZone[timezone]
65
+ format ? tz.strptime(timestamp, format) : tz.parse(timestamp)
66
+ end
67
+
68
+ def sanitize_phone(string, e164_format: false)
69
+ phone = Phonelib.parse(string)
70
+ return if phone.invalid?
71
+ return phone.e164 if e164_format
72
+
73
+ phone.sanitized
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Passwords
5
+ extend self
6
+
7
+ def check(hash, password)
8
+ SCrypt::Password.new(hash).is_password?(password)
9
+ end
10
+
11
+ def create_hash(password)
12
+ SCrypt::Password.create(password).to_s
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Random
5
+ extend self
6
+
7
+ def uuid
8
+ SecureRandom.uuid
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ class RequestWrapper
5
+ include Memery
6
+
7
+ def initialize(request)
8
+ self.request = request
9
+ end
10
+
11
+ memoize def params
12
+ parse_params
13
+ end
14
+
15
+ memoize def body
16
+ request.body.read.dup.force_encoding("utf-8")
17
+ end
18
+
19
+ def [](key)
20
+ params[key]
21
+ end
22
+
23
+ def rails_params
24
+ request.params
25
+ end
26
+
27
+ def raw_request
28
+ request
29
+ end
30
+
31
+ memoize def http_headers
32
+ headers = request.headers.select do |key, _value|
33
+ key.start_with?("HTTP_") || key.in?(ActionDispatch::Http::Headers::CGI_VARIABLES)
34
+ end
35
+
36
+ HTTP::Headers.coerce(headers.sort)
37
+ end
38
+
39
+ memoize def path_parameters
40
+ request.path_parameters.except(:controller, :action).stringify_keys
41
+ end
42
+
43
+ def headers
44
+ request.headers
45
+ end
46
+
47
+ def ip
48
+ request.ip
49
+ end
50
+
51
+ private
52
+
53
+ attr_accessor :request
54
+
55
+ def parse_params
56
+ case request.content_type
57
+ when "application/json"
58
+ Parsing.safely_parse_json(body)
59
+ when "application/xml"
60
+ Parsing.parse_xml(body)
61
+ else
62
+ request.get? ? request.GET : request.POST
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Rounding
5
+ extend self
6
+
7
+ def fancy_round(number, rounding_method: :round, ugliness_level: 1)
8
+ return 0 unless number.positive?
9
+ log = Math.log(number, 10).floor
10
+ coef = 2**ugliness_level
11
+ (number * coef).public_send(rounding_method, -log) / coef.to_f
12
+ end
13
+
14
+ def super_round(number, rounding_method: :round)
15
+ return 0 unless number.positive?
16
+
17
+ coef = 10**Math.log(number, 10).floor
18
+ num = number / coef.to_f
19
+
20
+ best_diff = best_target = nil
21
+
22
+ [1.0, 1.5, 2.5, 5.0, 10.0].each do |target|
23
+ diff = target - num
24
+
25
+ next if rounding_method == :ceil && diff.negative?
26
+ next if rounding_method == :floor && diff.positive?
27
+
28
+ if best_diff.nil? || diff.abs < best_diff
29
+ best_diff = diff.abs
30
+ best_target = target
31
+ end
32
+ end
33
+
34
+ (best_target.to_d * coef).to_f
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Store
5
+ extend self
6
+
7
+ include Memery
8
+
9
+ def []=(key, value)
10
+ attrs = { key: key.to_s, value: JSON.dump(value), updated_at: Time.current }
11
+ entry.upsert_dataset.insert(attrs)
12
+ clear_cache_for(key)
13
+ end
14
+
15
+ def [](key)
16
+ find(key)&.value
17
+ end
18
+
19
+ def delete(key)
20
+ result = !!find(key)&.delete
21
+ clear_cache_for(key) if result
22
+ result
23
+ end
24
+
25
+ def find(key)
26
+ Rails.cache.fetch(cache_key_for(key)) { entry[key.to_s] }
27
+ end
28
+
29
+ memoize def entry
30
+ Sequel::Model(UmbrellioUtils.config.store_table_name)
31
+ end
32
+
33
+ private
34
+
35
+ def cache_key_for(key)
36
+ "store-entry-#{key}"
37
+ end
38
+
39
+ def clear_cache_for(key)
40
+ Rails.cache.delete(cache_key_for(key))
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ module Vault
5
+ extend self
6
+
7
+ def secret_engine_present?(engine_path)
8
+ ::Vault.logical.read("sys/mounts").data.key?("#{engine_path}/".to_sym)
9
+ end
10
+
11
+ def create_kv_engine(path)
12
+ ::Vault.logical.write(
13
+ "sys/mounts/#{path}",
14
+ config: {},
15
+ generate_signing_key: true,
16
+ options: { version: 2 },
17
+ path: path.to_s,
18
+ type: "kv",
19
+ )
20
+ end
21
+
22
+ def write_to_kv(engine_path:, secret_path:, data:)
23
+ full_data_path = File.join(engine_path, "data", secret_path)
24
+ full_meta_path = File.join(engine_path, "metadata", secret_path)
25
+ ::Vault.logical.write(full_data_path, data: data)
26
+ ::Vault.logical.write(full_meta_path, id: secret_path, max_versions: 1, cas_required: false)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UmbrellioUtils
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/umbrellio_utils/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "umbrellio-utils"
7
+ spec.version = UmbrellioUtils::VERSION
8
+ spec.authors = ["JustAnotherDude"]
9
+ spec.email = ["VanyaZ158@gmail.com"]
10
+
11
+ spec.summary = "A set of utilities that speed up development"
12
+ spec.description = "UmbrellioUtils is collection of utility classes and helpers"
13
+ spec.homepage = "https://github.com/umbrellio/utils"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/umbrellio/utils"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_dependency "memery"
30
+
31
+ spec.add_development_dependency "bundler"
32
+ spec.add_development_dependency "bundler-audit"
33
+ spec.add_development_dependency "ci-helper"
34
+ spec.add_development_dependency "pry"
35
+ spec.add_development_dependency "rake"
36
+ spec.add_development_dependency "rspec"
37
+ spec.add_development_dependency "rubocop-config-umbrellio"
38
+ spec.add_development_dependency "simplecov"
39
+ spec.add_development_dependency "simplecov-lcov"
40
+ end
metadata ADDED
@@ -0,0 +1,214 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: umbrellio-utils
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - JustAnotherDude
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-04-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: memery
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler-audit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ci-helper
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-config-umbrellio
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: simplecov-lcov
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: UmbrellioUtils is collection of utility classes and helpers
154
+ email:
155
+ - VanyaZ158@gmail.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - ".github/workflows/test.yml"
161
+ - ".gitignore"
162
+ - ".rspec"
163
+ - ".rubocop.yml"
164
+ - Gemfile
165
+ - LICENSE.txt
166
+ - README.md
167
+ - Rakefile
168
+ - bin/console
169
+ - bin/setup
170
+ - lib/umbrellio-utils.rb
171
+ - lib/umbrellio_utils.rb
172
+ - lib/umbrellio_utils/cards.rb
173
+ - lib/umbrellio_utils/checks.rb
174
+ - lib/umbrellio_utils/constants.rb
175
+ - lib/umbrellio_utils/control.rb
176
+ - lib/umbrellio_utils/database.rb
177
+ - lib/umbrellio_utils/formatting.rb
178
+ - lib/umbrellio_utils/http_client.rb
179
+ - lib/umbrellio_utils/misc.rb
180
+ - lib/umbrellio_utils/parsing.rb
181
+ - lib/umbrellio_utils/passwords.rb
182
+ - lib/umbrellio_utils/random.rb
183
+ - lib/umbrellio_utils/request_wrapper.rb
184
+ - lib/umbrellio_utils/rounding.rb
185
+ - lib/umbrellio_utils/store.rb
186
+ - lib/umbrellio_utils/vault.rb
187
+ - lib/umbrellio_utils/version.rb
188
+ - umbrellio_utils.gemspec
189
+ homepage: https://github.com/umbrellio/utils
190
+ licenses:
191
+ - MIT
192
+ metadata:
193
+ homepage_uri: https://github.com/umbrellio/utils
194
+ source_code_uri: https://github.com/umbrellio/utils
195
+ post_install_message:
196
+ rdoc_options: []
197
+ require_paths:
198
+ - lib
199
+ required_ruby_version: !ruby/object:Gem::Requirement
200
+ requirements:
201
+ - - ">="
202
+ - !ruby/object:Gem::Version
203
+ version: 2.6.0
204
+ required_rubygems_version: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ requirements: []
210
+ rubygems_version: 3.2.15
211
+ signing_key:
212
+ specification_version: 4
213
+ summary: A set of utilities that speed up development
214
+ test_files: []