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.
@@ -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