Formatted with new scalafmt format

master
mgifos 7 years ago
parent 21024285ae
commit f2bbd95745
  1. 3
      .scalafmt.conf
  2. 47
      src/main/scala/com.github.mgifos.workouts/GarminConnect.scala
  3. 58
      src/main/scala/com.github.mgifos.workouts/Main.scala
  4. 34
      src/main/scala/com.github.mgifos.workouts/model/Duration.scala
  5. 29
      src/main/scala/com.github.mgifos.workouts/model/Step.scala
  6. 50
      src/main/scala/com.github.mgifos.workouts/model/Target.scala
  7. 8
      src/main/scala/com.github.mgifos.workouts/model/WeeklyPlan.scala
  8. 15
      src/main/scala/com.github.mgifos.workouts/model/Workout.scala

@ -0,0 +1,3 @@
maxColumn = 150
includeCurlyBraceInSelectChains = false
optIn.breakChainOnFirstMethodDot = false

@ -48,14 +48,13 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
val source = Source(workouts.map { workout => val source = Source(workouts.map { workout =>
val req = Post("https://connect.garmin.com/modern/proxy/workout-service/workout") val req = Post("https://connect.garmin.com/modern/proxy/workout-service/workout")
.withEntity(HttpEntity(`application/json`, workout.json.toString())) .withEntity(HttpEntity(`application/json`, workout.json.toString()))
.withHeaders(session.headers .withHeaders(
session.headers
:+ Referer("https://connect.garmin.com/modern/workout/create/running") :+ Referer("https://connect.garmin.com/modern/workout/create/running")
:+ RawHeader("NK", "NT")) :+ RawHeader("NK", "NT"))
workout.name -> req workout.name -> req
}) })
val flow = Flow[(String, HttpRequest)] val flow = Flow[(String, HttpRequest)].throttle(1, 1.second, 1, ThrottleMode.shaping).mapAsync(1) {
.throttle(1, 1.second, 1, ThrottleMode.shaping)
.mapAsync(1) {
case (workout, req) => case (workout, req) =>
Http().singleRequest(req).flatMap { res => Http().singleRequest(req).flatMap { res =>
if (res.status == OK) { if (res.status == OK) {
@ -88,16 +87,16 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
if ids.nonEmpty if ids.nonEmpty
} yield { } yield {
val label = s"$workout -> ${ids.mkString("[", ", ", "]")}" val label = s"$workout -> ${ids.mkString("[", ", ", "]")}"
label -> ids.map(id => Post(s"https://connect.garmin.com/modern/proxy/workout-service/workout/$id") label -> ids.map(
.withHeaders(session.headers id =>
Post(s"https://connect.garmin.com/modern/proxy/workout-service/workout/$id").withHeaders(
session.headers
:+ Referer("https://connect.garmin.com/modern/workouts") :+ Referer("https://connect.garmin.com/modern/workouts")
:+ RawHeader("NK", "NT") :+ RawHeader("NK", "NT")
:+ RawHeader("X-HTTP-Method-Override", "DELETE"))) :+ RawHeader("X-HTTP-Method-Override", "DELETE")))
} }
} }
val source = Source.fromFuture(futureRequests).flatMapConcat(Source(_)) val source = Source.fromFuture(futureRequests).flatMapConcat(Source(_)).throttle(1, 1.second, 1, ThrottleMode.shaping).mapAsync(1) {
.throttle(1, 1.second, 1, ThrottleMode.shaping)
.mapAsync(1) {
case (label, reqs) => case (label, reqs) =>
val statusesFut = Future.sequence(reqs.map(req => Http().singleRequest(req).withoutBody)) val statusesFut = Future.sequence(reqs.map(req => Http().singleRequest(req).withoutBody))
statusesFut.map { statuses => statusesFut.map { statuses =>
@ -127,7 +126,8 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
else log.error(s" Cannot schedule: $label") else log.error(s" Cannot schedule: $label")
} }
} }
.runWith(Sink.seq).map(_.length) .runWith(Sink.seq)
.map(_.length)
} }
/** /**
@ -137,20 +137,23 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
private def getWorkoutsMap()(implicit session: GarminSession): Future[Map[String, Seq[Long]]] = { private def getWorkoutsMap()(implicit session: GarminSession): Future[Map[String, Seq[Long]]] = {
val req = Get("https://connect.garmin.com/modern/proxy/workout-service/workouts?start=1&limit=9999&myWorkoutsOnly=true&sharedWorkoutsOnly=false") val req = Get("https://connect.garmin.com/modern/proxy/workout-service/workouts?start=1&limit=9999&myWorkoutsOnly=true&sharedWorkoutsOnly=false")
.withHeaders(session.headers .withHeaders(
session.headers
:+ Referer("https://connect.garmin.com/modern/workouts") :+ Referer("https://connect.garmin.com/modern/workouts")
:+ RawHeader("NK", "NT")) :+ RawHeader("NK", "NT"))
val source = Source.fromFuture( val source = Source.fromFuture(Http().singleRequest(req).flatMap { res =>
Http().singleRequest(req).flatMap { res =>
if (res.status == OK) if (res.status == OK)
res.body.map { json => res.body.map { json =>
Json.parse(json).asOpt[Seq[JsObject]].map { arr => Json
.parse(json)
.asOpt[Seq[JsObject]]
.map { arr =>
arr.map(x => (x \ "workoutName").as[String] -> (x \ "workoutId").as[Long]) arr.map(x => (x \ "workoutName").as[String] -> (x \ "workoutId").as[Long])
}.getOrElse(Seq.empty) }
.getOrElse(Seq.empty)
.groupBy { case (name, _) => name } .groupBy { case (name, _) => name }
.map { case (a, b) => a -> b.map(_._2) } .map { case (a, b) => a -> b.map(_._2) }
} } else {
else {
log.debug(s"Cannot retrieve workout list, response: $res") log.debug(s"Cannot retrieve workout list, response: $res")
Future.failed(new Error("Cannot retrieve workout list from Garmin Connect")) Future.failed(new Error("Cannot retrieve workout list from Garmin Connect"))
} }
@ -251,14 +254,14 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
) )
for { for {
res1 <- Http().singleRequest(HttpRequest(uri = Uri("https://sso.garmin.com/sso/login").withQuery(Query(params)))).withoutBody res1 <- Http().singleRequest(HttpRequest(uri = Uri("https://sso.garmin.com/sso/login").withQuery(Query(params)))).withoutBody
res2 <- Http().singleRequest( res2 <- Http()
.singleRequest(
HttpRequest( HttpRequest(
POST, POST,
Uri("https://sso.garmin.com/sso/login").withQuery(Query(params)), Uri("https://sso.garmin.com/sso/login").withQuery(Query(params)),
entity = FormData(Map( entity = FormData(Map("username" -> email, "password" -> password, "embed" -> "false")).toEntity
"username" -> email, ).withHeaders(extractCookies(res1)))
"password" -> password, .withoutBody
"embed" -> "false")).toEntity).withHeaders(extractCookies(res1))).withoutBody
sessionCookies <- redirectionLoop(0, "https://connect.garmin.com/modern", extractCookies(res2)) sessionCookies <- redirectionLoop(0, "https://connect.garmin.com/modern", extractCookies(res2))
} yield GarminSession(sessionCookies) } yield GarminSession(sessionCookies)
} }

@ -20,8 +20,7 @@ object Modes extends Enumeration {
val schedule: Mode = Value("schedule") val schedule: Mode = Value("schedule")
} }
case class Config( case class Config(mode: Option[Modes.Mode] = None,
mode: Option[Modes.Mode] = None,
system: MeasurementSystems.MeasurementSystem = MeasurementSystems.metric, system: MeasurementSystems.MeasurementSystem = MeasurementSystems.metric,
csv: String = "", csv: String = "",
delete: Boolean = false, delete: Boolean = false,
@ -59,10 +58,13 @@ object Main extends App {
opt[String]('p', "password").action((x, c) => c.copy(password = x)).text("Password to login to Garmin Connect") opt[String]('p', "password").action((x, c) => c.copy(password = x)).text("Password to login to Garmin Connect")
opt[MeasurementSystems.MeasurementSystem]('m', "measurement_system").action((x, c) => c.copy(system = x)).text(""""metric" (default) or "imperial" (miles, inches, ...) measurement system choice.""") opt[MeasurementSystems.MeasurementSystem]('m', "measurement_system")
.action((x, c) => c.copy(system = x))
.text(""""metric" (default) or "imperial" (miles, inches, ...) measurement system choice.""")
opt[Unit]('x', "delete").action((_, c) => c.copy(delete = true)).text( opt[Unit]('x', "delete")
"Delete all existing workouts with same names as the ones contained within the file. In case of import/schedule commands, " + .action((_, c) => c.copy(delete = true))
.text("Delete all existing workouts with same names as the ones contained within the file. In case of import/schedule commands, " +
"this will be done before the actual action.") "this will be done before the actual action.")
help("help").text("prints this usage text") help("help").text("prints this usage text")
@ -73,13 +75,15 @@ object Main extends App {
note("\n") note("\n")
cmd("import"). cmd("import")
action((_, c) => c.copy(mode = Some(Modes.`import`))).text( .action((_, c) => c.copy(mode = Some(Modes.`import`)))
"Imports all workout definitions from CSV file. If it's omitted, it is will be on by default.") .text("Imports all workout definitions from CSV file. If it's omitted, it is will be on by default.")
note("") note("")
cmd("schedule").action((_, c) => c.copy(mode = Some(Modes.schedule))).text( cmd("schedule")
.action((_, c) => c.copy(mode = Some(Modes.schedule)))
.text(
"Schedules your weekly plan defined in CSV in Garmin Connect calendar, starting from the first day of first week or" + "Schedules your weekly plan defined in CSV in Garmin Connect calendar, starting from the first day of first week or" +
" ending on the last day of the last week. Either start or end date must be entered so the scheduling can be done" + " ending on the last day of the last week. Either start or end date must be entered so the scheduling can be done" +
" properly. In case both are entered, start date has priority. All dates have to be entered in ISO date format" + " properly. In case both are entered, start date has priority. All dates have to be entered in ISO date format" +
@ -87,12 +91,15 @@ object Main extends App {
.children( .children(
opt[String]('s', "start").action((x, c) => c.copy(start = LocalDate.parse(x))).text("Date of the first day of the first week of the plan"), opt[String]('s', "start").action((x, c) => c.copy(start = LocalDate.parse(x))).text("Date of the first day of the first week of the plan"),
opt[String]('n', "end").action((x, c) => c.copy(end = LocalDate.parse(x))).text("Date of the last day of the last week of the plan\n"), opt[String]('n', "end").action((x, c) => c.copy(end = LocalDate.parse(x))).text("Date of the last day of the last week of the plan\n"),
checkConfig(c => checkConfig(
c =>
if (c.mode.contains(Modes.schedule) && c.start.isEqual(LocalDate.MIN) && c.end.isEqual(LocalDate.MIN)) if (c.mode.contains(Modes.schedule) && c.start.isEqual(LocalDate.MIN) && c.end.isEqual(LocalDate.MIN))
failure("Either start or end date must be entered!") failure("Either start or end date must be entered!")
else success)) else success)
)
note("EXAMPLES").text("EXAMPLES\n\nSchedules ultra 80k plan targeting 28-4-2018 for a race day (also deletes existing workouts with the same names)" + note("EXAMPLES").text(
"EXAMPLES\n\nSchedules ultra 80k plan targeting 28-4-2018 for a race day (also deletes existing workouts with the same names)" +
"\n\nquick-plan schedule -n 2018-04-29 -x -e your-mail-address@example.com ultra-80k-runnersworld.csv") "\n\nquick-plan schedule -n 2018-04-29 -x -e your-mail-address@example.com ultra-80k-runnersworld.csv")
note("") note("")
@ -107,11 +114,15 @@ object Main extends App {
val console = System.console() val console = System.console()
def proceedToGarmin() = { def proceedToGarmin() = {
val email = if (config.email.nonEmpty) config.email else { val email =
if (config.email.nonEmpty) config.email
else {
print("Please enter your email address to login to Garmin Connect: ") print("Please enter your email address to login to Garmin Connect: ")
console.readLine() console.readLine()
} }
val password = if (config.password.nonEmpty) config.password else { val password =
if (config.password.nonEmpty) config.password
else {
print("Password: ") print("Password: ")
new String(console.readPassword()) new String(console.readPassword())
} }
@ -154,21 +165,26 @@ object Main extends App {
/** /**
* Deletes existing workouts with the same names or not * Deletes existing workouts with the same names or not
*/ */
private def deleteWorkoutsTask(workouts: Seq[String])(implicit config: Config, garmin: GarminConnect, session: GarminSession): Future[Option[String]] = { private def deleteWorkoutsTask(
workouts: Seq[String])(implicit config: Config, garmin: GarminConnect, session: GarminSession): Future[Option[String]] = {
if (config.delete) if (config.delete)
garmin.deleteWorkouts(workouts).map(c => Some(s"$c deleted")) garmin.deleteWorkouts(workouts).map(c => Some(s"$c deleted"))
else else
Future.successful(None) Future.successful(None)
} }
private def createWorkoutsTask(workouts: Seq[WorkoutDef])(implicit config: Config, garmin: GarminConnect, session: GarminSession): Future[Option[Seq[GarminWorkout]]] = { private def createWorkoutsTask(
workouts: Seq[WorkoutDef])(implicit config: Config, garmin: GarminConnect, session: GarminSession): Future[Option[Seq[GarminWorkout]]] = {
if (config.mode.exists(Seq(Modes.`import`, Modes.schedule).contains)) if (config.mode.exists(Seq(Modes.`import`, Modes.schedule).contains))
garmin.createWorkouts(workouts).map(Option.apply) garmin.createWorkouts(workouts).map(Option.apply)
else else
Future.successful(None) Future.successful(None)
} }
private def scheduleTask(workouts: Seq[GarminWorkout])(implicit config: Config, garmin: GarminConnect, plan: WeeklyPlan, session: GarminSession): Future[Option[String]] = { private def scheduleTask(workouts: Seq[GarminWorkout])(implicit config: Config,
garmin: GarminConnect,
plan: WeeklyPlan,
session: GarminSession): Future[Option[String]] = {
if (config.mode.contains(Modes.schedule)) { if (config.mode.contains(Modes.schedule)) {
@ -178,9 +194,13 @@ object Main extends App {
} }
val woMap: Map[String, GarminWorkout] = Map(workouts.map(ga => ga.name -> ga): _*) val woMap: Map[String, GarminWorkout] = Map(workouts.map(ga => ga.name -> ga): _*)
val spec = plan.get().zipWithIndex.collect { val spec = plan
.get()
.zipWithIndex
.collect {
case (Some(ref), day) if !start.plusDays(day).isBefore(LocalDate.now()) => start.plusDays(day) -> woMap(ref.name) case (Some(ref), day) if !start.plusDays(day).isBefore(LocalDate.now()) => start.plusDays(day) -> woMap(ref.name)
}.to[Seq] }
.to[Seq]
garmin.schedule(spec).map(c => Some(s"$c scheduled")) garmin.schedule(spec).map(c => Some(s"$c scheduled"))
} else } else
Future.successful(None) Future.successful(None)

@ -8,37 +8,36 @@ sealed trait Duration {
} }
case class DistanceDuration(distance: Float, unit: DistanceUnit) extends Duration { case class DistanceDuration(distance: Float, unit: DistanceUnit) extends Duration {
override def json: JsObject = Json.obj( override def json: JsObject =
"endCondition" -> Json.obj( Json.obj(
"conditionTypeKey" -> "distance", "endCondition" -> Json.obj("conditionTypeKey" -> "distance", "conditionTypeId" -> 3),
"conditionTypeId" -> 3), "preferredEndConditionUnit" -> Json.obj("unitKey" -> unit.fullName),
"preferredEndConditionUnit" -> Json.obj(
"unitKey" -> unit.fullName),
"endConditionValue" -> unit.toMeters(distance), "endConditionValue" -> unit.toMeters(distance),
"endConditionCompare" -> JsNull, "endConditionCompare" -> JsNull,
"endConditionZone" -> JsNull) "endConditionZone" -> JsNull
)
} }
case class TimeDuration(minutes: Int = 0, seconds: Int = 0) extends Duration { case class TimeDuration(minutes: Int = 0, seconds: Int = 0) extends Duration {
override def json: JsObject = Json.obj( override def json: JsObject =
"endCondition" -> Json.obj( Json.obj(
"conditionTypeKey" -> "time", "endCondition" -> Json.obj("conditionTypeKey" -> "time", "conditionTypeId" -> 2),
"conditionTypeId" -> 2),
"preferredEndConditionUnit" -> JsNull, "preferredEndConditionUnit" -> JsNull,
"endConditionValue" -> (minutes * 60 + seconds), "endConditionValue" -> (minutes * 60 + seconds),
"endConditionCompare" -> JsNull, "endConditionCompare" -> JsNull,
"endConditionZone" -> JsNull) "endConditionZone" -> JsNull
)
} }
object LapButtonPressed extends Duration { object LapButtonPressed extends Duration {
override def json: JsObject = Json.obj( override def json: JsObject =
"endCondition" -> Json.obj( Json.obj(
"conditionTypeKey" -> "lap.button", "endCondition" -> Json.obj("conditionTypeKey" -> "lap.button", "conditionTypeId" -> 1),
"conditionTypeId" -> 1),
"preferredEndConditionUnit" -> JsNull, "preferredEndConditionUnit" -> JsNull,
"endConditionValue" -> JsNull, "endConditionValue" -> JsNull,
"endConditionCompare" -> JsNull, "endConditionCompare" -> JsNull,
"endConditionZone" -> JsNull) "endConditionZone" -> JsNull
)
} }
object Duration { object Duration {
@ -53,4 +52,3 @@ object Duration {
case _ => throw new IllegalArgumentException(s"Duration cannot be parsed $x") case _ => throw new IllegalArgumentException(s"Duration cannot be parsed $x")
} }
} }

