https://a.storyblok.com/f/270183/1368x665/d80f2b5c9f/kotlin_sdk-updates.png

Anuncio del SDK de servidor Kotlin de Vonage

Publicado el October 25, 2024

Tiempo de lectura: 16 minutos

Introducción

Cuando Kotlin apareció por primera vez a principios de 2010, pocos podían imaginar que sería un lenguaje de programación top 20 que es hoy, superando a otros lenguajes JVM establecidos como Groovy y Scala. Desde que se convirtió en el lenguaje de facto para el desarrollo de aplicaciones Android, el ecosistema no ha hecho más que expandirse con ricas características y una conferencia anual dedicada.

A pesar de la cadencia de lanzamiento actualizada y la afluencia de características del lenguaje para mantenerse relevante, Kotlin sigue siendo un lenguaje popular más allá de las aplicaciones móviles. Para el desarrollo del lado del servidor, Kotlin es una excelente opción por varias razones: un rico ecosistema de bibliotecas existentes (algo de lo que se benefician todos los lenguajes basados en JVM), incluso la última versión 2.0 sigue siendo compatible con el código de bytes de Java 8, que ya tiene una década, está elegantemente diseñado desde cero sin características heredadas y, en general, hace que la programación sea un placer con un mínimo de repeticiones y un potente pero intuitivo azúcar sintáctico y semántico.

Me complace anunciar que, después de trabajar en él a tiempo completo durante un trimestre, el Vonage Kotlin SDK ya está disponible oficialmente y es compatible con v1.0.0¡! Está publicado en Maven Centraldonde también encontrarás instrucciones para utilizarlo en tu sistema de compilación. Con este SDK, puedes utilizar todas las APIs de GA Vonage en Kotlin para hacer llamadas de voz, enviar y recibir SMS, y crear aplicaciones de Video. En esta entrada del blog, voy a proporcionar cómo y por qué este SDK llegó a ser.

Fondo

Reconociendo la creciente prevalencia de Kotlin en el lado del servidor, y la cara de asco que muchos desarrolladores Kotlin sienten cuando tienen que trabajar con Java, estábamos pensando en proporcionar fragmentos de código Kotlin para el Java Server SDK. Después de todo, una de las ventajas de Kotlin es su compatibilidad sin fisuras con Java. Sin embargo, decidimos ir un paso más allá. Queríamos ofrecer a los desarrolladores de Kotlin una experiencia realmente de primera clase, pero sin el coste y la carga de mantenimiento que supone desarrollar un SDK completamente desde cero. Nuestro objetivo era crear una prueba de concepto en un plazo relativamente corto, que con el tiempo diera lugar a un SDK completo, sin dejar de utilizar el SDK de Java para su implementación y modelo de datos. El resto está... bueno, en el historial de commit¡!

Motivación

La pregunta natural que me vino a la mente es: ¿qué le pasa al SDK de Java? Después de todo, lo he estado manteniendo más o menos a tiempo completo desde el segundo trimestre de 2022, y el propio Kotlin hace mucho trabajo para garantizar la compatibilidad. La respuesta está en el delta entre las características del lenguaje de Kotlin y Java. La reputación de Java por su verbosidad, combinada con la reticencia de las empresas a migrar de Java 8 (o al menos, a hacerlo a un ritmo glacial), y el hecho de que la compatibilidad entre los dos lenguajes es a nivel de código de bytes, significa que lo que puede ser idiomático en Java no necesariamente se traduce bien a Kotlin. Además, el impresionante arsenal de potentes características del lenguaje Kotlin sólo puede aprovecharse mediante código escrito en Kotlin.

Anulabilidad

Una de las características distintivas de Kotlin en comparación con Java es su distinción entre valores anulables y no anulables. Esto no se consigue mediante una estructura monádica como java.util.Optionalsino que es nativo del lenguaje y su sintaxis. Por lo tanto, Cualquier (el equivalente en Kotlin de Objeto) y ¿Cualquiera? son dos tipos claramente diferentes: el primero es no anulable y el segundo es anulable. Como Java no tiene esta distinción (excepto para los tipos primitivos), Kotlin tiene la noción de "tipos de plataforma"que se indican con un signo de exclamación (por ejemplo ¡Cualquiera!). Esto evita la característica de anulabilidad de Kotlin por compatibilidad. La creación de un SDK de Kotlin ofrece la oportunidad de ser explícito sobre lo que es y no es anulable, tanto en los parámetros de función como en los tipos de retorno.

