¿Alguien sabe de un ejemplo de un cliente tranquilo que sigue el principio de HATEOAS?


Así que ahora estoy entendiendo el punto de que todos deberíamos estar implementando nuestros servicios RESTful proporcionando representaciones que permitan a los clientes seguir el principio HATEOAS. Y aunque todo tiene sentido en teoría, he estado rastreando la web para encontrar un solo buen ejemplo de algún código de cliente que siga estrictamente la idea.

Cuanto más leo, más empiezo a sentir que esto es una discusión académica porque nadie lo está haciendo. La gente puede gemir todo les gustan los muchos defectos de la pila WS -*, pero al menos está claro cómo escribir clientes: puede analizar WSDL y generar código.

Ahora entiendo que esto no debería ser necesario con un buen servicio de descanso: solo debería necesitar saber acerca de las relaciones y representaciones involucradas y debería ser capaz de reaccionar dinámicamente a ellas. Pero aún así, ¿no debería este principio haber sido destilado y abstraído en algunas bibliotecas comunes a estas alturas? Alimentación en información sobre las representaciones y relaciones que podría recibir y obtener un código de nivel superior más útil que puede utilizar en su aplicación?

Estas son solo ideas mías a medio cocinar, pero me temo que si me zambullo y escribo una API RESTFUL correctamente ahora mismo, ¡nadie va a poder usarla! O al menos usarlo va a ser un dolor en la espalda debido a la milla extra que la gente tendrá que ir escribiendo código de pegamento para interpretar las relaciones y representaciones Yo proveo.

¿Puede alguien arrojar alguna luz sobre esto desde la perspectiva del cliente? ¿Puede alguien mostrar un ejemplo de código de cliente RESTful correctamente dinámico/reactivo para que pueda tener una idea de la audiencia para la que estoy escribiendo? (mejor aún un ejemplo de una API de cliente que proporciona algunas abstracciones) De lo contrario, todo es bastante teórico....

[editar: nota, he encontrado una pregunta similar aquí, que no creo que haya sido realmente contestada, el autor fue Wikipedia stub!]

Author: Community, 2009-11-21

6 answers

Hemos medio hecho esto en nuestro proyecto actual. Las representaciones que devolvemos se generan a partir de objetos de dominio, y el cliente puede solicitarlas en XML, JSON o XHTML. Si se trata de un cliente XHTML como Firefox, entonces una persona ve un conjunto de enlaces salientes desde el recurso raíz conocido y puede navegar por todos los demás recursos. Hasta ahora, pure HATEOAS, y una gran herramienta para desarrolladores.

Pero nos preocupa rendimiento cuando el cliente es un programa, no un humano usando un navegador. Para nuestras representaciones XML y JSON, actualmente hemos suprimido la generación de enlaces relacionados, ya que triplican los tamaños de representación y, por lo tanto, afectan sustancialmente la serialización/deserialización, el uso de memoria y el ancho de banda. Nuestra otra preocupación de eficiencia es que con pure HATEOAS, los programas cliente harán varias veces el número de solicitudes HTTP mientras navegan desde el enlace conocido hasta la información que necesitan. Así que parece mejor, desde un punto de vista de eficiencia , si los clientes tienen el conocimiento de los enlaces codificados en ellos.

Pero hacer eso significa que el cliente debe hacer una gran cantidad de concatenación de cadenas para formar los URI, que es propenso a errores y hace que sea difícil reorganizar el espacio de nombres de recursos. Por lo tanto, utilizamos un sistema de plantillas donde el código cliente selecciona una plantilla y le pide que se expanda desde un objeto de parámetro. Este es un tipo de relleno de formularios.

Estoy realmente ansioso por ver lo que otros tienen experiencia en esto. HATEOAS parece una buena idea aparte de los aspectos de rendimiento.

Editar: Nuestras plantillas son parte de una biblioteca cliente Java que escribimos sobre el framework Restlet. La biblioteca cliente maneja todos los detalles de solicitudes/respuestas HTTP, encabezados HTTP, deserialización / serialización, codificación GZIP, etc. Esto hace que el código real del cliente sea bastante conciso, y ayuda a aislarlo de algunos cambios en el lado del servidor.

Entrada de blog de Roy Fielding acerca de HATEOAS tiene una discusión muy relevante e interesante a continuación.

 17
Author: Jim Ferrans,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-11-27 13:53:50

Hasta ahora he construido dos clientes que acceden a los servicios REST. Ambos usan HATEOAS exclusivamente. He tenido mucho éxito al poder actualizar la funcionalidad del servidor sin actualizar el cliente.

Uso xml:base para habilitar urls relativas para reducir el ruido en mis documentos xml. Aparte de cargar imágenes y otros datos estáticos, generalmente solo sigo enlaces en las solicitudes de los usuarios, por lo que la sobrecarga de rendimiento de los enlaces no es significativa para mí.

