env_check 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.
data/bin/console ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "env_check"
6
+
7
+ # Load development environment if available
8
+ if File.exist?(".env")
9
+ require "dotenv"
10
+ Dotenv.load
11
+ end
12
+
13
+ # You can add fixtures and/or initialization code here to make experimenting
14
+ # with your gem easier. You can also use a different console, if you like.
15
+
16
+ puts "🔍 EnvCheck Console v#{EnvCheck::VERSION}"
17
+ puts "Available methods:"
18
+ puts " EnvCheck.verify # Verify default config"
19
+ puts " EnvCheck.verify! # Verify with exception on failure"
20
+ puts " EnvCheck.verify(file) # Verify custom config file"
21
+ puts ""
22
+
23
+ # Set up some useful variables for console testing
24
+ config_path = EnvCheck::Config.discover_config_path
25
+ if File.exist?(config_path)
26
+ puts "📁 Found #{config_path}"
27
+ config_result = begin
28
+ EnvCheck.verify
29
+ rescue StandardError
30
+ nil
31
+ end
32
+ if config_result
33
+ puts "✅ Config loaded successfully"
34
+ else
35
+ puts "⚠️ Config has issues"
36
+ end
37
+ else
38
+ puts "📝 No config file found. Run EnvCheck init first."
39
+ end
40
+
41
+ puts ""
42
+
43
+ require "irb"
44
+ IRB.start(__FILE__)
data/bin/env_check ADDED
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # bin/env_check - CLI for EnvCheck gem
5
+
6
+ require "optparse"
7
+ require "fileutils"
8
+ require_relative "../lib/env_check"
9
+
10
+ # Module for CLI output helpers
11
+ module CLIHelpers
12
+ def success(message)
13
+ puts message unless @options[:quiet]
14
+ end
15
+
16
+ def info(message)
17
+ puts message unless @options[:quiet]
18
+ end
19
+
20
+ def warning(message)
21
+ puts "⚠️ #{message}" unless @options[:quiet]
22
+ end
23
+
24
+ def error(message)
25
+ warn message
26
+ end
27
+
28
+ def error_exit(message, code = 1)
29
+ warn "Error: #{message}"
30
+ exit code
31
+ end
32
+
33
+ def default_config_path
34
+ Dir.exist?("config") ? "config/env_check.yml" : ".env_check.yml"
35
+ end
36
+ end
37
+
38
+ # CLI class for better organization
39
+ class EnvCheckCLI
40
+ include CLIHelpers
41
+ def initialize(args = ARGV)
42
+ @args = args
43
+ @options = {
44
+ config: nil, # Will be auto-discovered if not specified
45
+ quiet: false,
46
+ verbose: false
47
+ }
48
+ @command = nil
49
+ end
50
+
51
+ def run
52
+ parse_arguments
53
+ execute_command
54
+ rescue StandardError => e
55
+ error_exit(e.message.to_s)
56
+ end
57
+
58
+ private
59
+
60
+ def parse_arguments
61
+ parser = create_option_parser
62
+ parser.parse!(@args)
63
+ @command = @args.shift
64
+ end
65
+
66
+ def create_option_parser
67
+ OptionParser.new do |opts|
68
+ configure_banner_and_commands(opts)
69
+ configure_options(opts)
70
+ end
71
+ end
72
+
73
+ def configure_banner_and_commands(opts)
74
+ opts.banner = "Usage: env_check [options] <command>"
75
+ opts.separator ""
76
+ opts.separator "Commands:"
77
+ opts.separator " init Create a new env_check.yml configuration file"
78
+ opts.separator " check Validate environment variables against configuration"
79
+ opts.separator " version Show version number"
80
+ opts.separator ""
81
+ opts.separator "Options:"
82
+ end
83
+
84
+ def configure_options(opts)
85
+ opts.on("-c", "--config PATH",
86
+ "Configuration file path (auto-discovered: .env_check.yml or config/env_check.yml)") do |path|
87
+ @options[:config] = path
88
+ end
89
+
90
+ opts.on("-q", "--quiet", "Suppress output (only show errors)") do
91
+ @options[:quiet] = true
92
+ end
93
+
94
+ opts.on("-v", "--verbose", "Show detailed output") do
95
+ @options[:verbose] = true
96
+ end
97
+
98
+ opts.on("-h", "--help", "Show this help message") do
99
+ puts opts
100
+ exit 0
101
+ end
102
+ end
103
+
104
+ def execute_command
105
+ case @command
106
+ when "init"
107
+ init_command
108
+ when "check"
109
+ check_command
110
+ when "version"
111
+ version_command
112
+ when nil
113
+ show_help_and_exit
114
+ else
115
+ error_exit("Unknown command: #{@command}")
116
+ end
117
+ end
118
+
119
+ def init_command
120
+ path = @options[:config] || determine_init_path
121
+
122
+ begin
123
+ # Create directory if it doesn't exist
124
+ dir = File.dirname(path)
125
+ FileUtils.mkdir_p(dir) unless dir == "."
126
+
127
+ if File.exist?(path)
128
+ warning("Configuration file already exists: #{path}")
129
+ return
130
+ end
131
+
132
+ create_config_file(path)
133
+ success("Created configuration file: #{path}")
134
+
135
+ unless @options[:quiet]
136
+ puts "\nNext steps:"
137
+ puts "1. Edit #{path} to define your required environment variables"
138
+ puts "2. Run 'env_check check' to validate your environment"
139
+ end
140
+ rescue StandardError => e
141
+ error_exit("Failed to create configuration file: #{e.message}")
142
+ end
143
+ end
144
+
145
+ def check_command
146
+ path = @options[:config] || EnvCheck::Config.discover_config_path
147
+ validate_config_exists(path)
148
+
149
+ info("Checking environment variables using: #{path}") if @options[:verbose]
150
+
151
+ result = perform_validation(path)
152
+ handle_validation_result(result)
153
+ end
154
+
155
+ def validate_config_exists(path)
156
+ return if File.exist?(path)
157
+
158
+ error_exit("Configuration file not found: #{path}. Run 'env_check init' first.")
159
+ end
160
+
161
+ def perform_validation(path)
162
+ EnvCheck.verify(path)
163
+ rescue EnvCheck::Error => e
164
+ error_exit("Validation failed: #{e.message}")
165
+ rescue StandardError => e
166
+ error_exit("Unexpected error: #{e.message}")
167
+ end
168
+
169
+ def handle_validation_result(result)
170
+ if result.success?
171
+ handle_success_result(result)
172
+ else
173
+ handle_failure_result(result)
174
+ end
175
+ end
176
+
177
+ def handle_success_result(result)
178
+ success("✅ All environment variables are valid!")
179
+
180
+ if @options[:verbose] && !result.valid_vars.empty?
181
+ puts "\nValid variables:"
182
+ result.valid_vars.each { |var| puts " ✅ #{var}" }
183
+ end
184
+
185
+ display_warnings(result.warnings) unless result.warnings.empty?
186
+ end
187
+
188
+ def handle_failure_result(result)
189
+ error("❌ Environment validation failed!")
190
+
191
+ display_errors(result.errors) unless result.errors.empty?
192
+ display_warnings(result.warnings) unless result.warnings.empty?
193
+
194
+ exit 1
195
+ end
196
+
197
+ def display_errors(errors)
198
+ puts "\nErrors:"
199
+ errors.each { |error| puts " ❌ #{error}" }
200
+ end
201
+
202
+ def display_warnings(warnings)
203
+ puts "\nWarnings:"
204
+ warnings.each { |warning| puts " ⚠️ #{warning}" }
205
+ end
206
+
207
+ def version_command
208
+ puts EnvCheck::VERSION
209
+ end
210
+
211
+ def create_config_file(path)
212
+ File.write(path, config_template)
213
+ end
214
+
215
+ def config_template
216
+ <<~YAML
217
+ # EnvCheck Configuration
218
+ # Configure required and optional environment variables for your application
219
+
220
+ # Required environment variables (must be present and non-empty)
221
+ required:
222
+ - DATABASE_URL
223
+ - SECRET_KEY_BASE
224
+
225
+ # Optional environment variables with type validation
226
+ optional:
227
+ DEBUG: boolean # true, false, 1, 0, yes, no, on, off (case-insensitive)
228
+ PORT: port # valid port number (1-65535)
229
+ API_URL: url # must start with http:// or https://
230
+ ADMIN_EMAIL: email # valid email format
231
+ LOG_LEVEL: string # any string value
232
+ RATE_LIMIT: float # floating point number
233
+ CONFIG_PATH: path # file or directory path
234
+ SETTINGS: json # valid JSON string
235
+ #{" "}
236
+ # You can also configure per-environment settings:
237
+ # development:
238
+ # required:
239
+ # - DATABASE_URL
240
+ # optional:
241
+ # DEBUG: boolean
242
+ ##{" "}
243
+ # production:
244
+ # required:
245
+ # - DATABASE_URL
246
+ # - SECRET_KEY_BASE
247
+ # - RAILS_MASTER_KEY
248
+ # optional:
249
+ # REDIS_URL: url
250
+ YAML
251
+ end
252
+
253
+ def show_help_and_exit
254
+ puts create_option_parser.help
255
+ exit 1
256
+ end
257
+
258
+ # Determine the best path for init command
259
+ def determine_init_path
260
+ default_config_path
261
+ end
262
+ end
263
+
264
+ # Run the CLI
265
+ EnvCheckCLI.new.run if __FILE__ == $PROGRAM_NAME
data/bin/setup ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ echo "🔧 Setting up env_check development environment..."
7
+
8
+ # Install dependencies
9
+ bundle install
10
+
11
+ # Run any other automated setup that you need to do here
12
+ echo "✅ Running initial tests to verify setup..."
13
+ bundle exec rspec --format documentation
14
+
15
+ echo "🎯 Running RuboCop to check code style..."
16
+ bundle exec rubocop
17
+
18
+ echo ""
19
+ echo "🎉 Setup complete!"
20
+ echo ""
21
+ echo "Available commands:"
22
+ echo " ./bin/console # Start interactive console"
23
+ echo " ./bin/env_check # Run CLI tool"
24
+ echo " bundle exec rspec # Run tests"
25
+ echo " bundle exec rubocop # Check code style"
26
+ echo ""
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnvCheck
4
+ # Configuration management for EnvCheck
5
+ class Config
6
+ # Priority order for auto-discovery
7
+ DEFAULT_PATHS = [
8
+ ".env_check.yml", # Root level (simple/tool-focused projects)
9
+ "config/env_check.yml" # Rails convention (if config/ exists)
10
+ ].freeze
11
+
12
+ attr_reader :required_vars, :optional_vars, :config_path
13
+
14
+ def initialize(config_path = nil, environment = nil)
15
+ @config_path = config_path || ENV["ENV_CHECK_CONFIG"] || discover_config_path
16
+ @environment = environment || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
17
+ load_config
18
+ end
19
+
20
+ def self.from_hash(config_hash)
21
+ instance = allocate
22
+ instance.instance_variable_set(:@required_vars, config_hash["required"] || [])
23
+ instance.instance_variable_set(:@optional_vars,
24
+ instance.send(:normalize_optional_vars, config_hash["optional"] || {}))
25
+ instance.instance_variable_set(:@config_path, "inline")
26
+ instance
27
+ end
28
+
29
+ def valid?
30
+ File.exist?(@config_path)
31
+ end
32
+
33
+ # Discover config file using priority order
34
+ def self.discover_config_path
35
+ DEFAULT_PATHS.find { |path| File.exist?(path) } || DEFAULT_PATHS.first
36
+ end
37
+
38
+ private
39
+
40
+ def discover_config_path
41
+ self.class.discover_config_path
42
+ end
43
+
44
+ def load_config
45
+ if File.exist?(@config_path)
46
+ config = YAML.load_file(@config_path)
47
+
48
+ # Support environment-specific configurations
49
+ env_config = config[@environment] || config
50
+
51
+ @required_vars = env_config["required"] || []
52
+
53
+ # Handle optional vars - support both hash and array formats
54
+ optional_config = env_config["optional"] || {}
55
+ @optional_vars = normalize_optional_vars(optional_config)
56
+ else
57
+ @required_vars = []
58
+ @optional_vars = {}
59
+ end
60
+ end
61
+
62
+ # Normalize optional vars to support both hash and array formats
63
+ # Hash format: { "VAR" => "type" }
64
+ # Array format: [{ "VAR" => "type" }, "SIMPLE_VAR"]
65
+ def normalize_optional_vars(optional_config)
66
+ case optional_config
67
+ when Hash
68
+ optional_config
69
+ when Array
70
+ result = {}
71
+ optional_config.each do |item|
72
+ case item
73
+ when Hash
74
+ result.merge!(item)
75
+ when String
76
+ result[item] = nil
77
+ end
78
+ end
79
+ result
80
+ else
81
+ {}
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/env_check/rake_task.rb
4
+ require "rake"
5
+ require_relative "../env_check"
6
+
7
+ namespace :env do
8
+ desc "Check environment variable configuration"
9
+ task :check do
10
+ EnvCheck.verify!
11
+ end
12
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnvCheck
4
+ # Custom validator classes for different environment variable types
5
+ module Validators
6
+ class Base
7
+ def self.valid?(value)
8
+ raise NotImplementedError, "Subclasses must implement #valid?"
9
+ end
10
+ end
11
+
12
+ class Boolean < Base
13
+ VALID_VALUES = %w[true false 1 0 yes no on off].freeze
14
+
15
+ def self.valid?(value)
16
+ return false if value.nil? || value.strip.empty?
17
+
18
+ VALID_VALUES.include?(value.strip.downcase)
19
+ end
20
+ end
21
+
22
+ class Integer < Base
23
+ def self.valid?(value)
24
+ return false if value.nil? || value.strip.empty?
25
+
26
+ value.strip.match?(/\A-?\d+\z/)
27
+ end
28
+ end
29
+
30
+ class Float < Base
31
+ def self.valid?(value)
32
+ return false if value.nil? || value.strip.empty?
33
+
34
+ begin
35
+ Float(value.strip)
36
+ true
37
+ rescue ArgumentError
38
+ false
39
+ end
40
+ end
41
+ end
42
+
43
+ class Url < Base
44
+ def self.valid?(value)
45
+ return false if value.nil? || value.strip.empty?
46
+
47
+ value.strip.match?(%r{\Ahttps?://\S+\z})
48
+ end
49
+ end
50
+
51
+ class Email < Base
52
+ def self.valid?(value)
53
+ return false if value.nil? || value.strip.empty?
54
+
55
+ value.strip.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
56
+ end
57
+ end
58
+
59
+ class Path < Base
60
+ def self.valid?(value)
61
+ return false if value.nil? || value.strip.empty?
62
+
63
+ # Basic path validation - should not contain null bytes and be reasonable length
64
+ path = value.strip
65
+ !path.include?("\0") && path.length.positive? && path.length < 1000
66
+ end
67
+ end
68
+
69
+ class Port < Base
70
+ def self.valid?(value)
71
+ return false if value.nil? || value.strip.empty?
72
+
73
+ port = value.strip.to_i
74
+ port.positive? && port <= 65_535
75
+ end
76
+ end
77
+
78
+ class Enum < Base
79
+ def self.valid?(value, allowed_values = [])
80
+ return false if value.nil? || value.strip.empty?
81
+ return false if allowed_values.empty?
82
+
83
+ allowed_values.include?(value.strip)
84
+ end
85
+ end
86
+
87
+ class JsonString < Base
88
+ def self.valid?(value)
89
+ return false if value.nil? || value.strip.empty?
90
+
91
+ begin
92
+ require "json"
93
+ JSON.parse(value.strip)
94
+ true
95
+ rescue JSON::ParserError
96
+ false
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnvCheck
4
+ VERSION = "0.1.0"
5
+ end
data/lib/env_check.rb ADDED
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ # EnvCheck is a Ruby gem to validate presence and format of environment variables
4
+ # before your application boots or runs. Supports YAML configuration and type checking.
5
+ #
6
+ # Features:
7
+ # - Checks for required and optional environment variables
8
+ # - Validates types: boolean, integer, url, string (default)
9
+ # - Supports config via `ENV["ENV_CHECK_CONFIG"]`
10
+ # - Auto-loads `.env` file if present
11
+
12
+ require "yaml"
13
+
14
+ # Load version and rake task
15
+ require_relative "env_check/version"
16
+ require_relative "env_check/config"
17
+ require_relative "env_check/validators"
18
+ require_relative "env_check/rake_task" if defined?(Rake)
19
+
20
+ # Load .env automatically (if present)
21
+ if File.exist?(".env")
22
+ begin
23
+ require "dotenv"
24
+ Dotenv.load
25
+ rescue LoadError
26
+ warn "🔍 dotenv not installed — skipping .env loading"
27
+ end
28
+ end
29
+
30
+ # EnvCheck provides validation for environment variables using a YAML config file.
31
+ module EnvCheck
32
+ class Error < StandardError; end
33
+
34
+ class Result
35
+ attr_reader :errors, :warnings, :valid_vars
36
+
37
+ def initialize
38
+ @errors = []
39
+ @warnings = []
40
+ @valid_vars = []
41
+ end
42
+
43
+ def add_error(message)
44
+ @errors << message
45
+ end
46
+
47
+ def add_warning(message)
48
+ @warnings << message
49
+ end
50
+
51
+ def add_valid(var_name)
52
+ @valid_vars << var_name
53
+ end
54
+
55
+ def success?
56
+ @errors.empty?
57
+ end
58
+
59
+ def display_results
60
+ @valid_vars.each { |var| puts "✅ #{var} is set" }
61
+ @errors.each { |error| puts "❌ #{error}" }
62
+ @warnings.each { |warning| puts "⚠️ #{warning}" }
63
+ end
64
+ end
65
+
66
+ # Verifies environment variables against a YAML config file.
67
+ #
68
+ # @param config_path [String] path to the config file (auto-discovered: .env_check.yml or config/env_check.yml)
69
+ # @param environment [String] environment to use for environment-specific configuration
70
+ # @return [Result] validation result object
71
+ def self.verify(config_path = nil, environment = nil)
72
+ config = Config.new(config_path, environment)
73
+ result = Result.new
74
+
75
+ unless config.valid?
76
+ puts "⚠️ Config file not found: #{config.config_path}"
77
+ return result
78
+ end
79
+
80
+ validate_required_vars(config.required_vars, result)
81
+ validate_optional_vars(config.optional_vars, result)
82
+
83
+ result.display_results
84
+ result
85
+ end
86
+
87
+ # Verify with inline configuration (useful for testing or programmatic use)
88
+ def self.verify_with_config(config_hash)
89
+ config = Config.from_hash(config_hash)
90
+ result = Result.new
91
+
92
+ validate_required_vars(config.required_vars, result)
93
+ validate_optional_vars(config.optional_vars, result)
94
+
95
+ result.display_results
96
+ result
97
+ end
98
+
99
+ # Legacy method for backward compatibility - raises on error
100
+ def self.verify!(config_path = nil, environment = nil)
101
+ result = verify(config_path, environment)
102
+ raise Error, "Environment validation failed" unless result.success?
103
+
104
+ result
105
+ end
106
+
107
+ private_class_method def self.validate_required_vars(required_vars, result)
108
+ required_vars.each do |var|
109
+ if ENV[var].nil? || ENV[var].strip.empty?
110
+ result.add_error("Missing required ENV: #{var}")
111
+ else
112
+ result.add_valid(var)
113
+ end
114
+ end
115
+ end
116
+
117
+ private_class_method def self.validate_optional_vars(optional_vars, result)
118
+ optional_vars.each do |var, type|
119
+ value = ENV.fetch(var, nil)
120
+ next unless value
121
+
122
+ if valid_type?(value, type)
123
+ result.add_valid(var)
124
+ else
125
+ result.add_warning("#{var} should be a #{type}, got '#{value}'")
126
+ end
127
+ end
128
+ end
129
+
130
+ private_class_method def self.valid_type?(value, type)
131
+ case type.to_s.downcase
132
+ when "boolean"
133
+ Validators::Boolean.valid?(value)
134
+ when "integer"
135
+ Validators::Integer.valid?(value)
136
+ when "float"
137
+ Validators::Float.valid?(value)
138
+ when "url"
139
+ Validators::Url.valid?(value)
140
+ when "email"
141
+ Validators::Email.valid?(value)
142
+ when "path"
143
+ Validators::Path.valid?(value)
144
+ when "port"
145
+ Validators::Port.valid?(value)
146
+ when "json"
147
+ Validators::JsonString.valid?(value)
148
+ else
149
+ true # Default to valid for unknown types
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/tasks/env_check.rake
4
+
5
+ require_relative "../../lib/env_check"
6
+ if File.exist?(".env")
7
+ require "dotenv"
8
+ Dotenv.load
9
+ end
10
+
11
+ namespace :env do
12
+ desc "Check environment variable configuration"
13
+ task :check do
14
+ EnvCheck.verify!
15
+ end
16
+ end