Skip to content

Commit

Permalink
feat(client): Introduce RepositoriesApi for repository-related oper…
Browse files Browse the repository at this point in the history
…ations

Introduce the `RepositoriesApi` class to handle repository-related API
operations. Currently, it includes the `createOrtRun` and `getOrtRun`.
Additional functions can be added in the future.

Signed-off-by: Onur Demirci <[email protected]>
  • Loading branch information
bs-ondem committed Dec 6, 2024
1 parent 40d7ccb commit b42457b
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 0 deletions.
1 change: 1 addition & 0 deletions api/v1/client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
implementation(projects.api.v1.apiV1Model)
implementation(libs.ktorClientAuth)
implementation(libs.ktorClientContentNegotiation)
implementation(libs.ktorKotlinxSerializationMP)
Expand Down
39 changes: 39 additions & 0 deletions api/v1/client/src/commonMain/kotlin/HttpClientUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@
package org.eclipse.apoapsis.ortserver.client

import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.plugins.HttpResponseValidator
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.http.isSuccess
import io.ktor.serialization.kotlinx.json.json

import kotlinx.serialization.json.Json
Expand All @@ -46,3 +50,38 @@ fun createDefaultHttpClient(json: Json = Json.Default, engine: HttpClientEngine?
}
}
}

/**
* Create a customized HTTP client with a default configuration for the ORT server client, adding response validation
* and error handling by building upon [createDefaultHttpClient].
*/
fun createOrtHttpClient(
json: Json = Json.Default,
engine: HttpClientEngine? = null,
config: HttpClientConfig<*>.() -> Unit = {}
): HttpClient =
createDefaultHttpClient(json, engine).config {
HttpResponseValidator {
handleResponseExceptionWithRequest { cause, request ->
throw OrtServerClientException(
"Request to ${request.url} failed with exception: ${cause.message}",
cause
)
}

validateResponse { response ->
if (!response.status.isSuccess()) {
throw OrtServerClientException(
"Request failed with status ${response.status.value}: ${response.bodyAsText()}"
)
}
}
}

config()
}

/**
* An exception thrown by the ORT server client.
*/
class OrtServerClientException(message: String, cause: Throwable? = null) : Exception(message, cause)
53 changes: 53 additions & 0 deletions api/v1/client/src/commonMain/kotlin/api/RepositoriesApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2024 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.eclipse.apoapsis.ortserver.client.api

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody

import org.eclipse.apoapsis.ortserver.api.v1.model.CreateOrtRun
import org.eclipse.apoapsis.ortserver.api.v1.model.OrtRun

/**
* A client for the repositories API.
*/
class RepositoriesApi(
/**
* The configured HTTP client for the interaction with the API.
*/
private val client: HttpClient
) {
/**
* Create a new [ORT run][ortRun] for the given [repository][repositoryId].
*/
suspend fun createOrtRun(repositoryId: Long, ortRun: CreateOrtRun): OrtRun =
client.post("api/v1/repositories/$repositoryId/runs") {
setBody(ortRun)
}.body()

/**
* Get the [ORT run][OrtRun] with the given [repositoryId] and [index].
*/
suspend fun getOrtRun(repositoryId: Long, index: Long): OrtRun =
client.get("api/v1/repositories/$repositoryId/runs/$index").body()
}
124 changes: 124 additions & 0 deletions api/v1/client/src/commonTest/kotlin/RepositoriesApiTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (C) 2024 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.ktor.client.engine.mock.MockEngine
import io.ktor.http.HttpStatusCode

import kotlinx.datetime.Instant

import org.eclipse.apoapsis.ortserver.api.v1.model.CreateOrtRun
import org.eclipse.apoapsis.ortserver.api.v1.model.JobConfigurations
import org.eclipse.apoapsis.ortserver.api.v1.model.Jobs
import org.eclipse.apoapsis.ortserver.api.v1.model.OrtRun
import org.eclipse.apoapsis.ortserver.api.v1.model.OrtRunStatus
import org.eclipse.apoapsis.ortserver.client.OrtServerClientException
import org.eclipse.apoapsis.ortserver.client.api.RepositoriesApi
import org.eclipse.apoapsis.ortserver.client.createOrtHttpClient

class RepositoriesApiTest : StringSpec({
"createOrtRun" should {
"create and return an ORT run" {
val respondOrtRun = OrtRun(
id = 1,
index = 1,
organizationId = 1,
productId = 1,
repositoryId = 1,
revision = "main",
createdAt = Instant.parse("2024-01-01T00:00:00Z"),
jobConfigs = JobConfigurations(),
status = OrtRunStatus.CREATED,
jobs = Jobs(),
issues = emptyList(),
traceId = null,
labels = emptyMap()
)

val mockEngine = MockEngine { jsonRespond(respondOrtRun, HttpStatusCode.Created) }
val client = createOrtHttpClient(engine = mockEngine)

val repositoriesApi = RepositoriesApi(client)

val actualOrtRun = repositoriesApi.createOrtRun(
repositoryId = 1,
ortRun = CreateOrtRun(revision = "main", jobConfigs = JobConfigurations())
)

actualOrtRun shouldBe respondOrtRun
}

"throw an exception if the ORT run cannot be created" {
val mockEngine = MockEngine { jsonRespond("Invalid request", HttpStatusCode.NotFound) }
val client = createOrtHttpClient(engine = mockEngine)

val repositoriesApi = RepositoriesApi(client)

shouldThrow<OrtServerClientException> {
repositoriesApi.createOrtRun(
repositoryId = 1,
ortRun = CreateOrtRun(revision = "main", jobConfigs = JobConfigurations())
)
}
}
}

"getOrtRun" should {
"return an ORT run" {
val respondOrtRun = OrtRun(
id = 1,
index = 1,
organizationId = 1,
productId = 1,
repositoryId = 1,
revision = "main",
createdAt = Instant.parse("2024-01-01T00:00:00Z"),
jobConfigs = JobConfigurations(),
status = OrtRunStatus.CREATED,
jobs = Jobs(),
issues = emptyList(),
traceId = null,
labels = emptyMap()
)

val mockEngine = MockEngine { jsonRespond(respondOrtRun) }
val client = createOrtHttpClient(engine = mockEngine)

val repositoriesApi = RepositoriesApi(client)

val actualOrtRun = repositoriesApi.getOrtRun(respondOrtRun.repositoryId, respondOrtRun.index)

actualOrtRun shouldBe respondOrtRun
}

"throw an exception if the ORT run cannot be retrieved" {
val mockEngine = MockEngine { jsonRespond("Invalid request", HttpStatusCode.NotFound) }
val client = createOrtHttpClient(engine = mockEngine)

val repositoriesApi = RepositoriesApi(client)

shouldThrow<OrtServerClientException> {
repositoriesApi.getOrtRun(1L, 1L)
}
}
}
})
39 changes: 39 additions & 0 deletions api/v1/client/src/commonTest/kotlin/TestUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2024 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

import io.ktor.client.engine.mock.MockRequestHandleScope
import io.ktor.client.engine.mock.respond
import io.ktor.client.request.HttpResponseData
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.headersOf
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

/**
* Create JSON respond with the given [obj] and [statusCode].
*/
internal inline fun <reified T : Any> MockRequestHandleScope.jsonRespond(
obj: T,
statusCode: HttpStatusCode = HttpStatusCode.OK
): HttpResponseData = respond(
content = Json.encodeToString(obj),
status = statusCode,
headers = headersOf("Content-Type" to listOf(ContentType.Application.Json.toString()))
)

0 comments on commit b42457b

Please sign in to comment.