diff options
| author | Franck Cuny <franckcuny@gmail.com> | 2016-08-04 11:12:37 -0700 |
|---|---|---|
| committer | Franck Cuny <franckcuny@gmail.com> | 2016-08-04 11:12:37 -0700 |
| commit | 2d2a43f200b88627253f2906fbae87cef7c1e8ce (patch) | |
| tree | c65377350d12bd1e62e0bdd58458c1044541c27b /posts/2010-10-20-spore-update.org | |
| parent | Use Bullet list for the index. (diff) | |
| download | lumberjaph-2d2a43f200b88627253f2906fbae87cef7c1e8ce.tar.gz | |
Mass convert all posts from markdown to org.
Diffstat (limited to 'posts/2010-10-20-spore-update.org')
| -rw-r--r-- | posts/2010-10-20-spore-update.org | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/posts/2010-10-20-spore-update.org b/posts/2010-10-20-spore-update.org new file mode 100644 index 0000000..2febfe4 --- /dev/null +++ b/posts/2010-10-20-spore-update.org @@ -0,0 +1,380 @@ +As I've said +[[http://lumberjaph.net/conference/2010/10/12/osdcfr.html][in my OSDC +report]], after I +[[http://www.slideshare.net/franckcuny/spore][presented SPORE]] I've had +some positive feedback. In the last ten days, I've created a +[[http://groups.google.com/group/spore-rest][google group]] to discuss +the current specification and implementations, a +[[http://github.com/SPORE][SPORE account on github]] to hold the +implementation specifications and the API descriptions files, and more +importantly, we have some new implementations: + +- [[http://github.com/sukria/Ruby-Spore][Ruby]] +- [[http://github.com/francois2metz/node-spore][node.js]] +- [[http://github.com/nikopol/jquery-spore][Javascript]] (client side) +- PHP (not published yet) + +in addition to the already existing implementations: + +- [[http://github.com/franckcuny/net-http-spore][Perl]] +- [[http://github.com/fperrad/lua-Spore][Lua]] +- [[http://github.com/ngrunwald/clj-spore][Clojure]] +- [[http://github.com/elishowk/pyspore][Python]] + +In this post, I'll try to show some common usages for SPORE, in order to +give a better explanation of why I think it's needed. + +** Consistency + +#+BEGIN_EXAMPLE + require 'Spore' + + local github = Spore.new_from_spec 'github.json' + + github:enable 'Format.JSON' + github:enable 'Runtime' + + local r = github:user_information{format = 'json', username = 'schacon'} + + print(r.status) --> 200 + print(r.headers['x-runtime']) --> 126ms + print(r.body.user.name) --> Scott Chacon +#+END_EXAMPLE + +#+BEGIN_SRC perl + use Net::HTTP::Spore; + + my $gh = Net::HTTP::Spore->new_from_spec('github.json'); + + $gh->enable('Format::JSON'); + $gh->enable('Runtime'); + + my $r= $gh->user_information( format => 'json', username => 'schacon' ); + + say "HTTP status => ".$r->status; # 200 + say "Runtime => ".$r->header('X-Spore-Runtime'); # 128ms + say "username => ".$r->body->{user}->{name}; # Scott Chacon +#+END_SRC + +#+BEGIN_SRC ruby + + require 'spore' + + gh = Spore.new(File.join(File.dirname(__FILE__), 'github.json')) + + gh.enable(Spore::Middleware::Runtime) # will add a header with runtime + gh.enable(Spore::Middleware::Format::JSON) # will deserialize JSON responses + + # API call + r = gh.user_information( :format => 'json', :username => 'schacon' ) + + puts "HTTP status => ".r.code # 200 + puts "Runtime => ".r.header('X-Spore-Runtime') # 128ms + puts "username => ".r.body['user']['name'] # Scott Chacon +#+END_SRC + +As you can see in the previous example, I do the same request on the +GitHub API: fetch informations from the user "mojombo". In the three +languages, the API for the client is the same: + +- you create a client using the github.json API description +- you enable some middlewares +- you execute your request: the method name is the same, the argument + names are the same! +- you manipulate the result the same way + +** Easy to switch from a language to another + +You can switch from a language to another without any surprises. If you +must provide an API client to a third-party, you don't have to care +about what languages they use, you only need to provide a description. +Your methods call will be the same between all the languages, so it's +easy to switch between languages, without the need to chose an +appropriate client for your API (if one exists), to read the +documentation (when there is one), and having the client implementation +going in your way. + +** Better maintanability + +What annoys me the most when I want to use an API, is that I have to +choose between two, three, or more clients that will communicate with +this API. I need to read the documentations, the code, and test thoses +implementations to decide which one will best fit my needs, and won't go +in my way. And what if I need to do caching before the content is +deserialized ? And what if the remote API changes it's authentication +method (like twitter, from basic auth to OAuth) and the maintainer of +the client doesn't update the code ? + +With SPORE, you don't have to maintain a client, only a description +file. Your API changes, all you have to do is to update your +description, and all the clients, using any language, will be able to +use your new API, without the need to release a new client specific for +this API in javascript, Perl, Ruby, ... + +** Easy to use with APIs that are compatible + +If you want to use the Twitter public timeline: + +#+BEGIN_SRC perl + use Net::HTTP::Spore; + + my $client = Net::HTTP::Spore->new_from_spec('twitter.json'); + + $client->enable('Format::JSON'); + + my $r = $client->public_timeline( format => 'json' ); + + foreach my $t ( @{ $r->body } ) { + my $username = Encode::encode_utf8( $t->{user}->{name} ); + my $text = Encode::encode_utf8( $t->{text} ); + say $username . " says " . $text; + } +#+END_SRC + +And now on statusnet: + +#+BEGIN_SRC perl + use Net::HTTP::Spore; + + my $client = Net::HTTP::Spore->new_from_spec( + 'twitter.json', + base_url => 'http://identi.ca/api' + ); + + $client->enable('Format::JSON'); + + my $r = $client->public_timeline( format => 'json' ); + + foreach my $t ( @{ $r->body } ) { + my $username = Encode::encode_utf8( $t->{user}->{name} ); + my $text = Encode::encode_utf8( $t->{text} ); + say $username . " says " . $text; + } +#+END_SRC + +easy, right ? As both APIs are compatible, the only thing you need to do +is change the argument *base\_url* when you create your new client. + +** It's easy to write a description + +It's really easy to write a description for your API. Let's take a look +at the one for github: + +#+BEGIN_EXAMPLE + { + "base_url" : "http://github.com/api/v2/", + "version" : "0.2", + "methods" : { + "follow" : { + "required_params" : [ + "user", + "format" + ], + "path" : "/:format/user/follow/:user", + "method" : "POST", + "authentication" : true + } + }, + "name" : "GitHub", + "authority" : "GITHUB:franckcuny", + "meta" : { + "documentation" : "http://develop.github.com/" + } + } +#+END_EXAMPLE + +The important parts are the basic API description (with a name, a base +url for the API) and the list of available methods (here I've only put +the 'follow' method). + +More descriptions are available on +[[http://github.com/SPORE/api-description][GitHub]], as well as and the +[[http://github.com/SPORE/specifications/blob/master/spore_description.pod][full +specification]]. + +We also have +[[http://github.com/SPORE/specifications/blob/master/spore_validation.rx][a +schema]] to validate your descriptions. + +** Middlewares + +By default, your SPORE client will only do a request and return a +result. But it's easy to alter the default behavior with various +middlewares. The most obvious one is the deserialization for a response, +like the previous example with github and the middleware Format::JSON. + +*** Control your workflow + +The use of middlewares allow you to control your workflow as with +Plack/Rack/WSGI. You can easily imagine doing this: + +- check if the request has already been made and cached +- return the response if the cache is still valid +- perform the request +- send the content to a remote storer in raw format +- cache the raw data locally +- deserialize to json +- remove some data from the response +- give the response to the client + +Or to interrogate a site as an API: + +- send a request on a web page +- pass the response to a scraper, and put the data in JSON +- return the JSON with scraped data to the client + +*** Creating a repository on GitHub + +In this example, we use a middleware to authenticate on the GitHub API: + +#+BEGIN_SRC perl + use Config::GitLike; + use Net::HTTP::Spore; + + my $c = Config::GitLike::Git->new(); $c->load; + + my $login = $c->get(key => 'github.user'); + my $token = $c->get(key => 'github.token'); + + my $github = Net::HTTP::Spore->new_from_spec('github.json'); + + $github->enable('Format::JSON'); + $github->enable( + 'Auth::Basic', + username => $login . '/token', + password => $token, + ); + + my $res = $github->create_repo( + format => 'json', + payload => {name => $name, description => $desc} + ); +#+END_SRC + +The middleware Auth::Basic will add the *authorization* header to the +request, using the given tokens. + +*** SPORE + MooseX::Role::Parameterized + +I really like +[[http://search.cpan.org/perldoc?MooseX::Role::Parameterized][MooseX::Role::Parameterized]]. +This module allows you to build dynamically a Role to apply to your +class/object: + +#+BEGIN_SRC perl + package My::App::Role::SPORE; + + use MooseX::Role::Parameterized; + use Net::HTTP::Spore; + + parameter name => ( isa => 'Str', required => 1, ); + + role { + my $p = shift; + my $name = $p->name; + + has $name => ( + is => 'rw', + isa => 'Object', + lazy => 1, + default => sub { + my $self = shift; + my $client_config = $self->context->{spore}->{$name}; + my $client = Net::HTTP::Spore->new_from_spec( + $client_config->{spec}, + %{ $client_config->{options} }, + ); + foreach my $mw ( @{ $client_config->{middlewares} } ) { + $client->enable($mw); + } + }, + ); + }; + + 1; + + # in your app + + package My::App; + + use Moose; + + with 'My::App::Role::SPORE' => { name => 'couchdb' }, + 'My::App::Role::SPORE' => { name => 'url_solver' }; + + 1; +#+END_SRC + +This Role will add two new attributes to my class: *couchdb* and +*url\_solver*, reading from a config file a list of middlewares to apply +and the options (like base\_uri). + +** Testing my application that uses CouchDB + +This is a common case. In your application you use CouchDB to store some +information. When you run the tests for this application, you don't know +if there will be a couchdb running on the host, if it will be on the +default port, on what database should you do your tests, ... + +The Perl implementation of SPORE comes with a Mock middleware: + +#+BEGIN_SRC perl + package myapp; + + use Moose; + has couchdb_client => (is => 'rw', isa => 'Object'); + + use Test::More; + use JSON; + + use myapp; + + use Net::HTTP::Spore; + + my $content = { title => "blog post", website => "http://lumberjaph.net" }; + + my $mock_server = { + '/test_database/1234' => sub { + my $req = shift; + $req->new_response( + 200, + [ 'Content-Type' => 'application/json' ], + JSON::encode_json($content) + ); + }, + }; + + ok my $client = + Net::HTTP::Spore->new_from_spec( + '/home/franck/code/projects/spore/specifications/apps/couchdb.json', + base_url => 'http://localhost:5984' ); + + $client->enable('Format::JSON'); + $client->enable( 'Mock', tests => $mock_server ); + + my $app = myapp->new(couchdb_client => $client); + + my $res = + $app->client->get_document( database => 'test_database', doc_id => '1234' ); + is $res->[0], 200; + is_deeply $res->[2], $content; + is $res->header('Content-Type'), 'application/json'; + + done_testing; +#+END_SRC + +The middleware catches the request, checks if it matches something +defined by the user and returns a response. + +** So ... + +I really see SPORE as something Perlish: a glue. The various +implementations are a nice addition, and I'm happy to see some +suggestions and discussions about the specifications. + +I'm pretty confident that the current specification for the API +description is stable at this point. We still need to write more +middlewares to see if we can cover most of the usages easily, so we can +decide if the specification for the implementation is valid. + +(as always, thanks to bl0b!). |
