module Puuko Response = Struct.new(:status, :headers, :body) class Endpoint class InvalidCallbackKind < StandardError; end CALLBACK_KINDS = [ :before, :after, :rescue ] DEFAULT_HEADERS = { "Content-Type" => "application/json" } class << self def registered_callbacks @registered_callbacks ||= begin hash = {} CALLBACK_KINDS.each do |kind| hash[kind] = [] if self.superclass.respond_to?(:registered_callbacks) superclass.registered_callbacks[kind].each { |cb| hash[kind] << cb } end end hash end Hash[@registered_callbacks] end def register_callback(kind, &block) unless Puuko::Endpoint::CALLBACK_KINDS.include?(kind) raise InvalidCallbackKind, "Received invalid callback kind: #{kind}" end registered_callbacks[kind] << block if block_given? end def register_rescue_callback(exception_class, &block) register_callback(:rescue) do |e| self.instance_exec(&block) if e.is_a?(exception_class) end end def register_before_callback(&block) register_callback(:before, &block) end def register_after_callback(&block) register_callback(:after, &block) end end attr_reader :request attr_reader :params attr_reader :response def initialize(request, params) @request = request @params = params @response = Puuko::Response.new(500, DEFAULT_HEADERS, {}) @halted = false end def execute result = nil begin execute_callbacks(:before) result = handle unless halted? execute_callbacks(:after) rescue RuntimeError, StandardError => e execute_callbacks(:rescue, e) raise e unless halted? end result end def body @json ||= read_request_body do |data| JSON.parse(data, symbolize_names: false) rescue JSON::ParserError {} end end protected def halt!(status: 200, headers: DEFAULT_HEADERS, body: {}) @response = Puuko::Response.new(status, DEFAULT_HEADERS.merge(headers), body) @halted = true end protected def halted? @halted end protected def redirect_to(location) render status: 302, headers: { "Location" => location } end protected def render(status: 200, headers: DEFAULT_HEADERS, body: {}) @response = Puuko::Response.new(status, DEFAULT_HEADERS.merge(headers), body) end private def execute_callbacks(kind, *args) return if halted? unless Puuko::Endpoint::CALLBACK_KINDS.include?(kind) raise InvalidCallbackKind, "Received invalid callback kind: #{kind}" end self.class.registered_callbacks[kind].each do |callback| self.instance_exec(*args, &callback) break if halted? end end private def read_request_body request.body.rewind # in case someone already read it result = yield(request.body.read) request.body.rewind # to allow others read it if they want result end end end