Skip to content

Commit 21fb363

Browse files
committed
fix: ConferenceProvider did not include all the supported (and deprecated) types that the API handled.
1 parent 603dc64 commit 21fb363

File tree

6 files changed

+307
-18
lines changed

6 files changed

+307
-18
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Nylas Java SDK Changelog
22

3+
## [Unreleased]
4+
5+
### Fixed
6+
* ConferenceProvider did not include all the supported (and deprecated) types that the API handled.
7+
38
## [2.12.0]
49

510
### Added
Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,18 @@
11
package com.nylas.models
22

3-
import com.squareup.moshi.Json
4-
53
/**
64
* Enum for the different conferencing providers.
5+
* Uses a custom JsonAdapter for backward compatibility with multiple JSON representations.
76
*/
87
enum class ConferencingProvider {
9-
@Json(name = "Zoom Meeting")
10-
ZOOM_MEETING,
11-
12-
@Json(name = "Google Meet")
8+
// Current supported values
139
GOOGLE_MEET,
14-
15-
@Json(name = "Microsoft Teams")
16-
MICROSOFT_TEAMS,
17-
18-
@Json(name = "WebEx")
19-
WEBEX,
20-
21-
@Json(name = "GoToMeeting")
2210
GOTOMEETING,
23-
24-
@Json(name = "skypeForConsumer")
11+
MICROSOFT_TEAMS,
12+
SKYPE_FOR_BUSINESS,
2513
SKYPE_FOR_CONSUMER,
26-
27-
@Json(name = "unknown")
14+
TEAMS_FOR_BUSINESS,
15+
WEBEX,
16+
ZOOM_MEETING,
2817
UNKNOWN,
2918
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.nylas.util
2+
3+
import com.nylas.models.ConferencingProvider
4+
import com.squareup.moshi.*
5+
import java.io.IOException
6+
7+
/**
8+
* Custom JsonAdapter for ConferencingProvider enum that provides backward compatibility
9+
* by mapping multiple JSON values to the same enum constant.
10+
* @suppress Not for public use.
11+
*/
12+
class ConferencingProviderAdapter : JsonAdapter<ConferencingProvider>() {
13+
14+
companion object {
15+
/**
16+
* Mapping of JSON string values to enum constants.
17+
* This allows multiple JSON representations to map to the same enum value.
18+
*/
19+
private val JSON_TO_ENUM_MAP = mapOf(
20+
// Current/preferred values
21+
"Google Meet" to ConferencingProvider.GOOGLE_MEET,
22+
"GoToMeeting" to ConferencingProvider.GOTOMEETING,
23+
"Microsoft Teams" to ConferencingProvider.MICROSOFT_TEAMS,
24+
"Skype for Business" to ConferencingProvider.SKYPE_FOR_BUSINESS,
25+
"Skype for Consumer" to ConferencingProvider.SKYPE_FOR_CONSUMER,
26+
"Teams for Business" to ConferencingProvider.TEAMS_FOR_BUSINESS,
27+
"WebEx" to ConferencingProvider.WEBEX,
28+
"Zoom Meeting" to ConferencingProvider.ZOOM_MEETING,
29+
"unknown" to ConferencingProvider.UNKNOWN,
30+
31+
// Deprecated/legacy values (for backward compatibility)
32+
"skypeForConsumer" to ConferencingProvider.SKYPE_FOR_CONSUMER,
33+
"skypeForBusiness" to ConferencingProvider.SKYPE_FOR_BUSINESS,
34+
"teamsForBusiness" to ConferencingProvider.TEAMS_FOR_BUSINESS,
35+
)
36+
37+
/**
38+
* Mapping of enum constants to their preferred JSON representation.
39+
* Used for serialization.
40+
*/
41+
private val ENUM_TO_JSON_MAP = mapOf(
42+
ConferencingProvider.GOOGLE_MEET to "Google Meet",
43+
ConferencingProvider.GOTOMEETING to "GoToMeeting",
44+
ConferencingProvider.MICROSOFT_TEAMS to "Microsoft Teams",
45+
ConferencingProvider.SKYPE_FOR_BUSINESS to "Skype for Business",
46+
ConferencingProvider.SKYPE_FOR_CONSUMER to "Skype for Consumer",
47+
ConferencingProvider.TEAMS_FOR_BUSINESS to "Teams for Business",
48+
ConferencingProvider.WEBEX to "WebEx",
49+
ConferencingProvider.ZOOM_MEETING to "Zoom Meeting",
50+
ConferencingProvider.UNKNOWN to "unknown",
51+
)
52+
}
53+
54+
@FromJson
55+
@Throws(IOException::class)
56+
override fun fromJson(reader: JsonReader): ConferencingProvider? {
57+
if (reader.peek() == JsonReader.Token.NULL) {
58+
reader.nextNull<Any>()
59+
return null
60+
}
61+
62+
val jsonValue = reader.nextString()
63+
return JSON_TO_ENUM_MAP[jsonValue] ?: ConferencingProvider.UNKNOWN
64+
}
65+
66+
@ToJson
67+
override fun toJson(writer: JsonWriter, value: ConferencingProvider?) {
68+
if (value == null) {
69+
writer.nullValue()
70+
return
71+
}
72+
73+
val jsonValue = ENUM_TO_JSON_MAP[value] ?: "unknown"
74+
writer.value(jsonValue)
75+
}
76+
}

src/main/kotlin/com/nylas/util/JsonHelper.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class JsonHelper {
4040
.add(MicrosoftAdminConsentCredentialDataAdapter())
4141
.add(GoogleServiceAccountCredentialDataAdapter())
4242
.add(ConnectorOverrideCredentialDataAdapter())
43+
.add(ConferencingProviderAdapter())
4344
// Polymorphic adapters
4445
.add(WHEN_JSON_FACTORY)
4546
.add(FREE_BUSY_JSON_FACTORY)

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,93 @@ class EventsTests {
222222
val whenDate = event.getWhen() as When.Date
223223
assertEquals("2024-06-18", whenDate.date)
224224
}
225+
226+
@Test
227+
fun `Event deserializes with legacy conferencing provider values`() {
228+
val adapter = JsonHelper.moshi().adapter(Event::class.java)
229+
val jsonBuffer =
230+
Buffer().writeUtf8(
231+
"""
232+
{
233+
"busy": true,
234+
"calendar_id": "7d93zl2palhxqdy6e5qinsakt",
235+
"conferencing": {
236+
"provider": "skypeForBusiness",
237+
"details": {
238+
"url": "https://skype.example.com/meeting"
239+
}
240+
},
241+
"created_at": 1661874192,
242+
"grant_id": "41009df5-bf11-4c97-aa18-b285b5f2e386",
243+
"id": "5d3qmne77v32r8l4phyuksl2x",
244+
"object": "event",
245+
"status": "confirmed",
246+
"title": "Legacy Provider Test",
247+
"updated_at": 1661874192,
248+
"when": {
249+
"start_time": 1661874192,
250+
"end_time": 1661877792,
251+
"object": "timespan"
252+
}
253+
}
254+
""".trimIndent(),
255+
)
256+
257+
val event = adapter.fromJson(jsonBuffer)!!
258+
assertIs<Event>(event)
259+
assertIs<Conferencing.Details>(event.conferencing)
260+
val conferencingDetails = event.conferencing as Conferencing.Details
261+
// Legacy "skypeForBusiness" should map to SKYPE_FOR_BUSINESS
262+
assertEquals(ConferencingProvider.SKYPE_FOR_BUSINESS, conferencingDetails.provider)
263+
assertEquals("https://skype.example.com/meeting", conferencingDetails.details.url)
264+
}
265+
266+
@Test
267+
fun `Event serializes and deserializes conferencing provider consistently`() {
268+
val adapter = JsonHelper.moshi().adapter(Event::class.java)
269+
270+
// Test all current enum values
271+
val enumValues = ConferencingProvider.values()
272+
273+
for (provider in enumValues) {
274+
val eventJson = """
275+
{
276+
"busy": true,
277+
"calendar_id": "test-calendar",
278+
"conferencing": {
279+
"provider": "${provider.name}",
280+
"details": {
281+
"url": "https://example.com"
282+
}
283+
},
284+
"created_at": 1661874192,
285+
"grant_id": "test-grant",
286+
"id": "test-event",
287+
"object": "event",
288+
"status": "confirmed",
289+
"title": "Test Event",
290+
"updated_at": 1661874192,
291+
"when": {
292+
"start_time": 1661874192,
293+
"end_time": 1661877792,
294+
"object": "timespan"
295+
}
296+
}
297+
""".trimIndent()
298+
299+
// First deserialize with the enum name
300+
val event = adapter.fromJson(Buffer().writeUtf8(eventJson))
301+
if (event?.conferencing is Conferencing.Details) {
302+
// Then serialize back and verify it uses the correct JSON format
303+
val serialized = adapter.toJson(event)
304+
val deserialized = adapter.fromJson(serialized)
305+
306+
val originalProvider = (event.conferencing as Conferencing.Details).provider
307+
val roundTripProvider = (deserialized?.conferencing as? Conferencing.Details)?.provider
308+
assertEquals(originalProvider, roundTripProvider, "Round trip failed for provider $provider")
309+
}
310+
}
311+
}
225312
}
226313

227314
@Nested
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package com.nylas.util
2+
3+
import com.nylas.models.ConferencingProvider
4+
import com.squareup.moshi.Moshi
5+
import okio.Buffer
6+
import kotlin.test.Test
7+
import kotlin.test.assertEquals
8+
import kotlin.test.assertNull
9+
10+
class ConferencingProviderAdapterTest {
11+
12+
private val moshi = Moshi.Builder()
13+
.add(ConferencingProviderAdapter())
14+
.build()
15+
16+
private val adapter = moshi.adapter(ConferencingProvider::class.java)
17+
18+
@Test
19+
fun `deserializes current JSON values correctly`() {
20+
assertEquals(ConferencingProvider.GOOGLE_MEET, adapter.fromJson("\"Google Meet\""))
21+
assertEquals(ConferencingProvider.GOTOMEETING, adapter.fromJson("\"GoToMeeting\""))
22+
assertEquals(ConferencingProvider.MICROSOFT_TEAMS, adapter.fromJson("\"Microsoft Teams\""))
23+
assertEquals(ConferencingProvider.SKYPE_FOR_BUSINESS, adapter.fromJson("\"Skype for Business\""))
24+
assertEquals(ConferencingProvider.SKYPE_FOR_CONSUMER, adapter.fromJson("\"Skype for Consumer\""))
25+
assertEquals(ConferencingProvider.TEAMS_FOR_BUSINESS, adapter.fromJson("\"Teams for Business\""))
26+
assertEquals(ConferencingProvider.WEBEX, adapter.fromJson("\"WebEx\""))
27+
assertEquals(ConferencingProvider.ZOOM_MEETING, adapter.fromJson("\"Zoom Meeting\""))
28+
assertEquals(ConferencingProvider.UNKNOWN, adapter.fromJson("\"unknown\""))
29+
}
30+
31+
@Test
32+
fun `deserializes legacy deprecated JSON values correctly`() {
33+
// These should map to the same enum values as their current counterparts
34+
assertEquals(ConferencingProvider.SKYPE_FOR_CONSUMER, adapter.fromJson("\"skypeForConsumer\""))
35+
assertEquals(ConferencingProvider.SKYPE_FOR_BUSINESS, adapter.fromJson("\"skypeForBusiness\""))
36+
assertEquals(ConferencingProvider.TEAMS_FOR_BUSINESS, adapter.fromJson("\"teamsForBusiness\""))
37+
}
38+
39+
@Test
40+
fun `deserializes unknown values to UNKNOWN enum`() {
41+
assertEquals(ConferencingProvider.UNKNOWN, adapter.fromJson("\"unknownProvider\""))
42+
assertEquals(ConferencingProvider.UNKNOWN, adapter.fromJson("\"someRandomValue\""))
43+
assertEquals(ConferencingProvider.UNKNOWN, adapter.fromJson("\"\""))
44+
}
45+
46+
@Test
47+
fun `deserializes null values correctly`() {
48+
assertNull(adapter.fromJson("null"))
49+
}
50+
51+
@Test
52+
fun `serializes to current JSON format`() {
53+
assertEquals("\"Google Meet\"", adapter.toJson(ConferencingProvider.GOOGLE_MEET))
54+
assertEquals("\"GoToMeeting\"", adapter.toJson(ConferencingProvider.GOTOMEETING))
55+
assertEquals("\"Microsoft Teams\"", adapter.toJson(ConferencingProvider.MICROSOFT_TEAMS))
56+
assertEquals("\"Skype for Business\"", adapter.toJson(ConferencingProvider.SKYPE_FOR_BUSINESS))
57+
assertEquals("\"Skype for Consumer\"", adapter.toJson(ConferencingProvider.SKYPE_FOR_CONSUMER))
58+
assertEquals("\"Teams for Business\"", adapter.toJson(ConferencingProvider.TEAMS_FOR_BUSINESS))
59+
assertEquals("\"WebEx\"", adapter.toJson(ConferencingProvider.WEBEX))
60+
assertEquals("\"Zoom Meeting\"", adapter.toJson(ConferencingProvider.ZOOM_MEETING))
61+
assertEquals("\"unknown\"", adapter.toJson(ConferencingProvider.UNKNOWN))
62+
}
63+
64+
@Test
65+
fun `serializes null values correctly`() {
66+
assertEquals("null", adapter.toJson(null))
67+
}
68+
69+
@Test
70+
fun `round trip serialization with current values`() {
71+
val values = ConferencingProvider.values()
72+
for (value in values) {
73+
val json = adapter.toJson(value)
74+
val deserialized = adapter.fromJson(json)
75+
assertEquals(value, deserialized, "Round trip failed for $value")
76+
}
77+
}
78+
79+
@Test
80+
fun `backward compatibility roundtrip test`() {
81+
// Test that legacy JSON values can be deserialized and then serialized to current format
82+
val legacyToCurrentMappings = mapOf(
83+
"skypeForConsumer" to "Skype for Consumer",
84+
"skypeForBusiness" to "Skype for Business",
85+
"teamsForBusiness" to "Teams for Business"
86+
)
87+
88+
for ((legacyJson, expectedCurrentJson) in legacyToCurrentMappings) {
89+
val deserialized = adapter.fromJson("\"$legacyJson\"")
90+
val serialized = adapter.toJson(deserialized)
91+
assertEquals("\"$expectedCurrentJson\"", serialized,
92+
"Legacy value $legacyJson should serialize to current format $expectedCurrentJson")
93+
}
94+
}
95+
96+
@Test
97+
fun `integration test with JSON objects containing conferencing provider`() {
98+
// Test within a larger JSON context using full JsonHelper setup
99+
val moshiWithAllAdapters = JsonHelper.moshi()
100+
val eventAdapter = moshiWithAllAdapters.adapter(TestEvent::class.java)
101+
102+
val eventJson = """
103+
{
104+
"conferencing": {
105+
"provider": "skypeForConsumer",
106+
"details": {
107+
"url": "https://example.com"
108+
}
109+
}
110+
}
111+
""".trimIndent()
112+
113+
val event = eventAdapter.fromJson(eventJson)
114+
assertEquals(ConferencingProvider.SKYPE_FOR_CONSUMER, event?.conferencing?.provider)
115+
116+
// When serialized back, it should use the current format
117+
val serializedJson = eventAdapter.toJson(event)
118+
val deserializedAgain = eventAdapter.fromJson(serializedJson)
119+
assertEquals(ConferencingProvider.SKYPE_FOR_CONSUMER, deserializedAgain?.conferencing?.provider)
120+
}
121+
122+
// Test data classes for integration testing
123+
data class TestEvent(
124+
val conferencing: TestConferencing?
125+
)
126+
127+
data class TestConferencing(
128+
val provider: ConferencingProvider?,
129+
val details: Map<String, Any>?
130+
)
131+
}

0 commit comments

Comments
 (0)