diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af3046b..84c0a8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,13 +19,16 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - - name: Build with Gradle - uses: gradle/gradle-build-action@v2.9.0 - with: - arguments: build + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: "8.11.1" + - name: Run Build + run: ./gradlew build + diff --git a/README.md b/README.md index 826bc3d..90320f4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Welcome to PowerDale 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 power a house is drawing/using. +information about how much power a house is drawing/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. @@ -15,31 +15,13 @@ JOI Energy is a new start-up in the energy industry. Rather than selling energy from the market by recording their customers' energy usage from their smart meters and recommending the best supplier to meet their needs. -You have been placed into their development team, whose current goal is to produce an API which their customers and +You have been placed into their development team, whose current goal it is to produce an API which their customers and smart meters will interact with. Unfortunately, two members of the team are on annual leave, and another one has called in sick! You are left with another ThoughtWorker to progress with the current user stories on the story wall. This is your chance to make an impact on the business, improve the code base and deliver value. -## Story Wall - -At JOI energy the development team use a story wall or Kanban board to keep track of features or "stories" as they are -worked on. - -The wall you will be working from today has 7 columns: - -- Backlog -- Ready for Dev -- In Dev -- Ready for Testing -- In Testing -- Ready for sign off -- Done - -Examples can be found -here [https://leankit.com/learn/kanban/kanban-board/](https://leankit.com/learn/kanban/kanban-board/) - ## Users To trial the new JOI software 5 people from the JOI accounts team have agreed to test the service and share their energy @@ -57,21 +39,17 @@ These values are used in the code and in the following examples too. ## Requirements -The project requires [Java 17](https://adoptium.net/en-GB/) or -higher. +The project requires [Java 21](https://adoptium.net/en-GB/) or higher. If you have multiple JVMs, you +might consider a tool such as [sdkman](https://sdkman.io/) to help manage them. -The project makes use of Gradle and uses -the [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html), which means you don't need Gradle +The project makes use of Gradle and uses the [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html), which means you don't need Gradle installed. ## Useful Gradle commands -The project makes use of Gradle and uses the Gradle wrapper to help you out carrying some common tasks such as building -the project or running it. - ### List all Gradle tasks -List all the tasks that Gradle can do, such as `build` and `test`. +List all the tasks that Gradle can run, such as `build` and `test`. ```console $ ./gradlew tasks @@ -79,7 +57,7 @@ $ ./gradlew tasks ### Build the project -Compiles the project, runs the test and then creates an executable JAR file +Compiles the project, runs the tests and then creates an executable JAR file. ```console $ ./gradlew build @@ -138,30 +116,31 @@ Example of body Parameters -| Parameter | Description | -|----------------|-------------------------------------------------------| -| `smartMeterId` | One of the smart meters' id listed above | -| `time` | The date/time (as epoch) when the _reading_ was taken | -| `reading` | The consumption in `kW` at the _time_ of the reading | +| Parameter | Description | +|----------------|---------------------------------------------------------------| +| `smartMeterId` | One of the smart meters ids listed above | +| `time` | The date/time (as epoch seconds) when the _reading_ was taken | +| `reading` | The power consumption in `kW` at the _time_ of the reading | Example readings -| Date (`GMT`) | Epoch timestamp | Reading (`kW`) | -|-------------------|----------------:|---------------:| -| `2020-11-29 8:00` | 1606636800 | 0.0503 | -| `2020-11-29 8:01` | 1606636860 | 0.0621 | -| `2020-11-29 8:02` | 1606636920 | 0.0222 | -| `2020-11-29 8:03` | 1606636980 | 0.0423 | -| `2020-11-29 8:04` | 1606637040 | 0.0191 | +| Date (`GMT/UTC`) | Epoch timestamp (seconds) | Power Reading (`kW`) | +|-------------------|--------------------------:|---------------------:| +| `2020-11-29 8:00` | 1606636800 | 0.0503 | +| `2020-11-29 8:01` | 1606636860 | 0.0621 | +| `2020-11-29 8:02` | 1606636920 | 0.0222 | +| `2020-11-29 8:03` | 1606636980 | 0.0423 | +| `2020-11-29 8:04` | 1606637040 | 0.0191 | In the above example, the smart meter sampled readings, in `kW`, every minute. Note that the reading is in `kW` and -not `kWH`, which means that each reading represents the consumption at the reading time. If no power is being consumed +not `kWH`, which means that each reading represents the _power_ consumption at the reading time. If no power is being consumed at the time of reading, then the reading value will be `0`. Given that `0` may introduce new challenges, we can assume that there is always some consumption, and we will never have a `0` reading value. These readings are then sent by the -smart meter to the application using REST. There is a service in the application that calculates the `kWH` from these -readings. +smart meter to the application using the HTTP endpoint. -The following POST request, is an example request using CURL, sends the readings shown in the table above. +There is a service in the application that calculates the `kWH` from these readings. + +The following POST request, is an example request using `curl`, sends the readings shown in the table above. ```console $ curl \ @@ -171,7 +150,7 @@ $ curl \ -d '{"smartMeterId":"smart-meter-0","readings":[{"time":1606636800,"reading":0.0503},{"time":1606636860,"reading":0.0621},{"time":1606636920,"reading":0.0222},{"time":1606636980,"reading":0.0423},{"time":1606637040,"reading":0.0191}]}' ``` -The above command should return. +The above command should return: ```json { @@ -184,19 +163,19 @@ The above command should return. ### Get Stored Readings -Endpoint +Endpoint: ```text GET /readings/read/ ``` -Parameters +Parameters: -| Parameter | Description | -|----------------|------------------------------------------| -| `smartMeterId` | One of the smart meters' id listed above | +| Parameter | Description | +|----------------|-------------------------------------------| +| `smartMeterId` | One of the smart meters ids listed above. | -Retrieving readings using CURL +Retrieving readings using `curl`: ```console $ curl "http://localhost:8080/readings/read/smart-meter-0" @@ -237,25 +216,25 @@ Example output ### View Current Price Plan and Compare Usage Cost Against all Price Plans -Endpoint +Endpoint: ```text GET /price-plans/compare-all/ ``` -Parameters +Parameters: -| Parameter | Description | -|----------------|------------------------------------------| -| `smartMeterId` | One of the smart meters' id listed above | +| Parameter | Description | +|----------------|-------------------------------------------| +| `smartMeterId` | One of the smart meters ids listed above. | -Retrieving readings using CURL +Retrieving readings using `curl`: ```console $ curl "http://localhost:8080/price-plans/compare-all/smart-meter-0" ``` -Example output +Example output: ```json { @@ -275,26 +254,26 @@ Example output ### View Recommended Price Plans for Usage -Endpoint +Endpoint: ```text GET /price-plans/recommend/[?limit=] ``` -Parameters +Parameters: -| Parameter | Description | -|----------------|------------------------------------------------------| -| `smartMeterId` | One of the smart meters' id listed above | -| `limit` | (Optional) limit the number of plans to be displayed | +| Parameter | Description | +|----------------|-------------------------------------------------------| +| `smartMeterId` | One of the smart meters ids listed above. | +| `limit` | (Optional) limit the number of plans to be displayed. | -Retrieving readings using CURL +Retrieving readings using `curl`: ```console $ curl "http://localhost:8080/price-plans/recommend/smart-meter-0?limit=2" ``` -Example output +Example output: ```json { diff --git a/build.gradle.kts b/build.gradle.kts index 8f25976..9d4aee5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,12 +6,12 @@ plugins { id("com.github.ben-manes.versions") } -val kotlin_version: String by project -val ktor_version: String by project -val logback_version: String by project -val jackson_version: String by project -val strikt_version: String by project -val detekt_version: String by project +val kotlinVersion: String by project +val ktorVersion: String by project +val logbackVersion: String by project +val jacksonVersion: String by project +val striktVersion: String by project +val detektVersion: String by project application { mainClass.set("io.ktor.server.netty.EngineMain") @@ -23,25 +23,25 @@ repositories { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version") - implementation("io.ktor:ktor-server-netty:$ktor_version") - implementation("ch.qos.logback:logback-classic:$logback_version") - implementation("io.ktor:ktor-server-core:$ktor_version") - implementation("io.ktor:ktor-server-content-negotiation:$ktor_version") - implementation("io.ktor:ktor-serialization-jackson:$ktor_version") + implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") + implementation("io.ktor:ktor-server-netty:$ktorVersion") + implementation("ch.qos.logback:logback-classic:$logbackVersion") + implementation("io.ktor:ktor-server-core:$ktorVersion") + implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-serialization-jackson:$ktorVersion") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") - testImplementation("io.ktor:ktor-server-tests:$ktor_version") - testImplementation("io.strikt:strikt-core:$strikt_version") - testImplementation("io.ktor:ktor-client-content-negotiation:$ktor_version") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion") + testImplementation("io.ktor:ktor-server-test-host:$ktorVersion") + testImplementation("io.strikt:strikt-core:$striktVersion") + testImplementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detekt_version") + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion") } kotlin { - jvmToolchain(17) + jvmToolchain(21) } tasks { diff --git a/gradle.properties b/gradle.properties index 413b98c..d74e839 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ -detekt_version=1.23.1 -jackson_version=2.15.2 -kotlin_version=1.9.10 -ktor_version=2.3.4 -logback_version=1.4.11 -strikt_version=0.34.1 -versions_version=0.48.0 +detektVersion=1.23.7 +jacksonVersion=2.18.2 +kotlinVersion=2.1.0 +ktorVersion=3.0.2 +logbackVersion=1.5.12 +striktVersion=0.35.1 +versionsVersion=0.51.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac72c34..e2847c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index f9537bf..99ed45f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,13 +1,13 @@ pluginManagement { - val kotlin_version: String by settings - val ktor_version: String by settings - val detekt_version: String by settings - val versions_version: String by settings + val kotlinVersion: String by settings + val ktorVersion: String by settings + val detektVersion: String by settings + val versionsVersion: String by settings plugins { - kotlin("jvm") version kotlin_version - id("io.ktor.plugin") version ktor_version - id("io.gitlab.arturbosch.detekt") version detekt_version - id("com.github.ben-manes.versions") version versions_version + kotlin("jvm") version kotlinVersion + id("io.ktor.plugin") version ktorVersion + id("io.gitlab.arturbosch.detekt") version detektVersion + id("com.github.ben-manes.versions") version versionsVersion } } diff --git a/src/main/kotlin/de/tw/energy/Application.kt b/src/main/kotlin/de/tw/energy/Application.kt index a2a64f7..6c9a67e 100644 --- a/src/main/kotlin/de/tw/energy/Application.kt +++ b/src/main/kotlin/de/tw/energy/Application.kt @@ -11,7 +11,6 @@ import de.tw.energy.services.PricePlanService import io.ktor.http.HttpStatusCode import io.ktor.serialization.jackson.jackson import io.ktor.server.application.Application -import io.ktor.server.application.call import io.ktor.server.application.install import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.request.receive @@ -43,7 +42,7 @@ fun Application.module() { val controller = MeterReadingController(meterReadingsService) get("/read/{smartMeterId}") { - val smartMeterId = call.parameters["smartMeterId"] ?: "" + val smartMeterId = call.parameters["smartMeterId"].orEmpty() call.respondNullable(controller.readings(smartMeterId)) } @@ -65,12 +64,12 @@ fun Application.module() { ) get("/compare-all/{smartMeterId}") { - val smartMeterId = call.parameters["smartMeterId"] ?: "" + val smartMeterId = call.parameters["smartMeterId"].orEmpty() call.respondNullable(controller.calculatedCostForEachPricePlan(smartMeterId)) } get("/recommend/{smartMeterId}") { - val smartMeterId = call.parameters["smartMeterId"] ?: "" + val smartMeterId = call.parameters["smartMeterId"].orEmpty() val limit = call.request.queryParameters["limit"] call.respondNullable(controller.recommendCheapestPricePlans(smartMeterId, limit?.toInt())) } diff --git a/src/main/kotlin/de/tw/energy/services/MeterReadingService.kt b/src/main/kotlin/de/tw/energy/services/MeterReadingService.kt index 7e4dde6..53bae4f 100644 --- a/src/main/kotlin/de/tw/energy/services/MeterReadingService.kt +++ b/src/main/kotlin/de/tw/energy/services/MeterReadingService.kt @@ -9,5 +9,5 @@ class MeterReadingService(private val meterReadings: MutableMap