dotenv-merge 1.0.0 → 1.0.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.
@@ -6,7 +6,8 @@ module Dotenv
6
6
  # Parses and categorizes lines as assignments, comments, blank lines, or invalid.
7
7
  #
8
8
  # Inherits from Ast::Merge::AstNode for a normalized API across all ast-merge
9
- # content nodes. This provides #slice, #location, #unwrap, and other standard methods.
9
+ # content nodes. This provides TreeHaver::Node protocol compatibility including
10
+ # #slice, #location, #unwrap, #type, #text, and other standard methods.
10
11
  #
11
12
  # Dotenv files follow a simple format where each line is one of:
12
13
  # - `KEY=value` - Environment variable assignment
@@ -46,7 +47,7 @@ module Dotenv
46
47
  attr_reader :line_number
47
48
 
48
49
  # @return [Symbol, nil] The line type (:assignment, :comment, :blank, :invalid)
49
- attr_reader :type
50
+ attr_reader :line_type
50
51
 
51
52
  # @return [String, nil] The environment variable key (for assignments)
52
53
  attr_reader :key
@@ -64,7 +65,7 @@ module Dotenv
64
65
  def initialize(raw, line_number)
65
66
  @raw = raw
66
67
  @line_number = line_number
67
- @type = nil
68
+ @line_type = nil
68
69
  @key = nil
69
70
  @value = nil
70
71
  @export = false
@@ -80,11 +81,17 @@ module Dotenv
80
81
  super(slice: @raw, location: location)
81
82
  end
82
83
 
84
+ # TreeHaver::Node protocol: type
85
+ # @return [String] "env_line"
86
+ def type
87
+ "env_line"
88
+ end
89
+
83
90
  # Generate a unique signature for this line (used for merge matching)
84
91
  #
85
92
  # @return [Array<Symbol, String>, nil] Signature array [:env, key] for assignments, nil otherwise
86
93
  def signature
87
- return unless @type == :assignment
94
+ return unless @line_type == :assignment
88
95
 
89
96
  [:env, @key]
90
97
  end
@@ -93,28 +100,28 @@ module Dotenv
93
100
  #
94
101
  # @return [Boolean] true if the line is a valid KEY=value assignment
95
102
  def assignment?
96
- @type == :assignment
103
+ @line_type == :assignment
97
104
  end
98
105
 
99
106
  # Check if this line is a comment
100
107
  #
101
108
  # @return [Boolean] true if the line starts with #
102
109
  def comment?
103
- @type == :comment
110
+ @line_type == :comment
104
111
  end
105
112
 
106
113
  # Check if this line is blank (empty or whitespace only)
107
114
  #
108
115
  # @return [Boolean] true if the line is blank
109
116
  def blank?
110
- @type == :blank
117
+ @line_type == :blank
111
118
  end
112
119
 
113
120
  # Check if this line is invalid (unparseable)
114
121
  #
115
122
  # @return [Boolean] true if the line could not be parsed
116
123
  def invalid?
117
- @type == :invalid
124
+ @line_type == :invalid
118
125
  end
119
126
 
120
127
  # Check if this line has the export prefix
@@ -144,20 +151,20 @@ module Dotenv
144
151
  #
145
152
  # @return [String] A debug representation of this EnvLine
146
153
  def inspect
147
- "#<#{self.class.name} line=#{@line_number} type=#{@type} key=#{@key.inspect}>"
154
+ "#<#{self.class.name} line=#{@line_number} line_type=#{@line_type} key=#{@key.inspect}>"
148
155
  end
149
156
 
150
157
  private
151
158
 
152
- # Parse the raw line content and set type, key, value, and export
159
+ # Parse the raw line content and set line_type, key, value, and export
153
160
  #
154
161
  # @return [void]
155
162
  def parse!
156
163
  stripped = @raw.strip
157
164
  if stripped.empty?
158
- @type = :blank
165
+ @line_type = :blank
159
166
  elsif stripped.start_with?("#")
160
- @type = :comment
167
+ @line_type = :comment
161
168
  else
162
169
  parse_assignment!(stripped)
163
170
  end
@@ -178,14 +185,14 @@ module Dotenv
178
185
  key_part, value_part = line.split("=", 2)
