diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..549dce5a --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "cmd=$(jq -r '.tool_input.command // \"\"'); if echo \"$cmd\" | grep -q 'gh pr create'; then git diff main...HEAD --name-only | grep -q 'CHANGELOG.md' || echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"CHANGELOG.md has not been updated on this branch.\",\"additionalContext\":\"STOP: Do not open the PR yet. CHANGELOG.md has not been modified on this branch. Per .claude/instructions.md, open CHANGELOG.md and add an entry under the ## [Unreleased] section describing the changes in this PR, then retry gh pr create.\"}}'; fi" + } + ] + } + ] + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 77a90758..801019aa 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -14,7 +14,11 @@ "Bash(./gradlew:*)", "Bash(./gradlew build:*)", "Bash(gh pr *)", - "Bash(gh issue *)" + "Bash(gh issue *)", + "Bash(python3 -c ' *)", + "mcp__claude_ai_Atlassian__getJiraIssue", + "mcp__claude_ai_Atlassian__getAccessibleAtlassianResources", + "WebFetch(domain:developer.nylas.com)" ] } } diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ba6826..d28b9e49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ * `colorId` field on `Event`, `CreateEventRequest`, and `UpdateEventRequest` for Google Calendar event colors, mapped to the `color_id` JSON property. Valid values are strings `"1"` through `"11"`. See [Google Calendar Colors](https://developers.google.com/calendar/api/v3/reference/colors). +* `resources` field (`List`) on `Event`, `CreateEventRequest`, and `UpdateEventRequest` for associating rooms or resources with events +* `textDescription` field on `Event` for the plain-text description of an event (`text_description`) +* `EventResource` model with `email` (required) and `name` (optional) fields +* `select` and `tentativeAsBusy` query parameters on `ListEventQueryParams`, `FindEventQueryParams`, `CreateEventQueryParams`, and `UpdateEventQueryParams` +* `skipNylasEmail` query parameter on `SendRsvpQueryParams` to suppress Nylas notification emails when sending an RSVP +* `eventType` on `ListEventQueryParams` now accepts `List` (was `EventType`) to allow filtering by multiple event types simultaneously +* Extended `EventNotetaker.MeetingSettings` and `EventNotetakerRequest.MeetingSettings` with new fields: + - `actionItems` / `actionItemsSettings` (`ActionItemsSettings`) for AI-generated action items with optional custom instructions + - `summary` / `summarySettings` (`SummarySettings`) for AI-generated meeting summaries with optional custom instructions + - `leaveAfterSilenceSeconds` for configuring inactivity timeout + - `transcriptionSettings` (`TranscriptionSettings`) with `expectedLanguages` and `fallbackLanguage` ### Fixed * `Configuration.participants` now defaults to an empty list when the field is absent from the API response (e.g. group-event configurations), preventing a `JsonDataException` from being thrown during deserialization. diff --git a/src/main/kotlin/com/nylas/models/CreateEventQueryParams.kt b/src/main/kotlin/com/nylas/models/CreateEventQueryParams.kt index bf35ab84..5da36ec1 100644 --- a/src/main/kotlin/com/nylas/models/CreateEventQueryParams.kt +++ b/src/main/kotlin/com/nylas/models/CreateEventQueryParams.kt @@ -16,6 +16,16 @@ data class CreateEventQueryParams( */ @Json(name = "notify_participants") val notifyParticipants: Boolean? = null, + /** + * Comma-separated list of fields to return in the response. + */ + @Json(name = "select") + val select: String? = null, + /** + * When true, tentative events are counted as busy. + */ + @Json(name = "tentative_as_busy") + val tentativeAsBusy: Boolean? = null, ) : IQueryParams { /** * Builder for [CreateEventQueryParams]. @@ -25,6 +35,8 @@ data class CreateEventQueryParams( private val calendarId: String, ) { private var notifyParticipants: Boolean? = null + private var select: String? = null + private var tentativeAsBusy: Boolean? = null /** * Sets whether notifications containing the calendar event is sent to all event participants. @@ -33,6 +45,20 @@ data class CreateEventQueryParams( */ fun notifyParticipants(notifyParticipants: Boolean?) = apply { this.notifyParticipants = notifyParticipants } + /** + * Sets the comma-separated list of fields to return in the response. + * @param select The fields to return. + * @return The builder. + */ + fun select(select: String?) = apply { this.select = select } + + /** + * Sets whether tentative events are counted as busy. + * @param tentativeAsBusy Whether tentative events are counted as busy. + * @return The builder. + */ + fun tentativeAsBusy(tentativeAsBusy: Boolean?) = apply { this.tentativeAsBusy = tentativeAsBusy } + /** * Builds a [CreateEventQueryParams] instance. * @return The [CreateEventQueryParams] instance. @@ -40,6 +66,8 @@ data class CreateEventQueryParams( fun build() = CreateEventQueryParams( calendarId, notifyParticipants, + select, + tentativeAsBusy, ) } } diff --git a/src/main/kotlin/com/nylas/models/CreateEventRequest.kt b/src/main/kotlin/com/nylas/models/CreateEventRequest.kt index d42044b2..78bea9c6 100644 --- a/src/main/kotlin/com/nylas/models/CreateEventRequest.kt +++ b/src/main/kotlin/com/nylas/models/CreateEventRequest.kt @@ -101,6 +101,11 @@ data class CreateEventRequest( */ @Json(name = "color_id") val colorId: String? = null, + /** + * List of resources (e.g. rooms) to associate with the event. + */ + @Json(name = "resources") + val resources: List? = null, ) { /** * This sealed class represents the different types of event time configurations. @@ -519,6 +524,7 @@ data class CreateEventRequest( private var hideParticipant: Boolean? = null private var notetaker: EventNotetakerRequest? = null private var colorId: String? = null + private var resources: List? = null /** * Set the event title. @@ -642,6 +648,13 @@ data class CreateEventRequest( */ fun colorId(colorId: String) = apply { this.colorId = colorId } + /** + * Set the list of resources (e.g. rooms) to associate with the event. + * @param resources The list of resources. + * @return The builder. + */ + fun resources(resources: List) = apply { this.resources = resources } + /** * Builds the [CreateEventRequest] object. * @return [CreateEventRequest] object. @@ -665,6 +678,7 @@ data class CreateEventRequest( hideParticipant, notetaker, colorId, + resources, ) } } diff --git a/src/main/kotlin/com/nylas/models/Event.kt b/src/main/kotlin/com/nylas/models/Event.kt index 4c83af7c..48c8793f 100644 --- a/src/main/kotlin/com/nylas/models/Event.kt +++ b/src/main/kotlin/com/nylas/models/Event.kt @@ -162,6 +162,16 @@ data class Event( */ @Json(name = "color_id") val colorId: String? = null, + /** + * List of resources (e.g. rooms) associated with the event. + */ + @Json(name = "resources") + val resources: List? = null, + /** + * Plain-text description of the event, if available. + */ + @Json(name = "text_description") + val textDescription: String? = null, ) { /** * Get the type of object. diff --git a/src/main/kotlin/com/nylas/models/EventNotetaker.kt b/src/main/kotlin/com/nylas/models/EventNotetaker.kt index 8cfce0db..7ce54373 100644 --- a/src/main/kotlin/com/nylas/models/EventNotetaker.kt +++ b/src/main/kotlin/com/nylas/models/EventNotetaker.kt @@ -46,7 +46,60 @@ data class EventNotetaker( */ @Json(name = "transcription") val transcription: Boolean? = true, - ) + + /** + * When true, Notetaker generates action items from the meeting. + */ + @Json(name = "action_items") + val actionItems: Boolean? = null, + + /** + * Settings for action item generation. + */ + @Json(name = "action_items_settings") + val actionItemsSettings: ActionItemsSettings? = null, + + /** + * When true, Notetaker generates a summary of the meeting. + */ + @Json(name = "summary") + val summary: Boolean? = null, + + /** + * Settings for summary generation. + */ + @Json(name = "summary_settings") + val summarySettings: SummarySettings? = null, + + /** + * Number of seconds of silence after which Notetaker leaves the meeting. + */ + @Json(name = "leave_after_silence_seconds") + val leaveAfterSilenceSeconds: Int? = null, + + /** + * Settings for transcription. + */ + @Json(name = "transcription_settings") + val transcriptionSettings: TranscriptionSettings? = null, + ) { + data class ActionItemsSettings( + @Json(name = "custom_instructions") + val customInstructions: String? = null, + ) + + data class SummarySettings( + @Json(name = "custom_instructions") + val customInstructions: String? = null, + ) + + data class TranscriptionSettings( + @Json(name = "expected_languages") + val expectedLanguages: List? = null, + @Json(name = "fallback_language") + val fallbackLanguage: String? = null, + ) + } } /** @@ -87,5 +140,58 @@ data class EventNotetakerRequest( */ @Json(name = "transcription") val transcription: Boolean? = true, - ) + + /** + * When true, Notetaker generates action items from the meeting. + */ + @Json(name = "action_items") + val actionItems: Boolean? = null, + + /** + * Settings for action item generation. + */ + @Json(name = "action_items_settings") + val actionItemsSettings: ActionItemsSettings? = null, + + /** + * When true, Notetaker generates a summary of the meeting. + */ + @Json(name = "summary") + val summary: Boolean? = null, + + /** + * Settings for summary generation. + */ + @Json(name = "summary_settings") + val summarySettings: SummarySettings? = null, + + /** + * Number of seconds of silence after which Notetaker leaves the meeting. + */ + @Json(name = "leave_after_silence_seconds") + val leaveAfterSilenceSeconds: Int? = null, + + /** + * Settings for transcription. + */ + @Json(name = "transcription_settings") + val transcriptionSettings: TranscriptionSettings? = null, + ) { + data class ActionItemsSettings( + @Json(name = "custom_instructions") + val customInstructions: String? = null, + ) + + data class SummarySettings( + @Json(name = "custom_instructions") + val customInstructions: String? = null, + ) + + data class TranscriptionSettings( + @Json(name = "expected_languages") + val expectedLanguages: List? = null, + @Json(name = "fallback_language") + val fallbackLanguage: String? = null, + ) + } } diff --git a/src/main/kotlin/com/nylas/models/EventResource.kt b/src/main/kotlin/com/nylas/models/EventResource.kt new file mode 100644 index 00000000..5af43dcc --- /dev/null +++ b/src/main/kotlin/com/nylas/models/EventResource.kt @@ -0,0 +1,12 @@ +package com.nylas.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EventResource( + @Json(name = "email") + val email: String, + @Json(name = "name") + val name: String? = null, +) diff --git a/src/main/kotlin/com/nylas/models/FindEventQueryParams.kt b/src/main/kotlin/com/nylas/models/FindEventQueryParams.kt index 4e739f9f..c43cb536 100644 --- a/src/main/kotlin/com/nylas/models/FindEventQueryParams.kt +++ b/src/main/kotlin/com/nylas/models/FindEventQueryParams.kt @@ -11,6 +11,16 @@ data class FindEventQueryParams( */ @Json(name = "calendar_id") val calendarId: String, + /** + * Comma-separated list of fields to return in the response. + */ + @Json(name = "select") + val select: String? = null, + /** + * When true, tentative events are counted as busy. + */ + @Json(name = "tentative_as_busy") + val tentativeAsBusy: Boolean? = null, ) : IQueryParams { /** * Builder for [FindEventQueryParams]. @@ -19,12 +29,31 @@ data class FindEventQueryParams( data class Builder( private val calendarId: String, ) { + private var select: String? = null + private var tentativeAsBusy: Boolean? = null + + /** + * Sets the comma-separated list of fields to return in the response. + * @param select The fields to return. + * @return The builder. + */ + fun select(select: String?) = apply { this.select = select } + + /** + * Sets whether tentative events are counted as busy. + * @param tentativeAsBusy Whether tentative events are counted as busy. + * @return The builder. + */ + fun tentativeAsBusy(tentativeAsBusy: Boolean?) = apply { this.tentativeAsBusy = tentativeAsBusy } + /** * Builds a new [FindEventQueryParams] instance. * @return [FindEventQueryParams] */ fun build() = FindEventQueryParams( calendarId, + select, + tentativeAsBusy, ) } } diff --git a/src/main/kotlin/com/nylas/models/ListEventQueryParams.kt b/src/main/kotlin/com/nylas/models/ListEventQueryParams.kt index 3b7f81ec..6bd851d4 100644 --- a/src/main/kotlin/com/nylas/models/ListEventQueryParams.kt +++ b/src/main/kotlin/com/nylas/models/ListEventQueryParams.kt @@ -118,12 +118,22 @@ data class ListEventQueryParams( @Json(name = "attendees") val attendees: List? = null, /** - * Filter for events with the specified event type. + * Filter for events with the specified event type(s). * You can pass this query parameter multiple times to select or exclude multiple event types. * (Google only) */ @Json(name = "event_type") - val eventType: EventType? = null, + val eventType: List? = null, + /** + * Comma-separated list of fields to return in the response. + */ + @Json(name = "select") + val select: String? = null, + /** + * When true, tentative events are counted as busy. + */ + @Json(name = "tentative_as_busy") + val tentativeAsBusy: Boolean? = null, ) : IQueryParams { /** * Builder for [ListEventQueryParams]. @@ -151,7 +161,9 @@ data class ListEventQueryParams( private var updatedBefore: Long? = null private var updatedAfter: Long? = null private var attendees: List? = null - private var eventType: EventType? = null + private var eventType: List? = null + private var select: String? = null + private var tentativeAsBusy: Boolean? = null /** * Sets the maximum number of objects to return. @@ -291,13 +303,27 @@ data class ListEventQueryParams( fun attendees(attendees: List?) = apply { this.attendees = attendees } /** - * Sets the event type to filter for events with. - * You can pass this query parameter multiple times to select or exclude multiple event types. + * Sets the event type(s) to filter for events with. + * You can pass multiple event types to select or exclude multiple event types. * (Google only) - * @param eventType The event type to filter for events with. + * @param eventType The list of event types to filter for. + * @return The builder. + */ + fun eventType(eventType: List?) = apply { this.eventType = eventType } + + /** + * Sets the comma-separated list of fields to return in the response. + * @param select The fields to return. + * @return The builder. + */ + fun select(select: String?) = apply { this.select = select } + + /** + * Sets whether tentative events are counted as busy. + * @param tentativeAsBusy Whether tentative events are counted as busy. * @return The builder. */ - fun eventType(eventType: EventType?) = apply { this.eventType = eventType } + fun tentativeAsBusy(tentativeAsBusy: Boolean?) = apply { this.tentativeAsBusy = tentativeAsBusy } /** * Builds a [ListEventQueryParams] instance. @@ -323,6 +349,8 @@ data class ListEventQueryParams( updatedAfter, attendees, eventType, + select, + tentativeAsBusy, ) } } diff --git a/src/main/kotlin/com/nylas/models/SendRsvpQueryParams.kt b/src/main/kotlin/com/nylas/models/SendRsvpQueryParams.kt index 377bbeb2..4cb6bb3a 100644 --- a/src/main/kotlin/com/nylas/models/SendRsvpQueryParams.kt +++ b/src/main/kotlin/com/nylas/models/SendRsvpQueryParams.kt @@ -11,6 +11,11 @@ data class SendRsvpQueryParams( */ @Json(name = "calendar_id") val calendarId: String, + /** + * When true, Nylas does not send an email to the event organizer when the RSVP is sent. + */ + @Json(name = "skip_nylas_email") + val skipNylasEmail: Boolean? = null, ) : IQueryParams { /** @@ -18,11 +23,19 @@ data class SendRsvpQueryParams( * @param calendarId The ID of the calendar to create the event in. */ data class Builder(private val calendarId: String) { + private var skipNylasEmail: Boolean? = null + + /** + * Sets whether to skip sending a Nylas email to the event organizer. + * @param skipNylasEmail Whether to skip the email notification. + * @return The builder. + */ + fun skipNylasEmail(skipNylasEmail: Boolean?) = apply { this.skipNylasEmail = skipNylasEmail } /** * Builds a [SendRsvpQueryParams] instance. * @return The [SendRsvpQueryParams] instance. */ - fun build() = SendRsvpQueryParams(calendarId) + fun build() = SendRsvpQueryParams(calendarId, skipNylasEmail) } } diff --git a/src/main/kotlin/com/nylas/models/UpdateEventQueryParams.kt b/src/main/kotlin/com/nylas/models/UpdateEventQueryParams.kt index 99006f7f..8a052bc6 100644 --- a/src/main/kotlin/com/nylas/models/UpdateEventQueryParams.kt +++ b/src/main/kotlin/com/nylas/models/UpdateEventQueryParams.kt @@ -17,6 +17,16 @@ data class UpdateEventQueryParams( */ @Json(name = "notify_participants") val notifyParticipants: Boolean? = null, + /** + * Comma-separated list of fields to return in the response. + */ + @Json(name = "select") + val select: String? = null, + /** + * When true, tentative events are counted as busy. + */ + @Json(name = "tentative_as_busy") + val tentativeAsBusy: Boolean? = null, ) : IQueryParams { /** @@ -27,6 +37,8 @@ data class UpdateEventQueryParams( private val calendarId: String, ) { private var notifyParticipants: Boolean? = null + private var select: String? = null + private var tentativeAsBusy: Boolean? = null /** * Sets whether to send email notifications containing the calendar event to all event participants. @@ -36,6 +48,20 @@ data class UpdateEventQueryParams( */ fun notifyParticipants(notifyParticipants: Boolean?) = apply { this.notifyParticipants = notifyParticipants } + /** + * Sets the comma-separated list of fields to return in the response. + * @param select The fields to return. + * @return The builder. + */ + fun select(select: String?) = apply { this.select = select } + + /** + * Sets whether tentative events are counted as busy. + * @param tentativeAsBusy Whether tentative events are counted as busy. + * @return The builder. + */ + fun tentativeAsBusy(tentativeAsBusy: Boolean?) = apply { this.tentativeAsBusy = tentativeAsBusy } + /** * Builds a [UpdateEventQueryParams] instance. * @return The [UpdateEventQueryParams] instance. @@ -43,6 +69,8 @@ data class UpdateEventQueryParams( fun build() = UpdateEventQueryParams( calendarId, notifyParticipants, + select, + tentativeAsBusy, ) } } diff --git a/src/main/kotlin/com/nylas/models/UpdateEventRequest.kt b/src/main/kotlin/com/nylas/models/UpdateEventRequest.kt index 6c3af4b4..6c51e59f 100644 --- a/src/main/kotlin/com/nylas/models/UpdateEventRequest.kt +++ b/src/main/kotlin/com/nylas/models/UpdateEventRequest.kt @@ -101,6 +101,11 @@ data class UpdateEventRequest( */ @Json(name = "color_id") val colorId: String? = null, + /** + * List of resources (e.g. rooms) to associate with the event. + */ + @Json(name = "resources") + val resources: List? = null, ) { /** * This sealed class represents the different types of event time configurations. @@ -605,6 +610,7 @@ data class UpdateEventRequest( private var hideParticipant: Boolean? = null private var notetaker: EventNotetakerRequest? = null private var colorId: String? = null + private var resources: List? = null /** * Set the when object. @@ -736,6 +742,13 @@ data class UpdateEventRequest( */ fun colorId(colorId: String) = apply { this.colorId = colorId } + /** + * Update the list of resources (e.g. rooms) to associate with the event. + * @param resources The list of resources. + * @return The builder. + */ + fun resources(resources: List) = apply { this.resources = resources } + /** * Builds the [UpdateEventRequest] object. * @return [UpdateEventRequest] object. @@ -759,6 +772,7 @@ data class UpdateEventRequest( hideParticipant, notetaker, colorId, + resources, ) } } diff --git a/src/test/kotlin/com/nylas/resources/EventsTests.kt b/src/test/kotlin/com/nylas/resources/EventsTests.kt index 8f6ffac5..0ee2b19d 100644 --- a/src/test/kotlin/com/nylas/resources/EventsTests.kt +++ b/src/test/kotlin/com/nylas/resources/EventsTests.kt @@ -863,6 +863,7 @@ class EventsTests { assertEquals("v3/grants/$grantId/events", pathCaptor.firstValue) assertEquals(Types.newParameterizedType(ListResponse::class.java, Event::class.java), typeCaptor.firstValue) + assertEquals(listEventQueryParams, queryParamCaptor.firstValue) } @Test @@ -917,6 +918,7 @@ class EventsTests { assertEquals("v3/grants/$grantId/events/$eventId", pathCaptor.firstValue) assertEquals(Types.newParameterizedType(Response::class.java, Event::class.java), typeCaptor.firstValue) + assertEquals(findEventQueryParams, queryParamCaptor.firstValue) } @Test @@ -1202,6 +1204,7 @@ class EventsTests { assertEquals("v3/grants/$grantId/events/$eventId", pathCaptor.firstValue) assertEquals(DeleteResponse::class.java, typeCaptor.firstValue) + assertEquals(destroyEventQueryParams, queryParamCaptor.firstValue) } } @@ -1248,6 +1251,201 @@ class EventsTests { assertEquals("v3/grants/$grantId/events/$eventId/send-rsvp", pathCaptor.firstValue) assertEquals(DeleteResponse::class.java, typeCaptor.firstValue) assertEquals(adapter.toJson(sendRsvpRequest), requestBodyCaptor.firstValue) + assertEquals(sendRsvpQueryParams, queryParamCaptor.firstValue) + } + + @Test + fun `sending RSVP with skipNylasEmail calls requests with the correct params`() { + val eventId = "event-123" + val sendRsvpRequest = SendRsvpRequest(status = RsvpStatus.NO) + val sendRsvpQueryParams = SendRsvpQueryParams( + calendarId = "calendar-id", + skipNylasEmail = true, + ) + + events.sendRsvp(grantId, eventId, sendRsvpRequest, sendRsvpQueryParams) + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executePost( + any(), + any(), + any(), + queryParamCaptor.capture(), + anyOrNull(), + ) + + assertEquals(true, queryParamCaptor.firstValue.skipNylasEmail) + } + } + + @Nested + inner class NewFieldTests { + @Test + fun `Event deserializes resources and textDescription fields`() { + val adapter = JsonHelper.moshi().adapter(Event::class.java) + val jsonBuffer = Buffer().writeUtf8( + """ + { + "id": "event-123", + "grant_id": "grant-456", + "calendar_id": "primary", + "object": "event", + "when": {"date": "2024-06-18", "object": "date"}, + "resources": [ + {"email": "room@example.com", "name": "Conference Room A"}, + {"email": "projector@example.com"} + ], + "text_description": "Plain text version of description" + } + """.trimIndent(), + ) + + val event = adapter.fromJson(jsonBuffer)!! + assertEquals(2, event.resources?.size) + assertEquals("room@example.com", event.resources?.get(0)?.email) + assertEquals("Conference Room A", event.resources?.get(0)?.name) + assertEquals("projector@example.com", event.resources?.get(1)?.email) + assertEquals(null, event.resources?.get(1)?.name) + assertEquals("Plain text version of description", event.textDescription) + } + + @Test + fun `Event with null resources and textDescription deserializes correctly`() { + val adapter = JsonHelper.moshi().adapter(Event::class.java) + val jsonBuffer = Buffer().writeUtf8( + """ + { + "id": "event-123", + "grant_id": "grant-456", + "calendar_id": "primary", + "object": "event", + "when": {"date": "2024-06-18", "object": "date"} + } + """.trimIndent(), + ) + + val event = adapter.fromJson(jsonBuffer)!! + assertEquals(null, event.resources) + assertEquals(null, event.textDescription) + } + + @Test + fun `CreateEventRequest with resources serializes correctly`() { + val adapter = JsonHelper.moshi().adapter(CreateEventRequest::class.java) + val request = CreateEventRequest( + whenObj = CreateEventRequest.When.Time(1620000000), + resources = listOf( + EventResource(email = "room@example.com", name = "Conference Room A"), + EventResource(email = "projector@example.com"), + ), + ) + + val jsonMap = JsonHelper.moshi().adapter(Map::class.java).fromJson(adapter.toJson(request))!! + val resources = jsonMap["resources"] as List<*> + assertEquals(2, resources.size) + assertEquals("room@example.com", (resources[0] as Map<*, *>)["email"]) + assertEquals("Conference Room A", (resources[0] as Map<*, *>)["name"]) + assertEquals("projector@example.com", (resources[1] as Map<*, *>)["email"]) + } + + @Test + fun `UpdateEventRequest with resources serializes correctly`() { + val adapter = JsonHelper.moshi().adapter(UpdateEventRequest::class.java) + val request = UpdateEventRequest( + resources = listOf(EventResource(email = "room@example.com", name = "Room B")), + ) + + val jsonMap = JsonHelper.moshi().adapter(Map::class.java).fromJson(adapter.toJson(request))!! + val resources = jsonMap["resources"] as List<*> + assertEquals(1, resources.size) + assertEquals("room@example.com", (resources[0] as Map<*, *>)["email"]) + assertEquals("Room B", (resources[0] as Map<*, *>)["name"]) + } + + @Test + fun `EventNotetaker deserializes new meeting settings fields`() { + val adapter = JsonHelper.moshi().adapter(EventNotetaker::class.java) + val jsonBuffer = Buffer().writeUtf8( + """ + { + "id": "notetaker-123", + "name": "My Bot", + "meeting_settings": { + "video_recording": true, + "audio_recording": true, + "transcription": true, + "action_items": true, + "action_items_settings": {"custom_instructions": "Focus on action items"}, + "summary": true, + "summary_settings": {"custom_instructions": "Be concise"}, + "leave_after_silence_seconds": 300, + "transcription_settings": { + "expected_languages": ["en", "es"], + "fallback_language": "en" + } + } + } + """.trimIndent(), + ) + + val notetaker = adapter.fromJson(jsonBuffer)!! + val settings = notetaker.meetingSettings!! + assertEquals(true, settings.actionItems) + assertEquals("Focus on action items", settings.actionItemsSettings?.customInstructions) + assertEquals(true, settings.summary) + assertEquals("Be concise", settings.summarySettings?.customInstructions) + assertEquals(300, settings.leaveAfterSilenceSeconds) + assertEquals(listOf("en", "es"), settings.transcriptionSettings?.expectedLanguages) + assertEquals("en", settings.transcriptionSettings?.fallbackLanguage) + } + + @Test + fun `ListEventQueryParams supports multiple eventTypes`() { + val params = ListEventQueryParams.Builder("primary") + .eventType(listOf(EventType.DEFAULT, EventType.OUT_OF_OFFICE)) + .select("id,title") + .tentativeAsBusy(true) + .build() + + assertEquals(listOf(EventType.DEFAULT, EventType.OUT_OF_OFFICE), params.eventType) + assertEquals("id,title", params.select) + assertEquals(true, params.tentativeAsBusy) + } + + @Test + fun `FindEventQueryParams supports select and tentativeAsBusy`() { + val params = FindEventQueryParams.Builder("primary") + .select("id,title,when") + .tentativeAsBusy(false) + .build() + + assertEquals("id,title,when", params.select) + assertEquals(false, params.tentativeAsBusy) + } + + @Test + fun `CreateEventQueryParams supports select and tentativeAsBusy`() { + val params = CreateEventQueryParams.Builder("primary") + .notifyParticipants(true) + .select("id,title") + .tentativeAsBusy(true) + .build() + + assertEquals("id,title", params.select) + assertEquals(true, params.tentativeAsBusy) + assertEquals(true, params.notifyParticipants) + } + + @Test + fun `UpdateEventQueryParams supports select and tentativeAsBusy`() { + val params = UpdateEventQueryParams.Builder("primary") + .notifyParticipants(false) + .select("id,title") + .tentativeAsBusy(false) + .build() + + assertEquals("id,title", params.select) + assertEquals(false, params.tentativeAsBusy) + assertEquals(false, params.notifyParticipants) } } }