ioquatix/relaxo-query-server

View on GitHub
lib/relaxo/query_server/designer.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) 2012 Samuel G. D. Williams. <http://www.oriontransfer.co.nz>
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

require 'relaxo/query_server/shell'
require 'relaxo/query_server/mapper'
require 'relaxo/query_server/reducer'

module Relaxo
    module QueryServer
        # Indicates that a validation error has occured.
        class ValidationError < StandardError
            def initialize(details)
                super "Validation failed!"
                
                @details = details
            end
            
            # The details of the validation error, typically:
            #     {:forbidden => "Message"}
            # or
            #     {:unauthorized => "Message"}
            attr :details
        end
        
        # Indicates that the request was invalid for some reason (e.g. lacking appropriate authentication)
        class InvalidRequestError < StandardError
            def initilize(message, code = 400, details = {})
                super message
                
                @code = 400
                @details = details
            end
            
            # Returns a response suitable for passing back to the client.
            def to_response
                @details.merge(:code => @code)
            end
        end
        
        # Supports the `list` action which consumes rows and outputs encoded text in chunks.
        class ListRenderer < Process
            def initialize(context, function)
                super(context, function)
                
                @started = false
                @fetched_row = false
                @start_response = {:headers => {}}
                @chunks = []
            end
            
            def run(head, request)
                super(head, request)
                
                # Ensure that at least one row is read from input:
                get_row unless @fetched_row
                
                return ["end", @chunks]
            end

            def send(chunk)
                @chunks << chunk
                false
            end

            def each
                while row = get_row
                    yield row
                end
            end

            def get_row
                flush
                
                row = @context.shell.read_object
                @fetched_row = true
                
                case command = row[0]
                when "list_row"
                    row[1]
                when "list_end"
                    false
                else
                    raise RuntimeError.new("Input is not a row!")
                end
            end

            def start(response)
                raise RuntimeError.new("List already started!") if @started
                
                @start_response = response
            end

            def flush
                if @started
                    @context.shell.write_object ["chunks", @chunks]
                else
                    @context.shell.write_object ["start", @chunks, @start_response]
                    
                    @started = true
                end
                
                @chunks = []
            end
        end
        
        # Represents a design document which includes a variety of functionality for processing documents.
        class DesignDocument
            VALIDATED = 1
            
            def initialize(context, name, attributes = {})
                @context = context

                @name = name
                @attributes = attributes
            end

            # Lookup the given key in the design document's attributes.
            def [] key
                @attributes[key]
            end

            # Runs the given function with the given arguments.
            def run(function, arguments)
                action = function[0]
                function = function_for(function)

                self.send(action, function, *arguments)
            end

            # Implements the `filters` action.
            def filters(function, documents, request)
                results = documents.map{|document| !!function.call(document, request)}

                return [true, results]
            end

            # Implements the `shows` action.
            def shows(function, document, request)
                response = function.call(document, request)

                return ["resp", wrap_response(response)]
            end

            # Implements the `updates` action.
            def updates(function, document, request)
                raise InvalidRequestError.new("Unsupported method #{request['method']}") unless request['method'] == 'POST'

                document, response = function.call(document, request)

                return ["up", document, wrap_response(response)]
            rescue InvalidRequestError => error
                return ["up", null, error.to_response]
            end

            # Implements the `lists` action.
            def lists(function, head, request)
                ListRenderer.new(@context, function).run(head, request)
            end

            # Implements the `validates_doc_update` action.
            def validates(function, new_document, old_document, user_context)
                Process.new(@context, function).run(new_document, old_document, user_context)
                
                # Unless ValidationError was raised, we are okay.
                return VALIDATED
            rescue ValidationError => error
                error.details
            end

            alias validate_doc_update validates

            # Ensures that the response is the correct form.
            def wrap_response(response)
                String === response ? {"body" => response} : response
            end

            # Looks a up a function given a key path into the design document.
            def function_for(path)
                parent = @attributes

                function = path.inject(parent) do |current, key|
                    parent = current

                    throw ArgumentError.new("Invalid function name #{path.join(".")}") unless current

                    current[key]
                end

                # Compile the function if required:
                if String === function
                    parent[path.last] = @context.parse_function(function, binding, 'design-document')
                else
                    function
                end
            end
        end
        
        # Implements the design document state and interface.
        class Designer
            def initialize(context)
                @context = context
                @documents = {}
            end
            
            # Create a new design document.
            #
            # @param [String] name
            #     The name of the design document.
            # @param [Hash] attributes
            #     The contents of the design document.
            def create(name, attributes)
                @documents[name] = DesignDocument.new(@context, name, attributes)
            end
            
            # Run a function on a given design document.
            #
            # @param [String] name
            #     The name of the design document.
            # @param [Array] function
            #     A key path to the function to execute.
            # @param [Array] arguments
            #     The arguments to provide to the function.
            def run(name, function, arguments)
                document = @documents[name]
                
                raise ArgumentError.new("Invalid document name #{name}") unless document
                
                document.run(function, arguments)
            end
        end
    end
end