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