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 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
@@ -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
@@ -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
@@ -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
@@ -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: