relaxo-query-server 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +133 -0
- data/bin/relaxo-query-server +39 -0
- data/lib/relaxo/query_server.rb +40 -0
- data/lib/relaxo/query_server/context.rb +101 -0
- data/lib/relaxo/query_server/designer.rb +249 -0
- data/lib/relaxo/query_server/mapper.rb +68 -0
- data/lib/relaxo/query_server/process.rb +43 -0
- data/lib/relaxo/query_server/reducer.rb +94 -0
- data/lib/relaxo/query_server/shell.rb +82 -0
- data/lib/relaxo/query_server/version.rb +31 -0
- data/test/context_test.rb +25 -0
- data/test/test_filters.rb +24 -0
- data/test/test_lists.rb +46 -0
- data/test/test_map.rb +20 -0
- data/test/test_reduce.rb +15 -0
- data/test/test_shows.rb +53 -0
- data/test/test_updates.rb +19 -0
- data/test/test_validations.rb +22 -0
- data/test/test_views.rb +17 -0
- metadata +98 -0
data/README.md
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
Relaxo Query Server
|
2
|
+
===================
|
3
|
+
|
4
|
+
* Author: Samuel G. D. Williams (<http://www.oriontransfer.co.nz>)
|
5
|
+
* Copyright (C) 2012 Samuel G. D. Williams.
|
6
|
+
* Released under the MIT license.
|
7
|
+
|
8
|
+
The Relaxo Query Server implements the CouchDB Query Server protocol for CouchDB 1.1.0+. It provides a comprehensive Ruby-style view server along with full support for Design Document based processing.
|
9
|
+
|
10
|
+
*This software is currently under development and not ready for stable production usage.*
|
11
|
+
|
12
|
+
Installation
|
13
|
+
------------
|
14
|
+
|
15
|
+
Install the ruby gem as follows:
|
16
|
+
|
17
|
+
sudo gem install relaxo-query-server
|
18
|
+
|
19
|
+
Edit your etc/couchdb/local.ini and add the following:
|
20
|
+
|
21
|
+
[query_servers]
|
22
|
+
relaxo-ruby = relaxo-query-server
|
23
|
+
|
24
|
+
Make sure the `relaxo-query-server` executable is accessible from `$PATH`.
|
25
|
+
|
26
|
+
To build and install the gem from source:
|
27
|
+
|
28
|
+
cd build/
|
29
|
+
sudo GEM=gem1.9 rake1.9 install_gem
|
30
|
+
|
31
|
+
Usage
|
32
|
+
-----
|
33
|
+
|
34
|
+
### Mapping Function ###
|
35
|
+
|
36
|
+
Select documents of `type == 'user'`:
|
37
|
+
|
38
|
+
lambda do |document|
|
39
|
+
if document['type'] == 'user'
|
40
|
+
emit(document['_id'])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
### Reduction Function ###
|
45
|
+
|
46
|
+
Calculate the sum:
|
47
|
+
|
48
|
+
lambda do |keys,values,rereduce|
|
49
|
+
values.inject &:+
|
50
|
+
end
|
51
|
+
|
52
|
+
### Design Document ###
|
53
|
+
|
54
|
+
A simple application:
|
55
|
+
|
56
|
+
# design.yaml
|
57
|
+
- _id: "_design/users"
|
58
|
+
language: 'relaxo-ruby'
|
59
|
+
views:
|
60
|
+
service:
|
61
|
+
map: |
|
62
|
+
lambda do |doc|
|
63
|
+
emit(doc['_id']) if doc['type'] == 'user'
|
64
|
+
end
|
65
|
+
validate_doc_update: |
|
66
|
+
lambda do |new_document, old_document, user_context|
|
67
|
+
if !user_context.admin && new_document['admin'] == true
|
68
|
+
raise ValidationError.new(:forbidden => "Cannot create admin user!")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
updates:
|
72
|
+
user: |
|
73
|
+
lambda do |doc, request|
|
74
|
+
doc['updated_date'] = Date.today.iso8061; [doc, 'OK']
|
75
|
+
end
|
76
|
+
lists:
|
77
|
+
user: |
|
78
|
+
lambda do |head, request|
|
79
|
+
send "<ul>"
|
80
|
+
each do |user|
|
81
|
+
send "<li>" + user['name'] + "</li>"
|
82
|
+
end
|
83
|
+
send "</ul>"
|
84
|
+
end
|
85
|
+
filters:
|
86
|
+
regular_users: |
|
87
|
+
lambda do |doc, request|
|
88
|
+
doc['admin'] == false
|
89
|
+
end
|
90
|
+
shows:
|
91
|
+
user: |
|
92
|
+
lambda do |doc, request|
|
93
|
+
{
|
94
|
+
:code => 200,
|
95
|
+
:headers => {"Content-Type" => "text/html"}},
|
96
|
+
:body => "User: #{doc['name']}"
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
The previous `design.yaml` document can be loaded using the `relaxo` client command:
|
101
|
+
|
102
|
+
relaxo http://localhost:5984/test design.yaml
|
103
|
+
|
104
|
+
To Do
|
105
|
+
-----
|
106
|
+
|
107
|
+
- Improve documentation, including better sample code.
|
108
|
+
- More tests, including performance benchmarks.
|
109
|
+
- Explore `parse_function` to improve performance of code execution by caching functions.
|
110
|
+
- Helper methods for commonly used mapping functions.
|
111
|
+
|
112
|
+
License
|
113
|
+
-------
|
114
|
+
|
115
|
+
Copyright (c) 2010, 2011 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
|
116
|
+
|
117
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
118
|
+
of this software and associated documentation files (the "Software"), to deal
|
119
|
+
in the Software without restriction, including without limitation the rights
|
120
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
121
|
+
copies of the Software, and to permit persons to whom the Software is
|
122
|
+
furnished to do so, subject to the following conditions:
|
123
|
+
|
124
|
+
The above copyright notice and this permission notice shall be included in
|
125
|
+
all copies or substantial portions of the Software.
|
126
|
+
|
127
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
128
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
129
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
130
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
131
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
132
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
133
|
+
THE SOFTWARE.
|
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'relaxo/query_server'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
OPTIONS = {
|
7
|
+
:safe => 4,
|
8
|
+
}
|
9
|
+
|
10
|
+
ARGV.options do |o|
|
11
|
+
script_name = File.basename($0)
|
12
|
+
|
13
|
+
o.set_summary_indent(' ')
|
14
|
+
o.banner = "Usage: #{script_name} [options] [directory]"
|
15
|
+
o.define_head "This program is designed to be used with CouchDB."
|
16
|
+
|
17
|
+
o.separator ""
|
18
|
+
o.separator "Help and Copyright information"
|
19
|
+
|
20
|
+
o.on("--safe [level]", "Set the ruby $SAFE level to product the execution environment.") do |level|
|
21
|
+
OPTIONS[:safe] = level.to_i
|
22
|
+
end
|
23
|
+
|
24
|
+
o.separator ""
|
25
|
+
|
26
|
+
o.on_tail("--copy", "Display copyright information") do
|
27
|
+
puts "#{script_name}. Copyright (c) 2012 Samuel Williams. Released under the MIT license."
|
28
|
+
puts "See http://www.oriontransfer.co.nz/ for more information."
|
29
|
+
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
|
33
|
+
o.on_tail("-h", "--help", "Show this help message.") do
|
34
|
+
puts o
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
end.parse!
|
38
|
+
|
39
|
+
Relaxo::QueryServer.run!(OPTIONS)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Copyright (c) 2012 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'relaxo/query_server/context'
|
22
|
+
require 'relaxo/query_server/shell'
|
23
|
+
|
24
|
+
# Provide useful standard functionality:
|
25
|
+
require 'bigdecimal'
|
26
|
+
require 'date'
|
27
|
+
|
28
|
+
module Relaxo
|
29
|
+
module QueryServer
|
30
|
+
# Run the query server using `$stdin` and `$stdout` for communication.
|
31
|
+
def self.run!(options = {})
|
32
|
+
shell = Shell.new($stdin, $stdout)
|
33
|
+
context = Context.new(shell, options)
|
34
|
+
|
35
|
+
shell.run do |command|
|
36
|
+
context.run(command)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# Copyright (c) 2012 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'relaxo/query_server/shell'
|
22
|
+
require 'relaxo/query_server/mapper'
|
23
|
+
require 'relaxo/query_server/reducer'
|
24
|
+
require 'relaxo/query_server/designer'
|
25
|
+
|
26
|
+
module Relaxo
|
27
|
+
module QueryServer
|
28
|
+
class CompilationError < StandardError
|
29
|
+
end
|
30
|
+
|
31
|
+
# A query server context includes all state required for implementing the query server protocol.
|
32
|
+
class Context
|
33
|
+
# Given a function as text, and an execution scope, return a callable object.
|
34
|
+
def parse_function(text, scope, filename = 'query-server')
|
35
|
+
safe_level = @options[:safe] || 0
|
36
|
+
|
37
|
+
function = lambda { $SAFE = safe_level; eval(text, scope.send(:binding), filename) }.call
|
38
|
+
|
39
|
+
unless function.respond_to? :call
|
40
|
+
raise CompilationError.new('Expression does not evaluate to procedure!')
|
41
|
+
end
|
42
|
+
|
43
|
+
return function
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(shell, options = {})
|
47
|
+
@shell = shell
|
48
|
+
@options = options
|
49
|
+
|
50
|
+
@mapper = Mapper.new(self)
|
51
|
+
@reducer = Reducer.new(self)
|
52
|
+
@designer = Designer.new(self)
|
53
|
+
|
54
|
+
@config = {}
|
55
|
+
end
|
56
|
+
|
57
|
+
attr :config
|
58
|
+
attr :shell
|
59
|
+
|
60
|
+
# Return an error structure from the given exception.
|
61
|
+
def error_for_exception(exception)
|
62
|
+
["error", exception.class.to_s, exception.message, exception.backtrace]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Process a single command as per the query server protocol.
|
66
|
+
def run(command)
|
67
|
+
case command[0]
|
68
|
+
# ** Map functionality
|
69
|
+
when 'add_fun'
|
70
|
+
@mapper.add_function command[1]; true
|
71
|
+
when 'map_doc'
|
72
|
+
@mapper.map command[1]
|
73
|
+
when 'reset'
|
74
|
+
@config = command[1] || {}
|
75
|
+
@mapper = Mapper.new(self); true
|
76
|
+
|
77
|
+
# ** Reduce functionality
|
78
|
+
when 'reduce'
|
79
|
+
@reducer.reduce command[1], command[2]
|
80
|
+
when 'rereduce'
|
81
|
+
@reducer.rereduce command[1], command[2]
|
82
|
+
|
83
|
+
# ** Design document functionality
|
84
|
+
when 'ddoc'
|
85
|
+
if command[1] == 'new'
|
86
|
+
@designer.create(command[2], command[3]); true
|
87
|
+
else
|
88
|
+
@designer.run(command[1], command[2], command[3])
|
89
|
+
end
|
90
|
+
end
|
91
|
+
rescue Exception => exception
|
92
|
+
error_for_exception(exception)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Write a log message back to the shell.
|
96
|
+
def log(message)
|
97
|
+
@shell.write_object ['log', message]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
# Copyright (c) 2012 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'relaxo/query_server/shell'
|
22
|
+
require 'relaxo/query_server/mapper'
|
23
|
+
require 'relaxo/query_server/reducer'
|
24
|
+
|
25
|
+
module Relaxo
|
26
|
+
module QueryServer
|
27
|
+
# Indicates that a validation error has occured.
|
28
|
+
class ValidationError < StandardError
|
29
|
+
def initialize(details)
|
30
|
+
super "Validation failed!"
|
31
|
+
|
32
|
+
@details = details
|
33
|
+
end
|
34
|
+
|
35
|
+
# The details of the validation error, typically:
|
36
|
+
# {:forbidden => "Message"}
|
37
|
+
# or
|
38
|
+
# {:unauthorized => "Message"}
|
39
|
+
attr :details
|
40
|
+
end
|
41
|
+
|
42
|
+
# Indicates that the request was invalid for some reason (e.g. lacking appropriate authentication)
|
43
|
+
class InvalidRequestError < StandardError
|
44
|
+
def initilize(message, code = 400, details = {})
|
45
|
+
super message
|
46
|
+
|
47
|
+
@code = 400
|
48
|
+
@details = details
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a response suitable for passing back to the client.
|
52
|
+
def to_response
|
53
|
+
@details.merge(:code => @code)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Supports the `list` action which consumes rows and outputs encoded text in chunks.
|
58
|
+
class ListRenderer < Process
|
59
|
+
def initialize(context, function)
|
60
|
+
super(context, function)
|
61
|
+
|
62
|
+
@started = false
|
63
|
+
@fetched_row = false
|
64
|
+
@start_response = {:headers => {}}
|
65
|
+
@chunks = []
|
66
|
+
end
|
67
|
+
|
68
|
+
def run(head, request)
|
69
|
+
super(head, request)
|
70
|
+
|
71
|
+
# Ensure that at least one row is read from input:
|
72
|
+
get_row unless @fetched_row
|
73
|
+
|
74
|
+
return ["end", @chunks]
|
75
|
+
end
|
76
|
+
|
77
|
+
def send(chunk)
|
78
|
+
@chunks << chunk
|
79
|
+
false
|
80
|
+
end
|
81
|
+
|
82
|
+
def each
|
83
|
+
while row = get_row
|
84
|
+
yield row
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_row
|
89
|
+
flush
|
90
|
+
|
91
|
+
row = @context.shell.read_object
|
92
|
+
@fetched_row = true
|
93
|
+
|
94
|
+
case command = row[0]
|
95
|
+
when "list_row"
|
96
|
+
row[1]
|
97
|
+
when "list_end"
|
98
|
+
false
|
99
|
+
else
|
100
|
+
raise RuntimeError.new("Input is not a row!")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def start(response)
|
105
|
+
raise RuntimeError.new("List already started!") if @started
|
106
|
+
|
107
|
+
@start_response = response
|
108
|
+
end
|
109
|
+
|
110
|
+
def flush
|
111
|
+
if @started
|
112
|
+
@context.shell.write_object ["chunks", @chunks]
|
113
|
+
else
|
114
|
+
@context.shell.write_object ["start", @chunks, @start_response]
|
115
|
+
|
116
|
+
@started = true
|
117
|
+
end
|
118
|
+
|
119
|
+
@chunks = []
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Represents a design document which includes a variety of functionality for processing documents.
|
124
|
+
class DesignDocument
|
125
|
+
VALIDATED = 1
|
126
|
+
|
127
|
+
def initialize(context, name, attributes = {})
|
128
|
+
@context = context
|
129
|
+
|
130
|
+
@name = name
|
131
|
+
@attributes = attributes
|
132
|
+
end
|
133
|
+
|
134
|
+
# Lookup the given key in the design document's attributes.
|
135
|
+
def [] key
|
136
|
+
@attributes[key]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Runs the given function with the given arguments.
|
140
|
+
def run(function, arguments)
|
141
|
+
action = function[0]
|
142
|
+
function = function_for(function)
|
143
|
+
|
144
|
+
self.send(action, function, *arguments)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Implements the `filters` action.
|
148
|
+
def filters(function, documents, request)
|
149
|
+
results = documents.map{|document| !!function.call(document, request)}
|
150
|
+
|
151
|
+
return [true, results]
|
152
|
+
end
|
153
|
+
|
154
|
+
# Implements the `shows` action.
|
155
|
+
def shows(function, document, request)
|
156
|
+
response = function.call(document, request)
|
157
|
+
|
158
|
+
return ["resp", wrap_response(response)]
|
159
|
+
end
|
160
|
+
|
161
|
+
# Implements the `updates` action.
|
162
|
+
def updates(function, document, request)
|
163
|
+
raise InvalidRequestError.new("Unsupported method #{request['method']}") unless request['method'] == 'POST'
|
164
|
+
|
165
|
+
document, response = function.call(document, request)
|
166
|
+
|
167
|
+
return ["up", document, wrap_response(response)]
|
168
|
+
rescue InvalidRequestError => error
|
169
|
+
return ["up", null, error.to_response]
|
170
|
+
end
|
171
|
+
|
172
|
+
# Implements the `lists` action.
|
173
|
+
def lists(function, head, request)
|
174
|
+
ListRenderer.new(@context, function).run(head, request)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Implements the `validates_doc_update` action.
|
178
|
+
def validates(function, new_document, old_document, user_context)
|
179
|
+
ValidationProcess.new(@context, function).run(new_document, old_document, user_context)
|
180
|
+
|
181
|
+
# Unless ValidationError was raised, we are okay.
|
182
|
+
return VALIDATED
|
183
|
+
rescue ValidationError => error
|
184
|
+
error.details
|
185
|
+
end
|
186
|
+
|
187
|
+
alias validate_doc_update validates
|
188
|
+
|
189
|
+
# Ensures that the response is the correct form.
|
190
|
+
def wrap_response(response)
|
191
|
+
String === response ? {"body" => response} : response
|
192
|
+
end
|
193
|
+
|
194
|
+
# Looks a up a function given a key path into the design document.
|
195
|
+
def function_for(path)
|
196
|
+
parent = self
|
197
|
+
|
198
|
+
function = path.inject(parent) do |current, key|
|
199
|
+
parent = current
|
200
|
+
|
201
|
+
throw ArgumentError.new("Invalid function name #{path.join(".")}") unless current
|
202
|
+
|
203
|
+
current[key]
|
204
|
+
end
|
205
|
+
|
206
|
+
# Compile the function if required:
|
207
|
+
if String === function
|
208
|
+
parent[path.last] = @context.parse_function(function, self, 'design-document')
|
209
|
+
else
|
210
|
+
function
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Implements the design document state and interface.
|
216
|
+
class Designer
|
217
|
+
def initialize(context)
|
218
|
+
@context = context
|
219
|
+
@documents = {}
|
220
|
+
end
|
221
|
+
|
222
|
+
# Create a new design document.
|
223
|
+
#
|
224
|
+
# @param [String] name
|
225
|
+
# The name of the design document.
|
226
|
+
# @param [Hash] attributes
|
227
|
+
# The contents of the design document.
|
228
|
+
def create(name, attributes)
|
229
|
+
@documents[name] = DesignDocument.new(@context, name, attributes)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Run a function on a given design document.
|
233
|
+
#
|
234
|
+
# @param [String] name
|
235
|
+
# The name of the design document.
|
236
|
+
# @param [Array] function
|
237
|
+
# A key path to the function to execute.
|
238
|
+
# @param [Array] arguments
|
239
|
+
# The arguments to provide to the function.
|
240
|
+
def run(name, function, arguments)
|
241
|
+
document = @documents[name]
|
242
|
+
|
243
|
+
raise ArgumentError.new("Invalid document name #{name}") unless document
|
244
|
+
|
245
|
+
document.run(function, arguments)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# Copyright (c) 2012 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'relaxo/query_server/process'
|
22
|
+
|
23
|
+
module Relaxo
|
24
|
+
module QueryServer
|
25
|
+
# Supports `Mapper` by providing a context with an `emit` method that collects the results from the mapping function.
|
26
|
+
class MappingProcess < Process
|
27
|
+
def initialize(context, function)
|
28
|
+
super(context, function)
|
29
|
+
@results = []
|
30
|
+
end
|
31
|
+
|
32
|
+
# Emit a result
|
33
|
+
def emit(key, value = nil)
|
34
|
+
@results << [key, value]
|
35
|
+
end
|
36
|
+
|
37
|
+
def run(*args)
|
38
|
+
begin
|
39
|
+
call(*args)
|
40
|
+
rescue Exception => exception
|
41
|
+
# If the mapping function throws an error, report the error for this document:
|
42
|
+
return @context.error_for_exception(exception)
|
43
|
+
end
|
44
|
+
|
45
|
+
return @results
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Mapper
|
50
|
+
def initialize(context)
|
51
|
+
@context = context
|
52
|
+
@functions = []
|
53
|
+
end
|
54
|
+
|
55
|
+
# Adds a function by parsing the text, typically containing a textual representation of a lambda.
|
56
|
+
def add_function text
|
57
|
+
@functions << @context.parse_function(text, self)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Map a document to a set of results by appling all functions.
|
61
|
+
def map(document)
|
62
|
+
@functions.map do |function|
|
63
|
+
MappingProcess.new(@context, function).run(document)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Copyright (c) 2012 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module Relaxo
|
22
|
+
module QueryServer
|
23
|
+
# A simple high level functional process attached to a given `context`. Typically used for executing functions from map/reduce and design documents.
|
24
|
+
class Process
|
25
|
+
def initialize(context, function)
|
26
|
+
@context = context
|
27
|
+
@function = function
|
28
|
+
end
|
29
|
+
|
30
|
+
def log(message)
|
31
|
+
@context.log message
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(*args)
|
35
|
+
instance_exec *args, &@function
|
36
|
+
end
|
37
|
+
|
38
|
+
def run(*args)
|
39
|
+
call(*args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# Copyright (c) 2012 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'relaxo/query_server/process'
|
22
|
+
|
23
|
+
module Relaxo
|
24
|
+
module QueryServer
|
25
|
+
# Call the reduction function, and if it fails, respond with an error message.
|
26
|
+
class ReducingProcess < Process
|
27
|
+
def evaluate(*args)
|
28
|
+
begin
|
29
|
+
call(*args)
|
30
|
+
rescue Exception => exception
|
31
|
+
# If the mapping function throws an error, report the error for this document:
|
32
|
+
@context.error_for_exception(exception)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Implements the `reduce` and `rereduce` functions along with all associated state.
|
38
|
+
class Reducer
|
39
|
+
# Create a reducer attached to the given context.
|
40
|
+
def initialize(context)
|
41
|
+
@context = context
|
42
|
+
end
|
43
|
+
|
44
|
+
# Apply the reduce function to a given list of items. Functions are typically in the form of:
|
45
|
+
# functions = [lambda{|keys,values,rereduce| ...}]
|
46
|
+
#
|
47
|
+
# such that:
|
48
|
+
# items = [[key1, value1], [key2, value2], [key3, value3]]
|
49
|
+
# functions.map{|function| function.call(all keys, all values, false)}
|
50
|
+
#
|
51
|
+
# @param [Array] functions
|
52
|
+
# An array of functions to apply.
|
53
|
+
# @param [Array] items
|
54
|
+
# A composite list of items.
|
55
|
+
def reduce(functions, items)
|
56
|
+
functions = functions.collect do |function_text|
|
57
|
+
@context.parse_function(function_text, self)
|
58
|
+
end
|
59
|
+
|
60
|
+
keys, values = [], []
|
61
|
+
|
62
|
+
items.each do |value|
|
63
|
+
keys << value[0]
|
64
|
+
values << value[1]
|
65
|
+
end
|
66
|
+
|
67
|
+
result = functions.map do |function|
|
68
|
+
ReducingProcess.new(self, function).run(keys, values, false)
|
69
|
+
end
|
70
|
+
|
71
|
+
return [true, result]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Apply the rereduce functions to a given list of values.
|
75
|
+
# lambda{|keys,values,rereduce| ...}.call([], values, true)
|
76
|
+
#
|
77
|
+
# @param [Array] functions
|
78
|
+
# An array of functions to apply, in the form of:
|
79
|
+
# @param [Array] values
|
80
|
+
# An array of values to reduce
|
81
|
+
def rereduce(functions, values)
|
82
|
+
functions = functions.collect do |function_text|
|
83
|
+
@context.parse_function(function_text, self)
|
84
|
+
end
|
85
|
+
|
86
|
+
result = functions.map do |function|
|
87
|
+
ReducingProcess.new(self, function).run([], values, true)
|
88
|
+
end
|
89
|
+
|
90
|
+
return [true, result]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Copyright (c) 2012 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'json'
|
22
|
+
require 'logger'
|
23
|
+
|
24
|
+
module Relaxo
|
25
|
+
module QueryServer
|
26
|
+
# A simple wrapper that reads and writes objects using JSON serialization to the given `input` and `output` `IO`s.
|
27
|
+
class Shell
|
28
|
+
def initialize(input, output)
|
29
|
+
@input = input
|
30
|
+
@output = output
|
31
|
+
end
|
32
|
+
|
33
|
+
attr :input
|
34
|
+
attr :output
|
35
|
+
|
36
|
+
# Read a JSON serialised object from `input`.
|
37
|
+
def read_object
|
38
|
+
JSON.parse @input.readline
|
39
|
+
end
|
40
|
+
|
41
|
+
# Write a JSON serialized object to `output`.
|
42
|
+
def write_object object
|
43
|
+
@output.puts object.to_json
|
44
|
+
@output.flush
|
45
|
+
end
|
46
|
+
|
47
|
+
# Read commands from `input`, execute them and then write out the results.
|
48
|
+
def run
|
49
|
+
begin
|
50
|
+
while true
|
51
|
+
command = read_object
|
52
|
+
|
53
|
+
result = yield command
|
54
|
+
|
55
|
+
write_object(result)
|
56
|
+
end
|
57
|
+
rescue EOFError
|
58
|
+
# Finish...
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Used primarily for testing, allows the input and output of the server to be provided directly.
|
64
|
+
class MockShell < Shell
|
65
|
+
def initialize
|
66
|
+
super [], []
|
67
|
+
end
|
68
|
+
|
69
|
+
def read_object
|
70
|
+
if @input.size > 0
|
71
|
+
@input.shift
|
72
|
+
else
|
73
|
+
raise EOFError.new("No more objects")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def write_object object
|
78
|
+
@output << object
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Copyright (c) 2012 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module Relaxo
|
22
|
+
module QueryServer
|
23
|
+
module VERSION
|
24
|
+
MAJOR = 0
|
25
|
+
MINOR = 1
|
26
|
+
TINY = 0
|
27
|
+
|
28
|
+
STRING = [MAJOR, MINOR, TINY].join('.')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
$LOAD_PATH.unshift File.expand_path("../../lib/", __FILE__)
|
3
|
+
|
4
|
+
require 'test/unit'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
require "relaxo/query_server"
|
8
|
+
|
9
|
+
class ContextTest < Test::Unit::TestCase
|
10
|
+
def setup_context(options)
|
11
|
+
@shell = Relaxo::QueryServer::MockShell.new
|
12
|
+
@context = Relaxo::QueryServer::Context.new(@shell, options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_design_document name, attributes
|
16
|
+
@context.run ['ddoc', 'new', name, attributes]
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup
|
20
|
+
setup_context :safe => 0
|
21
|
+
end
|
22
|
+
|
23
|
+
def teardown
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
require 'context_test'
|
3
|
+
|
4
|
+
class FiltersTest < ContextTest
|
5
|
+
BASIC = "lambda{|doc, req| doc['good'] == true}"
|
6
|
+
|
7
|
+
def run_filter(opts={})
|
8
|
+
opts[:docs] ||= []
|
9
|
+
opts[:req] ||= {}
|
10
|
+
|
11
|
+
@context.run ["ddoc", "foo", ["filters", "basic"], [opts[:docs], opts[:req]]]
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_filters_updated
|
15
|
+
@context.run ["ddoc", "new", "foo", {"filters" => {"basic" => BASIC}}]
|
16
|
+
|
17
|
+
docs = (1..3).map do |i|
|
18
|
+
{"good" => i.odd?}
|
19
|
+
end
|
20
|
+
|
21
|
+
results = run_filter({:docs => docs})
|
22
|
+
assert_equal [true, [true, false, true]], results
|
23
|
+
end
|
24
|
+
end
|
data/test/test_lists.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
require 'context_test'
|
3
|
+
|
4
|
+
class ListsTest < ContextTest
|
5
|
+
ENTIRE = <<-RUBY
|
6
|
+
lambda{|head,request|
|
7
|
+
send "<ul>"
|
8
|
+
each do |row|
|
9
|
+
send "<li>" + row['count'] + "</li>"
|
10
|
+
end
|
11
|
+
send "</ul>"
|
12
|
+
}
|
13
|
+
RUBY
|
14
|
+
|
15
|
+
def setup
|
16
|
+
super
|
17
|
+
|
18
|
+
@rows = [
|
19
|
+
["list_row", {"count"=>"Neko"}],
|
20
|
+
["list_row", {"count"=>"Nezumi"}],
|
21
|
+
["list_row", {"count"=>"Zoe"}],
|
22
|
+
["list_end"]
|
23
|
+
]
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_lists_rows
|
27
|
+
create_design_document "test", {
|
28
|
+
'lists' => {
|
29
|
+
'entire' => ENTIRE
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
@rows.each {|row| @shell.input << row}
|
34
|
+
|
35
|
+
result = @context.run ['ddoc', 'test', ['lists', 'entire'], [{}, {}]]
|
36
|
+
|
37
|
+
assert_equal [
|
38
|
+
["start", ["<ul>"], {:headers=>{}}],
|
39
|
+
["chunks", ["<li>Neko</li>"]],
|
40
|
+
["chunks", ["<li>Nezumi</li>"]],
|
41
|
+
["chunks", ["<li>Zoe</li>"]]
|
42
|
+
], @shell.output
|
43
|
+
|
44
|
+
assert_equal ["end", ["</ul>"]], result
|
45
|
+
end
|
46
|
+
end
|
data/test/test_map.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
require 'context_test'
|
3
|
+
|
4
|
+
class MapTest < ContextTest
|
5
|
+
def test_map
|
6
|
+
response = @context.run ["add_fun", "lambda{|doc| emit('foo', doc['a']); emit('bar', doc['a'])}"]
|
7
|
+
assert_equal true, response
|
8
|
+
|
9
|
+
response = @context.run ["add_fun", "lambda{|doc| emit('baz', doc['a'])}"]
|
10
|
+
assert_equal true, response
|
11
|
+
|
12
|
+
response = @context.run(["map_doc", {"a" => "b"}])
|
13
|
+
expected = [
|
14
|
+
[["foo", "b"], ["bar", "b"]],
|
15
|
+
[["baz", "b"]]
|
16
|
+
]
|
17
|
+
|
18
|
+
assert_equal expected, response
|
19
|
+
end
|
20
|
+
end
|
data/test/test_reduce.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
require 'context_test'
|
3
|
+
|
4
|
+
class ReduceTest < ContextTest
|
5
|
+
SUM = "lambda{|k,v,r| v.inject &:+ }"
|
6
|
+
CONCAT = "lambda{|k,v,r| r ? v.join('_') : v.join(':') }"
|
7
|
+
|
8
|
+
def test_reduce
|
9
|
+
response = @context.run ["reduce", [SUM, CONCAT], (0...10).map{|i|[i,i*2]}]
|
10
|
+
assert_equal [true, [90, "0:2:4:6:8:10:12:14:16:18"]], response
|
11
|
+
|
12
|
+
response = @context.run ["rereduce", [SUM, CONCAT], (0...10).map{|i|i}]
|
13
|
+
assert_equal [true, [45, "0_1_2_3_4_5_6_7_8_9"]], response
|
14
|
+
end
|
15
|
+
end
|
data/test/test_shows.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
require 'context_test'
|
3
|
+
|
4
|
+
class ShowsTest < ContextTest
|
5
|
+
STRING = "lambda{|doc, req| [doc['title'], doc['body']].join(' - ') }"
|
6
|
+
HASH = <<-EOF
|
7
|
+
lambda{|doc, req|
|
8
|
+
resp = {"code" => 200, "headers" => {"X-Foo" => "Bar"}}
|
9
|
+
resp["body"] = [doc['title'], doc['body']].join(' - ')
|
10
|
+
resp
|
11
|
+
}
|
12
|
+
EOF
|
13
|
+
ERROR = "lambda{|doc,req| raise StandardError.new('error message') }"
|
14
|
+
|
15
|
+
def setup
|
16
|
+
super
|
17
|
+
|
18
|
+
@context.run ["ddoc", "new", "foo", {
|
19
|
+
"shows" => {
|
20
|
+
"string" => STRING,
|
21
|
+
"hash" => HASH,
|
22
|
+
"error" => ERROR
|
23
|
+
}
|
24
|
+
}
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
def run_show(opts={})
|
29
|
+
opts[:doc] ||= {"title" => "foo", "body" => "bar"}
|
30
|
+
opts[:req] ||= {}
|
31
|
+
opts[:design] ||= "string"
|
32
|
+
|
33
|
+
@context.run(["ddoc", "foo", ["shows", opts[:design]], [opts[:doc], opts[:req]]])
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_string
|
37
|
+
result = run_show
|
38
|
+
expected = ["resp", {"body" => "foo - bar"}]
|
39
|
+
assert_equal expected, result
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_hash
|
43
|
+
result = run_show({:design => "hash"})
|
44
|
+
expected = ["resp", {"body" => "foo - bar", "headers" => {"X-Foo" => "Bar"}, "code" => 200}]
|
45
|
+
assert_equal expected, result
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_error
|
49
|
+
result = run_show({:design => "error"})
|
50
|
+
expected = ["error", "StandardError", "error message"]
|
51
|
+
assert_equal expected, result[0...3]
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
require 'context_test'
|
3
|
+
|
4
|
+
class UpdatesTest < ContextTest
|
5
|
+
def test_compiles_functions
|
6
|
+
create_design_document "test", {
|
7
|
+
"updates" => {
|
8
|
+
"bar" => %q{
|
9
|
+
lambda{|doc, request| doc['updated'] = true; [doc, 'OK']}
|
10
|
+
}
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
document = {"foo" => "bar"}
|
15
|
+
response = @context.run ['ddoc', 'test', ['updates', 'bar'], [document, {'method' => 'POST'}]]
|
16
|
+
|
17
|
+
assert_equal ["up", document.update('updated' => true), {'body' => 'OK'}], response
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
require 'context_test'
|
3
|
+
|
4
|
+
class ValidationsTest < ContextTest
|
5
|
+
def test_compiles_functions
|
6
|
+
create_design_document "test", {
|
7
|
+
"validate_doc_update" => %q{
|
8
|
+
lambda{|new_document, old_document, user_context|
|
9
|
+
raise ValidationError.new('forbidden' => "bad") if new_document['bad']
|
10
|
+
|
11
|
+
true
|
12
|
+
}
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
response = @context.run ['ddoc', 'test', ['validate_doc_update'], [{'good' => true}, {'good' => true}, {}]]
|
17
|
+
assert_equal true, response
|
18
|
+
|
19
|
+
response = @context.run ['ddoc', 'test', ['validate_doc_update'], [{'bad' => true}, {'good' => true}, {}]]
|
20
|
+
assert_equal({'forbidden' => 'bad'}, response)
|
21
|
+
end
|
22
|
+
end
|
data/test/test_views.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
require 'context_test'
|
3
|
+
|
4
|
+
class ViewsTest < ContextTest
|
5
|
+
def test_compiles_functions
|
6
|
+
response = @context.run ["add_fun", "lambda {|doc| emit(nil, nil)}"]
|
7
|
+
assert_equal true, response
|
8
|
+
|
9
|
+
response = @context.run ["add_fun", "lambda {"]
|
10
|
+
assert_equal "error", response[0]
|
11
|
+
assert_equal "SyntaxError", response[1]
|
12
|
+
|
13
|
+
response = @context.run ["add_fun", "10"]
|
14
|
+
assert_equal "error", response[0]
|
15
|
+
assert_equal "Relaxo::QueryServer::CompilationError", response[1]
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: relaxo-query-server
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Samuel Williams
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-22 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: relaxo
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.3.1
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.3.1
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: json
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.7.3
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.7.3
|
46
|
+
description:
|
47
|
+
email: samuel.williams@oriontransfer.co.nz
|
48
|
+
executables:
|
49
|
+
- relaxo-query-server
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- bin/relaxo-query-server
|
54
|
+
- lib/relaxo/query_server/context.rb
|
55
|
+
- lib/relaxo/query_server/designer.rb
|
56
|
+
- lib/relaxo/query_server/mapper.rb
|
57
|
+
- lib/relaxo/query_server/process.rb
|
58
|
+
- lib/relaxo/query_server/reducer.rb
|
59
|
+
- lib/relaxo/query_server/shell.rb
|
60
|
+
- lib/relaxo/query_server/version.rb
|
61
|
+
- lib/relaxo/query_server.rb
|
62
|
+
- test/context_test.rb
|
63
|
+
- test/test_filters.rb
|
64
|
+
- test/test_lists.rb
|
65
|
+
- test/test_map.rb
|
66
|
+
- test/test_reduce.rb
|
67
|
+
- test/test_shows.rb
|
68
|
+
- test/test_updates.rb
|
69
|
+
- test/test_validations.rb
|
70
|
+
- test/test_views.rb
|
71
|
+
- README.md
|
72
|
+
homepage: http://www.oriontransfer.co.nz/gems/relaxo
|
73
|
+
licenses: []
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirements: []
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 1.8.23
|
93
|
+
signing_key:
|
94
|
+
specification_version: 3
|
95
|
+
summary: Relaxo Query Server provides support for executing CouchDB functions using
|
96
|
+
Ruby.
|
97
|
+
test_files: []
|
98
|
+
has_rdoc:
|