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,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HatiConfig
|
4
|
+
# Environment module provides functionality for managing environment-specific configurations.
|
5
|
+
module Environment
|
6
|
+
# Defines environment-specific configuration overrides.
|
7
|
+
#
|
8
|
+
# @param env_name [Symbol] The name of the environment (e.g., :development, :staging, :production)
|
9
|
+
# @yield The configuration block for the environment
|
10
|
+
# @example
|
11
|
+
# environment :development do
|
12
|
+
# config api_url: 'http://localhost:3000'
|
13
|
+
# config debug: true
|
14
|
+
# end
|
15
|
+
def environment(env_name, &block)
|
16
|
+
return unless current_environment == env_name
|
17
|
+
|
18
|
+
instance_eval(&block)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Sets the current environment.
|
22
|
+
#
|
23
|
+
# @param env [Symbol] The environment to set
|
24
|
+
# @example
|
25
|
+
# HatiConfig.environment = :production
|
26
|
+
def self.current_environment=(env)
|
27
|
+
@current_environment = env&.to_sym
|
28
|
+
end
|
29
|
+
|
30
|
+
# Gets the current environment.
|
31
|
+
#
|
32
|
+
# @return [Symbol] The current environment
|
33
|
+
def self.current_environment
|
34
|
+
@current_environment ||= begin
|
35
|
+
env = if ENV['HATI_ENV']
|
36
|
+
ENV['HATI_ENV']
|
37
|
+
elsif ENV['RACK_ENV']
|
38
|
+
ENV['RACK_ENV']
|
39
|
+
elsif ENV['RAILS_ENV']
|
40
|
+
ENV['RAILS_ENV']
|
41
|
+
else
|
42
|
+
'development'
|
43
|
+
end
|
44
|
+
env.to_sym
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Gets the current environment.
|
49
|
+
#
|
50
|
+
# @return [Symbol] The current environment
|
51
|
+
def current_environment
|
52
|
+
Environment.current_environment
|
53
|
+
end
|
54
|
+
|
55
|
+
# Temporarily changes the environment for a block of code.
|
56
|
+
#
|
57
|
+
# @param env [Symbol] The environment to use
|
58
|
+
# @yield The block to execute in the specified environment
|
59
|
+
# @example
|
60
|
+
# HatiConfig.with_environment(:staging) do
|
61
|
+
# # Configuration will use staging environment here
|
62
|
+
# end
|
63
|
+
def self.with_environment(env)
|
64
|
+
original_env = current_environment
|
65
|
+
self.current_environment = env
|
66
|
+
yield
|
67
|
+
ensure
|
68
|
+
self.current_environment = original_env
|
69
|
+
end
|
70
|
+
|
71
|
+
# Checks if the current environment matches the given environment.
|
72
|
+
#
|
73
|
+
# @param env [Symbol] The environment to check
|
74
|
+
# @return [Boolean] True if the current environment matches
|
75
|
+
def environment?(env)
|
76
|
+
current_environment == env.to_sym
|
77
|
+
end
|
78
|
+
|
79
|
+
# Checks if the current environment is development.
|
80
|
+
#
|
81
|
+
# @return [Boolean] True if in development environment
|
82
|
+
def development?
|
83
|
+
environment?(:development)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Checks if the current environment is test.
|
87
|
+
#
|
88
|
+
# @return [Boolean] True if in test environment
|
89
|
+
def test?
|
90
|
+
environment?(:test)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Checks if the current environment is staging.
|
94
|
+
#
|
95
|
+
# @return [Boolean] True if in staging environment
|
96
|
+
def staging?
|
97
|
+
environment?(:staging)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Checks if the current environment is production.
|
101
|
+
#
|
102
|
+
# @return [Boolean] True if in production environment
|
103
|
+
def production?
|
104
|
+
environment?(:production)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# HatiConfig module provides functionality for managing HatiConfig features.
|
4
|
+
module HatiConfig
|
5
|
+
# Custom error raised when a setting type does not match the expected type.
|
6
|
+
#
|
7
|
+
# @example Raising an SettingTypeError
|
8
|
+
# raise SettingTypeError.new(:int, "string")
|
9
|
+
# # => raises SettingTypeError with message
|
10
|
+
# # "Expected: <int>. Given: \"string\" which is <String> class."
|
11
|
+
class SettingTypeError < TypeError
|
12
|
+
# Initializes a new SettingTypeError.
|
13
|
+
#
|
14
|
+
# @param type [Symbol] The expected type.
|
15
|
+
# @param val [Object] The value that was provided.
|
16
|
+
def initialize(type, val)
|
17
|
+
super(type_error_msg(type, val))
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generates the error message for the exception.
|
21
|
+
#
|
22
|
+
# @param type [Symbol] The expected type.
|
23
|
+
# @param val [Object] The value that was provided.
|
24
|
+
# @return [String] The formatted error message.
|
25
|
+
def type_error_msg(type, val)
|
26
|
+
"Expected: <#{type}>. Given: #{val.inspect} which is <#{val.class}> class."
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Custom error raised when a type definition is missing.
|
31
|
+
#
|
32
|
+
# @example Raising a TypeCheckerError
|
33
|
+
# raise TypeCheckerError.new(:unknown_type)
|
34
|
+
# # => raises TypeCheckerError with message "No type Definition for: <unknown_type> type"
|
35
|
+
class TypeCheckerError < StandardError
|
36
|
+
# Initializes a new TypeCheckerError.
|
37
|
+
#
|
38
|
+
# @param type [Symbol] The type that is missing a definition.
|
39
|
+
def initialize(type)
|
40
|
+
super(definition_error_msg(type))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Generates the error message for the exception.
|
44
|
+
#
|
45
|
+
# @param type [Symbol] The type that is missing a definition.
|
46
|
+
# @return [String] The formatted error message.
|
47
|
+
def definition_error_msg(type)
|
48
|
+
"No type Definition for: <#{type}> type"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Custom error raised when data loading fails.
|
53
|
+
class LoadDataError < StandardError; end
|
54
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# HatiConfig module provides functionality for managing configuration features.
|
4
|
+
#
|
5
|
+
# @example Using HatiConfiguration in a class
|
6
|
+
# class MyApp
|
7
|
+
# include HatiConfiguration
|
8
|
+
#
|
9
|
+
# configure :settings do
|
10
|
+
# config api_key: "default_key"
|
11
|
+
# config max_retries: 3
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# MyApp.settings.api_key # => "default_key"
|
16
|
+
# MyApp.settings.max_retries # => 3
|
17
|
+
#
|
18
|
+
# @example Configuring with a block
|
19
|
+
# class AnotherApp
|
20
|
+
# include HatiConfiguration
|
21
|
+
#
|
22
|
+
# configure :app_settings do
|
23
|
+
# config feature_enabled: true
|
24
|
+
# config max_users: 100
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# AnotherApp.app_settings.feature_enabled # => true
|
29
|
+
# AnotherApp.app_settings.max_users # => 100
|
30
|
+
module HatiConfiguration
|
31
|
+
# Extends the base class with HatiConfig::Configuration and HatiConfig::Team module methods
|
32
|
+
#
|
33
|
+
# @param base [Class] The class extending this module
|
34
|
+
# @return [void]
|
35
|
+
def self.extended(base)
|
36
|
+
base.extend(HatiConfig::Configuration)
|
37
|
+
base.extend(HatiConfig::Team)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Isolated module handles isolated configurations.
|
41
|
+
#
|
42
|
+
# This module allows for configuration inheritance while maintaining
|
43
|
+
# isolation between parent and child configurations.
|
44
|
+
#
|
45
|
+
# @example Using Isolated configurations
|
46
|
+
# class ParentApp
|
47
|
+
# include HatiConfiguration::Isolated
|
48
|
+
#
|
49
|
+
# configure :parent_settings do
|
50
|
+
# config timeout: 30
|
51
|
+
# config tries: 3
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# class ChildApp < ParentApp
|
56
|
+
# parent_settings do
|
57
|
+
# config tries: 4
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# ChildApp.parent_settings.timeout # => 30
|
62
|
+
# ChildApp.parent_settings.tries # => 4
|
63
|
+
# ParentApp.parent_settings.tries # => 3
|
64
|
+
#
|
65
|
+
# @example Configuring a child class
|
66
|
+
# class AnotherChildApp < ParentApp
|
67
|
+
# parent_settings do
|
68
|
+
# config timeout: 60
|
69
|
+
# end
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# AnotherChildApp.parent_settings.timeout # => 60
|
73
|
+
# AnotherChildApp.parent_settings.tries # => 3
|
74
|
+
module Local
|
75
|
+
# Extends the base class with HatiConfig::Configuration and HatiConfig::Configuration::Isolated module methods
|
76
|
+
#
|
77
|
+
# @param base [Class] The class extending this module
|
78
|
+
# @return [void]
|
79
|
+
def self.extended(base)
|
80
|
+
base.extend(HatiConfig::Configuration)
|
81
|
+
base.extend(HatiConfig::Configuration::Local)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'aws-sdk-s3'
|
5
|
+
require 'redis'
|
6
|
+
require 'json'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
module HatiConfig
|
10
|
+
# RemoteLoader handles loading configurations from remote sources like HTTP, S3, and Redis.
|
11
|
+
# It supports automatic refresh and caching of configurations.
|
12
|
+
class RemoteLoader
|
13
|
+
class << self
|
14
|
+
# Loads configuration from an HTTP endpoint
|
15
|
+
#
|
16
|
+
# @param url [String] The URL to load the configuration from
|
17
|
+
# @param headers [Hash] Optional headers to include in the request
|
18
|
+
# @param refresh_interval [Integer] Optional interval in seconds to refresh the configuration
|
19
|
+
# @return [Hash] The loaded configuration
|
20
|
+
# @raise [LoadDataError] If the configuration cannot be loaded
|
21
|
+
def from_http(url:, headers: {})
|
22
|
+
uri = URI(url)
|
23
|
+
request = Net::HTTP::Get.new(uri)
|
24
|
+
headers.each { |key, value| request[key] = value }
|
25
|
+
|
26
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
27
|
+
http.request(request)
|
28
|
+
end
|
29
|
+
|
30
|
+
parse_response(response.body, File.extname(uri.path))
|
31
|
+
rescue StandardError => e
|
32
|
+
raise LoadDataError, "Failed to load configuration from HTTP: #{e.message}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Loads configuration from an S3 bucket
|
36
|
+
#
|
37
|
+
# @param bucket [String] The S3 bucket name
|
38
|
+
# @param key [String] The S3 object key
|
39
|
+
# @param region [String] The AWS region
|
40
|
+
# @param refresh_interval [Integer] Optional interval in seconds to refresh the configuration
|
41
|
+
# @return [Hash] The loaded configuration
|
42
|
+
# @raise [LoadDataError] If the configuration cannot be loaded
|
43
|
+
def from_s3(bucket:, key:, region:)
|
44
|
+
s3 = Aws::S3::Client.new(region: region)
|
45
|
+
response = s3.get_object(bucket: bucket, key: key)
|
46
|
+
parse_response(response.body.read, File.extname(key))
|
47
|
+
rescue Aws::S3::Errors::ServiceError => e
|
48
|
+
raise LoadDataError, "Failed to load configuration from S3: #{e.message}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Loads configuration from Redis
|
52
|
+
#
|
53
|
+
# @param host [String] The Redis host
|
54
|
+
# @param key [String] The Redis key
|
55
|
+
# @param port [Integer] The Redis port (default: 6379)
|
56
|
+
# @param db [Integer] The Redis database number (default: 0)
|
57
|
+
# @param refresh_interval [Integer] Optional interval in seconds to refresh the configuration
|
58
|
+
# @return [Hash] The loaded configuration
|
59
|
+
# @raise [LoadDataError] If the configuration cannot be loaded
|
60
|
+
def from_redis(host:, key:, port: 6379, db: 0)
|
61
|
+
redis = Redis.new(host: host, port: port, db: db)
|
62
|
+
data = redis.get(key)
|
63
|
+
raise LoadDataError, "Key '#{key}' not found in Redis" unless data
|
64
|
+
|
65
|
+
parse_response(data)
|
66
|
+
rescue Redis::BaseError => e
|
67
|
+
raise LoadDataError, "Failed to load configuration from Redis: #{e.message}"
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def parse_response(data, extension = nil)
|
73
|
+
case extension&.downcase
|
74
|
+
when '.json', nil
|
75
|
+
JSON.parse(data, symbolize_names: true)
|
76
|
+
when '.yaml', '.yml'
|
77
|
+
YAML.safe_load(data, symbolize_names: true)
|
78
|
+
else
|
79
|
+
raise LoadDataError, "Unsupported file format: #{extension}"
|
80
|
+
end
|
81
|
+
rescue JSON::ParserError, Psych::SyntaxError => e
|
82
|
+
raise LoadDataError, "Failed to parse configuration: #{e.message}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HatiConfig
|
4
|
+
# Schema module provides functionality for managing configuration schemas and versioning.
|
5
|
+
module Schema
|
6
|
+
# Defines a schema for configuration validation.
|
7
|
+
#
|
8
|
+
# @param version [String] The schema version (e.g., "1.0", "2.0")
|
9
|
+
# @yield The schema definition block
|
10
|
+
# @example
|
11
|
+
# schema version: "1.0" do
|
12
|
+
# required :database_url, type: :string
|
13
|
+
# optional :pool_size, type: :integer, default: 5
|
14
|
+
# deprecated :old_setting, since: "1.0", remove_in: "2.0"
|
15
|
+
# end
|
16
|
+
def schema(version: '1.0', &block)
|
17
|
+
@schema_version = version
|
18
|
+
@schema_definition = SchemaDefinition.new(version)
|
19
|
+
@schema_definition.instance_eval(&block) if block_given?
|
20
|
+
@schema_definition
|
21
|
+
end
|
22
|
+
|
23
|
+
# Gets the current schema version.
|
24
|
+
#
|
25
|
+
# @return [String] The current schema version
|
26
|
+
def schema_version
|
27
|
+
@schema_version || '1.0'
|
28
|
+
end
|
29
|
+
|
30
|
+
# Gets the schema definition.
|
31
|
+
#
|
32
|
+
# @return [SchemaDefinition] The schema definition
|
33
|
+
def schema_definition
|
34
|
+
@schema_definition ||= SchemaDefinition.new(schema_version)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Defines a migration between schema versions.
|
38
|
+
#
|
39
|
+
# @param versions [Hash] The from and to versions (e.g., "1.0" => "2.0")
|
40
|
+
# @yield [config] The migration block
|
41
|
+
# @example
|
42
|
+
# migration "1.0" => "2.0" do |config|
|
43
|
+
# config.replica_urls = [config.delete(:backup_url)].compact
|
44
|
+
# end
|
45
|
+
def migration(versions, &block)
|
46
|
+
schema_definition.add_migration(versions, block)
|
47
|
+
end
|
48
|
+
|
49
|
+
# SchemaDefinition class handles schema validation and migration.
|
50
|
+
class SchemaDefinition
|
51
|
+
attr_reader :version, :required_fields, :optional_fields, :deprecated_fields, :migrations
|
52
|
+
|
53
|
+
def initialize(version)
|
54
|
+
@version = version
|
55
|
+
@required_fields = {}
|
56
|
+
@optional_fields = {}
|
57
|
+
@deprecated_fields = {}
|
58
|
+
@migrations = {}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Defines a field in the schema.
|
62
|
+
#
|
63
|
+
# @param name [Symbol] The field name
|
64
|
+
# @param type [Symbol, Class] The field type
|
65
|
+
# @param required [Boolean] Whether the field is required
|
66
|
+
# @param default [Object] The default value for optional fields
|
67
|
+
# @param since [String] The version since this field is available
|
68
|
+
def field(name, type:, required: true, default: nil, since: version)
|
69
|
+
if required
|
70
|
+
required(name, type: type, since: since)
|
71
|
+
else
|
72
|
+
optional(name, type: type, default: default, since: since)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Defines a required field in the schema.
|
77
|
+
#
|
78
|
+
# @param name [Symbol] The field name
|
79
|
+
# @param type [Symbol, Class] The field type
|
80
|
+
# @param since [String] The version since this field is required
|
81
|
+
def required(name, type:, since: version)
|
82
|
+
@required_fields[name] = { type: type, since: since }
|
83
|
+
end
|
84
|
+
|
85
|
+
# Defines an optional field in the schema.
|
86
|
+
#
|
87
|
+
# @param name [Symbol] The field name
|
88
|
+
# @param type [Symbol, Class] The field type
|
89
|
+
# @param default [Object] The default value
|
90
|
+
# @param since [String] The version since this field is available
|
91
|
+
def optional(name, type:, default: nil, since: version)
|
92
|
+
@optional_fields[name] = { type: type, default: default, since: since }
|
93
|
+
end
|
94
|
+
|
95
|
+
# Marks a field as deprecated.
|
96
|
+
#
|
97
|
+
# @param name [Symbol] The field name
|
98
|
+
# @param since [String] The version since this field is deprecated
|
99
|
+
# @param remove_in [String] The version when this field will be removed
|
100
|
+
def deprecated(name, since:, remove_in:)
|
101
|
+
@deprecated_fields[name] = { since: since, remove_in: remove_in }
|
102
|
+
end
|
103
|
+
|
104
|
+
# Adds a migration between versions.
|
105
|
+
#
|
106
|
+
# @param from_version [String] The source version
|
107
|
+
# @param to_version [String] The target version
|
108
|
+
# @param block [Proc] The migration block
|
109
|
+
def add_migration(from_version, to_version = nil, block = nil, &implicit_block)
|
110
|
+
if block.nil? && to_version.respond_to?(:call)
|
111
|
+
# Handle the case where to_version is the block
|
112
|
+
block = to_version
|
113
|
+
if from_version.is_a?(Hash)
|
114
|
+
from_version, to_version = from_version.first
|
115
|
+
else
|
116
|
+
from_version, to_version = from_version.to_s.gsub(/['"{}]/, '').split('=>').map(&:strip)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
migration_block = block || implicit_block
|
121
|
+
raise MigrationError, 'Invalid migration format' unless from_version && to_version && migration_block
|
122
|
+
|
123
|
+
key = migration_key(from_version, to_version)
|
124
|
+
@migrations[key] = migration_block
|
125
|
+
end
|
126
|
+
|
127
|
+
def migration(versions, &block)
|
128
|
+
if versions.is_a?(Hash)
|
129
|
+
from_version, to_version = versions.first
|
130
|
+
else
|
131
|
+
from_version, to_version = versions.to_s.gsub(/['"{}]/, '').split('=>').map(&:strip)
|
132
|
+
end
|
133
|
+
raise MigrationError, 'Invalid migration format' unless from_version && to_version
|
134
|
+
|
135
|
+
add_migration(from_version, to_version, nil, &block)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Validates configuration data against the schema.
|
139
|
+
#
|
140
|
+
# @param data [Hash] The configuration data to validate
|
141
|
+
# @param current_version [String] The current schema version
|
142
|
+
# @raise [ValidationError] If validation fails
|
143
|
+
def validate(data, current_version = version)
|
144
|
+
validate_required_fields(data, current_version)
|
145
|
+
validate_deprecated_fields(data, current_version)
|
146
|
+
validate_types(data)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Migrates configuration data from one version to another.
|
150
|
+
#
|
151
|
+
# @param data [Hash] The configuration data to migrate
|
152
|
+
# @param from_version [String] The source version
|
153
|
+
# @param to_version [String] The target version
|
154
|
+
# @return [Hash] The migrated configuration data
|
155
|
+
def migrate(data, from_version, to_version)
|
156
|
+
key = migration_key(from_version, to_version)
|
157
|
+
migration = migrations[key]
|
158
|
+
raise MigrationError, "No migration path from #{from_version} to #{to_version}" unless migration
|
159
|
+
|
160
|
+
data = data.dup
|
161
|
+
migration.call(data)
|
162
|
+
data
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def migration_key(from_version, to_version)
|
168
|
+
"#{from_version}-#{to_version}"
|
169
|
+
end
|
170
|
+
|
171
|
+
def validate_required_fields(data, current_version)
|
172
|
+
required_fields.each do |name, field|
|
173
|
+
next if field[:since] > current_version
|
174
|
+
next if data.key?(name)
|
175
|
+
|
176
|
+
raise ValidationError, "Missing required field: #{name}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def validate_deprecated_fields(data, current_version)
|
181
|
+
deprecated_fields.each do |name, field|
|
182
|
+
next unless data.key?(name)
|
183
|
+
next unless field[:since] <= current_version
|
184
|
+
|
185
|
+
if field[:remove_in] <= current_version
|
186
|
+
raise ValidationError, "Field #{name} was removed in version #{field[:remove_in]}"
|
187
|
+
end
|
188
|
+
|
189
|
+
warn "Field #{name} is deprecated since version #{field[:since]} and will be removed in #{field[:remove_in]}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def validate_types(data)
|
194
|
+
all_fields = required_fields.merge(optional_fields)
|
195
|
+
data.each do |name, value|
|
196
|
+
field = all_fields[name]
|
197
|
+
next unless field
|
198
|
+
|
199
|
+
type = field[:type]
|
200
|
+
next if TypeChecker.call(value, type: type)
|
201
|
+
|
202
|
+
raise ValidationError, "Invalid type for field #{name}: expected #{type}, got #{value.class}"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Error raised when schema validation fails.
|
208
|
+
class ValidationError < StandardError; end
|
209
|
+
|
210
|
+
# Error raised when schema migration fails.
|
211
|
+
class MigrationError < StandardError; end
|
212
|
+
end
|
213
|
+
end
|