philiprehberger-env_diff 0.2.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +69 -4
- data/lib/philiprehberger/env_diff/diff.rb +13 -0
- data/lib/philiprehberger/env_diff/version.rb +1 -1
- data/lib/philiprehberger/env_diff.rb +87 -8
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e512ec86bdcf2702d8c0d53552284aa43f5f51b6506e61d1275aad3af1e9dca
|
|
4
|
+
data.tar.gz: 7e65c48367a0e9d853c647e6339e40063ba23f8873f5e9668009a2c081cef5db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec3677be78c8bc9df56fe6f2229fe625feae051ba6f6558f9d3bf0f033f0d575528a69ffe1c09642a6b8879bd01a363be75ef5a2524ddb46a19effe06fc59b1a
|
|
7
|
+
data.tar.gz: c79673bf83bfde8cd8aec1bcc824e08b6e82f7e39314d31c5a35fae8f87469f9398196d7dd04e92524550af9ac0a602152ed368cc27e22a8639f268c50085ec3
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-04-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Diff#status_for(key)` — returns the status of a single key as a Symbol (`:added`, `:removed`, `:changed`, `:unchanged`), or `nil` if the key is absent from both sides
|
|
14
|
+
|
|
15
|
+
## [0.3.0] - 2026-04-14
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `EnvDiff.validate(target, required:)` — check that all required keys exist in a target hash or .env file
|
|
19
|
+
- `case_sensitive:` keyword on `compare`, `from_hash`, `from_env_file`, and `from_system` — normalize keys to uppercase when false
|
|
20
|
+
- `EnvDiff.to_markdown(diff)` — format a diff result as a Markdown table string
|
|
21
|
+
- `EnvDiff.to_html(diff)` — format a diff result as an HTML table string
|
|
22
|
+
|
|
10
23
|
## [0.2.0] - 2026-04-03
|
|
11
24
|
|
|
12
25
|
### Added
|
data/README.md
CHANGED
|
@@ -82,6 +82,67 @@ diff.stats
|
|
|
82
82
|
# => { added: 1, removed: 1, changed: 1, unchanged: 1, total: 4 }
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### Per-key status
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
diff.status_for("NEW_KEY") # => :added
|
|
89
|
+
diff.status_for("OLD_KEY") # => :removed
|
|
90
|
+
diff.status_for("DATABASE_URL") # => :changed
|
|
91
|
+
diff.status_for("SECRET") # => :unchanged
|
|
92
|
+
diff.status_for("NOT_THERE") # => nil
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Validation
|
|
96
|
+
|
|
97
|
+
Check that all required keys exist in a target hash or `.env` file:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
target = { 'DATABASE_URL' => 'postgres://localhost', 'PORT' => '3000' }
|
|
101
|
+
result = Philiprehberger::EnvDiff.validate(target, required: %w[DATABASE_URL SECRET PORT])
|
|
102
|
+
result[:valid] # => false
|
|
103
|
+
result[:missing] # => ["SECRET"]
|
|
104
|
+
|
|
105
|
+
# Also works with .env file paths
|
|
106
|
+
result = Philiprehberger::EnvDiff.validate(".env.production", required: %w[DATABASE_URL SECRET])
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Case-Insensitive Comparison
|
|
110
|
+
|
|
111
|
+
Normalize keys to uppercase before comparing by passing `case_sensitive: false`:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
diff = Philiprehberger::EnvDiff.compare(
|
|
115
|
+
{ "db_host" => "localhost" },
|
|
116
|
+
{ "DB_HOST" => "localhost" },
|
|
117
|
+
case_sensitive: false
|
|
118
|
+
)
|
|
119
|
+
diff.changed? # => false
|
|
120
|
+
diff.unchanged # => ["DB_HOST"]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Export Formats
|
|
124
|
+
|
|
125
|
+
Format a diff result as a Markdown or HTML table:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
diff = Philiprehberger::EnvDiff.compare(source, target)
|
|
129
|
+
|
|
130
|
+
puts Philiprehberger::EnvDiff.to_markdown(diff)
|
|
131
|
+
# | Key | Status | Source | Target |
|
|
132
|
+
# | --- | ------ | ------ | ------ |
|
|
133
|
+
# | NEW_KEY | added | | added |
|
|
134
|
+
# | OLD_KEY | removed | remove_me | |
|
|
135
|
+
# | DATABASE_URL | changed | postgres://localhost/dev | postgres://prod-host/app |
|
|
136
|
+
# | SECRET | unchanged | abc | abc |
|
|
137
|
+
|
|
138
|
+
puts Philiprehberger::EnvDiff.to_html(diff)
|
|
139
|
+
# <table>
|
|
140
|
+
# <tr><th>Key</th><th>Status</th><th>Source</th><th>Target</th></tr>
|
|
141
|
+
# <tr><td>NEW_KEY</td><td>added</td><td></td><td>added</td></tr>
|
|
142
|
+
# ...
|
|
143
|
+
# </table>
|
|
144
|
+
```
|
|
145
|
+
|
|
85
146
|
### Compare against system ENV
|
|
86
147
|
|
|
87
148
|
```ruby
|
|
@@ -112,10 +173,13 @@ ENV
|
|
|
112
173
|
|
|
113
174
|
| Method / Class | Description |
|
|
114
175
|
|----------------|-------------|
|
|
115
|
-
| `EnvDiff.compare(source, target)` | Compare two hashes and return a `Diff` |
|
|
116
|
-
| `EnvDiff.from_hash(hash_a, hash_b)` | Alias for `compare` |
|
|
117
|
-
| `EnvDiff.from_env_file(path_a, path_b)` | Parse two `.env` files and compare them |
|
|
118
|
-
| `EnvDiff.from_system(target)` | Compare current `ENV` against a target hash or `.env` file path |
|
|
176
|
+
| `EnvDiff.compare(source, target, case_sensitive: true)` | Compare two hashes and return a `Diff` |
|
|
177
|
+
| `EnvDiff.from_hash(hash_a, hash_b, case_sensitive: true)` | Alias for `compare` |
|
|
178
|
+
| `EnvDiff.from_env_file(path_a, path_b, case_sensitive: true)` | Parse two `.env` files and compare them |
|
|
179
|
+
| `EnvDiff.from_system(target, case_sensitive: true)` | Compare current `ENV` against a target hash or `.env` file path |
|
|
180
|
+
| `EnvDiff.validate(target, required:)` | Check that all required keys exist in target; returns `{ valid:, missing: }` |
|
|
181
|
+
| `EnvDiff.to_markdown(diff)` | Format a diff result as a Markdown table string |
|
|
182
|
+
| `EnvDiff.to_html(diff)` | Format a diff result as an HTML table string |
|
|
119
183
|
| `Diff#added` | Array of keys in target but not source |
|
|
120
184
|
| `Diff#removed` | Array of keys in source but not target |
|
|
121
185
|
| `Diff#changed` | Hash of keys with different values |
|
|
@@ -126,6 +190,7 @@ ENV
|
|
|
126
190
|
| `Diff#to_json` | JSON serialization of the structured hash |
|
|
127
191
|
| `Diff#filter(pattern:)` | New `Diff` containing only keys matching the regex pattern |
|
|
128
192
|
| `Diff#stats` | Hash of counts: `:added`, `:removed`, `:changed`, `:unchanged`, `:total` |
|
|
193
|
+
| `Diff#status_for(key)` | Status of a single key as `:added`, `:removed`, `:changed`, `:unchanged`, or `nil` |
|
|
129
194
|
| `Parser.parse(content)` | Parse `.env` string into a hash |
|
|
130
195
|
| `Parser.parse_file(path:)` | Read and parse a `.env` file |
|
|
131
196
|
|
|
@@ -92,6 +92,19 @@ module Philiprehberger
|
|
|
92
92
|
}
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# Status of a single key in this diff.
|
|
96
|
+
#
|
|
97
|
+
# @param key [String] the key to look up
|
|
98
|
+
# @return [Symbol, nil] one of :added, :removed, :changed, :unchanged, or nil if absent from both sides
|
|
99
|
+
def status_for(key)
|
|
100
|
+
return :added if @added.include?(key)
|
|
101
|
+
return :removed if @removed.include?(key)
|
|
102
|
+
return :changed if @changed.key?(key)
|
|
103
|
+
return :unchanged if @unchanged.include?(key)
|
|
104
|
+
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
95
108
|
private
|
|
96
109
|
|
|
97
110
|
def build_changed(source, target)
|
|
@@ -12,36 +12,115 @@ module Philiprehberger
|
|
|
12
12
|
#
|
|
13
13
|
# @param source [Hash] the baseline environment hash
|
|
14
14
|
# @param target [Hash] the environment hash to compare against
|
|
15
|
+
# @param case_sensitive [Boolean] whether key comparison is case-sensitive (default: true)
|
|
15
16
|
# @return [Diff] the computed differences
|
|
16
|
-
def self.compare(source, target)
|
|
17
|
-
|
|
17
|
+
def self.compare(source, target, case_sensitive: true)
|
|
18
|
+
if case_sensitive
|
|
19
|
+
Diff.new(source, target)
|
|
20
|
+
else
|
|
21
|
+
normalized_source = source.transform_keys(&:upcase)
|
|
22
|
+
normalized_target = target.transform_keys(&:upcase)
|
|
23
|
+
Diff.new(normalized_source, normalized_target)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Validate that all required keys exist in target.
|
|
28
|
+
#
|
|
29
|
+
# @param target [Hash, String] a hash of target variables or a path to a .env file
|
|
30
|
+
# @param required [Array<String>] list of required key names
|
|
31
|
+
# @return [Hash] with :valid (Boolean) and :missing (Array<String>)
|
|
32
|
+
def self.validate(target, required:)
|
|
33
|
+
target_hash = target.is_a?(String) ? Parser.parse_file(path: target) : target
|
|
34
|
+
missing = required.reject { |key| target_hash.key?(key) }
|
|
35
|
+
{ valid: missing.empty?, missing: missing }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Format a diff result as a Markdown table string.
|
|
39
|
+
#
|
|
40
|
+
# @param diff [Diff] the diff to format
|
|
41
|
+
# @return [String] Markdown table
|
|
42
|
+
def self.to_markdown(diff)
|
|
43
|
+
lines = []
|
|
44
|
+
lines << '| Key | Status | Source | Target |'
|
|
45
|
+
lines << '| --- | ------ | ------ | ------ |'
|
|
46
|
+
|
|
47
|
+
diff.added.each do |key|
|
|
48
|
+
lines << "| #{key} | added | | #{diff.to_h[:added][key]} |"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
diff.removed.each do |key|
|
|
52
|
+
lines << "| #{key} | removed | #{diff.to_h[:removed][key]} | |"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
diff.changed.each do |key, vals|
|
|
56
|
+
lines << "| #{key} | changed | #{vals[:source]} | #{vals[:target]} |"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
diff.unchanged.each do |key|
|
|
60
|
+
lines << "| #{key} | unchanged | #{diff.to_h[:unchanged][key]} | #{diff.to_h[:unchanged][key]} |"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
lines.join("\n")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Format a diff result as an HTML table string.
|
|
67
|
+
#
|
|
68
|
+
# @param diff [Diff] the diff to format
|
|
69
|
+
# @return [String] HTML table
|
|
70
|
+
def self.to_html(diff)
|
|
71
|
+
rows = diff.added.map do |key|
|
|
72
|
+
" <tr><td>#{key}</td><td>added</td><td></td><td>#{diff.to_h[:added][key]}</td></tr>"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
diff.removed.each do |key|
|
|
76
|
+
rows << " <tr><td>#{key}</td><td>removed</td><td>#{diff.to_h[:removed][key]}</td><td></td></tr>"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
diff.changed.each do |key, vals|
|
|
80
|
+
rows << " <tr><td>#{key}</td><td>changed</td><td>#{vals[:source]}</td><td>#{vals[:target]}</td></tr>"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
diff.unchanged.each do |key|
|
|
84
|
+
val = diff.to_h[:unchanged][key]
|
|
85
|
+
rows << " <tr><td>#{key}</td><td>unchanged</td><td>#{val}</td><td>#{val}</td></tr>"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
lines = []
|
|
89
|
+
lines << '<table>'
|
|
90
|
+
lines << ' <tr><th>Key</th><th>Status</th><th>Source</th><th>Target</th></tr>'
|
|
91
|
+
lines.concat(rows)
|
|
92
|
+
lines << '</table>'
|
|
93
|
+
lines.join("\n")
|
|
18
94
|
end
|
|
19
95
|
|
|
20
96
|
# Alias for compare — compare two hashes.
|
|
21
97
|
#
|
|
22
98
|
# @param hash_a [Hash] the baseline environment hash
|
|
23
99
|
# @param hash_b [Hash] the environment hash to compare against
|
|
100
|
+
# @param case_sensitive [Boolean] whether key comparison is case-sensitive (default: true)
|
|
24
101
|
# @return [Diff] the computed differences
|
|
25
|
-
def self.from_hash(hash_a, hash_b)
|
|
26
|
-
compare(hash_a, hash_b)
|
|
102
|
+
def self.from_hash(hash_a, hash_b, case_sensitive: true)
|
|
103
|
+
compare(hash_a, hash_b, case_sensitive: case_sensitive)
|
|
27
104
|
end
|
|
28
105
|
|
|
29
106
|
# Parse two .env files and compare them.
|
|
30
107
|
#
|
|
31
108
|
# @param path_a [String] path to the source .env file
|
|
32
109
|
# @param path_b [String] path to the target .env file
|
|
110
|
+
# @param case_sensitive [Boolean] whether key comparison is case-sensitive (default: true)
|
|
33
111
|
# @return [Diff] the computed differences
|
|
34
|
-
def self.from_env_file(path_a, path_b)
|
|
35
|
-
compare(Parser.parse_file(path: path_a), Parser.parse_file(path: path_b))
|
|
112
|
+
def self.from_env_file(path_a, path_b, case_sensitive: true)
|
|
113
|
+
compare(Parser.parse_file(path: path_a), Parser.parse_file(path: path_b), case_sensitive: case_sensitive)
|
|
36
114
|
end
|
|
37
115
|
|
|
38
116
|
# Compare the current system ENV against a target hash or .env file path.
|
|
39
117
|
#
|
|
40
118
|
# @param target [Hash, String] a hash of target variables or a path to a .env file
|
|
119
|
+
# @param case_sensitive [Boolean] whether key comparison is case-sensitive (default: true)
|
|
41
120
|
# @return [Diff] the computed differences between ENV and target
|
|
42
|
-
def self.from_system(target)
|
|
121
|
+
def self.from_system(target, case_sensitive: true)
|
|
43
122
|
target_hash = target.is_a?(String) ? Parser.parse_file(path: target) : target
|
|
44
|
-
compare(ENV.to_h, target_hash)
|
|
123
|
+
compare(ENV.to_h, target_hash, case_sensitive: case_sensitive)
|
|
45
124
|
end
|
|
46
125
|
end
|
|
47
126
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-env_diff
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Parse .env files or environment hashes, compare them, and get a clear
|
|
14
14
|
report of added, removed, changed, and unchanged variables.
|