Skip to content

Commit 93bbd40

Browse files
authored
TW-4619: add "specific_time_availability" to calendars support [WIP, BLOCKED] (#304)
1 parent 336654d commit 93bbd40

File tree

5 files changed

+287
-1
lines changed

5 files changed

+287
-1
lines changed

.claude/settings.local.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"Bash(./gradlew clean build:*)",
1111
"Bash(java -version:*)",
1212
"Bash(/usr/libexec/java_home:*)",
13-
"Bash(./gradlew clean test:*)"
13+
"Bash(./gradlew build:*)"
1414
]
1515
}
1616
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- `activeCredentialId` field in `Connector` response model
1212
- `activeCredentialId` field in `UpdateConnectorRequest` for setting the active credential on a Connector
1313
* Enhanced `CredentialData.ConnectorOverride` to support optional `clientId` and `clientSecret` fields
14+
* Support for `specific_time_availability` field in `AvailabilityParticipant` to override open hours configurations for specific dates and time ranges
1415

1516
### Deprecated
1617
* `CreateCredentialRequest.Override` - Use `CreateCredentialRequest.Connector` instead

src/main/kotlin/com/nylas/models/AvailabilityParticipant.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ data class AvailabilityParticipant(
2222
*/
2323
@Json(name = "open_hours")
2424
val openHours: List<OpenHours>? = null,
25+
/**
26+
* An array of date and time ranges when the participant is available.
27+
* This can override the open_hours configurations for a specific date and time range.
28+
*/
29+
@Json(name = "specific_time_availability")
30+
val specificTimeAvailability: List<SpecificTimeAvailability>? = null,
2531
) {
2632
/**
2733
* A builder for creating an [AvailabilityParticipant].
@@ -32,6 +38,7 @@ data class AvailabilityParticipant(
3238
) {
3339
private var calendarIds: List<String>? = null
3440
private var openHours: List<OpenHours>? = null
41+
private var specificTimeAvailability: List<SpecificTimeAvailability>? = null
3542

3643
/**
3744
* Set the calendar IDs associated with each participant's email address.
@@ -47,6 +54,13 @@ data class AvailabilityParticipant(
4754
*/
4855
fun openHours(openHours: List<OpenHours>) = apply { this.openHours = openHours }
4956

57+
/**
58+
* Set the specific time availability to override the open hours for specific dates and time ranges.
59+
* @param specificTimeAvailability An array of date and time ranges when the participant is available.
60+
* @return The builder.
61+
*/
62+
fun specificTimeAvailability(specificTimeAvailability: List<SpecificTimeAvailability>) = apply { this.specificTimeAvailability = specificTimeAvailability }
63+
5064
/**
5165
* Build the [AvailabilityParticipant].
5266
* @return The [AvailabilityParticipant].
@@ -55,6 +69,7 @@ data class AvailabilityParticipant(
5569
email,
5670
calendarIds,
5771
openHours,
72+
specificTimeAvailability,
5873
)
5974
}
6075
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.nylas.models
2+
3+
import com.squareup.moshi.Json
4+
5+
/**
6+
* Class representation of a specific date and time range when a participant is available.
7+
* This can override the open_hours configurations for a specific date and time range.
8+
*/
9+
data class SpecificTimeAvailability(
10+
/**
11+
* The date in YYYY-MM-DD format.
12+
*/
13+
@Json(name = "date")
14+
val date: String,
15+
/**
16+
* The start time in HH:MM format.
17+
*/
18+
@Json(name = "start")
19+
val start: String,
20+
/**
21+
* The end time in HH:MM format.
22+
*/
23+
@Json(name = "end")
24+
val end: String,
25+
) {
26+
/**
27+
* A builder for creating a [SpecificTimeAvailability].
28+
* @param date The date in YYYY-MM-DD format.
29+
* @param start The start time in HH:MM format.
30+
* @param end The end time in HH:MM format.
31+
*/
32+
data class Builder(
33+
private val date: String,
34+
private val start: String,
35+
private val end: String,
36+
) {
37+
/**
38+
* Build the [SpecificTimeAvailability].
39+
* @return The [SpecificTimeAvailability].
40+
*/
41+
fun build() = SpecificTimeAvailability(
42+
date,
43+
start,
44+
end,
45+
)
46+
}
47+
}

src/test/kotlin/com/nylas/resources/CalendarsTest.kt

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,188 @@ class CalendarsTest {
102102
}
103103
}
104104

