lib/httpimagestore/configuration/handler.rb in httpimagestore-1.8.1 vs lib/httpimagestore/configuration/handler.rb in httpimagestore-1.9.0

- old
+ new

@@ -1,8 +1,7 @@ -require 'mime/types' -require 'digest/sha2' -require 'securerandom' +require 'httpimagestore/configuration/request_state' +require 'httpimagestore/ruby_string_template' module Configuration class ImageNotLoadedError < ConfigurationError def initialize(image_name) super "image '#{image_name}' not loaded" @@ -37,137 +36,27 @@ def initialize(image_name, meta_value) super "image '#{image_name}' does not have data for variable '#{meta_value}'" end end - class RequestState < Hash - include ClassLogging - - class Images < Hash - def initialize(memory_limit) - @memory_limit = memory_limit - super - end - - def []=(name, image) - if member?(name) - @memory_limit.return fetch(name).data.bytesize - end - super - end - - def [](name) - fetch(name){|image_name| raise ImageNotLoadedError.new(image_name)} - end - end - - def initialize(body = '', matches = {}, path = '', query_string = {}, memory_limit = MemoryLimit.new, headers = {}) - super() do |request_state, name| - # note that request_state may be different object when useing with_locals that creates duplicate - request_state[name] = request_state.generate_meta_variable(name) or raise VariableNotDefinedError.new(name) - end - - self[:path] = path - merge! matches - self[:query_string_options] = query_string.sort.map{|kv| kv.join(':')}.join(',') - - log.debug "processing request with body length: #{body.bytesize} bytes and variables: #{map{|k,v| "#{k}: '#{v}'"}.join(', ')}" - - @body = body - @images = Images.new(memory_limit) - @memory_limit = memory_limit - @output_callback = nil - - @headers = headers - end - - attr_reader :body - attr_reader :images - attr_reader :memory_limit - attr_reader :headers - - def with_locals(*locals) - locals = locals.reduce{|a, b| a.merge(b)} - log.debug "using additional local variables: #{locals}" - self.dup.merge!(locals) - end - - def output(&callback) - @output_callback = callback - end - - def output_callback - @output_callback or fail 'no output callback' - end - - def fetch_base_variable(name, base_name) - fetch(base_name, nil) or generate_meta_variable(base_name) or raise NoVariableToGenerateMetaVariableError.new(base_name, name) - end - - def generate_meta_variable(name) - log.debug "generating meta variable: #{name}" - val = case name - when :basename - path = Pathname.new(fetch_base_variable(name, :path)) - path.basename(path.extname).to_s - when :dirname - Pathname.new(fetch_base_variable(name, :path)).dirname.to_s - when :extension - Pathname.new(fetch_base_variable(name, :path)).extname.delete('.') - when :digest # deprecated - @body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name) - Digest::SHA2.new.update(@body).to_s[0,16] - when :input_digest - @body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name) - Digest::SHA2.new.update(@body).to_s[0,16] - when :input_sha256 - @body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name) - Digest::SHA2.new.update(@body).to_s - when :input_image_width - @images['input'].width or raise NoImageDataForVariableError.new('input', name) - when :input_image_height - @images['input'].height or raise NoImageDataForVariableError.new('input', name) - when :input_image_mime_extension - @images['input'].mime_extension or raise NoImageDataForVariableError.new('input', name) - when :image_digest - Digest::SHA2.new.update(@images[fetch_base_variable(name, :image_name)].data).to_s[0,16] - when :image_sha256 - Digest::SHA2.new.update(@images[fetch_base_variable(name, :image_name)].data).to_s - when :mimeextension # deprecated - image_name = fetch_base_variable(name, :image_name) - @images[image_name].mime_extension or raise NoImageDataForVariableError.new(image_name, name) - when :image_mime_extension - image_name = fetch_base_variable(name, :image_name) - @images[image_name].mime_extension or raise NoImageDataForVariableError.new(image_name, name) - when :image_width - image_name = fetch_base_variable(name, :image_name) - @images[image_name].width or raise NoImageDataForVariableError.new(image_name, name) - when :image_height - image_name = fetch_base_variable(name, :image_name) - @images[image_name].height or raise NoImageDataForVariableError.new(image_name, name) - when :uuid - SecureRandom.uuid - end - if val - log.debug "generated meta variable '#{name}': #{val}" - else - log.debug "could not generated meta variable '#{name}'" - end - val - end - end - module ImageMetaData attr_accessor :source_path attr_accessor :source_url attr_accessor :store_path attr_accessor :store_url def mime_extension - return nil unless mime_type - mime = MIME::Types[mime_type].first - mime.extensions.select{|e| e.length == 3}.first or mime.extensions.first + case mime_type + when nil then nil + when 'image/jpeg' then 'jpg' + when 'image/png' then 'png' + when 'image/gif' then 'gif' + else + # TODO: this does not work well; the resoult may not the most common extension (like 'jpeg') + mime = MIME::Types[mime_type].first or return nil + mime.preferred_extension + end end end class Image < Struct.new(:data, :mime_type, :width, :height) include ImageMetaData @@ -178,144 +67,61 @@ request_state.body.empty? and raise ZeroBodyLengthError request_state.images['input'] = Image.new(request_state.body) end end - class InclusionMatcher - def initialize(value, template) - @value = value - @template = RubyStringTemplate.new(template) if template - end - - def included?(request_state) - return true if not @template - @template.render(request_state).split(',').include? @value - end - end - - class HandlerStatement - module ImageName - attr_reader :image_name - - def initialize(global, *args) - @image_name = args.pop - - super(global, *args) - - config_local :imagename, @image_name # deprecated - config_local :image_name, @image_name - end - end - - module PathSpec - attr_reader :path_spec - - def initialize(global, *args) - @path_spec = args.pop - super(global, *args) - end - - def path_template - @global.paths[@path_spec] - end - end - - module ConditionalInclusion - def initialize(global, *args) - @matchers = [] - matcher = args.pop - @matchers << matcher if matcher - super(global, *args) - end - - def inclusion_matcher(matcher) - @matchers << matcher - end - - def included?(request_state) - return true if @matchers.empty? - @matchers.any? do |matcher| - matcher.included?(request_state) - end - end - - def excluded?(request_state) - not included? request_state - end - end - - def initialize(global, *args) - @global = global - @config_locals = {} - @module_args = args - end - - attr_reader :config_locals - def config_local(name, value) - @config_locals[name] = value - end - - def path_template(path_spec) - @global.paths[path_spec] - end - end - - class SourceStoreBase < HandlerStatement - include ImageName - include PathSpec - include ConditionalInclusion - - def initialize(global, image_name, matcher, path_spec) - super(global, image_name, path_spec, matcher) - end - - private - - def put_sourced_named_image(request_state) - rendered_path = path_template.render(request_state.with_locals(config_locals)) - - image = yield @image_name, rendered_path - - image.source_path = rendered_path - request_state.images[@image_name] = image - end - - def get_named_image_for_storage(request_state) - image = request_state.images[@image_name] - rendered_path = path_template.render(request_state.with_locals(config_locals)) - image.store_path = rendered_path - - yield @image_name, image, rendered_path - end - end - class Matcher def initialize(names, debug_type = '', debug_value = '', &matcher) @names = names @matcher = matcher @debug_type = debug_type @debug_value = case debug_value when Regexp "/#{debug_value.source}/" + when nil + nil else - debug_value.inspect + debug_value.to_s end end attr_reader :names attr_reader :matcher def to_s - if @names.empty? - "#{@debug_type}(#{@debug_value})" + if @debug_value + if @names.empty? + "#{@debug_type}(#{@debug_value})" + else + "#{@debug_type}(#{@names.join(',')} => #{@debug_value})" + end else - "#{@debug_type}(#{@names.join(',')} => #{@debug_value})" + @debug_type end end end class Handler < Scope + class HandlerConfiguration + def initialize(global, http_method, uri_matchers) + @global = global + @http_method = http_method + @uri_matchers = uri_matchers + @validators = [] + @sources = [] + @processors = [] + @stores = [] + @output = nil + end + + attr_accessor :global, :http_method, :uri_matchers, :validators, :sources, :processors, :stores, :output + + def to_s + "#{@http_method} #{@uri_matchers.join(', ')}" + end + end + def self.match(node) node.name == 'put' or node.name == 'post' or node.name == 'get' end @@ -323,24 +129,11 @@ def self.pre(configuration) configuration.handlers ||= [] end def self.parse(configuration, node) - handler_configuration = - Struct.new( - :global, - :http_method, - :uri_matchers, - :sources, - :processors, - :stores, - :output - ).new - - handler_configuration.global = configuration - handler_configuration.http_method = node.name - handler_configuration.uri_matchers = node.values.map do |matcher| + uri_matchers = node.values.map do |matcher| case matcher # URI matchers when %r{^:([^/]+)/(.*)/$} # :foobar/.*/ name = $1.to_sym _regexp = Regexp.new($2) @@ -379,36 +172,34 @@ end # Query string matchers when /^\&([^=]+)=(.+)$/# ?foo=bar name = $1.to_sym value = $2 - Matcher.new([name], 'QueryKeyValue', "#{name}=#{value}") do + Matcher.new([name], 'QueryKeyValue', "#{value}") do ->{req.GET[name.to_s] && req.GET[name.to_s] == value && captures.push(req.GET[name.to_s])} end when /^\&:(.+)\?(.*)$/# &:foo?bar name = $1.to_sym default = $2 - Matcher.new([name], 'QueryKeyDefault', "#{name}=<key>|#{default}") do + Matcher.new([name], 'QueryKeyDefault', "<key>|#{default}") do ->{captures.push(req.GET[name.to_s] || default)} end when /^\&:(.+)$/# &:foo name = $1.to_sym - Matcher.new([name], 'QueryKey', "#{name}=<key>") do + Matcher.new([name], 'QueryKey', "<key>") do ->{req.GET[name.to_s] && captures.push(req.GET[name.to_s])} end # Literal URI segment matcher else # foobar - Matcher.new([], "Literal", matcher) do + Matcher.new([], matcher, nil) do Regexp.escape(matcher) end end end - handler_configuration.sources = [] - handler_configuration.processors = [] - handler_configuration.stores = [] - handler_configuration.output = nil + handler_configuration = HandlerConfiguration.new(configuration, node.name, uri_matchers) + node.grab_attributes if handler_configuration.http_method != 'get' handler_configuration.sources << InputSource.new end @@ -424,8 +215,56 @@ log.warn 'no handlers configured' if configuration.handlers.empty? end end RequestState.logger = Global.logger_for(RequestState) + class OutputText < Scope + def self.match(node) + node.name == 'output_text' + end + + def self.parse(configuration, node) + configuration.output and raise StatementCollisionError.new(node, 'output') + text = node.grab_values('text').first + status, cache_control = *node.grab_attributes('status', 'cache-control') + configuration.output = OutputText.new(text, status || 200, cache_control) + end + + def initialize(text, status, cache_control) + @text = RubyStringTemplate.new(text || fail("no text?!")) + @status = status || 200 + @cache_control = cache_control + end + + def realize(request_state) + # make sure variables are available in request context + status = @status + text = @text.render(request_state) + cache_control = @cache_control + request_state.output do + res['Cache-Control'] = cache_control if cache_control + write_plain status.to_i, text.to_s + end + end + end + + class OutputOK < OutputText + def self.match(node) + node.name == 'output_ok' + end + + def self.parse(configuration, node) + configuration.output and raise StatementCollisionError.new(node, 'output') + cache_control = node.grab_attributes('cache-control').first + configuration.output = OutputOK.new(cache_control) + end + + def initialize(cache_control = nil) + super 'OK', 200, cache_control + end + end + Global.register_node_parser Handler + Handler::register_node_parser OutputText + Handler::register_node_parser OutputOK end