@ -8,21 +8,19 @@ trait Step {
def json(order: Int): JsValue def json(order: Int): JsValue
} }
abstract class DurationStep( abstract class DurationStep(override val `type`: String, override val typeId: Int) extends Step {
override val `type`: String,
override val typeId: Int) extends Step {
def duration: Duration def duration: Duration
def target: Option[Target] def target: Option[Target]
def json(order: Int): JsObject = Json.obj( def json(order: Int): JsObject =
Json.obj(
"type" -> "ExecutableStepDTO", "type" -> "ExecutableStepDTO",
"stepId" -> JsNull, "stepId" -> JsNull,
"stepOrder" -> order, "stepOrder" -> order,
"childStepId" -> JsNull, "childStepId" -> JsNull,
"description" -> JsNull, "description" -> JsNull,
"stepType" -> Json.obj( "stepType" -> Json.obj("stepTypeId" -> typeId, "stepTypeKey" -> `type`)
"stepTypeId" -> typeId, ) ++ duration.json ++ target.fold(NoTarget.json)(_.json)
"stepTypeKey" -> `type`)) ++ duration.json ++ target.fold(NoTarget.json)(_.json)
} }
case class WarmupStep(duration: Duration, target: Option[Target] = None) extends DurationStep("warmup", 1) case class WarmupStep(duration: Duration, target: Option[Target] = None) extends DurationStep("warmup", 1)
@ -37,17 +35,17 @@ case class RepeatStep(count: Int, steps: Seq[Step]) extends Step {
override def `type` = "repeat" override def `type` = "repeat"
override def typeId = 6 override def typeId = 6
override def json(order: Int) = Json.obj( override def json(order: Int) =
Json.obj(
"stepId" -> JsNull, "stepId" -> JsNull,
"stepOrder" -> order, "stepOrder" -> order,
"stepType" -> Json.obj( "stepType" -> Json.obj("stepTypeId" -> typeId, "stepTypeKey" -> "repeat"),
"stepTypeId" -> typeId,
"stepTypeKey" -> "repeat"),
"numberOfIterations" -> count, "numberOfIterations" -> count,
"smartRepeat" -> false, "smartRepeat" -> false,
"childStepId" -> 1, "childStepId" -> 1,
"workoutSteps" -> steps.zipWithIndex.map { case (s, i) => s.json(i + 1) }, "workoutSteps" -> steps.zipWithIndex.map { case (s, i) => s.json(i + 1) },
"type" -> "RepeatGroupDTO") "type" -> "RepeatGroupDTO"
)
} }
object Step { object Step {
@ -57,7 +55,8 @@ object Step {
private val ParamsRx = """^([\w-\.:\s]+)\s*(@(.*))?$""".r private val ParamsRx = """^([\w-\.:\s]+)\s*(@(.*))?$""".r
def parse(x: String)(implicit msys: MeasurementSystems.MeasurementSystem): Step = x match { def parse(x: String)(implicit msys: MeasurementSystems.MeasurementSystem): Step = x match {
case StepRx(header, subSteps, _) if subSteps.nonEmpty => header match { case StepRx(header, subSteps, _) if subSteps.nonEmpty =>
header match {
case StepHeader(name, params) => case StepHeader(name, params) =>
if (name != "repeat") throw new IllegalArgumentException(s"'$name' cannot contain sub-steps, it must be 'repeat'") if (name != "repeat") throw new IllegalArgumentException(s"'$name' cannot contain sub-steps, it must be 'repeat'")
RepeatStep(params.trim.toInt, subSteps.trim.lines.toList.map(parseDurationStep)) RepeatStep(params.trim.toInt, subSteps.trim.lines.toList.map(parseDurationStep))
@ -68,7 +67,8 @@ object Step {
} }
private def parseDurationStep(x: String)(implicit msys: MeasurementSystems.MeasurementSystem): DurationStep = x match { private def parseDurationStep(x: String)(implicit msys: MeasurementSystems.MeasurementSystem): DurationStep = x match {
case StepHeader(name, params) => name match { case StepHeader(name, params) =>
name match {
case "warmup" => WarmupStep.tupled(expect(params)) case "warmup" => WarmupStep.tupled(expect(params))
case "run" | "bike" | "go" => IntervalStep.tupled(expect(params)) case "run" | "bike" | "go" => IntervalStep.tupled(expect(params))
case "recover" => RecoverStep.tupled(expect(params)) case "recover" => RecoverStep.tupled(expect(params))
@ -85,4 +85,3 @@ object Step {
case raw => throw new IllegalArgumentException(s"Cannot parse step parameters $raw") case raw => throw new IllegalArgumentException(s"Cannot parse step parameters $raw")
} }
} }

@ -7,53 +7,53 @@ sealed trait Target {
} }
case class HrZoneTarget(zone: Int) extends Target { case class HrZoneTarget(zone: Int) extends Target {
override def json = Json.obj( override def json =
"targetType" -> Json.obj( Json.obj(
"workoutTargetTypeId" -> 4, "targetType" -> Json.obj("workoutTargetTypeId" -> 4, "workoutTargetTypeKey" -> "heart.rate.zone"),
"workoutTargetTypeKey" -> "heart.rate.zone"),
"targetValueOne" -> "", "targetValueOne" -> "",
"targetValueTwo" -> "", "targetValueTwo" -> "",
"zoneNumber" -> zone.toString) "zoneNumber" -> zone.toString
)
} }
case class HrCustomTarget(from: Int, to: Int) extends Target { case class HrCustomTarget(from: Int, to: Int) extends Target {
override def json = Json.obj( override def json =
"targetType" -> Json.obj( Json.obj(
"workoutTargetTypeId" -> 4, "targetType" -> Json.obj("workoutTargetTypeId" -> 4, "workoutTargetTypeKey" -> "heart.rate.zone"),
"workoutTargetTypeKey" -> "heart.rate.zone"),
"targetValueOne" -> from, "targetValueOne" -> from,
"targetValueTwo" -> to, "targetValueTwo" -> to,
"zoneNumber" -> JsNull) "zoneNumber" -> JsNull
)
} }
case class PaceTarget(from: Pace, to: Pace) extends Target { case class PaceTarget(from: Pace, to: Pace) extends Target {
override def json = Json.obj( override def json =
"targetType" -> Json.obj( Json.obj(
"workoutTargetTypeId" -> 6, "targetType" -> Json.obj("workoutTargetTypeId" -> 6, "workoutTargetTypeKey" -> "pace.zone"),
"workoutTargetTypeKey" -> "pace.zone"),
"targetValueOne" -> from.speed, "targetValueOne" -> from.speed,
"targetValueTwo" -> to.speed, "targetValueTwo" -> to.speed,
"zoneNumber" -> JsNull) "zoneNumber" -> JsNull
)
} }
case class SpeedTarget(from: Speed, to: Speed) extends Target { case class SpeedTarget(from: Speed, to: Speed) extends Target {
override def json = Json.obj( override def json =
"targetType" -> Json.obj( Json.obj(
"workoutTargetTypeId" -> 5, "targetType" -> Json.obj("workoutTargetTypeId" -> 5, "workoutTargetTypeKey" -> "speed.zone"),
"workoutTargetTypeKey" -> "speed.zone"),
"targetValueOne" -> from.speed, "targetValueOne" -> from.speed,
"targetValueTwo" -> to.speed, "targetValueTwo" -> to.speed,
"zoneNumber" -> JsNull) "zoneNumber" -> JsNull
)
} }
object NoTarget extends Target { object NoTarget extends Target {
override def json = Json.obj( override def json =
"targetType" -> Json.obj( Json.obj(
"workoutTargetTypeId" -> 1, "targetType" -> Json.obj("workoutTargetTypeId" -> 1, "workoutTargetTypeKey" -> "no.target"),
"workoutTargetTypeKey" -> "no.target"),
"targetValueOne" -> JsNull, "targetValueOne" -> JsNull,
"targetValueTwo" -> JsNull, "targetValueTwo" -> JsNull,
"zoneNumber" -> JsNull) "zoneNumber" -> JsNull
)
} }
case class Pace(uom: DistanceUnits.DistanceUnit, exp: String) { case class Pace(uom: DistanceUnits.DistanceUnit, exp: String) {

@ -10,9 +10,13 @@ class WeeklyPlan(csv: Array[Byte])(implicit msys: MeasurementSystems.Measurement
private lazy val processed: Seq[Option[Workout]] = { private lazy val processed: Seq[Option[Workout]] = {
def weekPlan(week: Week, previousWeeks: Seq[Option[Workout]]): Seq[Option[Workout]] = Seq.tabulate(7) { weekDayNo => def weekPlan(week: Week, previousWeeks: Seq[Option[Workout]]): Seq[Option[Workout]] =
Seq
.tabulate(7) { weekDayNo =>
week.lift(weekDayNo + 1).flatMap(text => Option(text.trim).filter(_.nonEmpty)) week.lift(weekDayNo + 1).flatMap(text => Option(text.trim).filter(_.nonEmpty))
}.foldLeft(Seq.empty[Option[Workout]])((acc, maybeDayText) => acc :+ maybeDayText.map { dayText => }
.foldLeft(Seq.empty[Option[Workout]])((acc, maybeDayText) =>
acc :+ maybeDayText.map { dayText =>
Workout.parse(dayText) match { Workout.parse(dayText) match {
case note: WorkoutNote => onlyDefs(previousWeeks ++ acc).find(_.name == dayText).map(_.toRef).getOrElse(note) case note: WorkoutNote => onlyDefs(previousWeeks ++ acc).find(_.name == dayText).map(_.toRef).getOrElse(note)
case w: Workout => w case w: Workout => w

@ -11,18 +11,17 @@ trait Workout {
case class WorkoutDef(sport: String, name: String, steps: Seq[Step] = Nil) extends Workout { case class WorkoutDef(sport: String, name: String, steps: Seq[Step] = Nil) extends Workout {
def toRef: WorkoutRef = WorkoutRef(name) def toRef: WorkoutRef = WorkoutRef(name)
def withStep(step: Step): WorkoutDef = WorkoutDef(sport, name, steps :+ step) def withStep(step: Step): WorkoutDef = WorkoutDef(sport, name, steps :+ step)
override def json(): JsValue = Json.obj( override def json(): JsValue =
"sportType" -> Json.obj( Json.obj(
"sportTypeId" -> sportId(sport), "sportType" -> Json.obj("sportTypeId" -> sportId(sport), "sportTypeKey" -> sportTypeKey(sport)),
"sportTypeKey" -> sportTypeKey(sport)),
"workoutName" -> name, "workoutName" -> name,
"workoutSegments" -> Json.arr( "workoutSegments" -> Json.arr(
Json.obj( Json.obj(
"segmentOrder" -> 1, "segmentOrder" -> 1,
"sportType" -> Json.obj( "sportType" -> Json.obj("sportTypeId" -> sportId(sport), "sportTypeKey" -> sport),
"sportTypeId" -> sportId(sport), "workoutSteps" -> steps.zipWithIndex.map { case (s, i) => s.json(i + 1) }
"sportTypeKey" -> sport), ))
"workoutSteps" -> steps.zipWithIndex.map { case (s, i) => s.json(i + 1) }))) )
} }
case class WorkoutDefFailure(`type`: String, original: String, cause: String) extends Workout { case class WorkoutDefFailure(`type`: String, original: String, cause: String) extends Workout {

Loading…
Cancel
Save