mattly-exegesis 0.2.0 → 0.2.1
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/README.rdoc +5 -9
- data/Rakefile +32 -0
- data/VERSION.yml +1 -1
- data/lib/exegesis/database.rb +38 -14
- data/lib/exegesis/design.rb +57 -21
- data/lib/exegesis/document/attachment.rb +44 -0
- data/lib/exegesis/document/attachments.rb +53 -0
- data/lib/exegesis/document/collection.rb +78 -0
- data/lib/exegesis/document/generic_document.rb +5 -0
- data/lib/exegesis/document.rb +30 -2
- data/lib/exegesis/model.rb +20 -2
- data/lib/exegesis/server.rb +1 -1
- data/lib/exegesis/utils/http.rb +15 -9
- data/lib/exegesis.rb +33 -16
- data/lib/monkeypatches/time.rb +5 -1
- data/test/attachments_test.rb +106 -0
- data/test/database_test.rb +57 -0
- data/test/design_test.rb +91 -49
- data/test/document_collection_test.rb +97 -0
- data/test/document_test.rb +21 -2
- data/test/fixtures/attachments/flavakitten.jpg +0 -0
- data/test/fixtures/designs/things/views/by_name/map.js +5 -0
- data/test/fixtures/designs/things/views/by_tag/map.js +13 -0
- data/test/fixtures/designs/{tags → things}/views/by_tag/reduce.js +0 -0
- data/test/fixtures/designs/things/views/for_path/map.js +11 -0
- data/test/http_test.rb +2 -2
- data/test/model_test.rb +75 -4
- metadata +27 -19
- data/test/fixtures/designs/foos.js +0 -12
- data/test/fixtures/designs/tags/views/by_tag/map.js +0 -8
data/README.rdoc
CHANGED
|
@@ -9,15 +9,9 @@ A CouchDB ODM (Object/Document Mapper) in Ruby.
|
|
|
9
9
|
|
|
10
10
|
== Features:
|
|
11
11
|
|
|
12
|
-
Encourages per-"Account" databases. Actually, does not even currently provide a way to do a
|
|
13
|
-
"singleton" or global database, however this is planned. Since a given class (say, "Article")
|
|
14
|
-
cannot know what database it is supposed to get/search from you cannot do classical class-based
|
|
15
|
-
finders such as "Article.find('value')".
|
|
12
|
+
Encourages per-"Account" databases. Actually, does not even currently provide a way to do a "singleton" or global database, however this is planned. Since a given class (say, "Article") cannot know what database it is supposed to get/search from you cannot do classical class-based finders such as "Article.find('value')". While it might be possible to pass in a database to use for some class-wide view, Exegesis takes the opinion that this is bad design for couchdb for the reasons that views may return multiple document types other than the desired class, and that views should be scoped to objects that mixin the database accessors.
|
|
16
13
|
|
|
17
|
-
CouchDB is table-less, and Exegesis's design reflects this. In CouchDB, Documents are retrieved
|
|
18
|
-
by their unique id, or can be queried from a view function in a design document. Exegesis provides
|
|
19
|
-
tools to aid this. Additionally, since view functions can be used for map/reduce computations against
|
|
20
|
-
your documents, Exegesis helps you get non-document data out of your views.
|
|
14
|
+
CouchDB is table-less, and Exegesis's design reflects this. In CouchDB, Documents are retrieved by their unique id, or can be queried from a view function in a design document. Exegesis provides tools to aid this. Additionally, since view functions can be used for map/reduce computations against your documents, Exegesis helps you get non-document data out of your views.
|
|
21
15
|
|
|
22
16
|
== Examples:
|
|
23
17
|
|
|
@@ -64,4 +58,6 @@ For running the tests:
|
|
|
64
58
|
* Test::Unit (you got it)
|
|
65
59
|
* Context (http://github.com/jeremymcanally/context, can install from github gems)
|
|
66
60
|
* Matchy (http://github.com/jeremymcanally/matchy, github gem version out of date; clone, build & install for now)
|
|
67
|
-
* Zebra (http://github.com/giraffesoft/zerba, depends on jeremymcanally-matchy, which is out of date; clone, build & install for now)
|
|
61
|
+
* Zebra (http://github.com/giraffesoft/zerba, depends on jeremymcanally-matchy, which is out of date; clone, build & install for now)
|
|
62
|
+
|
|
63
|
+
The test suite creates and destroys a database for each test that requires access to the database. This is slow, and the test suite may take some time to run. However, I would rather the test suite be slow and accurate than quick and full of mocking.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'rake'
|
|
2
|
+
require 'rake/testtask'
|
|
3
|
+
require 'rake/rdoctask'
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require 'jeweler'
|
|
7
|
+
Jeweler::Tasks.new do |s|
|
|
8
|
+
s.name = "exegesis"
|
|
9
|
+
s.summary = "TODO"
|
|
10
|
+
s.email = "matt@flowerpowered.com"
|
|
11
|
+
s.homepage = "http://github.com/mattly/exegesis"
|
|
12
|
+
s.description = "A Document <> Object Mapper for CouchDB Documents"
|
|
13
|
+
s.authors = ["Matt Lyon"]
|
|
14
|
+
s.add_dependency('rest-client', '>= 0.12.6')
|
|
15
|
+
end
|
|
16
|
+
rescue LoadError
|
|
17
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Rake::TestTask.new do |t|
|
|
21
|
+
t.libs << 'lib'
|
|
22
|
+
t.pattern = 'test/**/*_test.rb'
|
|
23
|
+
t.verbose = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Rake::RDocTask.new do |rdoc|
|
|
27
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
28
|
+
rdoc.title = 'test-gem'
|
|
29
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
|
30
|
+
rdoc.rdoc_files.include('README*')
|
|
31
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
32
|
+
end
|
data/VERSION.yml
CHANGED
data/lib/exegesis/database.rb
CHANGED
|
@@ -19,24 +19,35 @@ module Exegesis
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
# A hash mapping design names to class names.
|
|
23
|
-
def designs
|
|
24
|
-
@designs ||= {}
|
|
25
|
-
end
|
|
26
|
-
|
|
27
22
|
# declare a design document for this database. Creates a new class and yields a given block to the class to
|
|
28
23
|
# configure the design document and declare views; See Class methods for Exegesis::Design
|
|
29
24
|
def design design_name, opts={}, &block
|
|
30
25
|
klass_name = "#{design_name.to_s.capitalize}Design"
|
|
31
26
|
klass = const_set(klass_name, Class.new(Exegesis::Design))
|
|
32
|
-
designs[design_name] = klass
|
|
33
27
|
klass.design_directory = opts[:directory] || self.designs_directory + design_name.to_s
|
|
34
28
|
klass.design_name = opts[:name] || design_name.to_s
|
|
35
29
|
klass.compose_canonical
|
|
36
30
|
klass.class_eval &block
|
|
37
31
|
define_method design_name do
|
|
38
|
-
@
|
|
39
|
-
@
|
|
32
|
+
@exegesis_designs ||= {}
|
|
33
|
+
@exegesis_designs[design_name] ||= klass.new(self)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def named_document document_name, opts={}, &block
|
|
38
|
+
klass_name = document_name.to_s.capitalize.gsub(/_(\w)/) { $1.capitalize }
|
|
39
|
+
klass = const_set(klass_name, Class.new(Exegesis::GenericDocument))
|
|
40
|
+
klass.unique_id { document_name.to_s }
|
|
41
|
+
klass.class_eval &block if block
|
|
42
|
+
define_method document_name do
|
|
43
|
+
@exegesis_named_documents ||= {}
|
|
44
|
+
@exegesis_named_documents[document_name] ||= begin
|
|
45
|
+
get(document_name.to_s)
|
|
46
|
+
rescue RestClient::ResourceNotFound
|
|
47
|
+
doc = klass.new({}, self)
|
|
48
|
+
doc.save
|
|
49
|
+
doc
|
|
50
|
+
end
|
|
40
51
|
end
|
|
41
52
|
end
|
|
42
53
|
end
|
|
@@ -62,11 +73,18 @@ module Exegesis
|
|
|
62
73
|
@server.get @uri # raise RestClient::ResourceNotFound if the database does not exist
|
|
63
74
|
end
|
|
64
75
|
|
|
76
|
+
def to_s
|
|
77
|
+
"#<#{self.class.name}(Exegesis::Database):#{self.object_id} uri=#{uri}>"
|
|
78
|
+
end
|
|
79
|
+
alias :inspect :to_s
|
|
80
|
+
|
|
81
|
+
# performs a raw GET request against the database
|
|
65
82
|
def raw_get id, options={}
|
|
66
83
|
keys = options.delete(:keys)
|
|
84
|
+
id = Exegesis::Http.escape_id id
|
|
67
85
|
url = Exegesis::Http.format_url "#{@uri}/#{id}", options
|
|
68
86
|
if id.match(%r{^_design/.*/_view/.*$}) && keys
|
|
69
|
-
Exegesis::Http.post url, {:keys => keys}
|
|
87
|
+
Exegesis::Http.post url, {:keys => keys}.to_json
|
|
70
88
|
else
|
|
71
89
|
Exegesis::Http.get url
|
|
72
90
|
end
|
|
@@ -74,7 +92,13 @@ module Exegesis
|
|
|
74
92
|
|
|
75
93
|
# GETs a document with the given id from the database
|
|
76
94
|
def get id, opts={}
|
|
77
|
-
|
|
95
|
+
if id.kind_of?(Array)
|
|
96
|
+
collection = opts.delete(:collection) # nil or true for yes, false for no
|
|
97
|
+
r = post '_all_docs?include_docs=true', {"keys"=>id}
|
|
98
|
+
r['rows'].map {|d| Exegesis.instantiate d['doc'], self }
|
|
99
|
+
else
|
|
100
|
+
Exegesis.instantiate raw_get(id), self
|
|
101
|
+
end
|
|
78
102
|
end
|
|
79
103
|
|
|
80
104
|
# saves a document or collection thereof
|
|
@@ -92,17 +116,17 @@ module Exegesis
|
|
|
92
116
|
end
|
|
93
117
|
|
|
94
118
|
# PUTs the body to the given id in the database
|
|
95
|
-
def put id, body
|
|
96
|
-
Exegesis::Http.put "#{@uri}/#{id}", body
|
|
119
|
+
def put id, body, headers={}
|
|
120
|
+
Exegesis::Http.put "#{@uri}/#{id}", (body || '').to_json, headers
|
|
97
121
|
end
|
|
98
122
|
|
|
99
123
|
# POSTs the body to the database
|
|
100
|
-
def post url, body={}
|
|
124
|
+
def post url, body={}, headers={}
|
|
101
125
|
if body.is_a?(Hash) && body.empty?
|
|
102
126
|
body = url
|
|
103
127
|
url = ''
|
|
104
128
|
end
|
|
105
|
-
Exegesis::Http.post "#{@uri}/#{url}", body
|
|
129
|
+
Exegesis::Http.post "#{@uri}/#{url}", (body || '').to_json, headers
|
|
106
130
|
end
|
|
107
131
|
end
|
|
108
132
|
end
|
data/lib/exegesis/design.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'pathname'
|
|
2
2
|
module Exegesis
|
|
3
3
|
class Design
|
|
4
|
+
|
|
4
5
|
include Exegesis::Document
|
|
5
6
|
|
|
6
7
|
def self.design_directory= dir
|
|
@@ -36,46 +37,67 @@ module Exegesis
|
|
|
36
37
|
}
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
def self.views
|
|
41
|
+
@views ||= canonical_design['views'].keys
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.reduceable? view_name
|
|
45
|
+
view_name = view_name.to_s
|
|
46
|
+
views.include?(view_name) && canonical_design['views'][view_name].has_key?('reduce')
|
|
47
|
+
end
|
|
48
|
+
|
|
39
49
|
def self.view name, default_options={}
|
|
40
|
-
define_method name do
|
|
41
|
-
|
|
50
|
+
define_method name do |*opts|
|
|
51
|
+
options = parse_opts opts.shift, opts.first, default_options
|
|
52
|
+
Exegesis::DocumentCollection.new(call_view(name, options), database)
|
|
42
53
|
end
|
|
43
54
|
end
|
|
44
55
|
|
|
45
56
|
def self.docs name, default_options={}
|
|
46
|
-
|
|
57
|
+
view_name = default_options.delete(:view) || name
|
|
58
|
+
raise ArgumentError, "missing view #{view_name}" unless views.include?(view_name.to_s)
|
|
59
|
+
if [:reduce, :group, :group_level].any? {|key| default_options.has_key?(key)}
|
|
60
|
+
raise ArgumentError, "cannot reduce (:group, :group_level, :reduce) on a docs view"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
default_options = {:include_docs => true}.merge(default_options)
|
|
64
|
+
default_options.update({:reduce => false}) if reduceable?(view_name)
|
|
65
|
+
|
|
47
66
|
define_method name do |*opts|
|
|
48
67
|
key = opts.shift
|
|
49
68
|
options = parse_opts key, opts.first, default_options
|
|
50
|
-
|
|
51
|
-
ids = []
|
|
52
|
-
response.inject([]) do |memo, doc|
|
|
53
|
-
unless ids.include?(doc['id'])
|
|
54
|
-
ids << doc['id']
|
|
55
|
-
memo << Exegesis.instantiate(doc['doc'], database)
|
|
56
|
-
end
|
|
57
|
-
memo
|
|
58
|
-
end
|
|
69
|
+
Exegesis::DocumentCollection.new(call_view(view_name, options), database)
|
|
59
70
|
end
|
|
60
71
|
end
|
|
61
72
|
|
|
62
73
|
def self.hash name, default_options={}
|
|
63
|
-
default_options = {:group => true}.merge(default_options)
|
|
64
74
|
view_name = default_options.delete(:view) || name
|
|
75
|
+
raise ArgumentError, "missing view #{view_name}" unless views.include?(view_name.to_s)
|
|
76
|
+
raise NameError, "Cannot return a hash for views without a reduce function" unless reduceable?(view_name)
|
|
77
|
+
if default_options.has_key?(:group) && default_options[:group] == false
|
|
78
|
+
raise ArgumentError, "cannot turn off grouping for a hash view"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
default_options = {:group => true}.merge(default_options)
|
|
82
|
+
|
|
65
83
|
define_method name do |*opts|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
options.
|
|
84
|
+
options = parse_opts opts.shift, opts.first, default_options
|
|
85
|
+
|
|
86
|
+
if options.has_key?(:group) && options[:group] == false
|
|
87
|
+
raise ArgumentError, "cannot turn off grouping for a hash view"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if options[:key]
|
|
91
|
+
options.delete(:group)
|
|
92
|
+
options.delete(:group_level)
|
|
93
|
+
end
|
|
69
94
|
|
|
70
95
|
response = call_view view_name, options
|
|
71
96
|
if response.size == 1 && response.first['key'].nil?
|
|
72
97
|
response.first['value']
|
|
73
98
|
else
|
|
74
99
|
response.inject({}) do |memo, row|
|
|
75
|
-
|
|
76
|
-
memo[row['key']] = row['value']
|
|
77
|
-
end
|
|
78
|
-
memo
|
|
100
|
+
memo.update(row['key'] => row['value'])
|
|
79
101
|
end
|
|
80
102
|
end
|
|
81
103
|
end
|
|
@@ -84,7 +106,8 @@ module Exegesis
|
|
|
84
106
|
|
|
85
107
|
def initialize db
|
|
86
108
|
begin
|
|
87
|
-
super db.
|
|
109
|
+
super db.raw_get("_design/#{design_name}")
|
|
110
|
+
self.database = db
|
|
88
111
|
rescue RestClient::ResourceNotFound
|
|
89
112
|
db.put("_design/#{design_name}", self.class.canonical_design)
|
|
90
113
|
retry
|
|
@@ -113,6 +136,7 @@ module Exegesis
|
|
|
113
136
|
parse_key opts
|
|
114
137
|
parse_keys opts
|
|
115
138
|
parse_range opts
|
|
139
|
+
parse_reduce opts
|
|
116
140
|
opts
|
|
117
141
|
end
|
|
118
142
|
|
|
@@ -151,5 +175,17 @@ module Exegesis
|
|
|
151
175
|
end
|
|
152
176
|
end
|
|
153
177
|
|
|
178
|
+
def parse_reduce opts
|
|
179
|
+
if opts.has_key?(:group)
|
|
180
|
+
opts[:group_level] = opts.delete(:group) if opts[:group].is_a?(Numeric)
|
|
181
|
+
end
|
|
182
|
+
if opts.keys.any? {|key| [:group, :group_level].include?(key) }
|
|
183
|
+
raise ArgumentError, "cannot include_docs when reducing" if opts[:include_docs]
|
|
184
|
+
if opts.has_key?(:reduce) && opts[:reduce] == false
|
|
185
|
+
raise ArgumentError, "cannot reduce=false when either group or group_level is present"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
154
190
|
end
|
|
155
191
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Exegesis
|
|
2
|
+
module Document
|
|
3
|
+
class Attachment
|
|
4
|
+
|
|
5
|
+
attr_reader :name, :metadata, :document
|
|
6
|
+
|
|
7
|
+
def initialize(name, thing, doc)
|
|
8
|
+
@document = doc
|
|
9
|
+
@metadata = thing
|
|
10
|
+
@name = name
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_s
|
|
14
|
+
"#<Exegesis::Document::Attachment document=#{@document.uri} #{@metadata.inspect}>"
|
|
15
|
+
end
|
|
16
|
+
alias :inspect :to_s
|
|
17
|
+
|
|
18
|
+
def content_type
|
|
19
|
+
@metadata['content_type']
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def length
|
|
23
|
+
@metadata['length'] || -1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def stub?
|
|
27
|
+
@metadata['stub'] || false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def file
|
|
31
|
+
RestClient.get("#{document.database.uri}/#{document.id}/#{name}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_json
|
|
35
|
+
if @metadata['data']
|
|
36
|
+
{'content_type' => @metadata['content_type'], 'data' => @metadata['data']}.to_json
|
|
37
|
+
else
|
|
38
|
+
@metadata.to_json
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
module Exegesis
|
|
3
|
+
module Document
|
|
4
|
+
class Attachments < Hash
|
|
5
|
+
|
|
6
|
+
attr_accessor :document
|
|
7
|
+
|
|
8
|
+
def initialize doc
|
|
9
|
+
@document = doc
|
|
10
|
+
if @document['_attachments']
|
|
11
|
+
@document['_attachments'].each do |name,meta|
|
|
12
|
+
update(name => Exegesis::Document::Attachment.new(name, meta, document))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_s
|
|
18
|
+
"#<Exegesis::Document::Attachments document=#{@document.uri} attachments=#{keys.join(', ')}>"
|
|
19
|
+
end
|
|
20
|
+
alias :inspect :to_s
|
|
21
|
+
|
|
22
|
+
def dirty?
|
|
23
|
+
@dirty || false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def clean!
|
|
27
|
+
each do |name, attachment|
|
|
28
|
+
next if attachment.stub?
|
|
29
|
+
attachment.metadata['stub'] = true
|
|
30
|
+
attachment.metadata.delete('data')
|
|
31
|
+
end
|
|
32
|
+
@dirty = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# saves the attachment to the database NOW. does not keep the attachment in memory once this is done.
|
|
36
|
+
def put(name, contents, type)
|
|
37
|
+
r = Exegesis::Http.put("#{document.uri}/#{name}?rev=#{document.rev}", contents, {:content_type => type})
|
|
38
|
+
if r['ok']
|
|
39
|
+
document['_rev'] = r['rev']
|
|
40
|
+
update(name => Exegesis::Document::Attachment.new(name, {'content_type' => type, 'stub' => true, 'length' => contents.length}, document))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def []= name, contents_and_type
|
|
45
|
+
@dirty = true
|
|
46
|
+
content = contents_and_type.shift
|
|
47
|
+
meta = {'data' => Base64.encode64(content).gsub(/\s/,''), 'content_type' => contents_and_type.first, 'length' => content.length}
|
|
48
|
+
update(name => Exegesis::Document::Attachment.new(name, meta, document))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Exegesis
|
|
2
|
+
class DocumentCollection
|
|
3
|
+
include Enumerable
|
|
4
|
+
|
|
5
|
+
attr_reader :rows, :parent, :index
|
|
6
|
+
|
|
7
|
+
def initialize docs=[], master=nil, index=0
|
|
8
|
+
@rows = docs
|
|
9
|
+
if master.is_a?(Exegesis::DocumentCollection)
|
|
10
|
+
@parent = master
|
|
11
|
+
else
|
|
12
|
+
@database = master
|
|
13
|
+
end
|
|
14
|
+
@index = index
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_s
|
|
18
|
+
"#<Exegesis::DocumentCollection:#{object_id} database=#{database.uri} rows=#{size} depth=#{index}>"
|
|
19
|
+
end
|
|
20
|
+
alias :inspect :to_s
|
|
21
|
+
|
|
22
|
+
def database
|
|
23
|
+
@database || @parent.database
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def size
|
|
27
|
+
@rows.length
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def keys
|
|
31
|
+
@keys ||= rows.map {|r| r['key'] }.uniq
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def values
|
|
35
|
+
@values ||= rows.map {|r| r['value'] }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def documents
|
|
39
|
+
@documents ||= load_documents
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def [] key
|
|
43
|
+
@keymaps ||= {}
|
|
44
|
+
filtered_rows = rows.select do |row|
|
|
45
|
+
if row['key'].is_a?(Array)
|
|
46
|
+
row['key'][index] == key
|
|
47
|
+
else
|
|
48
|
+
row['key'] == key
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
new_index = index + 1
|
|
52
|
+
@keymaps[key] ||= self.class.new(filtered_rows, self, new_index)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def each &block
|
|
56
|
+
rows.each do |row|
|
|
57
|
+
yield row['key'], row['value'], documents[row['id']]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
def load_documents
|
|
63
|
+
docmap = {}
|
|
64
|
+
if parent.nil?
|
|
65
|
+
non_doc_rows = rows.select {|r| ! r.has_key?('doc') }
|
|
66
|
+
if non_doc_rows.empty?
|
|
67
|
+
rows.map {|r| docmap[r['id']] = Exegesis.instantiate(r['doc'], database) }
|
|
68
|
+
else
|
|
69
|
+
database.get(non_doc_rows.map{|r| r['id']}.uniq, :include_docs=>true).each {|doc| docmap[doc.id] = doc}
|
|
70
|
+
end
|
|
71
|
+
else
|
|
72
|
+
rows.map {|r| docmap[r['id']] = parent.documents[r['id']] }
|
|
73
|
+
end
|
|
74
|
+
docmap
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/exegesis/document.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
module Exegesis
|
|
2
2
|
module Document
|
|
3
|
+
autoload :Attachments, 'exegesis/document/attachments'
|
|
4
|
+
autoload :Attachment, 'exegesis/document/attachment'
|
|
5
|
+
|
|
6
|
+
class MissingDatabaseError < StandardError; end
|
|
7
|
+
class NewDocumentError < StandardError; end
|
|
3
8
|
|
|
4
9
|
def self.included base
|
|
5
10
|
base.send :include, Exegesis::Model
|
|
@@ -35,6 +40,20 @@ module Exegesis
|
|
|
35
40
|
@database = db
|
|
36
41
|
end
|
|
37
42
|
|
|
43
|
+
def uri
|
|
44
|
+
raise MissingDatabaseError if database.nil?
|
|
45
|
+
raise NewDocumentError if rev.nil? || id.nil?
|
|
46
|
+
"#{database.uri}/#{id}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reload
|
|
50
|
+
raise NewDocumentError if rev.nil? || id.nil?
|
|
51
|
+
raise MissingDatabaseError if database.nil?
|
|
52
|
+
@attachments = nil
|
|
53
|
+
@references = nil
|
|
54
|
+
@attributes = database.raw_get(id)
|
|
55
|
+
end
|
|
56
|
+
|
|
38
57
|
def == other
|
|
39
58
|
self.id == other.id
|
|
40
59
|
end
|
|
@@ -54,19 +73,28 @@ module Exegesis
|
|
|
54
73
|
else
|
|
55
74
|
save_document
|
|
56
75
|
end
|
|
76
|
+
@attachments.clean! if @attachments && @attachments.dirty?
|
|
57
77
|
end
|
|
58
78
|
|
|
59
79
|
def update_attributes attrs={}
|
|
60
|
-
raise ArgumentError, 'must include a matching _rev attribute' unless rev == attrs.delete('_rev')
|
|
80
|
+
raise ArgumentError, 'must include a matching _rev attribute' unless (rev || '') == (attrs.delete('_rev') || '')
|
|
61
81
|
super attrs
|
|
62
82
|
save
|
|
63
83
|
end
|
|
64
84
|
|
|
85
|
+
def attachments
|
|
86
|
+
@attachments ||= Exegesis::Document::Attachments.new(self)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_json
|
|
90
|
+
@attributes.merge({'_attachments' => @attachments}).to_json
|
|
91
|
+
end
|
|
92
|
+
|
|
65
93
|
private
|
|
66
94
|
|
|
67
95
|
def save_document
|
|
68
96
|
raise ArgumentError, "canont save without a database" unless database
|
|
69
|
-
database.save self
|
|
97
|
+
database.save self
|
|
70
98
|
end
|
|
71
99
|
|
|
72
100
|
def save_with_custom_unique_id
|
data/lib/exegesis/model.rb
CHANGED
|
@@ -4,13 +4,13 @@ module Exegesis
|
|
|
4
4
|
def self.included base
|
|
5
5
|
base.extend ClassMethods
|
|
6
6
|
base.send :include, InstanceMethods
|
|
7
|
-
Exegesis.model_classes[base.name] = base
|
|
8
7
|
base.send :attr_accessor, :attributes, :references, :parent
|
|
9
8
|
end
|
|
10
9
|
|
|
11
10
|
module ClassMethods
|
|
12
11
|
def expose *attrs
|
|
13
12
|
opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
|
|
13
|
+
raise ArgumentError, "casted keys cannot have defined writers" if opts[:as] && opts[:writer]
|
|
14
14
|
[attrs].flatten.each do |attrib|
|
|
15
15
|
attrib = attrib.to_s
|
|
16
16
|
if opts[:writer]
|
|
@@ -20,6 +20,7 @@ module Exegesis
|
|
|
20
20
|
end
|
|
21
21
|
if opts[:as] == :reference
|
|
22
22
|
define_reference attrib
|
|
23
|
+
define_reference_writer attrib unless opts[:writer] == false
|
|
23
24
|
elsif opts[:as]
|
|
24
25
|
define_caster attrib, opts[:as]
|
|
25
26
|
else
|
|
@@ -53,6 +54,22 @@ module Exegesis
|
|
|
53
54
|
end
|
|
54
55
|
end
|
|
55
56
|
|
|
57
|
+
def define_reference_writer attrib
|
|
58
|
+
define_writer(attrib) do |val|
|
|
59
|
+
if val.is_a?(String)
|
|
60
|
+
@attributes[attrib] = val
|
|
61
|
+
elsif val.is_a?(Exegesis::Document)
|
|
62
|
+
if val.rev && val.id
|
|
63
|
+
@attributes[attrib] = val.id
|
|
64
|
+
else
|
|
65
|
+
raise ArgumentError, "cannot reference unsaved documents"
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
raise ArgumentError, "was not a document or document id"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
56
73
|
def define_caster attrib, as
|
|
57
74
|
define_method(attrib) do
|
|
58
75
|
@attributes[attrib] = if @attributes[attrib].is_a?(Array)
|
|
@@ -109,8 +126,9 @@ module Exegesis
|
|
|
109
126
|
|
|
110
127
|
def cast as, value
|
|
111
128
|
return nil if value.nil?
|
|
129
|
+
return value unless [String, Hash, Fixnum, Float].include?(value.class)
|
|
112
130
|
klass = if as == :given && value.is_a?(Hash)
|
|
113
|
-
Exegesis.
|
|
131
|
+
Exegesis.constantize(value['class'])
|
|
114
132
|
elsif as.is_a?(Class)
|
|
115
133
|
as
|
|
116
134
|
else
|
data/lib/exegesis/server.rb
CHANGED
data/lib/exegesis/utils/http.rb
CHANGED
|
@@ -15,23 +15,29 @@ module Exegesis
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def escape_id id
|
|
18
|
-
|
|
18
|
+
if %r{^_design/(.*)/_view/(.*)} =~ id
|
|
19
|
+
"_design/#{CGI.escape($1)}/_view/#{CGI.escape($2)}"
|
|
20
|
+
elsif /^_design\/(.*)/ =~ id
|
|
21
|
+
"_design/#{CGI.escape($1)}"
|
|
22
|
+
else
|
|
23
|
+
CGI.escape(id)
|
|
24
|
+
end
|
|
19
25
|
end
|
|
20
26
|
|
|
21
|
-
def get url
|
|
22
|
-
JSON.parse(RestClient.get(url), :max_nesting => false)
|
|
27
|
+
def get url, headers={}
|
|
28
|
+
JSON.parse(RestClient.get(url, headers), :max_nesting => false)
|
|
23
29
|
end
|
|
24
30
|
|
|
25
|
-
def post url, body=''
|
|
26
|
-
JSON.parse(RestClient.post(url,
|
|
31
|
+
def post url, body='', headers={}
|
|
32
|
+
JSON.parse(RestClient.post(url, body, headers))
|
|
27
33
|
end
|
|
28
34
|
|
|
29
|
-
def put url, body=''
|
|
30
|
-
JSON.parse(RestClient.put(url,
|
|
35
|
+
def put url, body='', headers={}
|
|
36
|
+
JSON.parse(RestClient.put(url, body, headers))
|
|
31
37
|
end
|
|
32
38
|
|
|
33
|
-
def delete url
|
|
34
|
-
JSON.parse(RestClient.delete(url))
|
|
39
|
+
def delete url, headers={}
|
|
40
|
+
JSON.parse(RestClient.delete(url, headers))
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
end
|