Parámetros con nombre, por defecto y opcionales

La principal ventaja de la anulabilidad explícita se aprecia quizás mejor en los argumentos de función. El uso de Optional en Java no es precisamente elegante, pero en Kotlin, los parámetros opcionales pueden hacerse explícitos en el código declarándolos como anulables y estableciéndolos como nulos por defecto. Esto permite a los usuarios especificar sólo lo que realmente quieren y necesitan. Además, Kotlin tiene parámetros de función con nombre, lo que significa que los argumentos no tienen por qué proporcionarse en función de su posición; pueden declararse explícitamente, lo que permite al usuario omitir ciertos parámetros opcionales y hacer explícito qué parámetro está proporcionando sin tener que depender de las incrustaciones de código de un IDE, algo especialmente útil cuando hay varios parámetros del mismo tipo. Además, la posibilidad de establecer valores por defecto, incluso para parámetros no anulables, tiene la ventaja de que documenta explícitamente el valor por defecto, y significa que sólo es necesario declarar un método para cada función, en lugar de sobrecargas con un número variable de argumentos para cada combinación, como ocurre en Java.

Por ejemplo, una operación común en la mayoría de nuestras APIs es listar recursos - típicamente, este es el recurso central de una API - Usuarios, Llamadas, Applications, Emisiones, etc. La firma del método listRender en la Video API ilustra este punto: los valores por defecto para count y desplazamiento por defecto. Dado que ambos son del mismo tipo, proporcionar 4 variantes diferentes de este método en Java sería imposible. En Kotlin, sólo se necesita un método para cubrir los cuatro casos: sin parámetros, offset solamente, contar y ambos parámetros. Estos se invocarían de la siguiente manera:

video.listRenders()            // Default params
video.listRenders(count = 25)  // Count only
video.listRenders(offset = 10) // Offset only
video.listRenders(25, 10)      // Both params

Lambdas para constructores

Otro inconveniente del SDK de Java es su gran dependencia de los constructores. Ya he escrito anteriormente sobre el patrón constructory cómo sólo existe porque el lenguaje no tiene parámetros constructores con nombre y por defecto (opcionales). En algunos casos, Kotlin nos permite eliminar el uso de constructores por completo, confiando sólo en parámetros con nombre / opcionales. Un buen ejemplo de esto es la implementación de la API Number Insight. Fíjate en que bajo las cubiertas se usa el constructor del SDK de Java, pero la expresividad de Kotlin nos permite delinear los parámetros requeridos y opcionales usando sólo la firma del método.

Sin embargo, reimplementar todo utilizando este patrón requeriría más trabajo (y duplicación) del que estaba al alcance de este proyecto y aumentaría la carga de mantenimiento. En su lugar, el patrón constructor establecido en el SDK de Java se puede reutilizar en Kotlin de forma bastante elegante utilizando "trailing lambdas"; una característica que se utiliza comúnmente en la creación de lenguajes específicos de dominio (DSLs). Para ilustrarlo, empecemos con algunos ejemplos básicos. En la implementación de la implementación de la Messages APIexisten métodos de utilidad para cada combinación válida de tipo de mensaje y canal. Cada función toma como entrada una expresión lambda con la clase Builder apropiada como receptor. Esto significa que, desde la perspectiva de un usuario, el envío de un SMS tendría este aspecto:

vonage.messages.send(smsText {
  from("Kotlin SDK")
  to(System.getenv("TO_NUMBER"))
  text("Hello, World!")
})

Los métodos de, a y texto provienen del SmsTextRequest.Builder del SDK de Java y su superclase MessageRequest.Builderincluyendo la documentación y los parámetros. Sin embargo, desde la perspectiva de un desarrollador Kotlin, todo el proceso de obtener el constructor y llamar a la función método build() está oculto. Combinado con los parámetros por defecto, eso significa que si todos los parámetros de un constructor son opcionales, puede omitirse por completo. Por ejemplo, esta es la implementación del método createSession en la Video API:

