diff --git a/.rubocop.yml b/.rubocop.yml index ed0d2c1..67af5e2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,3 +12,6 @@ Metrics/BlockLength: - Rakefile ExcludedMethods: - route + +Naming/RescuedExceptionsVariableName: + PreferredName: error diff --git a/app.rb b/app.rb index c1c2b61..e4a56ac 100644 --- a/app.rb +++ b/app.rb @@ -45,23 +45,50 @@ class App < Roda 'X-XSS-Protection' => '1; mode=block' plugin :error_handler do |error| + handle_error(error) + end + + plugin :public + plugin :render, escape: true, layout: 'layout' + plugin :typecast_params + plugin :basic_auth + + route do |r| + path = RequestPath.new(request) + + r.root { view 'index' } + + r.public + + r.get 'health_check.txt' do + handle_health_check + end + + r.on String do |config_name_with_ext| + handle_local_config_feeds(path.full_config_name, config_name_with_ext) + end + + r.on String, String do |folder_name, config_name_with_ext| + handle_html2rss_configs(path.full_config_name, folder_name, config_name_with_ext) + end + end + + private + + def handle_error(error) # rubocop:disable Metrics/MethodLength case error when Html2rss::Config::ParamsMissing, Roda::RodaPlugins::TypecastParams::Error - @page_title = 'Parameters missing or invalid' - response.status = 422 + set_error_response('Parameters missing or invalid', 422) when Html2rss::AttributePostProcessors::UnknownPostProcessorName, Html2rss::ItemExtractors::UnknownExtractorName, Html2rss::Config::ChannelMissing - @page_title = 'Invalid feed config' - response.status = 422 + set_error_response('Invalid feed config', 422) when ::App::LocalConfig::NotFound, Html2rss::Configs::ConfigNotFound - @page_title = 'Feed config not found' - response.status = 404 + set_error_response('Feed config not found', 404) else - @page_title = 'Internal Server Error' - response.status = 500 + set_error_response('Internal Server Error', 500) end @show_backtrace = ENV.fetch('RACK_ENV', nil) == 'development' @@ -69,46 +96,32 @@ class App < Roda view 'error' end - plugin :public - plugin :render, escape: true, layout: 'layout' - plugin :typecast_params - plugin :basic_auth - - route do |r| - path = RequestPath.new(request) - - r.root do - view 'index' - end - - r.public + def set_error_response(page_title, status) + @page_title = page_title + response.status = status + end - r.get 'health_check.txt' do |_| - HttpCache.expires_now(response) + def handle_health_check + HttpCache.expires_now(response) - with_basic_auth(realm: HealthCheck, - username: HealthCheck::Auth.username, - password: HealthCheck::Auth.password) do - HealthCheck.run - end + with_basic_auth(realm: HealthCheck, + username: HealthCheck::Auth.username, + password: HealthCheck::Auth.password) do + HealthCheck.run end + end - # Route for feeds from the local feeds.yml - r.get String do |_config_name_with_ext| - Html2rssFacade.from_local_config(path.full_config_name, typecast_params) do |config| - response['Content-Type'] = 'text/xml' - - HttpCache.expires(response, config.ttl * 60, cache_control: 'public') - end + def handle_local_config_feeds(full_config_name, _config_name_with_ext) + Html2rssFacade.from_local_config(full_config_name, typecast_params) do |config| + response['Content-Type'] = 'text/xml' + HttpCache.expires(response, config.ttl * 60, cache_control: 'public') end + end - # Route for feeds from html2rss-configs - r.get String, String do |_folder_name, _config_name_with_ext| - Html2rssFacade.from_config(path.full_config_name, typecast_params) do |config| - response['Content-Type'] = 'text/xml' - - HttpCache.expires(response, config.ttl * 60, cache_control: 'public') - end + def handle_html2rss_configs(full_config_name, _folder_name, _config_name_with_ext) + Html2rssFacade.from_config(full_config_name, typecast_params) do |config| + response['Content-Type'] = 'text/xml' + HttpCache.expires(response, config.ttl * 60, cache_control: 'public') end end end diff --git a/app/health_check.rb b/app/health_check.rb index f0c5609..3ffc276 100644 --- a/app/health_check.rb +++ b/app/health_check.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true require 'parallel' - require_relative 'local_config' +require 'securerandom' module App ## @@ -11,18 +11,22 @@ module HealthCheck ## # Contains logic to obtain username and password to be used with HealthCheck endpoint. class Auth - def self.username - @username ||= ENV.delete('HEALTH_CHECK_USERNAME') do - SecureRandom.base64(32).tap do |string| - puts "HEALTH_CHECK_USERNAME env var. missing! Please set it. Using generated value instead: #{string}" - end + class << self + def username + @username ||= fetch_credential('HEALTH_CHECK_USERNAME') end - end - def self.password - @password ||= ENV.delete('HEALTH_CHECK_PASSWORD') do - SecureRandom.base64(32).tap do |string| - puts "HEALTH_CHECK_PASSWORD env var. missing! Please set it. Using generated value instead: #{string}" + def password + @password ||= fetch_credential('HEALTH_CHECK_PASSWORD') + end + + private + + def fetch_credential(env_var) + ENV.delete(env_var) do + SecureRandom.base64(32).tap do |string| + warn "ENV var. #{env_var} missing! Using generated value instead: #{string}" + end end end end @@ -34,12 +38,7 @@ def self.password # @return [String] "success" when all checks passed. def run broken_feeds = errors - - if broken_feeds.any? - broken_feeds.join("\n") - else - 'success' - end + broken_feeds.any? ? broken_feeds.join("\n") : 'success' end ## @@ -48,10 +47,16 @@ def errors [].tap do |errors| Parallel.each(LocalConfig.feed_names, in_threads: 4) do |feed_name| Html2rss.feed_from_yaml_config(LocalConfig::CONFIG_FILE, feed_name.to_s).to_s - rescue StandardError => e - errors << "[#{feed_name}] #{e.class}: #{e.message}" + rescue StandardError => error + errors << "[#{feed_name}] #{error.class}: #{error.message}" end end end + + def format_error(feed_name, error) + "[#{feed_name}] #{error.class}: #{error.message}" + end + + private_class_method :format_error end end diff --git a/app/html2rss_facade.rb b/app/html2rss_facade.rb index 12fd4b2..da4164b 100644 --- a/app/html2rss_facade.rb +++ b/app/html2rss_facade.rb @@ -15,7 +15,7 @@ class Html2rssFacade ## # @param feed_config [Hash] - # @param typecast_params + # @param typecast_params [Object] def initialize(feed_config, typecast_params) @feed_config = feed_config @typecast_params = typecast_params @@ -23,21 +23,19 @@ def initialize(feed_config, typecast_params) ## # @param name [String] the name of a html2rss-configs provided config. - # @param typecast_params - # @return [String] the serializied RSS feed + # @param typecast_params [Object] + # @return [String] the serialized RSS feed def self.from_config(name, typecast_params, &) feed_config = Html2rss::Configs.find_by_name(name) - new(feed_config, typecast_params).feed(&) end ## # @param name [String] the name of a feed in the file `config/feeds.yml` - # @param typecast_params - # @return [String] the serializied RSS feed + # @param typecast_params [Object] + # @return [String] the serialized RSS feed def self.from_local_config(name, typecast_params, &) - feed_config = LocalConfig.find name - + feed_config = LocalConfig.find(name) new(feed_config, typecast_params).feed(&) end @@ -45,19 +43,19 @@ def self.from_local_config(name, typecast_params, &) # @return [String] def feed config = self.class.feed_config_to_config(feed_config, typecast_params) - yield config if block_given? - Html2rss.feed(config).to_s end ## + # @param feed_config [Hash] + # @param typecast_params [Object] + # @param global_config [Hash] # @return [Html2rss::Config] # @raise [Roda::RodaPlugins::TypecastParams::Error] def self.feed_config_to_config(feed_config, typecast_params, global_config: LocalConfig.global) dynamic_params = Html2rss::Config::Channel.required_params_for_config(feed_config[:channel]) .to_h { |name| [name, typecast_params.str!(name)] } - Html2rss::Config.new(feed_config, global_config, dynamic_params) end end diff --git a/app/http_cache.rb b/app/http_cache.rb index cafa775..ceefc6f 100644 --- a/app/http_cache.rb +++ b/app/http_cache.rb @@ -10,23 +10,21 @@ module HttpCache ## # Sets Expires and Cache-Control headers to cache for `seconds`. - # @param response [#[]] + # @param response [Hash] # @param seconds [Integer] - # @param cache_control [String] + # @param cache_control [String, nil] def expires(response, seconds, cache_control: nil) response['Expires'] = (Time.now + seconds).httpdate - response['Cache-Control'] = if cache_control - "max-age=#{seconds},#{cache_control}" - else - "max-age=#{seconds}" - end + cache_value = "max-age=#{seconds}" + cache_value += ",#{cache_control}" if cache_control + response['Cache-Control'] = cache_value end ## # Sets Expires and Cache-Control headers to invalidate existing cache and # prevent caching. - # @param response [#[]] + # @param response [Hash] def expires_now(response) response['Expires'] = '0' response['Cache-Control'] = 'private,max-age=0,no-cache,no-store,must-revalidate' diff --git a/app/local_config.rb b/app/local_config.rb index 0296460..ac71c11 100644 --- a/app/local_config.rb +++ b/app/local_config.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'yaml' + module App ## # Provides helper methods to deal with the local config file at `CONFIG_FILE`. @@ -15,19 +16,19 @@ class NotFound < RuntimeError; end ## # @param name [String, Symbol, #to_sym] - # @return [Hash] + # @return [Hash] def find(name) - feeds&.fetch(name.to_sym, false) || raise(NotFound, "Did not find local feed config at '#{name}'") + feeds.fetch(name.to_sym) { raise NotFound, "Did not find local feed config at '#{name}'" } end ## - # @return [Hash] + # @return [Hash] def feeds - yaml[:feeds] || {} + yaml.fetch(:feeds, {}) end ## - # @return [Hash] + # @return [Hash] def global yaml.reject { |key| key == :feeds } end @@ -39,9 +40,11 @@ def feed_names end ## - # @return [Hash] + # @return [Hash] def yaml - YAML.safe_load(File.open(CONFIG_FILE), symbolize_names: true).freeze + YAML.safe_load_file(CONFIG_FILE, symbolize_names: true).freeze + rescue Errno::ENOENT => error + raise NotFound, "Configuration file not found: #{error.message}" end end end diff --git a/app/request_path.rb b/app/request_path.rb index 6d75047..db6fcf0 100644 --- a/app/request_path.rb +++ b/app/request_path.rb @@ -10,11 +10,11 @@ class RequestPath # @param request [Rack::Request, #path] def initialize(request) @full_path = request.path[1..] + parts = @full_path.split('/') - if @full_path.count('/').zero? + if parts.size == 1 @name_with_ext = @full_path else - parts = @full_path.split('/') @folder_name = parts[0..-2] @name_with_ext = parts[-1] end @@ -23,13 +23,13 @@ def initialize(request) ## # @return [String] def full_config_name - [folder_name, config_name].compact.join('/') + [@folder_name, config_name].compact.join('/') end ## # @return [String] def config_name - parts[...-1].join('.') + parts[..-2].join('.') end ##