179
186
  key_part = key_part.strip
180
187
  if valid_key?(key_part)
181
- @type = :assignment
188
+ @line_type = :assignment
182
189
  @key = key_part
183
190
  @value = unquote(value_part || "")
184
191
  else
185
- @type = :invalid
192
+ @line_type = :invalid
186
193
  end
187
194
  else
188
- @type = :invalid
195
+ @line_type = :invalid
189
196
  end
190
197
  end
191
198
 
@@ -33,10 +33,12 @@ module Dotenv
33
33
  # @param source [String] Dotenv source code to analyze
34
34
  # @param freeze_token [String] Token for freeze block markers (default: "dotenv-merge")
35
35
  # @param signature_generator [Proc, nil] Custom signature generator
36
- def initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil)
36
+ # @param options [Hash] Additional options (forward compatibility - ignored by FileAnalysis)
37
+ def initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, **options)
37
38
  @source = source
38
39
  @freeze_token = freeze_token
39
40
  @signature_generator = signature_generator
41
+ # **options captures any additional parameters (e.g., node_typing) for forward compatibility
40
42
 
41
43
  # Parse all lines
42
44
  @lines = parse_lines(source)
@@ -35,8 +35,9 @@ module Dotenv
35
35
  # Initialize a new merge result
36
36
  # @param template_analysis [FileAnalysis] Analysis of the template file
37
37
  # @param dest_analysis [FileAnalysis] Analysis of the destination file
38
- def initialize(template_analysis, dest_analysis)
39
- super(template_analysis: template_analysis, dest_analysis: dest_analysis)
38
+ # @param options [Hash] Additional options for forward compatibility
39
+ def initialize(template_analysis, dest_analysis, **options)
40
+ super(template_analysis: template_analysis, dest_analysis: dest_analysis, **options)
40
41
  end
41
42
 
42
43
  # Add content from the template at the given statement index
@@ -19,78 +19,108 @@ module Dotenv
19
19
  # add_template_only_nodes: true,
20
20
  # )
21
21
  # result = merger.merge
22
- class SmartMerger
23
- # @return [FileAnalysis] Analysis of template file
24
- attr_reader :template_analysis
25
-
26
- # @return [FileAnalysis] Analysis of destination file
27
- attr_reader :dest_analysis
28
-
22
+ #
23
+ # @example With node_typing for per-node-type preferences
24
+ # merger = SmartMerger.new(template, dest,
25
+ # node_typing: { "EnvLine" => ->(n) { NodeTyping.with_merge_type(n, :secret) } },
26
+ # preference: { default: :destination, secret: :template })
27
+ class SmartMerger < ::Ast::Merge::SmartMergerBase
29
28
  # Initialize a new SmartMerger
30
29
  #
31
30
  # @param template_content [String] Content of the template dotenv file
32
31
  # @param dest_content [String] Content of the destination dotenv file
33
- # @param preference [Symbol] Which version to prefer on match
34
- # (:template or :destination, default: :destination)
32
+ # @param signature_generator [Proc, nil] Custom signature generator
33
+ # @param preference [Symbol, Hash] :destination, :template, or per-type Hash
35
34
  # @param add_template_only_nodes [Boolean] Whether to add template-only env vars
36
35
  # (default: false)
37
36
  # @param freeze_token [String] Token for freeze block markers
38
37
  # (default: "dotenv-merge")
39
- # @param signature_generator [Proc, nil] Custom signature generator
38
+ # @param match_refiner [#call, nil] Match refiner for fuzzy matching
39
+ # @param regions [Array<Hash>, nil] Region configurations for nested merging
40
+ # @param region_placeholder [String, nil] Custom placeholder for regions
41
+ # @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
42
+ # for per-node-type merge preferences
43
+ # @param options [Hash] Additional options for forward compatibility
40
44
  def initialize(
41
45
  template_content,
42
46
  dest_content,
47
+ signature_generator: nil,
43
48
  preference: :destination,
44
49
  add_template_only_nodes: false,
45
- freeze_token: FileAnalysis::DEFAULT_FREEZE_TOKEN,
46
- signature_generator: nil
50
+ freeze_token: nil,
51
+ match_refiner: nil,
52
+ regions: nil,
53
+ region_placeholder: nil,
54
+ node_typing: nil,
55
+ **options
47
56
  )
