scalaanorm

Object construction with validation in Scala, using that in an Anorm parser


I have a simple case class Amount as below

case class Amount(value: Long, currency: Currency)

And an accompanying object to convert a string currency code into a Currency object

object Amount {
  private val log = Logger(getClass)
  def apply(value: Long, currencyCode: String) : Amount = {
    try {
      Amount(value, Currency.getInstance(currencyCode))
    } catch {
      case e: Exception =>
        log.error(s"Invalid currency code [$currencyCode]")
        throw new Exception(s"Invalid currency code [$currencyCode]")
    }
  }
}

Invocation :

val amount : Amount = Amount(1234, "USD")

When I read some data from the database, I have a custom parser such as

implicit val amountParser = Macro.parser[Amount]("value", "currencyCode")

However, the compiler complains

scala.ScalaReflectionException: value apply encapsulates multiple overloaded alternatives and cannot be treated as a method. Consider invoking `<offending symbol>.asTerm.alternatives` and manually picking the required method
[error]     at scala.reflect.api.Symbols$SymbolApi$class.asMethod(Symbols.scala:228)
[error]     at scala.reflect.internal.Symbols$SymbolContextApiImpl.asMethod(Symbols.scala:84)
[error]     at anorm.Macro$.parserImpl(Macro.scala:70)
[error]     at anorm.Macro$.namedParserImpl_(Macro.scala:25)
[error]     implicit val amountParser = Macro.parser[Amount]("value", "currencyCode")

How do I make this work ?

UPDATE

After understanding the response from @MikeAllen, I decided to leave the case class Amount and the object Amount as is, instead I wrote a custom parser for the Amount as below

    implicit private val amountParser = for {
        value <- long("value")
        currencyCode <- str("currency_code")
      } yield { 
           Amount(value, currencyCode) 
      }

Solution

  • The Scala compiler will automatically generate an Amount.apply factory method for creating case class instances, which is why you're getting this error - because you have multiple Amount.apply methods. One of these takes arguments of type (Long, Currency) and the other takes arguments of type (Long, String). The error message suggests that you need to select one of these from the overloaded alternatives reported through reflection.

    Alternatively, your case class and companion might be reworked as follows:

    final case class Amount(value: Long, currencyCode: String) {
    
      /** Currency. Will create an exception on instantiation if code is invalid. */
      val currency: Currency = {
        try {
          Currency.getInstance(currencyCode)
        }
        catch {
          case e: Exception =>
            Amount.log.error(s"Invalid currency code [$currencyCode]")
            throw new Exception(s"Invalid currency code [$currencyCode]")
        }
      }
    }
    
    object Amount {
      private val log = Logger(getClass)
    }
    

    This is not quite as elegant, admittedly, as you now have a field, currency, that isn't one of the case class's parameters and that isn't available for pattern matching, while also carrying around the string form.

    A better solution would be to keep your original case class and convert the currency code field from a String into a Currency, before creating the Amount instance, as part of the parser:

    val amountMapping = {
      get[Long]("value") ~ get[String]("currencyCode") map {
        case value ~ currencyCode => {
          val currency = {
            try {
              Currency.getInstance(currencyCode)
            }
            catch {
              case e: Exception =>
                Amount.log.error(s"Invalid currency code [$currencyCode]")
                throw new Exception(s"Invalid currency code [$currencyCode]")
            }
          }
          Amount(value, currency)
        }
      }
    }
    

    You can then use this to parse rows, for example with:

    def amounts(): List[Amount] = SQL("select * from amounts").as(amountMapping *)