jsonc-merge 1.0.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.
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ # std libs
4
+ require "set"
5
+
6
+ # External gems
7
+ # TreeHaver provides a unified cross-Ruby interface to tree-sitter.
8
+ # It handles grammar discovery and backend selection automatically
9
+ # via parser_for(:jsonc). No manual registration needed.
10
+ require "tree_haver"
11
+ require "version_gem"
12
+
13
+ # Shared merge infrastructure
14
+ require "ast/merge"
15
+
16
+ # This gem
17
+ require_relative "merge/version"
18
+
19
+ # Jsonc::Merge provides a generic JSONC file smart merge system using tree-sitter AST analysis.
20
+ # It intelligently merges template and destination JSON/JSONC files by identifying matching
21
+ # keys and resolving differences using structural signatures.
22
+ #
23
+ # JSONC (JSON with Comments) support allows merging configuration files that include
24
+ # comments (like devcontainer.json, tsconfig.json, VS Code settings, etc.).
25
+ #
26
+ # @example Basic usage
27
+ # template = File.read("template.json")
28
+ # destination = File.read("destination.json")
29
+ # merger = Jsonc::Merge::SmartMerger.new(template, destination)
30
+ # result = merger.merge
31
+ #
32
+ # @example With debug information
33
+ # merger = Jsonc::Merge::SmartMerger.new(template, destination)
34
+ # debug_result = merger.merge_with_debug
35
+ # puts debug_result[:content]
36
+ # puts debug_result[:statistics]
37
+ module Jsonc
38
+ # Smart merge system for JSONC files using tree-sitter AST analysis.
39
+ # Provides intelligent merging by understanding JSON structure
40
+ # rather than treating files as plain text.
41
+ #
42
+ # @see SmartMerger Main entry point for merge operations
43
+ # @see FileAnalysis Analyzes JSON structure
44
+ # @see ConflictResolver Resolves content conflicts
45
+ module Merge
46
+ # Base error class for Jsonc::Merge
47
+ # Inherits from Ast::Merge::Error for consistency across merge gems.
48
+ class Error < Ast::Merge::Error; end
49
+
50
+ # Raised when a JSON/JSONC file has parsing errors.
51
+ # Inherits from Ast::Merge::ParseError for consistency across merge gems.
52
+ #
53
+ # @example Handling parse errors
54
+ # begin
55
+ # analysis = FileAnalysis.new(json_content)
56
+ # rescue ParseError => e
57
+ # puts "JSON syntax error: #{e.message}"
58
+ # e.errors.each { |error| puts " #{error}" }
59
+ # end
60
+ class ParseError < Ast::Merge::ParseError
61
+ # @param message [String, nil] Error message (auto-generated if nil)
62
+ # @param content [String, nil] The JSON source that failed to parse
63
+ # @param errors [Array] Parse errors from tree-sitter
64
+ def initialize(message = nil, content: nil, errors: [])
65
+ super(message, errors: errors, content: content)
66
+ end
67
+ end
68
+
69
+ # Raised when the template file has syntax errors.
70
+ #
71
+ # @example Handling template parse errors
72
+ # begin
73
+ # merger = SmartMerger.new(template, destination)
74
+ # result = merger.merge
75
+ # rescue TemplateParseError => e
76
+ # puts "Template syntax error: #{e.message}"
77
+ # e.errors.each do |error|
78
+ # puts " #{error.message}"
79
+ # end
80
+ # end
81
+ class TemplateParseError < ParseError; end
82
+
83
+ # Raised when the destination file has syntax errors.
84
+ #
85
+ # @example Handling destination parse errors
86
+ # begin
87
+ # merger = SmartMerger.new(template, destination)
88
+ # result = merger.merge
89
+ # rescue DestinationParseError => e
90
+ # puts "Destination syntax error: #{e.message}"
91
+ # e.errors.each do |error|
92
+ # puts " #{error.message}"
93
+ # end
94
+ # end
95
+ class DestinationParseError < ParseError; end
96
+
97
+ autoload :CommentTracker, "jsonc/merge/comment_tracker"
98
+ autoload :DebugLogger, "jsonc/merge/debug_logger"
99
+ autoload :Emitter, "jsonc/merge/emitter"
100
+ autoload :FreezeNode, "jsonc/merge/freeze_node"
101
+ autoload :FileAnalysis, "jsonc/merge/file_analysis"
102
+ autoload :MergeResult, "jsonc/merge/merge_result"
103
+ autoload :NodeWrapper, "jsonc/merge/node_wrapper"
104
+ autoload :ConflictResolver, "jsonc/merge/conflict_resolver"
105
+ autoload :SmartMerger, "jsonc/merge/smart_merger"
106
+ end
107
+ end
108
+
109
+ # Register with ast-merge's MergeGemRegistry for RSpec dependency tags
110
+ # Only register if MergeGemRegistry is loaded (i.e., in test environment)
111
+ if defined?(Ast::Merge::RSpec::MergeGemRegistry)
112
+ Ast::Merge::RSpec::MergeGemRegistry.register(
113
+ :jsonc_merge,
114
+ require_path: "jsonc/merge",
115
+ merger_class: "Jsonc::Merge::SmartMerger",
116
+ test_source: "// comment\n{\"key\": \"value\"}",
117
+ category: :data,
118
+ )
119
+ end
120
+
121
+ Jsonc::Merge::Version.class_eval do
122
+ extend VersionGem::Basic
123
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # For technical reasons, if we move to Zeitwerk, this cannot be require_relative.
4
+ # See: https://github.com/fxn/zeitwerk#for_gem_extension
5
+ # Hook for other libraries to load this library (e.g. via bundler)
6
+ require "jsonc/merge"
@@ -0,0 +1,259 @@
1
+ # Type signatures for json-merge gem
2
+ # Smart merge for JSON/JSONC files using tree-sitter AST analysis
3
+
4
+ module Json
5
+ module Merge
6
+ VERSION: String
7
+
8
+ # Base error class for json-merge errors
9
+ class Error < StandardError
10
+ end
11
+
12
+ # Error raised when template JSON file has syntax errors
13
+ class TemplateParseError < Error
14
+ attr_reader errors: Array[untyped]
15
+ attr_reader content: String?
16
+
17
+ def initialize: (?String? message, ?errors: Array[untyped], ?content: String?) -> void
18
+ end
19
+
20
+ # Error raised when destination JSON file has syntax errors
21
+ class DestinationParseError < Error
22
+ attr_reader errors: Array[untyped]
23
+ attr_reader content: String?
24
+
25
+ def initialize: (?String? message, ?errors: Array[untyped], ?content: String?) -> void
26
+ end
27
+
28
+ # Debug logging utility for Json::Merge
29
+ module DebugLogger
30
+ extend Ast::Merge::DebugLogger
31
+
32
+ def self.env_var_name: () -> String
33
+ def self.env_var_name=: (String name) -> String
34
+ def self.log_prefix: () -> String
35
+ def self.log_prefix=: (String prefix) -> String
36
+ def self.enabled?: () -> bool
37
+ def self.debug: (String message, ?Hash[Symbol, untyped] context) -> void
38
+ def self.info: (String message) -> void
39
+ def self.warning: (String message) -> void
40
+ def self.time: [T] (String operation) { () -> T } -> T
41
+ def self.extract_node_info: (untyped node) -> Hash[Symbol, untyped]
42
+ end
43
+
44
+ # Freeze block node for JSON/JSONC files
45
+ class FreezeNode < Ast::Merge::FreezeNode
46
+ InvalidStructureError: singleton(Ast::Merge::FreezeNode::InvalidStructureError)
47
+ Location: singleton(Ast::Merge::FreezeNode::Location)
48
+
49
+ attr_reader lines: Array[String]
50
+
51
+ def initialize: (
52
+ start_line: Integer,
53
+ end_line: Integer,
54
+ lines: Array[String],
55
+ ?start_marker: String?,
56
+ ?end_marker: String?,
57
+ ?pattern_type: Symbol
58
+ ) -> void
59
+
60
+ def signature: () -> Array[Symbol | String]
61
+ def object?: () -> bool
62
+ def array?: () -> bool
63
+ def pair?: () -> bool
64
+ def slice: () -> String?
65
+ def inspect: () -> String
66
+
67
+ private
68
+
69
+ def validate_structure!: () -> void
70
+ end
71
+
72
+ # Wrapper for tree-sitter JSON nodes
73
+ class NodeWrapper
74
+ attr_reader node: untyped
75
+ attr_reader start_line: Integer
76
+ attr_reader end_line: Integer
77
+ attr_reader analysis: FileAnalysis?
78
+
79
+ def initialize: (
80
+ untyped node,
81
+ String source,
82
+ ?analysis: FileAnalysis?
83
+ ) -> void
84
+
85
+ def location: () -> Ast::Merge::FreezeNode::Location
86
+ def signature: () -> Array[untyped]
87
+ def freeze_node?: () -> bool
88
+ def object?: () -> bool
89
+ def array?: () -> bool
90
+ def pair?: () -> bool
91
+ def key: () -> String?
92
+ def value: () -> untyped
93
+ def slice: () -> String?
94
+ def text: () -> String
95
+ def inspect: () -> String
96
+ end
97
+
98
+ # Comment tracker for JSONC files
99
+ class CommentTracker
100
+ attr_reader comments: Array[Hash[Symbol, untyped]]
101
+ attr_reader source: String
102
+ attr_reader lines: Array[String]
103
+
104
+ def initialize: (String source) -> void
105
+
106
+ def extract_comments: () -> Array[Hash[Symbol, untyped]]
107
+ def comments_for_line: (Integer line) -> Array[Hash[Symbol, untyped]]
108
+ def leading_comments_for: (Integer line) -> Array[Hash[Symbol, untyped]]
109
+ def trailing_comment_for: (Integer line) -> Hash[Symbol, untyped]?
110
+ def line_comment?: (String line) -> bool
111
+ def block_comment?: (String line) -> bool
112
+ end
113
+
114
+ # File analysis for JSON/JSONC files
115
+ class FileAnalysis
116
+ include Ast::Merge::FileAnalysisBase
117
+
118
+ DEFAULT_FREEZE_TOKEN: String
119
+ PARSER_SEARCH_PATHS: Array[String]
120
+
121
+ attr_reader source: String
122
+ attr_reader lines: Array[String]
123
+ attr_reader ast: untyped
124
+ attr_reader statements: Array[NodeWrapper | FreezeNode]
125
+ attr_reader freeze_blocks: Array[FreezeNode]
126
+ attr_reader freeze_token: String
127
+ attr_reader signature_generator: (^(untyped) -> Array[untyped]?)?
128
+ attr_reader comment_tracker: CommentTracker
129
+ attr_reader errors: Array[untyped]
130
+
131
+ def self.find_parser_path: () -> String?
132
+
133
+ def initialize: (
134
+ String source,
135
+ ?freeze_token: String,
136
+ ?signature_generator: (^(untyped) -> Array[untyped]?)?,
137
+ ?parser_path: String?
138
+ ) -> void
139
+
140
+ def valid?: () -> bool
141
+ def nodes: () -> Array[NodeWrapper | FreezeNode]
142
+ def line_at: (Integer line_num) -> String?
143
+ def normalized_line: (Integer line_num) -> String?
144
+ def in_freeze_block?: (Integer line_num) -> bool
145
+ def freeze_block_at: (Integer line_num) -> FreezeNode?
146
+ def signature_at: (Integer index) -> Array[untyped]?
147
+ def generate_signature: (untyped node) -> Array[untyped]?
148
+ def compute_node_signature: (untyped node) -> Array[untyped]?
149
+
150
+ private
151
+
152
+ def parse_json: () -> void
153
+ def extract_nodes: (untyped tree_node) -> Array[NodeWrapper]
154
+ def extract_freeze_blocks: () -> Array[FreezeNode]
155
+ def integrate_nodes_and_freeze_blocks: () -> Array[NodeWrapper | FreezeNode]
156
+ end
157
+
158
+ # Result of a JSON merge operation
159
+ class MergeResult < Ast::Merge::MergeResult
160
+ DECISION_KEPT_TEMPLATE: Symbol
161
+ DECISION_KEPT_DEST: Symbol
162
+ DECISION_MERGED: Symbol
163
+ DECISION_ADDED: Symbol
164
+ DECISION_FREEZE_BLOCK: Symbol
165
+
166
+ attr_reader lines: Array[Hash[Symbol, untyped]]
167
+ attr_reader decisions: Array[Hash[Symbol, untyped]]
168
+ attr_reader statistics: Hash[Symbol, Integer]
169
+
170
+ def initialize: (
171
+ ?template_analysis: FileAnalysis?,
172
+ ?dest_analysis: FileAnalysis?,
173
+ ?conflicts: Array[Hash[Symbol, untyped]],
174
+ ?frozen_blocks: Array[FreezeNode],
175
+ ?stats: Hash[Symbol, untyped]
176
+ ) -> void
177
+
178
+ def add_line: (
179
+ String line,
180
+ decision: Symbol,
181
+ source: Symbol,
182
+ ?original_line: Integer?
183
+ ) -> void
184
+
185
+ def add_lines: (
186
+ Array[String] lines,
187
+ decision: Symbol,
188
+ source: Symbol,
189
+ ?start_line: Integer?
190
+ ) -> void
191
+
192
+ def add_blank_line: (?decision: Symbol, ?source: Symbol) -> void
193
+ def add_freeze_block: (FreezeNode freeze_node) -> void
194
+ def to_json: () -> String
195
+ def content: () -> Array[Hash[Symbol, untyped]]
196
+ def content_string: () -> String
197
+ def empty?: () -> bool
198
+
199
+ private
200
+
201
+ def track_statistics: (Symbol decision, Symbol source) -> void
202
+ end
203
+
204
+ # Smart merger for JSON/JSONC files
205
+ class SmartMerger
206
+ include Ast::Merge::MergerConfig
207
+
208
+ attr_reader template_analysis: FileAnalysis
209
+ attr_reader dest_analysis: FileAnalysis
210
+ attr_reader signature_match_preference: (Symbol | Hash[Symbol, Symbol])
211
+ attr_reader add_template_only_nodes: bool
212
+
213
+ def initialize: (
214
+ String template_content,
215
+ String dest_content,
216
+ ?signature_match_preference: (Symbol | Hash[Symbol, Symbol]),
217
+ ?add_template_only_nodes: bool,
218
+ ?freeze_token: String,
219
+ ?signature_generator: (^(untyped) -> Array[untyped]?)?,
220
+ ?node_splitter: Hash[Symbol, untyped]?,
221
+ ?parser_path: String?
222
+ ) -> void
223
+
224
+ def merge: () -> MergeResult
225
+
226
+ private
227
+
228
+ def perform_merge: () -> MergeResult
229
+ def merge_nodes: (MergeResult result) -> void
230
+ end
231
+
232
+ # Conflict resolver for JSON merges
233
+ class ConflictResolver
234
+ attr_reader template_analysis: FileAnalysis
235
+ attr_reader dest_analysis: FileAnalysis
236
+ attr_reader signature_match_preference: (Symbol | Hash[Symbol, Symbol])
237
+ attr_reader add_template_only_nodes: bool
238
+
239
+ def initialize: (
240
+ FileAnalysis template_analysis,
241
+ FileAnalysis dest_analysis,
242
+ ?signature_match_preference: (Symbol | Hash[Symbol, Symbol]),
243
+ ?add_template_only_nodes: bool
244
+ ) -> void
245
+
246
+ def resolve: (untyped boundary, MergeResult result) -> void
247
+ end
248
+
249
+ # Emitter for reconstructing JSON output
250
+ class Emitter
251
+ attr_reader result: MergeResult
252
+ attr_reader indent: Integer
253
+
254
+ def initialize: (MergeResult result, ?indent: Integer) -> void
255
+
256
+ def emit: () -> String
257
+ end
258
+ end
259
+ end
data.tar.gz.sig ADDED
@@ -0,0 +1 @@
1
+ ��~���bV(4F���]'�4LKa�U bI1��Z��P���m�d���9�� B�n%�U!Ը�oL&=ivH5E92��jZ��g������c �>���/�c��~S2�T