https://a.storyblok.com/f/270183/1368x665/28c6d5be54/sdk_rest-apis_24.png

Cómo un SDK puede añadir valor a las API REST

Publicado el August 4, 2022

Tiempo de lectura: 12 minutos

El Mensajes API v1 de Vonage fue recientemente promovida al estado de disponibilidad general. Luego de este anuncio, el equipo de herramientas de relaciones con los desarrolladores ha estado trabajando arduamente para implementar la compatibilidad con esta API en nuestros SDK de servidor.

De forma colectiva, decidimos que cada SDK debería implementar la API de la forma que tuviera más sentido, teniendo en cuenta el lenguaje, la comunidad y el enfoque general utilizado para dar soporte a otras API en el SDK, para mantener la coherencia. Nuestro objetivo es garantizar que cada SDK resulte idiomático, en lugar de adoptar un enfoque único en el que la implementación parezca autogenerada. La API Messages v1 es una buena forma de ilustrar esto, de ahí este artículo.

Naturalmente, como Java Developer Advocate del equipo, voy a centrarme en la implementación del SDK de Java y describir los fundamentos de su diseño. Pero primero, examinemos la especificación de la Messages APIque es lo que tenemos que implementar.

La API tiene un único punto final, que consiste en enviar un mensaje mediante una solicitud POST. La complejidad surge del esquema de los mensajes. Aquí hay dos conceptos principales: los canales y tipos de mensajes. El primero se refiere al servicio que se utiliza para enviar el mensaje (SMS, MMS, WhatsApp, Viber, Facebook Messenger). El segundo describe el soporte del mensaje (texto, imagen, audio, vídeo, archivo, etc.).

Cada canal admite un subconjunto de tipos de mensajes. Por ejemplo, no puedes enviar un Video por SMS. Algunos canales, como WhatsApp, admiten plantillas y tipos de mensaje personalizados, que te permiten enviar mensajes más complejos o compartir tu ubicación.

Además, cada canal impone restricciones únicas al contenido de los mensajes. Por ejemplo, el límite de longitud de los textos varía según el canal, al igual que los tipos de archivo admitidos para los mensajes multimedia; por ejemplo, WhatsApp admite una amplia gama de formatos de audio, mientras que Messenger sólo admite MP3. Para los mensajes multimedia, algunos canales permiten una descripción de texto opcional (denominada pie de foto en la especificación de la API) para acompañar al archivo, mientras que otros no.

Esto puede variar incluso dentro de un mismo canal: por ejemplo, se puede incluir un pie de foto con un MMS, excepto si se trata de una vCard (que, por cierto, es exclusiva del canal MMS). Desde el punto de vista de un implementador, el reto consiste en encontrar una forma estructurada y eficiente de recoger estos requisitos y minimizar la confusión de los usuarios de la API.

Como desarrollador Java, parece natural empezar por el modelo de datos. Inevitablemente acabamos con un tipo abstracto para representar un mensaje, que es aceptado por el método sendMessage de MessagesClient. Dado que la respuesta que obtenemos al enviar un mensaje es la misma independientemente del tipo de mensaje y del canal, no necesitamos preocuparnos por el mensaje en sí, simplemente lo reenviamos como una carga útil JSON y lo que obtenemos de vuelta es un identificador único del mensaje que se envió. Con esto en mente, nuestro resumen MessageRequest extrae los campos comunes a todos los mensajes, a saber: el tipo, el canal, el remitente, el destinatario y una cadena opcional de referencia al cliente. Dado que los canales y los tipos de mensaje están predefinidos, los representamos como enumeraciones.

Hay una mezcla de parámetros opcionales y obligatorios (por ejemplo, la referencia del cliente es opcional, pero el remitente y el destinatario son obligatorios). También hay más parámetros, dependiendo de las combinaciones de canal y tipo de mensaje. Como Java no tiene argumentos con nombre, utilizamos el patrón constructor para emularlo. Puedes leer más sobre esto aquí.

Queremos imponer que sólo se puedan construir combinaciones válidas de tipo de mensaje y canal, por lo que definimos esta matriz en el archivo Channel. Validamos todos los argumentos de los constructores de MessageRequest y sus subtipos. Aquí reside lo que creo que es la principal ventaja del diseño de la implementación de la API de Messages del SDK de Java: todo es correcto por construcción.

¿Qué significa esto exactamente? Significa que, en teoría, es difícil, idealmente imposible, construir un mensaje inválido. Desafío a cualquiera que lea esto a que intente hacer un "mal uso" de la Messages API utilizando el SDK de Java. Intentémoslo juntos.

MessageRequest es abstracto, por lo que tenemos que utilizar uno de sus subtipos concretos. Todos estos subtipos (por ejemplo, MmsImageRequest) ya establecen los parámetros Channel y MessageType. Además, aunque creáramos nuestra propia subclase de MessageRequestno podríamos evitar la comprobación de validación de MessageType y Channel, ya que se realiza en el constructor de MessageRequest. Además, los enums son implícitamente finales, por lo que no podemos añadir nuestras propias etiquetas MessageType o Channel o sobreescribir el comportamiento de validación.