fun createSession(properties: CreateSessionRequest.Builder.() -> Unit = {}): CreateSessionResponse =
  client.createSession(CreateSessionRequest.builder().apply(properties).build())

Para los parámetros por defecto, se puede llamar de la siguiente manera:

val session = vonage.video.createSession()

Para proporcionar parámetros opcionales, se puede utilizar la sintaxis lambda final, de la siguiente manera:

val session = vonage.video.createSession {
  mediaMode(MediaMode.RELAYED)
  archiveMode(ArchiveMode.MANUAL)
}

También es posible utilizar lambdas finales para proporcionar valores opcionales. Tal vez los ejemplos más complejos se encuentren en la implementación de la Voice API, donde la complejidad de definir los valores NCCOs en el SDK de Java es la que más se beneficia de este enfoque. Para ilustrarlo, a continuación se muestra un ejemplo de creación de una llamada de voz a un número de teléfono con dos acciones - Hablar y Conectar - en el SDK de Java junto con alguna otra configuración, escrita en Kotlin:

val callEvent = javaClient.voiceClient.createCall(Call.builder()
  .to(PhoneEndpoint("448001234567", "1p2#5"))
  .fromRandomNumber(true)
  .advancedMachineDetection(AdvancedMachineDetection.builder().build())
  .ncco(
    TalkAction.builder("Hello, this is a text-to-speech call.")
      .language(TextToSpeechLanguage.UNITED_KINGDOM_ENGLISH)
      .premium(true)
      .build(),
    ConnectAction.builder()
      .endpoint(SipEndpoint.builder("sip:me@example.org").build())
      .ringbackTone("http://example.com/ringback.mp3")
      .build()
  )
  .ringingTimer(30)
  .eventUrl("https://example.com/webhooks/events")
  .build()
)

Aunque es algo legible con la indentación adecuada, no es tan elegante como un enfoque más idiomático. Aquí está lo mismo escrito usando el SDK de Kotlin:

val callEvent = vonage.voice.createCall {
  toPstn("448001234567", "1p2#5")
  fromRandomNumber(true)
  advancedMachineDetection()
  ncco(
    talkAction("Hello, this is a text-to-speech call.") {
      language(TextToSpeechLanguage.UNITED_KINGDOM_ENGLISH)
      premium(true)
    },
    connectToSip("sip:me@example.org") {
      ringbackTone("http://example.com/ringback.mp3")
    }
  )
  ringingTimer(30)
  eventUrl("https://example.com/webhooks/events")
}

Observe cómo la sintaxis DSL fluye de forma más natural. En particular, el método toPstn libera al usuario de tener que llamar a la función a directamente y observe cómo, en ausencia de cualquier configuración para la Detección Avanzada de Máquinas, no se requiere ningún constructor o lambda. Sin embargo, para la función talkActionel parámetro obligatorio -texto a hablar- forma parte del constructor, mientras que los parámetros opcionales están en la lambda. Del mismo modo, para la acción connectToSip el URI es obligatorio, con un lambda opcional para configuración adicional. En ambos casos, la lambda puede omitirse, así:

val callEvent = vonage.voice.createCall {
  toPstn("448001234567", "1p2#5")
  fromRandomNumber(true)
  advancedMachineDetection()
  ncco(
    talkAction("Hello, this is a text-to-speech call.")
    connectToSip("sip:me@example.org")
  )
  ringingTimer(30)
  eventUrl("https://example.com/webhooks/events")
}

Puede ver cómo se implementa esto en las funciones connectTo* de la Voice APIque combinan dos constructores en una sola llamada a una función. Los parámetros de las funciones corresponden al endpoint al que se está conectando, y el lambda final sirve para configurar la función Conectar en sí.

Puntos finales basados en recursos

Además de aprovechar las funciones de Kotlin para reducir la repetición de tareas, otra diferencia entre el SDK de Kotlin y el de Java es que el SDK de Kotlin utiliza un enfoque basado en recursos para realizar llamadas a la API. En la mayoría de las API de Vonage, hay al menos un recurso que se puede consultar, crear, actualizar y eliminar. Para cada uno de estos recursos, existe una clase correspondiente a ese recurso que brinda acceso a estos puntos finales. Todos estos recursos tienen en común un identificador único. En lugar de tener que almacenarlo en caché en otro lugar para realizar repetidamente llamadas a la API de un recurso concreto, el SDK se encarga de ello, de modo que no es necesario proporcionar el identificador cada vez. Estos recursos pueden incluso anidarse, como se ejemplifica con la implementación de la Video API. He aquí un ejemplo:

