Skip to content

Commit

Permalink
Use pseudo random urn generator
Browse files Browse the repository at this point in the history
Urn represents a pseudo random Uniform Resource Name (URN) generator.
Invoking the method `next` returns a unique URN with a fixed prefix
and a random alphanumeric suffix.
  • Loading branch information
fumimowdan committed Oct 18, 2023
1 parent 2670363 commit 4218f51
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 30 deletions.
1 change: 1 addition & 0 deletions app/models/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,5 @@ def mark_as_qa!
validates(:subject, presence: true)
validates(:visa_type, presence: true)
validates(:applicant, presence: true)
validates(:urn, uniqueness: true)
end
123 changes: 100 additions & 23 deletions app/models/urn.rb
Original file line number Diff line number Diff line change
@@ -1,38 +1,115 @@
# frozen_string_literal: true

# Urn represents a Uniform Resource Name (URN) generator.
# It generates a URN with a fixed prefix and a random alphanumeric suffix.
# Urn represents a pseudo random Uniform Resource Name (URN) generator.
# Invoking the method `next` returns a unique URN with a fixed prefix
# and a random alphanumeric suffix.
#
# Urn.configure do |c|
# c.max_suffix = 11
# c.seeds = { teacher: ENV['TEACHER_URN_SEED'] }
# c.urns = ->(route) { Application.where(application_route: route).pluck(:urn) }
# end
#
# Example:
#
# Urn.generate('teacher') # => "IRPTE12345"
# Urn.generate('teacher') # => "IRPTE12345"
# Urn.generate('salaried_trainee') # => "IRPST12345"
# Urn.next('teacher') # => "IRPTE12345"
# Urn.next('teacher') # => "IRPTE12345"
# Urn.next('salaried_trainee') # => "IRPST12345"
#
class Urn
attr_reader :value
attr_writer :suffix
class NoUrnAvailableError < StandardError; end

def self.generate(applicant_type)
code = applicant_type_code(applicant_type)
PREFIX + code + Array.new(LENGTH) { CHARSET.sample }.join
class Config
def initialize
@default_prefix = "IRP"
@default_max_suffix = 99_999
@default_codes = {
teacher: "TE",
salaried_trainee: "ST",
}.with_indifferent_access
@default_urns = ->(_) { [] }
end

attr_writer :prefix, :codes, :max_suffix, :seeds, :urns, :padding_size

def prefix
@prefix || @default_prefix
end

def codes
(@codes || @default_codes).with_indifferent_access
end

def max_suffix
@max_suffix || @default_max_suffix
end

def padding_size
@padding_size || max_suffix.to_s.size
end

def seeds
(@seeds || {}).with_indifferent_access
end

def urns
@urns || @default_urns
end
end

CHARSET = %w[0 1 2 3 4 5 6 7 8 9].freeze
PREFIX = "IRP"
LENGTH = 5
private_constant :CHARSET, :PREFIX, :LENGTH
class << self
def configure
yield(config)
end

def config
return @config if @config.present?

@config = Config.new
end

def next(route)
routes[route].next
rescue KeyError
raise(ArgumentError, "Invalid route: #{route}, must be one of #{config.codes.keys}")
end

private

def routes
@routes ||= Hash.new do |hash, route|
hash[route] = urn_enumerator(
config.codes.fetch(route),
config.seeds.fetch(route, Random.new_seed),
config.urns.call(route),
)
end
end

def urns(code, seed)
Array
.new(config.max_suffix) { formatter(code, _1) }
.drop(1)
.shuffle!(random: Random.new(seed))
end

def formatter(code, suffix)
[config.prefix, code, sprintf("%0#{config.padding_size}d", suffix)].join
end

def available_urns(code, seed, used_urns)
urns(code, seed) - used_urns
end

def urn_enumerator(code, seed, used_urns)
list = available_urns(code, seed, used_urns)
error_msg = "you have exhausted urn for code #{code} you need to increase the size of the suffix"

Enumerator.new do |yielder|
list.each { yielder << _1 }

def self.applicant_type_code(applicant_type)
case applicant_type
when "teacher"
"TE"
when "salaried_trainee"
"ST"
else
raise(ArgumentError, "Invalid applicant type: #{applicant_type}")
raise(NoUrnAvailableError, error_msg)
end
end
end
private_methods :applicant_type_code
end
2 changes: 1 addition & 1 deletion app/services/submit_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def create_application
date_of_entry: form.date_of_entry,
start_date: form.start_date,
subject: SubjectStep.new(form).answer.formatted_value,
urn: Urn.generate(form.application_route),
urn: Urn.next(form.application_route),
visa_type: form.visa_type,
)
end
Expand Down
7 changes: 7 additions & 0 deletions config/initializers/urn.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require Rails.root.join("app/models/urn")

Urn.configure do |c|
c.prefix = "IRP"
c.max_suffix = 99_999
c.urns = ->(route) { Application.where(application_route: route).pluck(:urn) }
end
2 changes: 1 addition & 1 deletion db/migrate/20231009110217_add_application_indexes.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class AddApplicationIndexes < ActiveRecord::Migration[7.0]
def change
drop_table :urns
drop_table :urns # rubocop:disable Rails/ReversibleMigration
add_index :applications, :urn, unique: true
add_index :applications, :application_route
end
Expand Down
2 changes: 1 addition & 1 deletion spec/factories/applications.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
visa_type { VisaStep::VALID_ANSWERS_OPTIONS.reject { _1 == "Other" }.sample }
date_of_entry { Time.zone.today }
start_date { 1.month.from_now.to_date }
urn { Urn.generate(application_route) }
urn { Urn.next(application_route) }

factory :teacher_application do
application_route { "teacher" }
Expand Down
6 changes: 3 additions & 3 deletions spec/models/urn_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
require "rails_helper"

RSpec.describe Urn do
subject(:urn) { described_class.generate(applicant_type) }
subject(:urn) { described_class.next(applicant_type) }

describe ".generate" do
describe ".next" do
context 'when applicant type is "teacher"' do
let(:applicant_type) { "teacher" }

Expand All @@ -32,7 +32,7 @@
let(:applicant_type) { "invalid_type" }

it "raises an ArgumentError" do
expect { urn }.to raise_error(ArgumentError, "Invalid applicant type: invalid_type")
expect { urn }.to raise_error(ArgumentError, 'Invalid route: invalid_type, must be one of ["teacher", "salaried_trainee"]')
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/services/submit_form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@

context "applicant email" do
before do
allow(Urn).to receive(:generate).and_return(urn)
allow(Urn).to receive(:next).and_return(urn)
end

let(:urn) { "SOMEURN" }
Expand Down

0 comments on commit 4218f51

Please sign in to comment.