Además, los constructores de todas las subclases de MessageRequest están protegidos (en el caso de las clases abstractas) o son privados (en el caso de las clases concretas), y ninguna de las clases concretas puede extenderse ya que son final (lo mismo ocurre con los constructores). Así que, estructuralmente, no podemos crear una clase "mala". MessageRequest mala".

Bien, intentemos otra cosa. Si no podemos hacer un mal uso estructural MessageRequestquizás podamos pasar valores inválidos a los constructores. Por ejemplo, ¿podríamos enviar un texto vacío? ¿O pasar una cadena sin sentido en el campo to (destinatario) en lugar de un número? ¿O qué tal si omitimos parámetros obligatorios (como el remitente y el destinatario)?

Oh, aquí hay otra: todos los tipos de mensajes basados en archivos (los que aceptan una imagen, audio, video, etc.) tienen un campo URL. ¿Podríamos no pasar una cadena de URL no válida? ¿Y qué pasa con el campo Time-to-Live de los mensajes de Viber? Podríamos poner un número que está fuera de los límites aceptables ya que es sólo un int¿verdad? Oh, aquí hay un nicho: en las peticiones de Messenger, tanto Category y Tag son opcionales. Sin embargo, la API requiere que si Category tiene un valor de message-tagla etiqueta debe estar presente. ¿Y qué pasa con...?

Dejémoslo ahí. Todos estos casos han sido considerados, y ninguno de ellos es posible. Sí, tu código compilará si intentas pasar una URL malformada, un número no válido o te olvidas de establecer parámetros obligatorios en el constructor. Pero en tiempo de ejecución, no podrás enviar el mensaje. Ni siquiera llegarás a construir el objeto MessageRequest objeto. ¿Por qué? Porque cada parámetro se valida en el constructor. En el momento en que llame build() en el Constructor, el constructor es invocado, y si los parámetros usados para construir el mensaje faltan o no son válidos, entonces obtendrás un mensaje IllegalArgumentException explicando el problema. Esto es lo que quise decir cuando dije: "correcto por construcción". Si puede construir un MessageRequestentonces, por lo que sabemos, no es obviamente incorrecto, y puedes proceder a enviarlo.

Por supuesto, eso no significa que el mensaje sea completamente válido o que no reciba una respuesta de error del servidor. Por ejemplo, podría intentar enviar un mensaje de texto a un número de teléfono que no existe, pero que es compatible con E164. Este tipo de validación está fuera del alcance del SDK y la realiza el servicio backend con el que se comunica la API. Lo que el SDK evita son esencialmente los errores 422. Entonces, ¿a qué se contrapone esto? ¿A dónde quiero llegar? Para responder a esto, echemos un vistazo al diametralmente opuesto: cURL.

¿Por qué necesito el SDK cuando puedo utilizar la API directamente? Eso está bien, aquí hay un ejemplo de envío de una imagen a través de Viber utilizando cURL:

curl -X POST https://api.nexmo.com/v1/messages \ -H 'Authorization: Bearer '$JWT\ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -d $'{ "message_type": "image", "channel": "viber_service", "image": { "url": "https://example.com/image.jpg" }, "to": "447700900000", "from": "9876543210", "viber_service": { "ttl": 600 } }'

Aunque esto es relativamente fácil de leer, es fácil equivocarse al escribir una solicitud de este tipo manualmente. No sólo hay que recordar la URL del endpoint, el método HTTP, las cabeceras, el tipo de autorización, etc., sino también el formato JSON. Podrías pasar por alto las comillas (casi todo es una cadena), pero cuidado con eso ttl - en realidad es un entero, no un int ¡envuelto en una cadena! Tienes que recordar todos los campos y sus nombres exactos - aquí ningún IDE te va a ayudar con el autocompletado porque sólo estás escribiendo texto.

Tu terminal no sabe nada de la API, así que si tecleas algo mal, no hay forma de saberlo hasta que envías la petición y recibes una respuesta 422 de vuelta. Y recordando ese esquema, no es realmente intuitivo, ¿verdad? Si quieres establecer el tiempo de vida, tienes que acordarte de envolverlo en un objeto viber_service objeto. Y no olvides que la URL de la imagen también tiene que estar en un objeto. image oh y no se puede incluir un título con su imagen para los mensajes de Viber, pero cURL no le impedirá que lo intente. Usted puede pasar en lo que quieras como el cuerpo. Ni siquiera tiene que ser JSON válido, y mucho menos ajustarse al esquema de la API.

Es justo decir entonces que para utilizar la Messages API con cURL, vas a necesitar la referencia de la API a mano y referirte a ella extensamente para asegurarte de que tus peticiones son correctas - no sólo el cuerpo sino también las cabeceras (método de autorización, por ejemplo). Es propenso a errores, y no hay nada que te impida cometerlos o que te ayude a formular la solicitud de mensaje que pretendes.

Por el contrario, si desea utilizar la API Messages API a través del SDK de Java, no necesita la referencia de la API en absoluto. Todos los métodos públicos que necesitas están documentados, explicando qué es necesario y qué es opcional, el patrón de uso, ejemplos, etc. Incluso sin leer la documentación, puede explorar la API utilizando únicamente el autocompletado que proporciona su IDE. Por ejemplo, no va a tratar de establecer un título en una imagen de Viber porque no hay manera de hacerlo en el SDK de Java. Así que, como usuario, concluyes que no está soportado.

