lib/chamber/file_set.rb
# frozen_string_literal: true
require 'pathname'
require 'chamber/namespace_set'
require 'chamber/file'
require 'chamber/settings'
###
# Internal: Represents a set of settings files that should be considered for
# processing. Whether they actually *are* processed depends on their extension
# (only *.yml and *.yml.erb files are processed unless explicitly specified),
# and whether their namespace matches one of the namespaces passed to the
# FileSet (text after a dash '-' but before the extension is considered the
# namespace for the file).
#
# When converted to settings, files are always processed in the order of least
# specific to most specific. So if there are two files:
#
# * /tmp/settings.yml
# * /tmp/settings-blue.yml
#
# Then '/tmp/settings.yml' will be processed first and '/tmp/settings-blue.yml'
# will be processed second (assuming a namespace with the value 'blue' was
# passed in).
#
# If there are multiple namespaces, they will be process in the order that they
# appear in the passed in hash. So assuming two files:
#
# * /tmp/settings-blue.yml
# * /tmp/settings-green.yml
#
# Then:
#
# ```ruby
# FileSet.new files: '/tmp/settings*.yml',
# namespaces: ['blue', 'green']
# ```
#
# will process in this order:
#
# * /tmp/settings-blue.yml
# * /tmp/settings-green.yml
#
# Whereas:
#
# ```ruby
# FileSet.new files: '/tmp/settings*.yml',
# namespaces: ['green', 'blue']
# ```
#
# will process in this order:
#
# * /tmp/settings-green.yml
# * /tmp/settings-blue.yml
#
# Examples:
#
# ###
# # Assuming the following files exist:
# #
# # /tmp/settings.yml
# # /tmp/settings-blue.yml
# # /tmp/settings-green.yml
# # /tmp/settings/another.yml
# # /tmp/settings/another.json
# # /tmp/settings/yet_another-blue.yml.erb
# # /tmp/settings/yet_another-green.yml
# #
#
# ###
# # This will *consider* all files listed but will only process 'settings.yml'
# # and 'another.yml'
# #
# FileSet.new files: ['/tmp/settings.yml',
# '/tmp/settings']
#
# ###
# # This will all files in the 'settings' directory but will only process
# # 'another.yml' and 'yet_another-blue.yml.erb'
# #
# FileSet.new(files: '/tmp/settings',
# namespaces: {
# favorite_color: 'blue' } )
#
# ###
# # Passed in namespaces do not have to be hashes. Hash keys are used only for
# # human readability.
# #
# # This results in the same outcome as the example above.
# #
# FileSet.new(files: '/tmp/settings',
# namespaces: ['blue'])
#
# ###
# # This will process all files listed:
# #
# FileSet.new(files: [
# '/tmp/settings*.yml',
# '/tmp/settings',
# ],
# namespaces: %w{blue green})
#
# ###
# # This is the only way to explicitly specify files which do not end in
# # a 'yml' extension.
# #
# # This is the only example thus far which will process
# # '/tmp/settings/another.json'
# #
# FileSet.new(files: '/tmp/settings/*.json',
# namespaces: %w{blue green})
#
module Chamber
class FileSet
attr_accessor :decryption_keys,
:encryption_keys,
:basepath,
:signature_name
attr_reader :namespaces,
:paths
# rubocop:disable Metrics/ParameterLists
def initialize(files:,
basepath: nil,
decryption_keys: nil,
encryption_keys: nil,
namespaces: {},
signature_name: nil)
self.basepath = basepath
self.decryption_keys = decryption_keys
self.encryption_keys = encryption_keys
self.namespaces = namespaces
self.paths = files
self.signature_name = signature_name
end
# rubocop:enable Metrics/ParameterLists
###
# Internal: Returns an Array of the ordered list of files that was processed
# by Chamber in order to get the resulting settings values. This is useful
# for debugging if a given settings value isn't quite what you anticipated it
# should be.
#
# Returns an Array of file path strings
#
def filenames
@filenames ||= files.map(&:to_s)
end
###
# Internal: Converts the FileSet into a Settings object which represents all
# the settings specified in all of the files in the FileSet.
#
# This can be used in one of two ways. You may either specify a block which
# will be passed each file's settings as they are converted, or you can choose
# not to pass a block, in which case it will pass back a single completed
# Settings object to the caller.
#
# The reason the block version is used in Chamber.settings is because we want
# to be able to load each settings file as it's processed so that we can use
# those already-processed settings in subsequently processed settings files.
#
# Examples:
#
# ###
# # Specifying a Block
# #
# file_set = FileSet.new files: [ '/path/to/my/settings.yml' ]
#
# file_set.to_settings do |settings|
# # do stuff with each settings
# end
#
#
# ###
# # No Block Specified
# #
# file_set = FileSet.new files: [ '/path/to/my/settings.yml' ]
# file_set.to_settings
#
# # => <Chamber::Settings>
#
def to_settings
files.inject(Settings.new) do |settings, file|
settings.merge(file.to_settings).tap do |merged|
yield merged if block_given?
end
end
end
def secure
files.each(&:secure)
end
def unsecure
files.each(&:decrypt)
end
def sign
files.each(&:sign)
end
def verify
files.each_with_object({}) do |file, memo|
relative_filepath = Pathname.new(file.to_s).relative_path_from(basepath).to_s
memo[relative_filepath] = file.verify
end
end
protected
###
# Internal: Allows the paths for the FileSet to be set. It can either be an
# object that responds to `#each` like an Array or one that doesn't. In which
# case it will be considered a single path.
#
# All paths will be converted to Pathnames.
#
def paths=(raw_paths)
raw_paths = [raw_paths] unless raw_paths.respond_to? :each
@paths = raw_paths.map { |path| Pathname.new(path) }
end
###
# Internal: Allows the namespaces for the FileSet to be set. An Array or Hash
# can be passed; in both cases it will be converted to a NamespaceSet.
#
def namespaces=(raw_namespaces)
@namespaces = NamespaceSet.new(raw_namespaces)
end
###
# Internal: The set of files which are considered to be relevant, but with any
# duplicates removed.
#
def files
@files ||= lambda {
sorted_relevant_files = []
file_globs.each do |glob|
current_glob_files = Pathname.glob(glob)
relevant_glob_files = relevant_files & current_glob_files
relevant_glob_files.map! do |file|
File.new(path: file,
namespaces: namespaces,
decryption_keys: decryption_keys,
encryption_keys: encryption_keys,
signature_name: signature_name)
end
sorted_relevant_files += relevant_glob_files
end
sorted_relevant_files.uniq
}.call
end
private
def all_files
@all_files ||= file_globs
.map { |fg| Pathname.glob(fg) }
.flatten
.uniq
.sort
end
def non_namespaced_files
@non_namespaced_files ||= all_files - namespaced_files
end
def relevant_files
@relevant_files ||= non_namespaced_files + relevant_namespaced_files
end
def file_globs
@file_globs ||= paths.map do |path|
if path.directory?
path + '*.{yml,yml.erb}'
else
path
end
end
end
def namespaced_files
@namespaced_files ||= all_files.select do |file|
file.basename.fnmatch? '*-*'
end
end
def relevant_namespaced_files
file_holder = []
namespaces.each do |namespace|
file_holder << namespaced_files.select do |file|
file.basename.fnmatch? "*-#{namespace}.???"
end
end
file_holder.flatten
end
end
end