sr-couchy 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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