CustomerFwd uses ValueSemantics, translates old XMPP-SIP URI

Stephen Paul Weber created

More of the original data is kept now, so this object could be used for putting
to persistence as well as for loading from it.

Change summary

lib/bwmsgsv2_repo.rb      |  2 
lib/customer_fwd.rb       | 65 ++++++++++++++++++++++------------------
test/test_customer_fwd.rb | 51 ++++++++++++++++++++++++++++++++
web.rb                    | 19 +++++------
4 files changed, 96 insertions(+), 41 deletions(-)

Detailed changes

lib/bwmsgsv2_repo.rb 🔗

@@ -20,7 +20,7 @@ class Bwmsgsv2Repo
 		fetch_raw(sgx.from_jid).then do |(((ogm_url, fwd_time, fwd), trans_d), reg)|
 			sgx.with({
 				ogm_url: ogm_url,
-				fwd: CustomerFwd.for(fwd, fwd_time),
+				fwd: CustomerFwd.for(uri: fwd, timeout: fwd_time),
 				transcription_enabled: !trans_d,
 				registered?: reg
 			}.compact)

lib/customer_fwd.rb 🔗

@@ -1,17 +1,25 @@
 # frozen_string_literal: true
 
+require "value_semantics/monkey_patched"
 require "uri"
 
 class CustomerFwd
-	def self.for(uri, timeout)
+	def self.for(uri:, timeout:)
 		timeout = Timeout.new(timeout)
-		return if !uri || timeout.zero?
+		return None.new(uri: uri, timeout: timeout) if !uri || timeout.zero?
+		if uri =~ /\Asip:(.*)@sip.cheogram.com\Z/
+			uri = "xmpp:#{$1.gsub(/%([0-9A-F]{2})/i) { $1.to_i(16).chr }}"
+		end
 		URIS.fetch(uri.split(":", 2).first.to_sym) {
 			raise "Unknown forward URI: #{uri}"
-		}.new(uri, timeout)
+		}.new(uri: uri, timeout: timeout)
 	end
 
 	class Timeout
+		def self.new(s)
+			s.is_a?(self) ? s : super
+		end
+
 		def initialize(s)
 			@timeout = s.nil? || s.to_i.negative? ? 300 : s.to_i
 		end
@@ -25,54 +33,51 @@ class CustomerFwd
 		end
 	end
 
-	def create_call_request
+	value_semantics do
+		uri Either(String, NilClass)
+		# rubocop:disable Style/RedundantSelf
+		self.timeout Timeout, coerce: Timeout.method(:new)
+		# rubocop:enable Style/RedundantSelf
+	end
+
+	def with(new_attrs)
+		CustomerFwd.for(to_h.merge(new_attrs))
+	end
+
+	def create_call(account)
 		request = Bandwidth::ApiCreateCallRequest.new.tap do |cc|
 			cc.to = to
 			cc.call_timeout = timeout.to_i
+			yield cc if block_given?
 		end
-		yield request if block_given?
-		request
+		BANDWIDTH_VOICE.create_call(account, body: request).data.call_id
 	end
 
 	class Tel < CustomerFwd
-		attr_reader :timeout
-
-		def initialize(uri, timeout)
-			@tel = uri.sub(/^tel:/, "")
-			@timeout = timeout
-		end
-
 		def to
-			@tel
+			uri.sub(/^tel:/, "")
 		end
 	end
 
 	class SIP < CustomerFwd
-		attr_reader :timeout
-
-		def initialize(uri, timeout)
-			@uri = uri
-			@timeout = timeout
-		end
-
 		def to
-			@uri
+			uri
 		end
 	end
 
 	class XMPP < CustomerFwd
-		attr_reader :timeout
-
-		def initialize(uri, timeout)
-			@jid = uri.sub(/^xmpp:/, "")
-			@timeout = timeout
-		end
-
 		def to
-			"sip:#{ERB::Util.url_encode(@jid)}@sip.cheogram.com"
+			jid = uri.sub(/^xmpp:/, "")
+			"sip:#{ERB::Util.url_encode(jid)}@sip.cheogram.com"
 		end
 	end
 
+	class None < CustomerFwd
+		def create_call; end
+
+		def to; end
+	end
+
 	URIS = {
 		tel: Tel,
 		sip: SIP,

test/test_customer_fwd.rb 🔗

@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "customer_fwd"
+
+class Rantly
+	def jid
+		v = Blather::JID.new(Blather::JID.new(string, string).stripped.to_s)
+		guard !v.to_s.to_s.empty?
+		v
+	end
+end
+
+class CustomerFwdTest < Minitest::Test
+	property(:for_xmpp) { jid }
+	def for_xmpp(jid)
+		sip = "sip:#{ERB::Util.url_encode(jid.to_s)}@sip.cheogram.com"
+		fwd = CustomerFwd.for(uri: "xmpp:#{jid}", timeout: 10)
+		assert_kind_of CustomerFwd::XMPP, fwd
+		assert_equal sip, fwd.to
+	end
+
+	property(:for_xmpp_sip) { jid }
+	def for_xmpp_sip(jid)
+		sip = "sip:#{ERB::Util.url_encode(jid.to_s)}@sip.cheogram.com"
+		fwd = CustomerFwd.for(uri: sip, timeout: 10)
+		assert_kind_of CustomerFwd::XMPP, fwd
+		assert_equal sip, fwd.to
+	end
+
+	property(:for_tel) { "+#{string(:digit)}" }
+	def for_tel(tel)
+		fwd = CustomerFwd.for(uri: "tel:#{tel}", timeout: 10)
+		assert_kind_of CustomerFwd::Tel, fwd
+		assert_equal tel, fwd.to
+	end
+
+	property(:for_sip) { "#{string(:alnum)}@#{string(:alnum)}.example.com" }
+	def for_sip(sip)
+		fwd = CustomerFwd.for(uri: "sip:#{sip}", timeout: 10)
+		assert_kind_of CustomerFwd::SIP, fwd
+		assert_equal "sip:#{sip}", fwd.to
+	end
+
+	property(:for_bogus) { string }
+	def for_bogus(bogus)
+		assert_raises(RuntimeError) do
+			CustomerFwd.for(uri: "bogus:#{bogus}", timeout: 10)
+		end
+	end
+end

web.rb 🔗

@@ -257,17 +257,16 @@ class Web < Roda
 					CustomerRepo.new(
 						sgx_repo: Bwmsgsv2Repo.new
 					).find_by_tel(params["to"]).then(&:fwd).then do |fwd|
-						if fwd
+						call = fwd.create_call(CONFIG[:creds][:account]) do |cc|
 							true_inbound_call[pseudo_call_id] = params["callId"]
-							outbound_transfers[pseudo_call_id] = BANDWIDTH_VOICE.create_call(
-								CONFIG[:creds][:account],
-								body: fwd.create_call_request do |cc|
-									cc.from = params["from"]
-									cc.application_id = params["applicationId"]
-									cc.answer_url = url inbound_calls_path(nil)
-									cc.disconnect_url = url inbound_calls_path(:transfer_complete)
-								end
-							).data.call_id
+							cc.from = params["from"]
+							cc.application_id = params["applicationId"]
+							cc.answer_url = url inbound_calls_path(nil)
+							cc.disconnect_url = url inbound_calls_path(:transfer_complete)
+						end
+
+						if call
+							outbound_transfers[pseudo_call_id] = call
 							render :pause, locals: { duration: 300 }
 						else
 							render :redirect, locals: { to: inbound_calls_path(:voicemail) }