-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
common: Add
LanguageFields
& OptLanguageFields
types
Hopefully these types will be used with all stored language fields in the future.
- Loading branch information
Showing
7 changed files
with
249 additions
and
2 deletions.
There are no files selected for viewing
40 changes: 40 additions & 0 deletions
40
common/src/main/scala/no/ndla/common/model/domain/language/LanguageFields.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } | ||
|
||
} | ||
} |
70 changes: 70 additions & 0 deletions
70
common/src/main/scala/no/ndla/common/model/domain/language/OptLanguageFields.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
common/src/main/scala/no/ndla/common/model/domain/language/OptionalLanguageValue.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} | ||
} |
81 changes: 81 additions & 0 deletions
81
common/src/test/scala/no/ndla/common/model/domain/LanguageFieldsTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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())) | ||
} | ||
|
||
} |
2 changes: 1 addition & 1 deletion
2
language/src/main/scala/no/ndla/language/model/LanguageField.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
19 changes: 19 additions & 0 deletions
19
language/src/main/scala/no/ndla/language/model/WithLanguageAndValue.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters