Skip to content

Commit

Permalink
Change how we handle parent GUIDs for workouts
Browse files Browse the repository at this point in the history
  • Loading branch information
khy committed Mar 2, 2017
1 parent f4f5bd7 commit b6a1be6
Show file tree
Hide file tree
Showing 10 changed files with 88 additions and 77 deletions.
Expand Up @@ -38,10 +38,13 @@ class WorkoutsController(
}
}

def create = authenticated.async(parse.json) { request =>
def createChild(parentGuid: UUID) = create(Some(parentGuid))

def create(parentGuid: Option[UUID] = None) = authenticated.async(parse.json) { request =>
request.body.validate[core.Workout].fold(
error => Future.successful(BadRequest(error.toString)),
workout => workoutsService.addWorkout(
parentGuid = parentGuid,
workout = workout,
accessToken = request.accessToken
).flatMap { result =>
Expand Down
2 changes: 2 additions & 0 deletions modules/apis/workouts/app/db/Driver.scala
Expand Up @@ -4,6 +4,7 @@ import com.github.tminglei.slickpg._

trait Driver
extends ExPostgresDriver
with PgArraySupport
with PgDate2Support
with PgPlayJsonSupport
with array.PgArrayJdbcTypes
Expand All @@ -13,6 +14,7 @@ trait Driver

object Api
extends API
with ArrayImplicits
with DateTimeImplicits
with JsonImplicits
{
Expand Down
8 changes: 5 additions & 3 deletions modules/apis/workouts/app/db/Workout.scala
Expand Up @@ -10,6 +10,7 @@ case class WorkoutRecord(
guid: UUID,
schemaVersionMajor: Int,
schemaVersionMinor: Int,
parentGuids: Option[List[UUID]],
json: JsValue,
createdAt: ZonedDateTime,
createdByAccount: UUID,
Expand All @@ -25,11 +26,12 @@ class WorkoutsTable(tag: Tag)
with AuditData[WorkoutRecord]
{
def guid = column[UUID]("guid")
def parentGuids = column[Option[List[UUID]]]("parent_guids")
def json = column[JsValue]("json")

def * = (guid, schemaVersionMajor, schemaVersionMinor, json, createdAt,
createdByAccount, createdByAccessToken, deletedAt, deletedByAccount,
deletedByAccessToken) <> (WorkoutRecord.tupled, WorkoutRecord.unapply)
def * = (guid, schemaVersionMajor, schemaVersionMinor, parentGuids, json,
createdAt, createdByAccount, createdByAccessToken, deletedAt,
deletedByAccount, deletedByAccessToken) <> (WorkoutRecord.tupled, WorkoutRecord.unapply)
}

object Workouts extends TableQuery(new WorkoutsTable(_))
4 changes: 1 addition & 3 deletions modules/apis/workouts/app/models/JsonImplicits.scala
Expand Up @@ -37,7 +37,6 @@ object JsonImplicits {

implicit val measurementFormat = Json.format[Measurement]
implicit val variableFormat = Json.format[Variable]
implicit val movementFormat = Json.format[Movement]

implicit val coreMovementFormat = Json.format[core.Movement]
implicit val coreTaskMovementFormat = Json.format[core.TaskMovement]
Expand All @@ -51,8 +50,7 @@ object JsonImplicits {

implicit val coreWorkoutFormat = Json.format[core.Workout]

implicit val taskMovementFormat = Json.format[TaskMovement]
implicit val subTaskFormat = Json.format[SubTask]
implicit val movementFormat = Json.format[Movement]
implicit val workoutFormat = Json.format[Workout]

}
15 changes: 1 addition & 14 deletions modules/apis/workouts/app/models/Normalized.scala
Expand Up @@ -12,7 +12,6 @@ package core {
)

case class Workout(
parentGuid: Option[UUID],
name: Option[String],
reps: Option[Formula],
time: Option[Measurement],
Expand Down Expand Up @@ -48,7 +47,7 @@ case class Movement(

case class Workout(
guid: UUID,
parentGuid: Option[UUID],
parentGuids: Option[Seq[UUID]],
name: Option[String],
reps: Option[Formula],
time: Option[Measurement],
Expand All @@ -60,15 +59,3 @@ case class Workout(
deletedAt: Option[ZonedDateTime],
deletedBy: Option[User]
)

case class SubTask(
reps: Option[Formula],
movement: Option[TaskMovement]
)

case class TaskMovement(
guid: UUID,
name: String,
score: Option[String],
variables: Option[Seq[Variable]]
)
109 changes: 64 additions & 45 deletions modules/apis/workouts/app/services/WorkoutsService.scala
Expand Up @@ -48,7 +48,7 @@ class WorkoutsService(
error => throw new InvalidState(s"Invalid workout JSON from DB [$error]"),
workout => Workout(
guid = record.guid,
parentGuid = workout.parentGuid,
parentGuids = record.parentGuids,
name = workout.name,
reps = workout.reps,
time = workout.time,
Expand Down Expand Up @@ -82,15 +82,15 @@ class WorkoutsService(

parentGuids.foreach { parentGuids =>
query = query.filter { workout =>
workout.json +>> "parentGuid" inSet parentGuids.map(_.toString)
workout.parentGuids @& parentGuids.toList.bind
}
}

isChild.foreach { isChild =>
if (isChild) {
query = query.filter { _.json.+>("parentGuid").?.isDefined }
query = query.filter { _.parentGuids.isDefined }
} else {
query = query.filter { _.json.+>("parentGuid").?.isEmpty }
query = query.filter { _.parentGuids.isEmpty }
}
}

Expand All @@ -100,64 +100,83 @@ class WorkoutsService(
}
}

private def findWorkout(guid: UUID)(implicit ec: ExecutionContext): Future[Option[Workout]] = {
val query = Workouts.filter(_.guid === guid)
db.run(query.result).flatMap(db2api).map(_.headOption)
private def findRawWorkouts(guids: Seq[UUID])(implicit ec: ExecutionContext): Future[Seq[Workout]] = {
val query = Workouts.filter(_.guid.inSet(guids))
db.run(query.result).flatMap(db2api)
}

def addWorkout(
parentGuid: Option[UUID],
workout: core.Workout,
accessToken: AccessToken
)(implicit ec: ExecutionContext): Future[Validation[WorkoutRecord]] = {
// Find the "ancestry" of the workout (limited to 2 for now)
val futAncestry = for {
optParent <- workout.parentGuid.map { parentGuid =>
findWorkout(parentGuid)
}.getOrElse(Future.successful(None))
optGrandParent <- optParent.flatMap(_.parentGuid).map { grandParentGuid =>
findWorkout(grandParentGuid)
}.getOrElse(Future.successful(None))
} yield Seq(optParent, optGrandParent).flatten
// Find the "ancestry" of the workout
val futValAncestry = parentGuid.map { parentGuid =>
findRawWorkouts(Seq(parentGuid)).flatMap { workouts =>
workouts.headOption.map { workout =>
workout.parentGuids.map { parentGuids =>
findRawWorkouts(parentGuids)
}.getOrElse {
Future.successful(Nil)
}.map { parentWorkouts =>
Validation.success(workout +: parentWorkouts)
}
}.getOrElse {
Future.successful(Validation.failure(
key = "parentGuid",
messageKey = "unknownWorkoutGuid",
messageDetails = "specified" -> parentGuid.toString
))
}
}
}.getOrElse {
Future.successful(Validation.success(Nil))
}

val subTasks = WorkoutDsl.getSubTasks(workout)
val taskMovements = workout.movement.toSeq ++ subTasks.flatMap(_.movement)

// Fetch the actual movements referenced by the task movements
val futReferencedMovements = movementsService.
val futValReferencedMovements = movementsService.
getMovementsByGuid(taskMovements.map(_.guid)).
flatMap(movementsService.db2api)
flatMap(movementsService.db2api).
map(Validation.success)

for {
ancestry <- futAncestry
referencedMovements <- futReferencedMovements
valAncestry <- futValAncestry
valReferencedMovements <- futValReferencedMovements
result <- {
val validationErrors = WorkoutDsl.validateWorkout(workout, referencedMovements)

if (validationErrors.length > 0) {
Future.successful(Validation.failure(validationErrors))
} else {
val ancestryErrors = ancestry.headOption.map { parent =>
WorkoutDsl.validateAncestry(workout, parent)
}.getOrElse(Nil)
ValidationUtil.flatMapFuture(valAncestry ++ valReferencedMovements) { case (ancestry, referencedMovements) =>
val validationErrors = WorkoutDsl.validateWorkout(workout, referencedMovements)

if (ancestryErrors.length > 0) {
Future.successful(Validation.failure(ancestryErrors))
if (validationErrors.length > 0) {
Future.successful(Validation.failure(validationErrors))
} else {
val projection = Workouts.map { r =>
(r.guid, r.schemaVersionMajor, r.schemaVersionMinor, r.json,
r.createdByAccount, r.createdByAccessToken)
}.returning(Workouts.map(_.guid))

val insert = projection += ((UUID.randomUUID, 1, 0, Json.toJson(workout),
accessToken.resourceOwner.guid, accessToken.guid))

db.run(insert).flatMap { guid =>
val query = Workouts.filter(_.guid === guid)
db.run(query.result).map { workouts =>
workouts.headOption.map { workout =>
Validation.success(workout)
}.getOrElse {
throw new ResourceNotFound("workout", guid)
val ancestryErrors = ancestry.headOption.map { parent =>
WorkoutDsl.validateAncestry(workout, parent)
}.getOrElse(Nil)

if (ancestryErrors.length > 0) {
Future.successful(Validation.failure(ancestryErrors))
} else {
val projection = Workouts.map { r =>
(r.guid, r.schemaVersionMajor, r.schemaVersionMinor, r.parentGuids,
r.json, r.createdByAccount, r.createdByAccessToken)
}.returning(Workouts.map(_.guid))

val parentGuids = if (parentGuid.isDefined) Some(ancestry.map(_.guid).toList) else None

val insert = projection += ((UUID.randomUUID, 1, 0, parentGuids,
Json.toJson(workout), accessToken.resourceOwner.guid, accessToken.guid))

db.run(insert).flatMap { guid =>
val query = Workouts.filter(_.guid === guid)
db.run(query.result).map { workouts =>
workouts.headOption.map { workout =>
Validation.success(workout)
}.getOrElse {
throw new ResourceNotFound("workout", guid)
}
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions modules/apis/workouts/conf/workouts.routes
@@ -1,5 +1,6 @@
GET /workouts controllers.workouts.WorkoutsController.index
POST /workouts controllers.workouts.WorkoutsController.create
GET /workouts controllers.workouts.WorkoutsController.index
POST /workouts controllers.workouts.WorkoutsController.create()
POST /workouts/:guid/workouts controllers.workouts.WorkoutsController.createChild(guid: java.util.UUID)

GET /movements controllers.workouts.MovementsController.index
POST /movements controllers.workouts.MovementsController.create
Expand Down
1 change: 1 addition & 0 deletions modules/apis/workouts/schema/scripts/20161118-234700.sql
Expand Up @@ -2,6 +2,7 @@ CREATE TABLE workouts (
guid uuid PRIMARY KEY,
schema_version_major int NOT NULL,
schema_version_minor int NOT NULL,
parent_guids uuid[],
json jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
created_by_account uuid NOT NULL,
Expand Down
14 changes: 6 additions & 8 deletions modules/apis/workouts/test/TestHelper.scala
Expand Up @@ -40,26 +40,25 @@ class TestHelper(
db.run(sqlu"delete from movements")
}

def buildWorkout(
def buildWorkoutFromJson(
rawJson: String
) = Json.parse(rawJson).validate[core.Workout].fold(
error => throw new RuntimeException(s"Invalid workout JSON [$error]: $rawJson"),
workout => workout
)

def createWorkout(
rawJson: String
def createWorkoutFromJson(
rawJson: String,
parentGuid: Option[UUID] = None
)(implicit accessToken: AccessToken): WorkoutRecord = await {
workoutsService.addWorkout(buildWorkout(rawJson), accessToken)
workoutsService.addWorkout(parentGuid, buildWorkoutFromJson(rawJson), accessToken)
}.toSuccess.value

def buildWorkout(
parentGuid: Option[UUID] = None,
time: Option[Measurement] = None,
score: Option[String] = Some("time"),
movement: Option[MovementRecord] = None
) = core.Workout(
parentGuid = parentGuid,
name = None,
reps = None,
time = time,
Expand All @@ -81,13 +80,12 @@ class TestHelper(
movement: Option[MovementRecord] = None
)(implicit accessToken: AccessToken): WorkoutRecord = await {
val workout = buildWorkout(
parentGuid = parentGuid,
time = time,
score = score,
movement = movement.orElse { Some(createMovement()) }
)

workoutsService.addWorkout(workout, accessToken)
workoutsService.addWorkout(parentGuid, workout, accessToken)
}.toSuccess.value

def deleteWorkouts() {
Expand Down
Expand Up @@ -18,7 +18,7 @@ class ChildSpec extends IntegrationSpec {
"create a workout with subtasks" in {
val pullUp = testHelper.createMovement("Pull Up")

val parent = testHelper.createWorkout(s"""
val parent = testHelper.createWorkoutFromJson(s"""
{
"name": "Parent",
"reps": 1,
Expand Down

0 comments on commit b6a1be6

Please sign in to comment.