ripple 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. data/.document +5 -0
  2. data/.gitignore +26 -0
  3. data/LICENSE +13 -0
  4. data/README.textile +126 -0
  5. data/RELEASE_NOTES.textile +24 -0
  6. data/Rakefile +61 -0
  7. data/VERSION +1 -0
  8. data/lib/riak.rb +45 -0
  9. data/lib/riak/bucket.rb +105 -0
  10. data/lib/riak/client.rb +138 -0
  11. data/lib/riak/client/curb_backend.rb +63 -0
  12. data/lib/riak/client/http_backend.rb +209 -0
  13. data/lib/riak/client/net_http_backend.rb +49 -0
  14. data/lib/riak/failed_request.rb +37 -0
  15. data/lib/riak/i18n.rb +15 -0
  16. data/lib/riak/invalid_response.rb +25 -0
  17. data/lib/riak/link.rb +54 -0
  18. data/lib/riak/locale/en.yml +37 -0
  19. data/lib/riak/map_reduce.rb +240 -0
  20. data/lib/riak/map_reduce_error.rb +20 -0
  21. data/lib/riak/robject.rb +234 -0
  22. data/lib/riak/util/headers.rb +44 -0
  23. data/lib/riak/util/multipart.rb +52 -0
  24. data/lib/riak/util/translation.rb +29 -0
  25. data/lib/riak/walk_spec.rb +113 -0
  26. data/lib/ripple.rb +48 -0
  27. data/lib/ripple/core_ext/casting.rb +96 -0
  28. data/lib/ripple/document.rb +60 -0
  29. data/lib/ripple/document/attribute_methods.rb +111 -0
  30. data/lib/ripple/document/attribute_methods/dirty.rb +52 -0
  31. data/lib/ripple/document/attribute_methods/query.rb +49 -0
  32. data/lib/ripple/document/attribute_methods/read.rb +38 -0
  33. data/lib/ripple/document/attribute_methods/write.rb +36 -0
  34. data/lib/ripple/document/bucket_access.rb +38 -0
  35. data/lib/ripple/document/finders.rb +84 -0
  36. data/lib/ripple/document/persistence.rb +93 -0
  37. data/lib/ripple/document/persistence/callbacks.rb +48 -0
  38. data/lib/ripple/document/properties.rb +85 -0
  39. data/lib/ripple/document/validations.rb +44 -0
  40. data/lib/ripple/embedded_document.rb +38 -0
  41. data/lib/ripple/embedded_document/persistence.rb +46 -0
  42. data/lib/ripple/i18n.rb +15 -0
  43. data/lib/ripple/locale/en.yml +16 -0
  44. data/lib/ripple/property_type_mismatch.rb +23 -0
  45. data/lib/ripple/translation.rb +24 -0
  46. data/ripple.gemspec +159 -0
  47. data/spec/fixtures/cat.jpg +0 -0
  48. data/spec/fixtures/multipart-blank.txt +7 -0
  49. data/spec/fixtures/multipart-with-body.txt +16 -0
  50. data/spec/riak/bucket_spec.rb +141 -0
  51. data/spec/riak/client_spec.rb +169 -0
  52. data/spec/riak/curb_backend_spec.rb +50 -0
  53. data/spec/riak/headers_spec.rb +34 -0
  54. data/spec/riak/http_backend_spec.rb +136 -0
  55. data/spec/riak/link_spec.rb +50 -0
  56. data/spec/riak/map_reduce_spec.rb +347 -0
  57. data/spec/riak/multipart_spec.rb +36 -0
  58. data/spec/riak/net_http_backend_spec.rb +28 -0
  59. data/spec/riak/object_spec.rb +444 -0
  60. data/spec/riak/walk_spec_spec.rb +208 -0
  61. data/spec/ripple/attribute_methods_spec.rb +149 -0
  62. data/spec/ripple/bucket_access_spec.rb +48 -0
  63. data/spec/ripple/callbacks_spec.rb +86 -0
  64. data/spec/ripple/document_spec.rb +35 -0
  65. data/spec/ripple/embedded_document_spec.rb +52 -0
  66. data/spec/ripple/finders_spec.rb +146 -0
  67. data/spec/ripple/persistence_spec.rb +89 -0
  68. data/spec/ripple/properties_spec.rb +195 -0
  69. data/spec/ripple/ripple_spec.rb +43 -0
  70. data/spec/ripple/validations_spec.rb +64 -0
  71. data/spec/spec.opts +1 -0
  72. data/spec/spec_helper.rb +32 -0
  73. data/spec/support/http_backend_implementation_examples.rb +215 -0
  74. data/spec/support/mock_server.rb +58 -0
  75. metadata +221 -0
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,26 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ _notes
23
+ doc
24
+ .yardoc
25
+ create
26
+ update-rails
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,126 @@
1
+ h1. ripple
2
+
3
+ ripple is a rich Ruby client for Riak, Basho's distributed database. It includes two namespaces:
4
+
5
+ * @Riak@ contains a basic wrapper around typical operations, including bucket manipulation, object CRUD, link-walking, and map-reduce.
6
+ * @Ripple@ contains an ActiveModel-compatible modeling layer that is inspired by ActiveRecord, DataMapper, and MongoMapper.
7
+
8
+ h2. Dependencies
9
+
10
+ ripple requires Ruby 1.8.7 or later and versions 3 or above of ActiveModel and ActiveSupport (and their dependencies, including i18n). Please see the Rails 3 beta release notes for installing those gems. I highly recommend the "curb":http://curb.rubyforge.org/ gem for better HTTP client performance.
11
+
12
+ In development, you will also need these gems:
13
+
14
+ * jeweler
15
+ * rspec >= 1.3
16
+ * fakeweb >= 1.2
17
+ * curb >= 0.6
18
+ * rack >= 1.0
19
+ * yard >= 0.5.2
20
+
21
+ h2. Basic Example
22
+
23
+ <notextile><pre>require 'riak'
24
+
25
+ # Create a client interface
26
+ client = Riak::Client.new
27
+
28
+ # Retrieve a bucket
29
+ bucket = client.bucket("doc") # a Riak::Bucket
30
+
31
+ # Get an object from the bucket
32
+ object = bucket.get("index.html") # a Riak::RObject
33
+
34
+ # Change the object's data and save
35
+ object.data = "<html><body>Hello, world!</body></html>"
36
+ object.store
37
+
38
+ # Reload an object you already have
39
+ object.reload # Works if you have the key and vclock, using conditional GET
40
+ object.reload :force => true # Reloads whether you have the vclock or not
41
+
42
+ # Access more like a hash, client[bucket][key]
43
+ client['doc']['index.html'] # the Riak::RObject
44
+
45
+ # Create a new object
46
+ new_one = Riak::RObject.new(bucket, "application.js")
47
+ new_one.content_type = "application/javascript" # You must set the content type.
48
+ new_one.data = "alert('Hello, World!')"
49
+ new_one.store</pre></notextile>
50
+
51
+ h2. Map-Reduce Example
52
+
53
+ <notextile><pre>
54
+
55
+ # Assuming you've already instantiated a client, get the album titles for The Beatles
56
+ results = Riak::MapReduce.new(client).
57
+ add("artists","Beatles").
58
+ link(:bucket => "albums").
59
+ map("function(v){ return [JSON.parse(v.values[0].data).title]; }", :keep => true).run
60
+
61
+ p results # => ["Please Please Me", "With The Beatles", "A Hard Day's Night",
62
+ # "Beatles For Sale", "Help!", "Rubber Soul",
63
+ # "Revolver", "Sgt. Pepper's Lonely Hearts Club Band", "Magical Mystery Tour",
64
+ # "The Beatles", "Yellow Submarine", "Abbey Road", "Let It Be"]</pre></notextile>
65
+
66
+ h2. Document model Example
67
+
68
+ <notextile><pre>
69
+ require 'ripple'
70
+
71
+ class Email
72
+ include Ripple::Document
73
+ property :from, String, :presence => true
74
+ property :to, String, :presence => true
75
+ property :sent, Time, :default => proc { Time.now }
76
+ property :body, String
77
+ end
78
+
79
+ email = Email.find("37458abc752f8413e") # GET /raw/emails/37458abc752f8413e
80
+ email.from = "someone@nowhere.net"
81
+ email.save # PUT /raw/emails/37458abc752f8413e
82
+
83
+ reply = Email.new
84
+ reply.from = "justin@bashoooo.com"
85
+ reply.to = "sean@geeemail.com"
86
+ reply.body = "Riak is a good fit for scalable Ruby apps."
87
+ reply.save # POST /raw/emails (Riak-assigned key)
88
+ </pre></notextile>
89
+
90
+ h2. How to Contribute
91
+
92
+ * Fork the project on "Github":http://github.com/seancribbs/ripple. If you have already forked, use @git pull --rebase@ to reapply your changes on top of the mainline. Example:
93
+ <notextile><pre>$ git checkout master
94
+ $ git pull --rebase seancribbs master</pre></notextile>
95
+ * Create a topic branch. If you've already created a topic branch, rebase it on top of changes from the mainline "master" branch. Examples:
96
+ ** New branch:
97
+ <pre>$ git checkout -b topic</pre>
98
+ ** Existing branch:
99
+ <pre>$ git rebase master</pre>
100
+ * Write an RSpec example, set of examples, and/or Cucumber story that demonstrate the necessity and validity of your changes. *Patches without specs will most often be ignored. Just do it, you'll thank me later.* Documentation patches need no specs, of course.
101
+ * Make your feature addition or bug fix. Make your specs and stories pass (green).
102
+ * Run the suite using multiruby or rvm to ensure cross-version compatibility.
103
+ * Cleanup any trailing whitespace in your code (try @whitespace-mode@ in Emacs, or "Remove Trailing Spaces in Document" in the "Text" bundle in Textmate).
104
+ * Commit, do not mess with Rakefile or VERSION. If related to an existing issue in the "tracker":http://github.com/seancribbs/ripple/issues, include "Closes #X" in the commit message (where X is the issue number).
105
+ * Send me a pull request.
106
+
107
+ h2. License & Copyright
108
+
109
+ Copyright &copy;2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
110
+
111
+ Licensed under the Apache License, Version 2.0 (the "License");
112
+ you may not use this file except in compliance with the License.
113
+ You may obtain a copy of the License at
114
+
115
+ "http://www.apache.org/licenses/LICENSE-2.0":http://www.apache.org/licenses/LICENSE-2.0
116
+
117
+ Unless required by applicable law or agreed to in writing, software
118
+ distributed under the License is distributed on an "AS IS" BASIS,
119
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
120
+ See the License for the specific language governing permissions and
121
+ limitations under the License.
122
+
123
+ h2. Auxillary License
124
+
125
+ The included photo (spec/fixtures/cat.jpg) is Copyright &copy;2009 "Sean Cribbs":http://seancribbs.com/, and is
126
+ licensed under the "Creative Commons Attribution Non-Commercial 3.0":http://creativecommons.org/licenses/by-nc/3.0 license. !http://i.creativecommons.org/l/by-nc/3.0/88x31.png!
@@ -0,0 +1,24 @@
1
+ h1. Ripple Release Notes
2
+
3
+ h2. 0.5 Initial Release - 2010-02-10
4
+
5
+ This is the first release of Ripple, which would not have been possible
6
+ without the generous support of Sonian and Basho Technologies. Many thanks.
7
+ It includes:
8
+
9
+ * A robust basic client, @Riak@, with:
10
+ ** multiple HTTP backends (curb, net/http)
11
+ ** sensible client defaults (local, default port)
12
+ ** bucket access and manipulation, including key-streaming
13
+ ** object reading, storing, deleting and reloading
14
+ ** automatic de-serialization of JSON, YAML, and Marshal (when given the right content type)
15
+ ** streaming POST/PUT bodies (when given an IO)
16
+ ** method-chained map-reduce job construction
17
+ * A document-style modeling library, Ripple, with:
18
+ ** ActiveModel 3.0 compatibility
19
+ ** Property/attribute definition with automatic type-casting
20
+ ** Bucket selection based on class name, with single-bucket inheritance (configurable)
21
+ ** Validations
22
+ ** Dirty-tracking
23
+ ** Simple finders - all documents, by key
24
+ ** Reloading
@@ -0,0 +1,61 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = "ripple"
9
+ gem.summary = %Q{ripple is a rich Ruby client for Riak, Basho's distributed database.}
10
+ gem.description = %Q{ripple is a rich Ruby client for Riak, Basho's distributed database. It includes all the basics of accessing and manipulating Riak buckets and objects, and an object mapper library for building a rich domain on top of Riak.}
11
+ gem.email = "seancribbs@gmail.com"
12
+ gem.homepage = "http://seancribbs.github.com/ripple"
13
+ gem.authors = ["Sean Cribbs"]
14
+ gem.add_development_dependency "rspec", ">= 1.3"
15
+ gem.add_development_dependency "fakeweb", ">=1.2"
16
+ gem.add_development_dependency "rack", ">=1.0"
17
+ gem.add_development_dependency "yard", ">=0.5.2"
18
+ gem.add_development_dependency "curb", ">=0.6"
19
+ gem.add_dependency "activesupport", "3.0.0.beta"
20
+ gem.add_dependency "activemodel", "3.0.0.beta"
21
+ gem.requirements << "`gem install curb` for better HTTP performance"
22
+ end
23
+ Jeweler::GemcutterTasks.new
24
+ rescue LoadError
25
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
26
+ end
27
+
28
+ require 'spec/rake/spectask'
29
+ Spec::Rake::SpecTask.new(:spec) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.spec_files = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
35
+ spec.libs << 'lib' << 'spec'
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ spec.rcov_opts = ['--exclude', 'lib\/spec,bin\/spec,config\/boot.rb,gems,spec_helper']
39
+ end
40
+
41
+ task :spec => :check_dependencies
42
+
43
+ task :default => :spec
44
+
45
+ require 'yard'
46
+ YARD::Rake::YardocTask.new do |yard|
47
+ docfiles = FileList['lib/**/*.rb', 'README*', 'VERSION', 'LICENSE', 'RELEASE_NOTES.textile']
48
+ yard.files = docfiles
49
+ yard.options = ["--no-private"]
50
+ end
51
+
52
+ task :doc => :yard do
53
+ original_dir = Dir.pwd
54
+ docs_dir = File.expand_path(File.join(original_dir, "..", "ripple-docs"))
55
+ rm_rf File.join(docs_dir, "*")
56
+ cp_r File.join(original_dir, "doc", "."), docs_dir
57
+ touch File.join(docs_dir, '.nojekyll')
58
+ end
59
+
60
+ CLOBBER.include(".yardoc")
61
+ CLOBBER.include("doc")
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.0
@@ -0,0 +1,45 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ $KCODE = "UTF8" if RUBY_VERSION < "1.9"
15
+
16
+ require 'active_support/all'
17
+ require 'active_support/json'
18
+ require 'base64'
19
+ require 'uri'
20
+ require 'net/http'
21
+ require 'yaml'
22
+ require 'riak/i18n'
23
+
24
+ # The Riak module contains all aspects of the HTTP client interface
25
+ # to Riak.
26
+ module Riak
27
+ # Domain objects
28
+ autoload :Bucket, "riak/bucket"
29
+ autoload :Client, "riak/client"
30
+ autoload :Link, "riak/link"
31
+ autoload :WalkSpec, "riak/walk_spec"
32
+ autoload :RObject, "riak/robject"
33
+ autoload :MapReduce, "riak/map_reduce"
34
+
35
+ # Exceptions
36
+ autoload :FailedRequest, "riak/failed_request"
37
+ autoload :InvalidResponse, "riak/invalid_response"
38
+ autoload :MapReduceError, "riak/map_reduce_error"
39
+
40
+ module Util
41
+ autoload :Headers, "riak/util/headers"
42
+ autoload :Multipart, "riak/util/multipart"
43
+ autoload :Translation, "riak/util/translation"
44
+ end
45
+ end
@@ -0,0 +1,105 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require 'riak'
15
+
16
+ module Riak
17
+ # Represents and encapsulates operations on a Riak bucket. You may retrieve a bucket
18
+ # using {Client#bucket}, or create it manually and retrieve its meta-information later.
19
+ class Bucket
20
+ include Util::Translation
21
+ # @return [Riak::Client] the associated client
22
+ attr_reader :client
23
+
24
+ # @return [String] the bucket name
25
+ attr_reader :name
26
+
27
+ # @return [Hash] Internal Riak bucket properties.
28
+ attr_reader :props
29
+ alias_attribute :properties, :props
30
+
31
+ # Create a Riak bucket manually.
32
+ # @param [Client] client the {Riak::Client} for this bucket
33
+ # @param [String] name the name of the bucket
34
+ def initialize(client, name)
35
+ raise ArgumentError, t("client_type", :client => client.inspect) unless Client === client
36
+ raise ArgumentError, t("string_type", :string => name.inspect) unless String === name
37
+ @client, @name = client, name
38
+ end
39
+
40
+ # Load information for the bucket from a response given by the {Riak::Client::HTTPBackend}.
41
+ # Used mostly internally - use {Riak::Client#bucket} to get a {Bucket} instance.
42
+ # @param [Hash] response a response from {Riak::Client::HTTPBackend}
43
+ # @return [Bucket] self
44
+ # @see Client#bucket
45
+ def load(response={})
46
+ unless response.try(:[], :headers).try(:[],'content-type').try(:first) =~ /json$/
47
+ raise Riak::InvalidResponse.new({"content-type" => ["application/json"]}, response[:headers], t("loading_bucket", :name => name))
48
+ end
49
+ payload = JSON.parse(response[:body])
50
+ @keys = payload['keys'].map {|k| URI.unescape(k) } if payload['keys']
51
+ @props = payload['props'] if payload['props']
52
+ self
53
+ end
54
+
55
+ # Accesses or retrieves a list of keys in this bucket.
56
+ # If a block is given, keys will be streamed through
57
+ # the block (useful for large buckets). When streaming,
58
+ # results of the operation will not be retained in the local Bucket object.
59
+ # @param [Hash] options extra options
60
+ # @yield [Array<String>] a list of keys from the current chunk
61
+ # @option options [Boolean] :reload (false) If present, will force reloading of the bucket's keys from Riak
62
+ # @return [Array<String>] Keys in this bucket
63
+ def keys(options={})
64
+ if block_given?
65
+ @client.http.get(200, @client.prefix, name, {:props => false}, {}) do |chunk|
66
+ obj = JSON.parse(chunk) rescue {}
67
+ yield obj['keys'].map {|k| URI.unescape(k) } if obj['keys']
68
+ end
69
+ elsif @keys.nil? || options[:reload]
70
+ response = @client.http.get(200, @client.prefix, name, {:props => false}, {})
71
+ load(response)
72
+ end
73
+ @keys
74
+ end
75
+
76
+ # Sets internal properties on the bucket
77
+ # Note: this results in a request to the Riak server!
78
+ # @param [Hash] properties new properties for the bucket
79
+ # @return [Hash] the properties that were accepted
80
+ # @raise [FailedRequest] if the new properties were not accepted by the Riak server
81
+ def props=(properties)
82
+ raise ArgumentError, t("hash_type", :hash => properties.inspect) unless Hash === properties
83
+ body = {'props' => properties}.to_json
84
+ @client.http.put(204, @client.prefix, name, body, {"Content-Type" => "application/json"})
85
+ @props = properties
86
+ end
87
+
88
+ # Retrieve an object from within the bucket.
89
+ # @param [String] key the key of the object to retrieve
90
+ # @param [Hash] options query parameters for the request
91
+ # @option options [Fixnum] :r - the read quorum for the request - how many nodes should concur on the read
92
+ # @return [Riak::RObject] the object
93
+ # @raise [FailedRequest] if the object is not found or some other error occurs
94
+ def get(key, options={})
95
+ response = @client.http.get(200, @client.prefix, name, key, options, {})
96
+ RObject.new(self, key).load(response)
97
+ end
98
+ alias :[] :get
99
+
100
+ # @return [String] a representation suitable for IRB and debugging output
101
+ def inspect
102
+ "#<Riak::Bucket #{client.http.path(client.prefix, name).to_s}#{" keys=[#{keys.join(',')}]" if defined?(@keys)}>"
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,138 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require 'riak'
15
+
16
+ module Riak
17
+ # A client connection to Riak.
18
+ class Client
19
+ include Util::Translation
20
+
21
+ autoload :HTTPBackend, "riak/client/http_backend"
22
+ autoload :NetHTTPBackend, "riak/client/net_http_backend"
23
+ autoload :CurbBackend, "riak/client/curb_backend"
24
+
25
+ # When using integer client IDs, the exclusive upper-bound of valid values.
26
+ MAX_CLIENT_ID = 4294967296
27
+
28
+ # Regexp for validating hostnames, lifted from uri.rb in Ruby 1.8.6
29
+ HOST_REGEX = /^(?:(?:(?:[a-zA-Z\d](?:[-a-zA-Z\d]*[a-zA-Z\d])?)\.)*(?:[a-zA-Z](?:[-a-zA-Z\d]*[a-zA-Z\d])?)\.?|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[(?:(?:[a-fA-F\d]{1,4}:)*(?:[a-fA-F\d]{1,4}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(?:(?:[a-fA-F\d]{1,4}:)*[a-fA-F\d]{1,4})?::(?:(?:[a-fA-F\d]{1,4}:)*(?:[a-fA-F\d]{1,4}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))?)\])$/n
30
+
31
+ # @return [String] The host or IP address for the Riak endpoint
32
+ attr_reader :host
33
+
34
+ # @return [Fixnum] The port of the Riak HTTP endpoint
35
+ attr_reader :port
36
+
37
+ # @return [String] The internal client ID used by Riak to route responses
38
+ attr_reader :client_id
39
+
40
+ # @return [String] The URL path prefix to the "raw" HTTP endpoint
41
+ attr_accessor :prefix
42
+
43
+ # @return [String] The URL path to the map-reduce HTTP endpoint
44
+ attr_accessor :mapred
45
+
46
+ # Creates a client connection to Riak
47
+ # @param [Hash] options configuration options for the client
48
+ # @option options [String] :host ('127.0.0.1') The host or IP address for the Riak endpoint
49
+ # @option options [Fixnum] :port (8098) The port of the Riak HTTP endpoint
50
+ # @option options [String] :prefix ('/raw/') The URL path prefix to the "raw" HTTP endpoint
51
+ # @option options [String] :mapred ('/mapred') The path to the map-reduce HTTP endpoint
52
+ # @option options [Fixnum, String] :client_id (rand(MAX_CLIENT_ID)) The internal client ID used by Riak to route responses
53
+ # @raise [ArgumentError] raised if any options are invalid
54
+ def initialize(options={})
55
+ options.assert_valid_keys(:host, :port, :prefix, :client_id, :mapred)
56
+ self.host = options[:host] || "127.0.0.1"
57
+ self.port = options[:port] || 8098
58
+ self.client_id = options[:client_id] || make_client_id
59
+ self.prefix = options[:prefix] || "/raw/"
60
+ self.mapred = options[:mapred] || "/mapred"
61
+ raise ArgumentError, t("missing_host_and_port") unless @host && @port
62
+ end
63
+
64
+ # Set the client ID for this client. Must be a string or Fixnum value 0 =< value < MAX_CLIENT_ID.
65
+ # @param [String, Fixnum] value The internal client ID used by Riak to route responses
66
+ # @raise [ArgumentError] when an invalid client ID is given
67
+ # @return [String] the assigned client ID
68
+ def client_id=(value)
69
+ @client_id = case value
70
+ when 0...MAX_CLIENT_ID
71
+ b64encode(value)
72
+ when String
73
+ value
74
+ else
75
+ raise ArgumentError, t("invalid_client_id", :max_id => MAX_CLIENT_ID)
76
+ end
77
+ end
78
+
79
+ # Set the hostname of the Riak endpoint. Must be an IPv4, IPv6, or valid hostname
80
+ # @param [String] value The host or IP address for the Riak endpoint
81
+ # @raise [ArgumentError] if an invalid hostname is given
82
+ # @return [String] the assigned hostname
83
+ def host=(value)
84
+ raise ArgumentError, t("hostname_invalid") unless String === value && value.present? && value =~ HOST_REGEX
85
+ @host = value
86
+ end
87
+
88
+ # Set the port number of the Riak endpoint. This must be an integer between 0 and 65535.
89
+ # @param [Fixnum] value The port number of the Riak endpoint
90
+ # @raise [ArgumentError] if an invalid port number is given
91
+ # @return [Fixnum] the assigned port number
92
+ def port=(value)
93
+ raise ArgumentError, t("port_invalid") unless (0..65535).include?(value)
94
+ @port = value
95
+ end
96
+
97
+ # Automatically detects and returns an appropriate HTTP backend.
98
+ # The HTTP backend is used internally by the Riak client, but can also
99
+ # be used to access the server directly.
100
+ # @return [HTTPBackend] the HTTP backend for this client
101
+ def http
102
+ @http ||= begin
103
+ require 'curb'
104
+ CurbBackend.new(self)
105
+ rescue LoadError, NameError
106
+ warn t("install_curb")
107
+ NetHTTPBackend.new(self)
108
+ end
109
+ end
110
+
111
+ # Retrieves a bucket from Riak.
112
+ # @param [String] bucket the bucket to retrieve
113
+ # @param [Hash] options options for retrieving the bucket
114
+ # @option options [Boolean] :keys (true) whether to retrieve the bucket keys
115
+ # @option options [Boolean] :props (true) whether to retreive the bucket properties
116
+ # @return [Bucket] the requested bucket
117
+ def bucket(name, options={})
118
+ options.assert_valid_keys(:keys, :props)
119
+ response = http.get(200, prefix, name, options, {})
120
+ Bucket.new(self, name).load(response)
121
+ end
122
+ alias :[] :bucket
123
+
124
+ # @return [String] A representation suitable for IRB and debugging output.
125
+ def inspect
126
+ "#<Riak::Client #{http.root_uri.to_s}>"
127
+ end
128
+
129
+ private
130
+ def make_client_id
131
+ b64encode(rand(MAX_CLIENT_ID))
132
+ end
133
+
134
+ def b64encode(n)
135
+ Base64.encode64([n].pack("N")).chomp
136
+ end
137
+ end
138
+ end