105+
@Nested
106+
inner class SpecificTimeAvailabilityTests {
107+
@Test
108+
fun `SpecificTimeAvailability serializes properly`() {
109+
val adapter = JsonHelper.moshi().adapter(SpecificTimeAvailability::class.java)
110+
val jsonBuffer = Buffer().writeUtf8(
111+
"""
112+
{
113+
"date": "2026-03-18",
114+
"start": "09:00",
115+
"end": "17:00"
116+
}
117+
""".trimIndent(),
118+
)
119+
120+
val specificTimeAvailability = adapter.fromJson(jsonBuffer)!!
121+
assertIs<SpecificTimeAvailability>(specificTimeAvailability)
122+
assertEquals("2026-03-18", specificTimeAvailability.date)
123+
assertEquals("09:00", specificTimeAvailability.start)
124+
assertEquals("17:00", specificTimeAvailability.end)
125+
}
126+
127+
@Test
128+
fun `SpecificTimeAvailability serializes to JSON correctly`() {
129+
val adapter = JsonHelper.moshi().adapter(SpecificTimeAvailability::class.java)
130+
val specificTimeAvailability = SpecificTimeAvailability(
131+
date = "2026-03-18",
132+
start = "09:00",
133+
end = "17:00",
134+
)
135+
136+
val json = adapter.toJson(specificTimeAvailability)
137+
assertEquals("""{"date":"2026-03-18","start":"09:00","end":"17:00"}""", json)
138+
}
139+
140+
@Test
141+
fun `SpecificTimeAvailability round-trip serialization`() {
142+
val adapter = JsonHelper.moshi().adapter(SpecificTimeAvailability::class.java)
143+
val original = SpecificTimeAvailability(
144+
date = "2026-03-18",
145+
start = "09:00",
146+
end = "17:00",
147+
)
148+
149+
val json = adapter.toJson(original)
150+
val deserialized = adapter.fromJson(json)!!
151+
assertEquals(original.date, deserialized.date)
152+
assertEquals(original.start, deserialized.start)
153+
assertEquals(original.end, deserialized.end)
154+
}
155+
156+
@Test
157+
fun `SpecificTimeAvailability Builder works correctly`() {
158+
val specificTimeAvailability = SpecificTimeAvailability.Builder(
159+
date = "2026-03-18",
160+
start = "09:00",
161+
end = "17:00",
162+
).build()
163+
164+
assertEquals("2026-03-18", specificTimeAvailability.date)
165+
assertEquals("09:00", specificTimeAvailability.start)
166+
assertEquals("17:00", specificTimeAvailability.end)
167+
}
168+
169+
@Test
170+
fun `AvailabilityParticipant serializes with specificTimeAvailability`() {
171+
val adapter = JsonHelper.moshi().adapter(AvailabilityParticipant::class.java)
172+
val participant = AvailabilityParticipant(
173+
email = "test@nylas.com",
174+
calendarIds = listOf("primary"),
175+
specificTimeAvailability = listOf(
176+
SpecificTimeAvailability(
177+
date = "2026-03-18",
178+
start = "09:00",
179+
end = "17:00",
180+
),
181+
),
182+
)
183+
184+
val json = adapter.toJson(participant)
185+
val deserialized = adapter.fromJson(json)!!
186+
assertEquals("test@nylas.com", deserialized.email)
187+
assertEquals(1, deserialized.specificTimeAvailability?.size)
188+
assertEquals("2026-03-18", deserialized.specificTimeAvailability?.get(0)?.date)
189+
assertEquals("09:00", deserialized.specificTimeAvailability?.get(0)?.start)
190+
assertEquals("17:00", deserialized.specificTimeAvailability?.get(0)?.end)
191+
}
192+
193+
@Test
194+
fun `AvailabilityParticipant serializes without specificTimeAvailability for backward compatibility`() {
195+
val adapter = JsonHelper.moshi().adapter(AvailabilityParticipant::class.java)
196+
val participant = AvailabilityParticipant(
197+
email = "test@nylas.com",
198+
calendarIds = listOf("calendar-123"),
199+
)
200+
201+
val json = adapter.toJson(participant)
202+
val deserialized = adapter.fromJson(json)!!
203+
assertEquals("test@nylas.com", deserialized.email)
204+
assertEquals(null, deserialized.specificTimeAvailability)
205+
assertEquals(null, deserialized.openHours)
206+
}
207+
208+
@Test
209+
fun `AvailabilityParticipant deserializes JSON with specific_time_availability`() {
210+
val adapter = JsonHelper.moshi().adapter(AvailabilityParticipant::class.java)
211+
val jsonBuffer = Buffer().writeUtf8(
212+
"""
213+
{
214+
"email": "test@nylas.com",
215+
"calendar_ids": ["primary"],
216+
"specific_time_availability": [
217+
{
218+
"date": "2026-03-18",
219+
"start": "09:00",
220+
"end": "17:00"
221+
}
222+
]
223+
}
224+
""".trimIndent(),
225+
)
226+
227+
val participant = adapter.fromJson(jsonBuffer)!!
228+
assertEquals("test@nylas.com", participant.email)
229+
assertEquals(listOf("primary"), participant.calendarIds)
230+
assertEquals(1, participant.specificTimeAvailability?.size)
231+
assertEquals("2026-03-18", participant.specificTimeAvailability?.get(0)?.date)
232+
assertEquals("09:00", participant.specificTimeAvailability?.get(0)?.start)
233+
assertEquals("17:00", participant.specificTimeAvailability?.get(0)?.end)
234+
}
235+
236+
@Test
237+
fun `AvailabilityParticipant deserializes JSON without specific_time_availability for backward compatibility`() {
238+
val adapter = JsonHelper.moshi().adapter(AvailabilityParticipant::class.java)
239+
val jsonBuffer = Buffer().writeUtf8(
240+
"""
241+
{
242+
"email": "test@nylas.com",
243+
"calendar_ids": ["calendar-123"]
244+
}
245+
""".trimIndent(),
246+
)
247+
248+
val participant = adapter.fromJson(jsonBuffer)!!
249+
assertEquals("test@nylas.com", participant.email)
250+
assertEquals(null, participant.specificTimeAvailability)
251+
}
252+
253+
@Test
254+
fun `AvailabilityParticipant Builder works with specificTimeAvailability`() {
255+
val participant = AvailabilityParticipant.Builder("test@nylas.com")
256+
.calendarIds(listOf("primary"))
257+
.specificTimeAvailability(
258+
listOf(
259+
SpecificTimeAvailability(
260+
date = "2026-03-18",
261+
start = "09:00",
262+
end = "17:00",
263+
),
264+
),
265+
)
266+
.build()
267+
268+
assertEquals("test@nylas.com", participant.email)
269+
assertEquals(listOf("primary"), participant.calendarIds)
270+
assertEquals(1, participant.specificTimeAvailability?.size)
271+
assertEquals("2026-03-18", participant.specificTimeAvailability?.get(0)?.date)
272+
assertEquals("09:00", participant.specificTimeAvailability?.get(0)?.start)
273+
assertEquals("17:00", participant.specificTimeAvailability?.get(0)?.end)
274+
}
275+
276+
@Test
277+
fun `AvailabilityParticipant Builder works without specificTimeAvailability for backward compatibility`() {
278+
val participant = AvailabilityParticipant.Builder("test@nylas.com")
279+
.calendarIds(listOf("calendar-123"))
280+
.build()
281+
282+
assertEquals("test@nylas.com", participant.email)
283+
assertEquals(null, participant.specificTimeAvailability)
284+
}
285+
}
286+
105287
@Nested
106288
inner class CrudTests {
107289
private lateinit var grantId: String
@@ -341,6 +523,47 @@ class CalendarsTest {
341523
assertEquals(adapter.toJson(getAvailabilityRequest), requestBodyCaptor.firstValue)
342524
}
343525

526+
@Test
527+
fun `getting availability with specificTimeAvailability calls requests with the correct params`() {
528+
val adapter = JsonHelper.moshi().adapter(GetAvailabilityRequest::class.java)
529+
val getAvailabilityRequest = GetAvailabilityRequest(
530+
startTime = 1737540000,
531+
endTime = 1737712800,
532+
participants = listOf(
533+
AvailabilityParticipant(
534+
email = "nylastest8@gmail.com",
535+
calendarIds = listOf("primary"),
536+
specificTimeAvailability = listOf(
537+
SpecificTimeAvailability(
538+
date = "2026-03-18",
539+
start = "09:00",
540+
end = "17:00",
541+
),
542+
),
543+
),
544+
),
545+
durationMinutes = 30,
546+
)
547+
548+
calendars.getAvailability(getAvailabilityRequest)
549+
val pathCaptor = argumentCaptor<String>()
550+
val typeCaptor = argumentCaptor<Type>()
551+
val requestBodyCaptor = argumentCaptor<String>()
552+
val queryParamCaptor = argumentCaptor<ListCalendersQueryParams>()
553+
val overrideParamCaptor = argumentCaptor<RequestOverrides>()
554+
verify(mockNylasClient).executePost<ListResponse<Calendar>>(
555+
pathCaptor.capture(),
556+
typeCaptor.capture(),
557+
requestBodyCaptor.capture(),
558+
queryParamCaptor.capture(),
559+
overrideParamCaptor.capture(),
560+
)
561+
562+
assertEquals("v3/calendars/availability", pathCaptor.firstValue)
563+
assertEquals(Types.newParameterizedType(Response::class.java, GetAvailabilityResponse::class.java), typeCaptor.firstValue)
564+
assertEquals(adapter.toJson(getAvailabilityRequest), requestBodyCaptor.firstValue)
565+
}
566+
344567
@Test
345568
fun `getting free busy calls requests with the correct params`() {
346569
val grantId = "abc-123-grant-id"

0 commit comments

Comments
 (0)