From 362f37e6a7c99fb2b676b7327785b5eb8ad97474 Mon Sep 17 00:00:00 2001 From: Gordan Ovcaric Date: Wed, 18 Feb 2026 11:24:19 +0100 Subject: [PATCH] TW-4619: add "specific_time_availability" to calendars support --- .claude/settings.local.json | 2 +- CHANGELOG.md | 1 + .../nylas/models/AvailabilityParticipant.kt | 15 ++ .../nylas/models/SpecificTimeAvailability.kt | 47 ++++ .../com/nylas/resources/CalendarsTest.kt | 223 ++++++++++++++++++ 5 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/nylas/models/SpecificTimeAvailability.kt diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ba271215..964d0c51 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,7 @@ "Bash(./gradlew clean build:*)", "Bash(java -version:*)", "Bash(/usr/libexec/java_home:*)", - "Bash(./gradlew clean test:*)" + "Bash(./gradlew build:*)" ] } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 364a5ad7..0a38b5fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - `activeCredentialId` field in `Connector` response model - `activeCredentialId` field in `UpdateConnectorRequest` for setting the active credential on a Connector * Enhanced `CredentialData.ConnectorOverride` to support optional `clientId` and `clientSecret` fields +* Support for `specific_time_availability` field in `AvailabilityParticipant` to override open hours configurations for specific dates and time ranges ### Deprecated * `CreateCredentialRequest.Override` - Use `CreateCredentialRequest.Connector` instead diff --git a/src/main/kotlin/com/nylas/models/AvailabilityParticipant.kt b/src/main/kotlin/com/nylas/models/AvailabilityParticipant.kt index ba21885f..7c7ee322 100644 --- a/src/main/kotlin/com/nylas/models/AvailabilityParticipant.kt +++ b/src/main/kotlin/com/nylas/models/AvailabilityParticipant.kt @@ -22,6 +22,12 @@ data class AvailabilityParticipant( */ @Json(name = "open_hours") val openHours: List? = null, + /** + * An array of date and time ranges when the participant is available. + * This can override the open_hours configurations for a specific date and time range. + */ + @Json(name = "specific_time_availability") + val specificTimeAvailability: List? = null, ) { /** * A builder for creating an [AvailabilityParticipant]. @@ -32,6 +38,7 @@ data class AvailabilityParticipant( ) { private var calendarIds: List? = null private var openHours: List? = null + private var specificTimeAvailability: List? = null /** * Set the calendar IDs associated with each participant's email address. @@ -47,6 +54,13 @@ data class AvailabilityParticipant( */ fun openHours(openHours: List) = apply { this.openHours = openHours } + /** + * Set the specific time availability to override the open hours for specific dates and time ranges. + * @param specificTimeAvailability An array of date and time ranges when the participant is available. + * @return The builder. + */ + fun specificTimeAvailability(specificTimeAvailability: List) = apply { this.specificTimeAvailability = specificTimeAvailability } + /** * Build the [AvailabilityParticipant]. * @return The [AvailabilityParticipant]. @@ -55,6 +69,7 @@ data class AvailabilityParticipant( email, calendarIds, openHours, + specificTimeAvailability, ) } } diff --git a/src/main/kotlin/com/nylas/models/SpecificTimeAvailability.kt b/src/main/kotlin/com/nylas/models/SpecificTimeAvailability.kt new file mode 100644 index 00000000..6d5a47c4 --- /dev/null +++ b/src/main/kotlin/com/nylas/models/SpecificTimeAvailability.kt @@ -0,0 +1,47 @@ +package com.nylas.models + +import com.squareup.moshi.Json + +/** + * Class representation of a specific date and time range when a participant is available. + * This can override the open_hours configurations for a specific date and time range. + */ +data class SpecificTimeAvailability( + /** + * The date in YYYY-MM-DD format. + */ + @Json(name = "date") + val date: String, + /** + * The start time in HH:MM format. + */ + @Json(name = "start") + val start: String, + /** + * The end time in HH:MM format. + */ + @Json(name = "end") + val end: String, +) { + /** + * A builder for creating a [SpecificTimeAvailability]. + * @param date The date in YYYY-MM-DD format. + * @param start The start time in HH:MM format. + * @param end The end time in HH:MM format. + */ + data class Builder( + private val date: String, + private val start: String, + private val end: String, + ) { + /** + * Build the [SpecificTimeAvailability]. + * @return The [SpecificTimeAvailability]. + */ + fun build() = SpecificTimeAvailability( + date, + start, + end, + ) + } +} diff --git a/src/test/kotlin/com/nylas/resources/CalendarsTest.kt b/src/test/kotlin/com/nylas/resources/CalendarsTest.kt index ed719232..3bf73767 100644 --- a/src/test/kotlin/com/nylas/resources/CalendarsTest.kt +++ b/src/test/kotlin/com/nylas/resources/CalendarsTest.kt @@ -102,6 +102,188 @@ class CalendarsTest { } } + @Nested + inner class SpecificTimeAvailabilityTests { + @Test + fun `SpecificTimeAvailability serializes properly`() { + val adapter = JsonHelper.moshi().adapter(SpecificTimeAvailability::class.java) + val jsonBuffer = Buffer().writeUtf8( + """ + { + "date": "2026-03-18", + "start": "09:00", + "end": "17:00" + } + """.trimIndent(), + ) + + val specificTimeAvailability = adapter.fromJson(jsonBuffer)!! + assertIs(specificTimeAvailability) + assertEquals("2026-03-18", specificTimeAvailability.date) + assertEquals("09:00", specificTimeAvailability.start) + assertEquals("17:00", specificTimeAvailability.end) + } + + @Test + fun `SpecificTimeAvailability serializes to JSON correctly`() { + val adapter = JsonHelper.moshi().adapter(SpecificTimeAvailability::class.java) + val specificTimeAvailability = SpecificTimeAvailability( + date = "2026-03-18", + start = "09:00", + end = "17:00", + ) + + val json = adapter.toJson(specificTimeAvailability) + assertEquals("""{"date":"2026-03-18","start":"09:00","end":"17:00"}""", json) + } + + @Test + fun `SpecificTimeAvailability round-trip serialization`() { + val adapter = JsonHelper.moshi().adapter(SpecificTimeAvailability::class.java) + val original = SpecificTimeAvailability( + date = "2026-03-18", + start = "09:00", + end = "17:00", + ) + + val json = adapter.toJson(original) + val deserialized = adapter.fromJson(json)!! + assertEquals(original.date, deserialized.date) + assertEquals(original.start, deserialized.start) + assertEquals(original.end, deserialized.end) + } + + @Test + fun `SpecificTimeAvailability Builder works correctly`() { + val specificTimeAvailability = SpecificTimeAvailability.Builder( + date = "2026-03-18", + start = "09:00", + end = "17:00", + ).build() + + assertEquals("2026-03-18", specificTimeAvailability.date) + assertEquals("09:00", specificTimeAvailability.start) + assertEquals("17:00", specificTimeAvailability.end) + } + + @Test + fun `AvailabilityParticipant serializes with specificTimeAvailability`() { + val adapter = JsonHelper.moshi().adapter(AvailabilityParticipant::class.java) + val participant = AvailabilityParticipant( + email = "test@nylas.com", + calendarIds = listOf("primary"), + specificTimeAvailability = listOf( + SpecificTimeAvailability( + date = "2026-03-18", + start = "09:00", + end = "17:00", + ), + ), + ) + + val json = adapter.toJson(participant) + val deserialized = adapter.fromJson(json)!! + assertEquals("test@nylas.com", deserialized.email) + assertEquals(1, deserialized.specificTimeAvailability?.size) + assertEquals("2026-03-18", deserialized.specificTimeAvailability?.get(0)?.date) + assertEquals("09:00", deserialized.specificTimeAvailability?.get(0)?.start) + assertEquals("17:00", deserialized.specificTimeAvailability?.get(0)?.end) + } + + @Test + fun `AvailabilityParticipant serializes without specificTimeAvailability for backward compatibility`() { + val adapter = JsonHelper.moshi().adapter(AvailabilityParticipant::class.java) + val participant = AvailabilityParticipant( + email = "test@nylas.com", + calendarIds = listOf("calendar-123"), + ) + + val json = adapter.toJson(participant) + val deserialized = adapter.fromJson(json)!! + assertEquals("test@nylas.com", deserialized.email) + assertEquals(null, deserialized.specificTimeAvailability) + assertEquals(null, deserialized.openHours) + } + + @Test + fun `AvailabilityParticipant deserializes JSON with specific_time_availability`() { + val adapter = JsonHelper.moshi().adapter(AvailabilityParticipant::class.java) + val jsonBuffer = Buffer().writeUtf8( + """ + { + "email": "test@nylas.com", + "calendar_ids": ["primary"], + "specific_time_availability": [ + { + "date": "2026-03-18", + "start": "09:00", + "end": "17:00" + } + ] + } + """.trimIndent(), + ) + + val participant = adapter.fromJson(jsonBuffer)!! + assertEquals("test@nylas.com", participant.email) + assertEquals(listOf("primary"), participant.calendarIds) + assertEquals(1, participant.specificTimeAvailability?.size) + assertEquals("2026-03-18", participant.specificTimeAvailability?.get(0)?.date) + assertEquals("09:00", participant.specificTimeAvailability?.get(0)?.start) + assertEquals("17:00", participant.specificTimeAvailability?.get(0)?.end) + } + + @Test + fun `AvailabilityParticipant deserializes JSON without specific_time_availability for backward compatibility`() { + val adapter = JsonHelper.moshi().adapter(AvailabilityParticipant::class.java) + val jsonBuffer = Buffer().writeUtf8( + """ + { + "email": "test@nylas.com", + "calendar_ids": ["calendar-123"] + } + """.trimIndent(), + ) + + val participant = adapter.fromJson(jsonBuffer)!! + assertEquals("test@nylas.com", participant.email) + assertEquals(null, participant.specificTimeAvailability) + } + + @Test + fun `AvailabilityParticipant Builder works with specificTimeAvailability`() { + val participant = AvailabilityParticipant.Builder("test@nylas.com") + .calendarIds(listOf("primary")) + .specificTimeAvailability( + listOf( + SpecificTimeAvailability( + date = "2026-03-18", + start = "09:00", + end = "17:00", + ), + ), + ) + .build() + + assertEquals("test@nylas.com", participant.email) + assertEquals(listOf("primary"), participant.calendarIds) + assertEquals(1, participant.specificTimeAvailability?.size) + assertEquals("2026-03-18", participant.specificTimeAvailability?.get(0)?.date) + assertEquals("09:00", participant.specificTimeAvailability?.get(0)?.start) + assertEquals("17:00", participant.specificTimeAvailability?.get(0)?.end) + } + + @Test + fun `AvailabilityParticipant Builder works without specificTimeAvailability for backward compatibility`() { + val participant = AvailabilityParticipant.Builder("test@nylas.com") + .calendarIds(listOf("calendar-123")) + .build() + + assertEquals("test@nylas.com", participant.email) + assertEquals(null, participant.specificTimeAvailability) + } + } + @Nested inner class CrudTests { private lateinit var grantId: String @@ -341,6 +523,47 @@ class CalendarsTest { assertEquals(adapter.toJson(getAvailabilityRequest), requestBodyCaptor.firstValue) } + @Test + fun `getting availability with specificTimeAvailability calls requests with the correct params`() { + val adapter = JsonHelper.moshi().adapter(GetAvailabilityRequest::class.java) + val getAvailabilityRequest = GetAvailabilityRequest( + startTime = 1737540000, + endTime = 1737712800, + participants = listOf( + AvailabilityParticipant( + email = "nylastest8@gmail.com", + calendarIds = listOf("primary"), + specificTimeAvailability = listOf( + SpecificTimeAvailability( + date = "2026-03-18", + start = "09:00", + end = "17:00", + ), + ), + ), + ), + durationMinutes = 30, + ) + + calendars.getAvailability(getAvailabilityRequest) + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val requestBodyCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + val overrideParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost>( + pathCaptor.capture(), + typeCaptor.capture(), + requestBodyCaptor.capture(), + queryParamCaptor.capture(), + overrideParamCaptor.capture(), + ) + + assertEquals("v3/calendars/availability", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(Response::class.java, GetAvailabilityResponse::class.java), typeCaptor.firstValue) + assertEquals(adapter.toJson(getAvailabilityRequest), requestBodyCaptor.firstValue) + } + @Test fun `getting free busy calls requests with the correct params`() { val grantId = "abc-123-grant-id"