env_validator 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a90ea9c5fad700f6349c118e12389d662a4d13d4a582b4198aeb3744e872a1ba
4
+ data.tar.gz: f4a043838d0b748377c18b8b6844d1c8a5f70b1d3b2820ae9e8caac12127e0d7
5
+ SHA512:
6
+ metadata.gz: c8900c21a1d6f1cc4ec19c7283e3f9cc5084e195ab7f949d80d44230c8af248f702d1d63ab45e1315f0cb9c89383b445dc9fbe7c0de3036ae2cfb14d8894dac6
7
+ data.tar.gz: 808c9b4b117eb23afaa110623d86c499e855e0ddefe1ed0b33199e539d9a222e8345e6dc6eadae21aa342d9358974ef4be65924cbdb160461d94b8a890ae905a
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-09-04
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Alex Lopez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # EnvValidator
2
+
3
+ A lightweight Ruby gem that validates required environment variables at application boot time, providing type checking, format validation, and helpful error messages to prevent production disasters caused by missing or invalid configuration.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ ```bash
10
+ bundle add env_validator
11
+ ```
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ ```bash
16
+ gem install env_validator
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```ruby
22
+ # config/initializers/env_validator.rb (Rails)
23
+ # or at top of main application file
24
+
25
+ EnvValidator.configure do
26
+ # Required variables
27
+ required :DATABASE_URL, type: :url
28
+ required :SECRET_KEY_BASE, type: :string, min_length: 32
29
+ required :RAILS_ENV, type: :string, in: %w[development test production]
30
+
31
+ # Optional with defaults
32
+ optional :PORT, type: :integer, default: 3000
33
+ optional :LOG_LEVEL, type: :string, default: 'info', in: %w[debug info warn error]
34
+
35
+ # Format validation
36
+ required :STRIPE_API_KEY, format: /^sk_[a-zA-Z0-9]+$/
37
+ end
38
+
39
+ # Validate (call this after configuration)
40
+ EnvValidator.validate!
41
+ ```
42
+
43
+ ## Features
44
+
45
+ - ✅ **Required variable validation** - Ensure critical env vars exist
46
+ - ✅ **Type checking** - String, Integer, Boolean, URL, Email, JSON, Base64, File/Dir paths
47
+ - ✅ **Custom error messages** - Clear indication of what's missing/wrong
48
+ - ✅ **Simple DSL** - Easy configuration syntax
49
+ - ✅ **Boot-time validation** - Fail early before app starts
50
+ - ✅ **Optional variables with defaults** - Handle optional configuration
51
+ - ✅ **Format validation with regex** - API keys, tokens, custom formats
52
+ - ✅ **Constraints** - Length limits, numeric ranges, inclusion lists
53
+ - ✅ **Custom validators** - User-defined validation logic
54
+
55
+ ## Built-in Types
56
+
57
+ ```ruby
58
+ type: :string # Any string
59
+ type: :integer # Numeric string that converts to integer
60
+ type: :float # Numeric string that converts to float
61
+ type: :boolean # 'true', 'false', '1', '0', 'yes', 'no', 'on', 'off'
62
+ type: :url # Valid HTTP/HTTPS URL format
63
+ type: :email # Valid email format
64
+ type: :json # Valid JSON string
65
+ type: :base64 # Valid Base64 string
66
+ type: :file_path # Existing file path
67
+ type: :dir_path # Existing directory path
68
+ ```
69
+
70
+ ## Usage Examples
71
+
72
+ ### Basic Validation
73
+ ```ruby
74
+ ENV['API_KEY'] = 'sk_test_123'
75
+ ENV['DEBUG'] = 'true'
76
+
77
+ EnvValidator.configure do
78
+ required :API_KEY, type: :string
79
+ required :DEBUG, type: :boolean
80
+ end
81
+
82
+ EnvValidator.validate! # Passes
83
+ ```
84
+
85
+ ### Constraints and Defaults
86
+ ```ruby
87
+ EnvValidator.configure do
88
+ required :SECRET, type: :string, min_length: 32
89
+ optional :PORT, type: :integer, default: 3000, min: 1000, max: 9999
90
+ required :ENV, type: :string, in: %w[development test production]
91
+ end
92
+ ```
93
+
94
+ ### Custom Validation
95
+ ```ruby
96
+ EnvValidator.configure do
97
+ required :CUSTOM_VALUE, type: :string do |value|
98
+ value.start_with?('custom_') && value.length > 10
99
+ end
100
+ end
101
+ ```
102
+
103
+ ## Development
104
+
105
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
106
+
107
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
108
+
109
+ ## Contributing
110
+
111
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/env_validator.
112
+
113
+ ## License
114
+
115
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example demonstrating basic EnvValidator usage
5
+ require_relative "../lib/env_validator"
6
+
7
+ # Set up some test environment variables
8
+ ENV["DATABASE_URL"] = "https://db.example.com/mydb"
9
+ ENV["SECRET_KEY"] = "super_secret_key_with_32_chars!!"
10
+ ENV["DEBUG"] = "true"
11
+ ENV["PORT"] = "3000"
12
+
13
+ puts "=== EnvValidator Basic Usage Example ==="
14
+
15
+ begin
16
+ EnvValidator.configure do
17
+ # Required variables
18
+ required :DATABASE_URL, type: :url, description: "Primary database connection URL"
19
+ required :SECRET_KEY, type: :string, min_length: 32, description: "Application secret key"
20
+
21
+ # Optional variables with defaults
22
+ optional :PORT, type: :integer, default: 5000, min: 1000, max: 9999
23
+ optional :DEBUG, type: :boolean, default: false
24
+ optional :LOG_LEVEL, type: :string, default: "info", in: %w[debug info warn error]
25
+
26
+ # Format validation
27
+ required :API_KEY, type: :string, format: /^ak_[a-zA-Z0-9]{16}$/, description: "API key with specific format"
28
+ end
29
+
30
+ puts "\nConfiguration created successfully!"
31
+ puts "Now validating environment variables..."
32
+
33
+ EnvValidator.validate!
34
+ puts "✅ All environment variables are valid!"
35
+ rescue EnvValidator::ValidationErrors => e
36
+ puts "❌ Validation failed with the following errors:"
37
+ puts e.message
38
+ rescue EnvValidator::Error => e
39
+ puts "❌ Configuration error: #{e.message}"
40
+ end
41
+
42
+ puts "\n=== Demonstrating Error Messages ==="
43
+
44
+ # Clear API_KEY to show error message
45
+ ENV.delete("API_KEY")
46
+
47
+ begin
48
+ EnvValidator.validate!
49
+ rescue EnvValidator::ValidationErrors => e
50
+ puts "Expected error for missing API_KEY:"
51
+ puts e.message
52
+ end
53
+
54
+ puts "\n=== Demonstrating Type Validation Error ==="
55
+
56
+ # Set invalid port to show type error
57
+ ENV["PORT"] = "not_a_number"
58
+ EnvValidator.reset!
59
+
60
+ begin
61
+ EnvValidator.configure do
62
+ required :PORT, type: :integer
63
+ end
64
+
65
+ EnvValidator.validate!
66
+ rescue EnvValidator::ValidationErrors => e
67
+ puts "Expected error for invalid port type:"
68
+ puts e.message
69
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnvValidator
4
+ class Rule
5
+ attr_reader :name, :type, :required, :default, :format, :description, :constraints, :custom_validator
6
+
7
+ def initialize(name, required: true, type: :string, default: nil, format: nil,
8
+ description: nil, custom_validator: nil, **constraints)
9
+ @name = name.to_sym
10
+ @required = required
11
+ @type = type
12
+ @default = default
13
+ @format = format
14
+ @description = description
15
+ @constraints = constraints
16
+ @custom_validator = custom_validator
17
+ end
18
+
19
+ def required?
20
+ @required
21
+ end
22
+
23
+ def optional?
24
+ !@required
25
+ end
26
+
27
+ def has_default?
28
+ !@default.nil?
29
+ end
30
+
31
+ def has_format?
32
+ !@format.nil?
33
+ end
34
+
35
+ def has_constraints?
36
+ !@constraints.empty?
37
+ end
38
+
39
+ def has_custom_validator?
40
+ !@custom_validator.nil?
41
+ end
42
+ end
43
+
44
+ class Configuration
45
+ attr_reader :rules, :environments, :groups, :current_env
46
+
47
+ def initialize
48
+ @rules = {}
49
+ @environments = {}
50
+ @groups = {}
51
+ @current_env = detect_environment
52
+ end
53
+
54
+ def required(name, **options, &block)
55
+ custom_validator = block_given? ? block : nil
56
+ @rules[name.to_sym] = Rule.new(name, required: true, custom_validator: custom_validator, **options)
57
+ end
58
+
59
+ def optional(name, **options, &block)
60
+ custom_validator = block_given? ? block : nil
61
+ @rules[name.to_sym] = Rule.new(name, required: false, custom_validator: custom_validator, **options)
62
+ end
63
+
64
+ def environment(env_name, &block)
65
+ @environments[env_name.to_sym] = block
66
+ end
67
+
68
+ def group(group_name, &block)
69
+ @groups[group_name.to_sym] = block
70
+ end
71
+
72
+ def applicable_rules
73
+ rules = @rules.values.dup
74
+
75
+ # Apply environment-specific rules if they exist
76
+ if @environments[@current_env]
77
+ env_config = Configuration.new
78
+ env_config.instance_eval(&@environments[@current_env])
79
+ rules.concat(env_config.rules.values)
80
+ end
81
+
82
+ rules
83
+ end
84
+
85
+ def clear!
86
+ @rules.clear
87
+ @environments.clear
88
+ @groups.clear
89
+ end
90
+
91
+ private
92
+
93
+ def detect_environment
94
+ # Check for Rails environment first
95
+ return ENV["RAILS_ENV"].to_sym if ENV["RAILS_ENV"]
96
+
97
+ # Check for Rack environment
98
+ return ENV["RACK_ENV"].to_sym if ENV["RACK_ENV"]
99
+
100
+ # Check for general environment
101
+ return ENV["ENV"].to_sym if ENV["ENV"]
102
+
103
+ # Default to development
104
+ :development
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnvValidator
4
+ class Error < StandardError; end
5
+
6
+ class ValidationError < Error
7
+ attr_reader :variable_name, :rule
8
+
9
+ def initialize(variable_name, rule, message)
10
+ @variable_name = variable_name
11
+ @rule = rule
12
+ super(message)
13
+ end
14
+ end
15
+
16
+ class MissingVariableError < ValidationError
17
+ def initialize(variable_name, rule)
18
+ message = build_missing_message(variable_name, rule)
19
+ super(variable_name, rule, message)
20
+ end
21
+
22
+ private
23
+
24
+ def build_missing_message(variable_name, rule)
25
+ message = "#{variable_name} is required but not set."
26
+
27
+ message += " #{rule.description}" if rule.description
28
+
29
+ if (similar = find_similar_env_vars(variable_name))
30
+ message += " Did you mean #{similar}?"
31
+ end
32
+
33
+ message
34
+ end
35
+
36
+ def find_similar_env_vars(variable_name)
37
+ env_vars = ENV.keys
38
+ name_str = variable_name.to_s
39
+
40
+ env_vars.find do |var|
41
+ # Simple similarity check - same length and mostly same characters
42
+ var.length == name_str.length &&
43
+ (0...var.length).count { |i| var[i] == name_str[i] } >= (name_str.length * 0.7).to_i
44
+ end
45
+ end
46
+ end
47
+
48
+ class TypeError < ValidationError
49
+ def initialize(variable_name, rule, expected_type, actual_value)
50
+ message = "#{variable_name} must be #{expected_type_description(expected_type)}, got: #{actual_value.inspect}"
51
+ super(variable_name, rule, message)
52
+ end
53
+
54
+ private
55
+
56
+ def expected_type_description(type)
57
+ case type
58
+ when :string then "a string"
59
+ when :integer then "an integer"
60
+ when :boolean then "a boolean (true/false)"
61
+ when :url then "a valid URL"
62
+ when :email then "a valid email address"
63
+ when :json then "valid JSON"
64
+ else type.to_s
65
+ end
66
+ end
67
+ end
68
+
69
+ class FormatError < ValidationError
70
+ def initialize(variable_name, rule, pattern)
71
+ message = "#{variable_name} format is invalid. Expected format: #{pattern}"
72
+ super(variable_name, rule, message)
73
+ end
74
+ end
75
+
76
+ class ConstraintError < ValidationError
77
+ def initialize(variable_name, rule, constraint_type, constraint_value, actual_value)
78
+ message = build_constraint_message(variable_name, constraint_type, constraint_value, actual_value)
79
+ super(variable_name, rule, message)
80
+ end
81
+
82
+ private
83
+
84
+ def build_constraint_message(variable_name, constraint_type, constraint_value, actual_value)
85
+ case constraint_type
86
+ when :min_length
87
+ "#{variable_name} must be at least #{constraint_value} characters long, got #{actual_value.length}"
88
+ when :max_length
89
+ "#{variable_name} must be at most #{constraint_value} characters long, got #{actual_value.length}"
90
+ when :min
91
+ "#{variable_name} must be at least #{constraint_value}, got #{actual_value}"
92
+ when :max
93
+ "#{variable_name} must be at most #{constraint_value}, got #{actual_value}"
94
+ when :in
95
+ "#{variable_name} must be one of #{constraint_value.inspect}, got #{actual_value.inspect}"
96
+ else
97
+ "#{variable_name} failed constraint #{constraint_type}: #{constraint_value}"
98
+ end
99
+ end
100
+ end
101
+
102
+ class ValidationErrors < Error
103
+ attr_reader :errors
104
+
105
+ def initialize(errors)
106
+ @errors = errors
107
+ super(build_message)
108
+ end
109
+
110
+ private
111
+
112
+ def build_message
113
+ "Environment validation failed:\n" +
114
+ errors.map { |e| " - #{e.message}" }.join("\n")
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "json"
5
+ require "base64"
6
+
7
+ module EnvValidator
8
+ module Types
9
+ class Base
10
+ def validate(value)
11
+ raise NotImplementedError, "Subclasses must implement #validate"
12
+ end
13
+
14
+ def coerce(value)
15
+ value # Default: no coercion
16
+ end
17
+ end
18
+
19
+ class String < Base
20
+ def validate(value)
21
+ return true if value.is_a?(::String)
22
+
23
+ raise TypeError, "Expected string, got #{value.class}"
24
+ end
25
+ end
26
+
27
+ class Integer < Base
28
+ def validate(value)
29
+ # Try to convert to integer
30
+ Integer(value)
31
+ true
32
+ rescue ArgumentError
33
+ raise TypeError, "Expected integer, got #{value.inspect}"
34
+ end
35
+
36
+ def coerce(value)
37
+ Integer(value)
38
+ end
39
+ end
40
+
41
+ class Float < Base
42
+ def validate(value)
43
+ # Try to convert to float
44
+ Float(value)
45
+ true
46
+ rescue ArgumentError
47
+ raise TypeError, "Expected float, got #{value.inspect}"
48
+ end
49
+
50
+ def coerce(value)
51
+ Float(value)
52
+ end
53
+ end
54
+
55
+ class Boolean < Base
56
+ TRUTHY_VALUES = %w[true 1 yes y on].freeze
57
+ FALSY_VALUES = %w[false 0 no n off].freeze
58
+ VALID_VALUES = (TRUTHY_VALUES + FALSY_VALUES).freeze
59
+
60
+ def validate(value)
61
+ normalized = value.to_s.downcase.strip
62
+ return true if VALID_VALUES.include?(normalized)
63
+
64
+ raise TypeError, "Expected boolean (#{VALID_VALUES.join(", ")}), got #{value.inspect}"
65
+ end
66
+
67
+ def coerce(value)
68
+ normalized = value.to_s.downcase.strip
69
+ TRUTHY_VALUES.include?(normalized)
70
+ end
71
+ end
72
+
73
+ class URL < Base
74
+ def validate(value)
75
+ uri = URI.parse(value.to_s)
76
+
77
+ # Must have a scheme and host for a valid URL
78
+ raise TypeError, "Expected valid URL with scheme and host, got #{value.inspect}" unless uri.scheme && uri.host
79
+
80
+ # Must be HTTP or HTTPS
81
+ unless %w[http https].include?(uri.scheme.downcase)
82
+ raise TypeError, "Expected HTTP or HTTPS URL, got #{value.inspect}"
83
+ end
84
+
85
+ true
86
+ rescue URI::InvalidURIError
87
+ raise TypeError, "Expected valid URL, got #{value.inspect}"
88
+ end
89
+
90
+ def coerce(value)
91
+ URI.parse(value.to_s)
92
+ end
93
+ end
94
+
95
+ class Email < Base
96
+ # Basic email regex - not RFC compliant but good enough for most cases
97
+ EMAIL_REGEX = /\A[^@\s]+@[^@\s]+\.[^@\s]+\z/
98
+
99
+ def validate(value)
100
+ raise TypeError, "Expected valid email address, got #{value.inspect}" unless value.to_s.match?(EMAIL_REGEX)
101
+
102
+ true
103
+ end
104
+ end
105
+
106
+ class Json < Base
107
+ def validate(value)
108
+ JSON.parse(value.to_s)
109
+ true
110
+ rescue JSON::ParserError
111
+ raise TypeError, "Expected valid JSON, got #{value.inspect}"
112
+ end
113
+
114
+ def coerce(value)
115
+ JSON.parse(value.to_s)
116
+ end
117
+ end
118
+
119
+ class Base64 < Base
120
+ def validate(value)
121
+ # Check if it's valid base64
122
+ ::Base64.strict_decode64(value.to_s)
123
+ true
124
+ rescue ArgumentError
125
+ raise TypeError, "Expected valid Base64 string, got #{value.inspect}"
126
+ end
127
+
128
+ def coerce(value)
129
+ ::Base64.strict_decode64(value.to_s)
130
+ end
131
+ end
132
+
133
+ class FilePath < Base
134
+ def validate(value)
135
+ path = value.to_s
136
+ unless File.exist?(path) && File.file?(path)
137
+ raise TypeError, "Expected existing file path, got #{value.inspect}"
138
+ end
139
+
140
+ true
141
+ end
142
+ end
143
+
144
+ class DirPath < Base
145
+ def validate(value)
146
+ path = value.to_s
147
+ unless File.exist?(path) && File.directory?(path)
148
+ raise TypeError, "Expected existing directory path, got #{value.inspect}"
149
+ end
150
+
151
+ true
152
+ end
153
+ end
154
+
155
+ # Registry for type lookup
156
+ TYPE_REGISTRY = {
157
+ string: String,
158
+ integer: Integer,
159
+ float: Float,
160
+ boolean: Boolean,
161
+ url: URL,
162
+ email: Email,
163
+ json: Json,
164
+ base64: Base64,
165
+ file_path: FilePath,
166
+ dir_path: DirPath
167
+ }.freeze
168
+
169
+ def self.get_validator(type)
170
+ validator_class = TYPE_REGISTRY[type.to_sym]
171
+ raise ArgumentError, "Unknown type: #{type}" unless validator_class
172
+
173
+ validator_class.new
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "types"
4
+ require_relative "error"
5
+
6
+ module EnvValidator
7
+ class Validator
8
+ def initialize(configuration)
9
+ @config = configuration
10
+ end
11
+
12
+ def validate!
13
+ errors = []
14
+
15
+ @config.applicable_rules.each do |rule|
16
+ validate_rule(rule)
17
+ rescue ValidationError => e
18
+ errors << e
19
+ end
20
+
21
+ raise ValidationErrors, errors unless errors.empty?
22
+
23
+ true
24
+ end
25
+
26
+ private
27
+
28
+ def validate_rule(rule)
29
+ value = ENV[rule.name.to_s]
30
+
31
+ # Handle missing required variables
32
+ raise MissingVariableError.new(rule.name, rule) if rule.required? && value.nil?
33
+
34
+ # If optional and missing, use default if available
35
+ if value.nil? && rule.optional?
36
+ return unless rule.has_default?
37
+
38
+ ENV[rule.name.to_s] = rule.default.to_s
39
+ value = rule.default.to_s
40
+
41
+ # Optional and no default, skip validation
42
+
43
+ end
44
+
45
+ return if value.nil? # Should not happen but safety check
46
+
47
+ # Validate type
48
+ validate_type(rule, value)
49
+
50
+ # Validate format if specified
51
+ validate_format(rule, value) if rule.has_format?
52
+
53
+ # Validate constraints
54
+ validate_constraints(rule, value) if rule.has_constraints?
55
+
56
+ # Run custom validator if specified
57
+ validate_custom(rule, value) if rule.has_custom_validator?
58
+ end
59
+
60
+ def validate_type(rule, value)
61
+ type_validator = Types.get_validator(rule.type)
62
+ type_validator.validate(value)
63
+ rescue ::TypeError
64
+ raise EnvValidator::TypeError.new(rule.name, rule, rule.type, value)
65
+ rescue ArgumentError
66
+ raise EnvValidator::TypeError.new(rule.name, rule, rule.type, value)
67
+ end
68
+
69
+ def validate_format(rule, value)
70
+ pattern = rule.format
71
+ return if value.to_s.match?(pattern)
72
+
73
+ raise FormatError.new(rule.name, rule, pattern)
74
+ end
75
+
76
+ def validate_constraints(rule, value)
77
+ rule.constraints.each do |constraint_type, constraint_value|
78
+ validate_constraint(rule, value, constraint_type, constraint_value)
79
+ end
80
+ end
81
+
82
+ def validate_constraint(rule, value, constraint_type, constraint_value)
83
+ case constraint_type
84
+ when :min_length
85
+ if value.to_s.length < constraint_value
86
+ raise ConstraintError.new(rule.name, rule, constraint_type, constraint_value, value)
87
+ end
88
+ when :max_length
89
+ if value.to_s.length > constraint_value
90
+ raise ConstraintError.new(rule.name, rule, constraint_type, constraint_value, value)
91
+ end
92
+ when :min
93
+ # Coerce value to numeric for comparison
94
+ numeric_value = coerce_to_numeric(rule, value)
95
+ if numeric_value < constraint_value
96
+ raise ConstraintError.new(rule.name, rule, constraint_type, constraint_value, value)
97
+ end
98
+ when :max
99
+ # Coerce value to numeric for comparison
100
+ numeric_value = coerce_to_numeric(rule, value)
101
+ if numeric_value > constraint_value
102
+ raise ConstraintError.new(rule.name, rule, constraint_type, constraint_value, value)
103
+ end
104
+ when :in
105
+ unless constraint_value.include?(value.to_s)
106
+ raise ConstraintError.new(rule.name, rule, constraint_type, constraint_value, value)
107
+ end
108
+ end
109
+ end
110
+
111
+ def validate_custom(rule, value)
112
+ result = rule.custom_validator.call(value)
113
+ raise ValidationError.new(rule.name, rule, "Custom validation failed for #{rule.name}") unless result
114
+ rescue StandardError => e
115
+ raise e if e.is_a?(ValidationError)
116
+
117
+ raise ValidationError.new(rule.name, rule, "Custom validation error: #{e.message}")
118
+ end
119
+
120
+ def coerce_to_numeric(rule, value)
121
+ case rule.type
122
+ when :integer
123
+ Integer(value)
124
+ when :float
125
+ Float(value)
126
+ else
127
+ # For non-numeric types, try to convert anyway
128
+ begin
129
+ Float(value)
130
+ rescue ArgumentError
131
+ raise ConstraintError.new(rule.name, rule, :type, "numeric", value)
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnvValidator
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "env_validator/version"
4
+ require_relative "env_validator/error"
5
+ require_relative "env_validator/configuration"
6
+ require_relative "env_validator/validator"
7
+ require_relative "env_validator/types"
8
+
9
+ module EnvValidator
10
+ class << self
11
+ def configure(&block)
12
+ @configuration = Configuration.new
13
+ @configuration.instance_eval(&block)
14
+ @configuration
15
+ end
16
+
17
+ def validate!
18
+ raise Error, "No configuration defined. Call EnvValidator.configure first." unless @configuration
19
+
20
+ validator = Validator.new(@configuration)
21
+ validator.validate!
22
+ end
23
+
24
+ attr_reader :configuration
25
+
26
+ def reset!
27
+ @configuration = nil
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ module EnvValidator
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: env_validator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Lopez
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A lightweight gem that validates required environment variables at application
13
+ boot time, providing type checking, format validation, and helpful error messages
14
+ to prevent production disasters caused by missing or invalid configuration.
15
+ email:
16
+ - webodessa@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".rspec"
22
+ - ".rubocop.yml"
23
+ - CHANGELOG.md
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - examples/basic_usage.rb
28
+ - lib/env_validator.rb
29
+ - lib/env_validator/configuration.rb
30
+ - lib/env_validator/error.rb
31
+ - lib/env_validator/types.rb
32
+ - lib/env_validator/validator.rb
33
+ - lib/env_validator/version.rb
34
+ - sig/env_validator.rbs
35
+ homepage: https://github.com/logicalgroove/env_validator
36
+ licenses:
37
+ - MIT
38
+ metadata:
39
+ homepage_uri: https://github.com/logicalgroove/env_validator
40
+ source_code_uri: https://github.com/logicalgroove/env_validator
41
+ changelog_uri: https://github.com/logicalgroove/env_validator/blob/master/CHANGELOG.md
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 3.1.0
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.6.9
57
+ specification_version: 4
58
+ summary: Environment variable validation for Ruby applications
59
+ test_files: []