Skip to content

Commit 51b1c49

Browse files
authored
Merge pull request #3 from ctrl-hub/feat/timeband-support
feat: support timebands
2 parents 6c96c2a + d6674b1 commit 51b1c49

File tree

6 files changed

+268
-0
lines changed

6 files changed

+268
-0
lines changed

ai.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# AI Overview: Kotlin SDK
2+
3+
## 1. High Level Overview
4+
The Kotlin SDK is an SDK that provides a way to interact with the CtrlHub APIs using Kotlin.
5+
6+
## 2. Overview of Technologies
7+
- **Kotlin**
8+
- **Gradle**: Build tool for Kotlin projects.
9+
- **Ktor**: Asynchronous HTTP client for making API requests.
10+
11+
This is not a KMP project.
12+
13+
## 3. Project Structure
14+
- `src/main/kotlin`: Contains the main Kotlin code for the SDK.
15+
- `src/test/kotlin`: Contains the test code for the SDK.
16+
- `build.gradle.kts`: Gradle build file for the project.
17+
- `settings.gradle.kts`: Gradle settings file for the project.
18+
19+
## 4. Coding Style and Conventions
20+
### Router Pattern
21+
- Routers are used to organize API "domains" (e.g., operations, time bands, projects).
22+
- Each router is responsible for a specific resource or domain and provides methods to interact with the corresponding API endpoints.
23+
24+
### Router Structure
25+
- Routers extend a base `Router` class, which provides HTTP utility methods (e.g., `fetchJsonApiResource`, `fetchPaginatedJsonApiResources`).
26+
- Routers are typically constructed with a `HttpClient` instance.
27+
28+
### Request Parameters
29+
- Routers use request parameter classes (e.g., `OperationRequestParameters`, `TimeBandsRequestParameters`) to encapsulate query parameters such as pagination (`offset`, `limit`), filtering, and includes.
30+
- These parameter classes often inherit from abstract base classes like `AbstractRequestParameters` or `RequestParametersWithIncludes` for consistency and code reuse.
31+
32+
### Method Naming and Return Types
33+
- The main method to fetch all resources is named `all` and returns a paginated or full list (e.g., `PaginatedList<Operation>` or `java.util.List<TimeBand>`).
34+
- The method to fetch a single resource is named `one` and returns the resource object (e.g., `Operation`, `TimeBand`).
35+
- Methods are `suspend` functions to support asynchronous calls.
36+
37+
### Extension Properties
38+
- Routers register an extension property on the `Api` class for convenient access (e.g., `val Api.operations: OperationsRouter`, `val Api.timeBands: TimeBandsRouter`).
39+
40+
### Response Models
41+
- Response models are annotated for JSON:API serialization/deserialization using the `jasminb.jsonapi-converter` library.
42+
- IDs are always strings.
43+
- Collections use `java.util.List` for Java compatibility.
44+
45+
### Coding Conventions
46+
- Use idiomatic Kotlin.
47+
- Use `data class` for models.
48+
- Use `@JvmField` for Java interop if needed.
49+
- Use `suspend` functions for API calls to allow for asynchronous programming.
50+
- API responses are annotated using the `jasminb.jsonapi-converter` library for JSONAPI serialization/deserialization.
51+
- IDs are strings, not integers.
52+
- Use `@JvmField` for fields that need to be accessed from Java code.
53+
- Responses that reference a collection use `java.util.List` instead of Kotlin's `List` to ensure compatibility with Java and the JSON API library.
54+
- Routers register an extension function on the `Api` class to provide a convenient way to access the API endpoints.
55+
56+
## 5. Git Commits
57+
- Use conventional commit messages.
58+
- First line of the commit should be 72 characters or fewer.
59+
- Provide a description of the changes in the commit body if necessary.
60+
61+
## 6. Example
62+
- The `OperationsRouter` is a canonical example, with methods for `all` and `one`, a request parameters class supporting includes, and extension properties for access from `Api` and related routers.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.ctrlhub.core.settings.timebands
2+
3+
import com.ctrlhub.core.Api
4+
import com.ctrlhub.core.settings.timebands.response.TimeBand
5+
import com.ctrlhub.core.router.Router
6+
import com.ctrlhub.core.router.request.AbstractRequestParameters
7+
import com.ctrlhub.core.router.request.FilterOption
8+
import io.ktor.client.HttpClient
9+
10+
class TimeBandsRequestParameters(
11+
offset: Int? = 0,
12+
limit: Int? = 100,
13+
filterOptions: List<FilterOption> = emptyList()
14+
) : AbstractRequestParameters(offset, limit, filterOptions)
15+
16+
class TimeBandsRouter(httpClient: HttpClient) : Router(httpClient) {
17+
/**
18+
* Retrieve a list of all time bands
19+
*
20+
* @return A list of all time bands
21+
*/
22+
suspend fun all(
23+
organisationId: String,
24+
requestParameters: TimeBandsRequestParameters = TimeBandsRequestParameters()
25+
): List<TimeBand> {
26+
val endpoint = "/orgs/$organisationId/settings/time-bands"
27+
return fetchJsonApiResources(endpoint, requestParameters.toMap())
28+
}
29+
30+
/**
31+
* Retrieve a single time band by ID
32+
*
33+
* @param timeBandId String The time band ID to retrieve
34+
* @return The time band with the given ID
35+
*/
36+
suspend fun one(
37+
organisationId: String,
38+
timeBandId: String,
39+
requestParameters: TimeBandsRequestParameters = TimeBandsRequestParameters()
40+
): TimeBand {
41+
val endpoint = "/orgs/$organisationId/settings/time-bands/$timeBandId"
42+
return fetchJsonApiResource(endpoint, requestParameters.toMap(), TimeBand::class.java)
43+
}
44+
}
45+
46+
val Api.timeBands: TimeBandsRouter
47+
get() = TimeBandsRouter(httpClient)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.ctrlhub.core.settings.timebands.response
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator
4+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
5+
import com.fasterxml.jackson.annotation.JsonProperty
6+
import com.github.jasminb.jsonapi.StringIdHandler
7+
import com.github.jasminb.jsonapi.annotations.Id
8+
import com.github.jasminb.jsonapi.annotations.Meta
9+
import com.github.jasminb.jsonapi.annotations.Type
10+
11+
@Type("time-bands")
12+
@JsonIgnoreProperties(ignoreUnknown = true)
13+
data class TimeBand @JsonCreator constructor(
14+
@Id(StringIdHandler::class) var id: String = "",
15+
@JsonProperty("end") var end: String = "",
16+
@JsonProperty("name") var name: String = "",
17+
@JsonProperty("start") var start: String = "",
18+
@JsonProperty("meta") @Meta var meta: TimeBandMeta? = null
19+
) {
20+
@JsonIgnoreProperties(ignoreUnknown = true)
21+
data class TimeBandMeta @JsonCreator constructor(
22+
@JsonProperty("created_at") var createdAt: String = "",
23+
@JsonProperty("updated_at") var updatedAt: String = "",
24+
@JsonProperty("modified_at") var modifiedAt: String = ""
25+
)
26+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.ctrlhub.core.settings.timebands
2+
3+
import com.ctrlhub.core.configureForTest
4+
import com.ctrlhub.core.settings.timebands.response.TimeBand
5+
import io.ktor.client.*
6+
import io.ktor.client.engine.mock.*
7+
import io.ktor.http.*
8+
import kotlinx.coroutines.runBlocking
9+
import org.junit.jupiter.api.Test
10+
import java.nio.file.Files
11+
import java.nio.file.Paths
12+
import kotlin.test.assertEquals
13+
import kotlin.test.assertIs
14+
import kotlin.test.assertNotNull
15+
16+
class TimeBandsRouterTest {
17+
@Test
18+
fun `can retrieve all time bands`() {
19+
val jsonFilePath = Paths.get("src/test/resources/settings/time-bands/all-response.json")
20+
val jsonContent = Files.readString(jsonFilePath)
21+
22+
val mockEngine = MockEngine { request ->
23+
respond(
24+
content = jsonContent,
25+
status = HttpStatusCode.OK,
26+
headers = headersOf(HttpHeaders.ContentType, "application/json")
27+
)
28+
}
29+
30+
val router = TimeBandsRouter(httpClient = HttpClient(mockEngine).configureForTest())
31+
32+
runBlocking {
33+
val response = router.all("org-123")
34+
assertIs<java.util.List<TimeBand>>(response)
35+
assertEquals(3, response.size)
36+
assertNotNull(response[0].id)
37+
assertNotNull(response[0].name)
38+
}
39+
}
40+
41+
@Test
42+
fun `can retrieve a single time band`() {
43+
val jsonFilePath = Paths.get("src/test/resources/settings/time-bands/one-response.json")
44+
val jsonContent = Files.readString(jsonFilePath)
45+
46+
val mockEngine = MockEngine { request ->
47+
respond(
48+
content = jsonContent,
49+
status = HttpStatusCode.OK,
50+
headers = headersOf(HttpHeaders.ContentType, "application/json")
51+
)
52+
}
53+
54+
val router = TimeBandsRouter(httpClient = HttpClient(mockEngine).configureForTest())
55+
56+
runBlocking {
57+
val response = router.one("org-123", "b1a1a111-1111-1111-1111-111111111111")
58+
assertIs<TimeBand>(response)
59+
assertEquals("b1a1a111-1111-1111-1111-111111111111", response.id)
60+
assertNotNull(response.name)
61+
}
62+
}
63+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"data": [
3+
{
4+
"id": "b1a1a111-1111-1111-1111-111111111111",
5+
"type": "time-bands",
6+
"attributes": {
7+
"end": "11:00+0000",
8+
"name": "Lorem",
9+
"start": "07:00+0000"
10+
},
11+
"meta": {
12+
"created_at": "2025-01-01T00:00:00.000Z",
13+
"updated_at": "2025-01-01T00:00:00.000Z",
14+
"modified_at": "2025-01-01T00:00:00.000Z"
15+
}
16+
},
17+
{
18+
"id": "b2a2a222-2222-2222-2222-222222222222",
19+
"type": "time-bands",
20+
"attributes": {
21+
"end": "15:00+0000",
22+
"name": "Ipsum",
23+
"start": "11:00+0000"
24+
},
25+
"meta": {
26+
"created_at": "2025-01-01T00:00:00.000Z",
27+
"updated_at": "2025-01-01T00:00:00.000Z",
28+
"modified_at": "2025-01-01T00:00:00.000Z"
29+
}
30+
},
31+
{
32+
"id": "b3a3a333-3333-3333-3333-333333333333",
33+
"type": "time-bands",
34+
"attributes": {
35+
"end": "07:00+0000",
36+
"name": "Dolor",
37+
"start": "15:00+0000"
38+
},
39+
"meta": {
40+
"created_at": "2025-01-01T00:00:00.000Z",
41+
"updated_at": "2025-01-01T00:00:00.000Z",
42+
"modified_at": "2025-01-01T00:00:00.000Z"
43+
}
44+
}
45+
],
46+
"jsonapi": {
47+
"version": "1.0"
48+
}
49+
}
50+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"data": {
3+
"id": "b1a1a111-1111-1111-1111-111111111111",
4+
"type": "time-bands",
5+
"attributes": {
6+
"end": "11:00+0000",
7+
"name": "Lorem",
8+
"start": "07:00+0000"
9+
},
10+
"meta": {
11+
"created_at": "2025-01-01T00:00:00.000Z",
12+
"updated_at": "2025-01-01T00:00:00.000Z",
13+
"modified_at": "2025-01-01T00:00:00.000Z"
14+
}
15+
},
16+
"jsonapi": {
17+
"version": "1.0"
18+
}
19+
}
20+

0 commit comments

Comments
 (0)