sr-couchy 0.0.2

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 (41) hide show
  1. data/README.textile +77 -0
  2. data/Rakefile +46 -0
  3. data/bin/couchy +80 -0
  4. data/couchy.gemspec +58 -0
  5. data/lib/couchy.rb +21 -0
  6. data/lib/couchy/database.rb +129 -0
  7. data/lib/couchy/server.rb +89 -0
  8. data/spec/couchy_spec.rb +71 -0
  9. data/spec/database_spec.rb +417 -0
  10. data/spec/fixtures/attachments/test.html +11 -0
  11. data/spec/fixtures/views/lib.js +3 -0
  12. data/spec/fixtures/views/test_view/lib.js +3 -0
  13. data/spec/fixtures/views/test_view/only-map.js +4 -0
  14. data/spec/fixtures/views/test_view/test-map.js +3 -0
  15. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  16. data/spec/spec.opts +6 -0
  17. data/spec/spec_helper.rb +5 -0
  18. data/test/couchy_test.rb +13 -0
  19. data/test/database_test.rb +193 -0
  20. data/test/server_test.rb +211 -0
  21. data/test/test_helper.rb +10 -0
  22. data/vendor/addressable/.gitignore +7 -0
  23. data/vendor/addressable/CHANGELOG +51 -0
  24. data/vendor/addressable/LICENSE +20 -0
  25. data/vendor/addressable/README +24 -0
  26. data/vendor/addressable/Rakefile +51 -0
  27. data/vendor/addressable/lib/addressable/idna.rb +4867 -0
  28. data/vendor/addressable/lib/addressable/uri.rb +2212 -0
  29. data/vendor/addressable/lib/addressable/version.rb +35 -0
  30. data/vendor/addressable/spec/addressable/idna_spec.rb +196 -0
  31. data/vendor/addressable/spec/addressable/uri_spec.rb +3827 -0
  32. data/vendor/addressable/spec/data/rfc3986.txt +3419 -0
  33. data/vendor/addressable/tasks/clobber.rake +2 -0
  34. data/vendor/addressable/tasks/gem.rake +62 -0
  35. data/vendor/addressable/tasks/git.rake +40 -0
  36. data/vendor/addressable/tasks/metrics.rake +22 -0
  37. data/vendor/addressable/tasks/rdoc.rake +29 -0
  38. data/vendor/addressable/tasks/rubyforge.rake +89 -0
  39. data/vendor/addressable/tasks/spec.rake +107 -0
  40. data/vendor/addressable/website/index.html +107 -0
  41. metadata +113 -0