val existingSession = vonage.video.session(sessionId)
val streams = existingSession.listStreams()
existingSession.connection(connectionId).sendDtmf("1234")

Esto tendrá mucho más sentido cuando se utilice un IDE con autocompletado. En este sentido, las llamadas a la API se agrupan por tipo de recurso. Especialmente en APIs complejas como Video, donde hay recursos anidados e IDs, esto hace que la tarea de proporcionar un ID sea menos propensa a errores, ya que el parámetro requerido se proporciona por construcción. Esto permite que las firmas de los métodos para los recursos anidados sean más sencillas, para que pueda centrarse en proporcionar parámetros significativos de forma independiente al direccionamiento del recurso. En cambio, el SDK de Java es "plano/sin contexto", en el sentido de que suele haber una correspondencia unívoca entre los puntos finales de la especificación de la API y los puntos finales del SDK. Este enfoque mejora aún más la separación entre parámetros obligatorios y opcionales y significa que es más probable que una solicitud sea correcta por construcción.

Consulte la especificación de la API para silenciar un flujo de vídeo como ejemplo. Está claro que las coordenadas al endpoint requieren el ID de sesión y el ID de flujo. En implementación del SDK de Javaes posible confundir el ID de sesión y el ID de flujo pasándolos en el orden incorrecto. En cambio la implementación del SDK de Kotlin no requiere ningún parámetro, ya que no se envían datos. De forma similar para añadir un flujo a un archivola implementación del implementación del SDK de Kotlin tiene un único método en el que está claro que el ID del flujo es el parámetro que debe pasarse, dado el contexto. Por el contrario, la implementación implementación del SDK de Java tiene dos métodos (debido a los parámetros opcionales) y depende del nombre del método y de la documentación para mostrar cómo funciona y qué hace.

Quizá sea una cuestión de preferencias personales; no se puede argumentar que ninguno de los dos enfoques sea objetivamente mejor. Sin embargo, la coherencia con la que el SDK de Kotlin sigue este enfoque en todas las APIs en las que es posible es un aspecto distintivo significativo, especialmente si se tiene en cuenta la gran dependencia del SDK de Java de los constructores, incluso para los parámetros obligatorios.

Extensiones

Otra hermosa característica de Kotlin es funciones de extensiónla capacidad de definir métodos en clases existentes sin extenderlas. Esto es especialmente útil para mejorar los constructores definidos en el SDK de Java para hacerlos más idiomáticos en Kotlin. Tomemos, por ejemplo, la API de Applications. Cada Aplicación de Vonage puede tener múltiples capacidades, una de cada tipo. Cada capacidad tiene uno o más Webhooks, y cada Webhook tiene un tipo, como por ejemplo URL_de_respuesta, status_url, event_url etc. Sin embargo, dependiendo del tipo de Capacidad, sólo son aplicables determinados Webhooks. Por ejemplo, la capacidad Verify sólo tiene status_urlmientras que la capacidad de Voice tiene answer_url, fallback_answer_url y event_url. En el momento de escribir esto, la implementación de Java SDK de esto es bastante torpe. Para ilustrarlo, he aquí un ejemplo en el que se actualiza el nombre de una aplicación existente, se elimina la función Mensajes y se actualizan los webhooks para Verify y Voice. Tenga en cuenta que para actualizar una aplicación, primero hay que recuperarla porque cualquier configuración que se proporcione sobrescribirá la aplicación existente. Aquí se escribe utilizando el Java SDK:

val ac = javaClient.applicationClient
val existing = ac.getApplication(appId)
val updated = ac.updateApplication(Application.builder(existing)
  .name("My Updated Application")
  .addCapability(Verify.builder()
    .addWebhook(Webhook.Type.STATUS, Webhook.builder()
      .address("https://example.org/webhooks/verify/status")
      .method(HttpMethod.POST)
      .build()
    )
    .build()
  )
  .addCapability(Voice.builder()
    .addWebhook(Webhook.Type.ANSWER, Webhook.builder()
      .address("https://example.org/webhooks/voice/answer")
      .method(HttpMethod.POST)
      .build()
    )
    .addWebhook(Webhook.Type.EVENT, Webhook.builder()
      .address("https://example.org/webhooks/voice/event")
      .method(HttpMethod.GET)
      .build()
    )
    .build()
  )
  .removeCapability(Capability.Type.MESSAGES)
  .build()
)

