The point of comparison here is a simple function that looks up a string key in a nested map/dictionary (JSON data from the payload/claims of a JWT token) and returns the given key if it's there and None/nil otherwise.
Original function:
def verifyPartner(partner: String, payload: Map[String, java.lang.Object]): Either[String, String] = {
val appMetadata = payload.getOrElse("app_metadata", "")
val partnerOpt = withMap(appMetadata) { metadata =>
metadata.getOrElse("partners", "") match {
case l: java.util.ArrayList[Object] =>
val z: List[Option[String]] = l.toList.map {
case x: java.util.HashMap[String, String] =>
val y: Option[String] = x.toMap.get("id")
y.map(_.toString)
case _ => None
}
Some(z.flatten)
case _ => None
}
}
partnerOpt match {
case Some(partners) => Either.cond(partners.contains(partner), partner, "")
case None => Left("")
}
}
Naive rewrite of function mimicking approach in dynamic languages (non-idiomatic):
def verifyPartner(partner: String, payload: Map[String, java.lang.Object]): Either[String, String] = {
val partners = payload.getOrElse("app-metadata", Map()).asInstanceOf[Map[String, Object]].
getOrElse("partners", Array()).asInstanceOf[Array[Map[String, Object]]]
partners.find(p => p.getOrElse("id", None) == partner) match {
case Some(partnerObject) => Right(partner)
case None => Left("")
}
}
Test:
val partners = Array(Map("id" -> "foo"), Map("id" -> "bar"))
val payload = Map("app-metadata" -> Map("partners" -> partners))
verifyPartner("foo", payload) // => Right(foo)
verifyPartner("foobar", payload) // => Left()
With the help of Adam Bergmark (github.com/bergmark) I was able to refactor the Scala code by introducing proper types. First we installed the circe JSON library by adding it as a dependency in build.sbt:
val circeVersion = "0.8.0"
val dependencies = Seq(
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-derivation" % "0.8.0-M2",
"io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion
)
We introduced a data type for the JSON payload:
@JsonCodec case class JWTPartner(id: String)
@JsonCodec case class AppMetadata(partners: Array[JWTPartner], developer: Option[Boolean])
@JsonCodec case class JWTPayload(app_metadata: AppMetadata)
JSON decoding now looks like this:
val payload = parser.decode[JWTPayload](stringPayload)
With this in place the verifyPartner function is quite clean:
def verifyPartner(partnerId: String, payload: JWTPayload): Either[String, JWTPartner] = {
payload.app_metadata.partners.find(p => p.id == partnerId) match {
case Some(partner) => Right(partner)
case None => Left("Could not find partner")
}
}
Function:
(defn verify-partner [partner-id payload]
(let [payload-partners (get-in payload [:app-metadata :partners] [])
payload-ids (set (map :id payload-partners))]
(get payload-ids partner-id)))
Test:
(def payload {:app-metadata {:partners [{:id "foo"} {:id "bar"}]}})
(verify-partner "foo" payload) # => "foo"
(verify-partner "fooasdf" payload) # => nil
(verify-partner "fooasdf" {}) # => nil
(verify-partner "fooasdf" nil) # => nil
Function:
def verifyPartner(partner_id, payload):
payload_partners = (payload or {}).get("app-metadata", {}).get("partners", [])
payload_ids = [p["id"] for p in payload_partners]
return partner_id if partner_id in payload_ids else None
Test:
payload = {"app-metadata": {"partners": [{"id": "foo"}, {"id": "bar"}]}}
verifyPartner("foo", payload) # => "foo"
verifyPartner("foob", payload) # => None
verifyPartner("foob", {}) # => "foo"
verifyPartner("foob", None) # => "foo"
Function:
function verifyPartner(partnerId, payload) {
const payloadMetadata = (payload || {})['app-metadata'] || {}
const payloadPartners = payloadMetadata['partners'] || []
const payloadIds = payloadPartners.map(p => p['id'])
return payloadIds.includes(partnerId) ? partnerId : undefined
}
Test:
const payload = {'app-metadata': {partners: [{id: 'foo'}, {id: 'bar'}]}}
verifyPartner('foo', payload) // => 'foo'
verifyPartner('foos', payload) // => undefined
verifyPartner('foos', {}) // => undefined
verifyPartner('foos', null) // => undefined