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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 256215c35e39e22093122c4c9d2452e3298beb699cbb84189a14dbf88ce75dbb
4
- data.tar.gz: ddada81c036244804246bd9e8c5c790778a9621bf412aef912cedf15ecd4e116
3
+ metadata.gz: 5e512ec86bdcf2702d8c0d53552284aa43f5f51b6506e61d1275aad3af1e9dca
4
+ data.tar.gz: 7e65c48367a0e9d853c647e6339e40063ba23f8873f5e9668009a2c081cef5db
5
5
  SHA512:
6
- metadata.gz: ef02926fa7b051d80cde13876739650db6c3279a8f068836292ff2676633668cdabc6256e6c50ca82a37f4a806ed942deebdbecd703f210dcfcce14f0171db4e
7
- data.tar.gz: 9d991cd745f294cb1d989144ba6136d6f64a52af32d7107d05d38193bca92a6740add34ad57e43dae9fa4103f1256f1d6852fbb87e56ca44d9cb371092b1a081
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module EnvDiff
5
- VERSION = '0.2.0'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
@@ -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
- Diff.new(source, target)
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.2.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-04 00:00:00.000000000 Z
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.