Posty oznaczone etykietą api

RESTful JavaScript client w kwadrans

Próbowałem restful.js, próbowałem jQuery REST client, aż ostatecznie dałem sobie spokój. Dla mnie były jakieś trudne/ciężkie w użyciu. A czym powinien być REST client? Cienkim wrapperem na dowolnego klienta HTTP, który gada z dowolnym url-em.

Na własne potrzeby założyłem, że:

  • interfejs ma być banalnie prosty w użyciu
  • wystarczy obsługa tylko content type application/json
  • ma być obsługa nagłówków, także defaultowych (żeby się nie powtarzać)
  • dozwolona jest zależność od jQuery (można się tego względnie łatwo wyzbyć)

Powstał prototyp:

var RestAPI = function(url, params) {
    var absoluteUrlPattern = /^https?:\/\//i,
        self=this,
        params = params || {};

    this.defaultHeaders = params.headers || {};
    this.url = url;

    function isAbsoluteUrl(url) {
        return absoluteUrlPattern.test(url);
    }

    function resourceToUrl(resource) {
        resource = resource || '';
        if(isAbsoluteUrl(resource)) {
            return resource;
        } else {
            return self.url+'/'+resource;
        }
    }

    function makeResponseObject(jqXhr, data) {
        var headers = {},
            headersList = jqXhr.getAllResponseHeaders().split('\r\n');
        $.each(headersList, function(i, headerStr) {
            var headerTuple = headerStr.split(': ');
            if(headerTuple[0]) {
                headers[headerTuple[0].toLowerCase()] = headerTuple[1];
            }
        });
        return {
            status: jqXhr.status,
            statusText: jqXhr.statusText,
            headers: headers,
            data: data 
        };
    }

    function doMethod(type, resource, data, headers) {
        var dfr = $.Deferred(),
            completeHeaders = $.extend({}, self.defaultHeaders, headers);

        $.ajax({
            url: resourceToUrl(resource),
            type: type,
            dataType: 'json',
            contentType: 'application/json',
            headers: completeHeaders,
            data: typeof(data)!=='undefined' && data!==null ? JSON.stringify(data) : null
        }).then(function(respData,respStatus,jqXhr) {
            dfr.resolve(respData, makeResponseObject(jqXhr, respData));
        }, function(jqXhr) {
            dfr.reject(makeResponseObject(jqXhr));
        });
        return dfr.promise();
    };


    this.get = function(resource, params, headers) {
        return doMethod('GET', resource, params, headers);
    }

    this.post = function(resource, payload, headers) {
        return doMethod('POST', resource, payload, headers);
    }

    this.put = function(resource, payload, headers) {
        return doMethod('PUT', resource, payload, headers);
    }

    this.patch = function(resource, payload, headers) {
        return doMethod('PATCH', resource, payload, headers);
    }

    this['delete'] = function(resource, headers) {
        return doMethod('DELETE', resource, null, headers);
    }

}

Działa? Działa, i to ciekawie.

Przykład - kto mnie śledzi na GitHub?

var github = new RestAPI('https://api.github.com');

github.get('users/marcinn').then(function(user) {
    github.get(user.followers_url).then(function(followers) {
        console.log(followers);
    });
});

Koniec, kropka. Prosto i na temat. Pobawta się sami na plunkr.

Jak kogo interesuje - publikuję na licencji "Brać! Mać..." oczywiście bez żadnej gwarancji. Wspomnijta w creditsach, że był taki Nowak, któremu to się klepać kodu nie chciało.

I jeszcze jedno - skoro się niektórzy chwalą filesize, to ja też podam:

  • Original: 2370 bytes
  • Gzipped: 764 bytes

Niech przebijają.


A AngularJS i jego ngResource albo nawet Restangular? Głupotą jest mapowanie REST na CRUD (albo stosowanie active record). W REST mamy dobrze określone VERBS. Nie ma wśród nich save. I tyle w temacie.

REST po ludzku

Znalazłem ostatnio dwa ciekawe wpisy traktujące o architekturze REST. Pierwszy z nich jest autorstwa niezastąpionego Martina Fowlera, który ciekawie przedstawił drogę od prostych usług typu RPC aż po podstawy RESTful (ale nie samego REST-a). Zdecydowanie ułatwia przejście od jednej do drugiej koncepcji: http://martinfowler.com/articles/richardsonMaturityModel.html

