summaryrefslogtreecommitdiff
path: root/posts/2010-10-20-spore-update.md
diff options
context:
space:
mode:
authorFranck Cuny <franck.cuny@gmail.com>2016-08-13 09:04:37 -0700
committerFranck Cuny <franck.cuny@gmail.com>2016-08-13 09:04:37 -0700
commit44b0a689cdd2ae2f3f34451910eb1f09fd4d65dd (patch)
tree5a3588c8928bbd56f426b091b68f17cc533795d1 /posts/2010-10-20-spore-update.md
parentMove assets around (diff)
downloadlumberjaph-44b0a689cdd2ae2f3f34451910eb1f09fd4d65dd.tar.gz
some more clean up.
Diffstat (limited to '')
-rw-r--r--posts/2010-10-20-spore-update.md336
1 files changed, 161 insertions, 175 deletions
diff --git a/posts/2010-10-20-spore-update.md b/posts/2010-10-20-spore-update.md
index bd363c7..f6c2c63 100644
--- a/posts/2010-10-20-spore-update.md
+++ b/posts/2010-10-20-spore-update.md
@@ -14,54 +14,52 @@ in addition to the already existing implementations:
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
------------
+## Consistency
-``` example
- require 'Spore'
+``` lua
+require 'Spore'
- local github = Spore.new_from_spec 'github.json'
+local github = Spore.new_from_spec 'github.json'
- github:enable 'Format.JSON'
- github:enable 'Runtime'
+github:enable 'Format.JSON'
+github:enable 'Runtime'
- local r = github:user_information{format = 'json', username = 'schacon'}
+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
+print(r.status) --> 200
+print(r.headers['x-runtime']) --> 126ms
+print(r.body.user.name) --> Scott Chacon
```
``` perl
- use Net::HTTP::Spore;
+use Net::HTTP::Spore;
- my $gh = Net::HTTP::Spore->new_from_spec('github.json');
+my $gh = Net::HTTP::Spore->new_from_spec('github.json');
- $gh->enable('Format::JSON');
- $gh->enable('Runtime');
+$gh->enable('Format::JSON');
+$gh->enable('Runtime');
- my $r= $gh->user_information( format => 'json', username => 'schacon' );
+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
+say "HTTP status => ".$r->status; # 200
+say "Runtime => ".$r->header('X-Spore-Runtime'); # 128ms
+say "username => ".$r->body->{user}->{name}; # Scott Chacon
```
``` ruby
+require 'spore'
- require 'spore'
+gh = Spore.new(File.join(File.dirname(__FILE__), 'github.json'))
- 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
- 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' )
- # 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
+puts "HTTP status => ".r.code # 200
+puts "Runtime => ".r.header('X-Spore-Runtime') # 128ms
+puts "username => ".r.body['user']['name'] # Scott Chacon
```
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:
@@ -71,88 +69,84 @@ As you can see in the previous example, I do the same request on the GitHub API:
- 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
------------------------------------------
+## 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
----------------------
+## 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
------------------------------------------
+## Easy to use with APIs that are compatible
If you want to use the Twitter public timeline:
``` perl
- use Net::HTTP::Spore;
+use Net::HTTP::Spore;
- my $client = Net::HTTP::Spore->new_from_spec('twitter.json');
+my $client = Net::HTTP::Spore->new_from_spec('twitter.json');
- $client->enable('Format::JSON');
+$client->enable('Format::JSON');
- my $r = $client->public_timeline( 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;
- }
+foreach my $t ( @{ $r->body } ) {
+ my $username = Encode::encode_utf8( $t->{user}->{name} );
+ my $text = Encode::encode_utf8( $t->{text} );
+ say $username . " says " . $text;
+}
```
And now on statusnet:
``` perl
- use Net::HTTP::Spore;
+use Net::HTTP::Spore;
- my $client = Net::HTTP::Spore->new_from_spec(
- 'twitter.json',
- base_url => 'http://identi.ca/api'
- );
+my $client = Net::HTTP::Spore->new_from_spec(
+ 'twitter.json',
+ base_url => 'http://identi.ca/api'
+);
- $client->enable('Format::JSON');
+$client->enable('Format::JSON');
- my $r = $client->public_timeline( 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;
- }
+foreach my $t ( @{ $r->body } ) {
+ my $username = Encode::encode_utf8( $t->{user}->{name} );
+ my $text = Encode::encode_utf8( $t->{text} );
+ say $username . " says " . $text;
+}
```
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 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:
-``` 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/"
- }
- }
+``` json
+{
+ "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/"
+ }
+}
```
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).
@@ -161,12 +155,11 @@ More descriptions are available on [GitHub](http://github.com/SPORE/api-descript
We also have [a schema](http://github.com/SPORE/specifications/blob/master/spore_validation.rx) to validate your descriptions.
-Middlewares
------------
+## 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
+## Control your workflow
The use of middlewares allow you to control your workflow as with Plack/Rack/WSGI. You can easily imagine doing this:
@@ -185,32 +178,32 @@ Or to interrogate a site as an API:
- 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
+## Creating a repository on GitHub
In this example, we use a middleware to authenticate on the GitHub API:
``` perl
- use Config::GitLike;
- use Net::HTTP::Spore;
+use Config::GitLike;
+use Net::HTTP::Spore;
- my $c = Config::GitLike::Git->new(); $c->load;
+my $c = Config::GitLike::Git->new(); $c->load;
- my $login = $c->get(key => 'github.user');
- my $token = $c->get(key => 'github.token');
+my $login = $c->get(key => 'github.user');
+my $token = $c->get(key => 'github.token');
- my $github = Net::HTTP::Spore->new_from_spec('github.json');
+my $github = Net::HTTP::Spore->new_from_spec('github.json');
- $github->enable('Format::JSON');
- $github->enable(
- 'Auth::Basic',
- username => $login . '/token',
- password => $token,
- );
+$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}
- );
+my $res = $github->create_repo(
+ format => 'json',
+ payload => {name => $name, description => $desc}
+);
```
The middleware Auth::Basic will add the **authorization** header to the request, using the given tokens.
@@ -220,107 +213,100 @@ The middleware Auth::Basic will add the **authorization** header to the request,
I really like [MooseX::Role::Parameterized](http://search.cpan.org/perldoc?MooseX::Role::Parameterized). This module allows you to build dynamically a Role to apply to your class/object:
``` 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;
+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;
```
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
-----------------------------------------
+## 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:
``` perl
- package myapp;
+package myapp;
- use Moose;
- has couchdb_client => (is => 'rw', isa => 'Object');
+use Moose;
+has couchdb_client => (is => 'rw', isa => 'Object');
- use Test::More;
- use JSON;
+use Test::More;
+use JSON;
- use myapp;
+use myapp;
- use Net::HTTP::Spore;
+use Net::HTTP::Spore;
- my $content = { title => "blog post", website => "http://lumberjaph.net" };
+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)
- );
- },
- };
+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' );
+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 );
+$client->enable('Format::JSON');
+$client->enable( 'Mock', tests => $mock_server );
- my $app = myapp->new(couchdb_client => $client);
+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';
+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;
+done_testing;
```
The middleware catches the request, checks if it matches something defined by the user and returns a response.
-So ...
-------
+## 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.