lib/yard/server/commands/base.rb in yard-0.9.16 vs lib/yard/server/commands/base.rb in yard-0.9.17

- old
+ new

@@ -1,209 +1,209 @@ -# frozen_string_literal: true -require 'fileutils' - -module YARD - module Server - module Commands - # This is the base command class used to implement custom commands for - # a server. A command will be routed to by the {Router} class and return - # a Rack-style response. - # - # == Attribute Initializers - # All attributes can be initialized via options passed into the {#initialize} - # method. When creating a custom command, the {Adapter#options} will - # automatically be mapped to attributes by the same name on your class. - # - # class MyCommand < Base - # attr_accessor :myattr - # end - # - # Adapter.new(libs, {:myattr => 'foo'}).start - # - # # when a request comes in, cmd.myattr == 'foo' - # - # == Subclassing Notes - # To implement a custom command, override the {#run} method, not {#call}. - # In your implementation, you should set the body and status for requests. - # See details in the +#run+ method documentation. - # - # Note that if your command deals directly with libraries, you should - # consider subclassing the more specific {LibraryCommand} class instead. - # - # @abstract - # @see #run - class Base - # @group Basic Command and Adapter Options - - # @return [Hash] the options passed to the command's constructor - attr_accessor :command_options - - # @return [Adapter] the server adapter - attr_accessor :adapter - - # @return [Boolean] whether to cache - attr_accessor :caching - - # @group Attributes Set Per Request - - # @return [Rack::Request] request object - attr_accessor :request - - # @return [String] the path after the command base URI - attr_accessor :path - - # @return [Hash{String => String}] response headers - attr_accessor :headers - - # @return [Numeric] status code. Defaults to 200 per request - attr_accessor :status - - # @return [String] the response body. Defaults to empty string. - attr_accessor :body - - # @group Instance Method Summary - - # Creates a new command object, setting attributes named by keys - # in the options hash. After initialization, the options hash - # is saved in {#command_options} for further inspection. - # - # @example Creating a Command - # cmd = DisplayObjectCommand.new(:caching => true, :library => mylib) - # cmd.library # => mylib - # cmd.command_options # => {:caching => true, :library => mylib} - # @param [Hash] opts the options hash, saved to {#command_options} - # after initialization. - def initialize(opts = {}) - opts.each do |key, value| - send("#{key}=", value) if respond_to?("#{key}=") - end - self.command_options = opts - end - - # The main method called by a router with a request object. - # - # @note This command should not be overridden by subclasses. Implement - # the callback method {#run} instead. - # @param [Adapter Dependent] request the request object - # @return [Array(Numeric,Hash,Array<String>)] a Rack-style response - # of status, headers, and body wrapped in an array. - def call(request) - self.request = request - self.path ||= request.path_info[1..-1] - self.headers = {'Content-Type' => 'text/html'} - self.body = '' - self.status = 200 - add_cache_control - begin - run - rescue FinishRequest - nil # noop - rescue NotFoundError => e - self.body = e.message if e.message != e.class.to_s - not_found - end - - # keep this to support commands setting status manually. - not_found if status == 404 - - [status, headers, body.is_a?(Array) ? body : [body]] - end - - # @group Abstract Methods - - # Subclass this method to implement a custom command. This method - # should set the {#status} and {#body}, and optionally modify the - # {#headers}. Note that +#status+ defaults to 200. - # - # @example A custom command - # class ErrorCommand < Base - # def run - # self.body = 'ERROR! The System is down!' - # self.status = 500 - # self.headers['Conten-Type'] = 'text/plain' - # end - # end - # - # @abstract - # @return [void] - def run - raise NotImplementedError - end - - protected - - # @group Helper Methods - - # Renders a specific object if provided, or a regular template rendering - # if object is not provided. - # - # @todo This method is dependent on +#options+, it should be in {LibraryCommand}. - # @param [CodeObjects::Base, nil] object calls {CodeObjects::Base#format} if - # an object is provided, or {Templates::Engine.render} if object is nil. Both - # receive +#options+ as an argument. - # @return [String] the resulting output to display - def render(object = nil) - case object - when CodeObjects::Base - cache object.format(options) - when nil - cache Templates::Engine.render(options) - else - cache object - end - end - - # Override this method to implement custom caching mechanisms for - # - # @example Caching to memory - # $memory_cache = {} - # def cache(data) - # $memory_cache[path] = data - # end - # @param [String] data the data to cache - # @return [String] the same cached data (for chaining) - # @see StaticCaching - def cache(data) - if caching && adapter.document_root - path = File.join(adapter.document_root, request.path_info.sub(/\.html$/, '') + '.html') - path = path.sub(%r{/\.html$}, '.html') - FileUtils.mkdir_p(File.dirname(path)) - log.debug "Caching data to #{path}" - File.open(path, 'wb') {|f| f.write(data) } - end - self.body = data - end - - # Sets the body and headers for a 404 response. Does not modify the - # body if already set. - # - # @return [void] - def not_found - self.status = 404 - return unless body.empty? - self.body = "Not found: #{request.path}" - headers['Content-Type'] = 'text/plain' - headers['X-Cascade'] = 'pass' - headers.delete('Cache-Control') - end - - # Sets the headers and status code for a redirection to a given URL - # @param [String] url the URL to redirect to - # @raise [FinishRequest] causes the request to terminate. - def redirect(url) - headers['Location'] = url - self.status = 302 - raise FinishRequest - end - - private - - # Add a conservative cache control policy to reduce load on - # requests served with "?1234567890" style timestamp query strings. - def add_cache_control - return if request.query_string.to_i == 0 - headers['Cache-Control'] = 'private, max-age=300' - end - end - end - end -end +# frozen_string_literal: true +require 'fileutils' + +module YARD + module Server + module Commands + # This is the base command class used to implement custom commands for + # a server. A command will be routed to by the {Router} class and return + # a Rack-style response. + # + # == Attribute Initializers + # All attributes can be initialized via options passed into the {#initialize} + # method. When creating a custom command, the {Adapter#options} will + # automatically be mapped to attributes by the same name on your class. + # + # class MyCommand < Base + # attr_accessor :myattr + # end + # + # Adapter.new(libs, {:myattr => 'foo'}).start + # + # # when a request comes in, cmd.myattr == 'foo' + # + # == Subclassing Notes + # To implement a custom command, override the {#run} method, not {#call}. + # In your implementation, you should set the body and status for requests. + # See details in the +#run+ method documentation. + # + # Note that if your command deals directly with libraries, you should + # consider subclassing the more specific {LibraryCommand} class instead. + # + # @abstract + # @see #run + class Base + # @group Basic Command and Adapter Options + + # @return [Hash] the options passed to the command's constructor + attr_accessor :command_options + + # @return [Adapter] the server adapter + attr_accessor :adapter + + # @return [Boolean] whether to cache + attr_accessor :caching + + # @group Attributes Set Per Request + + # @return [Rack::Request] request object + attr_accessor :request + + # @return [String] the path after the command base URI + attr_accessor :path + + # @return [Hash{String => String}] response headers + attr_accessor :headers + + # @return [Numeric] status code. Defaults to 200 per request + attr_accessor :status + + # @return [String] the response body. Defaults to empty string. + attr_accessor :body + + # @group Instance Method Summary + + # Creates a new command object, setting attributes named by keys + # in the options hash. After initialization, the options hash + # is saved in {#command_options} for further inspection. + # + # @example Creating a Command + # cmd = DisplayObjectCommand.new(:caching => true, :library => mylib) + # cmd.library # => mylib + # cmd.command_options # => {:caching => true, :library => mylib} + # @param [Hash] opts the options hash, saved to {#command_options} + # after initialization. + def initialize(opts = {}) + opts.each do |key, value| + send("#{key}=", value) if respond_to?("#{key}=") + end + self.command_options = opts + end + + # The main method called by a router with a request object. + # + # @note This command should not be overridden by subclasses. Implement + # the callback method {#run} instead. + # @param [Adapter Dependent] request the request object + # @return [Array(Numeric,Hash,Array<String>)] a Rack-style response + # of status, headers, and body wrapped in an array. + def call(request) + self.request = request + self.path ||= request.path_info[1..-1] + self.headers = {'Content-Type' => 'text/html'} + self.body = '' + self.status = 200 + add_cache_control + begin + run + rescue FinishRequest + nil # noop + rescue NotFoundError => e + self.body = e.message if e.message != e.class.to_s + not_found + end + + # keep this to support commands setting status manually. + not_found if status == 404 + + [status, headers, body.is_a?(Array) ? body : [body]] + end + + # @group Abstract Methods + + # Subclass this method to implement a custom command. This method + # should set the {#status} and {#body}, and optionally modify the + # {#headers}. Note that +#status+ defaults to 200. + # + # @example A custom command + # class ErrorCommand < Base + # def run + # self.body = 'ERROR! The System is down!' + # self.status = 500 + # self.headers['Conten-Type'] = 'text/plain' + # end + # end + # + # @abstract + # @return [void] + def run + raise NotImplementedError + end + + protected + + # @group Helper Methods + + # Renders a specific object if provided, or a regular template rendering + # if object is not provided. + # + # @todo This method is dependent on +#options+, it should be in {LibraryCommand}. + # @param [CodeObjects::Base, nil] object calls {CodeObjects::Base#format} if + # an object is provided, or {Templates::Engine.render} if object is nil. Both + # receive +#options+ as an argument. + # @return [String] the resulting output to display + def render(object = nil) + case object + when CodeObjects::Base + cache object.format(options) + when nil + cache Templates::Engine.render(options) + else + cache object + end + end + + # Override this method to implement custom caching mechanisms for + # + # @example Caching to memory + # $memory_cache = {} + # def cache(data) + # $memory_cache[path] = data + # end + # @param [String] data the data to cache + # @return [String] the same cached data (for chaining) + # @see StaticCaching + def cache(data) + if caching && adapter.document_root + path = File.join(adapter.document_root, request.path_info.sub(/\.html$/, '') + '.html') + path = path.sub(%r{/\.html$}, '.html') + FileUtils.mkdir_p(File.dirname(path)) + log.debug "Caching data to #{path}" + File.open(path, 'wb') {|f| f.write(data) } + end + self.body = data + end + + # Sets the body and headers for a 404 response. Does not modify the + # body if already set. + # + # @return [void] + def not_found + self.status = 404 + return unless body.empty? + self.body = "Not found: #{request.path}" + headers['Content-Type'] = 'text/plain' + headers['X-Cascade'] = 'pass' + headers.delete('Cache-Control') + end + + # Sets the headers and status code for a redirection to a given URL + # @param [String] url the URL to redirect to + # @raise [FinishRequest] causes the request to terminate. + def redirect(url) + headers['Location'] = url + self.status = 302 + raise FinishRequest + end + + private + + # Add a conservative cache control policy to reduce load on + # requests served with "?1234567890" style timestamp query strings. + def add_cache_control + return if request.query_string.to_i == 0 + headers['Cache-Control'] = 'private, max-age=300' + end + end + end + end +end