class Ravioli::Builder

</td><td>

`ENV`

</td><td>

`ENV`

</td></tr><tr><td>

`config/credentials/staging.yml.enc` (only if running on staging)

</td><td>

`ENV`

</td><td>

`ENV`

</td></tr></tbody></table>

Constants

ENV_KEYS
EXTNAMES

Attributes

configuration[R]

Public Class Methods

new(class_name: "Configuration", hijack: false, namespace: nil, strict: false) click to toggle source
# File lib/ravioli/builder.rb, line 54
    def initialize(class_name: "Configuration", hijack: false, namespace: nil, strict: false)
      configuration_class = if namespace.present?
        namespace.class_eval <<-EOC, __FILE__, __LINE__ + 1
          # class Configuration < Ravioli::Configuration; end
          class #{class_name.to_s.classify} < Ravioli::Configuration; end
        EOC
        namespace.const_get(class_name)
      else
        Ravioli::Configuration
      end
      @strict = !!strict
      @configuration = configuration_class.new
      @reload_credentials = Set.new
      @reload_paths = Set.new
      @hijack = !!hijack

      if @hijack
        # Put this builder on the configurations stack - it will intercept setters on the underyling
        # configuration object as it loads files, and mark those files as needing a reload once
        # loading is complete
        Ravioli.configurations.push(self)
      end
    end

Public Instance Methods

add_staging_flag!(is_staging = Rails.env.production? && ENV["STAGING"].present?) click to toggle source

Automatically infer a `staging` status from the current environment

@param is_staging [boolean, present?] whether or not the current environment is considered a staging environment

# File lib/ravioli/builder.rb, line 81
def add_staging_flag!(is_staging = Rails.env.production? && ENV["STAGING"].present?)
  is_staging = is_staging.present?
  configuration.staging = is_staging
end
auto_load_credentials!() click to toggle source

Loads Rails encrypted credentials that it can. Checks for corresponding private key files, or ENV vars based on the {Ravioli::Builder credentials preadmlogic}

# File lib/ravioli/builder.rb, line 97
def auto_load_credentials!
  # Load the root config (supports using the master key or `RAILS_ROOT_KEY`)
  load_credentials(
    key_path: "config/master.key",
    env_names: %w[master root],
    quiet: true,
  )

  # Load any environment-specific configuration on top of it. Since Rails will try
  # `RAILS_MASTER_KEY` from the environment, we assume the same
  load_credentials(
    "config/credentials/#{Rails.env}",
    key_path: "config/credentials/#{Rails.env}.key",
    env_names: ["master"],
    quiet: true,
  )

  # Apply staging configuration on top of THAT, if need be
  if configuration.staging?
    load_credentials(
      "config/credentials/staging",
      env_names: %w[staging master],
      key_path: "config/credentials/staging.key",
      quiet: true,
    )
  end
end
auto_load_files!() click to toggle source

Iterates through the config directory (including nested folders) and calls {Ravioli::Builder::load_file} on each JSON or YAML file it finds. Ignores `config/locales`.

# File lib/ravioli/builder.rb, line 89
def auto_load_files!
  config_dir = Rails.root.join("config")
  Dir[config_dir.join("{[!locales/]**/*,*}.{json,yaml,yml}")].each do |config_file|
    auto_load_file(config_file)
  end
end
build!() click to toggle source

When the builder is done working, lock the configuration and return it

# File lib/ravioli/builder.rb, line 126
def build!
  if @hijack
    # Replace this builder with the underlying configuration on the configurations stack...
    Ravioli.configurations.delete(self)
    Ravioli.configurations.push(configuration)

    # ...and then reload any config file that referenced the configuration the first time it was
    # loaded!
    @reload_paths.each do |path|
      auto_load_file(path)
    end
  end

  configuration.freeze
end
load_credentials(path = "credentials", key_path: path, env_names: path.split("/").last, quiet: false) click to toggle source

Load secure credentials using a key either from a file or the ENV

# File lib/ravioli/builder.rb, line 151
def load_credentials(path = "credentials", key_path: path, env_names: path.split("/").last, quiet: false)
  error = nil
  env_names = Array(env_names).map { |env_name| parse_env_name(env_name) }
  env_names.each do |env_name|
    credentials = parse_credentials(path, env_name: env_name, key_path: key_path, quiet: quiet)
    if credentials.present?
      configuration.append(credentials)
      return credentials
    end
  rescue => e
    error = e
  end

  if error
    attempted_names = ["key file `#{key_path}'"]
    attempted_names.push(*env_names.map { |env_name| "`ENV[\"#{env_name}\"]'" })
    attempted_names = attempted_names.to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
    warn(
      "Could not decrypt `#{path}.yml.enc' with #{attempted_names}",
      error,
      critical: false,
    )
  end

  {}
end
load_file(path, options = {}) click to toggle source

Load a file either with a given path or by name (e.g. `config/whatever.yml` or `:whatever`)

# File lib/ravioli/builder.rb, line 143
def load_file(path, options = {})
  config = parse_config_file(path, options)
  configuration.append(config) if config.present?
rescue => error
  warn "Could not load config file #{path}", error
end

Private Instance Methods

auto_load_file(config_file) click to toggle source
# File lib/ravioli/builder.rb, line 185
def auto_load_file(config_file)
  basename = File.basename(config_file, File.extname(config_file))
  dirname = File.dirname(config_file)
  key = %w[app application].exclude?(basename) && dirname != config_dir
  load_file(config_file, key: key)