Son muchos constructores. Es fácil perderse, y no hay nada en el diseño que evite que especifiques el tipo de webhook equivocado para una capacidad. Compáralo con el SDK de Kotlin:

val ac = vonage.application
val application = ac.application(appId)
application.update {
  name("My Updated Application")
  verify {
    status {
      url("https://example.org/webhooks/verify/status")
      method(HttpMethod.POST)
    }
  }
  voice {
    answer {
      url("https://example.org/webhooks/voice/answer")
      method(HttpMethod.GET)
    }
    event {
      url("https://example.org/webhooks/voice/event")
      method(HttpMethod.POST)
    }
  }
  removeCapability(Capability.Type.MESSAGES)
}

Fíjese en que ni siquiera hemos tenido que obtener la aplicación utilizando el endpoint - el SDK se encarga de esto por ti. Además, las funciones de extensión definidas en cada Capacidad restringen los webhooks que se pueden definir, de modo que es correcto por construcción. Esto tendrá más sentido cuando se vea en un IDE con autocompletado, pero básicamente, cuando estás dentro de, por ejemplo, la función voice las únicas opciones que aparecerán son respuesta, fallbackAnswer y eventoporque son los únicos definidos en el constructor como extensión. Ni una sola mención a "build" en ninguna parte, y ni siquiera es necesario especificar el tipo de webhook directamente: basta con declarar el elemento apropiado.

Son pequeñas cosas como estas las que pueden mejorar la experiencia y la productividad de los desarrolladores. Por supuesto, dado que en Vonage no autogeneramos ninguno de nuestros SDK, esto podría aplicarse también en Java. De hecho, en el proceso de desarrollo del SDK de Kotlin, he hecho una lista de cosas a cambiar en el SDK de Java, tanto grandes como pequeñas. En el momento de escribir esto, ¡esta lista podría convertirse en 55 tickets de JIRA! Sin embargo, es un buen detalle que se puedan conseguir mejoras fáciles con funciones de extensión. Por supuesto, no todas las mejoras del SDK de Kotlin pueden ni deben trasladarse al SDK de Java.

Documentación

El SDK de Kotlin está documentado mediante KDocs. Cada función y clase tiene documentación escrita a mano sobre parámetros, tipo de retorno, excepciones y una descripción de lo que hace. Además, se publican en el moderno formato HTML Dokka, que mantiene el estilo de la documentación de la API de Kotlin frente al formato Javadocs, de aspecto comparativamente anticuado. Puedes navegar por la documentación utilizando cualquier servicio que pueda renderizar docs a partir de archivos JAR, como Javadoc.io. Por supuesto, la documentación también está disponible directamente en tu IDE.

Muestras de código

Encontrará fragmentos de código para todas las API compatibles en nuestro Portal para desarrolladores. Solo tiene que ir al producto que le interese y, en "Cree su solución", encontrará los fragmentos en los que Kotlin es uno de los lenguajes. También puedes consultarlos directamente y ejecutarlos utilizando la aplicación Vonage Kotlin Code Snippets repo en GitHub. Por ejemplo aquí tienes un fragmento de texto a voz "Hello World" que utiliza la Voice API en el SDK de Kotlin. Cada fragmento de código, tal y como aparece en la página de documentación, tiene también un enlace al código fuente de respaldo en GitHub. Para ejecutar los ejemplos, sólo tienes que comprobar el repositorio y establecer las variables de entorno, y luego ejecutarlo a través de Gradle o tu IDE. Las instrucciones para esto se pueden encontrar en el repositorio de README.

Pruebas

El SDK tiene 99% de cobertura de código en el momento de escribir este artículo. Se comprueban todas y cada una de las funciones, con parámetros obligatorios y opcionales. Incluso los elementos que residen en el SDK de Java se prueban para comprobar su integridad. El enfoque adoptado es de extremo a extremo / pruebas de integración ya que el SDK de Java ya prueba el modelo de datos y la validación de parámetros. Decidí utilizar WireMock para esto; un framework para pruebas de API.

