I have a scala (2.10.4) application where email addresses are passed around a lot, and I'd like to implement an abstraction that's called at IO to "sanitize" already validated email addresses.
Using scala.Proxy
is almost what I want, but I'm running into issues with asymmetric equality.
class SanitizedEmailAddress(s: String) extends Proxy with Ordered[SanitizedEmailAddress] { val self: String = s.toLowerCase.trim def compare(that: SanitizedEmailAddress) = self compareTo that.self } object SanitizedEmailAddress { def apply(s: String) = new SanitizedEmailAddress(s) implicit def sanitize(s: String): SanitizedEmailAddress = new SanitizedEmailAddress(s) implicit def underlying(e: SanitizedEmailAddress): String = e.self }
I'd like to have
val sanitizedEmail = SanitizedEmailAddress("Blah@Blah.com") val expected = "blah@blah.com" assert(sanitizedEmail == expected) // => true assert(expected == sanitizedEmail) // => true, but this currently returns false :(
Or something with similar functionality. Is there any non-cumbersome way to do this?
assert(sanitizedEmail.self == expected) // => true (but pretty bad, and someone will forget) // can have a custom equality method and use the "pimp-my-lib" pattern on strings, but then we have to remember to use that method every time
Thanks for your help.
3 Answers
Answers 1
I don't think it's possible, sorry.
I'm not sure it's quite right to want that, either. If a String
is truly equal to a SanitizedEmailAddress
, then what does the SanitizedEmailAddress
wrapper actually denote?
I think it would be more consistent to have String
not comparable to a SanitizedEmailAddress
, and require users to "sanitize" input before comparing it.
Answers 2
You want to use a marker trait to denote strings that conform.
A function that takes an Email
string knows that the string is correct.
If an ordinary string compares equal to an Email
string, then it must also be correct.
package object email { type Tagged[U] = { type Tag = U } type @@[T, U] = T with Tagged[U] def smartly[A](s: String): String @@ A = Email(s).asInstanceOf[String @@ A] } package email { trait Email extends Any object Email { def apply(s: String) = s.toLowerCase.trim } object Test extends App { def f(ok: String @@ Email) = { assert(ok.forall(c => !c.isLetter || c.isLower)) } val x = smartly[Email]("Joe@ACME.com") println(x) f(x) assert("joe@acme.com" == x) /* f("Junk@Yahoo.com") // DNC email.scala:22: error: type mismatch; found : String("Junk@Yahoo.com") required: email.@@[String,email.Email] (which expands to) String with AnyRef{type Tag = email.Email} f("Junk@Yahoo.com") ^ one error found */ } }
Answers 3
What about keeping all email type as String and pimp the 'sanitized' info implicitly:
object SanitizedEmailAddress { def apply(s: String): String = synchronized { val verified = s.toLowerCase.trim sanitized.update(verified, true) verified } def isSanitized(s: String): Boolean = synchronized { sanitized.contains(s) } private val sanitized = scala.collection.mutable.WeakHashMap.empty[String, Boolean] } implicit class emailOps(val email: String) extends AnyVal { def isSanitized: Boolean = SanitizedEmailAddress.isSanitized(email) }
And now:
val sanitizedEmail = SanitizedEmailAddress("Blah@Blah.com") val expected = "blah@blah.com" assert(sanitizedEmail == expected) // => true assert(expected == sanitizedEmail) // => true assert(sanitizedEmail.isSanitized == true) // => true assert("blah@blah.com".isSanitized == true) // => true assert("Blah@Blah.com".isSanitized == false) // => true
0 comments:
Post a Comment