relaxo-query-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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:
|