├── .bnsignore
├── .gitignore
├── Gemfile
├── Gemfile.lock
├── History.txt
├── README.rdoc
├── Rakefile
├── VERSION
├── bin
└── couch-docs
├── couch_docs.gemspec
├── fixtures
├── _design
│ ├── __lib
│ │ └── foo.js
│ ├── a
│ │ ├── b
│ │ │ ├── c.js
│ │ │ └── d.js
│ │ └── e.json
│ ├── j
│ │ └── q.json
│ └── x
│ │ └── z.js
├── bar.json
├── baz_with_attachments.json
├── baz_with_attachments
│ └── spacer.gif
└── foo.json
├── lib
├── couch_docs.rb
└── couch_docs
│ ├── command_line.rb
│ ├── design_directory.rb
│ ├── document_directory.rb
│ ├── store.rb
│ └── version.rb
├── spec
├── couch_docs
│ ├── command_line_spec.rb
│ ├── design_directory_spec.rb
│ ├── document_directory_spec.rb
│ └── store_spec.rb
├── couch_docs_spec.rb
├── spec.opts
└── spec_helper.rb
└── test
└── test_couch_docs.rb
/.bnsignore:
--------------------------------------------------------------------------------
1 | # The list of files that should be ignored by Mr Bones.
2 | # Lines that start with '#' are comments.
3 | #
4 | # A .gitignore file can be used instead by setting it as the ignore
5 | # file in your Rakefile:
6 | #
7 | # PROJ.ignore_file = '.gitignore'
8 | #
9 | # For a project with a C extension, the following would be a good set of
10 | # exclude patterns (uncomment them if you want to use them):
11 | # *.[oa]
12 | # *~
13 | announcement.txt
14 | coverage
15 | doc
16 | pkg
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pkg/*
2 | *.gem
3 | .bundle
4 | .rvmrc
5 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in couch_docs.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | couch_docs (1.3.2)
5 | directory_watcher (~> 1.3.0)
6 | json (~> 1.8.0)
7 | mime-types (~> 1.16)
8 | rest-client (~> 1.6.0)
9 |
10 | GEM
11 | remote: http://rubygems.org/
12 | specs:
13 | directory_watcher (1.3.2)
14 | json (1.8.1)
15 | mime-types (1.25.1)
16 | rest-client (1.6.7)
17 | mime-types (>= 1.16)
18 | rspec (1.3.1)
19 |
20 | PLATFORMS
21 | ruby
22 |
23 | DEPENDENCIES
24 | couch_docs!
25 | rspec (~> 1.3.0)
26 |
--------------------------------------------------------------------------------
/History.txt:
--------------------------------------------------------------------------------
1 | == 1.3.1 / 2011-01-02
2 |
3 | * Fix destructive pushes.
4 |
5 |
6 | == 1.3.0 / 2011-01-01
7 |
8 | * Attachment support
9 |
10 | * Attachments for the foo document would be stored in the foo sub-directory (the foo document itself is stored as foo.json).
11 |
12 | * Rudimentary mime-type support.
13 |
14 | * Works with dumping and pushing.
15 |
16 | * Minor bug fixes.
17 |
18 | == 1.2.1 / 2010-04-21
19 |
20 | * Update README to reflect changes in 1.2. No code changes.
21 |
22 | == 1.2.0 / 2010-03-29
23 |
24 | * Directory watcher only applies individual changes, not entire
25 | directory when individual changes are made.
26 |
27 | == 1.1.1 / 2010-03-15
28 |
29 | * Require RestClient 1.1.
30 |
31 | == 1.1.0 / 2010-03-13
32 |
33 | * Better command line experience.
34 | * Default to current directory.
35 | * Print help without args / better format (optparse).
36 | * Support the !code macro from couchapp.
37 | * Support a flag (-d) to only work on design docs.
38 | * Can create the DB if it doesn't already exist
39 | * Command line can be used to watch for local changes to be pushed immediately to the CouchDB server.
40 |
41 | == 1.0.0 / 2009-08-09
42 |
43 | * Update the couch-docs script to be able to dump a CouchDB database
44 | to a local directory as well as uploading a local directory into a
45 | CouchDB database.
46 |
47 | * CouchDB revision numbers are stripped when dumping (to prevent
48 | conflicts when re-loading)
49 |
50 | * Attachments are dumped as well.
51 |
52 | == 0.9.0 / 2009-08-08
53 |
54 | * Import from couch_design_docs (name change to reflect increased functionality)
55 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = NAME
2 |
3 | couch_docs - Manage CouchDB views and documents from the filesystem.
4 |
5 | Author: Chris Strom
6 |
7 | http://github.com/eee-c/couch_docs
8 |
9 | (formerly couch_design_docs)
10 |
11 | = DESCRIPTION
12 |
13 | I have two primary use cases: uploading fixture data for integration testing and editing CouchDB documents in my favorite editor. It could also be useful as a cheap back-up mechanism, though it likely would not work well with large datasets (see PROBLEMS/FUTURE).
14 |
15 | == Fixture Strategy
16 |
17 | Before an integration test, I create a new CouchDB database with a randomly generated name. I use couch_docs to upload fixture data to that database:
18 |
19 | CouchDocs.put_dir(DB_URL, DIRECTORY)
20 |
21 | Then, I allow the test run to proceed normally. At the end of the run, I teardown the test database.
22 |
23 | Since CouchDB database creation / destruction is so cheap, this allows for quite fast integration tests -- even including necessary design documents.
24 |
25 | == Editing with Emacs
26 |
27 | I much prefer editing JSON documents in Emacs (lesser editors should work) over using the Futon web interface built into CouchDB. To initiate this, I watch the directory containing the JSON document(s):
28 |
29 | couch-docs push -w localhost:5984/db
30 |
31 | The -w option uses directory watcher to watch for any changes on the file system. When a change occurs, the updated document is loaded to CouchDB immediately. This is very useful for rapid editing/prototyping of documents that are subsequently used in another medium (e.g. for a blog post).
32 |
33 | = FEATURES
34 |
35 | * Upload JSON documents stored on the filesystem into a CouchDB
36 | database.
37 |
38 | * Map .js files stored on the filesystem
39 | (e.g. _design/recipes/count_by_month/map.js) into CouchDB
40 | design documents.
41 |
42 | * Support for the !code macro from couchapp (useful for
43 | DRYing up map/reduce views as well as list/show documents.
44 |
45 | * Dump documents stored in CouchDB to the filesystem.
46 |
47 | * Attachments are stored as real files (not inline, mime64 encoded attributes on the JSON document)
48 |
49 | * De-resolution of !code macros.
50 |
51 | * Command line script (couch-docs) to push / dump CouchDB database.
52 |
53 | * Multiple options including a directory watcher for uploading
54 | directory changes
55 |
56 | = SYNOPSIS
57 |
58 | From the command line:
59 |
60 | # For dumping the contents of a CouchDB database to the filesystem
61 | couch-docs dump http://localhost:5984/db
62 |
63 | # For loading documents from the filesystem into CouchDB
64 | couch-docs push http://localhost:5984/db
65 |
66 |
67 | In code:
68 |
69 | DB_URL = "http://localhost:5984/db"
70 | DIRECTORY = "/repos/db/couchdb/"
71 |
72 | # /repos/db/couchdb/_design/lucene/transform.js
73 | # /repos/db/couchdb/foo.json
74 |
75 | CouchDocs.put_dir(DB_URL, DIRECTORY)
76 |
77 | # => lucene design document with a "transform" function containing
78 | # the contents of transform.js
79 | # - AND -
80 | # a document named "foo" with the JSON contents from the foo.json
81 | # file
82 |
83 | CouchDocs.dump(DB_URL, "/repos/db/bak")
84 |
85 | # => JSON dump of every document at DB_URL
86 |
87 | = REQUIREMENTS
88 |
89 | * CouchDB
90 | * JSON
91 | * RestClient
92 | * DirectoryWatcher
93 |
94 | = INSTALL
95 |
96 | * sudo gem install couch_docs
97 |
98 | = PROBLEMS/FUTURE
99 |
100 | * Does not honor CouchDB document revisions. Instead it deletes previous revisions and uploads an entirely new one.
101 |
102 | * A progress bar would be helpful.
103 |
104 | * Unit testing of view javascript would be very nice.
105 |
106 | * Will almost certainly not work for large datasets (>10k documents).
107 |
108 | = LICENSE
109 |
110 | (The MIT License)
111 |
112 | Copyright (c) 2010
113 |
114 | Permission is hereby granted, free of charge, to any person obtaining
115 | a copy of this software and associated documentation files (the
116 | 'Software'), to deal in the Software without restriction, including
117 | without limitation the rights to use, copy, modify, merge, publish,
118 | distribute, sublicense, and/or sell copies of the Software, and to
119 | permit persons to whom the Software is furnished to do so, subject to
120 | the following conditions:
121 |
122 | The above copyright notice and this permission notice shall be
123 | included in all copies or substantial portions of the Software.
124 |
125 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
126 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
127 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
128 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
129 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
130 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
131 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
132 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler'
2 | Bundler::GemHelper.install_tasks
3 |
4 | require 'spec/rake/spectask'
5 | Spec::Rake::SpecTask.new(:spec) do |spec|
6 | spec.libs << 'lib' << 'spec'
7 | spec.spec_files = FileList['spec/**/*_spec.rb']
8 | end
9 |
10 | task :default => :spec
11 |
12 | require 'rdoc/task'
13 | Rake::RDocTask.new do |rdoc|
14 | version = File.exist?('VERSION') ? File.read('VERSION') : ""
15 |
16 | rdoc.rdoc_dir = 'rdoc'
17 | rdoc.title = "couch_docs #{version}"
18 | rdoc.rdoc_files.include('README*')
19 | rdoc.rdoc_files.include('lib/**/*.rb')
20 | end
21 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 1.3.2
2 |
--------------------------------------------------------------------------------
/bin/couch-docs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'rubygems'
4 |
5 | require File.expand_path(
6 | File.join(File.dirname(__FILE__), %w[.. lib couch_docs]))
7 |
8 | # Put your code here
9 |
10 | CouchDocs::CommandLine.run ARGV
11 |
12 | # EOF
13 |
--------------------------------------------------------------------------------
/couch_docs.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "couch_docs/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "couch_docs"
7 | s.version = CouchDocs::VERSION
8 | s.platform = Gem::Platform::RUBY
9 | s.authors = ["Chris Strom"]
10 | s.email = ["chris@eeecooks.com"]
11 | s.homepage = "http://github.com/eee-c/couch_docs"
12 | s.summary = %q{Manage CouchDB views and documents}
13 | s.description = %q{Manage CouchDB views and documents.}
14 |
15 | s.rubyforge_project = "couch_docs"
16 |
17 | s.files = `git ls-files`.split("\n")
18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20 | s.require_paths = ["lib"]
21 |
22 | s.add_development_dependency "rspec", ["~> 1.3.0"]
23 |
24 | s.add_runtime_dependency(%q, ["~> 1.6.0"])
25 | s.add_runtime_dependency(%q, ["~> 1.8.0"])
26 | s.add_runtime_dependency(%q, ["~> 1.3.0"])
27 | s.add_runtime_dependency(%q, ["~> 1.16"])
28 | end
29 |
--------------------------------------------------------------------------------
/fixtures/_design/__lib/foo.js:
--------------------------------------------------------------------------------
1 | function foo () { return "foo"; }
2 |
--------------------------------------------------------------------------------
/fixtures/_design/a/b/c.js:
--------------------------------------------------------------------------------
1 | function(doc) { return true; }
--------------------------------------------------------------------------------
/fixtures/_design/a/b/d.js:
--------------------------------------------------------------------------------
1 | function(doc) { return true; }
--------------------------------------------------------------------------------
/fixtures/_design/a/e.json:
--------------------------------------------------------------------------------
1 | [{"one": "2"}]
2 |
--------------------------------------------------------------------------------
/fixtures/_design/j/q.json:
--------------------------------------------------------------------------------
1 | ["!code foo.js"]
2 |
--------------------------------------------------------------------------------
/fixtures/_design/x/z.js:
--------------------------------------------------------------------------------
1 | // !code foo.js
2 | function bar () { return "bar"; }
3 |
--------------------------------------------------------------------------------
/fixtures/bar.json:
--------------------------------------------------------------------------------
1 | {"bar":"2"}
--------------------------------------------------------------------------------
/fixtures/baz_with_attachments.json:
--------------------------------------------------------------------------------
1 | {"baz":"3"}
2 |
--------------------------------------------------------------------------------
/fixtures/baz_with_attachments/spacer.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eee-c/couch_docs/bb97eb57e4a6110172e7cf34ecc723deb89bcecb/fixtures/baz_with_attachments/spacer.gif
--------------------------------------------------------------------------------
/fixtures/foo.json:
--------------------------------------------------------------------------------
1 | {"foo":"1"}
--------------------------------------------------------------------------------
/lib/couch_docs.rb:
--------------------------------------------------------------------------------
1 | require 'ostruct'
2 |
3 | module CouchDocs
4 |
5 | # :stopdoc:
6 | LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
7 | PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
8 | # :startdoc:
9 |
10 | # Returns the version string for the library.
11 | #
12 | def self.version
13 | VERSION
14 | end
15 |
16 | # For a CouchDB database described by db_uri and a
17 | # directory, dir containing design documents, creates
18 | # design documents in the CouchDB database
19 | #
20 | def self.put_dir(db_uri, dir)
21 | self.put_design_dir(db_uri, "#{dir}/_design")
22 | self.put_document_dir(db_uri, dir)
23 | end
24 |
25 | # Alias for put_dir
26 | def self.upload_dir(db_uri, dir)
27 | self.put_dir(db_uri, dir)
28 | end
29 |
30 | # Upload design documents from dir to the CouchDB database
31 | # located at db_uri
32 | #
33 | def self.put_design_dir(db_uri, dir)
34 | store = Store.new(db_uri)
35 | dir = DesignDirectory.new(dir)
36 | store.put_design_documents(dir.to_hash)
37 | end
38 |
39 | # Upload documents from dir to the CouchDB database
40 | # located at db_uri
41 | #
42 | def self.put_document_dir(db_uri, dir)
43 | dir = DocumentDirectory.new(dir)
44 | dir.each_document do |name, contents|
45 | Store.put!("#{db_uri}/#{name}", contents)
46 | end
47 | end
48 |
49 |
50 | # Upload a document located at file_path to the CouchDB database
51 | # located at db_uri
52 | #
53 | def self.put_file(db_uri, file_path)
54 | contents = JSON.parse(File.read(file_path))
55 | name = File.basename(file_path, ".json")
56 | Store.put!("#{db_uri}/#{name}", contents)
57 | end
58 |
59 | # Dump all documents located at db_uri into the directory
60 | # dir
61 | #
62 | def self.dump(db_uri, dir, only=nil)
63 | null_dir = OpenStruct.new(:store_document => nil)
64 |
65 | doc_dir = (only == :design) ?
66 | null_dir : DocumentDirectory.new(dir)
67 | design_dir = (only == :doc) ?
68 | null_dir : DesignDirectory.new(dir)
69 |
70 | store = Store.new(db_uri, :only => only)
71 | store.map.each do |doc|
72 | doc.delete('_rev')
73 | (doc['_id'] =~ /^_design/ ? design_dir : doc_dir).
74 | store_document(doc)
75 | end
76 | end
77 |
78 | # Create or recreate the database located at db_uri
79 | def self.destructive_database_create(db_uri)
80 | Store.put!(db_uri, "")
81 | end
82 |
83 | # Returns the library path for the module. If any arguments are given,
84 | # they will be joined to the end of the libray path using
85 | # File.join.
86 | #
87 | def self.libpath( *args )
88 | args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
89 | end
90 |
91 | # Returns the lpath for the module. If any arguments are given,
92 | # they will be joined to the end of the path using
93 | # File.join.
94 | #
95 | def self.path( *args )
96 | args.empty? ? PATH : ::File.join(PATH, args.flatten)
97 | end
98 |
99 | # Utility method used to require all files ending in .rb that lie in the
100 | # directory below this file that has the same name as the filename passed
101 | # in. Optionally, a specific _directory_ name can be passed in such that
102 | # the _filename_ does not have to be equivalent to the directory.
103 | #
104 | def self.require_all_libs_relative_to( fname, dir = nil )
105 | dir ||= ::File.basename(fname, '.*')
106 | search_me = ::File.expand_path(
107 | ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
108 |
109 | Dir.glob(search_me).sort.each {|rb| require rb}
110 | end
111 |
112 | end # module CouchDocs
113 |
114 | CouchDocs.require_all_libs_relative_to(__FILE__)
115 |
116 | # EOF
117 |
--------------------------------------------------------------------------------
/lib/couch_docs/command_line.rb:
--------------------------------------------------------------------------------
1 | require 'optparse'
2 | require 'pp'
3 |
4 | require 'rubygems'
5 | require 'directory_watcher'
6 |
7 | module CouchDocs
8 | class CommandLine
9 | COMMANDS = %w{push dump}
10 | DEPRECATED_COMMANDS = %w{load}
11 |
12 | def self.run(*args)
13 | CommandLine.new(*args).run
14 | end
15 |
16 | attr_reader :command, :options
17 |
18 | def initialize(args)
19 | parse_options(args)
20 | end
21 |
22 | def run
23 | case command
24 | when "dump"
25 | CouchDocs.dump(@options[:couchdb_url],
26 | @options[:target_dir],
27 | @options[:dump])
28 | when "push"
29 | if @options[:destructive]
30 | CouchDocs.destructive_database_create(options[:couchdb_url])
31 | end
32 |
33 | dw = DirectoryWatcher.new @options[:target_dir]
34 | dw.glob = '**/*'
35 | dw.interval = 2.0
36 |
37 | dw.add_observer do |*args|
38 | puts "Updating documents on CouchDB Server..."
39 | directory_watcher_update(args)
40 | end
41 |
42 | if @options[:watch]
43 | dw.start
44 |
45 | begin
46 | sleep 30 while active?
47 | rescue Interrupt
48 | dw.stop
49 | puts
50 | end
51 | else
52 | dw.run_once
53 | end
54 | when "load" # DEPRECATED
55 | CouchDocs.put_dir(@options[:target_dir],
56 | @options[:couchdb_url])
57 | end
58 | end
59 |
60 | def parse_options(args)
61 | @options = { :target_dir => "." }
62 |
63 | options_parser = OptionParser.new do |opts|
64 | opts.banner = "Usage: couch-docs push|dump [OPTIONS] couchdb_url [target_dir]"
65 |
66 | opts.separator ""
67 | opts.separator "If a target_dir is not specified, the current working directory will be used."
68 |
69 | opts.separator ""
70 | opts.separator "Push options:"
71 |
72 | opts.on("-R", "--destructive",
73 | "Drop the couchdb_uri (if it exists) and create a new database") do
74 | @options[:destructive] = true
75 | end
76 |
77 | # TODO: bulk_docs in 1.2
78 | # opts.on("-b", "--bulk [BATCH_SIZE=1000]", Integer,
79 | # "Use bulk insert when pushing new documents") do |batch_size|
80 | # @options[:bulk] = true
81 | # @options[:batch_size] = batch_size || 1000
82 | # end
83 |
84 | opts.on("-w", "--watch", "Watch the directory for changes, uploading when detected") do
85 | @options[:watch] = true
86 | end
87 |
88 | opts.separator ""
89 | opts.separator "Dump options:"
90 |
91 | opts.on("-d", "--design", "Only dump design documents") do
92 | @options[:dump] = :design
93 | end
94 | opts.on("-D", "--data", "Only dump data documents") do
95 | @options[:dump] = :doc
96 | end
97 |
98 | opts.separator ""
99 | opts.separator "Common options:"
100 |
101 | opts.on_tail("-v", "--version", "Show version") do
102 | puts File.basename($0) + " " + CouchDocs::VERSION
103 | exit
104 | end
105 |
106 | # No argument, shows at tail. This will print an options summary.
107 | # Try it and see!
108 | opts.on_tail("-h", "--help", "Show this message") do
109 | puts opts
110 | exit
111 | end
112 | end
113 |
114 | begin
115 | options_parser.parse!(args)
116 | additional_help = "#{options_parser.banner}\n\nTry --help for more options."
117 | unless (COMMANDS+DEPRECATED_COMMANDS).include? args.first
118 | puts "invalid command: \"#{args.first}\". Must be one of #{COMMANDS.join(', ')}.\n\n"
119 | puts additional_help
120 | exit
121 | end
122 | @command = args.shift
123 | unless args.first
124 | puts "Missing required couchdb_uri argument.\n\n"
125 | puts additional_help
126 | exit
127 | end
128 | @options[:couchdb_url] = args.shift
129 | @options[:target_dir] = args.shift if (args.size >= 1)
130 |
131 | rescue OptionParser::InvalidOption => e
132 | raise e
133 | end
134 | end
135 |
136 | def directory_watcher_update(args)
137 | if initial_add? args
138 | CouchDocs.put_dir(@options[:couchdb_url],
139 | @options[:target_dir])
140 | else
141 | if design_doc_update? args
142 | CouchDocs.put_design_dir(@options[:couchdb_url],
143 | "#{@options[:target_dir]}/_design")
144 | end
145 | documents(args).each do |update|
146 | CouchDocs.put_file(@options[:couchdb_url],
147 | update.path)
148 | end
149 | end
150 | rescue RestClient::ResourceNotFound => e
151 | $stderr.puts "\n"
152 | $stderr.puts e.message
153 | $stderr.puts "Does the database exist? Try using the -R option."
154 | $stderr.puts "\n"
155 | rescue Exception => e
156 | $stderr.puts "\n"
157 | $stderr.puts e.message
158 | $stderr.puts e.backtrace
159 | end
160 |
161 | def initial_add?(args)
162 | args.all? { |f| f.type == :added }
163 | end
164 |
165 | def design_doc_update?(args)
166 | args.any? { |f| f.path =~ /_design/ }
167 | end
168 |
169 | def documents(args)
170 | args.reject { |f| f.path =~ /_design/ }
171 | end
172 |
173 | private
174 | def active?; true end
175 | end
176 | end
177 |
--------------------------------------------------------------------------------
/lib/couch_docs/design_directory.rb:
--------------------------------------------------------------------------------
1 | class Hash
2 | def deep_merge(other)
3 | self.merge(other) do |key, oldval, newval|
4 | oldval.deep_merge(newval)
5 | end
6 | end
7 | end
8 |
9 | module CouchDocs
10 | class DesignDirectory
11 |
12 | attr_accessor :couch_view_dir
13 |
14 | def self.a_to_hash(a)
15 | key = a.first
16 | if (a.length > 2)
17 | { key => a_to_hash(a[1,a.length]) }
18 | else
19 | { key => a.last }
20 | end
21 | end
22 |
23 | def initialize(path)
24 | Dir.new(path) # Just checkin'
25 | @couch_view_dir = path
26 | end
27 |
28 | # Load
29 |
30 | def to_hash
31 | Dir["#{couch_view_dir}/**/*.{js,json}"].inject({}) do |memo, filename|
32 | DesignDirectory.
33 | a_to_hash(expand_file(filename)).
34 | deep_merge(memo)
35 | end
36 | end
37 |
38 | def expand_file(filename)
39 | if filename =~ /\.js$/
40 | name_value_pair = [
41 | File.basename(filename, '.js'),
42 | read_js_value(filename)
43 | ]
44 | elsif filename =~ /\.json$/
45 | name_value_pair = [
46 | File.basename(filename, '.json'),
47 | read_json_value(filename)
48 | ]
49 | end
50 |
51 | name_value_pair[0].gsub!(/%2F/, '/')
52 |
53 | File.dirname(filename).
54 | gsub(/#{couch_view_dir}\/?/, '').
55 | split(/\//) + name_value_pair
56 | end
57 |
58 | def read_json_value(filename)
59 | JSON.parse(File.new(filename).read)
60 | end
61 |
62 | def read_js_value(filename)
63 | File.
64 | readlines(filename).
65 | map { |line| process_code_macro(line) }.
66 | join
67 | end
68 |
69 | def process_code_macro(line)
70 | if line =~ %r{\s*//\s*!code\s*(\S+)\s*}
71 | "// !begin code #{$1}\n" +
72 | read_from_lib($1) +
73 | "// !end code #{$1}\n"
74 | else
75 | line
76 | end
77 | end
78 |
79 | def read_from_lib(path)
80 | File.read("#{couch_view_dir}/__lib/#{path}")
81 | end
82 |
83 | # Store
84 |
85 | def store_document(doc)
86 | id = doc['_id']
87 | self.save_js(nil, id, doc)
88 | end
89 |
90 | def save_js(rel_path, key, value)
91 | if value.is_a? Hash
92 | save_js_hash(rel_path, key, value)
93 | else
94 | save_js_value(rel_path, key, value)
95 | end
96 | end
97 |
98 | def remove_code_macros(js)
99 | js =~ %r{// !begin code ([.\w]+)$}m
100 | lib = $1
101 | if lib and js =~ %r{// !end code #{lib}$}m
102 | remove_code_macros(js.sub(%r{// !begin code #{lib}.+// !end code #{lib}}m, "// !code #{lib}"))
103 | else
104 | js
105 | end
106 | end
107 |
108 | private
109 | def save_js_hash(rel_path, id, hash)
110 | hash.each_pair do |k, v|
111 | next if k == '_id'
112 | self.save_js([rel_path, id].compact.join('/'), k, v)
113 | end
114 | end
115 |
116 | def save_js_value(rel_path, id, value)
117 | ext = value.is_a?(String) ? "js" : "json"
118 | value = value.is_a?(String) ? remove_code_macros(value) : value.to_json
119 |
120 |
121 | path = couch_view_dir + '/' + rel_path
122 | FileUtils.mkdir_p(path)
123 |
124 | file = File.new("#{path}/#{id.gsub(/\//, '%2F')}.#{ext}", "w+")
125 | file.write(value)
126 | file.close
127 | end
128 | end
129 | end
130 |
--------------------------------------------------------------------------------
/lib/couch_docs/document_directory.rb:
--------------------------------------------------------------------------------
1 | require 'base64'
2 |
3 | module CouchDocs
4 | class DocumentDirectory
5 |
6 | attr_accessor :couch_doc_dir
7 |
8 | def initialize(path)
9 | Dir.new(path)
10 | @couch_doc_dir = path
11 | end
12 |
13 | def each_document
14 | Dir["#{couch_doc_dir}/*.json"].each do |filename|
15 | id = File.basename(filename, '.json')
16 | json = JSON.parse(File.new(filename).read)
17 |
18 | if File.directory? "#{couch_doc_dir}/#{id}"
19 | json["_attachments"] ||= { }
20 | Dir["#{couch_doc_dir}/#{id}/*"].each do |attachment|
21 | next unless File.file? attachment
22 | attachment_name = File.basename(attachment)
23 | json["_attachments"][attachment_name] = file_as_attachment(attachment)
24 | end
25 | end
26 |
27 | yield [ id, json ]
28 | end
29 | end
30 |
31 | def store_document(doc)
32 | file = File.new("#{couch_doc_dir}/#{doc['_id']}.json", "w+")
33 | store_attachments(doc['_id'], doc.delete('_attachments'))
34 | file.write(doc.to_json)
35 | file.close
36 | end
37 |
38 | def file_as_attachment(file)
39 | type = mime_type(file)
40 | data = File.read(file)
41 |
42 | attachment = {
43 | "data" => Base64.encode64(data).gsub(/\n/, '')
44 | }
45 | if type
46 | attachment.merge!({"content_type" => type})
47 | end
48 |
49 | attachment
50 | end
51 |
52 | def store_attachments(id, attachments)
53 | return unless attachments
54 |
55 | make_attachment_dir(id)
56 | attachments.each do |filename, opts|
57 | save_attachment(id, filename, opts['data'])
58 | end
59 | end
60 |
61 | def make_attachment_dir(id)
62 | FileUtils.mkdir_p "#{couch_doc_dir}/#{id}"
63 | end
64 |
65 | def save_attachment(id, filename, data)
66 | file = File.new "#{couch_doc_dir}/#{id}/#{filename}", "w"
67 | file.write Base64.decode64(data)
68 | file.close
69 | end
70 |
71 | private
72 | def mime_type(file)
73 | type = MIME::Types.type_for(file).first.to_s
74 | (type && type != '') ? type : nil
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/couch_docs/store.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'restclient'
3 | require 'json'
4 |
5 | module CouchDocs
6 | class Store
7 | include Enumerable
8 |
9 | attr_accessor :url, :design_docs_only
10 |
11 | # Initialize a CouchDB store object. Requires a URL for the
12 | # target CouchDB database.
13 | #
14 | def initialize(url, options={})
15 | @url = url
16 | @design_docs_only = (options[:only] == :design)
17 | end
18 |
19 | # Loads all supplied design documents in the current store.
20 | # Given a hash h, the keys being the CouchDB document
21 | # name and values of design documents
22 | #
23 | def put_design_documents(h)
24 | h.each_pair do |document_name, doc|
25 | Store.put!("#{url}/_design/#{document_name}", doc)
26 | end
27 | end
28 |
29 | # Create or replace the document located at path with the
30 | # Hash document doc
31 | #
32 | def self.put!(path, doc)
33 | self.put(path, doc)
34 | rescue RestClient::RequestFailed
35 | self.delete_and_put(path, doc)
36 | end
37 |
38 | def self.delete_and_put(path, doc)
39 | self.delete(path)
40 | self.put(path, doc)
41 | end
42 |
43 | def self.put(path, doc)
44 | RestClient.put path,
45 | doc.to_json,
46 | :content_type => 'application/json'
47 | end
48 |
49 | def self.post(path, doc)
50 | RestClient.post path,
51 | doc.to_json,
52 | :content_type => 'application/json'
53 | end
54 |
55 | def self.delete(path)
56 | # retrieve existing to obtain the revision
57 | old = self.get(path)
58 | url = old['_rev'] ? path + "?rev=#{old['_rev']}" : path
59 | RestClient.delete(url)
60 | end
61 |
62 | def self.get(path)
63 | JSON.parse(RestClient.get(path))
64 | end
65 |
66 | def each
67 | all_url = "#{url}/_all_docs" +
68 | (design_docs_only ? '?startkey=%22_design%22&endkey=%22_design0%22' : "")
69 | Store.get(all_url)['rows'].each do |rec|
70 | yield Store.get("#{url}/#{rec['id']}?attachments=true")
71 | end
72 | end
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/lib/couch_docs/version.rb:
--------------------------------------------------------------------------------
1 | module CouchDocs
2 | VERSION = "1.3.2"
3 | end
4 |
--------------------------------------------------------------------------------
/spec/couch_docs/command_line_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | describe CouchDocs::CommandLine do
4 | it "should be able to run a single instance of a command line" do
5 | CouchDocs::CommandLine.
6 | should_receive(:new).
7 | with('foo', 'bar').
8 | and_return(mock("Command Line").as_null_object)
9 |
10 | CouchDocs::CommandLine.run('foo', 'bar')
11 | end
12 |
13 | it "should run the command line instance" do
14 | command_line = mock("Command Line").as_null_object
15 | command_line.
16 | should_receive(:run)
17 |
18 | CouchDocs::CommandLine.stub!(:new).and_return(command_line)
19 |
20 | CouchDocs::CommandLine.run('foo', 'bar')
21 | end
22 |
23 | context "an instance that dumps a CouchDB database" do
24 | it "should dump CouchDB documents from uri to dir when run" do
25 | @it = CouchDocs::CommandLine.new(['dump', 'uri', 'dir'])
26 |
27 | CouchDocs.
28 | should_receive(:dump).
29 | with("uri", "dir", nil)
30 |
31 | @it.run
32 | end
33 |
34 | it "should be able to dump only design documents" do
35 | @it = CouchDocs::CommandLine.new(['dump', 'uri', 'dir', '-d'])
36 |
37 | CouchDocs.
38 | should_receive(:dump).
39 | with("uri", "dir", :design)
40 |
41 | @it.run
42 | end
43 |
44 | it "should be able to dump only regular documents" do
45 | @it = CouchDocs::CommandLine.new(['dump', 'uri', 'dir', '-D'])
46 |
47 | CouchDocs.
48 | should_receive(:dump).
49 | with("uri", "dir", :doc)
50 |
51 | @it.run
52 | end
53 |
54 | it "should be an initial add if everything is an add" do
55 | @it = CouchDocs::CommandLine.new(['push', 'uri'])
56 | args = [mock(:type => :added),
57 | mock(:type => :added)]
58 | @it.should be_initial_add(args)
59 | end
60 |
61 | it "should not be an initial add if something is not an add" do
62 | @it = CouchDocs::CommandLine.new(['push', 'uri'])
63 | args = [mock(:type => :foo),
64 | mock(:type => :added)]
65 | @it.should_not be_initial_add(args)
66 | end
67 |
68 | it "should be a design docs update if something changes in _design" do
69 | @it = CouchDocs::CommandLine.new(['push', 'uri'])
70 | args = [mock(:path => "foo"),
71 | mock(:path => "_design")]
72 | @it.should be_design_doc_update(args)
73 | end
74 |
75 | it "should know document updates" do
76 | @it = CouchDocs::CommandLine.new(['push', 'uri'])
77 | doc_update = mock(:path => "foo")
78 | args = [doc_update,
79 | mock(:path => "_design")]
80 |
81 | @it.
82 | documents(args).
83 | should == [doc_update]
84 | end
85 |
86 |
87 | context "updates on the filesystem" do
88 | before(:each) do
89 | @args = mock("args")
90 | @it = CouchDocs::CommandLine.new(%w(push uri dir))
91 | end
92 | it "should only update design docs if only local design docs have changed" do
93 | CouchDocs.
94 | should_receive(:put_dir)
95 |
96 | @it.stub!(:initial_add?).and_return(true)
97 | @it.directory_watcher_update(@args)
98 | end
99 | context "not an inital add" do
100 | before(:each) do
101 | @it.stub!(:initial_add?).and_return(false)
102 | @it.stub!(:design_doc_update?).and_return(false)
103 | @it.stub!(:documents).and_return([])
104 | CouchDocs.stub!(:put_design_dir)
105 | end
106 | it "should update design docs if there are design document updates" do
107 | CouchDocs.
108 | should_receive(:put_design_dir)
109 |
110 | @it.stub!(:design_doc_update?).and_return(true)
111 | @it.directory_watcher_update(@args)
112 | end
113 | it "should update documents (if any)" do
114 | file_mock = mock("File", :path => "/foo")
115 | @it.stub!(:documents).and_return([file_mock])
116 |
117 | CouchDocs.
118 | should_receive(:put_file).
119 | with("uri", "/foo")
120 |
121 | @it.directory_watcher_update(@args)
122 | end
123 | end
124 | end
125 | end
126 |
127 | context "pushing" do
128 | before(:each) do
129 | CouchDocs.stub!(:put_dir)
130 |
131 | @dw = mock("Directory Watcher").as_null_object
132 | DirectoryWatcher.stub!(:new).and_return(@dw)
133 | end
134 |
135 | it "should know watch" do
136 | @it = CouchDocs::CommandLine.new(%w(push uri dir -w))
137 | @it.options[:watch].should be_true
138 | end
139 |
140 | it "should run once normally" do
141 | @dw.should_receive(:run_once)
142 |
143 | @it = CouchDocs::CommandLine.new(%w(push uri dir))
144 | @it.run
145 | end
146 |
147 | it "should start a watcher with -w" do
148 | @dw.should_receive(:start)
149 |
150 | @it = CouchDocs::CommandLine.new(%w(push uri dir -w))
151 | @it.stub!(:active?).and_return(false)
152 | @it.run
153 | end
154 | end
155 |
156 | context "an instance that uploads to a CouchDB database" do
157 | before(:each) do
158 | @it = CouchDocs::CommandLine.new(['load', 'dir', 'uri'])
159 | end
160 |
161 | it "should load CouchDB documents from dir to uri when run" do
162 | CouchDocs.
163 | should_receive(:put_dir).
164 | with("uri", "dir")
165 |
166 | @it.run
167 | end
168 | end
169 |
170 | end
171 |
--------------------------------------------------------------------------------
/spec/couch_docs/design_directory_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | describe CouchDocs::DesignDirectory do
4 | it "should require a root directory for instantiation" do
5 | lambda { CouchDocs::DesignDirectory.new }.
6 | should raise_error
7 |
8 | lambda { CouchDocs::DesignDirectory.new("foo") }.
9 | should raise_error
10 |
11 | lambda { CouchDocs::DesignDirectory.new("fixtures/_design")}.
12 | should_not raise_error
13 | end
14 |
15 | it "should convert arrays into deep hashes" do
16 | CouchDocs::DesignDirectory.
17 | a_to_hash(%w{a b c d}).
18 | should == {
19 | 'a' => {
20 | 'b' => {
21 | 'c' => 'd'
22 | }
23 | }
24 | }
25 | end
26 |
27 | context "a valid directory" do
28 | before(:each) do
29 | @it = CouchDocs::DesignDirectory.new("fixtures/_design")
30 | end
31 |
32 | it "should list dirs, basename and contents of a js file" do
33 | @it.expand_file("fixtures/_design/a/b/c.js").
34 | should == ['a', 'b', 'c', 'function(doc) { return true; }']
35 | end
36 |
37 | it "should list dirs, basename and contents of a json file" do
38 | @it.expand_file("fixtures/_design/a/e.json").
39 | should == ['a', 'e', [{"one" => "2"}]]
40 | end
41 |
42 | it "should assemble all documents into a single docs structure" do
43 | @it.to_hash['a'].
44 | should == {
45 | 'b' => {
46 | 'c' => 'function(doc) { return true; }',
47 | 'd' => 'function(doc) { return true; }'
48 | },
49 | 'e' => [{"one" => "2"}]
50 | }
51 | end
52 |
53 | it "should process code macros when assembling" do
54 | @it.to_hash['x'].
55 | should == {
56 | 'z' =>
57 | "// !begin code foo.js\n" +
58 | "function foo () { return \"foo\"; }\n" +
59 | "// !end code foo.js\n" +
60 | "function bar () { return \"bar\"; }\n"
61 | }
62 | end
63 |
64 | it "should ignore macro escape sequence when reading JSON" do
65 | @it.to_hash['j'].
66 | should == {'q' => ["!code foo.js"]}
67 | end
68 |
69 | it "should work with absolute !code paths"
70 |
71 | it "should replace !code macros with the contents of the referenced file in lib" do
72 | @it.stub!(:read_from_lib).and_return("awesome javascript")
73 |
74 | @it.
75 | process_code_macro(" // !code foo/bar.js ").
76 | should =~ /awesome javascript/
77 | end
78 |
79 | it "should not affect normal lines when processing macros" do
80 | @it.
81 | process_code_macro(" var foo = 'bar'; ").
82 | should == " var foo = 'bar'; "
83 | end
84 |
85 | it "should find files with relative paths in __lib" do
86 | File.
87 | should_receive(:read).
88 | with("fixtures/_design/__lib/foo.js")
89 |
90 | @it.read_from_lib("foo.js")
91 | end
92 |
93 | end
94 |
95 | context "saving a JSON attribute" do
96 | before(:each) do
97 | @it = CouchDocs::DesignDirectory.new("/tmp")
98 |
99 | FileUtils.stub!(:mkdir_p)
100 | @file = mock("File").as_null_object
101 | File.stub!(:new).and_return(@file)
102 | end
103 |
104 | it "should not mangle json valued attributes" do
105 | @file.
106 | should_receive(:write).
107 | with(%{["bar","baz"]})
108 |
109 | @it.save_js(nil, "_design/foo", { "foo" => ["bar","baz"] })
110 | end
111 |
112 | it "should save in a .json file" do
113 | File.
114 | should_receive(:new).
115 | with("/tmp/_design/foo/foo.json", "w+").
116 | and_return(@file)
117 |
118 | @it.save_js(nil, "_design/foo", { "foo" => ["bar","baz"] })
119 | end
120 | end
121 |
122 | context "saving a JS attribute" do
123 | before(:each) do
124 | @it = CouchDocs::DesignDirectory.new("/tmp")
125 |
126 | FileUtils.stub!(:mkdir_p)
127 | @file = mock("File").as_null_object
128 | File.stub!(:new).and_return(@file)
129 | end
130 |
131 | it "should not store _id" do
132 | File.
133 | should_not_receive(:new).
134 | with("/tmp/_design/foo/_id.js", "w+")
135 |
136 | @it.save_js(nil, "_design/foo", { "_id" => "_design/foo"})
137 | end
138 |
139 | it "should create map the design document attribute to the filesystem" do
140 | FileUtils.
141 | should_receive(:mkdir_p).
142 | with("/tmp/_design/foo")
143 |
144 | @it.save_js("_design/foo", "bar", "json")
145 | end
146 |
147 | it "should store the attribute to the filesystem" do
148 | File.
149 | should_receive(:new).
150 | with("/tmp/_design/foo/bar.js", "w+")
151 |
152 | @it.save_js("_design/foo", "bar", "json")
153 | end
154 |
155 | it "should store hash values to the filesystem" do
156 | File.
157 | should_receive(:new).
158 | with("/tmp/_design/foo/bar/baz.js", "w+")
159 |
160 | @it.save_js("_design/foo", "bar", { "baz" => "json" })
161 | end
162 |
163 | it "should store the attribute to the filesystem" do
164 | @file.
165 | should_receive(:write).
166 | with("json")
167 |
168 | @it.save_js("_design/foo", "bar", "json")
169 | end
170 |
171 | it "should store the attributes with slashes to the filesystem" do
172 | File.
173 | should_receive(:new).
174 | with("/tmp/_design/foo/bar%2Fbaz.js", "w+")
175 |
176 | @it.save_js("_design/foo", "bar/baz", "json")
177 | end
178 |
179 | it "should strip lib code when dumping" do
180 | js = <<_JS
181 | // !begin code foo.js
182 | function foo () { return 'foo'; }
183 | // !end code foo.js
184 | // !begin code bar.js
185 | function bar () { return 'bar'; }
186 | // !end code bar.js
187 | function baz () { return 'baz'; }
188 | _JS
189 |
190 | @it.
191 | remove_code_macros(js).
192 | should == "// !code foo.js\n" +
193 | "// !code bar.js\n" +
194 | "function baz () { return 'baz'; }\n"
195 | end
196 | end
197 | end
198 |
--------------------------------------------------------------------------------
/spec/couch_docs/document_directory_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | describe CouchDocs::DocumentDirectory do
4 | it "should require a root directory for instantiation" do
5 | lambda { CouchDocs::DocumentDirectory.new }.
6 | should raise_error
7 |
8 | lambda { CouchDocs::DocumentDirectory.new("foo") }.
9 | should raise_error
10 |
11 | lambda { CouchDocs::DocumentDirectory.new("fixtures")}.
12 | should_not raise_error
13 | end
14 |
15 | context "a valid directory" do
16 | before(:each) do
17 | @it = CouchDocs::DocumentDirectory.new("fixtures")
18 | end
19 |
20 | it "should be able to iterate over the documents" do
21 | everything = []
22 | @it.each_document do |name, contents|
23 | everything << [name, contents]
24 | end
25 | everything.
26 | should include ['bar', {"bar" => "2"}]
27 |
28 | everything.
29 | should include ['foo', {"foo" => "1"}]
30 | end
31 |
32 | it "should be able to store a document" do
33 | file = mock("File", :write => 42, :close => true)
34 | File.
35 | should_receive(:new).
36 | with("fixtures/foo.json", "w+").
37 | and_return(file)
38 |
39 | @it.store_document({'_id' => 'foo'})
40 | end
41 |
42 | it "should be able to save a document as JSON" do
43 | file = mock("File", :close => true)
44 | File.stub!(:new).and_return(file)
45 |
46 | file.should_receive(:write).with(%Q|{"_id":"foo"}|)
47 |
48 | @it.store_document({'_id' => 'foo'})
49 | end
50 |
51 | context "pushing attachments to CouchDB" do
52 | before(:each) do
53 | @spacer_b64 = "R0lGODlhAQABAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD//////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///yH5BAEAABAALAAAAAABAAEAAAgEALkFBAA7"
54 | end
55 |
56 | it "should connect attachments by sub-directory name (foo.json => foo/)" do
57 | @it.stub!(:mime_type)
58 |
59 | everything = []
60 | @it.each_document do |name, contents|
61 | everything << [name, contents]
62 | end
63 |
64 | everything.
65 | should include(['baz_with_attachments',
66 | {'baz' => '3',
67 | "_attachments" => { "spacer.gif" => {"data" => @spacer_b64} } }])
68 | end
69 | it "should mime 64 encode attachments" do
70 | # covered above
71 | end
72 | it "should ignore non-file attachments" do
73 | # covered above
74 | end
75 |
76 | context "infering mime type" do
77 | before(:each) do
78 | File.stub!(:read).and_return("asdf")
79 | Base64.stub!(:encode64).and_return("asdf")
80 | end
81 |
82 | it "should guess gif mime type" do
83 | @it.file_as_attachment("spacer.gif").
84 | should == {
85 | "data" => "asdf",
86 | "content_type" => "image/gif"
87 | }
88 | end
89 |
90 | it "should guess jpeg mime type" do
91 | @it.file_as_attachment("spacer.jpg").
92 | should == {
93 | "data" => "asdf",
94 | "content_type" => "image/jpeg"
95 | }
96 | end
97 |
98 | it "should guess png mime type" do
99 | @it.file_as_attachment("spacer.png").
100 | should == {
101 | "data" => "asdf",
102 | "content_type" => "image/png"
103 | }
104 | end
105 |
106 | it "should default to no mime type" do
107 | @it.file_as_attachment("spacer.foo").
108 | should == {
109 | "data" => "asdf"
110 | }
111 | end
112 | end
113 |
114 | it "should give precedence to filesystem attachments" do
115 | @it.stub!(:mime_type)
116 |
117 | JSON.stub!(:parse).
118 | and_return({ "baz" => "3",
119 | "_attachments" => {
120 | "spacer.gif" => "asdf",
121 | "baz.jpg" => "asdf"
122 | }
123 | })
124 |
125 | everything = []
126 | @it.each_document do |name, contents|
127 | everything << [name, contents]
128 | end
129 |
130 | everything.
131 | should include(['baz_with_attachments',
132 | {'baz' => '3',
133 | "_attachments" => { "spacer.gif" => {"data" => @spacer_b64}, "baz.jpg" => "asdf" } }])
134 | end
135 |
136 | end
137 | context "dump attachments from CouchDB" do
138 | before(:each) do
139 | FileUtils.stub!(:mkdir_p)
140 | end
141 |
142 | it "should store attachments" do
143 | file = mock("File").as_null_object
144 | File.stub!(:new).and_return(file)
145 |
146 | @it.
147 | should_receive(:store_attachments).
148 | with('foo', 'bar')
149 |
150 | @it.store_document({'_id' => 'foo',
151 | '_attachments' => 'bar'})
152 | end
153 |
154 | context "storing attachments" do
155 | before(:each) do
156 | @attachments = { 'foo.txt' => { 'data' => 'attachment data' } }
157 | end
158 |
159 | it "should make a directory to hold the attachments" do
160 | @it.
161 | should_receive(:make_attachment_dir).
162 | with('foo')
163 |
164 | @it.stub!(:save_attachment)
165 | @it.store_attachments('foo', @attachments)
166 | end
167 |
168 | it "should create a sub-directory with document ID" do
169 | FileUtils.
170 | should_receive(:mkdir_p).
171 | with("fixtures/foo")
172 |
173 | @it.stub!(:save_attachment)
174 | @it.make_attachment_dir('foo')
175 | end
176 |
177 | it "should save attachments to the filesystem" do
178 | @it.
179 | should_receive(:save_attachment).
180 | with('foo', 'foo.txt', 'attachment data')
181 |
182 | @it.stub!(:save_attachment)
183 | @it.store_attachments('foo', @attachments)
184 | end
185 |
186 | it "should dump with native encoding (non-mime64)" do
187 | file = mock("File").as_null_object
188 | File.stub!(:new).and_return(file)
189 |
190 | file.
191 | should_receive(:write)
192 |
193 | @it.save_attachment('foo', 'foo.txt', 'ZGF0YQ==')
194 | end
195 | end
196 |
197 | it "should not include the attachments attribute" do
198 | file = mock("File", :close => true)
199 | File.stub!(:new).and_return(file)
200 |
201 | file.
202 | should_receive(:write).
203 | with('{"_id":"foo"}')
204 |
205 | @it.stub!(:store_attachments)
206 |
207 | @it.store_document({'_id' => 'foo',
208 | '_attachments' => 'foo'})
209 | end
210 | end
211 | end
212 | end
213 |
--------------------------------------------------------------------------------
/spec/couch_docs/store_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2 |
3 | describe CouchDocs::Store do
4 | it "should require a CouchDB URL Root for instantiation" do
5 | lambda { CouchDocs::Store.new }.
6 | should raise_error
7 |
8 | lambda { CouchDocs::Store.new("uri") }.
9 | should_not raise_error
10 | end
11 |
12 | context "a valid store" do
13 | before(:each) do
14 | @it = CouchDocs::Store.new("uri")
15 |
16 | @hash = {
17 | 'a' => {
18 | 'b' => {
19 | 'c' => 'function(doc) { return true; }'
20 | }
21 | }
22 | }
23 | end
24 |
25 | it "should be able to put a new document" do
26 | CouchDocs::Store.
27 | should_receive(:put).
28 | with("uri", { })
29 |
30 | CouchDocs::Store.put!("uri", { })
31 | end
32 |
33 | it "should delete existing docs if first put fails" do
34 | CouchDocs::Store.
35 | stub!(:put).
36 | and_raise(RestClient::RequestFailed)
37 |
38 | CouchDocs::Store.
39 | should_receive(:delete_and_put).
40 | with("uri", { })
41 |
42 | CouchDocs::Store.put!("uri", { })
43 | end
44 |
45 | it "should be able to delete and put" do
46 | CouchDocs::Store.
47 | should_receive(:delete).
48 | with("uri")
49 |
50 | CouchDocs::Store.
51 | should_receive(:put).
52 | with("uri", { })
53 |
54 | CouchDocs::Store.delete_and_put("uri", { })
55 | end
56 |
57 | it "should be able to load a hash into design docs" do
58 | RestClient.
59 | should_receive(:put).
60 | with("uri/_design/a",
61 | '{"b":{"c":"function(doc) { return true; }"}}',
62 | :content_type => 'application/json')
63 | @it.put_design_documents(@hash)
64 | end
65 |
66 | it "should be able to retrieve an existing document" do
67 | RestClient.
68 | stub!(:get).
69 | and_return('{"_rev":"1234"}')
70 |
71 | CouchDocs::Store.get("uri").should == { '_rev' => "1234" }
72 | end
73 |
74 | it "should be able to delete an existing document" do
75 | CouchDocs::Store.stub!(:get).and_return({ '_rev' => '1234' })
76 |
77 | RestClient.
78 | should_receive(:delete).
79 | with("uri?rev=1234")
80 |
81 | CouchDocs::Store.delete("uri")
82 | end
83 |
84 | it "deletes without revision if none is present (e.g. database delete)" do
85 | CouchDocs::Store.stub!(:get).and_return({ })
86 |
87 | RestClient.
88 | should_receive(:delete).
89 | with("uri")
90 |
91 | CouchDocs::Store.delete("uri")
92 | end
93 |
94 | it "should be able to load each document" do
95 | CouchDocs::Store.stub!(:get).
96 | with("uri/_all_docs").
97 | and_return({ "total_rows" => 2,
98 | "offset" => 0,
99 | "rows" => [{"id"=>"1", "value"=>{}, "key"=>"1"},
100 | {"id"=>"2", "value"=>{}, "key"=>"2"}]})
101 |
102 | CouchDocs::Store.stub!(:get).with("uri/1?attachments=true")
103 | CouchDocs::Store.should_receive(:get).with("uri/2?attachments=true")
104 |
105 | @it.each { }
106 | end
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/spec/couch_docs_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe CouchDocs do
4 | it "should be able to create (or delete/create) a DB" do
5 | CouchDocs::Store.
6 | should_receive(:put!).
7 | with("couchdb_url", anything())
8 |
9 | CouchDocs.destructive_database_create("couchdb_url")
10 | end
11 |
12 | it "should be able to load design and normal documents" do
13 | CouchDocs.
14 | should_receive(:put_design_dir).
15 | with("uri", "fixtures/_design")
16 |
17 | CouchDocs.
18 | should_receive(:put_document_dir).
19 | with("uri", "fixtures")
20 |
21 | CouchDocs.put_dir("uri", "fixtures")
22 | end
23 |
24 | it "should be able to load directory/JS files into CouchDB as design docs" do
25 | store = mock("Store")
26 | CouchDocs::Store.stub!(:new).and_return(store)
27 |
28 | dir = mock("Design Directory")
29 | dir.stub!(:to_hash).and_return({ "foo" => "bar" })
30 | CouchDocs::DesignDirectory.stub!(:new).and_return(dir)
31 |
32 | store.
33 | should_receive(:put_design_documents).
34 | with({ "foo" => "bar" })
35 |
36 | CouchDocs.put_design_dir("uri", "fixtures")
37 | end
38 |
39 | it "should be able to load documents into CouchDB" do
40 | dir = mock("Document Directory")
41 | dir.
42 | stub!(:each_document).
43 | and_yield('foo', {"foo" => "1"})
44 |
45 | CouchDocs::DocumentDirectory.stub!(:new).and_return(dir)
46 |
47 | CouchDocs::Store.
48 | should_receive(:put!).
49 | with('uri/foo', {"foo" => "1"})
50 |
51 | CouchDocs.put_document_dir("uri", "fixtures")
52 | end
53 |
54 | it "should be able to upload a single document into CouchDB" do
55 | CouchDocs::Store.
56 | should_receive(:put!).
57 | with('uri/foo', {"foo" => "1"})
58 |
59 | File.stub!(:read).and_return('{"foo": "1"}')
60 |
61 | CouchDocs.put_file("uri", "/foo")
62 | end
63 |
64 | context "dumping CouchDB documents to a directory" do
65 | before(:each) do
66 | @store = mock("Store")
67 | CouchDocs::Store.stub!(:new).and_return(@store)
68 |
69 | @des_dir = mock("Design Directory").as_null_object
70 | CouchDocs::DesignDirectory.stub!(:new).and_return(@des_dir)
71 |
72 | @dir = mock("Document Directory").as_null_object
73 | CouchDocs::DocumentDirectory.stub!(:new).and_return(@dir)
74 | end
75 | it "should be able to store all CouchDB documents on the filesystem" do
76 | @store.stub!(:map).and_return([{'_id' => 'foo'}])
77 | @dir.
78 | should_receive(:store_document).
79 | with({'_id' => 'foo'})
80 |
81 | CouchDocs.dump("uri", "fixtures")
82 | end
83 | it "should ignore design documents" do
84 | @store.stub!(:map).and_return([{'_id' => '_design/foo'}])
85 | @dir.
86 | should_not_receive(:store_document)
87 |
88 | CouchDocs.dump("uri", "fixtures")
89 | end
90 | it "should strip revision numbers" do
91 | @store.stub!(:map).
92 | and_return([{'_id' => 'foo', '_rev' => '1-1234'}])
93 | @dir.
94 | should_receive(:store_document).
95 | with({'_id' => 'foo'})
96 |
97 | CouchDocs.dump("uri", "fixtures")
98 | end
99 | it "should not dump regular docs when asked for only design docs" do
100 | @store.stub!(:map).
101 | and_return([{'foo' => 'bar'}])
102 |
103 | @dir.
104 | should_not_receive(:store_document)
105 |
106 | CouchDocs.dump("uri", "fixtures", :design)
107 | end
108 | it "should not dump design docs when asked for only regular docs" do
109 | @store.stub!(:map).
110 | and_return([{'_id' => '_design/foo'}])
111 |
112 | @des_dir.
113 | should_not_receive(:store_document)
114 |
115 | CouchDocs.dump("uri", "fixtures", :doc)
116 | end
117 | end
118 | end
119 |
120 | # EOF
121 |
--------------------------------------------------------------------------------
/spec/spec.opts:
--------------------------------------------------------------------------------
1 | -cfs --color
2 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'couch_docs'
2 | require 'spec'
3 | require 'spec/autorun'
4 |
5 | Spec::Runner.configure do |config|
6 | # == Mock Framework
7 | #
8 | # RSpec uses it's own mocking framework by default. If you prefer to
9 | # use mocha, flexmock or RR, uncomment the appropriate line:
10 | #
11 | # config.mock_with :mocha
12 | # config.mock_with :flexmock
13 | # config.mock_with :rr
14 | end
15 |
16 | # EOF
17 |
--------------------------------------------------------------------------------
/test/test_couch_docs.rb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eee-c/couch_docs/bb97eb57e4a6110172e7cf34ecc723deb89bcecb/test/test_couch_docs.rb
--------------------------------------------------------------------------------