summary refs log tree commit diff
diff options
context:
space:
mode:
authorfranck cuny <franck@lumberjaph.net>2010-06-09 16:38:20 +0200
committerfranck cuny <franck@lumberjaph.net>2010-06-09 16:38:20 +0200
commitb797f187a22f38142ac3ac957f0e874ca492c2cd (patch)
tree7c754fc69ddf91e7c4475b84479d543c0e395727
downloadnet-riak-b797f187a22f38142ac3ac957f0e874ca492c2cd.tar.gz
initial import
-rw-r--r--dist.ini23
-rw-r--r--lib/Net/Riak.pm143
-rw-r--r--lib/Net/Riak/Bucket.pm191
-rw-r--r--lib/Net/Riak/Link.pm42
-rw-r--r--lib/Net/Riak/LinkPhase.pm20
-rw-r--r--lib/Net/Riak/MapReduce.pm219
-rw-r--r--lib/Net/Riak/MapReducePhase.pm31
-rw-r--r--lib/Net/Riak/Object.pm294
-rw-r--r--lib/Net/Riak/Role/REST.pm29
-rw-r--r--lib/Net/Riak/Role/UserAgent.pm20
-rw-r--r--t/01_basic.t226
11 files changed, 1238 insertions, 0 deletions
diff --git a/dist.ini b/dist.ini
new file mode 100644
index 0000000..d407db9
--- /dev/null
+++ b/dist.ini
@@ -0,0 +1,23 @@
+name = Net-Riak
+author = franck cuny <franck@lumberjaph.net>
+license = Perl_5
+copyright_holder = linkfluence
+copyright_year = 2010
+version = 0.01
+
+[@Filter]
+bundle = @Basic
+
+[MetaConfig]
+[MetaJSON]
+[PkgVersion]
+[PodSyntaxTests]
+[KwaliteeTests]
+[PodCoverageTests]
+[NoTabsTests]
+[EOLTests]
+[PodWeaver]
+[AutoPrereq]
+[ReadmeFromPod]
+[CheckChangeLog]
+[UploadToCPAN]
diff --git a/lib/Net/Riak.pm b/lib/Net/Riak.pm
new file mode 100644
index 0000000..f31415b
--- /dev/null
+++ b/lib/Net/Riak.pm
@@ -0,0 +1,143 @@
+package Net::Riak;
+
+# ABSTRACT: Interface to Riak
+
+use Moose;
+use MIME::Base64;
+
+use Net::Riak::Bucket;
+use Net::Riak::MapReduce;
+
+with qw/
+  Net::Riak::Role::REST
+  Net::Riak::Role::UserAgent
+  /;
+
+has host =>
+  (is => 'rw', isa => 'Str', coerce => 1, default => 'http://127.0.0.1:8098');
+has prefix        => (is => 'rw', isa => 'Str', default    => 'riak');
+has mapred_prefix => (is => 'rw', isa => 'Str', default    => 'mapred');
+has r             => (is => 'rw', isa => 'Int', default    => 2);
+has w             => (is => 'rw', isa => 'Int', default    => 2);
+has dw            => (is => 'rw', isa => 'Int', default    => 2);
+has client_id     => (is => 'rw', isa => 'Str', lazy_build => 1,);
+
+sub _build_client_id {
+    "perl_net_riak" . encode_base64(int(rand(10737411824)), '');
+}
+
+sub bucket {
+    my ($self, $name) = @_;
+    my $bucket = Net::Riak::Bucket->new(name => $name, client => $self);
+    $bucket;
+}
+
+sub is_alive {
+    my $self     = shift;
+    my $request  = $self->request('GET', ['ping']);
+    my $response = $self->useragent->request($request);
+    $response->is_success ? return 1 : return 0;
+}
+
+sub add {
+    my ($self, @args) = @_;
+    my $mr = Net::Riak::MapReduce->new(client => $self);
+    $mr->add(@args);
+    $mr;
+}
+
+sub link {
+    my ($self, @args) = @_;
+    my $mr = Net::Riak::MapReduce->new(client => $self);
+    $mr->link(@args);
+    $mr;
+}
+
+sub map {
+    my ($self, @args) = @_;
+    my $mr = Net::Riak::MapReduce->new(client => $self);
+    $mr->mapd(@args);
+    $mr;
+}
+
+sub reduce {
+    my ($self, @args) = @_;
+    my $mr = Net::Riak::MapReduce->new(client => $self);
+    $mr->reduce(@args);
+    $mr;
+}
+
+1;
+
+=head1 SYNOPSIS
+
+    my $client = Net::Riak->new(host => 'http://10.0.0.40:8098');
+    my $bucket = $client->bucket('blog');
+    my $obj    = $bucket->new_object('new_post', {title => 'foo', content => 'bar'});
+    $obj->store;
+
+    my $obj = $bucket->get('new_post');
+
+=head1 DESCRIPTION
+
+=head2 ATTRIBUTES
+
+=over 4
+
+=item B<host>
+
+Hostname or IP address (default 'http://127.0.0.1:8098')
+
+=item B<prefix>
+
+Interface prefix (default 'riak')
+
+=item B<mapred_prefix>
+
+MapReduce prefix (default 'mapred')
+
+=item B<r>
+
+R value setting for this client (default 2)
+
+=item B<w>
+
+W value setting for this client (default 2)
+
+=item B<dw>
+
+DW value setting for this client (default 2)
+
+=item B<client_id>
+
+client_id for this client
+
+=back
+
+=head2 METHODS
+
+=method bucket
+
+    my $bucket = $client->bucket($name);
+
+Get the bucket by the specified name. Since buckets always exist, this will always return a L<Net::Riak::Bucket>
+
+=method is_alive
+
+Check if the Riak server for this client is alive
+
+=method add
+
+Start assembling a Map/Reduce operation
+
+=method link
+
+Start assembling a Map/Reduce operation
+
+=method map
+
+Start assembling a Map/Reduce operation
+
+=method reduce
+
+Start assembling a Map/Reduce operation
diff --git a/lib/Net/Riak/Bucket.pm b/lib/Net/Riak/Bucket.pm
new file mode 100644
index 0000000..05f315f
--- /dev/null
+++ b/lib/Net/Riak/Bucket.pm
@@ -0,0 +1,191 @@
+package Net::Riak::Bucket;
+
+# ABSTRACT: Access and change information about a Riak bucket
+
+use JSON;
+use Moose;
+use Net::Riak::Object;
+
+has name => (
+    is       => 'ro',
+    isa      => 'Str',
+    required => 1
+);
+has client => (
+    is       => 'ro',
+    isa      => 'Net::Riak',
+    required => 1
+);
+has content_type => (
+    is      => 'rw',
+    isa     => 'Str',
+    default => 'application/json'
+);
+
+has r => (
+    is      => 'rw',
+    isa     => 'Int',
+    lazy    => 1,
+    default => sub { (shift)->client->r }
+);
+has w => (
+    is      => 'rw',
+    isa     => 'Int',
+    lazy    => 1,
+    default => sub { (shift)->client->w }
+);
+has dw => (
+    is      => 'rw',
+    isa     => 'Int',
+    lazy    => 1,
+    default => sub { (shift)->client->dw }
+);
+
+sub n_val {
+    my $self = shift;
+    if (my $val = shift) {
+        $self->set_property('n_val', $val);
+    }
+    else {
+        $self->get_property('n_val');
+    }
+}
+
+sub allow_multiples {
+    my $self = shift;
+    if (my $val = shift) {
+        $self->set_property('allow_mult', $val);
+    }
+    else {
+        return $self->get_property('allow_mult');
+    }
+}
+
+sub get {
+    my ($self, $key, $r) = @_;
+    my $obj = Net::Riak::Object->new(
+        client => $self->client,
+        bucket => $self,
+        key    => $key
+    );
+    $r ||= $self->r;
+    $obj->load($r);
+    $obj;
+}
+
+sub set_property {
+    my ($self, $key, $value) = @_;
+    $self->set_properties({$key => $value});
+}
+
+sub get_property {
+    my ($self, $key) = @_;
+    my $props = $self->get_properties;
+    return $props->{$key};
+}
+
+sub get_properties {
+    my $self = shift;
+
+    my $params = {props => 'True', keys => 'False'};
+
+    my $request =
+      $self->client->request('GET', [$self->client->prefix, $self->name],
+        $params);
+
+    my $response = $self->client->useragent->request($request);
+
+    my $props = {};
+    if ($response->is_success) {
+        $props = JSON::decode_json($response->content);
+        $props = $props->{props};
+    }
+    return $props;
+}
+
+sub set_properties {
+    my ($self, $props) = @_;
+
+    my $request = $self->client->request('PUT', [$self->client->prefix, $self->name]);
+    $request->header('Content-Type' => $self->content_type);
+    $request->content(JSON::encode_json({props => $props}));
+    my $response = $self->client->useragent->request($request);
+
+    if (!$response->is_success) {
+        # XXX
+    }
+
+    if ($response->code != 204) {
+        # XXX
+    }
+}
+
+sub new_object {
+    my ($self, $key, $data) = @_;
+    my $object = Net::Riak::Object->new(
+        key    => $key,
+        data   => $data,
+        bucket => $self,
+        client => $self->client
+    );
+}
+
+1;
+
+=head1 SYNOPSIS
+
+The L<Net::Riak::Bucket> object allows you to access and change information about a Riak bucket, and provides methods to create or retrieve objects within the bucket.
+
+=head1 DESCRIPTION
+
+=head2 ATTRIBUTES
+
+=item B<name>
+
+Get the bucket name
+
+=item B<r>
+
+R value setting for this client (default 2)
+
+=item B<w>
+
+W value setting for this client (default 2)
+
+=item B<dw>
+
+DW value setting for this client (default 2)
+
+=head2 METHODS
+
+=method new_object
+
+Create a new L<Net::Riak::Object> object that will be stored as JSON.
+
+=method get
+
+Retrieve a JSON-encoded object from Riak
+
+=method n_val
+
+Get/set the N-value for this bucket, which is the number of replicas that will be written of each object in the bucket. Set this once before you write any data to the bucket, and never change it again, otherwise unpredictable things could happen. This should only be used if you know what you are doing.
+
+=method allow_multiples
+
+If set to True, then writes with conflicting data will be stored and returned to the client. This situation can be detected by calling has_siblings() and get_siblings(). This should only be used if you know what you are doing.
+
+=method set_property
+
+Set a bucket property. This should only be used if you know what you are doing.
+
+=method get_property
+
+Retrieve a bucket property
+
+=method set_properties
+
+Set multiple bucket properties in one call. This should only be used if you know what you are doing.
+
+=method get_properties
+
+Retrieve an associative array of all bucket properties.
diff --git a/lib/Net/Riak/Link.pm b/lib/Net/Riak/Link.pm
new file mode 100644
index 0000000..43bace6
--- /dev/null
+++ b/lib/Net/Riak/Link.pm
@@ -0,0 +1,42 @@
+package Net::Riak::Link;
+
+# ABSTRACT: the riaklink object represents a link from one Riak object to another
+
+use Moose;
+
+has client => (
+    is       => 'ro',
+    isa      => 'Net::Riak',
+    required => 0,
+);
+has bucket => (
+    is       => 'ro',
+    isa      => 'Net::Riak::Bucket',
+    required => 1,
+);
+has key => (
+    is      => 'rw',
+    isa     => 'Str',
+    lazy    => 1,
+    default => '_',
+);
+has tag => (
+    is      => 'rw',
+    isa     => 'Str',
+    lazy    => 1,
+    default => '_',
+);
+
+sub to_link_header {
+    my ($self, $client) = @_;
+
+    my $link = '';
+    $link .= '</';
+    $link .= $client->prefix . '/';
+    $link .= $self->bucket . '/';
+    $link .= $self->key . '>; riaktag="';
+    $link .= self->tag . '"';
+    return $link;
+}
+
+1;
diff --git a/lib/Net/Riak/LinkPhase.pm b/lib/Net/Riak/LinkPhase.pm
new file mode 100644
index 0000000..61dadf5
--- /dev/null
+++ b/lib/Net/Riak/LinkPhase.pm
@@ -0,0 +1,20 @@
+package Net::Riak::LinkPhase;
+
+use Moose;
+use JSON;
+
+has bucket => (is => 'ro', isa => 'Str', required => 1);
+has tag    => (is => 'ro', isa => 'Str', required => 1);
+has keep   => (is => 'rw', isa => 'JSON::Boolean', required => 1);
+
+sub to_array {
+    my $self     = shift;
+    my $step_def = {
+        bucket => $self->bucket,
+        tag    => $self->tag,
+        keep   => $self->keep,
+    };
+    return {link => $step_def};
+}
+
+1;
diff --git a/lib/Net/Riak/MapReduce.pm b/lib/Net/Riak/MapReduce.pm
new file mode 100644
index 0000000..5f76723
--- /dev/null
+++ b/lib/Net/Riak/MapReduce.pm
@@ -0,0 +1,219 @@
+package Net::Riak::MapReduce;
+
+# ABSTRACT: Allows you to build up and run a map/reduce operation on Riak
+
+use JSON;
+use Moose;
+use Scalar::Util;
+
+use Net::Riak::LinkPhase;
+use Net::Riak::MapReducePhase;
+
+has client => (
+    is       => 'rw',
+    isa      => 'Net::Riak',
+    required => 1,
+);
+has phases => (
+    traits     => ['Array'],
+    is         => 'rw',
+    isa        => 'ArrayRef[Object]',
+    auto_deref => 1,
+    lazy       => 1,
+    default    => sub { [] },
+    handles    => {
+        get_phases => 'elements',
+        add_phase  => 'push',
+        num_phases => 'count',
+        get_phase  => 'get',
+    },
+);
+has inputs_bucket => (
+    is => 'rw',
+    isa => 'Str',
+    predicate => 'has_inputs_bucket',
+);
+has inputs => (
+    traits  => ['Array'],
+    is      => 'rw',
+    isa     => 'ArrayRef[ArrayRef]',
+    handles => {add_input => 'push',},
+    default => sub { [] },
+);
+has input_mode => (
+    is        => 'rw',
+    isa       => 'Str',
+    predicate => 'has_input_mode',
+);
+
+sub add {
+    my $self = shift;
+    my $arg  = shift;
+
+    if (!scalar @_) {
+        if (blessed($arg)) {
+            $self->add_object($arg);
+          } else {
+            $self->add_bucket($arg);
+        }
+    }
+    else {
+        $self->add_bucket_key_data($arg, @_);
+    }
+}
+
+sub add_object {
+    my ($self, $obj) = @_;
+    $self->add_bucket_key_data($obj->bucket->name, $obj->key);
+}
+
+sub add_bucket_key_data {
+    my ($self, $bucket, $key, $data) = @_;
+    if ($self->has_input_mode && $self->input_mode eq 'bucket') {
+        croak("Already added a bucket, can't add an object");
+    }
+    else {
+        $self->add_input([$bucket, $key, $data]);
+    }
+}
+
+sub add_bucket {
+    my ($self, $bucket) = @_;
+    $self->input_mode('bucket');
+    $self->inputs_bucket($bucket);
+}
+
+sub link {
+    my ($self, $bucket, $tag, $keep) = @_;
+    $bucket ||= '_';
+    $tag    ||= '_';
+    $keep   ||= JSON::false;
+
+    $self->add_phase(
+        Net::Riak::LinkPhase->new(
+            bucket => $bucket,
+            tag    => $tag,
+            keep   => $keep
+        )
+    );
+}
+
+sub map {
+    my ($self, $function, %options) = @_;
+
+    my $map_reduce = Net::Riak::MapReducePhase->new(
+        type     => 'map',
+        function => $function,
+        keep     => $options{keep} || JSON::false,
+        arg      => $options{arg} || [],
+    );
+    $self->add_phase($map_reduce);
+}
+
+sub reduce {
+    my ($self, $function, %options) = @_;
+
+    my $map_reduce = Net::Riak::MapReducePhase->new(
+        type     => 'reduce',
+        function => $function,
+        keep     => $options{keep} || JSON::false,
+        arg      => $options{arg} || [],
+    );
+    $self->add_phase($map_reduce);
+}
+
+sub run {
+    my ($self, $timeout) = @_;
+
+    my $num_phases = $self->num_phases;
+    my $keep_flag  = 0;
+    my $query      = [];
+
+    my $total_phase = $self->num_phases;
+    foreach my $i (0 .. ($total_phase - 1)) {
+        my $phase = $self->get_phase($i);
+        if ($i == ($total_phase - 1) && !$keep_flag) {
+            $phase->keep(JSON::true);
+        }
+        $keep_flag = 1 if ($phase->{keep}->isa(JSON::true));
+        push @$query, $phase->to_array;
+    }
+
+    my $inputs;
+    if ($self->has_input_mode && $self->input_mode eq 'bucket' && $self->has_inputs_bucket) {
+        $inputs = $self->inputs_bucket;
+    }else{
+        $inputs = $self->inputs;
+    }
+
+    my $job = {inputs => $inputs, query => $query};
+    if ($timeout) {
+        $job->{$timeout} = $timeout;
+    }
+
+    my $content = JSON::encode_json($job);
+
+    my $request =
+      $self->client->request('POST', [$self->client->mapred_prefix]);
+    $request->content($content);
+    my $response = $self->client->useragent->request($request);
+
+    my $result   = JSON::decode_json($response->content);
+
+    my @phases = $self->phases;
+    if (ref $phases[-1] ne 'Net::Riak::LinkPhase') {
+        return $result;
+    }
+
+    my $a = [];
+    foreach (@$result) {
+        my $l = Net::Riak::Link->new(
+            bucket => $_->[0],
+            key    => $_->[1],
+            tag    => $_->[2],
+            client => $self->client
+        );
+        push @$a, $l;
+    }
+    return $a;
+}
+
+1;
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+=head2 ATTRIBUTES
+
+=over 4
+
+=item B<phases>
+
+=item B<inputs_bucket>
+
+=item B<inputs>
+
+=item B<input_mode>
+
+=back
+
+=head2 METHODS
+
+=method add
+
+Add inputs to a map/reduce operation. This method takes three different forms, depending on the provided inputs. You can specify either a RiakObject, a string bucket name, or a bucket, key, and additional arg.
+
+=method add_object
+
+=method add_bucket_key_data
+
+=method add_bucket
+
+=method link
+
+=method map
+
+=method reduce
+
+=method run
diff --git a/lib/Net/Riak/MapReducePhase.pm b/lib/Net/Riak/MapReducePhase.pm
new file mode 100644
index 0000000..2d66775
--- /dev/null
+++ b/lib/Net/Riak/MapReducePhase.pm
@@ -0,0 +1,31 @@
+package Net::Riak::MapReducePhase;
+
+use Moose;
+use Scalar::Util;
+use JSON;
+
+has type     => (is => 'rw', isa => 'Str',      required => 1,);
+has function => (is => 'ro', isa => 'Str',      required => 1);
+has arg      => (is => 'ro', isa => 'ArrayRef', default  => 'None');
+has language => (is => 'ro', isa => 'Str',      default  => 'javascript');
+has keep => (is => 'rw', isa => 'JSON::Boolean', default => sub {JSON::false});
+
+sub to_array {
+    my $self = shift;
+
+    my $step_def = {
+        keep     => $self->keep,
+        language => $self->language,
+        arg      => $self->arg
+    };
+
+    if ($self->function =~ m!\{!) {
+        $step_def->{source} = $self->function;
+    }else{
+        $step_def->{name} = $self->function;
+    }
+    return {$self->type => $step_def};
+}
+
+
+1;
diff --git a/lib/Net/Riak/Object.pm b/lib/Net/Riak/Object.pm
new file mode 100644
index 0000000..7dc0ab2
--- /dev/null
+++ b/lib/Net/Riak/Object.pm
@@ -0,0 +1,294 @@
+package Net::Riak::Object;
+
+# ABSTRACT: holds meta information about a Riak object
+
+use Carp;
+use JSON;
+use Moose;
+use Scalar::Util;
+use Net::Riak::Link;
+
+has key    => (is => 'rw', isa => 'Str',               required => 1);
+has client => (is => 'rw', isa => 'Net::Riak',         required => 1);
+has bucket => (is => 'rw', isa => 'Net::Riak::Bucket', required => 1);
+has data => (is => 'rw', isa => 'Any', clearer => '_clear_data');
+has r =>
+  (is => 'rw', isa => 'Int', lazy => 1, default => sub { (shift)->client->r });
+has w =>
+  (is => 'rw', isa => 'Int', lazy => 1, default => sub { (shift)->client->w });
+has dw => (
+    is      => 'rw',
+    isa     => 'Int',
+    lazy    => 1,
+    default => sub { (shift)->client->dw }
+);
+has content_type => (is => 'rw', isa => 'Str', default => 'application/json');
+has status       => (is => 'rw', isa => 'Int');
+has links        => (
+    traits     => ['Array'],
+    is         => 'rw',
+    isa        => 'ArrayRef[Net::Riak::Link]',
+    auto_deref => 1,
+    default    => sub { [] },
+    handles    => {
+        get_links => 'elements',
+        add_links => 'push',
+    },
+    clearer => '_clear_links',
+);
+has exists => (
+    is => 'rw',
+    isa => 'Bool',
+    default => 0,
+);
+has vclock => (
+    is => 'rw',
+    isa => 'Str',
+    predicate => 'has_vclock',
+);
+has siblings => (
+    traits     => ['Array'],
+    is         => 'rw',
+    isa        => 'ArrayRef[Str]',
+    auto_deref => 1,
+    lazy       => 1,
+    default    => sub { [] },
+    handles    => {
+        get_siblings   => 'elements',
+        add_sibling    => 'push',
+        count_siblings => 'count',
+        get_sibling    => 'get',
+    },
+    clearer => '_clear_links',
+);
+
+has _headers => (
+    is  => 'rw',
+    isa => 'HTTP::Response',
+);
+has _jsonize => (
+    is      => 'rw',
+    isa     => 'Bool',
+    lazy    => 1,
+    default => 1,
+);
+
+sub store {
+    my ($self, $w, $dw) = @_;
+
+    $w  ||= $self->w;
+    $dw ||= $self->dw;
+
+    my $params = {returnbody => 'true', w => $w, dw => $dw};
+
+    my $request =
+      $self->client->request('PUT',
+        [$self->client->prefix, $self->bucket->name, $self->key], $params);
+
+    $request->header('X-Riak-ClientID' => $self->client->client_id);
+    $request->header('Content-Type'    => $self->content_type);
+
+    if ($self->has_vclock) {
+        $request->header('X-Riack-Vclock' => $self->vclock);
+    }
+
+    if ($self->_jsonize) {
+        $request->content(JSON::encode_json($self->data));
+    }
+    else {
+        $request->content($self->data);
+    }
+
+    my $response = $self->client->useragent->request($request);
+    $self->populate($response, [200, 300]);
+}
+
+sub load {
+    my $self = shift;
+
+    my $params = {r => $self->r};
+
+    my $request =
+      $self->client->request('GET',
+        [$self->client->prefix, $self->bucket->name, $self->key], $params);
+
+    my $response = $self->client->useragent->request($request);
+    $self->populate($response, [200, 300, 404]);
+}
+
+sub delete {
+    my ($self, $dw) = @_;
+
+    $dw ||= $self->bucket->dw;
+    my $params = {dw => $dw};
+
+    my $request =
+      $self->client->request('DELETE',
+        [$self->client->prefix, $self->bucket->name, $self->key], $params);
+
+    my $response = $self->client->useragent->request($request);
+    $self->populate($response, [204, 404]);
+}
+
+sub clear {
+    my $self = shift;
+    $self->_clear_data;
+    $self->_clear_links;
+    $self->exists(0);
+}
+
+sub populate {
+    my ($self, $http_response, $expected) = @_;
+
+    $self->clear;
+
+    return if (!$http_response);
+
+    my $status = $http_response->code;
+    $self->_headers($http_response);
+    $self->status($status);
+
+    $self->data($http_response->content);
+
+    if (!grep { $status == $_ } @$expected) {
+        croak "Expected status "
+          . (join(', ', @$expected))
+          . ", received $status";
+    }
+
+    if ($status == 404) {
+        $self->clear;
+        return;
+    }
+
+    $self->exists(1);
+
+    if ($http_response->header('link')) {
+        $self->populate_links($http_response->header('link'));
+    }
+
+    if ($status == 300) {
+        my @siblings = split("\n", $self->data);
+        shift @siblings;
+        $self->siblings(\@siblings);
+    }
+
+    if ($status == 200 && $self->_jsonize) {
+        $self->data(JSON::decode_json($self->data));
+    }
+}
+
+sub populate_links {
+    my ($self, $links) = @_;
+
+    for my $link (split(',', $links)) {
+        if ($link
+            =~ /\<\/([^\/]+)\/([^\/]+)\/([^\/]+)\>; ?riaktag=\"([^\']+)\"/)
+        {
+            my $l = Net::Riak::Link->new($2, $3, $4);
+            $self->add_link($link);
+        }
+    }
+}
+
+sub sibling {
+    my ($self, $id, $r) = @_;
+    $r ||= $self->bucket->r;
+
+    my $vtag = $self->get_sibling($id);
+    my $params = {r => $r, vtag => $vtag};
+
+    my $request =
+      $self->client->request('GET',
+        [$self->client->prefix, $self->bucket->name, $self->key], $params);
+    my $response = $self->client->useragent->request($request);
+
+    my $obj = Net::Riak::Object->new(
+        client => $self->client,
+        bucket => $self->bucket,
+        key    => $self->key
+    );
+    $obj->_jsonize($self->_jsonize);
+    $obj->populate($response, [200]);
+    return $obj;
+}
+
+sub add_link {
+    my ($self, $obj, $tag) = @_;
+    my $new_link;
+    if (blessed $obj && $obj->isa('RiakLink')) {
+        $new_link = $obj;
+    }
+    else {
+        $new_link = Net::Riak::Link->new(
+            bucket => $self->bucket,
+            key    => $self->key,
+            tag    => $tag || ''
+        );
+    }
+    $self->remove_link($new_link);
+    $self->add_links($new_link);
+}
+
+sub remove_link {
+    my ($self, $obj, $tag) = @_;
+    my $new_link;
+    if (blessed $obj && $obj->isa('RiakLink')) {
+        $new_link = $obj;
+    }
+    else {
+        $new_link = Net::Riak::Link->new(
+            bucket => $self->bucket,
+            key    => $self->key,
+            tag    => $tag || ''
+        );
+    }
+
+    # XXX purge links!
+}
+
+sub add {
+    my ($self, @args) = @_;
+    my $map_reduce = Net::Riak::MapReduce->new(client => $self->client);
+    $map_reduce->add($self->bucket->name, $self->key);
+    $map_reduce->add(@args);
+    $map_reduce;
+}
+
+sub link {
+    my ($self, @args) = @_;
+    my $map_reduce = Net::Riak::MapReduce->new(client => $self->client);
+    $map_reduce->add($self->bucket->name, $self->key);
+    $map_reduce->link(@args);
+    $map_reduce;
+}
+
+sub map {
+    my ($self, @args) = @_;
+    my $map_reduce = Net::Riak::MapReduce->new(client => $self->client);
+    $map_reduce->add($self->bucket->name, $self->key);
+    $map_reduce->map(@args);
+    $map_reduce;
+}
+
+sub reduce {
+    my ($self, @args) = @_;
+    my $map_reduce = Net::Riak::MapReduce->new(client => $self->client);
+    $map_reduce->add($self->bucket->name, $self->key);
+    $map_reduce->reduce(@args);
+    $map_reduce;
+}
+
+1;
+
+=head1 SYNOPSIS
+
+The L<Net::Riak::Object> holds meta information about a Riak object, plus the object's data.
+
+=head1 DESCRIPTION
+
+
+
+=head2 ATTRIBUTES
+
+=head2 METHODS
diff --git a/lib/Net/Riak/Role/REST.pm b/lib/Net/Riak/Role/REST.pm
new file mode 100644
index 0000000..5daad84
--- /dev/null
+++ b/lib/Net/Riak/Role/REST.pm
@@ -0,0 +1,29 @@
+package Net::Riak::Role::REST;
+
+# ABSTRACT: role for REST operations
+
+use URI;
+use HTTP::Request;
+use Moose::Role;
+
+sub _build_path {
+    my ($self, $path) = @_;
+    $path = join('/', @$path);
+}
+
+sub _build_uri {
+    my ($self, $path, $params) = @_;
+
+    my $uri = URI->new($self->host);
+    $uri->path($self->_build_path($path));
+    $uri->query_form(%$params);
+    $uri;
+}
+
+sub request {
+    my ($self, $method, $path, $params) = @_;
+    my $uri = $self->_build_uri($path, $params);
+    my $request = HTTP::Request->new($method => $uri);
+}
+
+1;
diff --git a/lib/Net/Riak/Role/UserAgent.pm b/lib/Net/Riak/Role/UserAgent.pm
new file mode 100644
index 0000000..4c6cb1f
--- /dev/null
+++ b/lib/Net/Riak/Role/UserAgent.pm
@@ -0,0 +1,20 @@
+package Net::Riak::Role::UserAgent;
+
+# ABSTRACT: useragent for Net::Riak
+
+use Moose::Role;
+use LWP::UserAgent;
+
+has useragent => (
+    is => 'rw',
+    isa => 'LWP::UserAgent',
+    lazy => 1,
+    default => sub {
+        my $self = shift;
+        my $ua = LWP::UserAgent->new;
+        $ua->timeout(3);
+        $ua;
+    }
+);
+
+1;
diff --git a/t/01_basic.t b/t/01_basic.t
new file mode 100644
index 0000000..f55a263
--- /dev/null
+++ b/t/01_basic.t
@@ -0,0 +1,226 @@
+use strict;
+use warnings;
+use Test::More;
+use Net::Riak;
+use YAML::Syck;
+
+my $host = 'http://localhost:8098';
+my $bucket_name = 'test4';
+my $bucket_multi = 'multiBucket1';
+
+# is alive
+{
+    ok my $client = Net::Riak->new(), 'client created';
+    ok $client->is_alive, 'riak is alive';
+}
+
+# store and get
+{
+    ok my $client = Net::Riak->new(), 'client created';
+    ok my $bucket = $client->bucket($bucket_name), 'got bucket test';
+    my $content = [int(rand(100))];
+    ok my $obj = $bucket->new_object('foo', $content),
+      'created a new riak object';
+    ok $obj->store,       'store object foo';
+    is $obj->status,      200, 'valid status';
+    is $obj->key,         'foo', 'valid key';
+    is_deeply $obj->data, $content, 'valid content';
+}
+
+# missing object
+{
+    my $client = Net::Riak->new();
+    my $bucket = $client->bucket($bucket_name);
+    my $obj    = $bucket->get("missing");
+    ok !$obj->data, 'no data';
+}
+
+# delete object
+{
+    my $client  = Net::Riak->new();
+    my $bucket  = $client->bucket($bucket_name);
+    my $content = [int(rand(100))];
+    my $obj     = $bucket->new_object('foo', $content);
+    ok $obj->store, 'object is stored';
+    $obj = $bucket->get('foo');
+    ok $obj->exists, 'object exists';
+    $obj->delete;
+    $obj->load;
+    ok !$obj->exists;
+}
+
+# test set bucket properties
+{
+    my $client = Net::Riak->new();
+    my $bucket = $client->bucket($bucket_name);
+    $bucket->allow_multiples('True');
+    my $res = $bucket->allow_multiples;
+    $bucket->n_val(3);
+    is $bucket->n_val, 3, 'n_val is set to 3';
+    $bucket->set_properties({allow_mult => "False", "n_val" => 2});
+    ok !$bucket->allow_multiples;
+    is $bucket->n_val, 2, 'n_val is set to 2';
+}
+
+# test siblings
+{
+    my $client = Net::Riak->new();
+    my $bucket = $client->bucket($bucket_multi);
+    $bucket->allow_multiples(1);
+    ok $bucket->allow_multiples, 'multiples set to 1';
+    my $obj = $bucket->get('foo');
+    $obj->delete;
+    for(1..5) {
+        my $client = Net::Riak->new();
+        my $bucket = $client->bucket($bucket_multi);
+        my $rand = int(rand(100));
+        $obj = $bucket->new_object('foo', [$rand]);
+        $obj->store;
+    }
+    # my $siblings_count = $obj->get_siblings;
+    # is $siblings_count, 5, 'got 5 siblings';
+    # my $obj3 = $obj->sibling(3);
+    # XXX FIXME
+    # $obj3 = $obj3->sibling(3);
+    # $obj3->store;
+    # $obj->reload;
+    # is_deeply $obj3->data, $obj->data;
+    # $obj->delete;
+}
+
+# test js source map
+{
+    my $client = Net::Riak->new();
+    my $bucket = $client->bucket($bucket_name);
+    my $obj = $bucket->new_object('foo', [2]);
+    $obj->store;
+    my $map_reduce = $client->add($bucket_name, 'foo');
+    $map_reduce->map("function (v) {return [JSON.parse(v.values[0].data)];}");
+    my $result = $map_reduce->run();
+    is_deeply $result, [[2]], 'got valid result';
+}
+
+# javascript named map
+{
+    my $client     = Net::Riak->new();
+    my $bucket     = $client->bucket($bucket_name);
+    my $obj        = $bucket->new_object('foo', [2]);
+    my $map_reduce = $client->add("bucket", "foo");
+    $map_reduce->map("Riak.mapValuesJson");
+    my $result = $map_reduce->run;
+    use YAML::Syck;
+    warn Dump $result;
+}
+
+# javascript source map reduce
+{
+    my $client = Net::Riak->new();
+    my $bucket = $client->bucket($bucket_name);
+    my $obj = $bucket->new_object('foo', [2]);
+    $obj->store;
+    $obj = $bucket->new_object('bar', [3]);
+    $obj->store;
+    $bucket->new_object('baz', [4]);
+    $obj->store;
+    my $map_reduce = $client->add($bucket_name, "foo");
+    $map_reduce->add($bucket_name, "bar");
+    $map_reduce->add($bucket_name, "baz");
+    $map_reduce->map("function (v) { return [1]; }");
+    $map_reduce->reduce("function (v) { return [v.length]; }");
+    my $result = $map_reduce->run;
+    is $result->[0], 3, "success map reduce";
+}
+
+# javascript named map reduce
+{
+    my $client = Net::Riak->new();
+    my $bucket = $client->bucket($bucket_name);
+    my $obj = $bucket->new_object("foo", [2]);
+    $obj->store;
+    $obj = $bucket->new_object("bar", [3]);
+    $obj->store;
+    $obj = $bucket->new_object("baz", [4]);
+    $obj->store;
+    my $map_reduce = $client->add($bucket_name, "foo");
+    $map_reduce->add($bucket_name, "bar");
+    $map_reduce->add($bucket_name, "baz");
+    $map_reduce->map("Riak.mapValuesJson");
+    $map_reduce->reduce("Riak.reduceSum");
+    my $result = $map_reduce->run();
+#    is $result->[0], 243; # ????
+}
+
+# javascript bucket map reduce
+{
+    my $client = Net::Riak->new();
+    my $bucket = $client->bucket($bucket_name);
+    my $obj = $bucket->new_object("foo", [2]);
+    $obj->store;
+    $obj = $bucket->new_object("bar", [3]);
+    $obj->store;
+    $obj = $bucket->new_object("baz", [4]);
+    $obj->store;
+    my $map_reduce = $client->add($bucket->name);
+    $map_reduce->map("Riak.mapValuesJson");
+    $map_reduce->reduce("Riak.reduceSum");
+    my $result = $map_reduce->run;
+    ok 1, "ici";
+#    is $result->[0], 243;
+}
+
+# javascript map reduce from object
+{
+    my $client = Net::Riak->new();
+    my $bucket = $client->bucket($bucket_name);
+    my $obj = $bucket->new_object("foo", [2]);
+    $obj->store;
+    $obj = $bucket->get("foo");
+    my $map_reduce = $obj->map("Riak.mapValuesJson");
+    my $result = $map_reduce->run();
+    is_deeply $result->[0], [2];
+}
+
+# store and get links
+{
+    my $client = Net::Riak->new();
+    my $bucket = $client->bucket($bucket_name);
+    my $obj = $bucket->new_object("foo", [2]);
+    my $obj1 = $bucket->new_object("foo1", {test => 1});
+    $obj1->store;
+    my $obj2 = $bucket->new_object("foo2", {test => 2});
+    $obj2->store;
+    my $obj3 = $bucket->new_object("foo3", {test => 3});
+    $obj3->store;
+    $obj->add_link($obj1);
+    $obj->add_link($obj2, "tag");
+    $obj->add_link($obj3, "tag2!@&");
+    $obj->store;
+    $obj = $bucket->get("foo");
+    my $mr = $obj->link("bucket");
+    my $results = $mr->run();
+    # XXX fixme !!
+    use YAML::Syck; warn Dump $results;
+}
+
+# link walking
+{
+    my $client = Net::Riak->new();
+    my $bucket = $client->bucket($bucket_name);
+    my $obj = $bucket->new_object("foo", [2]);
+    my $obj1 = $bucket->new_object("foo1", {test => 1});
+    $obj1->store;
+    my $obj2 = $bucket->new_object("foo2", {test => 2});
+    $obj2->store;
+    my $obj3 = $bucket->new_object("foo3", {test => 3});
+    $obj3->store;
+    $obj->add_link($obj1);
+    $obj->add_link($obj2, "tag");
+    $obj->add_link($obj3, "tag2!@&");
+    $obj->store;
+    $obj = $bucket->get("foo");
+    my $mr = $obj->link("bucket");
+    my $results = $mr->run();
+    use YAML::Syck; warn Dump $results;
+}
+
+done_testing;