Z kolei drugą perełką jest wpis Ryana Tomayko, programisty GitHuba i Heroku, który wytłumaczył idee RESTful (wirtualnej?) żonie. Niestety środowisko gender zaatakowało Ryana i autor usunął artykuł-wywiad, ale na szczęście (w tym wypadku) Internet zachował jego kopie. Znajdziecie takową m.in. na archive.org: http://web.archive.org/web/20130116005443/http://tomayko.com/writings/rest-to-my-wife

Miłej lektury!

Allegro Web API - czy jest wadliwe?

Allegro WebAPI jest bodaj najbardziej rozbudowanym i najlepiej udokumentowanym API spośród polskich serwisów aukcyjnych. Allegro jest wiodącym serwisem aukcyjnym w Polsce, z największą liczbą sprzedających, kupujących oraz aplikacji ułatwiających integrację z serwisem.

Podczas implementowania klienta Allegro natrafiłem na kilka mniej lub bardziej istotnych problemów. Poniżej przedstawię kluczowe dolegliwości Allegro API i serwisu testowego, które moim zdaniem powinny zostać wyeliminowane w nowszych wersjach.

Allegro WebAPI

  1. niespójność argumentów

    raz podaje się country-code, country-id, czasem zamiast myślnika używany jest podkreślnik, a w nowszych metodach zamiast webapi-key i country-code uzywany jest session-handle.

  2. niespójność odpowiedzi

    niektóre metody zwracają listy, a niektóre słowniki - czasem odwoływać można się przez klucz, a czasem trzeba przez index (najlepiej zamapować indeksy na klucze, co zrobiłem w moim kliencie Allegro)

  3. nazwy kluczy

    • prefiksowanie wszystkich kluczy nie ma sensu

      wydłuża kod, jest bardziej błędogenne (prawdopodobieństwo literówek jest większe)

    • myślniki użyte w kluczach to fatalny pomysł

      zwykle do właściwości obiektów SOAP`a można dostać się przez... właściwości obiektu; w przypadku Allegro odwoływać można się tylko przez klucz (nie można używać myślnika w nazwach properties)

  4. wersjonowanie

    API Allegro jest wersjonowane, ale w danym czasie mamy dostęp tylko do najnowszej wersji. Aplikacje klienckie przestają działać prawidłowo, jeśli w API zostanie zerwana kompatybilność wstecz. A to zdarza się dość często. Niestety o takiej krytycznej zmianie świadczy często skala lamentów na forum Allegro WebAPI. Trzeba (niestety) śledzić komunikaty.

    Takie podejście uniemożliwia napisanie działającej aplikacji klienckiej. Co najwyżej można ustrzec się przed padem za pomocą odmowy wykonania operacji i komunikatu w stylu "API Allegro zostało zaktualizowane. Skontaktuj się ze sprzedawcą".

    Brak dostępu do różnych wersji API uważam to za największą wadę Allegro.

    Kluczowe zmiany, zrywające BC, powinny być wprowadzane tylko w wersjach, których pierwszy (najważniejszy) numer ulega zmianie. Pozostałe numery wersji powinny odpowiadać odpowiednio za minor features oraz bugfixes. Kilka głównych gałęzi API powinno być stale dostępnych.

    Nowe wersje również powinny być udostępniane programistom, aby mogli przygotować klienty pod nowe API. Jednocześnie byliby betatesterami.

  5. skomplikowane struktury dancych

    Niektóre metody API przyjmują zadziwiające struktury danych, aby zrealizować jakieś zadanie. Niech za przykład posłuży metoda doNewAuctionExt i sposób przekazania danych za pomocą fields.

Środowisko testowe (testwebapi.pl)

  1. Niedostępne opcje

    Niektóre funkcjonalności nie są dostępne w środowisku testowym, np. Płacę z Allegro (PzA). Rozumiem, że PzA jest oddzielnym podsystemem i nie ma trybu testowego.

  2. Puste wyniki

    Niektóre funkcje API w trybie testowym nie zwracają danych. Nie da się w pełni przetestować aplikacji klienckiej.

  3. Błędy interfejsu

    Formularze, które po zwalidowaniu gubią dane doprowadzają do powstawania nerwowych drgawek. Stąd wnioskuję, że oprogramowanie testowe jest zupełnie inne niż wersja produkcyjna (tam nie występują podobne błędy).

  4. Urywający się PHP

    Często po edycji aukcji lub wystawiania na nowo nie ma przycisku "List an item". Nie ma też stopki. Skrypt PHP urywa się gdzieś w połowie. Wystawienie nowej aukcji rozwiązuje problem, ale wydłuża czas testowania.

Podsumowanie

Czy kiedyś powyższe problemy będą rozwiązane? Liczę na to. A póki co koszty implementacji i utrzymania oprogramowania opartego na API Allegro idą górę.

Mimo wyżej wymienionych wad Allegro WebAPI jest dość porządne, szczególnie ze względu na:

  • ilość dostępnych funkcji,
  • dość dobrą i pełną dokumentację,
  • stały support ze strony Allegro na forum,
  • komunikaty o zmianach zrywających kompatybilność (lepsze to niż nic, choć nie rozwiązuje problemu wersjonowania).

API jednego z serwisów aukcyjnych na Węgrzech wypada przy Allegro bardzo słabo.

Kiedy Django zawodzi

Słoneczny dzień

Django jest świetnym rozwiązaniem należącym do kategorii full stack framework. Spójna budowa, setki aplikacji, rozwiązania generyczne (content types framework, comments framework) - batteries included. Świetne narzędzie do realizacji małych i średnich projektów, pozwala zaoszczędzić wiele czasu, nawet gdy istnieje potrzeba napisania od podstaw własnych rozszerzeń. Ostatnio jednak natrafiłem na mur.

Nadciągają cumulusy, czy rozumiesz co to znaczy?

Realizuję zupełnie nową funkcjonalność do portalu górskiego , która generuje sporo żądań XHR. Z rozpędu dodałem widoki w Django i podpiąłem je do szablonów. Rezultat powalił mnie... powolnością odpowiedzi. I to nie chodzi już o benchmarki, tylko o zwykłe odczucia użytkownika.

Szukając rozwiązania udałem się w kierunku Twisted i Tornado. Twisted jest dojrzałym frameworkiem sieciowym sterowanym zdarzeniami, a Tornado młodym web serverem napisanym specjalnie na potrzeby FriendFeed. Obydwa produkty cechuje wysoka wydajność, lecz z racji prostoty Tornado zająłem się nim na pierwszym miejscu. Niestety polubiłem go i mam nadzieję, że kiedyś wrócę do Twisted przy realizacji jakiegoś projektu. Do rzeczy.

Tornado

Tornado wieje z siłą urywającą nie łeb, ale setki głów. Niech poniższy kod (zacytowany ze strony Tornado) posłuży za prosty benchmark i przykład jednocześnie:

import tornado.httpserver
import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

application = tornado.web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

Wyniki prostego pomiaru na mojej niespecjalnej maszynie:

$ ab -c 1000 -n 10000 http://127.0.0.1:8888/
>> 3208.29 [#/sec]

Zainteresowanych dokładniejszymi testami odsyłam do porównania serwerów asynchronicznych.

Oczywiście docelowo nie będzie tak różowo. Zapytania bazodanowe i kosztowne algorytmy osłabią ten wynik. Krótkie podsumowanie wydajności tej samej funkcji REST API:

  • Django/ORM: 2.47 rq/sec
  • Django/plain db queries (podsystem jako osobny pakiet): 66 rq/sec
  • Tornado/plain db queries (dokladnie to samo, co wyżej): 215 rq/sec (-c100 -n10000, 0 failed)
  • Tornado/plain db queries/in memory cache: 2183 rq/sec (-c100 -n10000, 0 failed)

Django server zapychał się już przy 400 requestach przy concurrency 10.

Jak przetrwać burzę

Jeśli projekt oparłeś w całości na Django, to skazany będziesz albo na refaktoryzację, albo na kombinatorykę pozwalającą importować moduły z aplikacji Django. Ja popełniłem ten błąd i umieściłem API oraz algorytmy w pakiecie zależnym od Django. Na szczęście był to dopiero prototyp i przeniosłem całość do odrębnego pakietu.

Prawidłowe rozwarstwienie podsystemu uczyni go łatwym do integracji. Dobrze jest wydzielić core do oddzielnego pakietu. Nie stosować w nim rozwiązań opierających się na elementach frameworków typu full-stack, szczególnie nie przywiązywać się do warstwy dostępu do danych. Zastosować wzorzec proxy, który ułatwi również opracowanie unit testów. Nie ulegać magii ani kucom na każdym kroku.