dotlyte 0.1.1
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/README.md +30 -0
- data/lib/dotlyte/coercion.rb +55 -0
- data/lib/dotlyte/config.rb +209 -0
- data/lib/dotlyte/encryption.rb +120 -0
- data/lib/dotlyte/errors.rb +56 -0
- data/lib/dotlyte/interpolation.rb +169 -0
- data/lib/dotlyte/loader.rb +425 -0
- data/lib/dotlyte/masking.rb +79 -0
- data/lib/dotlyte/merger.rb +21 -0
- data/lib/dotlyte/validator.rb +231 -0
- data/lib/dotlyte/version.rb +5 -0
- data/lib/dotlyte/watcher.rb +152 -0
- data/lib/dotlyte.rb +57 -0
- metadata +76 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dotlyte
|
|
4
|
+
# Schema rule for a single config key.
|
|
5
|
+
class SchemaRule
|
|
6
|
+
attr_accessor :type, :required, :format, :pattern, :enum_values,
|
|
7
|
+
:min, :max, :default_value, :sensitive, :doc
|
|
8
|
+
|
|
9
|
+
def initialize(**kwargs)
|
|
10
|
+
@type = kwargs[:type]
|
|
11
|
+
@required = kwargs.fetch(:required, false)
|
|
12
|
+
@format = kwargs[:format]
|
|
13
|
+
@pattern = kwargs[:pattern]
|
|
14
|
+
@enum_values = kwargs[:enum_values]
|
|
15
|
+
@min = kwargs[:min]
|
|
16
|
+
@max = kwargs[:max]
|
|
17
|
+
@default_value = kwargs[:default_value]
|
|
18
|
+
@sensitive = kwargs.fetch(:sensitive, false)
|
|
19
|
+
@doc = kwargs[:doc]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# A single schema violation.
|
|
24
|
+
class SchemaViolation
|
|
25
|
+
attr_reader :key, :message, :rule
|
|
26
|
+
|
|
27
|
+
def initialize(key:, message:, rule:)
|
|
28
|
+
@key = key
|
|
29
|
+
@message = message
|
|
30
|
+
@rule = rule
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_s
|
|
34
|
+
"[#{rule}] #{key}: #{message}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Schema validation engine for DOTLYTE v2.
|
|
39
|
+
module Validator
|
|
40
|
+
FORMAT_PATTERNS = {
|
|
41
|
+
"email" => /\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\z/,
|
|
42
|
+
"uuid" => /\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/,
|
|
43
|
+
"date" => /\A\d{4}-\d{2}-\d{2}\z/,
|
|
44
|
+
"ipv4" => /\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
# Validate config data against a schema.
|
|
48
|
+
#
|
|
49
|
+
# @param data [Hash] the config data
|
|
50
|
+
# @param schema [Hash<String, SchemaRule>] the schema
|
|
51
|
+
# @param strict [Boolean] reject unknown keys
|
|
52
|
+
# @return [Array<SchemaViolation>]
|
|
53
|
+
def self.validate(data, schema, strict: false)
|
|
54
|
+
violations = []
|
|
55
|
+
|
|
56
|
+
schema.each do |key, rule|
|
|
57
|
+
val = get_nested(data, key)
|
|
58
|
+
|
|
59
|
+
if val.nil?
|
|
60
|
+
if rule.required
|
|
61
|
+
violations << SchemaViolation.new(
|
|
62
|
+
key: key, message: "required key '#{key}' is missing", rule: "required"
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
next
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Type check
|
|
69
|
+
if rule.type && !check_type(val, rule.type)
|
|
70
|
+
violations << SchemaViolation.new(
|
|
71
|
+
key: key,
|
|
72
|
+
message: "expected type '#{rule.type}', got #{val.class}",
|
|
73
|
+
rule: "type"
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Format check
|
|
78
|
+
if rule.format && val.is_a?(String) && !check_format(val, rule.format)
|
|
79
|
+
violations << SchemaViolation.new(
|
|
80
|
+
key: key,
|
|
81
|
+
message: "value '#{val}' does not match format '#{rule.format}'",
|
|
82
|
+
rule: "format"
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Pattern check
|
|
87
|
+
if rule.pattern && val.is_a?(String) && !val.match?(Regexp.new(rule.pattern))
|
|
88
|
+
violations << SchemaViolation.new(
|
|
89
|
+
key: key,
|
|
90
|
+
message: "value '#{val}' does not match pattern '#{rule.pattern}'",
|
|
91
|
+
rule: "pattern"
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Enum check
|
|
96
|
+
if rule.enum_values && !rule.enum_values.include?(val)
|
|
97
|
+
violations << SchemaViolation.new(
|
|
98
|
+
key: key,
|
|
99
|
+
message: "value #{val} not in allowed values: #{rule.enum_values}",
|
|
100
|
+
rule: "enum"
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Min/Max
|
|
105
|
+
if val.is_a?(Numeric)
|
|
106
|
+
if rule.min && val < rule.min
|
|
107
|
+
violations << SchemaViolation.new(
|
|
108
|
+
key: key, message: "value #{val} is less than minimum #{rule.min}", rule: "min"
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
if rule.max && val > rule.max
|
|
112
|
+
violations << SchemaViolation.new(
|
|
113
|
+
key: key, message: "value #{val} is greater than maximum #{rule.max}", rule: "max"
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Strict mode
|
|
120
|
+
if strict
|
|
121
|
+
flat_keys = flatten_keys(data)
|
|
122
|
+
flat_keys.each do |k|
|
|
123
|
+
unless schema.key?(k)
|
|
124
|
+
violations << SchemaViolation.new(
|
|
125
|
+
key: k, message: "unknown key '#{k}' (strict mode)", rule: "strict"
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
violations
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Apply schema defaults to data.
|
|
135
|
+
def self.apply_defaults(data, schema)
|
|
136
|
+
schema.each do |key, rule|
|
|
137
|
+
next if rule.default_value.nil?
|
|
138
|
+
next unless get_nested(data, key).nil?
|
|
139
|
+
|
|
140
|
+
set_nested(data, key, rule.default_value)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Get all sensitive keys from schema.
|
|
145
|
+
def self.sensitive_keys(schema)
|
|
146
|
+
schema.select { |_, rule| rule.sensitive }.keys
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Assert valid — raises ValidationError on failure.
|
|
150
|
+
def self.assert_valid!(data, schema, strict: false)
|
|
151
|
+
violations = validate(data, schema, strict: strict)
|
|
152
|
+
return if violations.empty?
|
|
153
|
+
|
|
154
|
+
msg = "Schema validation failed:\n" +
|
|
155
|
+
violations.map { |v| " - #{v}" }.join("\n")
|
|
156
|
+
raise ValidationError.new(msg, violations: violations)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Get a nested value via dot-notation key.
|
|
160
|
+
def self.get_nested(data, key)
|
|
161
|
+
parts = key.to_s.split(".")
|
|
162
|
+
current = data
|
|
163
|
+
parts.each do |part|
|
|
164
|
+
return nil unless current.is_a?(Hash)
|
|
165
|
+
|
|
166
|
+
current = current.key?(part) ? current[part] : current[part.to_sym]
|
|
167
|
+
return nil if current.nil?
|
|
168
|
+
end
|
|
169
|
+
current
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Set a nested value via dot-notation key.
|
|
173
|
+
def self.set_nested(data, key, value)
|
|
174
|
+
parts = key.to_s.split(".")
|
|
175
|
+
current = data
|
|
176
|
+
parts[0..-2].each do |part|
|
|
177
|
+
current[part] ||= {}
|
|
178
|
+
current[part] = {} unless current[part].is_a?(Hash)
|
|
179
|
+
current = current[part]
|
|
180
|
+
end
|
|
181
|
+
current[parts.last] = value
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
class << self
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def check_type(val, expected)
|
|
188
|
+
case expected
|
|
189
|
+
when "string" then val.is_a?(String)
|
|
190
|
+
when "number" then val.is_a?(Numeric)
|
|
191
|
+
when "boolean" then [true, false].include?(val)
|
|
192
|
+
when "array" then val.is_a?(Array)
|
|
193
|
+
when "hash", "object" then val.is_a?(Hash)
|
|
194
|
+
else true
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def check_format(val, fmt)
|
|
199
|
+
case fmt
|
|
200
|
+
when "url"
|
|
201
|
+
val.start_with?("http://") || val.start_with?("https://")
|
|
202
|
+
when "ip", "ipv4"
|
|
203
|
+
FORMAT_PATTERNS["ipv4"].match?(val)
|
|
204
|
+
when "port"
|
|
205
|
+
p = Integer(val, exception: false)
|
|
206
|
+
p && p >= 1 && p <= 65_535
|
|
207
|
+
when "email"
|
|
208
|
+
FORMAT_PATTERNS["email"].match?(val)
|
|
209
|
+
when "uuid"
|
|
210
|
+
FORMAT_PATTERNS["uuid"].match?(val)
|
|
211
|
+
when "date"
|
|
212
|
+
FORMAT_PATTERNS["date"].match?(val)
|
|
213
|
+
else
|
|
214
|
+
val.match?(Regexp.new(fmt))
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def flatten_keys(data, prefix = "", out = [])
|
|
219
|
+
data.each do |key, value|
|
|
220
|
+
full_key = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
|
|
221
|
+
if value.is_a?(Hash)
|
|
222
|
+
flatten_keys(value, full_key, out)
|
|
223
|
+
else
|
|
224
|
+
out << full_key
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
out
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Dotlyte
|
|
6
|
+
# Polling-based file watcher for DOTLYTE v2.
|
|
7
|
+
class ConfigWatcher
|
|
8
|
+
# Information about a detected change.
|
|
9
|
+
ChangeEvent = Struct.new(:path, :changed_keys, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
# @param files [Array<String>] files to watch
|
|
12
|
+
# @param debounce_ms [Integer] polling interval in milliseconds
|
|
13
|
+
def initialize(files:, debounce_ms: 300)
|
|
14
|
+
@files = files
|
|
15
|
+
@interval = [debounce_ms, 100].max / 1000.0
|
|
16
|
+
@last_modified = {}
|
|
17
|
+
@on_change = nil
|
|
18
|
+
@key_watchers = {}
|
|
19
|
+
@on_error = nil
|
|
20
|
+
@previous_data = nil
|
|
21
|
+
@running = false
|
|
22
|
+
@thread = nil
|
|
23
|
+
|
|
24
|
+
@files.each do |f|
|
|
25
|
+
@last_modified[f] = File.mtime(f).to_f if File.exist?(f)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Register general change callback.
|
|
30
|
+
def on_change(&block)
|
|
31
|
+
@on_change = block
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Watch a specific key for changes.
|
|
35
|
+
def watch_key(key, &block)
|
|
36
|
+
@key_watchers[key] = block
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Register error callback.
|
|
40
|
+
def on_error(&block)
|
|
41
|
+
@on_error = block
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Start watching with a reload proc.
|
|
45
|
+
#
|
|
46
|
+
# @param reload_fn [Proc] called to reload config, must return Hash
|
|
47
|
+
def start(reload_fn)
|
|
48
|
+
return if @running
|
|
49
|
+
|
|
50
|
+
@running = true
|
|
51
|
+
@thread = Thread.new do
|
|
52
|
+
while @running
|
|
53
|
+
poll(reload_fn)
|
|
54
|
+
sleep @interval
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
@thread.abort_on_exception = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Stop watching.
|
|
61
|
+
def stop
|
|
62
|
+
@running = false
|
|
63
|
+
@thread&.join(2)
|
|
64
|
+
@thread = nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def poll(reload_fn)
|
|
70
|
+
changed_file = nil
|
|
71
|
+
|
|
72
|
+
@files.each do |f|
|
|
73
|
+
next unless File.exist?(f)
|
|
74
|
+
|
|
75
|
+
mtime = File.mtime(f).to_f
|
|
76
|
+
prev = @last_modified[f]
|
|
77
|
+
if prev.nil? || prev != mtime
|
|
78
|
+
@last_modified[f] = mtime
|
|
79
|
+
changed_file = f
|
|
80
|
+
break
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
return unless changed_file
|
|
85
|
+
|
|
86
|
+
new_data = reload_fn.call
|
|
87
|
+
return unless new_data
|
|
88
|
+
|
|
89
|
+
changed_keys = if @previous_data
|
|
90
|
+
diff_maps(@previous_data, new_data)
|
|
91
|
+
else
|
|
92
|
+
flatten_keys(new_data)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
@on_change&.call(ChangeEvent.new(path: changed_file, changed_keys: changed_keys))
|
|
96
|
+
|
|
97
|
+
if @previous_data
|
|
98
|
+
changed_keys.each do |key|
|
|
99
|
+
watcher = @key_watchers[key]
|
|
100
|
+
next unless watcher
|
|
101
|
+
|
|
102
|
+
old_val = Validator.get_nested(@previous_data, key)
|
|
103
|
+
new_val = Validator.get_nested(new_data, key)
|
|
104
|
+
watcher.call(old_val, new_val)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
@previous_data = new_data
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
@on_error&.call(e)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def diff_maps(old_map, new_map)
|
|
114
|
+
old_flat = flatten_map(old_map)
|
|
115
|
+
new_flat = flatten_map(new_map)
|
|
116
|
+
changed = Set.new
|
|
117
|
+
|
|
118
|
+
new_flat.each do |k, v|
|
|
119
|
+
changed.add(k) if old_flat[k] != v
|
|
120
|
+
end
|
|
121
|
+
old_flat.each_key do |k|
|
|
122
|
+
changed.add(k) unless new_flat.key?(k)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
changed.to_a
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def flatten_map(data, prefix = "", out = {})
|
|
129
|
+
data.each do |key, value|
|
|
130
|
+
full_key = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
|
|
131
|
+
if value.is_a?(Hash)
|
|
132
|
+
flatten_map(value, full_key, out)
|
|
133
|
+
else
|
|
134
|
+
out[full_key] = value
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
out
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def flatten_keys(data, prefix = "", out = [])
|
|
141
|
+
data.each do |key, value|
|
|
142
|
+
full_key = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
|
|
143
|
+
if value.is_a?(Hash)
|
|
144
|
+
flatten_keys(value, full_key, out)
|
|
145
|
+
else
|
|
146
|
+
out << full_key
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
out
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
data/lib/dotlyte.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require "dotlyte/version"
|
|
5
|
+
require "dotlyte/errors"
|
|
6
|
+
require "dotlyte/coercion"
|
|
7
|
+
require "dotlyte/merger"
|
|
8
|
+
require "dotlyte/interpolation"
|
|
9
|
+
require "dotlyte/validator"
|
|
10
|
+
require "dotlyte/encryption"
|
|
11
|
+
require "dotlyte/masking"
|
|
12
|
+
require "dotlyte/config"
|
|
13
|
+
require "dotlyte/loader"
|
|
14
|
+
require "dotlyte/watcher"
|
|
15
|
+
|
|
16
|
+
# DOTLYTE — The universal configuration library (v2).
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# config = Dotlyte.load
|
|
20
|
+
# config.port # automatically Integer
|
|
21
|
+
# config.debug # automatically Boolean
|
|
22
|
+
# config.database.host # dot-notation access
|
|
23
|
+
#
|
|
24
|
+
# @example Advanced
|
|
25
|
+
# config = Dotlyte.load(
|
|
26
|
+
# env: "production",
|
|
27
|
+
# schema: { "port" => Dotlyte::SchemaRule.new(type: "number", required: true) },
|
|
28
|
+
# strict: true,
|
|
29
|
+
# find_up: true
|
|
30
|
+
# )
|
|
31
|
+
module Dotlyte
|
|
32
|
+
# Load configuration from all available sources.
|
|
33
|
+
#
|
|
34
|
+
# @param files [Array<String>, nil] Explicit files to load.
|
|
35
|
+
# @param prefix [String, nil] Environment variable prefix to strip.
|
|
36
|
+
# @param defaults [Hash, nil] Default values (lowest priority).
|
|
37
|
+
# @param sources [Array<String>, nil] Custom source order.
|
|
38
|
+
# @param env [String, nil] Environment name.
|
|
39
|
+
# @param schema [Hash<String, SchemaRule>, nil] Validation schema.
|
|
40
|
+
# @param strict [Boolean] Reject unknown keys.
|
|
41
|
+
# @param interpolate_vars [Boolean] Enable ${VAR} interpolation (default: true).
|
|
42
|
+
# @param overrides [Hash, nil] Override values (highest priority).
|
|
43
|
+
# @param debug [Boolean] Enable debug output.
|
|
44
|
+
# @param find_up [Boolean] Walk up directories to find config files.
|
|
45
|
+
# @param root_markers [Array<String>] Markers for root directory detection.
|
|
46
|
+
# @param cwd [String, nil] Working directory override.
|
|
47
|
+
# @param allow_all_env_vars [Boolean] Import all env vars without filtering.
|
|
48
|
+
# @param watch [Boolean] Watch files for changes.
|
|
49
|
+
# @param debounce_ms [Integer] Polling interval for watcher.
|
|
50
|
+
# @param custom_sources [Array<#load>] Custom source objects.
|
|
51
|
+
# @return [Config] Merged configuration object.
|
|
52
|
+
def self.load(**options)
|
|
53
|
+
Loader.new(**{
|
|
54
|
+
defaults: options[:defaults] || {}
|
|
55
|
+
}.merge(options)).load
|
|
56
|
+
end
|
|
57
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dotlyte
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- DOTLYTE Contributors
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: base64
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
description: One API to load .env, YAML, JSON, TOML, environment variables, and defaults
|
|
28
|
+
with automatic type coercion, layered priority, AES-256-GCM encryption, schema validation,
|
|
29
|
+
variable interpolation, sensitive value masking, and file watching.
|
|
30
|
+
email:
|
|
31
|
+
- hello@dotlyte.dev
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- README.md
|
|
37
|
+
- lib/dotlyte.rb
|
|
38
|
+
- lib/dotlyte/coercion.rb
|
|
39
|
+
- lib/dotlyte/config.rb
|
|
40
|
+
- lib/dotlyte/encryption.rb
|
|
41
|
+
- lib/dotlyte/errors.rb
|
|
42
|
+
- lib/dotlyte/interpolation.rb
|
|
43
|
+
- lib/dotlyte/loader.rb
|
|
44
|
+
- lib/dotlyte/masking.rb
|
|
45
|
+
- lib/dotlyte/merger.rb
|
|
46
|
+
- lib/dotlyte/validator.rb
|
|
47
|
+
- lib/dotlyte/version.rb
|
|
48
|
+
- lib/dotlyte/watcher.rb
|
|
49
|
+
homepage: https://dotlyte.dev
|
|
50
|
+
licenses:
|
|
51
|
+
- MIT
|
|
52
|
+
metadata:
|
|
53
|
+
homepage_uri: https://dotlyte.dev
|
|
54
|
+
source_code_uri: https://github.com/dotlyte-io/dotlyte/tree/main/langs/ruby
|
|
55
|
+
changelog_uri: https://github.com/dotlyte-io/dotlyte/blob/main/langs/ruby/CHANGELOG.md
|
|
56
|
+
post_install_message:
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: 3.0.0
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 3.0.3.1
|
|
72
|
+
signing_key:
|
|
73
|
+
specification_version: 4
|
|
74
|
+
summary: The universal .env and configuration library with encryption, validation,
|
|
75
|
+
and more.
|
|
76
|
+
test_files: []
|