48
- @preference = preference
49
- @add_template_only_nodes = add_template_only_nodes
50
-
51
- # Parse template
52
- @template_analysis = FileAnalysis.new(
57
+ super(
53
58
  template_content,
54
- freeze_token: freeze_token,
55
- signature_generator: signature_generator,
56
- )
57
-
58
- # Parse destination
59
- @dest_analysis = FileAnalysis.new(
60
59
  dest_content,
61
- freeze_token: freeze_token,
62
60
  signature_generator: signature_generator,
61
+ preference: preference,
62
+ add_template_only_nodes: add_template_only_nodes,
63
+ freeze_token: freeze_token,
64
+ match_refiner: match_refiner,
65
+ regions: regions,
66
+ region_placeholder: region_placeholder,
67
+ node_typing: node_typing,
68
+ **options
63
69
  )
70
+ end
71
+
72
+ protected
64
73
 
65
- @result = MergeResult.new(@template_analysis, @dest_analysis)
74
+ # @return [Class] The analysis class for dotenv files
75
+ def analysis_class
76
+ FileAnalysis
66
77
  end
67
78
 
68
- # Perform the merge operation
69
- #
70
- # @return [String] The merged content as a string
71
- def merge
72
- merge_result.to_s
79
+ # @return [String] The default freeze token
80
+ def default_freeze_token
81
+ "dotenv-merge"
82
+ end
83
+
84
+ # @return [Class, nil] No separate resolver class for dotenv
85
+ def resolver_class
86
+ nil
87
+ end
88
+
89
+ # @return [Class, nil] Result class (built with analysis args)
90
+ def result_class
91
+ nil
92
+ end
93
+
94
+ # Build the result with required analysis arguments
95
+ def build_result
96
+ MergeResult.new(@template_analysis, @dest_analysis)
73
97
  end
74
98
 
75
- # Perform the merge operation and return the full result object
99
+ # @return [Class] The template parse error class for dotenv
100
+ def template_parse_error_class
101
+ ParseError
102
+ end
103
+
104
+ # @return [Class] The destination parse error class for dotenv
105
+ def destination_parse_error_class
106
+ ParseError
107
+ end
108
+
109
+ # Perform the dotenv-specific merge with custom alignment logic
76
110
  #
77
- # @return [MergeResult] The merge result containing merged content
78
- def merge_result
79
- return @merge_result if @merge_result
80
-
81
- @merge_result = DebugLogger.time("SmartMerger#merge") do
82
- alignment = align_statements
83
-
84
- DebugLogger.debug("Alignment complete", {
85
- total_entries: alignment.size,
86
- matches: alignment.count { |e| e[:type] == :match },
87
- template_only: alignment.count { |e| e[:type] == :template_only },
88
- dest_only: alignment.count { |e| e[:type] == :dest_only },
89
- })
90
-
91
- process_alignment(alignment)
92
- @result
93
- end
111
+ # @return [MergeResult] The merge result
112
+ def perform_merge
113
+ alignment = align_statements
114
+
115
+ DebugLogger.debug("Alignment complete", {
116
+ total_entries: alignment.size,
117
+ matches: alignment.count { |e| e[:type] == :match },
118
+ template_only: alignment.count { |e| e[:type] == :template_only },
119
+ dest_only: alignment.count { |e| e[:type] == :dest_only },
120
+ })
121
+
122
+ process_alignment(alignment)
123
+ @result
94
124
  end
95
125
 
96
126
  private
@@ -211,8 +241,10 @@ module Dotenv
211
241
  return
212
242
  end
213
243
 
214
- # Apply preference
215
- case @preference
244
+ # Resolve preference (handles both Symbol and Hash preferences)
245
+ resolved_pref = resolve_preference(entry[:template_stmt], entry[:dest_stmt])
246
+
247
+ case resolved_pref
216
248
  when :template
217
249
  @result.add_from_template(entry[:template_index], decision: MergeResult::DECISION_TEMPLATE)
218
250
  when :destination
@@ -222,6 +254,47 @@ module Dotenv
222
254
  end
223
255
  end
224
256
 
257
+ # Resolve preference for a matched pair
258
+ # @param template_stmt [Object] Template statement
259
+ # @param dest_stmt [Object] Destination statement
260
+ # @return [Symbol] :template or :destination
261
+ def resolve_preference(template_stmt, dest_stmt)
262
+ return @preference if @preference.is_a?(Symbol)
263
+
264
+ # Hash preference - check for node_typing-based merge_types
265
+ if @preference.is_a?(Hash)
266
+ # Apply node_typing if configured
267
+ typed_template = apply_node_typing(template_stmt)
268
+ apply_node_typing(dest_stmt)
269
+
270
+ # Check template merge_type first
271
+ if Ast::Merge::NodeTyping.typed_node?(typed_template)
272
+ merge_type = typed_template.merge_type
273
+ return @preference[merge_type] if @preference.key?(merge_type)
274
+ end
275
+
276
+ # Fall back to default
277
+ return @preference[:default] || :destination
278
+ end
279
+
280
+ :destination
281
+ end
282
+
283
+ # Apply node typing to a statement if node_typing is configured
284
+ # @param stmt [Object] The statement
285
+ # @return [Object] The statement, possibly wrapped with merge_type
286
+ def apply_node_typing(stmt)
287
+ return stmt unless @node_typing
288
+ return stmt unless stmt
289
+
290
+ # Check by class name
291
+ type_key = stmt.class.name&.split("::")&.last
292
+ callable = @node_typing[type_key] || @node_typing[type_key&.to_sym]
293
+ return callable.call(stmt) if callable
294
+
295
+ stmt
296
+ end
297
+
225
298
  # Process a template-only entry
226
299
  # @param entry [Hash] Alignment entry
227
300
  # @return [void]
@@ -5,7 +5,7 @@ module Dotenv
5
5
  # Version information for Dotenv::Merge
6
6
  module Version
7
7
  # Current version of the dotenv-merge gem
8
- VERSION = "1.0.0"
8
+ VERSION = "1.0.1"
9
9
  end
10
10
  VERSION = Version::VERSION # traditional location
11
11
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dotenv-merge
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -43,14 +43,20 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.0'
46
+ version: '2.0'
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 2.0.6
47
50
  type: :runtime
48
51
  prerelease: false
49
52
  version_requirements: !ruby/object:Gem::Requirement
50
53
  requirements:
51
54
  - - "~>"
52
55
  - !ruby/object:Gem::Version
53
- version: '1.0'
56
+ version: '2.0'
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 2.0.6
54
60
  - !ruby/object:Gem::Dependency
55
61
  name: version_gem
56
62
  requirement: !ruby/object:Gem::Requirement
@@ -266,10 +272,10 @@ licenses:
266
272
  - MIT
267
273
  metadata:
268
274
  homepage_uri: https://dotenv-merge.galtzo.com/
269
- source_code_uri: https://github.com/kettle-rb/dotenv-merge/tree/v1.0.0
270
- changelog_uri: https://github.com/kettle-rb/dotenv-merge/blob/v1.0.0/CHANGELOG.md
275
+ source_code_uri: https://github.com/kettle-rb/dotenv-merge/tree/v1.0.1
276
+ changelog_uri: https://github.com/kettle-rb/dotenv-merge/blob/v1.0.1/CHANGELOG.md
271
277
  bug_tracker_uri: https://github.com/kettle-rb/dotenv-merge/issues
272
- documentation_uri: https://www.rubydoc.info/gems/dotenv-merge/1.0.0
278
+ documentation_uri: https://www.rubydoc.info/gems/dotenv-merge/1.0.1
273
279
  funding_uri: https://github.com/sponsors/pboling
274
280
  wiki_uri: https://github.com/kettle-rb/dotenv-merge/wiki
275
281
  news_uri: https://www.railsbling.com/tags/dotenv-merge
@@ -298,7 +304,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
298
304
  - !ruby/object:Gem::Version
299
305
  version: '0'
300
306
  requirements: []
301
- rubygems_version: 4.0.1
307
+ rubygems_version: 4.0.3
302
308
  specification_version: 4
303
309
  summary: "☯️ Intelligent .env file merging using structured parsing"
304
310
  test_files: []
metadata.gz.sig CHANGED
Binary file