Formatted with new scalafmt format

master
mgifos 7 years ago
parent 21024285ae
commit f2bbd95745
  1. 3
      .scalafmt.conf
  2. 59
      src/main/scala/com.github.mgifos.workouts/GarminConnect.scala
  3. 64
      src/main/scala/com.github.mgifos.workouts/Main.scala
  4. 36
      src/main/scala/com.github.mgifos.workouts/model/Duration.scala
  5. 31
      src/main/scala/com.github.mgifos.workouts/model/Step.scala
  6. 52
      src/main/scala/com.github.mgifos.workouts/model/Target.scala
  7. 8
      src/main/scala/com.github.mgifos.workouts/model/WeeklyPlan.scala
  8. 17
      src/main/scala/com.github.mgifos.workouts/model/Workout.scala

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

@ -2,7 +2,7 @@ package com.github.mgifos.workouts
import java.time.LocalDate import java.time.LocalDate
import akka.actor.{ Actor, ActorRef, ActorSystem, Props } import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import akka.http.scaladsl.Http import akka.http.scaladsl.Http
import akka.http.scaladsl.client.RequestBuilding._ import akka.http.scaladsl.client.RequestBuilding._
import akka.http.scaladsl.model.ContentTypes._ import akka.http.scaladsl.model.ContentTypes._
@ -12,17 +12,17 @@ import akka.http.scaladsl.model.Uri.Query
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.model.headers._
import akka.pattern.ask import akka.pattern.ask
import akka.stream.scaladsl.{ Flow, Sink, Source } import akka.stream.scaladsl.{Flow, Sink, Source}
import akka.stream.{ Materializer, ThrottleMode } import akka.stream.{Materializer, ThrottleMode}
import akka.util.Timeout import akka.util.Timeout
import com.github.mgifos.workouts.GarminConnect._ import com.github.mgifos.workouts.GarminConnect._
import com.github.mgifos.workouts.model.WorkoutDef import com.github.mgifos.workouts.model.WorkoutDef
import com.typesafe.scalalogging.Logger import com.typesafe.scalalogging.Logger
import play.api.libs.json.{ JsObject, Json } import play.api.libs.json.{JsObject, Json}
import scala.collection.immutable.{ Map, Seq } import scala.collection.immutable.{Map, Seq}
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.{ ExecutionContext, Future } import scala.concurrent.{ExecutionContext, Future}
import scala.language.implicitConversions import scala.language.implicitConversions
import scala.util.Failure import scala.util.Failure
@ -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)
} }

@ -1,18 +1,18 @@
package com.github.mgifos.workouts package com.github.mgifos.workouts
import java.nio.file.{ Files, Paths } import java.nio.file.{Files, Paths}
import java.time.LocalDate import java.time.LocalDate
import akka.actor.ActorSystem import akka.actor.ActorSystem
import akka.http.scaladsl.Http import akka.http.scaladsl.Http
import akka.stream.ActorMaterializer import akka.stream.ActorMaterializer
import com.github.mgifos.workouts.model.{ WeeklyPlan, _ } import com.github.mgifos.workouts.model.{WeeklyPlan, _}
import com.typesafe.scalalogging.Logger import com.typesafe.scalalogging.Logger
import scopt.OptionParser import scopt.OptionParser
import scala.collection.immutable.Seq import scala.collection.immutable.Seq
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.{ Await, ExecutionContextExecutor, Future } import scala.concurrent.{Await, ExecutionContextExecutor, Future}
object Modes extends Enumeration { object Modes extends Enumeration {
type Mode = Value type Mode = Value
@ -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)

@ -1,44 +1,43 @@
package com.github.mgifos.workouts.model package com.github.mgifos.workouts.model
import com.github.mgifos.workouts.model.DistanceUnits.DistanceUnit import com.github.mgifos.workouts.model.DistanceUnits.DistanceUnit
import play.api.libs.json.{ JsNull, JsObject, Json } import play.api.libs.json.{JsNull, JsObject, Json}
sealed trait Duration { sealed trait Duration {
def json: JsObject def json: JsObject
} }
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")
} }
} }

@ -1,6 +1,6 @@
package com.github.mgifos.workouts.model package com.github.mgifos.workouts.model
import play.api.libs.json.{ JsNull, JsObject, JsValue, Json } import play.api.libs.json.{JsNull, JsObject, JsValue, Json}
trait Step { trait Step {
def `type`: String def `type`: String
@ -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")
} }
} }

@ -1,59 +1,59 @@
package com.github.mgifos.workouts.model package com.github.mgifos.workouts.model
import play.api.libs.json.{ JsNull, JsObject, Json } import play.api.libs.json.{JsNull, JsObject, Json}
sealed trait Target { sealed trait Target {
def json: JsObject def json: JsObject
} }
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

@ -1,6 +1,6 @@
package com.github.mgifos.workouts.model package com.github.mgifos.workouts.model
import play.api.libs.json.{ JsValue, Json } import play.api.libs.json.{JsValue, Json}
import Workout._ import Workout._
trait Workout { trait Workout {
@ -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