Skip to content

Commit

Permalink
common: Add LanguageFields & OptLanguageFields types
Browse files Browse the repository at this point in the history
Hopefully these types will be used with all stored language fields in
the future.
  • Loading branch information
jnatten committed Jan 15, 2025
1 parent 944d064 commit 4889725
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Part of NDLA backend.common.main
* Copyright (C) 2025 NDLA
*
* See LICENSE
*
*/

package no.ndla.common.model.domain.language

import io.circe.*
import io.circe.syntax.EncoderOps
import no.ndla.language.Language
import no.ndla.language.model.{BaseWithLanguageAndValue, WithLanguageAndValue}

case class LanguageFields[T: Encoder: Decoder](internal: Map[String, T]) {
def getWithLanguageFields: Seq[WithLanguageAndValue[T]] = internal.map { case (language, value) =>
BaseWithLanguageAndValue(language, value)
}.toSeq
def get(language: String): Option[WithLanguageAndValue[T]] =
internal.get(language).map(BaseWithLanguageAndValue(language, _))
def findByLanguageOrBestEffort(language: String): Option[WithLanguageAndValue[T]] =
Language.findByLanguageOrBestEffort(getWithLanguageFields, language)
}

object LanguageFields {
def empty[T: Encoder: Decoder]: LanguageFields[T] = LanguageFields(Map.empty)
def fromFields[T](
fields: Seq[WithLanguageAndValue[T]]
)(implicit encoder: Encoder[T], decoder: Decoder[T]): LanguageFields[T] = {
val underlyingMap = fields.map(f => f.language -> f.value).toMap
LanguageFields(underlyingMap)
}

implicit def encoder[T: Encoder]: Encoder[LanguageFields[T]] = Encoder.instance { lf => lf.internal.asJson }
implicit def decoder[T: Decoder: Encoder]: Decoder[LanguageFields[T]] = Decoder.instance { json =>
json.as[Map[String, T]].map { m => LanguageFields(m) }

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Part of NDLA backend.common.main
* Copyright (C) 2025 NDLA
*
* See LICENSE
*
*/

package no.ndla.common.model.domain.language

import io.circe.syntax.EncoderOps
import io.circe.{Decoder, Encoder, Json}
import no.ndla.common.model.domain.language.OptionalLanguageValue.{NotWantedKey, NotWantedKeyT}
import no.ndla.language.model.WithLanguageAndValue

case class OptLanguageFields[T: Encoder: Decoder](
internal: Map[String, Either[NotWantedKeyT, Option[T]]]
) {
def get(language: String): Option[OptionalLanguageValue[T]] = {
val res = internal.get(language)
res match {
case None => None
case Some(Right(Some(value))) => Some(Exists(value))
case Some(Right(None)) => None
case Some(Left(_)) => Some(NotWanted())
}
}

def withUnwanted(language: String): OptLanguageFields[T] = {
val updated: Map[String, Either[NotWantedKeyT, Option[T]]] = internal.updated(language, Left(NotWantedKey))
OptLanguageFields(updated)
}
}

object OptLanguageFields {

def fromFields[T](
fields: Seq[WithLanguageAndValue[T]]
)(implicit encoder: Encoder[T], decoder: Decoder[T]): OptLanguageFields[T] = {
val underlyingMap = fields.map(f => f.language -> Right(Some(f.value))).toMap
OptLanguageFields(underlyingMap)
}

implicit def eitherEncoder[T](implicit e: Encoder[T]): Encoder[Either[NotWantedKeyT, Option[T]]] = Encoder.instance {
case Right(value) => value.asJson
case Left(_) => Json.obj(NotWantedKey -> Json.True)
}

implicit def eitherDecoder[T](implicit d: Decoder[T]): Decoder[Either[NotWantedKeyT, Option[T]]] = Decoder.instance {
cursor =>
val x = cursor.downField(NotWantedKey)
val notWantedField = x.as[Option[Boolean]]
notWantedField match {
case Right(Some(true)) =>
Right(Left(NotWantedKey))
case _ =>
cursor.as[Option[T]].map(Right(_))
}
}

implicit def encoder[T: Encoder]: Encoder[OptLanguageFields[T]] = Encoder.instance { lf =>
lf.internal.asJson
}

implicit def decoder[T: Decoder: Encoder]: Decoder[OptLanguageFields[T]] = Decoder.instance { json =>
json.as[Map[String, Either[NotWantedKeyT, Option[T]]]].map { m =>
OptLanguageFields(m)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Part of NDLA common
* Copyright (C) 2025 NDLA
*
* See LICENSE
*
*/

package no.ndla.common.model.domain.language

import io.circe.syntax.EncoderOps
import io.circe.{Decoder, Encoder, HCursor, Json}

sealed trait OptionalLanguageValue[T]
case class Exists[T: Encoder: Decoder](value: T) extends OptionalLanguageValue[T]
case class NotWanted[T]() extends OptionalLanguageValue[T]

object OptionalLanguageValue {
type NotWantedKeyT = "__notwanted__"
final val NotWantedKey = "__notwanted__"
implicit def encoder[T](implicit valueEncoder: Encoder[T]): Encoder[OptionalLanguageValue[T]] = Encoder.instance {
case Exists(value) => Json.obj("value" -> value.asJson)
case NotWanted() => Json.obj(NotWantedKey -> Json.True)
}

implicit def decoder[T: Encoder: Decoder]: Decoder[OptionalLanguageValue[T]] =
(c: HCursor) => {
c.downField(NotWantedKey).as[Option[Boolean]].flatMap {
case Some(true) => Right(NotWanted())
case _ =>
val field = c.downField("value")
val parsed = field.as[T]
parsed.map(value => Exists(value))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Part of NDLA common
* Copyright (C) 2025 NDLA
*
* See LICENSE
*
*/

package no.ndla.common.model.domain

import no.ndla.common.CirceUtil
import no.ndla.common.model.domain.language.*
import no.ndla.language.model.BaseWithLanguageAndValue
import no.ndla.scalatestsuite.UnitTestSuite

class LanguageFieldsTest extends UnitTestSuite {

test("That language fields serialize and deserialize as expected") {
import io.circe.syntax.*
val fields = Seq(
BaseWithLanguageAndValue("nb", "bokmål"),
BaseWithLanguageAndValue("nn", "nynorsk"),
BaseWithLanguageAndValue("en", "english")
)

val languageFields = LanguageFields.fromFields(fields)
val jsonString = languageFields.asJson.noSpaces
val result = CirceUtil.unsafeParseAs[LanguageFields[String]](jsonString)

result should be(languageFields)
}

test("That language fields are found by language or best effort according to language priority") {
val fields = Seq(
BaseWithLanguageAndValue("nb", "bokmål"),
BaseWithLanguageAndValue("nn", "nynorsk"),
BaseWithLanguageAndValue("en", "english")
)

val languageFields = LanguageFields.fromFields(fields)

languageFields.findByLanguageOrBestEffort("nb") should be(Some(BaseWithLanguageAndValue("nb", "bokmål")))
languageFields.findByLanguageOrBestEffort("sma") should be(Some(BaseWithLanguageAndValue("nb", "bokmål")))
languageFields.findByLanguageOrBestEffort("nn") should be(Some(BaseWithLanguageAndValue("nn", "nynorsk")))
}

test("That the LanguageFields type is able to differentiate between a missing and not needed field") {

val fields = Seq(
BaseWithLanguageAndValue[OptionalLanguageValue[String]]("nb", Exists("bokmål")),
BaseWithLanguageAndValue[OptionalLanguageValue[String]]("nn", NotWanted())
)

val languageFields = LanguageFields.fromFields(fields)
val jsonString = CirceUtil.toJsonString(languageFields)

val result = CirceUtil.unsafeParseAs[LanguageFields[OptionalLanguageValue[String]]](jsonString)
result should be(languageFields)

result.get("nb") should be(Some(BaseWithLanguageAndValue("nb", Exists("bokmål"))))
result.get("nn") should be(Some(BaseWithLanguageAndValue("nn", NotWanted())))
}

test("That the OptLanguageFields type is able to differentiate between a missing and not needed field") {

val fields = Seq(
BaseWithLanguageAndValue[String]("nb", "bokmål")
)

val languageFields = OptLanguageFields.fromFields(fields).withUnwanted("en")
val jsonString = CirceUtil.toJsonString(languageFields)

val result = CirceUtil.unsafeParseAs[OptLanguageFields[String]](jsonString)
result should be(languageFields)

result.get("nb") should be(Some(Exists("bokmål")))
result.get("nn") should be(None)
result.get("en") should be(Some(NotWanted()))
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package no.ndla.language.model

trait LanguageField[T] extends WithLanguage {
trait LanguageField[T] extends WithLanguageAndValue[T] {
def value: T
def isEmpty: Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Part of NDLA backend.language.main
* Copyright (C) 2025 NDLA
*
* See LICENSE
*
*/

package no.ndla.language.model

trait WithLanguageAndValue[T] extends WithLanguage {
def language: String
def value: T
}

case class BaseWithLanguageAndValue[T](
language: String,
value: T
) extends WithLanguageAndValue[T]
3 changes: 2 additions & 1 deletion project/languagelib.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ object languagelib extends Module {
lazy val dependencies: Seq[ModuleID] = withLogging(
Seq(
"org.scalatest" %% "scalatest" % ScalaTestV % "test"
)
),
tapirHttp4sCirce
)

override lazy val settings: Seq[Def.Setting[?]] = Seq(
Expand Down

0 comments on commit 4889725

Please sign in to comment.