Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"Bash(./gradlew clean build:*)",
"Bash(java -version:*)",
"Bash(/usr/libexec/java_home:*)",
"Bash(./gradlew clean test:*)"
"Bash(./gradlew build:*)"
]
}
}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/main/kotlin/com/nylas/models/AvailabilityParticipant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ data class AvailabilityParticipant(
*/
@Json(name = "open_hours")
val openHours: List<OpenHours>? = 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<SpecificTimeAvailability>? = null,
) {
/**
* A builder for creating an [AvailabilityParticipant].
Expand All @@ -32,6 +38,7 @@ data class AvailabilityParticipant(
) {
private var calendarIds: List<String>? = null
private var openHours: List<OpenHours>? = null
private var specificTimeAvailability: List<SpecificTimeAvailability>? = null

/**
* Set the calendar IDs associated with each participant's email address.
Expand All @@ -47,6 +54,13 @@ data class AvailabilityParticipant(
*/
fun openHours(openHours: List<OpenHours>) = 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<SpecificTimeAvailability>) = apply { this.specificTimeAvailability = specificTimeAvailability }

/**
* Build the [AvailabilityParticipant].
* @return The [AvailabilityParticipant].
Expand All @@ -55,6 +69,7 @@ data class AvailabilityParticipant(
email,
calendarIds,
openHours,
specificTimeAvailability,
)
}
}
47 changes: 47 additions & 0 deletions src/main/kotlin/com/nylas/models/SpecificTimeAvailability.kt
Original file line number Diff line number Diff line change
@@ -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,
)
}
}
223 changes: 223 additions & 0 deletions src/test/kotlin/com/nylas/resources/CalendarsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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>(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
Expand Down Expand Up @@ -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<String>()
val typeCaptor = argumentCaptor<Type>()
val requestBodyCaptor = argumentCaptor<String>()
val queryParamCaptor = argumentCaptor<ListCalendersQueryParams>()
val overrideParamCaptor = argumentCaptor<RequestOverrides>()
verify(mockNylasClient).executePost<ListResponse<Calendar>>(
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"
Expand Down
Loading