hati-config 0.1.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/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +818 -0
- data/hati-config.gemspec +42 -0
- data/lib/hati_config/cache.rb +284 -0
- data/lib/hati_config/configuration.rb +140 -0
- data/lib/hati_config/encryption.rb +244 -0
- data/lib/hati_config/environment.rb +107 -0
- data/lib/hati_config/errors.rb +54 -0
- data/lib/hati_config/hati_configuration.rb +84 -0
- data/lib/hati_config/remote_loader.rb +86 -0
- data/lib/hati_config/schema.rb +213 -0
- data/lib/hati_config/setting.rb +389 -0
- data/lib/hati_config/team.rb +85 -0
- data/lib/hati_config/type_checker.rb +103 -0
- data/lib/hati_config/type_map.rb +72 -0
- data/lib/hati_config/version.rb +5 -0
- data/lib/hati_config.rb +15 -0
- metadata +123 -0
@@ -0,0 +1,389 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# HatiConfig module provides functionality for managing HatiConfig features.
|
7
|
+
module HatiConfig
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
9
|
+
|
10
|
+
# Setting class provides a configuration tree structure for managing settings.
|
11
|
+
#
|
12
|
+
# This class allows for dynamic configuration management, enabling
|
13
|
+
# the loading of settings from hashes, YAML, or JSON formats.
|
14
|
+
#
|
15
|
+
# @example Basic usage
|
16
|
+
# settings = Setting.new do
|
17
|
+
# config(:key1, value: "example")
|
18
|
+
# config(:key2, type: :int)
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
class Setting
|
22
|
+
extend HatiConfig::Environment
|
23
|
+
include HatiConfig::Environment
|
24
|
+
extend HatiConfig::Schema
|
25
|
+
extend HatiConfig::Cache
|
26
|
+
extend HatiConfig::Encryption
|
27
|
+
|
28
|
+
# Dynamically define methods for each type in TypeMap.
|
29
|
+
#
|
30
|
+
# @!method int(value)
|
31
|
+
# Sets an integer configuration value.
|
32
|
+
# @param value [Integer] The integer value to set.
|
33
|
+
#
|
34
|
+
# @!method string(value)
|
35
|
+
# Sets a string configuration value.
|
36
|
+
# @param value [String] The string value to set.
|
37
|
+
#
|
38
|
+
# ... (other type methods)
|
39
|
+
HatiConfig::TypeMap.list_types.each do |type|
|
40
|
+
define_method(type.downcase) do |stng, lock = nil|
|
41
|
+
params = { type: type }
|
42
|
+
params[:lock] = lock if lock.nil?
|
43
|
+
|
44
|
+
config(stng, **params)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Initializes a new Setting instance.
|
49
|
+
#
|
50
|
+
# @yield [self] Configures the instance upon creation if a block is given.
|
51
|
+
def initialize(&block)
|
52
|
+
@config_tree = {}
|
53
|
+
@schema = {}
|
54
|
+
@immutable_schema = {}
|
55
|
+
@encrypted_tree = {}
|
56
|
+
|
57
|
+
if self.class.encryption_config.key_provider
|
58
|
+
self.class.encryption do
|
59
|
+
key_provider :env
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
instance_eval(&block) if block_given?
|
64
|
+
end
|
65
|
+
|
66
|
+
# Loads configuration from a hash with an optional schema.
|
67
|
+
#
|
68
|
+
# @param data [Hash] The hash containing configuration data.
|
69
|
+
# @param schema [Hash] Optional schema for type validation.
|
70
|
+
# @raise [NoMethodError] If a method corresponding to a key is not defined.
|
71
|
+
# @raise [SettingTypeError] If a value doesn't match the specified type in the schema.
|
72
|
+
#
|
73
|
+
# @example Loading from a hash with type validation
|
74
|
+
# settings.load_from_hash({ name: "admin", max_connections: 10 }, schema: { name: :str, max_connections: :int })
|
75
|
+
def load_from_hash(data, schema: {}, lock_schema: {}, encrypted_fields: {})
|
76
|
+
data.each do |key, value|
|
77
|
+
key = key.to_sym
|
78
|
+
type = schema[key] if schema
|
79
|
+
lock = lock_schema[key] if lock_schema
|
80
|
+
encrypted = encrypted_fields[key] if encrypted_fields
|
81
|
+
|
82
|
+
if value.is_a?(Hash)
|
83
|
+
configure(key) do
|
84
|
+
load_from_hash(value,
|
85
|
+
schema: schema.is_a?(Hash) ? schema[key] : {},
|
86
|
+
lock_schema: lock_schema.is_a?(Hash) ? lock_schema[key] : {},
|
87
|
+
encrypted_fields: encrypted_fields.is_a?(Hash) ? encrypted_fields[key] : {})
|
88
|
+
end
|
89
|
+
elsif value.is_a?(Setting)
|
90
|
+
configure(key) do
|
91
|
+
load_from_hash(value.to_h, schema: schema[key], lock_schema: lock_schema[key],
|
92
|
+
encrypted_fields: encrypted_fields[key])
|
93
|
+
end
|
94
|
+
else
|
95
|
+
config(key => value, type: type, lock: lock, encrypted: encrypted)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Configures a node of the configuration tree.
|
101
|
+
#
|
102
|
+
# @param node [Symbol, String] The name of the config node key.
|
103
|
+
# @yield [Setting] A block that configures the new node.
|
104
|
+
#
|
105
|
+
# @example Configuring a new node
|
106
|
+
# settings.configure(:database) do
|
107
|
+
# config(:host, value: "localhost")
|
108
|
+
# config(:port, value: 5432)
|
109
|
+
# end
|
110
|
+
def configure(node, &block)
|
111
|
+
if config_tree[node]
|
112
|
+
config_tree[node].instance_eval(&block)
|
113
|
+
else
|
114
|
+
create_new_node(node, &block)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Configures a setting with a given name and type.
|
119
|
+
#
|
120
|
+
# @param setting [Symbol, Hash, nil] The name of the setting or a hash of settings.
|
121
|
+
# @param type [Symbol, nil] The expected type of the setting.
|
122
|
+
# @param opt [Hash] Additional options for configuration.
|
123
|
+
# @return [self] The current instance for method chaining.
|
124
|
+
# @raise [SettingTypeError] If the value does not match the expected type.
|
125
|
+
#
|
126
|
+
# @example Configuring a setting
|
127
|
+
# settings.config(max_connections: 10, type: :int)
|
128
|
+
def config(setting = nil, type: nil, lock: nil, encrypted: false, **opt)
|
129
|
+
return self if !setting && opt.empty?
|
130
|
+
|
131
|
+
# If setting is a symbol/string and we have keyword options, merge them
|
132
|
+
if (setting.is_a?(Symbol) || setting.is_a?(String)) && !opt.empty?
|
133
|
+
raw_stngs = opt.merge(setting => opt[:value])
|
134
|
+
raw_stngs.delete(:value)
|
135
|
+
else
|
136
|
+
raw_stngs = setting || opt
|
137
|
+
end
|
138
|
+
stngs = extract_setting_info(raw_stngs)
|
139
|
+
|
140
|
+
stng_lock = determine_lock(stngs, lock)
|
141
|
+
stng_type = determine_type(stngs, type)
|
142
|
+
stng_encrypted = determine_encrypted(stngs, encrypted)
|
143
|
+
|
144
|
+
if stng_encrypted
|
145
|
+
value = stngs[:value]
|
146
|
+
if value.nil? && config_tree[stngs[:name]]
|
147
|
+
value = config_tree[stngs[:name]]
|
148
|
+
value = self.class.encryption_config.decrypt(value) if @encrypted_tree[stngs[:name]]
|
149
|
+
end
|
150
|
+
|
151
|
+
if value.is_a?(HatiConfig::Setting)
|
152
|
+
# Handle nested settings
|
153
|
+
value.instance_eval(&block) if block_given?
|
154
|
+
elsif !value.nil?
|
155
|
+
# If we're setting a new value or updating an existing one
|
156
|
+
raise SettingTypeError.new('string (encrypted values must be strings)', value) unless value.is_a?(String)
|
157
|
+
|
158
|
+
stngs[:value] = self.class.encryption_config.encrypt(value)
|
159
|
+
@encrypted_tree[stngs[:name]] = true
|
160
|
+
# If we're just marking an existing value as encrypted
|
161
|
+
elsif config_tree[stngs[:name]]
|
162
|
+
value = config_tree[stngs[:name]]
|
163
|
+
raise SettingTypeError.new('string (encrypted values must be strings)', value) unless value.is_a?(String)
|
164
|
+
|
165
|
+
stngs[:value] = self.class.encryption_config.encrypt(value)
|
166
|
+
@encrypted_tree[stngs[:name]] = true
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
validate_and_set_configuration(stngs, stng_lock, stng_type, stng_encrypted)
|
171
|
+
self
|
172
|
+
end
|
173
|
+
|
174
|
+
# Returns the type schema of the configuration.
|
175
|
+
#
|
176
|
+
# @return [Hash] A hash representing the type schema.
|
177
|
+
# @example Retrieving the type schema
|
178
|
+
# schema = settings.type_schema
|
179
|
+
def type_schema
|
180
|
+
{}.tap do |hsh|
|
181
|
+
config_tree.each do |k, v|
|
182
|
+
v.is_a?(HatiConfig::Setting) ? (hsh[k] = v.type_schema) : hsh.merge!(schema)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def lock_schema
|
188
|
+
{}.tap do |hsh|
|
189
|
+
config_tree.each do |k, v|
|
190
|
+
v.is_a?(HatiConfig::Setting) ? (hsh[k] = v.lock_schema) : hsh.merge!(immutable_schema)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Converts the configuration tree into a hash.
|
196
|
+
#
|
197
|
+
# @return [Hash] The config tree as a hash.
|
198
|
+
# @example Converting to hash
|
199
|
+
# hash = settings.to_h
|
200
|
+
def to_h
|
201
|
+
{}.tap do |hsh|
|
202
|
+
config_tree.each do |k, v|
|
203
|
+
hsh[k] = if v.is_a?(HatiConfig::Setting)
|
204
|
+
v.to_h
|
205
|
+
else
|
206
|
+
get_value(k)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Converts the configuration tree into YAML format.
|
213
|
+
#
|
214
|
+
# @param dump [String, nil] Optional file path to dump the YAML.
|
215
|
+
# @return [String, nil] The YAML string or nil if dumped to a file.
|
216
|
+
# @example Converting to YAML
|
217
|
+
# yaml_string = settings.to_yaml
|
218
|
+
# settings.to_yaml(dump: "config.yml") # Dumps to a file
|
219
|
+
def to_yaml(dump: nil)
|
220
|
+
yaml = to_h.to_yaml
|
221
|
+
dump ? File.write(dump, yaml) : yaml
|
222
|
+
end
|
223
|
+
|
224
|
+
# Converts the configuration tree into JSON format.
|
225
|
+
#
|
226
|
+
# @return [String] The JSON representation of the configuration tree.
|
227
|
+
# @example Converting to JSON
|
228
|
+
# json_string = settings.to_json
|
229
|
+
def to_json(*_args)
|
230
|
+
to_h.to_json
|
231
|
+
end
|
232
|
+
|
233
|
+
# Provides hash-like access to configuration values
|
234
|
+
#
|
235
|
+
# @param key [Symbol, String] The key to access
|
236
|
+
# @return [Object] The value associated with the key
|
237
|
+
def [](key)
|
238
|
+
key = key.to_sym if key.is_a?(String)
|
239
|
+
return get_value(key) if config_tree.key?(key)
|
240
|
+
|
241
|
+
raise NoMethodError, "undefined method `[]' with key #{key} for #{self.class}"
|
242
|
+
end
|
243
|
+
|
244
|
+
# Sets a configuration value using hash-like syntax
|
245
|
+
#
|
246
|
+
# @param key [Symbol, String] The key to set
|
247
|
+
# @param value [Object] The value to set
|
248
|
+
def []=(key, value)
|
249
|
+
key = key.to_sym if key.is_a?(String)
|
250
|
+
config(key => value)
|
251
|
+
end
|
252
|
+
|
253
|
+
protected
|
254
|
+
|
255
|
+
# @return [Hash] The schema of configuration types.
|
256
|
+
attr_reader :schema, :immutable_schema
|
257
|
+
|
258
|
+
private
|
259
|
+
|
260
|
+
# @return [Hash] The tree structure of configuration settings.
|
261
|
+
attr_reader :config_tree
|
262
|
+
|
263
|
+
# Creates a new node in the configuration tree.
|
264
|
+
#
|
265
|
+
# @param node [Symbol] The name of the node to create.
|
266
|
+
# @yield [Setting] A block to configure the new setting.
|
267
|
+
# @return [Setting] The newly created setting node.
|
268
|
+
def create_new_node(node, &block)
|
269
|
+
new_node = HatiConfig::Setting.new
|
270
|
+
if self.class.encryption_config.key_provider
|
271
|
+
new_node.class.encryption do
|
272
|
+
key_provider :env
|
273
|
+
end
|
274
|
+
end
|
275
|
+
new_node.instance_eval(&block) if block_given?
|
276
|
+
config_tree[node] = new_node
|
277
|
+
define_node_methods(node)
|
278
|
+
new_node
|
279
|
+
end
|
280
|
+
|
281
|
+
# Defines singleton methods for the given node.
|
282
|
+
#
|
283
|
+
# @param node [Symbol] The name of the node to define methods for.
|
284
|
+
def define_node_methods(node)
|
285
|
+
define_singleton_method(node) do |*_args, &node_block|
|
286
|
+
if node_block
|
287
|
+
config_tree[node].instance_eval(&node_block)
|
288
|
+
else
|
289
|
+
config_tree[node]
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Extracts the setting information from the provided input.
|
295
|
+
#
|
296
|
+
# @param stngs [Symbol, Hash] The setting name or a hash containing the setting name and value.
|
297
|
+
# @return [Hash] A hash containing the setting name and its corresponding value.
|
298
|
+
def extract_setting_info(stngs)
|
299
|
+
val = nil
|
300
|
+
lock = nil
|
301
|
+
encrypted = nil
|
302
|
+
|
303
|
+
if stngs.is_a?(Symbol)
|
304
|
+
name = stngs
|
305
|
+
elsif stngs.is_a?(Hash)
|
306
|
+
lock = stngs.delete(:lock)
|
307
|
+
encrypted = stngs.delete(:encrypted)
|
308
|
+
name, val = stngs.to_a.first
|
309
|
+
end
|
310
|
+
|
311
|
+
{ name: name, value: val, lock: lock, encrypted: encrypted }
|
312
|
+
end
|
313
|
+
|
314
|
+
def handle_value(key, value, type, lock, encrypted = false)
|
315
|
+
validate_mutable!(key, lock) if lock
|
316
|
+
validate_setting!(value, type) if type
|
317
|
+
|
318
|
+
config(key => value, type: type, lock: lock, encrypted: encrypted)
|
319
|
+
self
|
320
|
+
end
|
321
|
+
|
322
|
+
def get_value(key)
|
323
|
+
value = config_tree[key]
|
324
|
+
return value if value.is_a?(HatiConfig::Setting)
|
325
|
+
return self.class.encryption_config.decrypt(value) if @encrypted_tree[key]
|
326
|
+
|
327
|
+
value
|
328
|
+
end
|
329
|
+
|
330
|
+
# Validates the setting value against the expected type.
|
331
|
+
#
|
332
|
+
# @param stng_val [Object] The value of the setting to validate.
|
333
|
+
# @param stng_type [Symbol] The expected type of the setting.
|
334
|
+
# @raise [SettingTypeError] If the setting value does not match the expected type.
|
335
|
+
def validate_setting!(stng_val, stng_type)
|
336
|
+
is_valid = HatiConfig::TypeChecker.call(stng_val, type: stng_type)
|
337
|
+
raise HatiConfig::SettingTypeError.new(stng_type, stng_val) unless !stng_val || is_valid
|
338
|
+
end
|
339
|
+
|
340
|
+
def validate_mutable!(name, stng_lock)
|
341
|
+
raise "<#{name}> setting is immutable" if stng_lock
|
342
|
+
end
|
343
|
+
|
344
|
+
# Sets the configuration for a given setting name, value, and type.
|
345
|
+
#
|
346
|
+
# @param stng_name [Symbol] The name of the setting to configure.
|
347
|
+
# @param stng_val [Object] The value to assign to the setting.
|
348
|
+
# @param stng_type [Symbol] The type of the setting.
|
349
|
+
def set_configuration(stng_name:, stng_val:, stng_type:, stng_lock:)
|
350
|
+
schema[stng_name] = stng_type
|
351
|
+
config_tree[stng_name] = stng_val
|
352
|
+
immutable_schema[stng_name] = stng_lock
|
353
|
+
|
354
|
+
return if respond_to?(stng_name)
|
355
|
+
|
356
|
+
define_singleton_method(stng_name) do |value = nil, type: nil, lock: nil, encrypted: false|
|
357
|
+
return get_value(stng_name) unless value || encrypted
|
358
|
+
|
359
|
+
if encrypted && !value.nil? && !value.is_a?(String)
|
360
|
+
raise SettingTypeError.new('string (encrypted values must be strings)', value)
|
361
|
+
end
|
362
|
+
|
363
|
+
config(**{ stng_name => value, type: type, lock: lock, encrypted: encrypted })
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
def determine_lock(stngs, lock)
|
368
|
+
lock.nil? ? stngs[:lock] : lock
|
369
|
+
end
|
370
|
+
|
371
|
+
def determine_type(stngs, type)
|
372
|
+
type || schema[stngs[:name]] || :any
|
373
|
+
end
|
374
|
+
|
375
|
+
def determine_encrypted(stngs, encrypted)
|
376
|
+
encrypted.nil? ? stngs[:encrypted] : encrypted
|
377
|
+
end
|
378
|
+
|
379
|
+
def validate_and_set_configuration(stngs, stng_lock, stng_type, stng_encrypted)
|
380
|
+
validate_mutable!(stngs[:name], stng_lock) if stngs[:value] && config_tree[stngs[:name]] && !stng_lock
|
381
|
+
validate_setting!(stngs[:value], stng_type)
|
382
|
+
|
383
|
+
set_configuration(stng_name: stngs[:name], stng_val: stngs[:value], stng_type: stng_type, stng_lock: stng_lock)
|
384
|
+
@encrypted_tree[stngs[:name]] = stng_encrypted if stng_encrypted
|
385
|
+
self
|
386
|
+
end
|
387
|
+
end
|
388
|
+
# rubocop:enable Metrics/ClassLength
|
389
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HatiConfig
|
4
|
+
# Team module provides functionality for managing team-specific configurations.
|
5
|
+
module Team
|
6
|
+
# Defines team-specific configuration namespace.
|
7
|
+
#
|
8
|
+
# @param team_name [Symbol] The name of the team (e.g., :frontend, :backend, :mobile)
|
9
|
+
# @yield The configuration block for the team
|
10
|
+
# @example
|
11
|
+
# team :frontend do
|
12
|
+
# configure :settings do
|
13
|
+
# config api_endpoint: '/api/v1'
|
14
|
+
# config cache_ttl: 300
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
def team(team_name, &block)
|
18
|
+
team_module = Module.new do
|
19
|
+
extend HatiConfig::Configuration
|
20
|
+
extend HatiConfig::Environment
|
21
|
+
end
|
22
|
+
|
23
|
+
const_name = team_name.to_s.capitalize
|
24
|
+
const_set(const_name, team_module)
|
25
|
+
team_module.instance_eval(&block) if block_given?
|
26
|
+
|
27
|
+
# Define method for accessing team module
|
28
|
+
singleton_class.class_eval do
|
29
|
+
define_method(team_name) { const_get(const_name) }
|
30
|
+
end
|
31
|
+
|
32
|
+
team_module
|
33
|
+
end
|
34
|
+
|
35
|
+
# Gets a list of all defined teams.
|
36
|
+
#
|
37
|
+
# @return [Array<Symbol>] The list of team names
|
38
|
+
def teams
|
39
|
+
constants.select { |c| const_get(c).is_a?(Module) && const_get(c).respond_to?(:configure) }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Gets a specific team's configuration module.
|
43
|
+
#
|
44
|
+
# @param team_name [Symbol] The name of the team
|
45
|
+
# @return [Module] The team's configuration module
|
46
|
+
# @raise [NameError] If the team does not exist
|
47
|
+
def [](team_name)
|
48
|
+
const_get(team_name.to_s.capitalize)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Checks if a team exists.
|
52
|
+
#
|
53
|
+
# @param team_name [Symbol] The name of the team
|
54
|
+
# @return [Boolean] True if the team exists
|
55
|
+
def team?(team_name)
|
56
|
+
const_defined?(team_name.to_s.capitalize)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Removes a team's configuration.
|
60
|
+
#
|
61
|
+
# @param team_name [Symbol] The name of the team
|
62
|
+
# @return [Boolean] True if the team was removed
|
63
|
+
def remove_team?(team_name)
|
64
|
+
const_name = team_name.to_s.capitalize
|
65
|
+
return false unless const_defined?(const_name)
|
66
|
+
|
67
|
+
remove_const(const_name)
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
# Temporarily switches to a team's configuration context.
|
72
|
+
#
|
73
|
+
# @param team_name [Symbol] The name of the team
|
74
|
+
# @yield The block to execute in the team's context
|
75
|
+
# @example
|
76
|
+
# with_team(:frontend) do
|
77
|
+
# # Configuration will use frontend team's context here
|
78
|
+
# end
|
79
|
+
def with_team(team_name)
|
80
|
+
raise NameError, "Team '#{team_name}' does not exist" unless team?(team_name)
|
81
|
+
|
82
|
+
yield self[team_name]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# HatiConfig module provides functionality for managing HatiConfig features.
|
4
|
+
module HatiConfig
|
5
|
+
# This class is responsible for type checking in the Hati configuration.
|
6
|
+
class TypeChecker
|
7
|
+
class << self
|
8
|
+
# Calls the appropriate validation method based on the type.
|
9
|
+
#
|
10
|
+
# @param value [Object] The value to validate.
|
11
|
+
# @param type [Symbol, Array<Symbol, Class>, Class] The type(s) to validate against.
|
12
|
+
# @return [Boolean] True if the value matches the type, false otherwise.
|
13
|
+
# @raise [TypeCheckerError] if the type is unsupported or not defined.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# TypeChecker.call(1, type: :int) # => true
|
17
|
+
# TypeChecker.call(1, type: :numeric) # => true
|
18
|
+
# TypeChecker.call("hello", type: [:str, Integer]) # => true
|
19
|
+
# TypeChecker.call(CustomClass.new, type: CustomClass) # => true
|
20
|
+
def call(value, type:, **_opts)
|
21
|
+
case type
|
22
|
+
when Symbol
|
23
|
+
base_type(value, fetch_type(type))
|
24
|
+
when Array
|
25
|
+
if type.length == 1 && type.first.is_a?(Symbol)
|
26
|
+
# Array type validation (e.g., [:string] for array of strings)
|
27
|
+
return false unless value.is_a?(Array)
|
28
|
+
|
29
|
+
value.all? { |v| call(v, type: type.first) }
|
30
|
+
else
|
31
|
+
# Union type validation (e.g., [:string, Integer] for string or integer)
|
32
|
+
one_of(value, type)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
custom_type?(value, type)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Validates if value matches the base type.
|
40
|
+
#
|
41
|
+
# @param value [Object] the value to validate
|
42
|
+
# @param type [Class] the type to validate against
|
43
|
+
# @return [Boolean] true if the value matches the base type
|
44
|
+
# @raise [TypeCheckerError] if the type is unsupported or not defined.
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# TypeChecker.base_type(1, Integer) # => true
|
48
|
+
# TypeChecker.base_type(1, [:int, :float, :big_decimal]) # => true
|
49
|
+
def base_type(value, type)
|
50
|
+
type = fetch_type(type) if type.is_a?(Symbol)
|
51
|
+
|
52
|
+
return type.call(value) if type.is_a?(Proc)
|
53
|
+
|
54
|
+
type.is_a?(Array) ? one_of(value, type) : value.is_a?(type)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Checks if value matches any type in the array.
|
58
|
+
#
|
59
|
+
# @param value [Object] the value to validate
|
60
|
+
# @param array_type [Array<Symbol, Class>] the array of types to validate against
|
61
|
+
# @return [Boolean] true if the value matches any type in the array
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# TypeChecker.one_of(1, [Integer, :str]) # => true
|
65
|
+
# TypeChecker.one_of("hello", [Integer, String, :sym]) # => true
|
66
|
+
# TypeChecker.one_of(nil, [:null, Integer, String]) # => true
|
67
|
+
# TypeChecker.one_of(1.5, [:int, :float, :big_decimal]) # => true
|
68
|
+
def one_of(value, array_type)
|
69
|
+
array_type.any? { |type| type.is_a?(Symbol) ? base_type(value, type) : custom_type?(value, type) }
|
70
|
+
end
|
71
|
+
|
72
|
+
# Validates if value is of the specified custom type.
|
73
|
+
#
|
74
|
+
# @param value [Object] the value to validate
|
75
|
+
# @param type [Class] the custom type to validate against
|
76
|
+
# @return [Boolean] true if the value is of the specified custom type
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
# TypeChecker.custom_type(1, Integer) # => true
|
80
|
+
# TypeChecker.custom_type(CustomClass.new, CustomClass) # => true
|
81
|
+
def custom_type?(value, type)
|
82
|
+
value.is_a?(type)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Fetches the basic type from the type map.
|
86
|
+
#
|
87
|
+
# @param type [Symbol] the type to fetch
|
88
|
+
# @return [Class] the corresponding basic type
|
89
|
+
# @raise [TypeCheckerError] if the type does not exist in TYPE_MAP
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
# TypeChecker.fetch_type(:int) # => Integer
|
93
|
+
# TypeChecker.fetch_type(:null) # => NilClass
|
94
|
+
# TypeChecker.fetch_type(:bool) # => [:truthy, :falsy]
|
95
|
+
def fetch_type(type)
|
96
|
+
basic_type = TypeMap.get(type)
|
97
|
+
raise HatiConfig::TypeCheckerError, type unless basic_type
|
98
|
+
|
99
|
+
basic_type
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
# HatiConfig module provides functionality for managing HatiConfig features.
|
7
|
+
module HatiConfig
|
8
|
+
# A class that maps type symbols to their corresponding Ruby classes and provides
|
9
|
+
# predefined collections of type symbols for various categories.
|
10
|
+
#
|
11
|
+
# This class allows retrieval of Ruby classes based on type symbols and provides
|
12
|
+
# methods to access collections of specific type symbols such as booleans, numerics,
|
13
|
+
# and chronological types.
|
14
|
+
class TypeMap
|
15
|
+
TYPE_MAP = {
|
16
|
+
# base types
|
17
|
+
int: Integer,
|
18
|
+
integer: Integer,
|
19
|
+
str: String,
|
20
|
+
string: String,
|
21
|
+
sym: Symbol,
|
22
|
+
null: NilClass,
|
23
|
+
true_class: TrueClass,
|
24
|
+
false_class: FalseClass,
|
25
|
+
# data structures
|
26
|
+
hash: Hash,
|
27
|
+
array: Array,
|
28
|
+
# numeric types
|
29
|
+
big_decimal: BigDecimal,
|
30
|
+
float: Float,
|
31
|
+
complex: Complex,
|
32
|
+
rational: Rational,
|
33
|
+
# time types
|
34
|
+
date: Date,
|
35
|
+
date_time: DateTime,
|
36
|
+
time: Time,
|
37
|
+
# any type
|
38
|
+
any: Object,
|
39
|
+
# composite types
|
40
|
+
bool: %i[true_class false_class],
|
41
|
+
numeric: %i[int float big_decimal],
|
42
|
+
kernel_num: %i[int float big_decimal complex rational],
|
43
|
+
chrono: %i[date date_time time]
|
44
|
+
}.freeze
|
45
|
+
|
46
|
+
# Retrieves the Ruby class associated with a given type symbol.
|
47
|
+
#
|
48
|
+
# @param type [Symbol] The type symbol to look up. Must be one of the keys in TYPE_MAP.
|
49
|
+
# @return [Class, nil] The corresponding Ruby class or nil if the type symbol is not found.
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# TypeMap.get(:int) # => Integer
|
53
|
+
# TypeMap.get(:str) # => String
|
54
|
+
# TypeMap.get(:unknown) # => nil
|
55
|
+
def self.get(type)
|
56
|
+
TYPE_MAP[type]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns an array of all type symbols defined in TYPE_MAP.
|
60
|
+
#
|
61
|
+
# @return [Array<Symbol>] An array containing all keys from TYPE_MAP.
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# TypeMap.list_types # => [:int, :str, :sym, :null, :true_class, :false_class,
|
65
|
+
# :hash, :array, :big_decimal, :float, :complex,
|
66
|
+
# :rational, :date, :date_time, :time, :any,
|
67
|
+
# :bool, :numeric, :kernel_num, :chrono]
|
68
|
+
def self.list_types
|
69
|
+
TYPE_MAP.keys
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/hati_config.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hati_config/version'
|
4
|
+
require 'hati_config/errors'
|
5
|
+
require 'hati_config/type_map'
|
6
|
+
require 'hati_config/type_checker'
|
7
|
+
require 'hati_config/remote_loader'
|
8
|
+
require 'hati_config/environment'
|
9
|
+
require 'hati_config/team'
|
10
|
+
require 'hati_config/schema'
|
11
|
+
require 'hati_config/cache'
|
12
|
+
require 'hati_config/encryption'
|
13
|
+
require 'hati_config/setting'
|
14
|
+
require 'hati_config/configuration'
|
15
|
+
require 'hati_config/hati_configuration'
|