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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +239 -5
  5. data/app/controllers/dynamic_links/application_controller.rb +17 -1
  6. data/app/controllers/dynamic_links/redirects_controller.rb +29 -0
  7. data/app/controllers/dynamic_links/v1/short_links_controller.rb +93 -12
  8. data/app/jobs/dynamic_links/generate_short_links_job.rb +1 -1
  9. data/app/jobs/dynamic_links/shorten_url_job.rb +31 -0
  10. data/app/models/dynamic_links/client.rb +5 -0
  11. data/app/models/dynamic_links/shortened_url.rb +20 -2
  12. data/config/routes.rb +3 -0
  13. data/db/migrate/20231228165744_create_dynamic_links_clients.rb +8 -1
  14. data/db/migrate/20231228175142_create_dynamic_links_shortened_urls.rb +3 -4
  15. data/db/migrate/20240128030329_fix_citus_index.rb +34 -0
  16. data/db/migrate/20240128030419_add_unique_index_to_shortened_urls.rb +5 -0
  17. data/lib/dynamic_links/async/locker.rb +60 -0
  18. data/lib/dynamic_links/configuration.rb +95 -3
  19. data/lib/dynamic_links/error_classes.rb +6 -0
  20. data/lib/dynamic_links/logger.rb +32 -0
  21. data/lib/dynamic_links/redis_config.rb +22 -0
  22. data/lib/dynamic_links/shortener.rb +52 -0
  23. data/lib/dynamic_links/shortening_strategies/base_strategy.rb +6 -0
  24. data/lib/dynamic_links/shortening_strategies/nano_id_strategy.rb +7 -9
  25. data/lib/dynamic_links/shortening_strategies/redis_counter_strategy.rb +25 -14
  26. data/lib/dynamic_links/strategy_factory.rb +28 -1
  27. data/lib/dynamic_links/validator.rb +14 -0
  28. data/lib/dynamic_links/version.rb +1 -1
  29. data/lib/dynamic_links.rb +54 -17
  30. 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,5 @@
1
+ class AddUniqueIndexToShortenedUrls < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_index :dynamic_links_shortened_urls, [:client_id, :short_url], unique: true
4
+ end
5
+ 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
- attr_accessor :shortening_strategy
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 = :MD5 # Default 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,6 @@
1
+ module DynamicLinks
2
+ class InvalidURIError < ::URI::InvalidURIError; end
3
+ class ConfigurationError < StandardError; end
4
+ class MissingDependency < LoadError; end
5
+ class ShorteningFailed < StandardError; end
6
+ end
@@ -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 size [Integer] The size (length) of the generated Nano ID
15
- def shorten(url, size: MIN_LENGTH)
16
- ::Nanoid.generate(size: 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
- def initialize
15
- # TODO: use pool of connections
16
- @redis = Redis.new
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
- counter = @redis.incr(REDIS_COUNTER_KEY)
24
-
25
- short_url = base62_encode("#{counter}#{url.hash.abs}".to_i)
26
- short_url.ljust(min_length, '0')
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
- ShorteningStrategies::NanoIdStrategy.new
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
+
@@ -1,3 +1,3 @@
1
1
  module DynamicLinks
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
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/configuration"
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
- strategy_key = configuration.shortening_strategy
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
- strategy.shorten(url)
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