@@ -0,0 +1,77 @@
1
+ h1. Couchy: There Are Many Like It but This One is Mine
2
+
3
+ Couchy is a simple, no-frills, Ruby wrapper around the nice
4
+ "CouchDB HTTP API":http://wiki.apache.org/couchdb/HttpRestApt.
5
+
6
+ h2. Quick overview
7
+
8
+ <pre>
9
+ server = CouchRest.new
10
+ database = server.database('articles')
11
+ response = database.save(:title => 'Atom-Powered Robots Run Amok', :content => 'Some text.')
12
+ document = database.get(response['id'])
13
+ database.delete(document)
14
+ puts document.inspect
15
+ </pre>
16
+
17
+
18
+ h2. Requirements
19
+
20
+ The fellowing gems are required:
21
+
22
+ * @rest-client@
23
+ * @addressable@
24
+
25
+ To run the test and generate the code coverage report, you also need :
26
+
27
+ * @test/spec@
28
+ * @mocha@
29
+ * @rcov@
30
+ * @rspec@ (for the legacy tests)
31
+
32
+ and @yard@ to generate the documentation.
33
+
34
+ h2. Genesis
35
+
36
+ It started as an Hardcore Forking Action of "jchris's CouchRest":original
37
+ I ended up:
38
+
39
+ * Writing more tests. The legacy tests are rather integration tests while those
40
+ I wrote are unit tests.
41
+ * DRY-ing it up
42
+ * Writing some doc using "YARD":yard
43
+
44
+ Unfortunately, jchris didn't merge the changes for some reasons.
45
+
46
+ Thank a lot to him for having written CouchRest in the first place and having
47
+ allowed me to relicense my fork under another license.
48
+
49
+ h2. License
50
+
51
+ (The MIT License)
52
+
53
+ Copyright (c) 2008 "Simon Rozet":sr <simon@rozet.name>
54
+
55
+ Permission is hereby granted, free of charge, to any person obtaining
56
+ a copy of this software and associated documentation files (the
57
+ 'Software'), to deal in the Software without restriction, including
58
+ without limitation the rights to use, copy, modify, merge, publish,
59
+ distribute, sublicense, and/or sell copies of the Software, and to
60
+ permit persons to whom the Software is furnished to do so, subject to
61
+ the following conditions:
62
+
63
+ The above copyright notice and this permission notice shall be
64
+ included in all copies or substantial portions of the Software.
65
+
66
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
67
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
68
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
69
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
70
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
71
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
72
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
73
+
74
+ [yard]http://yard.soen.ca/
75
+ [couchdb]http://couchdb.org
76
+ [original]http://github.com/jchris/couchrest
77
+ [sr]http://purl.org/net/sr/
@@ -0,0 +1,46 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+ require 'rcov/rcovtask'
4
+ require 'yard'
5
+
6
+ task :default => :test
7
+
8
+ task :test => :"test:all"
9
+
10
+ namespace :test do
11
+ desc 'Run all tests'
12
+ task :all => [:unit, :integration]
13
+
14
+ desc 'Run unit tests'
15
+ task :unit do
16
+ sh 'testrb test/*.rb'
17
+ end
18
+
19
+ desc "Run integration tests"
20
+ Spec::Rake::SpecTask.new('integration') do |t|
21
+ t.spec_files = FileList['spec/*_spec.rb']
22
+ end
23
+ end
24
+
25
+ Rcov::RcovTask.new do |t|
26
+ t.test_files = FileList['test/*_test.rb']
27
+ t.rcov_opts << '-Ilib'
28
+ t.rcov_opts << '-x"home"'
29
+ t.verbose = true
30
+ end
31
+
32
+ YARD::Rake::YardocTask.new
33
+
34
+ task :publish => [:"publish:doc", :"publish:coverage"]
35
+
36
+ namespace :publish do
37
+ task :doc => :yardoc do
38
+ sh 'cp -r doc ~/web/atonie.org/2008/couchy'
39
+ sh 'cd ~/web/atonie.org && git add 2008/couchy/doc && git commit -m "update couchy doc"'
40
+ end
41
+
42
+ task :coverage => :rcov do
43
+ sh 'cp -r coverage ~/web/atonie.org/2008/couchy'
44
+ sh 'cd ~/web/atonie.org && git add 2008/couchy/coverage && git commit -m "update couchy coverage"'
45
+ end
46
+ end
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'thor'
4
+ require 'fileutils'
5
+ require 'pathname'
6
+
7
+ require File.dirname(__FILE__) + '/../lib/couchy'
8
+
9
+ module Couchy
10
+ class ViewManager < Thor
11
+ desc 'generate DESIGN VIEW',
12
+ 'Generate directory structure for DESIGN/VIEW in PATH suitable for the `push` task.'
13
+ def generate(path, design, view='all')
14
+ path = File.join(Dir.pwd, path, design)
15
+ FileUtils.mkdir_p path
16
+ create_sample_view(path, view)
17
+ end
18
+
19
+ desc 'push PATH DATABASE', 'Push views avalaible in PATH to DATABASE.'
20
+ method_options :server => :optional,
21
+ :force => :boolean
22
+ def push(path, database_name)
23
+ @database_name = database_name
24
+ designs = find_designs(path)
25
+ documents = designs.map { |design| document_for(*design) }
26
+ documents.each do |document|
27
+ if document_exists?(document) && options[:force]
28
+ puts "Deleting `#{document['_id']}'"
29
+ database.delete database.get(document['_id'])
30
+ end
31
+ puts "Saving `#{document['_id']}' to `#{database.name}' on `#{server.uri}'"
32
+ database.save(document)
33
+ end
34
+ end
35
+
36
+ private
37
+ def find_designs(path)
38
+ views = Dir[File.join(path, '**/*-*.js')].inject({}) do |memo, path|
39
+ path = Pathname.new(path)
40
+ parts = path.split
41
+ design = parts.first.split.last.to_s
42
+ view = parts.last.to_s
43
+ view_type = view[/\-(\w+)\.js/] && $1
44
+ view_body = File.read(path)
45
+ memo.tap do |memo|
46
+ memo[design] = {} unless memo[design]
47
+ memo[design][view_type] = view_body
48
+ end
49
+ end
50
+ end
51
+
52
+ def document_for(name, views)
53
+ { '_id' => "_design/#{name}",
54
+ 'language' => 'javascript',
55
+ 'views' => views }
56
+ end
57
+
58
+ def create_sample_view(path, view)
59
+ File.open(File.join(path, "#{view}-map.js"), 'w') do |file|
60
+ file.puts 'function(doc) {'
61
+ file.puts ' emit(null, doc);'
62
+ file.puts '}'
63
+ end
64
+ end
65
+
66
+ def document_exists?(document)
67
+ database.documents['rows'].collect { |row| row['id'] }.include?(document['_id'])
68
+ end
69
+
70
+ def database
71
+ @database ||= server.database(@database_name)
72
+ end
73
+
74
+ def server
75
+ @server ||= options[:server] ? Couchy::Server.new(options[:server]) : Couchy.new
76
+ end
77
+ end
78
+ end
79
+
80
+ Couchy::ViewManager.start
@@ -0,0 +1,58 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'couchy'
3
+ s.version = '0.0.2'
4
+ s.date = '2008-10-07'
5
+ s.summary = 'Simple, no-frills, Ruby wrapper around the nice CouchDB HTTP API'
6
+ s.description = 'Simple, no-frills, Ruby wrapper around the nice CouchDB HTTP API'
7
+ s.homepage = 'http://github.com/sr/couchy'
8
+ s.email = 'simon@rozet.name'
9
+ s.authors = ['Simon Rozet']
10
+ s.has_rdoc = false
11
+ s.executables = ['couchy']
12
+
13
+ s.files = %w(
14
+ README.textile
15
+ Rakefile
16
+ bin/couchy
17
+ couchy.gemspec
18
+ lib/couchy.rb
19
+ lib/couchy/database.rb
20
+ lib/couchy/server.rb
21
+ spec/couchy_spec.rb
22
+ spec/database_spec.rb
23
+ spec/fixtures/attachments/test.html
24
+ spec/fixtures/views/lib.js
25
+ spec/fixtures/views/test_view/lib.js
26
+ spec/fixtures/views/test_view/only-map.js
27
+ spec/fixtures/views/test_view/test-map.js
28
+ spec/fixtures/views/test_view/test-reduce.js
29
+ spec/spec.opts
30
+ spec/spec_helper.rb
31
+ test/couchy_test.rb
32
+ test/database_test.rb
33
+ test/server_test.rb
34
+ test/test_helper.rb
35
+ vendor/addressable/.gitignore
36
+ vendor/addressable/CHANGELOG
37
+ vendor/addressable/LICENSE
38
+ vendor/addressable/README
39
+ vendor/addressable/Rakefile
40
+ vendor/addressable/lib/addressable/idna.rb
41
+ vendor/addressable/lib/addressable/uri.rb
42
+ vendor/addressable/lib/addressable/version.rb
43
+ vendor/addressable/spec/addressable/idna_spec.rb
44
+ vendor/addressable/spec/addressable/uri_spec.rb
45
+ vendor/addressable/spec/data/rfc3986.txt
46
+ vendor/addressable/tasks/clobber.rake
47
+ vendor/addressable/tasks/gem.rake
48
+ vendor/addressable/tasks/git.rake
49
+ vendor/addressable/tasks/metrics.rake
50
+ vendor/addressable/tasks/rdoc.rake
51
+ vendor/addressable/tasks/rubyforge.rake
52
+ vendor/addressable/tasks/spec.rake
53
+ vendor/addressable/website/index.html
54
+ )
55
+
56
+ s.test_files = s.files.select { |path| path =~ /^(test|spec)/ }
57
+ s.add_dependency('rest-client', ['> 0.0.0'])
58
+ end
@@ -0,0 +1,21 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+ require 'rest_client'
4
+
5
+ $:.unshift File.dirname(__FILE__) + '/../vendor/addressable/lib'
6
+ require 'addressable/uri'
7
+
8
+ $:.unshift File.dirname(__FILE__) + '/couchy'
9
+
10
+ module Couchy
11
+ autoload :Server, 'server'
12
+ autoload :Database, 'database'
13
+
14
+ # Shortcut for Couchy::Server.new
15
+ #
16
+ # @param [String] uri The URI of the CouchDB server. defaults to "http://localhost:5984/"
17
+ # @return Couchy::Server
18
+ def self.new(server_uri='http://localhost:5984/')
19
+ Server.new(server_uri)
20
+ end
21
+ end
@@ -0,0 +1,129 @@
1
+ require 'cgi'
2
+ require 'base64'
3
+
4
+ module Couchy
5
+ class Database
6
+ attr_accessor :server, :name
7
+
8
+ def initialize(server, database_name)
9
+ @name = database_name
10
+ @server = server
11
+ end
12
+
13
+ # Gets a list of all the documents available in the database
14
+ #
15
+ # @param [Hash] params
16
+ #
17
+ # @return [Hash] Parsed server response
18
+ def documents(params={})
19
+ server.get("#{name}/_all_docs", params)
20
+ end
21
+
22
+ # Creates a temporary view
23
+ #
24
+ # @param [String] function The view function
25
+ # @param [Hash] params
26
+ #
27
+ # @return [Hash] Parsed server response
28
+ def temp_view(function, params={})
29
+ server.post("#{name}/_temp_view", function,
30
+ params.merge!(:headers => {'Content-Type' => 'application/json'}))
31
+ end
32
+
33
+ # Query a view
34
+ #
35
+ # @param [String] view_name The name of the view
36
+ # @param [Hash] params
37
+ #
38
+ # @return [Hash] Parsed server response
39
+ def view(view_name, params={})
40
+ server.get("#{name}/_view/#{view_name}", params)
41
+ end
42
+
43
+ # Retrieves a document by its ID
44
+ #
45
+ # @param [String] id The ID of the document
46
+ #
47
+ # @return [Hash] Parsed server response
48
+ def get(id)
49
+ server.get("#{name}/#{CGI.escape(id)}")
50
+ end
51
+
52
+ # Retrieves an attachment from a document
53
+ #
54
+ # @param [String] document_id The ID of the document
55
+ # @param [String] attachment_name The name of the attachment
56
+ #
57
+ # @return [Hash] Parsed server response
58
+ def fetch_attachment(document_id, attachment_name)
59
+ server.get("#{name}/#{CGI.escape(document_id)}/#{CGI.escape(attachment_name)}", :no_json => true)
60
+ end
61
+
62
+ # Saves or updates a document
63
+ #
64
+ # @param [Hash] doc The document
65
+ #
66
+ # @return [Hash] Parsed server response
67
+ def save(doc)
68
+ doc = encode_attachments_of(doc)
69
+
70
+ if doc['_id']
71
+ server.put("#{name}/#{CGI.escape(doc['_id'])}", doc)
72
+ else
73
+ server.post("#{name}", doc)
74
+ end
75
+ end
76
+
77
+ # Saves or updates a bunch of documents
78
+ #
79
+ # @param [Array] docs The documents to save
80
+ #
81
+ # @return [Hash] Parsed server response
82
+ def bulk_save(docs)
83
+ server.post("#{name}/_bulk_docs", {:docs => docs})
84
+ end
85
+
86
+ # Deletes a document
87
+ #
88
+ # @param [String, Hash] document Document ID or an Hash representing the document
89
+ # @param [String] revision Document's revision
90
+ #
91
+ # @raise ArgumentError When the Hash representing the document neither has
92
+ # an ID nor a revision
93
+ #
94
+ # @raise ArgumentError When document is neither an ID nor an Hash
95
+ # representing a document
96
+ #
97
+ # @return [Hash] Parsed server response
98
+ def delete(document, revision=nil)
99
+ case document
100
+ when String
101
+ raise ArgumentError, 'Document revision must be specified' unless revision
102
+ server.delete "#{name}/#{CGI.escape(document)}", :rev => revision
103
+ when Hash
104
+ raise ArgumentError, 'Document ID and revision must be specified' unless
105
+ document['_id'] && document['_rev']
106
+ server.delete("#{name}/#{CGI.escape(document['_id'])}", :rev => document['_rev'])
107
+ else
108
+ raise ArgumentError, 'Document must be an Hash representing the document or its ID'
109
+ end
110
+ end
111
+
112
+ # Deletes the current database
113
+ #
114
+ # @return [Hash] Parsed server response
115
+ def delete!
116
+ server.delete(name)
117
+ end
118
+
119
+ private
120
+ def encode_attachments_of(doc)
121
+ return doc unless doc['_attachments']
122
+ doc['_attachments'].each { |_, v| v.update('data' => base64(v['data'])) } and doc
123
+ end
124
+
125
+ def base64(data)
126
+ Base64.encode64(data.to_s).gsub(/\s/,'')
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,89 @@
1
+ module Couchy
2
+ class Server
3
+ attr_accessor :uri
4
+
5
+ def initialize(uri)
6
+ @uri = Addressable::URI.parse(uri)
7
+ end
8
+
9
+ # Gets information about the server
10
+ #
11
+ # @return [Hash] Parsed server response
12
+ def info
13
+ get '/'
14
+ end
15
+
16
+ # Restarts the server
17
+ #
18
+ # @return [Hash] Parsed server response
19
+ def restart!
20
+ post '_restart'
21
+ end
22
+
23
+ # Gets a list of all the databases available on the server
24
+ #
25
+ # @return [Array] Parsed server response
26
+ def databases
27
+ get '_all_dbs'
28
+ end
29
+
30
+ # Gets a new [Couchy::Database] for the database
31
+ #
32
+ # @param [String] name The name of the database
33
+ #
34
+ # @return [Couchy::Database] The database
35
+ def database(name)
36
+ Database.new(self, name)
37
+ end
38
+
39
+ # Creates a database
40
+ #
41
+ # @param [String] name Database's name
42
+ #
43
+ # @return [Couchy::Database] The newly created database
44
+ def create_db(name)
45
+ put(name)
46
+ database(name)
47
+ end
48
+
49
+ def get(path, params={})
50
+ need_json = !params.delete(:no_json)
51
+ response = RestClient.get(uri_for(path, params))
52
+ need_json ? json(response, :max_nesting => false) : response
53
+ end
54
+
55
+ def post(path, doc=nil, params={})
56
+ headers = params.delete(:headers)
57
+ payload = doc.to_json if doc
58
+ json RestClient.post(uri_for(path, params), payload, headers)
59
+ end
60
+
61
+ def put(path, doc=nil)
62
+ payload = doc.to_json if doc
63
+ json RestClient.put(uri_for(path), payload)
64
+ end
65
+
66
+ def delete(path, params={})
67
+ json RestClient.delete(uri_for(path, params))
68
+ end
69
+
70
+ private
71
+ def uri_for(path, params={})
72
+ u = uri.join(path)
73
+ u.query_values = stringify_keys_and_jsonify_values(params) if params.any?
74
+ u.to_s
75
+ end
76
+
77
+ def json(json_string, options={})
78
+ JSON.parse(json_string, options)
79
+ end
80
+
81
+ def stringify_keys_and_jsonify_values(hash)
82
+ hash.inject({}) do |memo, (key, value)|
83
+ value = value.to_json if %w(key startkey endkey).include?(key.to_s)
84
+ memo[key.to_s] = value.to_s
85
+ memo
86
+ end
87
+ end
88
+ end
89
+ end