dynamic_links 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +37 -0
- data/MIT-LICENSE +21 -0
- data/README.md +239 -5
- data/app/controllers/dynamic_links/application_controller.rb +17 -1
- data/app/controllers/dynamic_links/redirects_controller.rb +29 -0
- data/app/controllers/dynamic_links/v1/short_links_controller.rb +93 -12
- data/app/jobs/dynamic_links/generate_short_links_job.rb +1 -1
- data/app/jobs/dynamic_links/shorten_url_job.rb +31 -0
- data/app/models/dynamic_links/client.rb +5 -0
- data/app/models/dynamic_links/shortened_url.rb +20 -2
- data/config/routes.rb +3 -0
- data/db/migrate/20231228165744_create_dynamic_links_clients.rb +8 -1
- data/db/migrate/20231228175142_create_dynamic_links_shortened_urls.rb +3 -4
- data/db/migrate/20240128030329_fix_citus_index.rb +34 -0
- data/db/migrate/20240128030419_add_unique_index_to_shortened_urls.rb +5 -0
- data/lib/dynamic_links/async/locker.rb +60 -0
- data/lib/dynamic_links/configuration.rb +95 -3
- data/lib/dynamic_links/error_classes.rb +6 -0
- data/lib/dynamic_links/logger.rb +32 -0
- data/lib/dynamic_links/redis_config.rb +22 -0
- data/lib/dynamic_links/shortener.rb +52 -0
- data/lib/dynamic_links/shortening_strategies/base_strategy.rb +6 -0
- data/lib/dynamic_links/shortening_strategies/nano_id_strategy.rb +7 -9
- data/lib/dynamic_links/shortening_strategies/redis_counter_strategy.rb +25 -14
- data/lib/dynamic_links/strategy_factory.rb +28 -1
- data/lib/dynamic_links/validator.rb +14 -0
- data/lib/dynamic_links/version.rb +1 -1
- data/lib/dynamic_links.rb +54 -17
- metadata +73 -16
@@ -0,0 +1,34 @@
|
|
1
|
+
class FixCitusIndex < ActiveRecord::Migration[7.1]
|
2
|
+
def up
|
3
|
+
if DynamicLinks.configuration.db_infra_strategy == :sharding
|
4
|
+
# execute SQL to remove primary key constraint
|
5
|
+
execute <<-SQL
|
6
|
+
ALTER TABLE dynamic_links_shortened_urls
|
7
|
+
DROP CONSTRAINT dynamic_links_shortened_urls_pkey;
|
8
|
+
SQL
|
9
|
+
|
10
|
+
execute <<-SQL
|
11
|
+
ALTER TABLE dynamic_links_shortened_urls
|
12
|
+
ADD PRIMARY KEY (id, client_id);
|
13
|
+
SQL
|
14
|
+
create_distributed_table :dynamic_links_shortened_urls, :client_id
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# this code is untested
|
19
|
+
def down
|
20
|
+
if DynamicLinks.configuration.db_infra_strategy == :sharding
|
21
|
+
drop_distributed_table :dynamic_links_shortened_urls, :client_id
|
22
|
+
|
23
|
+
execute <<-SQL
|
24
|
+
ALTER TABLE dynamic_links_shortened_urls
|
25
|
+
DROP CONSTRAINT dynamic_links_shortened_urls_pkey;
|
26
|
+
SQL
|
27
|
+
|
28
|
+
execute <<-SQL
|
29
|
+
ALTER TABLE dynamic_links_shortened_urls
|
30
|
+
ADD PRIMARY KEY (id);
|
31
|
+
SQL
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module DynamicLinks
|
2
|
+
module Async
|
3
|
+
# @author Saiqul Haq <saiqulhaq@gmail.com>
|
4
|
+
class Locker
|
5
|
+
LockAcquisitionError = Class.new(StandardError)
|
6
|
+
LockReleaseError = Class.new(StandardError)
|
7
|
+
attr_reader :cache_store
|
8
|
+
|
9
|
+
def initialize(cache_store = DynamicLinks.configuration.cache_store)
|
10
|
+
@cache_store = cache_store
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate_lock_key(client, url)
|
14
|
+
"lock:shorten_url#{client.id}:#{url_to_lock_key(url)}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def locked?(lock_key)
|
18
|
+
cache_store.exist?(lock_key)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Acquires a lock for the given key and executes the block if lock is acquired.
|
22
|
+
# This method won't release the lock after block execution.
|
23
|
+
# We release the lock in the job after the job is done.
|
24
|
+
# @param [String] lock_key, it's better to use generate_lock_key method to generate lock_key
|
25
|
+
# @param [Integer] expires_in, default is 60 seconds
|
26
|
+
# @param [Block] block, the block to be executed if lock is acquired
|
27
|
+
# @return [Boolean]
|
28
|
+
def lock_if_absent(lock_key, expires_in: 60, &block)
|
29
|
+
is_locked = false
|
30
|
+
begin
|
31
|
+
is_locked = cache_store.increment(lock_key, 1, expires_in: expires_in) == 1
|
32
|
+
yield if is_locked && block_given?
|
33
|
+
|
34
|
+
unless is_locked
|
35
|
+
DynamicLinks::Logger.log_info "Unable to acquire lock for key: #{lock_key}"
|
36
|
+
end
|
37
|
+
rescue => e
|
38
|
+
DynamicLinks::Logger.log_error("Locking error: #{e.message}")
|
39
|
+
raise e
|
40
|
+
end
|
41
|
+
|
42
|
+
is_locked
|
43
|
+
end
|
44
|
+
|
45
|
+
# Deletes an entry in the cache. Returns true if an entry is deleted and false otherwise.
|
46
|
+
# @return [Boolean]
|
47
|
+
def unlock(lock_key)
|
48
|
+
deleted = cache_store.delete(lock_key)
|
49
|
+
raise LockReleaseError, "Unable to release lock for key: #{lock_key}" unless deleted
|
50
|
+
deleted
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def url_to_lock_key(url)
|
56
|
+
Digest::SHA256.hexdigest(url)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -1,10 +1,102 @@
|
|
1
1
|
module DynamicLinks
|
2
|
+
# @author Saiqul Haq <saiqulhaq@gmail.com>
|
2
3
|
class Configuration
|
3
|
-
|
4
|
+
attr_reader :shortening_strategy, :enable_rest_api, :db_infra_strategy,
|
5
|
+
:async_processing, :redis_counter_config, :cache_store,
|
6
|
+
:enable_fallback_mode, :firebase_host
|
4
7
|
|
8
|
+
VALID_DB_INFRA_STRATEGIES = [:standard, :sharding].freeze
|
9
|
+
|
10
|
+
DEFAULT_SHORTENING_STRATEGY = :md5
|
11
|
+
DEFAULT_ENABLE_REST_API = true
|
12
|
+
DEFAULT_DB_INFRA_STRATEGY = :standard
|
13
|
+
DEFAULT_ASYNC_PROCESSING = false
|
14
|
+
DEFAULT_REDIS_COUNTER_CONFIG = RedisConfig.new
|
15
|
+
# use any class that extends ActiveSupport::Cache::Store, default is MemoryStore
|
16
|
+
DEFAULT_CACHE_STORE = ActiveSupport::Cache::MemoryStore.new
|
17
|
+
DEFAULT_ENABLE_FALLBACK_MODE = false
|
18
|
+
DEFAULT_FIREBASE_HOST = nil
|
19
|
+
|
20
|
+
# Usage:
|
21
|
+
# DynamicLinks.configure do |config|
|
22
|
+
# config.shortening_strategy = :md5 # or other strategy name, see StrategyFactory for available strategies
|
23
|
+
# config.enable_rest_api = true # or false. when false, the API requests will be rejected
|
24
|
+
# config.db_infra_strategy = :standard # or :sharding. if sharding is used, then xxx
|
25
|
+
# config.async_processing = false # or true. if true, the shortening process will be done asynchronously using ActiveJob
|
26
|
+
# config.redis_counter_config = RedisConfig.new # see RedisConfig documentation for more details
|
27
|
+
# # if you use Redis
|
28
|
+
# config.cache_store = ActiveSupport::Cache::RedisCacheStore.new(url: 'redis://localhost:6379/0/cache')
|
29
|
+
# # if you use Memcached
|
30
|
+
# config.cache_store = ActiveSupport::Cache::MemCacheStore.new('localhost:11211')
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# @return [Configuration]
|
5
34
|
def initialize
|
6
|
-
@shortening_strategy =
|
35
|
+
@shortening_strategy = DEFAULT_SHORTENING_STRATEGY
|
36
|
+
@enable_rest_api = DEFAULT_ENABLE_REST_API
|
37
|
+
@db_infra_strategy = DEFAULT_DB_INFRA_STRATEGY
|
38
|
+
@async_processing = DEFAULT_ASYNC_PROCESSING
|
39
|
+
# config for RedisCounterStrategy
|
40
|
+
@redis_counter_config = DEFAULT_REDIS_COUNTER_CONFIG
|
41
|
+
@cache_store = DEFAULT_CACHE_STORE
|
42
|
+
@enable_fallback_mode = DEFAULT_ENABLE_FALLBACK_MODE
|
43
|
+
@firebase_host = DEFAULT_FIREBASE_HOST
|
44
|
+
end
|
45
|
+
|
46
|
+
def shortening_strategy=(strategy)
|
47
|
+
unless StrategyFactory::VALID_SHORTENING_STRATEGIES.include?(strategy)
|
48
|
+
raise ArgumentError, "Invalid shortening strategy, provided strategy: #{strategy}. Valid strategies are: #{StrategyFactory::VALID_SHORTENING_STRATEGIES.join(', ')}"
|
49
|
+
end
|
50
|
+
|
51
|
+
@shortening_strategy = strategy
|
52
|
+
end
|
53
|
+
|
54
|
+
def enable_rest_api=(value)
|
55
|
+
raise ArgumentError, "enable_rest_api must be a boolean" unless [true, false].include?(value)
|
56
|
+
@enable_rest_api = value
|
57
|
+
end
|
58
|
+
|
59
|
+
def db_infra_strategy=(strategy)
|
60
|
+
raise ArgumentError, "Invalid DB infra strategy" unless VALID_DB_INFRA_STRATEGIES.include?(strategy)
|
61
|
+
@db_infra_strategy = strategy
|
62
|
+
end
|
63
|
+
|
64
|
+
def async_processing=(value)
|
65
|
+
raise ArgumentError, "async_processing must be a boolean" unless [true, false].include?(value)
|
66
|
+
@async_processing = value
|
67
|
+
end
|
68
|
+
|
69
|
+
def redis_counter_config=(config)
|
70
|
+
raise ArgumentError, "redis_counter_config must be an instance of RedisConfig" unless config.is_a?(RedisConfig)
|
71
|
+
@redis_counter_config = config
|
72
|
+
end
|
73
|
+
|
74
|
+
def cache_store=(store)
|
75
|
+
raise ArgumentError, "cache_store must be an instance of ActiveSupport::Cache::Store" unless store.is_a?(ActiveSupport::Cache::Store)
|
76
|
+
@cache_store = store
|
77
|
+
end
|
78
|
+
|
79
|
+
def enable_fallback_mode=(value)
|
80
|
+
raise ArgumentError, "enable_fallback_mode must be a boolean" unless [true, false].include?(value)
|
81
|
+
@enable_fallback_mode = value
|
82
|
+
end
|
83
|
+
|
84
|
+
def firebase_host=(host)
|
85
|
+
# allow nil or blank host (optional, depends on your app logic)
|
86
|
+
if host.nil? || host.strip.empty?
|
87
|
+
@firebase_host = nil
|
88
|
+
return
|
89
|
+
end
|
90
|
+
|
91
|
+
begin
|
92
|
+
uri = URI.parse(host.to_s)
|
93
|
+
valid = uri.is_a?(URI::HTTP) && uri.host.present?
|
94
|
+
raise unless valid
|
95
|
+
rescue
|
96
|
+
raise ArgumentError, "firebase_host must be a valid URL with a host"
|
97
|
+
end
|
98
|
+
|
99
|
+
@firebase_host = host
|
7
100
|
end
|
8
101
|
end
|
9
102
|
end
|
10
|
-
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module DynamicLinks
|
2
|
+
# @author Saiqul Haq <saiqulhaq@gmail.com>
|
3
|
+
class Logger
|
4
|
+
def self.instance
|
5
|
+
@logger ||= Rails.logger
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.log_info(message)
|
9
|
+
instance.info(message)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.log_error(message)
|
13
|
+
instance.error(message)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.log_warn(message)
|
17
|
+
instance.warn(message)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.log_debug(message)
|
21
|
+
instance.debug(message)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.log_fatal(message)
|
25
|
+
instance.fatal(message)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.log_unknown(message)
|
29
|
+
instance.unknown(message)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# @author Saiqul Haq <saiqulhaq@gmail.com>
|
2
|
+
|
3
|
+
module DynamicLinks
|
4
|
+
# RedisConfig is a class to hold Redis configuration
|
5
|
+
class RedisConfig
|
6
|
+
attr_accessor :config, :pool_size, :pool_timeout
|
7
|
+
|
8
|
+
# @param [Hash] config
|
9
|
+
# Default to an empty hash, can be overridden
|
10
|
+
# config = {
|
11
|
+
# host: 'localhost',
|
12
|
+
# port: 6379
|
13
|
+
# }
|
14
|
+
# @param [Integer] pool_size Default to 5, can be overridden
|
15
|
+
# @param [Integer] pool_timeout Default to 5, can be overridden
|
16
|
+
def initialize(config = {}, pool_size = 5, pool_timeout = 5)
|
17
|
+
@config = config
|
18
|
+
@pool_size = pool_size
|
19
|
+
@pool_timeout = pool_timeout
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module DynamicLinks
|
2
|
+
# @author Saiqul Haq <saiqulhaq@gmail.com>
|
3
|
+
class Shortener
|
4
|
+
attr_reader :locker, :strategy, :storage, :async_worker
|
5
|
+
|
6
|
+
def initialize(locker: DynamicLinks::Async::Locker.new,
|
7
|
+
strategy: StrategyFactory.get_strategy(DynamicLinks.configuration.shortening_strategy),
|
8
|
+
storage: ShortenedUrl,
|
9
|
+
async_worker: ShortenUrlJob)
|
10
|
+
@locker = locker
|
11
|
+
@strategy = strategy
|
12
|
+
@storage = storage
|
13
|
+
@async_worker = async_worker
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param client [Client] the client that owns the url
|
17
|
+
# @param url [String] the url to be shortened
|
18
|
+
# @return [String] the shortened url
|
19
|
+
def shorten(client, url)
|
20
|
+
short_url = strategy.shorten(url)
|
21
|
+
|
22
|
+
if strategy.always_growing?
|
23
|
+
storage.create!(client: client, url: url, short_url: short_url)
|
24
|
+
else
|
25
|
+
storage.find_or_create!(client, short_url, url)
|
26
|
+
end
|
27
|
+
URI::Generic.build({scheme: client.scheme, host: client.hostname, path: "/#{short_url}"}).to_s
|
28
|
+
rescue => e
|
29
|
+
DynamicLinks::Logger.log_error("Error shortening URL: #{e.message}")
|
30
|
+
raise e
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param client [Client] the client that owns the url
|
34
|
+
# @param url [String] the url to be shortened
|
35
|
+
def shorten_async(client, url)
|
36
|
+
lock_key = locker.generate_lock_key(client, url)
|
37
|
+
|
38
|
+
locker.lock_if_absent(lock_key) do
|
39
|
+
short_url = strategy.shorten(url)
|
40
|
+
content = {
|
41
|
+
url: url,
|
42
|
+
short_url: short_url
|
43
|
+
}
|
44
|
+
|
45
|
+
async_worker.perform_later(client, url, short_url, lock_key)
|
46
|
+
end
|
47
|
+
rescue => e
|
48
|
+
DynamicLinks::Logger.log_error("Error shortening URL asynchronously: #{e.message}")
|
49
|
+
raise e
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -9,6 +9,12 @@ module DynamicLinks
|
|
9
9
|
raise NotImplementedError, "You must implement the shorten method"
|
10
10
|
end
|
11
11
|
|
12
|
+
# Determines if the strategy always generates a new shortened URL
|
13
|
+
# @return [Boolean]
|
14
|
+
def always_growing?
|
15
|
+
false # Default behavior is not to always grow
|
16
|
+
end
|
17
|
+
|
12
18
|
private
|
13
19
|
|
14
20
|
# Convert an integer into a Base62 string
|
@@ -3,17 +3,15 @@ module DynamicLinks
|
|
3
3
|
# Shortens the given URL using Nano ID
|
4
4
|
# This strategy will generate a different short URL for the same given URL
|
5
5
|
class NanoIDStrategy < BaseStrategy
|
6
|
-
begin
|
7
|
-
require 'nanoid'
|
8
|
-
rescue LoadError
|
9
|
-
raise 'Missing dependency: Please add "nanoid" to your Gemfile to use NanoIdStrategy.'
|
10
|
-
end
|
11
|
-
|
12
6
|
# Shortens the given URL using Nano ID
|
13
7
|
# @param url [String] The URL to shorten (not directly used in Nano ID strategy)
|
14
|
-
# @param
|
15
|
-
def shorten(url,
|
16
|
-
::Nanoid.generate(size:
|
8
|
+
# @param min_length [Integer] The size (length) of the generated Nano ID
|
9
|
+
def shorten(url, min_length: MIN_LENGTH)
|
10
|
+
::Nanoid.generate(size: min_length)
|
11
|
+
end
|
12
|
+
|
13
|
+
def always_growing?
|
14
|
+
true # This strategy always generates a new shortened URL
|
17
15
|
end
|
18
16
|
end
|
19
17
|
end
|
@@ -1,29 +1,40 @@
|
|
1
|
-
|
2
1
|
module DynamicLinks
|
3
2
|
module ShorteningStrategies
|
3
|
+
# usage:
|
4
|
+
# Using default configuration from DynamicLinks configuration
|
5
|
+
# default_strategy = DynamicLinks::ShorteningStrategies::RedisCounterStrategy.new
|
6
|
+
#
|
7
|
+
# Using a custom configuration
|
8
|
+
# custom_redis_config = { host: 'custom-host', port: 6380 }
|
9
|
+
# custom_strategy = DynamicLinks::ShorteningStrategies::RedisCounterStrategy.new(custom_redis_config)
|
4
10
|
class RedisCounterStrategy < BaseStrategy
|
5
|
-
begin
|
6
|
-
require 'redis'
|
7
|
-
rescue LoadError
|
8
|
-
raise 'Missing dependency: Please add "redis" to your Gemfile to use RedisCounterStrategy.'
|
9
|
-
end
|
10
|
-
|
11
11
|
MIN_LENGTH = 12
|
12
12
|
REDIS_COUNTER_KEY = "dynamic_links:counter".freeze
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
# @param redis_config [Hash]
|
15
|
+
def initialize(redis_config = nil)
|
16
|
+
super()
|
17
|
+
|
18
|
+
configuration = redis_config.nil? ? DynamicLinks.configuration.redis_counter_config : DynamicLinks::Configuration::RedisConfig.new(redis_config)
|
19
|
+
@redis = ConnectionPool.new(size: configuration.pool_size, timeout: configuration.pool_timeout) do
|
20
|
+
Redis.new(configuration.config)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def always_growing?
|
25
|
+
true # This strategy always generates a new shortened URL
|
17
26
|
end
|
18
27
|
|
19
28
|
# Shortens the given URL using a Redis counter
|
20
29
|
# @param url [String] The URL to shorten
|
21
30
|
# @return [String] The shortened URL, 12 characters long
|
22
31
|
def shorten(url, min_length: MIN_LENGTH)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
32
|
+
@redis.with do |conn|
|
33
|
+
counter = conn.incr(REDIS_COUNTER_KEY)
|
34
|
+
short_url = base62_encode("#{counter}#{url.hash.abs}".to_i)
|
35
|
+
short_url = short_url.ljust(min_length, '0')
|
36
|
+
short_url
|
37
|
+
end
|
27
38
|
end
|
28
39
|
end
|
29
40
|
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
module DynamicLinks
|
2
2
|
class StrategyFactory
|
3
|
+
VALID_SHORTENING_STRATEGIES = [:md5, :sha256, :crc32,
|
4
|
+
:nano_id, :redis_counter, :mock].freeze
|
5
|
+
|
3
6
|
def self.get_strategy(strategy_name)
|
4
7
|
case strategy_name
|
5
8
|
when :md5
|
@@ -9,8 +12,10 @@ module DynamicLinks
|
|
9
12
|
when :crc32
|
10
13
|
ShorteningStrategies::CRC32Strategy.new
|
11
14
|
when :nano_id
|
12
|
-
|
15
|
+
ensure_nanoid_available
|
16
|
+
ShorteningStrategies::NanoIDStrategy.new
|
13
17
|
when :redis_counter
|
18
|
+
ensure_redis_available
|
14
19
|
ShorteningStrategies::RedisCounterStrategy.new
|
15
20
|
when :mock
|
16
21
|
ShorteningStrategies::MockStrategy.new
|
@@ -18,6 +23,28 @@ module DynamicLinks
|
|
18
23
|
raise "Unknown strategy: #{strategy_name}"
|
19
24
|
end
|
20
25
|
end
|
26
|
+
|
27
|
+
def self.ensure_nanoid_available
|
28
|
+
begin
|
29
|
+
require 'nanoid'
|
30
|
+
rescue LoadError
|
31
|
+
raise 'Missing dependency: Please add "nanoid" to your Gemfile to use NanoIdStrategy.'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.ensure_redis_available
|
36
|
+
begin
|
37
|
+
require 'redis'
|
38
|
+
rescue LoadError
|
39
|
+
raise 'Missing dependency: Please add "redis" to your Gemfile to use RedisCounterStrategy.'
|
40
|
+
end
|
41
|
+
|
42
|
+
begin
|
43
|
+
require 'connection_pool'
|
44
|
+
rescue LoadError
|
45
|
+
raise 'Missing dependency: Please add "connection_pool" to your Gemfile to use RedisCounterStrategy.'
|
46
|
+
end
|
47
|
+
end
|
21
48
|
end
|
22
49
|
end
|
23
50
|
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module DynamicLinks
|
2
|
+
class Validator
|
3
|
+
# Validates if the given URL is a valid HTTP or HTTPS URL
|
4
|
+
# @param url [String] The URL to validate
|
5
|
+
# @return [Boolean] true if valid, false otherwise
|
6
|
+
def self.valid_url?(url)
|
7
|
+
uri = URI.parse(url)
|
8
|
+
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
9
|
+
rescue URI::InvalidURIError
|
10
|
+
false
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
data/lib/dynamic_links.rb
CHANGED
@@ -1,5 +1,31 @@
|
|
1
|
+
# @author Saiqul Haq <saiqulhaq@gmail.com>
|
2
|
+
|
3
|
+
if ENV['RAILS_ENV'] == 'test'
|
4
|
+
require 'simplecov'
|
5
|
+
|
6
|
+
SimpleCov.start do
|
7
|
+
load_profile "test_frameworks"
|
8
|
+
|
9
|
+
add_filter %r{^/config/}
|
10
|
+
add_filter %r{^/db/}
|
11
|
+
|
12
|
+
add_group "Controllers", "app/controllers"
|
13
|
+
add_group "Channels", "app/channels"
|
14
|
+
add_group "Models", "app/models"
|
15
|
+
add_group "Mailers", "app/mailers"
|
16
|
+
add_group "Helpers", "app/helpers"
|
17
|
+
add_group "Jobs", %w[app/jobs app/workers]
|
18
|
+
add_group "DynamicLinks", "lib/"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
1
22
|
require "dynamic_links/version"
|
2
23
|
require "dynamic_links/engine"
|
24
|
+
require "dynamic_links/logger"
|
25
|
+
require "dynamic_links/error_classes"
|
26
|
+
require "dynamic_links/redis_config"
|
27
|
+
require "dynamic_links/configuration"
|
28
|
+
require "dynamic_links/validator"
|
3
29
|
require "dynamic_links/strategy_factory"
|
4
30
|
require "dynamic_links/shortening_strategies/base_strategy"
|
5
31
|
require "dynamic_links/shortening_strategies/sha256_strategy"
|
@@ -8,7 +34,8 @@ require "dynamic_links/shortening_strategies/crc32_strategy"
|
|
8
34
|
require "dynamic_links/shortening_strategies/nano_id_strategy"
|
9
35
|
require "dynamic_links/shortening_strategies/redis_counter_strategy"
|
10
36
|
require "dynamic_links/shortening_strategies/mock_strategy"
|
11
|
-
require "dynamic_links/
|
37
|
+
require "dynamic_links/async/locker"
|
38
|
+
require "dynamic_links/shortener"
|
12
39
|
|
13
40
|
module DynamicLinks
|
14
41
|
class << self
|
@@ -23,26 +50,20 @@ module DynamicLinks
|
|
23
50
|
end
|
24
51
|
end
|
25
52
|
|
26
|
-
def self.shorten_url(url)
|
27
|
-
|
28
|
-
|
29
|
-
begin
|
30
|
-
strategy = StrategyFactory.get_strategy(strategy_key)
|
31
|
-
rescue RuntimeError => e
|
32
|
-
# This will catch the 'Unknown strategy' error from the factory
|
33
|
-
raise "Invalid shortening strategy: #{strategy_key}. Error: #{e.message}"
|
34
|
-
rescue ArgumentError
|
35
|
-
raise "#{strategy_key} strategy needs to be initialized with arguments"
|
36
|
-
rescue => e
|
37
|
-
raise "Unexpected error while initializing the strategy: #{e.message}"
|
38
|
-
end
|
53
|
+
def self.shorten_url(url, client, async: DynamicLinks.configuration.async_processing)
|
54
|
+
raise InvalidURIError, 'Invalid URL' unless Validator.valid_url?(url)
|
39
55
|
|
40
|
-
|
56
|
+
shortener = Shortener.new
|
57
|
+
if async
|
58
|
+
shortener.shorten_async(client, url)
|
59
|
+
else
|
60
|
+
shortener.shorten(client, url)
|
61
|
+
end
|
41
62
|
end
|
42
63
|
|
43
64
|
# mimic Firebase Dynamic Links API
|
44
|
-
def self.generate_short_url(original_url)
|
45
|
-
short_link = shorten_url(original_url)
|
65
|
+
def self.generate_short_url(original_url, client)
|
66
|
+
short_link = shorten_url(original_url, client)
|
46
67
|
|
47
68
|
{
|
48
69
|
shortLink: short_link,
|
@@ -50,4 +71,20 @@ module DynamicLinks
|
|
50
71
|
warning: []
|
51
72
|
}
|
52
73
|
end
|
74
|
+
|
75
|
+
def self.resolve_short_url(short_link)
|
76
|
+
DynamicLinks::ShortenedUrl.find_by(short_url: short_link)&.url
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.find_short_link(long_url, client)
|
80
|
+
short_link = DynamicLinks::ShortenedUrl.find_by(url: long_url, client_id: client.id)
|
81
|
+
if short_link
|
82
|
+
{
|
83
|
+
short_url: "#{client.scheme}://#{client.hostname}/#{short_link.short_url}",
|
84
|
+
full_url: long_url
|
85
|
+
}
|
86
|
+
else
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
end
|
53
90
|
end
|