couchdb-client 1.0.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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +47 -0
- data/Rakefile +2 -0
- data/couchdb-client.gemspec +20 -0
- data/lib/couchdb/client/version.rb +5 -0
- data/lib/couchdb/client.rb +77 -0
- data/lib/couchdb/database.rb +99 -0
- data/lib/couchdb/document.rb +54 -0
- data/lib/couchdb/errors.rb +89 -0
- data/lib/couchdb/json_object.rb +188 -0
- data/lib/couchdb/model.rb +111 -0
- data/lib/couchdb-client.rb +1 -0
- data/lib/couchdb.rb +35 -0
- data/test/client_test.rb +50 -0
- data/test/database_test.rb +31 -0
- data/test/json_object_test.rb +79 -0
- data/test/model_test.rb +31 -0
- data/test/test_all.rb +1 -0
- data/test/test_helper.rb +29 -0
- metadata +124 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Gimi Liang
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# Couchdb-client
|
2
|
+
|
3
|
+
'couchdb-client' is a pure ruby, easy to use CouchDB client library.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'couchdb-client'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install couchdb-client
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
require 'couchdb-client'
|
22
|
+
|
23
|
+
client = CouchDB.connect :host => 'localhost', :port => 5984 # => CouchDB::Client instance
|
24
|
+
|
25
|
+
db = client['some_database'] # => CouchDB::Database
|
26
|
+
|
27
|
+
doc = db.put 'key' => 'value'
|
28
|
+
|
29
|
+
doc = db.get 'xxx' # => CouchDB::Document instance w/ _id 'xxx'
|
30
|
+
|
31
|
+
doc.update!(:some_field => 'new_value')
|
32
|
+
# or
|
33
|
+
db.put doc.update('some_field' => 'new_value')
|
34
|
+
|
35
|
+
doc.delete!
|
36
|
+
# or
|
37
|
+
db.delete doc._id
|
38
|
+
|
39
|
+
db.exists? 'xxx'
|
40
|
+
|
41
|
+
## Contributing
|
42
|
+
|
43
|
+
1. Fork it
|
44
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
45
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
46
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
47
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/couchdb/client/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Gimi Liang"]
|
6
|
+
gem.email = ["liang.gimi@gmail.com"]
|
7
|
+
gem.description = %q{A pure Ruby CouchDB client.}
|
8
|
+
gem.summary = %q{A pure Ruby CouchDB client.}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "couchdb-client"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = CouchDB::Client::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'json', ['~> 1.7.5']
|
19
|
+
gem.add_dependency 'httparty', ['~> 0.8.3']
|
20
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module CouchDB
|
2
|
+
class Client
|
3
|
+
def self.default_options
|
4
|
+
{:host => 'localhost', :port => 5984}
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
options = self.class.default_options.merge normalize_options(options)
|
9
|
+
@connection = establish_connection options
|
10
|
+
end
|
11
|
+
|
12
|
+
def all_dbs
|
13
|
+
get '_all_dbs'
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: Get a Database with the given name.
|
17
|
+
def db(name, doc_class = Document)
|
18
|
+
DataBase.new self, name, doc_class
|
19
|
+
end
|
20
|
+
|
21
|
+
alias [] db
|
22
|
+
|
23
|
+
def get(path)
|
24
|
+
send_http_request :get, path
|
25
|
+
end
|
26
|
+
|
27
|
+
def put(path, options = {})
|
28
|
+
send_http_request :put, path, {:headers => {'Content-Type' => 'application/json'}}.merge!(options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def post(path, options = {})
|
32
|
+
send_http_request :post, path, {:headers => {'Content-Type' => 'application/json'}}.merge!(options)
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(path, options = {})
|
36
|
+
send_http_request :delete, path, options
|
37
|
+
end
|
38
|
+
|
39
|
+
def head(path, options = {})
|
40
|
+
send_http_request :head, path, options
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_reader :connection
|
46
|
+
|
47
|
+
def normalize_options(options)
|
48
|
+
options.inject({}) { |h, (k, v)| h[k.to_sym] = v; h }
|
49
|
+
end
|
50
|
+
|
51
|
+
def establish_connection(options)
|
52
|
+
class << self; self end.tap { |singleton_class|
|
53
|
+
singleton_class.send :include, HTTParty
|
54
|
+
singleton_class.base_uri base_uri_from_options(options)
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def base_uri_from_options(options)
|
59
|
+
scheme = options[:ssl] ? 'https' : 'http'
|
60
|
+
"#{scheme}://#{options[:host]}".tap { |uri|
|
61
|
+
uri << ":#{options[:port]}" if options[:port]
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def send_http_request(verb, path, options = {})
|
66
|
+
CouchDB.debug { "[CouchDB] Request: #{verb} /#{path} #{options.inspect}" }
|
67
|
+
resp = connection.send(verb, "/#{path}", options)
|
68
|
+
CouchDB.debug { "[CouchDB] Response: #{resp.code} #{resp.body.inspect}" }
|
69
|
+
if resp.code < 300
|
70
|
+
JSON.load resp.body
|
71
|
+
else
|
72
|
+
raise HTTPError, resp
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module CouchDB
|
2
|
+
class DataBase
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(client, name, doc_class = Document)
|
6
|
+
raise ArgumentError, "doc_class must be a Document." unless doc_class <= Document
|
7
|
+
|
8
|
+
@client = client
|
9
|
+
@name = name
|
10
|
+
@doc_class = doc_class
|
11
|
+
end
|
12
|
+
|
13
|
+
def set_doc_class(doc_class)
|
14
|
+
raise ArgumentError, "doc_class must be a Document." unless doc_class <= Document
|
15
|
+
|
16
|
+
@doc_class = doc_class
|
17
|
+
end
|
18
|
+
|
19
|
+
# Create or update a document.
|
20
|
+
def create!
|
21
|
+
client.put name
|
22
|
+
end
|
23
|
+
|
24
|
+
def ensure_exist!
|
25
|
+
create!
|
26
|
+
rescue HTTPError => e
|
27
|
+
raise e unless e.code == 419
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete!
|
31
|
+
client.delete name
|
32
|
+
end
|
33
|
+
|
34
|
+
def all_docs(options = nil)
|
35
|
+
client.get path_for('_all_docs')
|
36
|
+
end
|
37
|
+
|
38
|
+
def new_doc(data)
|
39
|
+
doc_class.new self, data
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: retrieve a document by its _id. Also see `find`.
|
43
|
+
def get(_id)
|
44
|
+
new_doc client.get(path_for(_id))
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: retrieve a document by its _id.
|
48
|
+
#
|
49
|
+
# This method is similar to the `get` method, the only difference is
|
50
|
+
# `get` will raise CouchDB::HTTPError when CouchDB returns errors,
|
51
|
+
# while `find` will only return nil. Which means `get` gives you
|
52
|
+
# a more flexible way to handle errors.
|
53
|
+
def find(_id)
|
54
|
+
get _id
|
55
|
+
rescue HTTPError => e
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
# Public: put a hash-ish into the database.
|
60
|
+
#
|
61
|
+
# This method can be used to create and update a document.
|
62
|
+
def put(data)
|
63
|
+
data = new_doc data
|
64
|
+
raise InvalidObject.new(data) unless data.valid?
|
65
|
+
|
66
|
+
resp =
|
67
|
+
if id = data.delete('_id')
|
68
|
+
client.put path_for(id), :body => encode(data)
|
69
|
+
else
|
70
|
+
client.post name, :body => encode(data)
|
71
|
+
end
|
72
|
+
|
73
|
+
data.merge! '_id' => resp['id'], '_rev' => resp['rev']
|
74
|
+
end
|
75
|
+
|
76
|
+
def delete(_id, _rev)
|
77
|
+
client.delete path_for(_id), :query => {'rev' => _rev}
|
78
|
+
end
|
79
|
+
|
80
|
+
# Public: is there a document whose id is _id?
|
81
|
+
def exist?(_id)
|
82
|
+
client.head path_for(_id)
|
83
|
+
rescue CouchDB::HTTPError
|
84
|
+
raise $! unless $!.code == 404
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
attr_reader :client, :doc_class
|
90
|
+
|
91
|
+
def path_for(_id)
|
92
|
+
"#{name}/#{_id}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def encode(document)
|
96
|
+
JSON.fast_generate document
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module CouchDB
|
2
|
+
class Document < JSONObject
|
3
|
+
class << self
|
4
|
+
alias fixed_schema! fixed_structure!
|
5
|
+
alias fixed_schema? fixed_structure?
|
6
|
+
alias dynamic_schema! dynamic_structure!
|
7
|
+
alias dynamic_schema? dynamic_structure?
|
8
|
+
end
|
9
|
+
|
10
|
+
property :_id, :string
|
11
|
+
property :_rev, :string
|
12
|
+
|
13
|
+
attr_reader :db
|
14
|
+
|
15
|
+
def initialize(db, attributes = nil)
|
16
|
+
super attributes
|
17
|
+
@db = db
|
18
|
+
send :after_initialize if respond_to?(:after_initialize)
|
19
|
+
end
|
20
|
+
|
21
|
+
def _rev
|
22
|
+
self['_rev']
|
23
|
+
end
|
24
|
+
|
25
|
+
alias rev _rev
|
26
|
+
|
27
|
+
def _id
|
28
|
+
self['_id']
|
29
|
+
end
|
30
|
+
|
31
|
+
alias id _id
|
32
|
+
|
33
|
+
def new_record?
|
34
|
+
_id.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
def save
|
38
|
+
send :before_save if respond_to?(:before_save)
|
39
|
+
replace db.put(self)
|
40
|
+
end
|
41
|
+
|
42
|
+
def update!(attributes)
|
43
|
+
send :before_update if respond_to?(:before_update)
|
44
|
+
update attributes
|
45
|
+
save
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete!
|
49
|
+
raise InvalidOperation, "Can not delete a document without _id or _rev." unless id and rev
|
50
|
+
send :before_delete if respond_to?(:before_delete)
|
51
|
+
db.delete id, rev
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module CouchDB
|
2
|
+
class Error < RuntimeError; end
|
3
|
+
|
4
|
+
class InvalidOperation < Error; end
|
5
|
+
|
6
|
+
class PropertyError < Error
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
def initialize(name, msg = nil)
|
10
|
+
@name = name
|
11
|
+
msg ||= "Property error: #{name}"
|
12
|
+
super msg
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_hash
|
16
|
+
{:property => name, :error => 'error'}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class UndefinedProperty < PropertyError
|
21
|
+
def initialize(name)
|
22
|
+
super name, "Property #{name.inspect} is not defined."
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_hash
|
26
|
+
{:property => name, :error => 'undefined'}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class MissingProperty < PropertyError
|
31
|
+
def initialize(name)
|
32
|
+
super name, "Property #{name.inspect} is required, but not given."
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_hash
|
36
|
+
{:property => name, :error => 'missing'}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class InvalidValue < PropertyError
|
41
|
+
attr_accessor :value, :reason
|
42
|
+
|
43
|
+
def initialize(name, value, reason = nil)
|
44
|
+
@value, @reason = value, reason
|
45
|
+
super name, "#{value.inspect} is not a valid value for #{name} (#{reason})."
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_hash
|
49
|
+
{:property => name, :value => value, :error => 'invalid'}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class InvalidObject < Error
|
54
|
+
attr_reader :errors
|
55
|
+
|
56
|
+
def initialize(json_object)
|
57
|
+
@errors = json_object.errors.inject({}) { |h, (name, error)|
|
58
|
+
h[name] = case error
|
59
|
+
when PropertyError
|
60
|
+
error.to_hash.tap { |hash| hash.delete :property }
|
61
|
+
else
|
62
|
+
{:error => 'unknown'}
|
63
|
+
end
|
64
|
+
h
|
65
|
+
}
|
66
|
+
|
67
|
+
super "#{json_object} is invalid."
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_hash
|
71
|
+
@errors
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_json
|
75
|
+
JSON.fast_generate @errors
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class HTTPError < Error
|
80
|
+
attr_reader :code, :body
|
81
|
+
|
82
|
+
def initialize(response)
|
83
|
+
@code = response.code
|
84
|
+
@body = JSON.parse response.body if response.body
|
85
|
+
|
86
|
+
super @body ? @body['reason'] : @code
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module CouchDB
|
2
|
+
# Public: The Ruby class that represents the JSON Object.
|
3
|
+
# All properties (keys) in a JSONObject are strings.
|
4
|
+
# (even strings are not the best hash key in Ruby)
|
5
|
+
class JSONObject < Hash
|
6
|
+
# Private: The propety definition object.
|
7
|
+
class Property
|
8
|
+
BuiltinTypes = {
|
9
|
+
:string => lambda { |v| v.to_s },
|
10
|
+
:int => lambda { |v| Integer(v) },
|
11
|
+
:float => lambda { |v| Float(v) },
|
12
|
+
:bool => lambda { |v| !!v },
|
13
|
+
:array => lambda { |v| Array(v) },
|
14
|
+
:hash => lambda { |v| v.to_hash },
|
15
|
+
:object => JSONObject
|
16
|
+
}
|
17
|
+
|
18
|
+
attr_reader :name, :default
|
19
|
+
|
20
|
+
def initialize(name, type, options = {}, &blk)
|
21
|
+
@name = name
|
22
|
+
|
23
|
+
if type.is_a?(Symbol)
|
24
|
+
raise ArgumentError, "Unknow property type #{type.inspect}." unless BuiltinTypes.has_key?(type)
|
25
|
+
type = BuiltinTypes[type]
|
26
|
+
end
|
27
|
+
type = Class.new JSONObject, &blk if type == JSONObject
|
28
|
+
|
29
|
+
@convertor =
|
30
|
+
if type.respond_to?(:call)
|
31
|
+
type
|
32
|
+
elsif convert_method = [:convert, :new].detect { |m| type.respond_to? m }
|
33
|
+
type.method convert_method
|
34
|
+
else
|
35
|
+
raise ArgumentError, "Property type should has :convert, :call or :new method."
|
36
|
+
end
|
37
|
+
|
38
|
+
@required = options[:required]
|
39
|
+
@default = options[:default]
|
40
|
+
@validator = options[:validate]
|
41
|
+
end
|
42
|
+
|
43
|
+
def required?
|
44
|
+
@required
|
45
|
+
end
|
46
|
+
|
47
|
+
def has_default?
|
48
|
+
!@default.nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
def convert(value)
|
52
|
+
@convertor.call value
|
53
|
+
rescue
|
54
|
+
raise $!.is_a?(InvalidValue) ? $! : InvalidValue.new(name, value, $!.message)
|
55
|
+
end
|
56
|
+
|
57
|
+
def valid_value?(value)
|
58
|
+
@validator.nil? or value.nil? or @validator.call(value)
|
59
|
+
end
|
60
|
+
end # Property
|
61
|
+
|
62
|
+
# Public: Make this object become a fixed struture object, which means
|
63
|
+
# all properties of this object have to be declared (using the
|
64
|
+
# `property` method) before being used.
|
65
|
+
def self.fixed_structure!
|
66
|
+
@fixed_structure = true
|
67
|
+
end
|
68
|
+
|
69
|
+
# Public: Make this object a dynamic struture object, which means
|
70
|
+
# its properties are dynamic (just like a Hash).
|
71
|
+
def self.dynamic_structure!
|
72
|
+
@fixed_structure = false
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.fixed_structure?
|
76
|
+
@fixed_structure
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.dynamic_structure?
|
80
|
+
not fixed_structure?
|
81
|
+
end
|
82
|
+
|
83
|
+
# Public: Properties will inherit from the parent class.
|
84
|
+
def self.properties
|
85
|
+
@properties ||= {}.tap { |h| h.merge! superclass.properties if superclass < JSONObject }
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.property(name, type = :string, options = {}, &blk)
|
89
|
+
name = name.to_s
|
90
|
+
properties[name] = Property.new(name, type, options, &blk)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Public: lookup a property definition by its name.
|
94
|
+
def self.lookup(property_name)
|
95
|
+
properties[property_name.to_s]
|
96
|
+
end
|
97
|
+
|
98
|
+
attr_reader :errors
|
99
|
+
|
100
|
+
def initialize(data = nil)
|
101
|
+
replace data if data
|
102
|
+
set_defaults
|
103
|
+
@errors = {}
|
104
|
+
end
|
105
|
+
|
106
|
+
def []=(name, value)
|
107
|
+
super name.to_s, convert_value(name, value)
|
108
|
+
end
|
109
|
+
|
110
|
+
def store(name, value)
|
111
|
+
super name.to_s, convert_value(name, value)
|
112
|
+
end
|
113
|
+
|
114
|
+
def update(data)
|
115
|
+
super convert_hash(data)
|
116
|
+
end
|
117
|
+
|
118
|
+
def merge!(data)
|
119
|
+
super convert_hash(data)
|
120
|
+
end
|
121
|
+
|
122
|
+
def merge(data)
|
123
|
+
super convert_hash(data)
|
124
|
+
end
|
125
|
+
|
126
|
+
def replace(data)
|
127
|
+
super convert_hash(data)
|
128
|
+
end
|
129
|
+
|
130
|
+
def valid?
|
131
|
+
validate!
|
132
|
+
errors.empty?
|
133
|
+
end
|
134
|
+
|
135
|
+
def validate!
|
136
|
+
errors.clear
|
137
|
+
|
138
|
+
properties.each { |k, property|
|
139
|
+
if property.required? and self[k].nil?
|
140
|
+
errors[k] = MissingProperty.new(k)
|
141
|
+
next
|
142
|
+
end
|
143
|
+
|
144
|
+
if not property.valid_value?(self[k])
|
145
|
+
errors[k] = InvalidValue.new(k, self[k])
|
146
|
+
end
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
def inspect
|
151
|
+
"#{self.class.name}#{super}"
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def properties
|
157
|
+
self.class.properties
|
158
|
+
end
|
159
|
+
|
160
|
+
def fixed_structure?
|
161
|
+
self.class.fixed_structure?
|
162
|
+
end
|
163
|
+
|
164
|
+
def dynamic_structure?
|
165
|
+
self.class.dynamic_structure?
|
166
|
+
end
|
167
|
+
|
168
|
+
def convert_value(property_name, value)
|
169
|
+
unless value.nil?
|
170
|
+
property = self.class.lookup property_name
|
171
|
+
raise UndefinedProperty.new(property_name) if fixed_structure? && property.nil?
|
172
|
+
value = property.convert(value) if property
|
173
|
+
end
|
174
|
+
|
175
|
+
value
|
176
|
+
end
|
177
|
+
|
178
|
+
def convert_hash(hash)
|
179
|
+
Hash[hash.map { |k, v| [k.to_s, convert_value(k, v)] }]
|
180
|
+
end
|
181
|
+
|
182
|
+
def set_defaults
|
183
|
+
properties.values.each { |property|
|
184
|
+
self[property.name] = property.default if self[property.name].nil? and property.has_default?
|
185
|
+
}
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module CouchDB
|
2
|
+
class Model
|
3
|
+
# call-seq
|
4
|
+
# establish_connection options
|
5
|
+
#
|
6
|
+
# call-seq
|
7
|
+
# establish_connection client
|
8
|
+
def self.establish_connection(options_or_client)
|
9
|
+
@connection = options_or_client
|
10
|
+
@connection = Client.new connection unless connection.is_a?(Client)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.connection
|
14
|
+
@connection || superclass <= Model && superclass.connection || nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.db
|
18
|
+
@db ||= connection && connection[db_name, doc_class]
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.set_db_name(name)
|
22
|
+
@db_name = name
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.db_name
|
26
|
+
@db_name || superclass <= Model && superclass.db_name || nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.set_doc_class(doc_class)
|
30
|
+
raise ArgumentError, "Not a Document." unless doc_class <= Document
|
31
|
+
return if @doc_class == doc_class
|
32
|
+
@doc_class = doc_class
|
33
|
+
@db = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.doc_class
|
37
|
+
@doc_class || superclass <= Model && superclass.doc_class || Document
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.find(id)
|
41
|
+
doc = db.find(id)
|
42
|
+
new doc if doc
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :doc
|
46
|
+
|
47
|
+
def initialize(attributes = nil)
|
48
|
+
@doc = self.class.db.new_doc attributes
|
49
|
+
end
|
50
|
+
|
51
|
+
def [](attribute)
|
52
|
+
@doc[attribute]
|
53
|
+
end
|
54
|
+
|
55
|
+
def []=(attribute, value)
|
56
|
+
@doc[attribute] = value
|
57
|
+
end
|
58
|
+
|
59
|
+
def read(chained_keys, splitter = '.')
|
60
|
+
chained_keys.split(splitter).inject(@doc) { |value, key|
|
61
|
+
value = value.respond_to?(key) ? value.send(key) : value[key]
|
62
|
+
break if value.nil?
|
63
|
+
value
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def _id
|
68
|
+
@doc._id
|
69
|
+
end
|
70
|
+
|
71
|
+
alias id _id
|
72
|
+
|
73
|
+
def _rev
|
74
|
+
@doc._rev
|
75
|
+
end
|
76
|
+
|
77
|
+
alias rev _rev
|
78
|
+
|
79
|
+
def new_record?
|
80
|
+
@doc.new_record?
|
81
|
+
end
|
82
|
+
|
83
|
+
def update(attributes)
|
84
|
+
@doc.update! attributes
|
85
|
+
end
|
86
|
+
|
87
|
+
def save
|
88
|
+
@doc.save
|
89
|
+
end
|
90
|
+
|
91
|
+
def delete
|
92
|
+
@doc.delete!
|
93
|
+
freeze
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_hash
|
97
|
+
@doc.to_hash
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_json
|
101
|
+
JSON.fast_generate @doc
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def freeze
|
107
|
+
super
|
108
|
+
@doc.freeze
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'couchdb'
|
data/lib/couchdb.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'httparty'
|
3
|
+
|
4
|
+
module CouchDB
|
5
|
+
require 'couchdb/errors'
|
6
|
+
|
7
|
+
require 'couchdb/client'
|
8
|
+
require 'couchdb/database'
|
9
|
+
require 'couchdb/json_object'
|
10
|
+
require 'couchdb/document'
|
11
|
+
|
12
|
+
require 'couchdb/model'
|
13
|
+
|
14
|
+
class << self
|
15
|
+
# Public: A sugar method for creating a Client instance.
|
16
|
+
def connect(options = {})
|
17
|
+
Client.new options
|
18
|
+
end
|
19
|
+
|
20
|
+
def logger
|
21
|
+
@logger ||= begin
|
22
|
+
require 'logger'
|
23
|
+
Logger.new($stdout).tap { |logger| logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def logger=(logger)
|
28
|
+
@logger = logger
|
29
|
+
end
|
30
|
+
|
31
|
+
def debug
|
32
|
+
logger.debug yield if logger.debug?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/test/client_test.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class ClientTest < CouchDB::TestCase
|
6
|
+
def test_get_all_dbs
|
7
|
+
assert_includes client.all_dbs, 'couchdb-client_test_fixtures'
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_get_all_documents
|
11
|
+
resp = db.all_docs
|
12
|
+
|
13
|
+
assert_equal resp['total_rows'], resp['rows'].size
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_create_documents
|
17
|
+
doc = db.put :type => 'human', :name => 'Gimi'
|
18
|
+
|
19
|
+
assert_kind_of CouchDB::Document, doc
|
20
|
+
assert doc.id
|
21
|
+
assert doc.rev
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_get_documents
|
25
|
+
doc = db.put :type => 'human', :name => 'Gimi'
|
26
|
+
doc2 = db.get doc.id
|
27
|
+
|
28
|
+
assert_kind_of CouchDB::Document, doc2
|
29
|
+
assert_equal doc.id, doc2.id
|
30
|
+
assert_equal doc.rev, doc2.rev
|
31
|
+
assert_equal doc.values_at('type', 'name'), doc2.values_at('type', 'name')
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_update_documents
|
35
|
+
doc = db.put :type => 'human', :name => 'Gimi'
|
36
|
+
rev = doc.rev
|
37
|
+
doc.update! :name => 'GimiL'
|
38
|
+
|
39
|
+
assert_equal 'GimiL', doc['name']
|
40
|
+
refute_equal rev, doc.rev
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_delete_documents
|
44
|
+
doc = db.put :type => 'human', :name => 'Gimi'
|
45
|
+
doc.delete!
|
46
|
+
assert_raises CouchDB::HTTPError do
|
47
|
+
db.get doc.id
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class DatabaseTest < CouchDB::TestCase
|
6
|
+
def test_default_doc_class
|
7
|
+
doc = db.put :type => 'person', :name => 'Gimi'
|
8
|
+
assert_instance_of CouchDB::Document, doc
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_custom_doc_class
|
12
|
+
db = client.db(db_name, TestDoc)
|
13
|
+
|
14
|
+
assert_raises CouchDB::InvalidObject do
|
15
|
+
db.put :key => 'value'
|
16
|
+
end
|
17
|
+
|
18
|
+
doc = db.put :name => 'Gimi'
|
19
|
+
assert_instance_of TestDoc, doc
|
20
|
+
assert_equal 'person', doc['type']
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_exist
|
24
|
+
refute db.exist?('non_exist_id')
|
25
|
+
end
|
26
|
+
|
27
|
+
class TestDoc < CouchDB::Document
|
28
|
+
property :type, :string, :default => 'person'
|
29
|
+
property :name, :string, :required => true
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class JSONObjectTest < MiniTest::Unit::TestCase
|
4
|
+
def test_keys_are_strings
|
5
|
+
o = CouchDB::JSONObject.new
|
6
|
+
o[:key] = 'some_value'
|
7
|
+
assert_equal 'some_value', o['key']
|
8
|
+
|
9
|
+
o = CouchDB::JSONObject.new :key => 'some_value'
|
10
|
+
assert_equal 'some_value', o['key']
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_builtin_type_converter
|
14
|
+
skip
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_dynamic_structure
|
18
|
+
o = CouchDB::JSONObject.new
|
19
|
+
o[:key] = 'some_value'
|
20
|
+
assert o.valid?
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_fixed_structure
|
24
|
+
o = Class.new(CouchDB::JSONObject) do
|
25
|
+
fixed_structure!
|
26
|
+
|
27
|
+
property :valid_key
|
28
|
+
end.new
|
29
|
+
|
30
|
+
assert o['valid_key'] = 'some_value'
|
31
|
+
assert_raises CouchDB::UndefinedProperty do
|
32
|
+
o['invalid_key'] = 'some_value'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_required_property
|
37
|
+
o = Class.new(CouchDB::JSONObject) do
|
38
|
+
property :required_key, :string, :required => true
|
39
|
+
end.new
|
40
|
+
|
41
|
+
refute o.valid?
|
42
|
+
assert_instance_of CouchDB::MissingProperty, o.errors['required_key']
|
43
|
+
|
44
|
+
o[:required_key] = 'some_value'
|
45
|
+
assert o.valid?
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_property_validation
|
49
|
+
o = Class.new(CouchDB::JSONObject) do
|
50
|
+
property :key, :string, :validate => lambda { |v| %w[hello world].include? v }
|
51
|
+
end.new
|
52
|
+
|
53
|
+
o[:key] = 'hello'
|
54
|
+
assert o.valid?
|
55
|
+
|
56
|
+
o[:key] = 'hell'
|
57
|
+
refute o.valid?
|
58
|
+
assert_instance_of CouchDB::InvalidValue, o.errors['key']
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_property_inherent
|
62
|
+
parent = Class.new CouchDB::JSONObject do
|
63
|
+
fixed_structure!
|
64
|
+
|
65
|
+
property :name, :string, :required => true
|
66
|
+
end
|
67
|
+
|
68
|
+
child = Class.new parent do
|
69
|
+
property :parent, :string, :required => true
|
70
|
+
end
|
71
|
+
|
72
|
+
p = parent.new :name => 'Gimi'
|
73
|
+
assert p.valid?
|
74
|
+
|
75
|
+
c = child.new :parent => 'Gimi'
|
76
|
+
refute c.valid?
|
77
|
+
assert_instance_of CouchDB::MissingProperty, c.errors['name']
|
78
|
+
end
|
79
|
+
end
|
data/test/model_test.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ModelTest < CouchDB::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
CouchDB::Model.establish_connection client
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_read_method
|
10
|
+
model = Class.new(CouchDB::Model) do
|
11
|
+
set_doc_class Class.new(CouchDB::Document) {
|
12
|
+
property :key_one, :object do
|
13
|
+
property :inner_key, :string
|
14
|
+
end
|
15
|
+
|
16
|
+
property :key_two, :object do
|
17
|
+
def foo
|
18
|
+
'foo'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
model.set_db_name db_name
|
25
|
+
|
26
|
+
sth = model.new :key_one => {:inner_key => 'inner_value'}, :key_two => {}
|
27
|
+
|
28
|
+
assert_equal 'inner_value', sth.read('key_one.inner_key')
|
29
|
+
assert_equal 'foo', sth.read('key_two.foo')
|
30
|
+
end
|
31
|
+
end
|
data/test/test_all.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Dir[File.expand_path('../*_test.rb', __FILE__)].each { |test_case| require test_case }
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler.setup
|
5
|
+
|
6
|
+
require 'couchdb-client'
|
7
|
+
require 'minitest/autorun'
|
8
|
+
|
9
|
+
class CouchDB::TestCase < MiniTest::Unit::TestCase
|
10
|
+
attr_reader :client, :db
|
11
|
+
|
12
|
+
def setup
|
13
|
+
super
|
14
|
+
|
15
|
+
@client = CouchDB.connect
|
16
|
+
@db = @client[db_name]
|
17
|
+
@db.ensure_exist!
|
18
|
+
end
|
19
|
+
|
20
|
+
def teardown
|
21
|
+
db.delete!
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def db_name
|
27
|
+
@db_name ||= 'couchdb-client_test_fixtures'
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: couchdb-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Gimi Liang
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-09-12 00:00:00 +08:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: json
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 1
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 7
|
33
|
+
- 5
|
34
|
+
version: 1.7.5
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: httparty
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 57
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
- 8
|
49
|
+
- 3
|
50
|
+
version: 0.8.3
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
description: A pure Ruby CouchDB client.
|
54
|
+
email:
|
55
|
+
- liang.gimi@gmail.com
|
56
|
+
executables: []
|
57
|
+
|
58
|
+
extensions: []
|
59
|
+
|
60
|
+
extra_rdoc_files: []
|
61
|
+
|
62
|
+
files:
|
63
|
+
- .gitignore
|
64
|
+
- Gemfile
|
65
|
+
- LICENSE
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- couchdb-client.gemspec
|
69
|
+
- lib/couchdb-client.rb
|
70
|
+
- lib/couchdb.rb
|
71
|
+
- lib/couchdb/client.rb
|
72
|
+
- lib/couchdb/client/version.rb
|
73
|
+
- lib/couchdb/database.rb
|
74
|
+
- lib/couchdb/document.rb
|
75
|
+
- lib/couchdb/errors.rb
|
76
|
+
- lib/couchdb/json_object.rb
|
77
|
+
- lib/couchdb/model.rb
|
78
|
+
- test/client_test.rb
|
79
|
+
- test/database_test.rb
|
80
|
+
- test/json_object_test.rb
|
81
|
+
- test/model_test.rb
|
82
|
+
- test/test_all.rb
|
83
|
+
- test/test_helper.rb
|
84
|
+
has_rdoc: true
|
85
|
+
homepage: ""
|
86
|
+
licenses: []
|
87
|
+
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
hash: 3
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
version: "0"
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
hash: 3
|
108
|
+
segments:
|
109
|
+
- 0
|
110
|
+
version: "0"
|
111
|
+
requirements: []
|
112
|
+
|
113
|
+
rubyforge_project:
|
114
|
+
rubygems_version: 1.3.9.5
|
115
|
+
signing_key:
|
116
|
+
specification_version: 3
|
117
|
+
summary: A pure Ruby CouchDB client.
|
118
|
+
test_files:
|
119
|
+
- test/client_test.rb
|
120
|
+
- test/database_test.rb
|
121
|
+
- test/json_object_test.rb
|
122
|
+
- test/model_test.rb
|
123
|
+
- test/test_all.rb
|
124
|
+
- test/test_helper.rb
|