Custom serialization

109 views
Skip to first unread message

Leon Radley

unread,
Nov 6, 2012, 7:19:20 AM11/6/12
to scala...@googlegroups.com
Hey!

I've got this idea of how to solve localization in a play app.

I would have a class that would simply be a wrapper around a Map[String, String]
where the map key would be the Locale such as "en-US"

and by implicitly passing the play.api.i18n.Lang I could get a value from the map.

Since alot of my model fields would be translatable, it would be a pain having to duplicate the logic for getting the values from the map.
So my though was if I could get salat to deserialize a Map[String, String] into my custom class LocalizedString.

Would it be possible to create a custom serializer, deserializer for LocalizedString?

Leon Radley

unread,
Nov 7, 2012, 2:35:56 AM11/7/12
to scala...@googlegroups.com
Some more info after yesterdays trial and errors...

I've come up with a model that looks like this

package models

import collection.immutable.Map
import collection.immutable.MapProxy
import play.api.i18n.Lang


/**
 * Used to store a map of lang code -> translation
 * {{{
 *   case class MyModel(
 *     name: LocalizedString,
 *     description: LocalizedString
 *   )
 *
 *   val myModel = MyModel(
 *    name = LocalizedString("the name in my current locale"),
 *    description = LocalizedString("en" -> "An English description", "sv" -> "En Svensk beskrivning")
 *   )
 *
 *   myModel.name = the name in my current locale
 *
 *   myModel.name("default name") = if the LocalizedString doesn't contain the Lang it will use "default name"
 *
 *   // We could also have the default message in a lang file
 *   import play.api.i18n.Messages
 *   myModel.name(Messages("name.default"))
 *   
 *   // And since the LocalizedString is also a map we have all the goodies from the Map API
 *   myModel.description.map((code, desc) => println("Description in: " + code + ": " + desc))
 *   
 * }}}
 * @param self
 */
class LocalizedString(val self: Map[String, String]) extends MapProxy[String, String] {

  // Get localized string or fallback to if there is a string at all or fallback even further to an the default argument specified.
  def apply(default: String = "")(implicit lang: Lang): String = self.get(lang.code).getOrElse(self.headOption.map(_._2).getOrElse(default))

  // Get localized string or fallback to if there is a string at all and fallback even further to an empty string.
  def toString()(implicit lang: Lang): String = apply()(lang)
}

object LocalizedString {

  // Allow implicit conversions
  def apply(foo: Map[String, String]) = new LocalizedString(foo)
  implicit def map2LocalizedString(map: Map[String, String]): LocalizedString = LocalizedString(map)
  implicit def localizedString2Map(localizedString: LocalizedString): Map[String, String] = localizedString.self

  // Factory methods
  def apply(): LocalizedString = new LocalizedString(Map.empty)
def apply(value: String)(implicit lang: Lang): LocalizedString = LocalizedString(lang.code -> value)
def apply(keys: (String, String)*): LocalizedString = new LocalizedString(Map(keys:_*))
}

The problem is that salat doesn't understand that LocalizedString is a sub type of Map and thus doesn't deserialize it.

It does serialize it though.

"name" : {
  "en" : "English string",
  "sv" : "Swedish string"
}


The error I'm getting is

---------- CONSTRUCTOR EXPECTS FOR PARAM [1] --------------
NAME:         name
TYPE:      models.LocalizedString
DEFAULT ARG   [Missing, but unnecessary because input value was supplied]
@Ignore      false
---------- CONSTRUCTOR INPUT ------------------------
TYPE: com.mongodb.BasicDBObject
VALUE:
{ "en" : "English string" , "sv" : "Swedish string"}
------------------------------------------------------------

So my question is, can i force salat to use my LocalizedString or get it to understand subtypes of Map?

rose katherine toomey

unread,
Nov 7, 2012, 3:37:39 AM11/7/12
to scala-salat
Hi Leon,

I saw your question yesterday but unfortunately I am trying to fix a chart for a demo in six hours.

Salat's collection support is explictly limited to those listed on the wiki.  It can't understand custom subtypes of map.  I could add probably add MapProxy support though....  I'd need certain methods to be implemented to avoid the infinite stacktraces that result from unimplemented methods in MapProxy (if you've ever hit one you'd know what I'm talking about)....

Collection support is fairly straightforward, so if you want to work backward from the collection support spec, you could probably figure out MapProxy support yourself.  The trick is, you'd have to take the path, get the class, walk the interfaces, and see if it is an instance of MapProxy.  I recently added BitSet support, which hits all the same places you'd need to go.
 
Alas, there isn't any support for custom transformers yet. 

Best,
Rose

Leon Radley

unread,
Nov 7, 2012, 5:56:45 AM11/7/12
to scala...@googlegroups.com
Since LocalizedString could be implicitly converted to and from a map, where would I import my conversions so that salat (the grater?) would use the implicit conversions?
or wouldn't that work?

Leon Radley

unread,
Nov 8, 2012, 10:06:18 AM11/8/12
to scala...@googlegroups.com
I've been looking the source code of salat, and have a question.

After looking at this this file
I can only deduct that salat uses class names to deserialize which is a big problem.

MapProxy implements Map which means that LocalizedString is a Map
But since we simply look if the class string ends with "Map" it won't detect the subtyping.

How can we improve this?

Is it possible to adopt the same functionality that Play uses for it's JSON lib.


It uses implicit vals to expose Reads and Writes.

Here are some blogposts on the subject

This could mean that to extend salat could be as simple as implementing a Read and a Write and exposing the implicit variables to the grater via the context.

What do you think?


On Wednesday, November 7, 2012 9:38:01 AM UTC+1, rktoomey wrote:

rose katherine toomey

unread,
Nov 8, 2012, 10:28:42 AM11/8/12
to scala-salat
Hi Leon,

Actually, Salat uses the class names from the pickled scala sigs.  So you'd need detailed understanding of what the TypeRefType for LocalizedString looks like, so that you could generically figure out where the details you need to actually deserialize each part correctly are located.

In the case of MapProxy, your issue would be that the existing map matcher is too greedy.

But now I've had a night of sleep, I think you can do this more simply - you could use this example to create a cursor where you take the raw DBObject, deserialize LocalizedString yourself, and then pass it onward to the grater for the top level object:

That should get things working for the time being.

I'll look at the Play docs.  I don't know if I'll take that approach though.  Implicits are great until your code reaches a point where manifests and implicits and view bounds all collide, making the unsugared syntax just miserable to work with (look at some of the stuff in SalatDAO).

Best,
Rose
Reply all
Reply to author
Forward
0 new messages