# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module Google
module Cloud
##
# Configuration mechanism for Google Cloud libraries. A Config object
# contains a list of predefined keys, some of which are values and others
# of which are subconfigurations, i.e. categories. Field access is
# generally validated to ensure that the field is defined, and when a
# a value is set, it is validated for the correct type. Warnings are
# printed when a validation fails.
#
# You generally access fields and subconfigs by calling accessor methods.
# Methods meant for "administration" such as adding options, are named
# with a trailing "!" or "?" so they don't pollute the method namespace.
# It is also possible to access a field using the `[]` operator.
#
# Note that config objects inherit from `BasicObject`. This means it does
# not define many methods you might expect to find in most Ruby objects.
# For example, `to_s`, `inspect`, `is_a?`, `instance_variable_get`, and so
# forth.
#
# @example
# require "google/cloud/config"
#
# config = Google::Cloud::Config.create do |c|
# c.add_field! :opt1, 10
# c.add_field! :opt2, :one, enum: [:one, :two, :three]
# c.add_field! :opt3, "hi", match: [String, Symbol]
# c.add_field! :opt4, "hi", match: /^[a-z]+$/, allow_nil: true
# c.add_config! :sub do |c2|
# c2.add_field! :opt5, false
# end
# end
#
# config.opt1 #=> 10
# config.opt1 = 20 #=> 20
# config.opt1 #=> 20
# config.opt1 = "hi" #=> "hi" (but prints a warning)
# config.opt1 = nil #=> nil (but prints a warning)
#
# config.opt2 #=> :one
# config.opt2 = :two #=> :two
# config.opt2 #=> :two
# config.opt2 = :four #=> :four (but prints a warning)
#
# config.opt3 #=> "hi"
# config.opt3 = "hiho" #=> "hiho"
# config.opt3 #=> "hiho"
# config.opt3 = "HI" #=> "HI" (but prints a warning)
#
# config.opt4 #=> "yo"
# config.opt4 = :yo #=> :yo (Strings and Symbols allowed)
# config.opt4 #=> :yo
# config.opt4 = 3.14 #=> 3.14 (but prints a warning)
# config.opt4 = nil #=> nil (no warning: nil allowed)
#
# config.sub #=> <Google::Cloud::Config>
#
# config.sub.opt5 #=> false
# config.sub.opt5 = true #=> true (true and false allowed)
# config.sub.opt5 #=> true
# config.sub.opt5 = nil #=> nil (but prints a warning)
#
# config.opt9 = "hi" #=> "hi" (warning about unknown key)
# config.opt9 #=> "hi" (no warning: key now known)
# config.sub.opt9 #=> nil (warning about unknown key)
#
class Config < BasicObject
##
# Constructs a Config object. If a block is given, yields `self` to the
# block, which makes it convenient to initialize the structure by making
# calls to `add_field!` and `add_config!`.
#
# @param [boolean] show_warnings Whether to print warnings when a
# validation fails. Defaults to `true`.
# @return [Config] The constructed Config object.
#
def self.create show_warnings: true
config = new [], show_warnings: show_warnings
yield config if block_given?
config
end
##
# Determines if the given object is a config. Useful because Config
# does not define the `is_a?` method.
#
# @return [boolean]
#
def self.config? obj
Config.send :===, obj
end
##
# Internal constructor. Generally you should not call `new` directly,
# but instead use the `Config.create` method. The initializer is used
# directly by a few older clients that expect a legacy interface.
#
# @private
#
def initialize legacy_categories = {}, opts = {}
@show_warnings = opts.fetch :show_warnings, false
@values = {}
@defaults = {}
@validators = {}
add_options legacy_categories
end
##
# Legacy method of adding subconfigs. This is used by older versions of
# the stackdriver client libraries but should not be used in new code.
#
# @deprecated
# @private
#
def add_options legacy_categories
[legacy_categories].flatten(1).each do |sub_key|
case sub_key
when ::Symbol
add_config! sub_key, Config.new
when ::Hash
sub_key.each do |k, v|
add_config! k, Config.new(v)
end
else
raise ArgumentError "Category must be a Symbol or Hash"
end
end
end
##
# Add a value field to this configuration.
#
# You must provide a key, which becomes the field name in this config.
# Field names may comprise only letters, numerals, and underscores, and
# must begin with a letter. This will create accessor methods for the
# new configuration key.
#
# You may pass an initial value (which defaults to nil if not provided).
#
# You may also specify how values are validated. Validation is defined
# as follows:
#
# * If you provide a block or a `:validator` option, it is used as the
# validator. A proposed value is passed to the proc, which should
# return `true` or `false` to indicate whether the value is valid.
# * If you provide a `:match` option, it is compared to the proposed
# value using the `===` operator. You may, for example, provide a
# class, a regular expression, or a range. If you pass an array,
# the value is accepted if _any_ of the elements match.
# * If you provide an `:enum` option, it should be an `Enumerable`.
# A proposed value is valid if it is included.
# * Otherwise if you do not provide any of the above options, then a
# default validation strategy is inferred from the initial value:
# * If the initial is `true` or `false`, then either boolean value
# is considered valid. This is the same as `enum: [true, false]`.
# * If the initial is `nil`, then any object is considered valid.
# * Otherwise, any object of the same class as the initial value is
# considered valid. This is effectively the same as
# `match: initial.class`.
# * You may also provide the `:allow_nil` option, which, if set to
# true, alters any of the above validators to allow `nil` values.
#
# In many cases, you may find that the default validation behavior
# (interpreted from the initial value) is sufficient. If you want to
# accept any value, use `match: Object`.
#
# @param [String, Symbol] key The name of the option
# @param [Object] initial Initial value (defaults to nil)
# @param [Hash] opts Validation options
#
# @return [Config] self for chaining
#
def add_field! key, initial = nil, opts = {}, &block
key = validate_new_key! key
opts[:validator] = block if block
validator = resolve_validator! initial, opts
validate_value! key, validator, initial
@values[key] = initial
@defaults[key] = initial
@validators[key] = validator
self
end
##
# Add a subconfiguration field to this configuration.
#
# You must provide a key, which becomes the method name that you use to
# navigate to the subconfig. Names may comprise only letters, numerals,
# and underscores, and must begin with a letter.
#
# If you provide a block, the subconfig object is passed to the block,
# so you can easily add fields to the subconfig.
#
# You may also pass in a config object that already exists. This will
# "attach" that configuration in this location.
#
# @param [String, Symbol] key The name of the subconfig
# @param [Config] config A config object to attach here. If not provided,
# creates a new config.
#
# @return [Config] self for chaining
#
def add_config! key, config = nil, &block
key = validate_new_key! key
if config.nil?
config = Config.create(&block)
elsif block
yield config
end
@values[key] = config
@defaults[key] = config
@validators[key] = SUBCONFIG
self
end
##
# Cause a key to be an alias of another key. The two keys will refer to
# the same field.
#
def add_alias! key, to_key
key = validate_new_key! key
@values.delete key
@defaults.delete key
@validators[key] = to_key.to_sym
self
end
##
# Restore the original default value of the given key.
# If the key is omitted, restore the original defaults for all keys,
# and all keys of subconfigs, recursively.
#
# @param [Symbol, nil] key The key to reset. If omitted or `nil`,
# recursively reset all fields and subconfigs.
#
def reset! key = nil
if key.nil?
@values.each_key { |k| reset! k }
else
key = key.to_sym
if @defaults.key? key
@values[key] = @defaults[key]
@values[key].reset! if @validators[key] == SUBCONFIG
elsif @values.key? key
warn! "Key #{key.inspect} has not been added, but has a value. Removing the value."
@values.delete key
else
warn! "Key #{key.inspect} does not exist. Nothing to reset."
end
end
self
end
##
# Remove the given key from the configuration, deleting any validation
# and value. If the key is omitted, delete all keys. If the key is an
# alias, deletes the alias but leaves the original.
#
# @param [Symbol, nil] key The key to delete. If omitted or `nil`,
# delete all fields and subconfigs.
#
def delete! key = nil
if key.nil?
@values.clear
@defaults.clear
@validators.clear
else
@values.delete key
@defaults.delete key
@validators.delete key
end
self
end
##
# Assign an option with the given name to the given value.
#
# @param [Symbol, String] key The option name
# @param [Object] value The new option value
#
def []= key, value
key = resolve_key! key
validate_value! key, @validators[key], value
@values[key] = value
end
##
# Get the option or subconfig with the given name.
#
# @param [Symbol, String] key The option or subconfig name
# @return [Object] The option value or subconfig object
#
def [] key
key = resolve_key! key
warn! "Key #{key.inspect} does not exist. Returning nil." unless @validators.key? key
value = @values[key]
value = value.call if Config::DeferredValue === value
value
end
##
# Check if the given key has been set in this object. Returns true if the
# key has been added as a normal field, subconfig, or alias, or if it has
# not been added explicitly but still has a value.
#
# @param [Symbol] key The key to check for.
# @return [boolean]
#
def value_set? key
@values.key? resolve_key! key
end
alias option? value_set?
##
# Check if the given key has been explicitly added as a field name.
#
# @param [Symbol] key The key to check for.
# @return [boolean]
#
def field? key
@validators[key.to_sym].is_a? ::Proc
end
alias respond_to? field?
##
# Check if the given key has been explicitly added as a subconfig name.
#
# @param [Symbol] key The key to check for.
# @return [boolean]
#
def subconfig? key
@validators[key.to_sym] == SUBCONFIG
end
##
# Check if the given key has been explicitly added as an alias.
# If so, return the target, otherwise return nil.
#
# @param [Symbol] key The key to check for.
# @return [Symbol,nil] The alias target, or nil if not an alias.
#
def alias? key
target = @validators[key.to_sym]
target.is_a?(::Symbol) ? target : nil # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
end
##
# Return a list of explicitly added field names.
#
# @return [Array<Symbol>] a list of field names as symbols.
#
def fields!
@validators.keys.find_all { |key| @validators[key].is_a? ::Proc }
end
##
# Return a list of explicitly added subconfig names.
#
# @return [Array<Symbol>] a list of subconfig names as symbols.
#
def subconfigs!
@validators.keys.find_all { |key| @validators[key] == SUBCONFIG }
end
##
# Return a list of alias names.
#
# @return [Array<Symbol>] a list of alias names as symbols.
#
def aliases!
@validators.keys.find_all { |key| @validators[key].is_a? ::Symbol }
end
##
# Returns a string representation of this configuration state, including
# subconfigs. Only explicitly added fields and subconfigs are included.
#
# @return [String]
#
def to_s!
elems = @validators.keys.map do |k|
v = @values[k]
vstr = Config.config?(v) ? v.to_s! : v.inspect
" #{k}=#{vstr}"
end
"<Config:#{elems.join}>"
end
alias inspect to_s!
##
# Returns a nested hash representation of this configuration state,
# including subconfigs. Only explicitly added fields and subconfigs are
# included.
#
# @return [Hash]
#
def to_h!
h = {}
@validators.each_key do |k|
v = @values[k]
h[k] = Config.config?(v) ? v.to_h! : v.inspect
end
h
end
##
# Search the given environment variable names for valid credential data
# that can be passed to `Google::Auth::Credentials.new`.
# If a variable contains a valid file path, returns that path as a string.
# If a variable contains valid JSON, returns the parsed JSON as a hash.
# If no variables contain valid data, returns nil.
# @private
#
def self.credentials_from_env *vars
vars.each do |var|
data = ::ENV[var]
next unless data
str = data.strip
return str if ::File.file? str
json = begin
::JSON.parse str
rescue ::StandardError
nil
end
return json if json.is_a? ::Hash
end
nil
end
##
# @private
# Create a configuration value that will be invoked when retrieved.
#
def self.deferred &block
DeferredValue.new(&block)
end
##
# @private
# Dynamic methods accessed as keys.
#
def method_missing name, *args
name_str = name.to_s
super unless name_str =~ /^[a-zA-Z]\w*=?$/
if name_str.end_with? "="
self[name_str[0...-1]] = args.first
else
self[name]
end
end
##
# @private
# Dynamic methods accessed as keys.
#
def respond_to_missing? name, include_private
return true if value_set? name.to_s.chomp("=")
super
end
##
# @private
# Implement standard nil check
#
# @return [false]
#
def nil?
false
end
##
# @private
# Implement standard is_a check
#
# @return [boolean]
#
def is_a? klass
klass == Config
end
private
##
# @private A validator that allows all values
#
OPEN_VALIDATOR = proc { true }
##
# @private a list of key names that are technically illegal because
# they clash with method names.
#
ILLEGAL_KEYS = [:add_options,
:initialize,
:inspect,
:instance_eval,
:instance_exec,
:method_missing,
:send,
:singleton_method_added,
:singleton_method_removed,
:singleton_method_undefined].freeze
##
# @private sentinel indicating a subconfig in the validators hash
#
SUBCONFIG = ::Object.new
def resolve_key! key
key = key.to_sym
alias_target = @validators[key]
alias_target.is_a?(::Symbol) ? alias_target : key
end
def validate_new_key! key
key_str = key.to_s
key = key.to_sym
if key_str !~ /^[a-zA-Z]\w*$/ || ILLEGAL_KEYS.include?(key)
warn! "Illegal key name: #{key_str.inspect}. Method dispatch will not work for this key."
end
warn! "Key #{key.inspect} already exists. It will be replaced." if @validators.key? key
key
end
def resolve_validator! initial, opts
allow_nil = initial.nil? || opts[:allow_nil]
if opts.key? :validator
build_proc_validator! opts[:validator], allow_nil
elsif opts.key? :match
build_match_validator! opts[:match], allow_nil
elsif opts.key? :enum
build_enum_validator! opts[:enum], allow_nil
elsif [true, false].include? initial
build_enum_validator! [true, false], allow_nil
elsif initial.nil?
OPEN_VALIDATOR
else
klass = Config.config?(initial) ? Config : initial.class
build_match_validator! klass, allow_nil
end
end
def build_match_validator! matches, allow_nil
matches = ::Kernel.Array(matches)
matches += [nil] if allow_nil && !matches.include?(nil)
->(val) { matches.any? { |m| m.send :===, val } }
end
def build_enum_validator! allowed, allow_nil
allowed = ::Kernel.Array(allowed)
allowed += [nil] if allow_nil && !allowed.include?(nil)
->(val) { allowed.include? val }
end
def build_proc_validator! proc, allow_nil
->(val) { proc.call(val) || (allow_nil && val.nil?) }
end
def validate_value! key, validator, value
value = value.call if Config::DeferredValue === value
case validator
when ::Proc
unless validator.call value
warn! "Invalid value #{value.inspect} for key #{key.inspect}. Setting anyway."
end
when Config
if value != validator
warn! "Key #{key.inspect} refers to a subconfig and shouldn't be changed. Setting anyway."
end
else
warn! "Key #{key.inspect} has not been added. Setting anyway."
end
end
def warn! msg
return unless @show_warnings
location = ::Kernel.caller_locations.find do |s|
!s.to_s.include? "/google/cloud/config.rb:"
end
::Kernel.warn "#{msg} at #{location}"
end
##
# @private
#
class DeferredValue
def initialize &block
@callback = block
end
def call
@callback.call
end
end
end
end
end