He creado una pequeña biblioteca de funciones de alto nivel para facilitar la afirmación de los cuerpos y cabeceras de las peticiones y respuestas. El resultado es que cada prueba se lee como una especificación, como deberían hacerlo todas las buenas pruebas. La anatomía de una prueba es imitar la URL del punto final esperado, el método HTTP, el cuerpo de la solicitud (JSON o parámetros de consulta), las cabeceras (auth, tipo de contenido, agente de usuario), los parámetros de respuesta esperados (si los hay, en formato JSON) y el código de estado HTTP. En esencia, esto refleja la especificación de la API en el código. Para que la escritura de pruebas sea menos prolija y propensa a errores, los cuerpos de solicitud y respuesta no se escriben directamente como JSON, sino como un archivo Map<String, Any>.. Esto se puede serializar utilizando Jackson, por lo que todo lo que se necesita es proporcionar los nombres correctos de las propiedades y los valores esperados.

He aquí un ejemplo que prueba el envío de una nueva solicitud a un usuario de la la Verify API. Puedes ver todos los elementos en juego aquí, el enfoque declarativo para definir la especificación y cómo captura todos los elementos de la especificación OpenAPI. En la práctica, la mayoría de las pruebas del SDK utilizan incluso métodos de nivel superior declarados en el mismo archivo para reducir aún más la duplicación y la complejidad. Además, se comprueba cada parámetro, por lo que a menudo cada punto final tendrá dos métodos de prueba: uno para todos los parámetros y otro sólo para los parámetros requeridos. Las pruebas más complejas son las de Voice APIpero aún así, el enfoque declarativo hace que sean mucho más legibles y fáciles de mantener que las pruebas comparativamente verbosas que utilizan JSON directamente en el SDK de Java. Por ejemplo, compare las pruebas de la Video API en el Java SDK y SDK de Kotliny estoy seguro de que estarás de acuerdo.

La ventaja de tener pruebas escritas desde cero independientemente de la implementación subyacente es que, en el futuro, cualquiera podría reescribir el SDK en Kotlin puro sin utilizar el SDK de Java. Las pruebas describen la especificación, por lo que sólo habría que cambiar los usos reales del SDK, no los mocks. Lo único que el SDK de Kotlin no prueba es la validación de parámetros; es decir, si ciertas combinaciones de parámetros son válidas, o parámetros requeridos en los constructores. La validación del modelo de datos se prueba en el SDK de Java, y no se duplica a propósito en el SDK de Kotlin. Algunos podrían incluso argumentar que el backend de la API debería hacer la validación y devolver una respuesta de error, no el SDK. Para evitar duplicar la lógica de validación y sobrecargar aún más las pruebas y el tiempo de desarrollo, esto se dejó intencionadamente fuera del ámbito, ya que es una preocupación ortogonal.

Próximos pasos

Aunque el SDK ahora es oficialmente compatible con las implementaciones de todas las API de Vonage en estado de disponibilidad general, esto es sólo el comienzo. Además de mantenernos al día con las nuevas funciones y API junto con el SDK de Java, hay tutoriales para escribir, integraciones para crear y mejoras varias para realizar según los comentarios de los usuarios. A propósito de esto, agradecemos enormemente las aportaciones y sugerencias de la comunidad, aunque supongan cambios de última hora. Pruebe el SDK y díganos qué le parece.

Despedida

Esto es todo por ahora. Si te encuentras con algún problema o tienes sugerencias de mejora, no dudes en plantear un problema en GitHubo ponte en contacto con nosotros en X (antes Twitter) o pásate por nuestro Slack de la comunidad. Espero que tengas una experiencia agradable usando el SDK de Kotlin, y espero tus comentarios.

Compartir:

https://a.storyblok.com/f/270183/400x400/46a3751f47/sina-madani.png
Sina MadaniVonage Antiguo miembro del equipo

Sina es promotora de desarrollo Java en Vonage. Procede del mundo académico y, en general, siente curiosidad por todo lo relacionado con los coches, los ordenadores, la programación, la tecnología y la naturaleza humana. En su tiempo libre, se le puede encontrar paseando o jugando a videojuegos de competición.