end
extract_environmental_config(config) click to toggle source
# File lib/ravioli/builder.rb, line 192
def extract_environmental_config(config)
  # Check if the config hash is keyed by environment - if not, just return it as-is. It's
  # considered "keyed by environment" if it contains ONLY env-specific keys.
  return config unless (config.keys & ENV_KEYS).any? && (config.keys - ENV_KEYS).empty?

  # Combine environmental config in the following order:
  # 1. Shared config
  # 2. Environment-specific
  # 3. Staging-specific (if we're in a staging environment)
  environments = ["shared", Rails.env.to_s]
  environments.push("staging") if configuration.staging?
  config.values_at(*environments).inject({}) { |final_config, environment_config|
    final_config.deep_merge((environment_config || {}))
  }
end
method_missing(*args, &block) click to toggle source

rubocop:disable Style/MethodMissingSuper rubocop:disable Style/MissingRespondToMissing

# File lib/ravioli/builder.rb, line 210
def method_missing(*args, &block)
  if @current_path
    @reload_paths.add(@current_path)
  end

  if @current_credentials
    @reload_credentials.add(@current_credentials)
  end

  configuration.send(*args, &block)
end
parse_config_file(path, options = {}) click to toggle source

rubocop:enable Style/MissingRespondToMissing rubocop:enable Style/MethodMissingSuper

# File lib/ravioli/builder.rb, line 224
def parse_config_file(path, options = {})
  # Stash a reference to the file we're parsing, so we can reload it later if it tries to use
  # the configuration object
  @current_path = path
  path = path_to_config_file_path(path)

  config = case path.extname.downcase
  when ".json"
    parse_json_config_file(path)
  when ".yml", ".yaml"
    parse_yaml_config_file(path)
  else
    raise ParseError.new("Ravioli doesn't know how to parse #{path}")
  end

  # We are no longer loading anything
  @current_path = nil
  # At least expect a hash to be returned from the loaded config file
  return {} unless config.is_a?(Hash)

  # Extract a merged config based on the Rails.env (if the file is keyed that way)
  config = extract_environmental_config(config)

  # Key the configuration according the passed-in options
  key = options.delete(:key) { true }
  return config if key == false # `key: false` means don't key the configuration at all

  if key == true
    # `key: true` means key it automatically based on the filename
    name = File.basename(path, File.extname(path))
    name = File.dirname(path).split(Pathname::SEPARATOR_PAT).last if name.casecmp("config").zero?
  else
    # `key: :anything_else` means use `:anything_else` as the key
    name = key.to_s
  end

  {name => config}
end
parse_credentials(path, key_path: path, env_name: path.split("/").last, quiet: false) click to toggle source
# File lib/ravioli/builder.rb, line 268
def parse_credentials(path, key_path: path, env_name: path.split("/").last, quiet: false)
  @current_credentials = path
  env_name = parse_env_name(env_name)
  key_path = path_to_config_file_path(key_path, extnames: "key", quiet: true)
  options = {key_path: key_path.to_s}
  options[:env_key] = ENV[env_name].present? ? env_name : "__RAVIOLI__#{SecureRandom.hex(6)}"

  path = path_to_config_file_path(path, extnames: "yml.enc", quiet: quiet)
  (Rails.application.encrypted(path, **options)&.config || {}).tap do
    @current_credentials = nil
  end
end
parse_env_name(env_name) click to toggle source
# File lib/ravioli/builder.rb, line 263
def parse_env_name(env_name)
  env_name = env_name.to_s
  env_name.match?(/^RAILS_/) ? env_name : "RAILS_#{env_name.upcase}_KEY"
end
parse_json_config_file(path) click to toggle source
# File lib/ravioli/builder.rb, line 281
def parse_json_config_file(path)
  contents = File.read(path)
  JSON.parse(contents).deep_transform_keys { |key| key.to_s.underscore }
end
parse_yaml_config_file(path) click to toggle source
# File lib/ravioli/builder.rb, line 286
def parse_yaml_config_file(path)
  contents = File.read(path)
  erb = ERB.new(contents).tap { |renderer| renderer.filename = path.to_s }
  YAML.safe_load(erb.result, [Symbol], aliases: true)
end
path_to_config_file_path(path, extnames: EXTNAMES, quiet: false) click to toggle source
# File lib/ravioli/builder.rb, line 292
def path_to_config_file_path(path, extnames: EXTNAMES, quiet: false)
  original_path = path.dup
  unless path.is_a?(Pathname)
    path = path.to_s
    path = path.match?(Pathname::SEPARATOR_PAT) ? Pathname.new(path) : Pathname.new("config").join(path)
  end
  path = Rails.root.join(path) unless path.absolute?

  # Try to guess an extname, if we weren't given one
  if path.extname.blank?
    Array(extnames).each do |extname|
      other_path = path.sub_ext(".#{extname}")
      if other_path.exist?
        path = other_path
        break
      end
    end
  end

  warn "Could not resolve a configuration file at #{original_path.inspect}" unless quiet || path.exist?

  path
end
warn(message, error = $!, critical: true) click to toggle source
# File lib/ravioli/builder.rb, line 316
def warn(message, error = $!, critical: true)
  message = "[Ravioli] #{message}"
  message = "#{message}:\n\n#{error.cause.inspect}" if error&.cause.present?
  if @strict && critical
    raise BuildError.new(message, error)
  else
    Rails.logger.try(:warn, message) if defined? Rails
    $stderr.write message # rubocop:disable Rails/Output
  end
end