En los clientes, el único común la funcionalidad que he sentido la necesidad de crear es envoltorios alrededor de mis tipos de medios y una clase para administrar enlaces.


Actualización:

Parece que hay dos maneras distintas de tratar con las interfaces REST desde la perspectiva del cliente. La primera es donde el cliente sabe qué información quiere obtener y conoce los enlaces que necesita atravesar para llegar a esa información. El segundo enfoque es útil cuando hay un usuario humano de la aplicación cliente que controla enlaces a seguir y el cliente puede no saber de antemano qué tipo de medio será devuelto desde el servidor. Para el valor de entretenimiento, llamo a estos dos tipos de cliente, el minero de datos y el despachador, respectivamente.

El Minero de datos

Por ejemplo, imagine por un momento que la API de Twitter era realmente RESTful y quería escribir un cliente que recuperara el mensaje de estado más reciente del seguidor más reciente de un usuario de Twitter en particular.

Suponiendo que estaba usando el increíble nuevo Microsoft.Http.HttpClient library, y yo había escrito algunos métodos de extensión "ReadAs" para analizar el XML procedente de la API de twitter, me imagino que sería algo como esto:

var twitterService = HttpClient.Get("http://api.twitter.com").Content.ReadAsTwitterService();

var userLink = twitterService.GetUserLink("DarrelMiller");
var userPage = HttpClient.Get(userLink).Content.ReadAsTwitterUserPage();

var followersLink = userPage.GetFollowersLink();
var followersPage = HttpClient.Get(followersLink).Content.ReadAsFollowersPage();
var followerUserName = followersPage.FirstFollower.UserName;

var followerUserLink = twitterService.GetUserLink(followerUserName);
var followerUserPage = HttpClient.Get(followerUserLink).Content.ReadAsTwitterUserPage();

var followerStatuses = HttpClient.Get(followerUserPage.GetStatusesLink()).Content.ReadAsTwitterUserPage();

var statusMessage = followerStatuses.LastMessage; 

El Despachador

Para ilustrar mejor este ejemplo, imagine que estaba implementando un cliente que renderizaba información genealógica. El cliente debe ser capaz de mostrar el árbol, profundizar en la información sobre una persona en particular y ver imágenes relacionadas. Considere la siguiente fragmento de código:

 void ProcessResponse(HttpResponseMessage response) {
            IResponseController controller;

            switch(response.Content.ContentType) {
                case "vnd.MyCompany.FamilyTree+xml":
                    controller = new FamilyTreeController(response);
                    controller.Execute();
                    break;
                case "vnd.MyCompany.PersonProfile+xml":
                    controller = new PersonProfileController(response);
                    controller.Execute();
                    break;
                case "image/jpeg":
                    controller = new ImageController(response);
                    controller.Execute();
                    break;
            }

        }

La aplicación cliente puede usar un mecanismo completamente genérico para seguir enlaces y pasar la respuesta a este método de envío. Desde aquí, la instrucción switch pasa control a una clase de controlador específica que sabe cómo interpretar y representar la información en función del tipo de medio.

Obviamente hay muchas más piezas en la aplicación cliente, pero estas son las que corresponden a HATEOAS. Siéntase libre de pedirme que aclare cualquier punto como he repasado muchos detalles.

 10
Author: Darrel Miller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-11-28 16:47:30

Nokia Places API es RESTful y utiliza hipermedia en todo. También está claro en su documentación para desalentar el uso de plantillas URI/hardcoding:

Uso de Enlaces Hipermedia

Su aplicación debe utilizar los enlaces hipermedia expuestos en el JSON las respuestas para solicitudes posteriores dentro de una tarea fluyen, en lugar de tratando de construir un URI para los siguientes pasos a través de plantillas de URI. Por al usar plantillas URI, su solicitud no incluiría elementos críticos información que se requiere para crear la respuesta para el siguiente solicitud.

 2
Author: Brian Kelly,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-31 19:15:21

Jim, también estaba un poco frustrado con la falta de ejemplos con un cliente tranquilo siguiendo a HATEOAS, así que escribí una publicación en el blog mostrando un ejemplo adecuado de HATEOAS para crear y realizar un pedido. Hay sorprendentemente pocos ejemplos de hacer esto a través de una API y me pareció un toque confuso, pero aquí está el enlace: Ejemplo De API Usando Rest. Hazme saber lo que piensas y lo que crees que hice mal.

 0
Author: jeremyh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-16 20:57:36

He escrito dos clientes de HATEOAS, una en Java y otra en Ruby y comparto tu frustración. En ambas ocasiones hubo una completa falta de soporte de herramientas para lo que estaba haciendo. Por ejemplo, la API REST que estaba usando me diría qué método HTTP usar para cada control de hipertexto, pero HttpClient no te deja pasar el método, así que terminé con el siguiente código feo (POR cierto, todo el código vive dentro de una tarea Ant personalizada, de ahí las BuildExceptions):

