Skip to content

Commit d9df9a5

Browse files
committed
Data generators for comingBackMode service
1 parent 41e250b commit d9df9a5

File tree

7 files changed

+438
-0
lines changed

7 files changed

+438
-0
lines changed

README.md

+265
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
- [Server](#server-2)
2323
- [Client](#client-2)
2424
- [Result](#result-1)
25+
- [Bidirectional streaming RPC service: `comingBackMode`](#bidirectional-streaming-rpc-service-comingbackmode)
26+
- [Protocol](#protocol-4)
27+
- [Server](#server-3)
28+
- [Client](#client-3)
29+
- [Result](#result-2)
2530

2631
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
2732

@@ -641,6 +646,266 @@ INFO - * New Temperature 👍 --> Temperature(76.42,TemperatureUnit(Fahrenheit
641646
INFO - * New Temperature 👍 --> Temperature(75.95,TemperatureUnit(Fahrenheit))
642647
```
643648

649+
## Bidirectional streaming RPC service: `comingBackMode`
650+
651+
To illustrate the bidirectional streaming, we are going to build a new service that makes the server react to real-time info provided by the client. In this case, as we said above, the client (the mobile app) will emit a stream of coordinates (latitude and longitude), and the server (the smart home) will trigger some actions according to the distance.
652+
653+
### Protocol
654+
655+
So let's add this service to the protocol.
656+
657+
**_Messages.scala_**
658+
659+
Adding new models:
660+
661+
```scala
662+
case class Point(lat: Double, long: Double)
663+
case class Location(currentLocation: Point, destination: Point, distanceToDestination: Double)
664+
case class SmartHomeAction(description: String, isDone: Boolean)
665+
@message
666+
final case class ComingBackModeResponse(actions: List[SmartHomeAction])
667+
```
668+
669+
**_SmartHomeService.scala_**
670+
671+
And the `comingBackMode` operation:
672+
673+
```scala
674+
@service(Protobuf) trait SmartHomeService[F[_]] {
675+
676+
def isEmpty(request: IsEmptyRequest): F[IsEmptyResponse]
677+
678+
def getTemperature(empty: Empty.type): Stream[F, Temperature]
679+
680+
def comingBackMode(request: Stream[F, Location]): Stream[F, ComingBackModeResponse]
681+
}
682+
```
683+
684+
### Server
685+
686+
So there is a new function to be implemented in the interpreter:
687+
688+
```scala
689+
override def comingBackMode(request: Stream[F, Location]): Stream[F, ComingBackModeResponse] =
690+
for {
691+
_ <- Stream.eval(Logger[F].info(s"$serviceName - Enabling Coming Back Home mode"))
692+
location <- request
693+
_ <- Stream.eval(
694+
if (location.distanceToDestination > 0.0d) Logger[F].info(s"$serviceName - Distance to destination: ${location.distanceToDestination} mi")
695+
else Logger[F].info(s"$serviceName - You have reached your destination 🏡"))
696+
response <- Stream.eval(SmartHomeSupervisor[F].performAction(location))
697+
} yield response
698+
```
699+
700+
### Client
701+
702+
Again, if the client will emit a stream of locations, we should develop a producer, which as been created at `LocationGenerators`. No big deal, so far. But we have to add this operation in the `SmartHomeServiceApi`:
703+
704+
```scala
705+
trait SmartHomeServiceApi[F[_]] {
706+
def isEmpty(): F[Boolean]
707+
def getTemperature(): Stream[F, Temperature]
708+
def comingBackMode(locations: Stream[F, Location]): F[Boolean]
709+
}
710+
```
711+
712+
Whose interpretation could be:
713+
714+
```scala
715+
def comingBackMode(locations: Stream[F, Location]): Stream[F, ComingBackModeResponse] = for {
716+
client <- Stream.eval(clientF)
717+
response <- client.comingBackMode(locations)
718+
} yield response
719+
```
720+
721+
Now, we have all the ingredients to proceed in the ClientApp:
722+
723+
```scala
724+
for {
725+
serviceApi <- SmartHomeServiceApi.createInstance(config.host.value, config.port.value)
726+
_ <- Stream.eval(serviceApi.isEmpty)
727+
summary <- serviceApi.getTemperature
728+
_ <- Stream.eval(Logger[F].info(s"The average temperature is: ${summary.averageTemperature}"))
729+
response <- serviceApi.comingBackMode(LocationsGenerator.get[F])
730+
} yield response.actions
731+
```
732+
733+
### Result
734+
735+
When we run the client now with `sbt runClient` we get:
736+
737+
```bash
738+
INFO - 👀 - Waiting for a new location...
739+
740+
INFO - 👀 - Waiting for a new location...
741+
742+
INFO - 👀 - Waiting for a new location...
743+
744+
INFO - 👀 - Waiting for a new location...
745+
746+
INFO - 👮 - Enable security cameras
747+
748+
INFO - 👀 - Waiting for a new location...
749+
750+
INFO - 👀 - Waiting for a new location...
751+
752+
INFO - 👀 - Waiting for a new location...
753+
754+
INFO - 👀 - Waiting for a new location...
755+
756+
INFO - 👀 - Waiting for a new location...
757+
758+
INFO - 👀 - Waiting for a new location...
759+
760+
INFO - 💦 - Disable irrigation system
761+
762+
INFO - 🔌 - Send Rumba to the charging dock
763+
764+
INFO - 🛋 - Start heating the living room
765+
766+
INFO - 👀 - Waiting for a new location...
767+
768+
INFO - 👀 - Waiting for a new location...
769+
770+
INFO - 👀 - Waiting for a new location...
771+
772+
INFO - 👀 - Waiting for a new location...
773+
774+
INFO - 👀 - Waiting for a new location...
775+
776+
INFO - 🔥 - Fireplace in ambient mode
777+
778+
INFO - 🗞 - Get news summary
779+
780+
INFO - 👀 - Waiting for a new location...
781+
782+
INFO - 👀 - Waiting for a new location...
783+
784+
INFO - 👀 - Waiting for a new location...
785+
786+
INFO - 👀 - Waiting for a new location...
787+
788+
INFO - 👀 - Waiting for a new location...
789+
790+
INFO - 👀 - Waiting for a new location...
791+
792+
INFO - 💧 - Increase the power of the hot water heater
793+
794+
INFO - 🛁 - Turn the towel heaters on
795+
796+
INFO - 👀 - Waiting for a new location...
797+
798+
INFO - 👀 - Waiting for a new location...
799+
800+
INFO - 👀 - Waiting for a new location...
801+
802+
INFO - 👀 - Waiting for a new location...
803+
804+
INFO - 👀 - Waiting for a new location...
805+
806+
INFO - 👀 - Waiting for a new location...
807+
808+
INFO - 😎 - Low the blinds
809+
810+
INFO - 💡 - Turn on the lights
811+
812+
INFO - 👀 - Waiting for a new location...
813+
814+
INFO - 👀 - Waiting for a new location...
815+
816+
INFO - 👀 - Waiting for a new location...
817+
818+
INFO - 👀 - Waiting for a new location...
819+
820+
INFO - 👀 - Waiting for a new location...
821+
822+
INFO - 👀 - Waiting for a new location...
823+
824+
INFO - 👩 - Connect Alexa
825+
826+
INFO - 📺 - Turn on the TV
827+
828+
INFO - 👀 - Waiting for a new location...
829+
830+
INFO - 👀 - Waiting for a new location...
831+
832+
INFO - 👀 - Waiting for a new location...
833+
834+
INFO - 👀 - Waiting for a new location...
835+
836+
INFO - 👀 - Waiting for a new location...
837+
838+
INFO - 👀 - Waiting for a new location...
839+
840+
INFO - 🔦 - Turn exterior lights on
841+
842+
INFO - 👀 - Waiting for a new location...
843+
844+
INFO - 👀 - Waiting for a new location...
845+
846+
INFO - 👀 - Waiting for a new location...
847+
848+
INFO - 🚪 - Unlock doors
849+
850+
INFO - Removed 1 RPC clients from cache.
851+
```
852+
853+
And the server log the request as expected:
854+
855+
```bash
856+
INFO - SmartHomeService - Enabling Coming Back Home mode
857+
INFO - SmartHomeService - Distance to destination: 6.39 mi
858+
INFO - SmartHomeService - Distance to destination: 6.26 mi
859+
INFO - SmartHomeService - Distance to destination: 6.13 mi
860+
INFO - SmartHomeService - Distance to destination: 6.0 mi
861+
INFO - SmartHomeService - Distance to destination: 5.87 mi
862+
INFO - SmartHomeService - Distance to destination: 5.74 mi
863+
INFO - SmartHomeService - Distance to destination: 5.61 mi
864+
INFO - SmartHomeService - Distance to destination: 5.48 mi
865+
INFO - SmartHomeService - Distance to destination: 5.35 mi
866+
INFO - SmartHomeService - Distance to destination: 5.22 mi
867+
INFO - SmartHomeService - Distance to destination: 5.09 mi
868+
INFO - SmartHomeService - Distance to destination: 4.96 mi
869+
INFO - SmartHomeService - Distance to destination: 4.83 mi
870+
INFO - SmartHomeService - Distance to destination: 4.7 mi
871+
INFO - SmartHomeService - Distance to destination: 4.57 mi
872+
INFO - SmartHomeService - Distance to destination: 4.44 mi
873+
INFO - SmartHomeService - Distance to destination: 4.31 mi
874+
INFO - SmartHomeService - Distance to destination: 4.18 mi
875+
INFO - SmartHomeService - Distance to destination: 4.05 mi
876+
INFO - SmartHomeService - Distance to destination: 3.91 mi
877+
INFO - SmartHomeService - Distance to destination: 3.78 mi
878+
INFO - SmartHomeService - Distance to destination: 3.65 mi
879+
INFO - SmartHomeService - Distance to destination: 3.52 mi
880+
INFO - SmartHomeService - Distance to destination: 3.39 mi
881+
INFO - SmartHomeService - Distance to destination: 3.26 mi
882+
INFO - SmartHomeService - Distance to destination: 3.13 mi
883+
INFO - SmartHomeService - Distance to destination: 3.0 mi
884+
INFO - SmartHomeService - Distance to destination: 2.87 mi
885+
INFO - SmartHomeService - Distance to destination: 2.74 mi
886+
INFO - SmartHomeService - Distance to destination: 2.61 mi
887+
INFO - SmartHomeService - Distance to destination: 2.48 mi
888+
INFO - SmartHomeService - Distance to destination: 2.35 mi
889+
INFO - SmartHomeService - Distance to destination: 2.22 mi
890+
INFO - SmartHomeService - Distance to destination: 2.09 mi
891+
INFO - SmartHomeService - Distance to destination: 1.96 mi
892+
INFO - SmartHomeService - Distance to destination: 1.83 mi
893+
INFO - SmartHomeService - Distance to destination: 1.7 mi
894+
INFO - SmartHomeService - Distance to destination: 1.57 mi
895+
INFO - SmartHomeService - Distance to destination: 1.44 mi
896+
INFO - SmartHomeService - Distance to destination: 1.3 mi
897+
INFO - SmartHomeService - Distance to destination: 1.17 mi
898+
INFO - SmartHomeService - Distance to destination: 1.04 mi
899+
INFO - SmartHomeService - Distance to destination: 0.91 mi
900+
INFO - SmartHomeService - Distance to destination: 0.78 mi
901+
INFO - SmartHomeService - Distance to destination: 0.65 mi
902+
INFO - SmartHomeService - Distance to destination: 0.52 mi
903+
INFO - SmartHomeService - Distance to destination: 0.39 mi
904+
INFO - SmartHomeService - Distance to destination: 0.26 mi
905+
INFO - SmartHomeService - Distance to destination: 0.13 mi
906+
INFO - SmartHomeService - You have reached your destination 🏡
907+
```
908+
644909
<!-- DOCTOC SKIP -->
645910
# Copyright
646911

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.fortyseven.client
2+
3+
import cats.effect.{Async, Timer}
4+
import cats.syntax.flatMap._
5+
import com.fortyseven.protocol.{Location, Point}
6+
import fs2.Stream
7+
import io.chrisdavenport.log4cats.Logger
8+
9+
import scala.concurrent.duration._
10+
import scala.math.BigDecimal.RoundingMode
11+
import scala.util.Random
12+
13+
trait GeoCalculator {
14+
private val AVERAGE_RADIUS_OF_EARTH_KM: Double = 6371d
15+
private val AVERAGE_RADIUS_OF_EARTH_MILES: Double = 3959d
16+
17+
private[this] def calculateDistance(startingPoint: Point, destination: Point): Double = {
18+
val latDistance = Math.toRadians(startingPoint.lat - destination.lat)
19+
val lngDistance = Math.toRadians(startingPoint.long - destination.long)
20+
val sinLat = Math.sin(latDistance / 2)
21+
val sinLng = Math.sin(lngDistance / 2)
22+
val a = sinLat * sinLat +
23+
(Math.cos(Math.toRadians(startingPoint.lat)) *
24+
Math.cos(Math.toRadians(destination.lat)) *
25+
sinLng * sinLng)
26+
2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
27+
}
28+
29+
def calculateDistanceInKilometer(startingPoint: Point, destination: Point): Double =
30+
BigDecimal(calculateDistance(startingPoint, destination) * AVERAGE_RADIUS_OF_EARTH_KM)
31+
.setScale(2, RoundingMode.HALF_UP)
32+
.toDouble
33+
34+
def calculateDistanceInMiles(startingPoint: Point, destination: Point): Double =
35+
BigDecimal(calculateDistance(startingPoint, destination) * AVERAGE_RADIUS_OF_EARTH_MILES)
36+
.setScale(2, RoundingMode.HALF_UP)
37+
.toDouble
38+
}
39+
40+
object LocationsGenerator extends GeoCalculator {
41+
val startingPoint = Point(47.582678d, -122.334617d)
42+
val destination = Point(47.616187d, -122.203689d)
43+
val startingLocation = Location(
44+
startingPoint,
45+
destination,
46+
calculateDistanceInMiles(startingPoint, destination)
47+
)
48+
49+
def get[F[_]: Async: Logger: Timer]: Stream[F, Location] =
50+
Stream
51+
.iterateEval(startingLocation)(location => nextLocation(location))
52+
.takeWhile(location =>
53+
destination.lat - location.currentLocation.lat > 0.0001d && destination.long - location.currentLocation.long > 0.0001d)
54+
.append(Stream.emit(Location(destination, destination, 0d)).covary[F])
55+
56+
def nextLocation[F[_]: Async: Timer](location: Location): F[Location] =
57+
Timer[F]
58+
.sleep(1.seconds)
59+
.flatMap(_ =>
60+
Async[F].delay {
61+
val nextPoint =
62+
Point(
63+
closeLocation(location.currentLocation.lat, 0.00067018d),
64+
closeLocation(location.currentLocation.long, 0.00261856d))
65+
Location(nextPoint, destination, calculateDistanceInMiles(nextPoint, destination))
66+
})
67+
68+
def closeLocation(point: Double, delta: Double): Double = {
69+
val increment: Double = Random.nextDouble() / 1000000d
70+
val signal = if (Random.nextBoolean()) 1 else -1
71+
point + delta + signal * increment
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.fortyseven.commons
2+
3+
import cats.{Show, Traverse}
4+
import cats.effect.Sync
5+
import cats.instances.list._
6+
import cats.syntax.show._
7+
import fs2.Sink
8+
import io.chrisdavenport.log4cats.Logger
9+
10+
class LogSink[F[_]: Sync: Logger] {
11+
def showLine[I: Show]: Sink[F, I] = Sink[F, I](item => Logger[F].info(item.show))
12+
13+
def showLines[I: Show]: Sink[F, List[I]] = Sink[F, List[I]] { items =>
14+
Traverse[List].traverse_(items)(item => Logger[F].info(item.show))
15+
}
16+
}
17+
18+
object LogSink {
19+
implicit def instance[F[_]: Sync: Logger]: LogSink[F] = new LogSink[F]
20+
21+
def apply[F[_]](implicit ev: LogSink[F]): LogSink[F] = ev
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.fortyseven.protocol
2+
3+
import cats.Show
4+
5+
object implicits {
6+
implicit val catsShowInstanceForSmartHomeAction: Show[SmartHomeAction] =
7+
new Show[SmartHomeAction] {
8+
override def show(action: SmartHomeAction): String = s"${action.value}\n"
9+
}
10+
}

protocol/src/main/scala/protocol/Messages.scala

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ case class TemperatureUnit(value: String) extends AnyVal
66

77
case class Temperature(value: Double, unit: TemperatureUnit)
88

9+
case class Point(lat: Double, long: Double)
10+
11+
case class Location(currentLocation: Point, destination: Point, distanceToDestination: Double)
12+
13+
case class SmartHomeAction(value: String) extends AnyVal
14+
915
@message
1016
final case class IsEmptyRequest()
1117

0 commit comments

Comments
 (0)