qat-core 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +674 -0
- data/lib/qat/configuration.rb +172 -0
- data/lib/qat/configuration/environment.rb +103 -0
- data/lib/qat/configuration/parser.rb +100 -0
- data/lib/qat/core.rb +53 -0
- data/lib/qat/core/version.rb +11 -0
- data/lib/qat/core_ext/integer.rb +12 -0
- data/lib/qat/core_ext/object/deep_compact.rb +144 -0
- data/lib/qat/time.rb +156 -0
- data/lib/qat/time/zone/unix.rb +13 -0
- data/lib/qat/time/zone/windows.rb +15 -0
- data/lib/qat/time/zone/windows/tz_data.xml +680 -0
- data/lib/qat/utils/hash.rb +73 -0
- data/lib/qat/utils/network.rb +18 -0
- metadata +293 -0
@@ -0,0 +1,172 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'forwardable'
|
3
|
+
require 'yaml'
|
4
|
+
require 'erb'
|
5
|
+
require 'ipaddr'
|
6
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
7
|
+
require 'active_support/core_ext/object/deep_dup'
|
8
|
+
require 'active_support/core_ext/hash/keys'
|
9
|
+
require 'active_support/core_ext/hash/deep_merge'
|
10
|
+
require_relative 'core'
|
11
|
+
require_relative 'utils/network'
|
12
|
+
require_relative 'configuration/environment'
|
13
|
+
require_relative 'configuration/parser'
|
14
|
+
require 'qat/logger'
|
15
|
+
|
16
|
+
#QAT Module works as a namespace for all sub modules.
|
17
|
+
#Some singleton methods are also available, as defined by various sub classes.
|
18
|
+
module QAT
|
19
|
+
|
20
|
+
# This class represents {QAT}'s Configuration manager
|
21
|
+
#
|
22
|
+
# This class should be used to manage configurations from multiple environments in test projects.
|
23
|
+
# Given a defined environment, all configurations for that environment will be loaded into a cache
|
24
|
+
# from the corresponding folder (with the same name) under the +config+ directory.
|
25
|
+
#
|
26
|
+
# A +common+ folder is also supported which contains configurations shared between multiple environments.
|
27
|
+
# A configuration file can exist in both the environment folder and the common folder with the content of both
|
28
|
+
# being merged at load time and the configuration from the environment taking precedence over the shared (common)
|
29
|
+
# configuration.
|
30
|
+
#
|
31
|
+
# Also supported is the cross-referencing of configurations between files.
|
32
|
+
# Given a file 'foo.yml' with a configuration 'foo: bar'
|
33
|
+
# and a second file 'baz.yml' with configuration 'baz: foo.foo',
|
34
|
+
# after configurations are loaded to cache, the value of 'baz' in cache will be 'bar'.
|
35
|
+
# Cross-referencing between files is made with syntax:
|
36
|
+
# - +<file.yml>+
|
37
|
+
# `
|
38
|
+
# key: <other_file.yml>.<key>
|
39
|
+
# `
|
40
|
+
#@since 0.1.0
|
41
|
+
class Configuration
|
42
|
+
extend Forwardable
|
43
|
+
include QAT::Logger
|
44
|
+
include Utils::Network
|
45
|
+
include Environment
|
46
|
+
include Parser
|
47
|
+
|
48
|
+
def_delegators :@cache, :empty?, :fetch, :keys, :has_key?, :include?, :key?, :member?, :reject, :select, :values_at,
|
49
|
+
:stringify_keys, :symbolize_keys, :dig
|
50
|
+
|
51
|
+
#@param directory [String] path to configuration directory
|
52
|
+
#@param environment [String] name of the environment from which configurations will be loaded
|
53
|
+
def initialize(directory=Dir.pwd, environment=nil)
|
54
|
+
self.directory = directory
|
55
|
+
log.info { "Initializing configuration from directory #{self.directory}" }
|
56
|
+
self.environment = validate_environment(environment)
|
57
|
+
log.info { "Initialized configuration with environment '#{self.environment}'" }
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns a copy from cache to avoid data manipulation on runtime
|
61
|
+
#@param key [String|Symbol] cache key
|
62
|
+
#@return [Object]
|
63
|
+
def [](key)
|
64
|
+
@cache[key].deep_dup
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
# Resolves references between configuration files.
|
69
|
+
def resolve_references!
|
70
|
+
intersect_config @cache
|
71
|
+
end
|
72
|
+
|
73
|
+
# Resets the configuration cache
|
74
|
+
def reset_cache!
|
75
|
+
@cache = ActiveSupport::HashWithIndifferentAccess.new unless @cache and @cache.empty?
|
76
|
+
end
|
77
|
+
|
78
|
+
# Loads the environment configurations for the defined environment.
|
79
|
+
# If an invalid +environment+ was given, an exception is thrown.
|
80
|
+
#
|
81
|
+
#@see #environment=
|
82
|
+
#@raise [NoEnvironmentDefined] When no environment definition if found
|
83
|
+
#@raise [NoEnvironmentFolder] When there is no environment folder for environment definition
|
84
|
+
def load_env!
|
85
|
+
raise NoEnvironmentDefined.new 'No environment definition found!' unless environment
|
86
|
+
|
87
|
+
env_folder = File.join @directory, environment
|
88
|
+
raise NoEnvironmentFolder.new "No folder '#{environment}' found in directory #{@directory}" unless Dir.exist? env_folder
|
89
|
+
|
90
|
+
common_folder = File.join @directory, 'common'
|
91
|
+
|
92
|
+
reset_cache!
|
93
|
+
log.debug { "Loading configuration cache using environment #{environment}" }
|
94
|
+
|
95
|
+
[common_folder, env_folder].each do |folder|
|
96
|
+
if Dir.exist? folder
|
97
|
+
log.debug { "Loading folder #{folder}" }
|
98
|
+
load_root_files(folder)
|
99
|
+
load_root_folders(folder)
|
100
|
+
else
|
101
|
+
log.debug { "Folder #{folder} not found, skipping" }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
log.info { "Configuration cache loaded" }
|
105
|
+
|
106
|
+
resolve_references!
|
107
|
+
|
108
|
+
log.info { "Configuration cache references resolved" }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Analyses +config+ and replaces references between configuration files.
|
112
|
+
#
|
113
|
+
#@param config [ActiveSupport::HashWithIndifferentAccess]
|
114
|
+
#@return [ActiveSupport::HashWithIndifferentAccess]
|
115
|
+
def intersect_config(config)
|
116
|
+
new_hash = {}
|
117
|
+
config.each do |key, value|
|
118
|
+
if value.is_a?(Hash)
|
119
|
+
new_value = intersect_config(value)
|
120
|
+
elsif value.is_a?(Array)
|
121
|
+
new_value = value.map { |element| referenced_value(element) }
|
122
|
+
else
|
123
|
+
new_value = referenced_value(value)
|
124
|
+
end
|
125
|
+
new_hash[key] = new_value
|
126
|
+
end
|
127
|
+
config.update(new_hash)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns the value referenced by a given +reference+.
|
131
|
+
#
|
132
|
+
#@param reference [Object] object containing references/values
|
133
|
+
#@return [Object]
|
134
|
+
def referenced_value(reference)
|
135
|
+
if reference.is_a?(Array)
|
136
|
+
reference.each { |element| referenced_value(element) }
|
137
|
+
elsif reference.is_a?(Hash)
|
138
|
+
reference.each { |key, value| reference[key] = referenced_value(value) }
|
139
|
+
else
|
140
|
+
return reference unless reference.is_a?(String) && reference.match(/^\w+(\.\w+)+$/) && !is_ip?(reference)
|
141
|
+
|
142
|
+
value = reference.split('.').inject(@cache) { |cache, key| cache[key] } rescue reference
|
143
|
+
|
144
|
+
if value.equal? reference
|
145
|
+
log.debug { "Reference '#{reference}' not found, keeping original value" }
|
146
|
+
else
|
147
|
+
log.debug { "Replacing '#{reference}' reference for value '#{value}'" }
|
148
|
+
end
|
149
|
+
|
150
|
+
value
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
public
|
155
|
+
# This class represents a configuration loading error when there is no defined environment
|
156
|
+
class NoEnvironmentDefined < StandardError
|
157
|
+
end
|
158
|
+
# This class represents a configuration loading error when there is no folder for defined environment
|
159
|
+
class NoEnvironmentFolder < StandardError
|
160
|
+
end
|
161
|
+
# This class represents a configuration loading error when there is a folder has an invalid name
|
162
|
+
class InvalidFolderName < StandardError
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Singleton access to a {QAT::Configuration} object. Only available in cucumber mode.
|
167
|
+
#@return [QAT::Configuration] Configuration object for the config folder.
|
168
|
+
def self.configuration
|
169
|
+
to_load = ENV['QAT_CONFIG_FOLDER'] || 'config'
|
170
|
+
@configuration ||= QAT::Configuration.new to_load
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module QAT
|
2
|
+
class Configuration
|
3
|
+
# Namespace for environment helper methods
|
4
|
+
module Environment
|
5
|
+
attr_reader :environment, :directory
|
6
|
+
|
7
|
+
# Sets the configuration directory
|
8
|
+
#
|
9
|
+
# If an invalid path is given, an exception will thrown.
|
10
|
+
# Given a valid +directory+ path:
|
11
|
+
# - if the path is the same as the current path no configurations will loaded.
|
12
|
+
# - if the identifier is for a new path, cache is cleared and the new configurations are loaded.
|
13
|
+
#
|
14
|
+
#@param directory [String] directory path
|
15
|
+
#@raise [ArgumentError] When directory is invalid.
|
16
|
+
def directory=(directory)
|
17
|
+
raise ArgumentError.new 'No directory to set' unless directory
|
18
|
+
unless File.exist? directory and File.directory? directory
|
19
|
+
raise ArgumentError.new "#{directory} does not exist or is not a directory"
|
20
|
+
end
|
21
|
+
if @directory and File.absolute_path(directory) == File.absolute_path(@directory)
|
22
|
+
log.info { "New directory is the same as the old one, cache will not be refreshed." }
|
23
|
+
else
|
24
|
+
old_dir = @directory
|
25
|
+
@directory = directory
|
26
|
+
log.info { "Using directory #{@directory}" }
|
27
|
+
reset_cache!
|
28
|
+
load_env! if old_dir
|
29
|
+
end
|
30
|
+
rescue NoEnvironmentDefined
|
31
|
+
log.warn "Environment is not defined! When defined, cache will be refreshed."
|
32
|
+
#Loading can happen when environment is defined
|
33
|
+
rescue NoEnvironmentFolder
|
34
|
+
log.warn "Defined environment does not exist in current directory, choose a valid environment to load cache"
|
35
|
+
self.environment = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sets the current environment
|
39
|
+
#
|
40
|
+
# Given a +environment+ identifier:
|
41
|
+
# - if the identifier is the same as the current environment no configurations will loaded.
|
42
|
+
# - if the identifier is for a new environment, cache is cleared and the new configurations are loaded.
|
43
|
+
# - if a +nil+ environment is given, configuration cache is cleared.
|
44
|
+
#
|
45
|
+
#@param environment [String] environment identifier (name)
|
46
|
+
#@raise [NoEnvironmentDefined] When no environment definition if found
|
47
|
+
#@raise [NoEnvironmentFolder] When there is no environment folder for environment definition
|
48
|
+
def environment=(environment)
|
49
|
+
return if @environment == environment
|
50
|
+
old_env = @environment
|
51
|
+
@environment = environment
|
52
|
+
|
53
|
+
if environment
|
54
|
+
load_env!
|
55
|
+
else
|
56
|
+
reset_cache!
|
57
|
+
end
|
58
|
+
rescue
|
59
|
+
@environment = old_env
|
60
|
+
raise
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns all the defined environments in the configuration directory
|
64
|
+
#
|
65
|
+
#@return [Array<String>] All environments defined
|
66
|
+
def environments
|
67
|
+
Dir.glob(File.join directory, '*/').map { |entry| File.basename(entry) }.reject { |entry| entry == 'common' }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Executes a code +block+ in all environments of the current configuration directory
|
71
|
+
#
|
72
|
+
#@yield block to execute
|
73
|
+
#@yieldparam [Configuration] Configuration loaded for each environment
|
74
|
+
def each_environment(&block)
|
75
|
+
old_env = @environment
|
76
|
+
environments.sort.each do |env|
|
77
|
+
self.environment = env
|
78
|
+
block.call(self)
|
79
|
+
end
|
80
|
+
ensure
|
81
|
+
self.environment = old_env
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
# Validates and returns the environment to be used based on precedences
|
86
|
+
# @param environment [String] environment reference
|
87
|
+
# @return [String]
|
88
|
+
def validate_environment(environment)
|
89
|
+
if environment
|
90
|
+
log.debug { "Reading environment from default variable" }
|
91
|
+
environment
|
92
|
+
elsif ENV['QAT_CONFIG_ENV']
|
93
|
+
log.debug { "Reading environment from QAT_CONFIG_ENV environment variable" }
|
94
|
+
ENV['QAT_CONFIG_ENV']
|
95
|
+
else
|
96
|
+
default_file = File.join @directory, 'default.yml'
|
97
|
+
log.debug { "Reading default file #{default_file}" }
|
98
|
+
read_yml(default_file)['env'] rescue nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module QAT
|
2
|
+
class Configuration
|
3
|
+
# Namespace for configuration parsing helper methods
|
4
|
+
module Parser
|
5
|
+
private
|
6
|
+
|
7
|
+
# Loads all files from the root configuration folder
|
8
|
+
#@param folder [String] root configuration folder path
|
9
|
+
def load_root_files(folder)
|
10
|
+
Dir.glob(File.join(folder, '*.yml')).each do |file|
|
11
|
+
log.debug { "Loading file #{file}" }
|
12
|
+
data = read_yml file
|
13
|
+
|
14
|
+
base = File.basename(file, '.yml').to_sym
|
15
|
+
|
16
|
+
@cache[base] ||= HashWithIndifferentAccess.new
|
17
|
+
|
18
|
+
@cache[base].update data.with_indifferent_access do |key, old, new|
|
19
|
+
log.debug { "Replacing key '#{key}' in cache with value '#{new}' (previously was '#{old}')" }
|
20
|
+
new
|
21
|
+
end
|
22
|
+
log.debug { 'Loaded file' }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Loads all folders from the root configuration folder
|
27
|
+
#@param folder [String] root configuration folder path
|
28
|
+
def load_root_folders(folder)
|
29
|
+
child_folders(folder).each do |child_folder|
|
30
|
+
log.debug { "Loading folder #{child_folder}" }
|
31
|
+
|
32
|
+
base = File.basename(child_folder).to_sym
|
33
|
+
|
34
|
+
raise(InvalidFolderName, "A file with name #{base} exists and was already loaded!") if File.exist?("#{child_folder}.yml")
|
35
|
+
|
36
|
+
@cache[base] ||= {}
|
37
|
+
|
38
|
+
data = load_child_folder(child_folder).deep_symbolize_keys!
|
39
|
+
|
40
|
+
merge_with_cache!(base, data)
|
41
|
+
|
42
|
+
log.debug { 'Loaded folder' }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Return the child folder for a given directory
|
47
|
+
# @param folder [String] directory path
|
48
|
+
# @return [Array]
|
49
|
+
def child_folders(folder)
|
50
|
+
Dir.glob(File.join(folder, '*')).select { |entry| File.directory? entry }
|
51
|
+
end
|
52
|
+
|
53
|
+
def merge_with_cache!(base, data)
|
54
|
+
symbolized_cache = @cache[base].deep_symbolize_keys
|
55
|
+
|
56
|
+
symbolized_cache.deep_merge! data do |key, old, new|
|
57
|
+
log.debug { "Replacing key '#{key}' in cache with value '#{new}' (previously was '#{old}')" }
|
58
|
+
new
|
59
|
+
end
|
60
|
+
|
61
|
+
@cache[base] = symbolized_cache.with_indifferent_access
|
62
|
+
end
|
63
|
+
|
64
|
+
# Loads a configuration folder within another folder
|
65
|
+
#@param folder [String] configuration folder path
|
66
|
+
def load_child_folder(folder)
|
67
|
+
content = {}
|
68
|
+
|
69
|
+
Dir.glob(File.join(folder, '*.yml')).each do |file|
|
70
|
+
base = File.basename(file, '.yml').to_sym
|
71
|
+
|
72
|
+
content[base] = read_yml(file)
|
73
|
+
end
|
74
|
+
|
75
|
+
folders = Dir.glob(File.join(folder, '*')).select { |entry| File.directory? entry }
|
76
|
+
folders.each do |folder_name|
|
77
|
+
base = File.basename(folder_name).to_sym
|
78
|
+
|
79
|
+
raise(InvalidFolderName, "A file with name #{base} exists and was already loaded!") if content[base]
|
80
|
+
content[base] = load_child_folder(folder_name)
|
81
|
+
end
|
82
|
+
|
83
|
+
content
|
84
|
+
end
|
85
|
+
|
86
|
+
# Reads and parses a YAML file. Allows for ERB syntax to be used.
|
87
|
+
#
|
88
|
+
#@param file_path [String] YAML file path
|
89
|
+
#@return [Hash]
|
90
|
+
#@raise Psych::SyntaxError
|
91
|
+
def read_yml(file_path)
|
92
|
+
log.debug "Loading '#{file_path}'..."
|
93
|
+
YAML::load(ERB.new(File.read(file_path)).result) || {}
|
94
|
+
rescue Psych::SyntaxError
|
95
|
+
log.error "Error parsing YAML file '#{file_path}'!"
|
96
|
+
raise
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/lib/qat/core.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'forwardable'
|
3
|
+
require 'singleton'
|
4
|
+
require_relative 'core/version'
|
5
|
+
|
6
|
+
module QAT
|
7
|
+
#Core shared memory class. Works as a singleton.
|
8
|
+
#
|
9
|
+
#Apart from a regular shared cache class with CRUD operations, it also has a reset functionality,
|
10
|
+
#allowing to define exception values that will never be reset.
|
11
|
+
#
|
12
|
+
#@since 0.1.0
|
13
|
+
class Core
|
14
|
+
include Singleton
|
15
|
+
extend Forwardable
|
16
|
+
|
17
|
+
def_delegators :@storage, :[], :[]=, :store, :delete
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@storage = {}
|
21
|
+
@exceptions = []
|
22
|
+
end
|
23
|
+
|
24
|
+
#Makes a key permanent in the cache, so that it won't deleted when {#reset!} is called.
|
25
|
+
#@param [Object] key Key to make permanent
|
26
|
+
#@see #reset!
|
27
|
+
def make_permanent key
|
28
|
+
@exceptions << key unless @exceptions.include? key
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
#Stores value to the given key and marks key as permanent, so that it won't deleted when {#reset!} is called.
|
33
|
+
#@param [Object] key Key to make permanent
|
34
|
+
#@param [Object] value Value to store in cache
|
35
|
+
#@see #reset!
|
36
|
+
def store_permanently key, value
|
37
|
+
make_permanent key
|
38
|
+
store key, value
|
39
|
+
end
|
40
|
+
|
41
|
+
#Deletes all keys not maked as permanent from the cache.
|
42
|
+
#@since 0.1.0
|
43
|
+
def reset!
|
44
|
+
@storage.select! { |key, _| @exceptions.include?(key) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class << self
|
49
|
+
extend Forwardable
|
50
|
+
def_delegators :'QAT::Core.instance', *(QAT::Core.public_instance_methods - Object.public_instance_methods)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
#QAT Module works as a namespace for all sub modules.
|
3
|
+
#Some singleton methods are also available, as defined by various sub classes.
|
4
|
+
#@since 0.1.0
|
5
|
+
module QAT
|
6
|
+
# Namespace for QAT Core implementation
|
7
|
+
class Core
|
8
|
+
# Represents QAT's Core version
|
9
|
+
VERSION = '6.0.0'
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# Integer Class extension
|
2
|
+
class Integer
|
3
|
+
# Generates a random integer with a given number of digits
|
4
|
+
# @param length [Integer] number of digits of random integer. Default is 1.
|
5
|
+
# @return [Integer]
|
6
|
+
# @since 1.2.0
|
7
|
+
def self.random(length=1)
|
8
|
+
raise(ArgumentError, 'Argument should be an Integer!') unless length.is_a?(Integer)
|
9
|
+
elements = (0..9).to_a
|
10
|
+
[(1..9).to_a.sample, (length-1).times.map { elements.sample }].flatten.join.to_i
|
11
|
+
end
|
12
|
+
end
|