¿Qué hay de la descubribilidad? Bueno, todas las MessageRequest siguen el esquema de nomenclatura [Channel][MessageType]Request (por ejemplo, SmsTextRequest). Incluso puedes usar tu IDE para listar todas las subclases de MessageRequest. Y sólo hay una forma de construir peticiones: a través del constructor asociado a cada MessageRequest subclase. Además, los constructores de los constructores no son públicos, por lo que la única forma de instanciar un constructor es llamando al método static builder() de la clase. Esto, combinado con los constructores privados del paquete, evita que hagas un mal uso de ellos.

Un lenguaje fuertemente tipado como Java le impide proporcionar valores no válidos, no sólo en tiempo de ejecución, sino también en tiempo de compilación. Por ejemplo, puede que no sepa cuáles son los valores válidos para Category en el objeto opcional viber_service pero en el SDK de Java, el compilador / IDE los hace obvios para usted porque es un enum.

¿Y lo de envolverlo en un viber_service objeto? No tienes que preocuparte por eso, sólo tienes que establecer lo que necesitas en el constructor, y el SDK se encargará de ello por ti. De hecho, el SDK se encarga de toda la serialización y deserialización. Como usuario, no sabes ni necesitas preocuparte de qué formato de serialización se está utilizando entre bastidores: todo está bajo control. Esa es una de las principales ventajas de utilizar un SDK en lugar de la API: es declarativo en lugar de imperativo. Usted declara qué desea enviar, no cómo enviarlo.

Míralo de esta manera: supongamos que decidimos que todas nuestras APIs van a utilizar Avro o Protobuf (o, Dios no lo quiera, el viejo XML) en lugar de JSON. Esos scripts cURL que tenías por ahí van a necesitar una reescritura. Tal vez si tienes suerte y mantenemos el esquema exactamente igual, y eres un gurú de RegEx, podrías al menos automatizar parcialmente la migración.

¿Y si cambiamos el esquema? Siguiendo con el ejemplo anterior, ¿qué pasaría si decidiéramos que el parámetro url ya no necesita estar envuelto en un objeto? O eliminamos el viber_service que se utiliza para ttl y category ¿? ¿Y si cambiamos el nombre del viber_service tipo de mensaje a viber por coherencia? Si realizas tus peticiones manualmente con cURL, tienes que ser consciente de todos estos cambios. Si está utilizando el SDK de Java, sólo tiene que actualizar a la siguiente versión - un cambio de un solo carácter en su pom.xml o build.gradle . No hay necesidad de refactorizar nada, todo se hace entre bastidores.

Aunque me he centrado en el SDK de Java, vale la pena señalar que la tipificación fuerte no es un requisito previo para la validación. Simplemente hace que sea más fácil de aplicar mediante el uso del compilador en lugar de la lógica codificada a mano.

Por ejemplo, la implementación de la API Messages del SDK de Python tiene un tamaño minúsculo en comparación con el SDK de Java (¡es un único archivo de menos de 100 líneas!), y del SDK de Ruby del SDK de Ruby también es relativamente pequeña. Ambos SDK realizan una validación básica de los parámetros.

El mismo enfoque podría haberse utilizado en el SDK de Java: podríamos aceptar una construcción Map con los parámetros y validarlos dinámicamente, pero esto es más propenso a errores ya que el compilador no puede ayudarnos. El tamaño de la implementación del SDK de Java se debe principalmente a que Java es un lenguaje verboso; por ejemplo, es similar a la implementación de implementación de C en principio, pero con muchas más líneas de código.

Puede ser un ejercicio interesante para el lector comparar las Pull Requests iniciales para la implementación de Messages v1 a través de la plataforma Java, Python, Ruby y PHP SDKs para evaluar esto.

Sin embargo, este enfoque estricto del diseño del SDK tiene sus desventajas. Supongamos que en algún momento en el futuro, la API admite el envío de vídeo a través de Viber. Usted puede inmediatamente tomar ventaja de esto con cURL, pero un SDK que valida el tipo de mensaje y combinaciones de canales requerirá una actualización.

También supone una mayor carga de mantenimiento para los responsables del SDK cuando la API cambia con frecuencia, ya que la lógica de validación, los tipos de datos, etc. deben actualizarse para reflejar estos cambios. Pero una vez que las cosas se asientan y la API es estable (lo que suele ocurrir cuando nuestras API pasan de Beta a GA), esto deja de ser un problema. En última instancia, nuestro objetivo es ofrecer la mejor experiencia de usuario posible, y espero que este artículo le haya convencido del valor que los SDK a medida pueden aportar a las API REST.

La participación de la comunidad es siempre bienvenida. No dudes en unirte a nosotros en el Slack de la comunidad de Vonage o envíanos un mensaje en Twitter. Si tienes alguna sugerencia de mejora o detectas un error, no dudes en plantear un problema en GitHub.

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.