|
|
@@ -4,128 +4,312 @@ import UnitType._
|
|
|
import scala.util.{Try,Success,Failure}
|
|
|
import scala.concurrent.Future
|
|
|
import com.weEat.shared.exceptions._
|
|
|
-import gov.usda.nal.fdc.models.{SearchResultFood,FullFoodItem,FoodPortion}
|
|
|
-import gov.usda.nal.fdc.controllers.FoodController
|
|
|
-import gov.usda.nal.fdc.models.DataType._
|
|
|
import play.api.libs.json.Json
|
|
|
import com.weEat.shared.models.IdentifierHelper._
|
|
|
+import com.weEat.controllers.{FoodController,USDAController,UserController}
|
|
|
+import play.api.libs.json.{JsonConfiguration,JsonNaming}
|
|
|
+import gov.usda.nal.fdc.models.{FoodItem,SearchResultFood,FullFoodItem,FoodPortion}
|
|
|
+import gov.usda.nal.fdc.models.DataType._
|
|
|
|
|
|
sealed trait FoodNode {
|
|
|
- import FoodNode.NodeType._
|
|
|
+ import FoodNodeId.NodeType._
|
|
|
|
|
|
- def _id: Option[Identifier]
|
|
|
- def nodeType: NodeType
|
|
|
- def defaultUnit: UnitType = MASS
|
|
|
+ implicit val ctx = scala.concurrent.ExecutionContext.global
|
|
|
+
|
|
|
+ def name: String
|
|
|
+ def defaultUnitType: UnitType = MASS
|
|
|
def density: Option[Float] = None
|
|
|
def massPerUnit: Option[Float] = None
|
|
|
- def withId(id: Identifier): FoodNode
|
|
|
-}
|
|
|
-
|
|
|
-case class RecipeNode(
|
|
|
- val _id: Option[Identifier],
|
|
|
- val name: String,
|
|
|
- val ingredients: Seq[RecipeNode.Ingredient],
|
|
|
- val steps: Seq[String]
|
|
|
-) extends FoodNode {
|
|
|
- val nodeType = FoodNode.NodeType.RECIPE
|
|
|
+ def withId(id: Identifier, uid: Identifier): FoodNodeId
|
|
|
+ def nutrient(str: String): Future[Float]
|
|
|
+ def nodeType: NodeType
|
|
|
+ //def imageIds: Seq[Identifier]
|
|
|
|
|
|
- def withId(id: Identifier) = RecipeNode(Some(id), name, ingredients, steps)
|
|
|
+ //def image(idx: Int): APIRoute[Image] = FoodController.getImages(imageIds(idx))
|
|
|
}
|
|
|
|
|
|
-object RecipeNode {
|
|
|
+sealed trait FoodNodeId extends FoodNode {
|
|
|
+ val _id: Identifier
|
|
|
+ val uid: Identifier
|
|
|
|
|
|
- implicit val idFmt = com.weEat.shared.models.IdentifierHelper.fmt
|
|
|
+ def withId(id: Identifier): FoodNodeId = withId(id, uid)
|
|
|
|
|
|
- case class Ingredient(val id: Identifier, val amount: Float) {
|
|
|
- lazy val food: FoodNode = ???
|
|
|
-
|
|
|
- private def convert(
|
|
|
- amount: Double,
|
|
|
- typ: UnitType,
|
|
|
- to: MeasureUnit
|
|
|
- ): Try[Double] = (typ, to.typ) match {
|
|
|
- case (fTyp, tTyp) if fTyp == tTyp => Success(amount * to.conversionRatio)
|
|
|
- case (MASS, VOLUME) => food.density match {
|
|
|
- case Some(density) => convert(amount / density, VOLUME, to)
|
|
|
- case None => Failure(new IncompleteDataException("density"))
|
|
|
- }
|
|
|
- case (MASS, NUMBER) => food.massPerUnit match {
|
|
|
- case Some(massPerUnit) => convert(amount / massPerUnit, NUMBER, to)
|
|
|
- case None => Failure(new IncompleteDataException("mass/unit"))
|
|
|
- }
|
|
|
- case (VOLUME, _) => food.density match {
|
|
|
- case Some(density) => convert(amount * density, MASS, to)
|
|
|
- case None => Failure(new IncompleteDataException("density"))
|
|
|
- }
|
|
|
- case (NUMBER, _) => food.density match {
|
|
|
- case Some(massPerUnit) => convert(amount * massPerUnit, MASS, to)
|
|
|
- case None => Failure(new IncompleteDataException("mass/unit"))
|
|
|
- }
|
|
|
- }
|
|
|
+ lazy val user = UserController.get(uid.toString)()
|
|
|
+}
|
|
|
|
|
|
- def to(unit: MeasureUnit) = convert(amount, food.defaultUnit, unit)
|
|
|
+case class Ingredient(
|
|
|
+ val id: Ingredient.IngredientId,
|
|
|
+ val amount: Float,
|
|
|
+ val unit: MeasureUnit
|
|
|
+) {
|
|
|
+ implicit val ctx = scala.concurrent.ExecutionContext.global
|
|
|
+
|
|
|
+ lazy val food = id match {
|
|
|
+ case Ingredient.FoodNodeId(id) => FoodController.get(id.toString)()
|
|
|
+ case Ingredient.USDAId(id) =>
|
|
|
+ USDAController.getFood(id)().map(USDANodeNoId.fromFoodItem)
|
|
|
}
|
|
|
|
|
|
- object Ingredient {
|
|
|
- implicit val ingredFmt = Json.using[Json.WithDefaultValues].format[Ingredient]
|
|
|
+ def in(target: MeasureUnit) = food.transform {
|
|
|
+ case Success(f) => unit.convert(f, amount, target)
|
|
|
+ case err: Failure[_] => err
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
+object Ingredient {
|
|
|
import play.api.libs.json.{JsonConfiguration,JsonNaming}
|
|
|
+ import com.weEat.shared.models.{FoodNodeId => FoodNodeWithId}
|
|
|
+
|
|
|
+ sealed trait IngredientId {
|
|
|
+ def typ: String
|
|
|
+ }
|
|
|
+ case class FoodNodeId(id: Identifier) extends IngredientId {
|
|
|
+ def typ = "food"
|
|
|
+ }
|
|
|
+ case class USDAId(id: Long) extends IngredientId {
|
|
|
+ def typ = "usda"
|
|
|
+ }
|
|
|
+
|
|
|
+ implicit def foodNodeToId(food: FoodNodeWithId) = FoodNodeId(food._id)
|
|
|
+ implicit def foodItemToId(food: FoodItem) = USDAId(food.fdcId)
|
|
|
|
|
|
implicit val config = JsonConfiguration(JsonNaming.SnakeCase)
|
|
|
- implicit val fmt = Json.using[Json.WithDefaultValues].format[RecipeNode]
|
|
|
+ implicit val identifierFmt = com.weEat.shared.models.IdentifierHelper.fmt
|
|
|
+ implicit val fnIdFmt = Json.using[Json.WithDefaultValues].format[FoodNodeId]
|
|
|
+ implicit val usdaIdFmt = Json.using[Json.WithDefaultValues].format[USDAId]
|
|
|
+ implicit val idFmt = Json.using[Json.WithDefaultValues].format[IngredientId]
|
|
|
+ implicit val fmt = Json.using[Json.WithDefaultValues].format[Ingredient]
|
|
|
+
|
|
|
+ def fromFoodNode(
|
|
|
+ node: FoodNode,
|
|
|
+ amount: Float,
|
|
|
+ unit: MeasureUnit
|
|
|
+ ): Ingredient = {
|
|
|
+ class CachedIngredient(node: FoodNode, amount: Float, unit: MeasureUnit)
|
|
|
+ extends Ingredient(node match {
|
|
|
+ case food: FoodNodeWithId => FoodNodeId(food._id)
|
|
|
+ case food: USDANodeNoId => USDAId(food.fdcId)
|
|
|
+ case _ => ???
|
|
|
+ }, amount, unit) {
|
|
|
+
|
|
|
+ override lazy val food = Future.successful(node)
|
|
|
+
|
|
|
+ override def copy(
|
|
|
+ id: Ingredient.IngredientId = id,
|
|
|
+ amount: Float = amount,
|
|
|
+ unit: MeasureUnit = unit
|
|
|
+ ) = {
|
|
|
+ if (id != this.id) super.copy(id, amount)
|
|
|
+ else new CachedIngredient(node, amount, unit)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ new CachedIngredient(node, amount, unit)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+sealed trait RecipeNode extends FoodNode {
|
|
|
+ val nodeType = FoodNodeId.NodeType.RECIPE
|
|
|
+
|
|
|
+ def stdQties: Float
|
|
|
+ def stdQtiesPServing: Float
|
|
|
+ def defaultUnitType: UnitType
|
|
|
+ def ingredients: Seq[Ingredient]
|
|
|
+ def steps: Seq[String]
|
|
|
+
|
|
|
+ def withId(id: Identifier, uid: Identifier) = RecipeNodeId(
|
|
|
+ id,
|
|
|
+ uid,
|
|
|
+ name,
|
|
|
+ stdQties,
|
|
|
+ stdQtiesPServing,
|
|
|
+ defaultUnitType,
|
|
|
+ ingredients,
|
|
|
+ steps,
|
|
|
+ density,
|
|
|
+ massPerUnit
|
|
|
+ )
|
|
|
+
|
|
|
+ def nutrient(num: String) = ingredients.map({ ingr =>
|
|
|
+ ingr.food.map( f =>
|
|
|
+ f.nutrient(num)
|
|
|
+ .map(_ * (ingr.unit
|
|
|
+ .convert(f, ingr.amount, f.defaultUnitType.defaultUnit) match {
|
|
|
+ case Success(amt) => amt.toFloat
|
|
|
+ // 2021-05-31: If we reach this case, it means we somehow can't
|
|
|
+ // convert the unit from whatever it's measured in to how we track
|
|
|
+ // it in the data layer. The UI should prevent this from ever
|
|
|
+ // happening.
|
|
|
+ case Failure(e) => throw e
|
|
|
+ }))
|
|
|
+ ).flatten
|
|
|
+ }).fold(Future.successful(0.0f))({ case (a, b) =>
|
|
|
+ a.zipWith(b) { case (x, y) => x + y }
|
|
|
+ }).map { _ / stdQties / 100.0f }
|
|
|
}
|
|
|
|
|
|
-case class USDANode(
|
|
|
- val _id: Option[Identifier],
|
|
|
+case class RecipeNodeNoId(
|
|
|
val name: String,
|
|
|
- val fdcId: Long,
|
|
|
+ val stdQties: Float,
|
|
|
+ val stdQtiesPServing: Float,
|
|
|
+ override val defaultUnitType: UnitType,
|
|
|
+ val ingredients: Seq[Ingredient],
|
|
|
+ val steps: Seq[String],
|
|
|
override val density: Option[Float],
|
|
|
- override val massPerUnit: Option[Float],
|
|
|
- override val defaultUnit: UnitType,
|
|
|
- val calories: Float,
|
|
|
- val nutrients: Map[String, Float]
|
|
|
-) extends FoodNode {
|
|
|
- val nodeType = FoodNode.NodeType.USDA
|
|
|
+ override val massPerUnit: Option[Float]
|
|
|
+) extends RecipeNode
|
|
|
+
|
|
|
+case class RecipeNodeId(
|
|
|
+ val _id: Identifier,
|
|
|
+ val uid: Identifier,
|
|
|
+ val name: String,
|
|
|
+ val stdQties: Float,
|
|
|
+ val stdQtiesPServing: Float,
|
|
|
+ override val defaultUnitType: UnitType,
|
|
|
+ val ingredients: Seq[Ingredient],
|
|
|
+ val steps: Seq[String],
|
|
|
+ override val density: Option[Float],
|
|
|
+ override val massPerUnit: Option[Float]
|
|
|
+) extends RecipeNode with FoodNodeId
|
|
|
+
|
|
|
+object RecipeNodeNoId {
|
|
|
+ implicit val config = JsonConfiguration(JsonNaming.SnakeCase)
|
|
|
+ implicit val idFmt = com.weEat.shared.models.IdentifierHelper.fmt
|
|
|
+ implicit val fmt = Json.using[Json.WithDefaultValues].format[RecipeNodeNoId]
|
|
|
+}
|
|
|
+
|
|
|
+object RecipeNodeId {
|
|
|
+ implicit val config = JsonConfiguration(JsonNaming.SnakeCase)
|
|
|
+ implicit val idFmt = com.weEat.shared.models.IdentifierHelper.fmt
|
|
|
+ implicit val fmt = Json.using[Json.WithDefaultValues].format[RecipeNodeId]
|
|
|
+}
|
|
|
|
|
|
- def withId(id: Identifier) = USDANode(
|
|
|
- Some(id),
|
|
|
+sealed trait USDANode extends FoodNode {
|
|
|
+ val nodeType = FoodNodeId.NodeType.USDA
|
|
|
+
|
|
|
+ def fdcId: Long
|
|
|
+ def calories: Float
|
|
|
+ def nutrients: Map[String, Float]
|
|
|
+
|
|
|
+ def withId(id: Identifier, uid: Identifier) = USDANodeId(
|
|
|
+ id,
|
|
|
+ uid,
|
|
|
name,
|
|
|
fdcId,
|
|
|
density,
|
|
|
massPerUnit,
|
|
|
- defaultUnit,
|
|
|
calories,
|
|
|
nutrients
|
|
|
)
|
|
|
+
|
|
|
+ def nutrient(str: String) = Future.successful(nutrients.get(str).getOrElse(0.0f))
|
|
|
}
|
|
|
|
|
|
-object USDANode {
|
|
|
+case class USDANodeNoId(
|
|
|
+ val name: String,
|
|
|
+ val fdcId: Long,
|
|
|
+ override val density: Option[Float],
|
|
|
+ override val massPerUnit: Option[Float],
|
|
|
+ val calories: Float,
|
|
|
+ val nutrients: Map[String, Float]
|
|
|
+) extends USDANode
|
|
|
+
|
|
|
+case class USDANodeId(
|
|
|
+ val _id: Identifier,
|
|
|
+ val uid: Identifier,
|
|
|
+ val name: String,
|
|
|
+ val fdcId: Long,
|
|
|
+ override val density: Option[Float],
|
|
|
+ override val massPerUnit: Option[Float],
|
|
|
+ val calories: Float,
|
|
|
+ val nutrients: Map[String, Float]
|
|
|
+) extends USDANode with FoodNodeId
|
|
|
+
|
|
|
+object USDANodeNoId {
|
|
|
val kcalNutrientId = 1008
|
|
|
|
|
|
- def fromSearchResult(usda: SearchResultFood) = USDANode(
|
|
|
- None,
|
|
|
+ def fromSearchResult(usda: SearchResultFood) = USDANodeNoId(
|
|
|
usda.description,
|
|
|
usda.fdcId,
|
|
|
None,
|
|
|
None,
|
|
|
- MASS,
|
|
|
- usda.foodNutrients.find(_.nutrientId == kcalNutrientId).map(_.value)
|
|
|
- .getOrElse(0.0f),
|
|
|
- usda.foodNutrients.map(x => (x.nutrientNumber, x.value)).toMap
|
|
|
+ usda.foodNutrients
|
|
|
+ .find(_.nutrientId == kcalNutrientId)
|
|
|
+ .map(_.value)
|
|
|
+ .getOrElse(0.0f),
|
|
|
+ usda.foodNutrients
|
|
|
+ .map(x => (x.nutrientNumber, x.value)).toMap
|
|
|
)
|
|
|
|
|
|
+ import gov.usda.nal.fdc.models._
|
|
|
+
|
|
|
+ def fromFoodItem(usda: FoodItem) = usda match {
|
|
|
+ case AbridgedFoodItem(id, _, desc, nutr, _, _, _, _, _) => USDANodeNoId(
|
|
|
+ desc,
|
|
|
+ id,
|
|
|
+ None,
|
|
|
+ None,
|
|
|
+ nutr.find(_.nutrientId == kcalNutrientId).map(_.value).getOrElse(0.0f),
|
|
|
+ nutr.map(x => (x.nutrientNumber, x.value)).toMap
|
|
|
+ )
|
|
|
+ case BrandedFoodItem(id, _, desc, _, _, _, _, _, _, _, _, servSize, servUnit,
|
|
|
+ _, nutr, _, _, _) => USDANodeNoId(
|
|
|
+ desc,
|
|
|
+ id,
|
|
|
+ None,
|
|
|
+ None,
|
|
|
+ nutr.find(_.nutrient.id == kcalNutrientId).flatMap(_.amount).getOrElse(0.0f),
|
|
|
+ nutr.flatMap(x => x.amount.map(amt => (x.nutrient.number, amt))).toMap
|
|
|
+ )
|
|
|
+ case FoundationFoodItem(id, _, desc, _, _, _, _, _, _, _, _, nutr, port, _,
|
|
|
+ _) => USDANodeNoId(
|
|
|
+ desc,
|
|
|
+ id,
|
|
|
+ None,
|
|
|
+ None,
|
|
|
+ nutr.calories.getOrElse(0.0f),
|
|
|
+ Seq(
|
|
|
+ nutr.fat.map(amt => ("204", amt)),
|
|
|
+ nutr.saturatedFat.map(amt => ("606", amt)),
|
|
|
+ nutr.transFat.map(amt => ("605", amt)),
|
|
|
+ nutr.cholesterol.map(amt => ("601", amt)),
|
|
|
+ nutr.sodium.map(amt => ("307", amt)),
|
|
|
+ nutr.carbohydrates.map(amt => ("205", amt)),
|
|
|
+ nutr.fiber.map(amt => ("291", amt)),
|
|
|
+ nutr.sugars.map(amt => ("269", amt)),
|
|
|
+ nutr.protein.map(amt => ("203", amt)),
|
|
|
+ nutr.calcium.map(amt => ("301", amt)),
|
|
|
+ nutr.iron.map(amt => ("303", amt)),
|
|
|
+ nutr.postassium.map(amt => ("306", amt)),
|
|
|
+ nutr.calories.map(amt => ("208", amt))
|
|
|
+ ).flatten.toMap
|
|
|
+ )
|
|
|
+ case SurveyFoodItem(id, _, desc, _, _, _, _, _, _, port, _, _) => ???
|
|
|
+ case SRLegacyFoodItem(_, _, desc, _, _, _, _, _, _, nutr, _, _, _, port, _) => ???
|
|
|
+ case SampleFoodItem(id, _, desc, _, _, _) => ???
|
|
|
+ }
|
|
|
+
|
|
|
import play.api.libs.functional.syntax._
|
|
|
- import play.api.libs.json.{Format,JsonConfiguration,JsonNaming}
|
|
|
|
|
|
implicit val config = JsonConfiguration(JsonNaming.SnakeCase)
|
|
|
implicit val idFmt = com.weEat.shared.models.IdentifierHelper.fmt
|
|
|
- implicit val fmt = Json.using[Json.WithDefaultValues].format[USDANode]
|
|
|
+ implicit val fmt = Json.using[Json.WithDefaultValues].format[USDANodeNoId]
|
|
|
}
|
|
|
|
|
|
-object FoodNode {
|
|
|
+object USDANodeId {
|
|
|
+ import play.api.libs.functional.syntax._
|
|
|
+
|
|
|
+ implicit val config = JsonConfiguration(JsonNaming.SnakeCase)
|
|
|
+ implicit val idFmt = com.weEat.shared.models.IdentifierHelper.fmt
|
|
|
+ implicit val fmt = Json.using[Json.WithDefaultValues].format[USDANodeId]
|
|
|
+}
|
|
|
+
|
|
|
+object FoodNodeId {
|
|
|
val collectionName = "foods"
|
|
|
|
|
|
+ import play.api.libs.functional.syntax._
|
|
|
+ import play.api.libs.json._
|
|
|
+ import java.text.ParseException
|
|
|
+
|
|
|
object NodeType extends Enumeration {
|
|
|
type NodeType = Value
|
|
|
val USDA, RECIPE = Value
|
|
|
@@ -136,22 +320,42 @@ object FoodNode {
|
|
|
}
|
|
|
|
|
|
import NodeType._
|
|
|
+
|
|
|
+ implicit val requestWrites = Writes[FoodNodeId](requ =>
|
|
|
+ (requ match {
|
|
|
+ case node: USDANodeId => Json.toJson(node)
|
|
|
+ case node: RecipeNodeId => Json.toJson(node)
|
|
|
+ }).asInstanceOf[JsObject] + ("node_type", Json.toJson(requ.nodeType))
|
|
|
+ )
|
|
|
+
|
|
|
+ implicit val requestReads = Reads[FoodNodeId](json =>
|
|
|
+ (json \ "node_type").as[NodeType] match {
|
|
|
+ case USDA => Json.fromJson[USDANodeId](json)
|
|
|
+ case RECIPE => Json.fromJson[RecipeNodeId](json)
|
|
|
+ }
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+object FoodNode {
|
|
|
+ import FoodNodeId.NodeType
|
|
|
+ import FoodNodeId.NodeType._
|
|
|
import play.api.libs.functional.syntax._
|
|
|
import play.api.libs.json._
|
|
|
import java.text.ParseException
|
|
|
|
|
|
implicit val requestWrites = Writes[FoodNode](requ =>
|
|
|
(requ match {
|
|
|
- case node: USDANode => Json.toJson(node)
|
|
|
- case node: RecipeNode => Json.toJson(node)
|
|
|
+ case node: USDANodeNoId => Json.toJson(node)
|
|
|
+ case node: RecipeNodeNoId => Json.toJson(node)
|
|
|
+ case node: FoodNodeId => Json.toJson(node)
|
|
|
}).asInstanceOf[JsObject] + ("node_type", Json.toJson(requ.nodeType))
|
|
|
)
|
|
|
|
|
|
implicit val requestReads = Reads[FoodNode](json =>
|
|
|
(json \ "node_type").as[NodeType] match {
|
|
|
- case USDA => Json.fromJson[USDANode](json)
|
|
|
- case RECIPE => Json.fromJson[RecipeNode](json)
|
|
|
- case _ => ???
|
|
|
+ case USDA => Json.fromJson[USDANodeNoId](json)
|
|
|
+ case RECIPE => Json.fromJson[RecipeNodeNoId](json)
|
|
|
}
|
|
|
)
|
|
|
}
|
|
|
+
|