0

I'm using spray with json4s, and I've got the implementation below to handle put requests for updating objects... My problem with it, is that I first extract an instance of SomeObject from the json, but being a RESTful api, I want the ID to be specified in the URL. So then I must somehow create another instance of SomeObject that is indexed with the ID... to do this, I'm using a constructor like SomeObject(id: Long, obj: SomeObject). It works well enough, but the implementation is ugly and it feels inefficient. What can I do so I can somehow stick the ID in there so that I'm only creating one instance of SomeObject?

class ApplicationRouter extends BaseRouter {
  val routes =
    pathPrefix("some-path") {
      path("destination-resource" \ IntNumber) { id =>
        entity(as[JObject]) { rawData =>
          val extractedObject = rawData.camelizeKeys.extract[SomeObject]
          val extractedObjectWithId = SomeObject(id, extractedObject)
          handleRequest(extractedObjectWithId)
        }
      }
    }
}

case class SomeObject(id: Long, data: String, someValue: Double, someDate: DateTime) {
  def this(data: String, someValue: Double, someDate: DateTime) = this(0, data, someValue, someDate)
  def this(id: Long, obj: SomeObject) = this(id, obj.data, obj.someValue, obj.someDate)
}
JBarber
  • 212
  • 1
  • 7

3 Answers3

1

I figured out a solution to this after digging around through the docs for awhile:

class ApplicationRouter extends BaseRouter {
  val routes =
    pathPrefix("some-path") {
      path("destination-resource" \ IntNumber) { id =>
        entity(as[JObject]) { rawData =>
          val extractedObject = rawData.camelizeKeys.merge { 
              ("id", id)
          }.extract[SomeObject]
          handleRequest(extractedObject)
        }
      }
    }
}
JBarber
  • 212
  • 1
  • 7
0

Since id field is not set on all instances, it means it is optional so use Option type to indicate it. Define your case class with id: Option[Long] field. This make json parser to skip id field when it is absent but allow you to assign a value when you have.

case class SomeObject(id: Option[Long], data: String, someValue: Double, someDate: DateTime)

class ApplicationRouter extends BaseRouter {
  val routes =
    pathPrefix("some-path") {
      path("destination-resource" \ IntNumber) { id =>
        entity(as[JObject]) { rawData =>
          val extractedObject = rawData.camelizeKeys.extract[SomeObject]
          val extractedObjectWithId = extractedObject.copy(id = Some(id))
          handleRequest(extractedObjectWithId)
        }
      }
    }
}

And don't worry about performance impacts of creating new objects. It may affect performance much less than you thought. You should measure performance before improving it.

Mustafa Simav
  • 1,019
  • 5
  • 6
  • If I am not mistaken, the ID is not optional, but is not present in the JSON value, because it comes from a different source: The URL. In other words, making `id` optional would be semantically wrong. – Kulu Limpa Oct 21 '14 at 12:48
-1

I do not know about efficiency, but you can make your code "less ugly" by defining a SomeObjectBuilder, to which you extract your JSON value.

case class SomeObjectBuilder(data: String, someValue: Double, someDate: DateTime) {
  def setId(id: Long) = SomeObject(id, data, someValue, someDate)
}

case class SomeObject(id: Long, data: String, someValue: Double, someDate: DateTime) 

With the extraction:

class ApplicationRouter extends BaseRouter {
  val routes =
    pathPrefix("some-path") {
      path("destination-resource" \ IntNumber) { id =>
        entity(as[JObject]) { rawData =>
          val extractedObject = rawData.camelizeKeys.extract[SomeObjectBuilder]
          val extractedObjectWithId = extractedObject.setId(id)
          handleRequest(extractedObjectWithId)
        }
      }
    }
}

This way, you are not using a default id set to zero, which is, if I understand correctly, never correct. The only reason you're setting it to zero is that the value is not known by the extractor, so, using the builder, you make a partial instantiation explicit.

Kulu Limpa
  • 3,501
  • 1
  • 21
  • 31
  • I can't really think of a better solution than this (though I wouldn't use setId for the function name there), but it still doesn't feel right. I was hoping there was some way for scala to extract a case class in to a tuple, and then for scala to pass the parameters of that tuple as parameters of the function. so it would come out like `SomeObject(id, someObject.toTuple.asParams)` Then scala would use a compiler macro to extract the parameters so it actually reads as something like `SomeObject(id, someObject.toTuple._1, someObject.toTuple._2, someObject.toTuple._3)` oh well. – JBarber Oct 18 '14 at 18:54
  • Since your ID is known prior to extracting, you could also define a custom deserializer. This is done by extending `CustomSerializer`, which takes a partial function describing deserialization. The "tuple" you mention sounds much like providing the `id` as a closure to this partial function. However, in my opinion, implementing a custom serializer makes the code more complex than setting the `id` manually. – Kulu Limpa Oct 19 '14 at 03:16