From 2fd790e87cb114144645c7fd9118cfdc154c9e9a Mon Sep 17 00:00:00 2001 From: mgifos Date: Sun, 18 Mar 2018 10:06:48 +0100 Subject: [PATCH] Json dump --- build.sbt | 1 + .../model/Duration.scala | 53 ++++++- .../model/Step.scala | 48 ++++-- .../model/Target.scala | 41 +++++- .../model/Workout.scala | 26 +++- src/test/resources/run-fast.json | 138 ++++++++++++++++++ .../mgifos/workouts/model/WorkoutSpec.scala | 32 ++-- 7 files changed, 306 insertions(+), 33 deletions(-) create mode 100644 src/test/resources/run-fast.json diff --git a/build.sbt b/build.sbt index 35c3574..8c0ff25 100644 --- a/build.sbt +++ b/build.sbt @@ -6,5 +6,6 @@ scalaVersion := "2.12.4" libraryDependencies ++= Seq( "com.github.tototoshi" %% "scala-csv" % "1.3.5", + "com.typesafe.play" %% "play-json" % "2.6.9", "org.scalatest" %% "scalatest" % "3.0.5" % "test" ) \ No newline at end of file diff --git a/src/main/scala/com.github.mgifos.workouts/model/Duration.scala b/src/main/scala/com.github.mgifos.workouts/model/Duration.scala index d3c5c8d..d9de7f4 100644 --- a/src/main/scala/com.github.mgifos.workouts/model/Duration.scala +++ b/src/main/scala/com.github.mgifos.workouts/model/Duration.scala @@ -1,17 +1,57 @@ package com.github.mgifos.workouts.model import com.github.mgifos.workouts.model.DistanceUnits.DistanceUnit +import play.api.libs.json.{ JsNull, JsObject, Json } -sealed trait Duration +sealed trait Duration { + 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( + "endCondition" -> Json.obj( + "conditionTypeKey" -> "distance", + "conditionTypeId" -> 3), + "preferredEndConditionUnit" -> Json.obj( + "unitKey" -> unit.fullName), + "endConditionValue" -> unit.toMeters(distance), + "endConditionCompare" -> JsNull, + "endConditionZone" -> JsNull) +} object DistanceUnits extends Enumeration { - type DistanceUnit = Value - val km, mi, m = Value + type DistanceUnit = DistVal + val km = Value("km", "kilometer", _ * 1000F) + val mi = Value("mi", "mile", _ * 1609.344F) + val m = Value("m", "meter", _ * 1F) + + class DistVal(name: String, val fullName: String, val toMeters: (Float) => Float) extends Val(nextId, name) + protected final def Value(name: String, fullName: String, toMeters: (Float) => Float): DistVal = new DistVal(name, fullName, toMeters) + + def named(name: String): DistVal = withName(name).asInstanceOf[DistVal] } -case class TimeDuration(hours: Int = 0, 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( + "endCondition" -> Json.obj( + "conditionTypeKey" -> "time", + "conditionTypeId" -> 2), + "preferredEndConditionUnit" -> JsNull, + "endConditionValue" -> (minutes * 60 + seconds), + "endConditionCompare" -> JsNull, + "endConditionZone" -> JsNull) +} + +object LapButtonPressed extends Duration { + override def json: JsObject = Json.obj( + "endCondition" -> Json.obj( + "conditionTypeKey" -> "lap.button", + "conditionTypeId" -> 1), + "preferredEndConditionUnit" -> JsNull, + "endConditionValue" -> JsNull, + "endConditionCompare" -> JsNull, + "endConditionZone" -> JsNull) +} object Duration { @@ -19,8 +59,9 @@ object Duration { private val MinutesRx = """^(\d{1,3}):(\d{2})$""".r def parse(x: String): Duration = x match { - case DistanceRx(quantity, _, unit) => DistanceDuration(quantity.toFloat, DistanceUnits.withName(unit)) + case DistanceRx(quantity, _, unit) => DistanceDuration(quantity.toFloat, DistanceUnits.named(unit)) case MinutesRx(minutes, seconds) => TimeDuration(minutes = minutes.toInt, seconds = seconds.toInt) + case "lap-button" => LapButtonPressed case _ => throw new IllegalArgumentException(s"Duration cannot be parsed $x") } } diff --git a/src/main/scala/com.github.mgifos.workouts/model/Step.scala b/src/main/scala/com.github.mgifos.workouts/model/Step.scala index 5448f44..c639d3b 100644 --- a/src/main/scala/com.github.mgifos.workouts/model/Step.scala +++ b/src/main/scala/com.github.mgifos.workouts/model/Step.scala @@ -1,30 +1,60 @@ package com.github.mgifos.workouts.model -sealed trait Step { +import play.api.libs.json.{ JsNull, JsObject, JsValue, Json } + +trait Step { def `type`: String + def typeId: Int + def json(order: Int): JsValue } -abstract class DurationStep(stepType: String, duration: Duration, target: Option[Target] = None) extends Step { - override def `type` = stepType +abstract class DurationStep( + override val `type`: String, + override val typeId: Int) extends Step { + def duration: Duration + def target: Option[Target] + + def json(order: Int): JsObject = Json.obj( + "type" -> "ExecutableStepDTO", + "stepId" -> JsNull, + "stepOrder" -> order, + "childStepId" -> JsNull, + "description" -> JsNull, + "stepType" -> Json.obj( + "stepTypeId" -> typeId, + "stepTypeKey" -> `type`)) ++ duration.json ++ target.fold(NoTarget.json)(_.json) } -case class WarmupStep(duration: Duration, target: Option[Target] = None) extends DurationStep("warmup", duration) +case class WarmupStep(duration: Duration, target: Option[Target] = None) extends DurationStep("warmup", 1) -case class RunStep(duration: Duration, target: Option[Target] = None) extends DurationStep("run", duration) +case class CooldownStep(duration: Duration, target: Option[Target] = None) extends DurationStep("cooldown", 2) -case class RecoverStep(duration: Duration, target: Option[Target] = None) extends DurationStep("recover", duration) +case class RunStep(duration: Duration, target: Option[Target] = None) extends DurationStep("interval", 3) -case class CooldownStep(duration: Duration, target: Option[Target] = None) extends DurationStep("cooldown", duration) +case class RecoverStep(duration: Duration, target: Option[Target] = None) extends DurationStep("recovery", 4) -case class RepeatStep(count: Int, definition: Seq[DurationStep]) extends Step { +case class RepeatStep(count: Int, steps: Seq[Step]) extends Step { override def `type` = "repeat" + override def typeId = 6 + + override def json(order: Int) = Json.obj( + "stepId" -> JsNull, + "stepOrder" -> 2, + "stepType" -> Json.obj( + "stepTypeId" -> 6, + "stepTypeKey" -> "repeat"), + "numberOfIterations" -> count, + "smartRepeat" -> false, + "childStepId" -> 1, + "workoutSteps" -> steps.zipWithIndex.map { case (s, i) => s.json(i + 1) }, + "type" -> "RepeatGroupDTO") } object Step { private val StepRx = """^(-\s\w*:\s.*)((\n\s{1,}-\s.*)*)$""".r private val StepHeader = """^\s*-\s*(\w*):(.*)$""".r - private val ParamsRx = """^([\w\.:\s]+)\s*(@(.*))?$""".r + private val ParamsRx = """^([\w-\.:\s]+)\s*(@(.*))?$""".r def parse(x: String): Step = x match { case StepRx(header, subSteps, _) if subSteps.nonEmpty => header match { diff --git a/src/main/scala/com.github.mgifos.workouts/model/Target.scala b/src/main/scala/com.github.mgifos.workouts/model/Target.scala index 50f7f30..837176d 100644 --- a/src/main/scala/com.github.mgifos.workouts/model/Target.scala +++ b/src/main/scala/com.github.mgifos.workouts/model/Target.scala @@ -1,13 +1,48 @@ package com.github.mgifos.workouts.model -sealed trait Target +import play.api.libs.json.{ JsNull, JsObject, Json } -case class HrZoneTarget(zone: Int) extends Target -case class PaceTarget(from: Pace, to: Pace) extends Target +sealed trait Target { + def json: JsObject +} + +case class HrZoneTarget(zone: Int) extends Target { + override def json = Json.obj( + "targetType" -> Json.obj( + "workoutTargetTypeId" -> 4, + "workoutTargetTypeKey" -> "heart.rate.zone"), + "targetValueOne" -> "", + "targetValueTwo" -> "", + "zoneNumber" -> zone.toString) +} +case class PaceTarget(from: Pace, to: Pace) extends Target { + override def json = Json.obj( + "targetType" -> Json.obj( + "workoutTargetTypeId" -> 6, + "workoutTargetTypeKey" -> "pace.zone"), + "targetValueOne" -> from.speed, + "targetValueTwo" -> to.speed, + "zoneNumber" -> JsNull) +} + +object NoTarget extends Target { + override def json = Json.obj( + "targetType" -> Json.obj( + "workoutTargetTypeId" -> 1, + "workoutTargetTypeKey" -> "no.target"), + "targetValueOne" -> JsNull, + "targetValueTwo" -> JsNull, + "zoneNumber" -> JsNull) +} case class Pace(exp: String) { def minutes: Int = exp.trim.takeWhile(_ != ':').toInt def seconds: Int = exp.trim.split(":").last.toInt + + /** + * @return Speed in m/s + */ + def speed: Double = 1000D / (minutes * 60 + seconds) } object Target { diff --git a/src/main/scala/com.github.mgifos.workouts/model/Workout.scala b/src/main/scala/com.github.mgifos.workouts/model/Workout.scala index 2870385..48461ea 100644 --- a/src/main/scala/com.github.mgifos.workouts/model/Workout.scala +++ b/src/main/scala/com.github.mgifos.workouts/model/Workout.scala @@ -1,15 +1,35 @@ package com.github.mgifos.workouts.model -trait Workout +import play.api.libs.json.{ JsValue, Json } + +trait Workout { + def json: JsValue +} case class WorkoutDef(name: String, steps: Seq[Step] = Nil) extends Workout { def toRef: WorkoutRef = WorkoutRef(name) def withStep(step: Step): WorkoutDef = WorkoutDef(name, steps :+ step) + def json: JsValue = Json.obj( + "sportType" -> Json.obj( + "sportTypeId" -> 1, + "sportTypeKey" -> "running"), + "workoutName" -> name, + "workoutSegments" -> Json.arr( + Json.obj( + "segmentOrder" -> 1, + "sportType" -> Json.obj( + "sportTypeId" -> 1, + "sportTypeKey" -> "running"), + "workoutSteps" -> steps.zipWithIndex.map { case (s, i) => s.json(i + 1) }))) } -case class WorkoutRef(name: String) extends Workout +case class WorkoutRef(name: String) extends Workout { + def json: JsValue = Json.obj() +} -case class WorkoutNote(note: String) extends Workout +case class WorkoutNote(note: String) extends Workout { + def json: JsValue = Json.obj() +} object Workout { diff --git a/src/test/resources/run-fast.json b/src/test/resources/run-fast.json new file mode 100644 index 0000000..856acca --- /dev/null +++ b/src/test/resources/run-fast.json @@ -0,0 +1,138 @@ +{ + "sportType": { + "sportTypeId": 1, + "sportTypeKey": "running" + }, + "workoutName": "run-fast", + "workoutSegments": [ + { + "segmentOrder": 1, + "sportType": { + "sportTypeId": 1, + "sportTypeKey": "running" + }, + "workoutSteps": [ + { + "type": "ExecutableStepDTO", + "stepId": null, + "stepOrder": 1, + "childStepId": null, + "description": null, + "stepType": { + "stepTypeId": 1, + "stepTypeKey": "warmup" + }, + "endCondition": { + "conditionTypeKey": "time", + "conditionTypeId": 2 + }, + "preferredEndConditionUnit": null, + "endConditionValue": 600, + "endConditionCompare": null, + "endConditionZone": null, + "targetType": { + "workoutTargetTypeId": 1, + "workoutTargetTypeKey": "no.target" + }, + "targetValueOne": null, + "targetValueTwo": null, + "zoneNumber": null + }, + { + "stepId": null, + "stepOrder": 2, + "stepType": { + "stepTypeId": 6, + "stepTypeKey": "repeat" + }, + "numberOfIterations": 2, + "smartRepeat": false, + "childStepId": 1, + "workoutSteps": [ + { + "type": "ExecutableStepDTO", + "stepId": null, + "stepOrder": 1, + "childStepId": null, + "description": null, + "stepType": { + "stepTypeId": 3, + "stepTypeKey": "interval" + }, + "endCondition": { + "conditionTypeKey": "distance", + "conditionTypeId": 3 + }, + "preferredEndConditionUnit": { + "unitKey": "meter" + }, + "endConditionValue": 1500, + "endConditionCompare": null, + "endConditionZone": null, + "targetType": { + "workoutTargetTypeId": 6, + "workoutTargetTypeKey": "pace.zone" + }, + "targetValueOne": 3.7037037037037037, + "targetValueTwo": 3.3333333333333335, + "zoneNumber": null + }, + { + "type": "ExecutableStepDTO", + "stepId": null, + "stepOrder": 2, + "childStepId": null, + "description": null, + "stepType": { + "stepTypeId": 4, + "stepTypeKey": "recovery" + }, + "endCondition": { + "conditionTypeKey": "time", + "conditionTypeId": 2 + }, + "preferredEndConditionUnit": null, + "endConditionValue": 90, + "endConditionCompare": null, + "endConditionZone": null, + "targetType": { + "workoutTargetTypeId": 4, + "workoutTargetTypeKey": "heart.rate.zone" + }, + "targetValueOne": "", + "targetValueTwo": "", + "zoneNumber": "2" + } + ], + "type": "RepeatGroupDTO" + }, + { + "type": "ExecutableStepDTO", + "stepId": null, + "stepOrder": 3, + "childStepId": null, + "description": null, + "stepType": { + "stepTypeId": 2, + "stepTypeKey": "cooldown" + }, + "endCondition": { + "conditionTypeKey": "lap.button", + "conditionTypeId": 1 + }, + "preferredEndConditionUnit": null, + "endConditionValue": null, + "endConditionCompare": null, + "endConditionZone": null, + "targetType": { + "workoutTargetTypeId": 1, + "workoutTargetTypeKey": "no.target" + }, + "targetValueOne": null, + "targetValueTwo": null, + "zoneNumber": null + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala b/src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala index ce4cf75..d0e6df0 100644 --- a/src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala +++ b/src/test/scala/com/github/mgifos/workouts/model/WorkoutSpec.scala @@ -2,9 +2,20 @@ package com.github.mgifos.workouts.model import org.scalatest.{ FlatSpec, Matchers } import com.github.mgifos.workouts.model.DistanceUnits._ +import play.api.libs.json.Json class WorkoutSpec extends FlatSpec with Matchers { + /* + workout: run-fast + - warmup: 10:00 + - repeat: 2 + - run: 1500m @ 4:30-5:00 + - recover: 01:30 @ z2 + - cooldown: lap-button + */ + val testWO = "workout: run-fast\n- warmup: 10:00\n- repeat: 2\n - run: 1500m @ 4:30-5:00\n - recover: 01:30 @ z2\n- cooldown: lap-button" + "Workout" should "parse correctly" in { Workout.parseDef("") should be('left) Workout.parseDef("workout") should be('left) @@ -13,22 +24,19 @@ class WorkoutSpec extends FlatSpec with Matchers { Workout.parseDef("workout run-fast") should be('left) Workout.parseDef(" workout: run-fast") should be('left) - /* - workout: run-fast - - warmup: 10:00 - - repeat: 2 - - run: 1500m @ z4 - - recover: 01:30 @ z2 - - cooldown: 5:00 - */ - val testWO = "workout: run-fast\n- warmup: 10:00\n- repeat: 2\n - run: 1500m @ z4\n - recover: 01:30 @ z2\n- cooldown: 5:00" Workout.parseDef(testWO) should be( Right( WorkoutDef("run-fast", Seq( WarmupStep(TimeDuration(minutes = 10)), RepeatStep(2, Seq( - RunStep(DistanceDuration(1500, m), Some(HrZoneTarget(4))), - RecoverStep(TimeDuration(0, 1, 30), Some(HrZoneTarget(2))))), - CooldownStep(TimeDuration(minutes = 5)))))) + RunStep(DistanceDuration(1500, m), Some(PaceTarget(Pace("4:30"), Pace("5:00")))), + RecoverStep(TimeDuration(1, 30), Some(HrZoneTarget(2))))), + CooldownStep(LapButtonPressed))))) + } + + "Workout" should "dump json correctly" in { + val is = getClass.getClassLoader.getResourceAsStream("run-fast.json") + val expectJson = Json.parse(is) + Workout.parseDef(testWO).map(_.json) should be(Right(expectJson)) } }