private HttpMethod getHypermediaControl(Node href, Node method,
        NodeList children) {
    if (href == null) {
        return null;
    }
    HttpMethod control;
    if (method == null || method.getNodeValue().equals("")
            || method.getNodeValue().equalsIgnoreCase("GET")) {
        control = new GetMethod(href.getNodeValue());
    } else if (method.getNodeValue().equalsIgnoreCase("POST")) {
        control = new PostMethod(href.getNodeValue());
    } else if (method.getNodeValue().equalsIgnoreCase("PUT")) {
        control = new PutMethod(href.getNodeValue());
    } else if (method.getNodeValue().equalsIgnoreCase("DELETE")) {
        control = new DeleteMethod(href.getNodeValue());
    } else {
        throw new BuildException("Unknown/Unimplemented method "
                + method.getNodeValue());
    }
    control.addRequestHeader(accept);
    return control;
}

Esto terminó siendo la base para un REST métodos de utilidad de cliente que uso.

private HttpMethod getHypermediaControl(String path, Document source)
        throws TransformerException, IOException {

    Node node = XPathAPI.selectSingleNode(source, path);
    return getHypermediaControl(node);
}

private HttpMethod getHypermediaControl(Node node) {
    if (node == null) {
        return null;
    }
    NamedNodeMap attributes = node.getAttributes();
    if (attributes == null) {
        return null;
    }
    Node href = attributes.getNamedItem("href");
    Node method = attributes.getNamedItem("method");
    HttpMethod control = getHypermediaControl(href, method,
            node.getChildNodes());
    return control;
}

private Document invokeHypermediaControl(HttpClient client, Document node,
        final String path) throws TransformerException, IOException,
        HttpException, URIException, SAXException,
        ParserConfigurationException, FactoryConfigurationError {
    HttpMethod method = getHypermediaControl(path, node);
    if (method == null) {
        throw new BuildException("Unable to find hypermedia controls for "
                + path);
    }
    int status = client.executeMethod(method);

    if (status != HttpStatus.SC_OK) {
        log(method.getStatusLine().toString(), Project.MSG_ERR);
        log(method.getResponseBodyAsString(), Project.MSG_ERR);
        throw new BuildException("Unexpected status code ("
                + method.getStatusCode() + ") from " + method.getURI());
    }
    String strResp = method.getResponseBodyAsString();
    StringReader reader = new StringReader(strResp);
    Document resp = getBuilder().parse(new InputSource(reader));
    Node rval = XPathAPI.selectSingleNode(resp, "/");
    if (rval == null) {
        log(method.getStatusLine().toString(), Project.MSG_ERR);
        log(method.getResponseBodyAsString(), Project.MSG_ERR);
        throw new BuildException("Could not handle response");
    }
    method.releaseConnection();
    return resp;
}

Con este poco de código, puedo escribir fácilmente clientes que atravesarán los controles de hipermedia en los documentos que se devuelven. El bit principal que falta es soporte para parámetros de formulario. Afortunadamente para mí, todos los controles que estoy usando no tienen parámetros, excepto uno (sigo la regla de tres en lo que respecta a la refactorización). Para completar esto es lo que se ve ese fragmento de código como:

    HttpMethod licenseUpdateMethod = getHypermediaControl(
            "/license/update", licenseNode);
    if (licenseUpdateMethod == null) {
        log(getStringFromDoc(licenseNode), Project.MSG_ERR);
        throw new BuildException(
                "Unable to find hypermedia controls to get the test suites or install the license");
    } else if (license != null) {
        EntityEnclosingMethod eem = (EntityEnclosingMethod) licenseUpdateMethod;
        Part[] parts = { new StringPart("license", this.license) };
        eem.setRequestEntity(new MultipartRequestEntity(parts, eem
                .getParams()));
        int status2 = client.executeMethod(eem);
        if (status2 != HttpStatus.SC_OK) {
            log(eem.getStatusLine().toString(), Project.MSG_ERR);
            log(eem.getResponseBodyAsString(), Project.MSG_ERR);
            throw new BuildException("Unexpected status code ("
                    + eem.getStatusCode() + ") from " + eem.getURI());
        }
        eem.releaseConnection();
    }

Ahora, lo que debería estar haciendo es mirar a los hijos de /license/update para averiguar qué parámetros deben pasarse, pero eso tendrá que esperar hasta que tenga dos formas parametrizadas más que necesito seguir.

POR cierto, después de todo el esfuerzo, ha sido extremadamente satisfactorio y fácil modificar el servidor sin afectar al cliente. Se sintió tan bien que me sorprende que no esté prohibido en algunos estados.

 0
Author: Tom Howard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-31 20:27:42

Su navegador web de elección es un cliente "pure HATEOAS" para toda la WWW.

La pregunta realmente no tiene sentido imo.

 -3
Author: Gandalf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-11-23 21:21:39