Skip to content

Commit

Permalink
Ports to Scala 3 (#4)
Browse files Browse the repository at this point in the history
Also tidies up, improves the README.
  • Loading branch information
jejking-tw authored Sep 8, 2023
1 parent 83447fc commit b4542c0
Show file tree
Hide file tree
Showing 20 changed files with 159 additions and 92 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -220,4 +220,6 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk

# End of https://www.gitignore.io/api/sbt,scala,linux,macos,windows,intellij,intellij+all
# End of https://www.gitignore.io/api/sbt,scala,linux,macos,windows,intellij,intellij+all

.bsp/
33 changes: 17 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Welcome to PowerDale

PowerDale is a small town with around 100 residents. Most houses have a smartmeter installed that can save and send information
about how much energy a house has used.
PowerDale is a small town with around 100 residents. Most houses have a smart meter installed that can save and send information
about how much energy a house is using at a given point in time.

There are three major providers of energy in town that charge different amounts for the power they supply.

Expand All @@ -12,7 +12,7 @@ There are three major providers of energy in town that charge different amounts
# Introducing JOI Energy

JOI Energy is a new startup in the energy industry.
Rather than selling energy they want to differentiate themselves from the market by recording their customers' energy usage from their smartmeters and
Rather than selling energy they want to differentiate themselves from the market by recording their customers' energy usage from their smart meters and
recommending the best suppler to meet their needs.

You have been placed into their development team, whose current goal is to produce an API which their customers and smart meters will interact with.
Expand Down Expand Up @@ -49,7 +49,7 @@ To trial the new JOI software 5 people from the JOI accounts team have agreed to
## Overview

JOI Energy is a new energy company that uses data to ensure customers are
able to be on the best pricePlan for their energy consumption.
able to be on the best price plan for their energy consumption.

## API

Expand All @@ -70,14 +70,14 @@ POST
{
"smartMeterId": <smartMeterId>,
"electricityReadings": [
{ "time": <timestamp>, "reading": <reading> },
{ "time": "2019-01-24T18:11:27.142Z", "reading": <reading> },
...
]
}
```

`timestamp`: Unix timestamp, e.g. `1504777098`
`reading`: kW reading of meter at that time, e.g. `0.0503`
`timestamp`: Timestamp in ISO Format
`reading`: kW reading of meter at that time as a number, e.g. `0.0503`

### Get Stored Readings

Expand All @@ -94,8 +94,8 @@ GET

```json
[
{ "time": "2017-09-07T10:37:52.362Z", "reading": 1.3524882598124337 },
...
{ "time": "2017-09-07T10:37:52.362Z", "reading": 1.3524882598124337 },
...
]
```

Expand Down Expand Up @@ -148,14 +148,13 @@ GET

## Requirements

- [Java 13](https://www.oracle.com/technetwork/java/javase/downloads/jdk13-downloads-5672538.html)
- [Sbt 1.3.7+](https://www.scala-sbt.org/)
The project is written in Scala 3. We recommend using [Java 17](https://adoptium.net/en-GB/) or higher.

## Build
The build system is SBT - we are using the latest version, currently 1.9.3.

```console
$ sbt build
```
We use Akka HTTP in this implementation.

## Build

## Test

Expand All @@ -166,5 +165,7 @@ $ sbt test
## Run

```console
$ sbt run # available at localhost:8080 by default
$ sbt run
```

The application starts on `localhost:8080` by default.
47 changes: 28 additions & 19 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
inThisBuild(
List(
name := "joi-energy-scala",
version := "0.1",
scalaVersion := "2.13.10",
Compile / mainClass := Some("com.tw.energy.WebApp"),
run / mainClass := Some("com.tw.energy.WebApp"),
),
)
scalaVersion := "3.3.0"
))


val circeVersion = "0.14.5"
val akkaVersion = "2.6.20"
val akkaHttpVersion = "10.2.10"
val akkaVersion = "2.8.3"
val akkaHttpVersion = "10.5.2"
val akkaHttpCirceVersion = "1.39.2"
val scalaTestVersion = "3.2.15"
val scalaTestVersion = "3.2.16"

libraryDependencies += "com.typesafe.akka" %% "akka-http" % akkaHttpVersion // Http Server library: https://doc.akka.io/docs/akka-http/current/server-side/index.html
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % akkaVersion // Required by akka-http
lazy val circeCore = "io.circe" %% "circe-core" % circeVersion
lazy val circeGeneric = "io.circe" %% "circe-generic" % circeVersion
lazy val circeParser = "io.circe" %% "circe-parser" % circeVersion
lazy val akkaHttp = "com.typesafe.akka" %% "akka-http" % akkaHttpVersion // Http Server library: https://doc.akka.io/docs/akka-http/current/server-side/index.html
lazy val akkaStream = "com.typesafe.akka" %% "akka-stream" % akkaVersion // Required by akka-http
// lazy val akkaHttpCirce = "de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirceVersion

//json library: https://circe.github.io/circe/
libraryDependencies += "io.circe" %% "circe-core" % circeVersion
libraryDependencies += "io.circe" %% "circe-generic" % circeVersion
libraryDependencies += "io.circe" %% "circe-parser" % circeVersion
libraryDependencies += "de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirceVersion
lazy val scalaTest = "org.scalatest" %% "scalatest" % scalaTestVersion % Test // Test framework: http://www.scalatest.org/
lazy val akkaHttpTestKit = "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % Test // utilities to test routes: https://doc.akka.io/docs/akka-http/current/routing-dsl/testkit.html
lazy val akkaTestKit = "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test // required by akka-http-testkit

libraryDependencies += "org.scalatest" %% "scalatest" % scalaTestVersion % Test //Test framework: http://www.scalatest.org/
libraryDependencies += "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % Test //utilities to test routes: https://doc.akka.io/docs/akka-http/current/routing-dsl/testkit.html
libraryDependencies += "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test //required by akka-http-testkit
libraryDependencies ++= List(
akkaHttp,
akkaStream,
akkaTestKit,
akkaHttpTestKit,
circeCore,
circeGeneric,
circeParser,
scalaTest
)

scalacOptions ++= Seq("-deprecation", "-feature")

//conflictWarning := ConflictWarning.default
conflictManager := ConflictManager.latestRevision
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version = 1.8.2
sbt.version = 1.9.3
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package com.tw.energy

import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import com.tw.energy.controller.{MeterReadingController, PricePlanComparatorController}
import com.tw.energy.service.{AccountService, MeterReadingService, PricePlanService}
import com.tw.energy.controller.MeterReadingController
import com.tw.energy.controller.PricePlanComparatorController
import com.tw.energy.service.AccountService
import com.tw.energy.service.MeterReadingService
import com.tw.energy.service.PricePlanService


class JOIEnergyApplication {
Expand Down
3 changes: 2 additions & 1 deletion src/main/scala/com/tw/energy/Configuration.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.tw.energy

import com.tw.energy.domain.{ElectricityReading, PricePlan}
import com.tw.energy.domain.ElectricityReading
import com.tw.energy.domain.PricePlan
import com.tw.energy.generator.Generator

object Configuration {
Expand Down
5 changes: 3 additions & 2 deletions src/main/scala/com/tw/energy/WebServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Route

import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.io.StdIn

object WebServer

class WebServer(val route: Route, val host: String = "localhost", val port: Int = 8080) {
implicit val system: ActorSystem = ActorSystem("joi-energy-system")
implicit val system: ActorSystem = ActorSystem("joy-of-energy-system")
implicit val executionContext: ExecutionContext = system.dispatcher

def start(): RunningServer = {
Expand Down
42 changes: 39 additions & 3 deletions src/main/scala/com/tw/energy/controller/JsonSupport.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
package com.tw.energy.controller

import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport
import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller}
import akka.http.scaladsl.model.{HttpCharsets, MediaTypes}
import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller}
import akka.http.scaladsl.util.FastFuture
import akka.util.ByteString
import io.circe.{Decoder, Encoder, Error}
import io.circe.syntax.*
import io.circe.parser.decode

trait JsonSupport extends FailFastCirceSupport {
}
import java.nio.charset.StandardCharsets

/**
* Rudimentary support for handling json marshalling and unmarshalling JSON using Circe. To use,
* ensure that `Encoder` and `Decoder` implicits are available for the type you want to handle. For example,
* by `import io.circe.generic.auto.*` to gain automatic encoder and decoder generation for case classes.
*
* Inspired by Heiko Seeberger's code at https://github.com/hseeberger/akka-http-json/tree/master/akka-http-circe/src - but
* which doesn't yet support Scala 3 out of the box.
*/
trait JsonSupport {

implicit def jsonToEntityMarshaller[A](implicit encoder: Encoder[A]): ToEntityMarshaller[A] =
Marshaller.StringMarshaller.wrap(MediaTypes.`application/json`)((a:A) => a.asJson.noSpaces)

implicit def jsonFromEntityUnmarshaller[A](implicit decoder: Decoder[A]): FromEntityUnmarshaller[A] = {
Unmarshaller
.byteStringUnmarshaller
.forContentTypes(MediaTypes.`application/json`)
.andThen(stringViaDecoderUnmarshaller)
}

private def stringViaDecoderUnmarshaller[A](implicit decoder: Decoder[A]): Unmarshaller[ByteString, A] =
Unmarshaller.withMaterializer[ByteString, A](_ => _ => { bs =>
decode[A](bs.decodeString(StandardCharsets.UTF_8)).fold(
(error: Error) => FastFuture.failed(error),
(a: A) => FastFuture.successful(a)
)
})

}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package com.tw.energy.controller

import akka.http.scaladsl.marshalling.ToResponseMarshallable
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives.{complete, get, path, _}
import akka.http.scaladsl.marshalling.{ToEntityMarshaller, ToResponseMarshallable, ToResponseMarshaller}
import akka.http.scaladsl.model.*
import akka.http.scaladsl.server.Directives.*
import akka.http.scaladsl.server.PathMatchers.Segment
import akka.http.scaladsl.server.Route
import com.tw.energy.domain.MeterReadings
import com.tw.energy.domain.{ElectricityReading, MeterReadings}
import com.tw.energy.domain.StringTypes.SmartMeterId
import com.tw.energy.service.MeterReadingService
import io.circe.generic.auto._

import io.circe.generic.auto.*

class MeterReadingController(meterReadingService: MeterReadingService) extends JsonSupport {
def routes: Route = pathPrefix("readings") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package com.tw.energy.controller

import akka.http.scaladsl.marshalling.ToResponseMarshallable
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives.{complete, get, path, _}
import akka.http.scaladsl.marshalling.{ToEntityMarshaller, ToResponseMarshallable}
import akka.http.scaladsl.model.*
import akka.http.scaladsl.server.Directives.*
import akka.http.scaladsl.server.PathMatchers.Segment
import akka.http.scaladsl.server.Route
import com.tw.energy.domain.PricePlanCosts
import com.tw.energy.domain.{ElectricityReading, PricePlanCosts}
import com.tw.energy.domain.StringTypes.{PlanName, SmartMeterId}
import com.tw.energy.service.{AccountService, PricePlanService}
import io.circe.generic.auto._
import io.circe.generic.auto.*


class PricePlanComparatorController(pricePlanService: PricePlanService, accountService: AccountService) extends JsonSupport {
class PricePlanComparatorController(pricePlanService: PricePlanService, accountService: AccountService) extends JsonSupport {
def routes: Route = pathPrefix("price-plans") {
get {
path("compare-all" / Segment) { smartMeterId =>
Expand Down
6 changes: 4 additions & 2 deletions src/main/scala/com/tw/energy/domain/PricePlan.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.tw.energy.domain

import java.time.{DayOfWeek, LocalDateTime}
import com.tw.energy.domain.StringTypes.EnergySupplier
import com.tw.energy.domain.StringTypes.PlanName

import com.tw.energy.domain.StringTypes.{EnergySupplier, PlanName}
import java.time.DayOfWeek
import java.time.LocalDateTime

case class PricePlan(planName: PlanName, energySupplier: EnergySupplier, unitRate: BigDecimal, peakTimeMultipliers: List[PeakTimeMultiplier] = List()) {
def calculatePrice(localDateTime: LocalDateTime): BigDecimal = {
Expand Down
3 changes: 1 addition & 2 deletions src/main/scala/com/tw/energy/generator/Generator.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.tw.energy.generator

import java.time.Instant

import com.tw.energy.domain.ElectricityReading

import java.time.Instant
import scala.util.Random

object Generator {
Expand Down
3 changes: 2 additions & 1 deletion src/main/scala/com/tw/energy/service/AccountService.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.tw.energy.service

import com.tw.energy.domain.StringTypes.{AccountId, SmartMeterId}
import com.tw.energy.domain.StringTypes.AccountId
import com.tw.energy.domain.StringTypes.SmartMeterId

class AccountService(private val pricePlanIdByAccountId: Map[SmartMeterId, AccountId]) {
def getPricePlanIdForSmartMeterId(smartMeterId: SmartMeterId): Option[AccountId] = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.tw.energy.service

import com.tw.energy.domain.ElectricityReading
import com.tw.energy.domain.MeterReadings
import com.tw.energy.domain.StringTypes.SmartMeterId
import com.tw.energy.domain.{ElectricityReading, MeterReadings}

class MeterReadingService(private[service] var readingsByMeterId: Map[SmartMeterId, Seq[ElectricityReading]] = Map()) {
def getReadings(smartMeterId: SmartMeterId): Option[Seq[ElectricityReading]] = {
Expand Down
6 changes: 4 additions & 2 deletions src/main/scala/com/tw/energy/service/PricePlanService.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.tw.energy.service


import com.tw.energy.domain.StringTypes.{PlanName, SmartMeterId}
import com.tw.energy.domain.{ElectricityReading, PricePlan}
import com.tw.energy.domain.ElectricityReading
import com.tw.energy.domain.PricePlan
import com.tw.energy.domain.StringTypes.PlanName
import com.tw.energy.domain.StringTypes.SmartMeterId

class PricePlanService(pricePlans: Seq[PricePlan], meterReadingService: MeterReadingService) {

Expand Down
12 changes: 7 additions & 5 deletions src/test/scala/EndpointIntegrationTest.scala
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.*
import com.tw.energy.JOIEnergyApplication
import com.tw.energy.WebServer
import com.tw.energy.controller.JsonSupport
import com.tw.energy.domain.MeterReadings
import com.tw.energy.generator.Generator
import com.tw.energy.{JOIEnergyApplication, WebServer}
import io.circe.generic.auto._
import io.circe.syntax._
import io.circe.generic.auto.*
import io.circe.syntax.*
import org.scalatest.BeforeAndAfterAll
import org.scalatest.flatspec.AsyncFlatSpec
import org.scalatest.matchers.should.Matchers

import scala.concurrent.Future
import scala.util.Random

class EndpointIntegrationTest extends AsyncFlatSpec with Matchers with BeforeAndAfterAll {
class EndpointIntegrationTest extends AsyncFlatSpec with Matchers with BeforeAndAfterAll with JsonSupport {
private implicit val system: ActorSystem = ActorSystem()
private val application = new JOIEnergyApplication
private val port = 8000 + Random.nextInt(1000)
Expand Down
Loading

0 comments on commit b4542c0

Please sign in to comment.