psych-pure 0.1.3 → 0.2.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: 62e40e29dab03fe116feadefc2a62967ec840deb52bee18bff89ae1318bf4d02
4
- data.tar.gz: 3f5612a277682bbef3a698e03cc3bde6e9da02dc3f3024d9978aff34af8a624c
3
+ metadata.gz: 3b495922e35ffc07633da357273f8df93cc949fff0582eb5d6bf6d05c1e3ea19
4
+ data.tar.gz: 4a42f95af624a0712a49ea6dddd3ab5d0e382f65fb48a4d5a6e8fcbdf0195f39
5
5
  SHA512:
6
- metadata.gz: 5be228176e34e4e908e1ea6af1cebbf41b59fa311de3e4a5abec21aefd9e6028f87473714b9e2c0ccf554bec1a379986b9323744c94ebdf9144cdc08d9116991
7
- data.tar.gz: 4ba02fec03109e76b40883918d1d46125bdd8c1afb9d486a5af2decb868ebc6c967229dda3758e6281b5df709a89b2e6f704d747bad7f307d748861aba580b98
6
+ metadata.gz: fd5c511ad9ba206d9d7f012a80cb0924c07f3b9b0b0e860d296404ec7b4a39a5bc5cd1391ae207e213ba4b19d197dc341550bab944a4c384164e30021fdde9d0
7
+ data.tar.gz: 92bbc9e614dbd526792c45dca5cb1e5b38e463f2def07e47bc8b419f97236dce6823ef6b2303036a8db59c755e463e34a47254052b36688a3eb9824062043830
data/CHANGELOG.md CHANGED
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2025-11-11
10
+
11
+ - Add `sequence_indent` option to `Psych::Pure.dump` to control whether sequence elements contained within mapping elements are indented.
12
+ - Properly handle mutation methods on loaded objects that mutate in place.
13
+
14
+ ## [0.1.4] - 2025-11-10
15
+
16
+ - Fix up comment handling preceding sequence elements.
17
+ - Properly update hashes in mutation methods that are loaded through `Psych::Pure.load`.
18
+ - Properly raise syntax errors when the parser does not finish.
19
+
9
20
  ## [0.1.3] - 2025-10-24
10
21
 
11
22
  - Fix up roundtripping when using `<<` inside mappings.
@@ -31,7 +42,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
31
42
 
32
43
  - 🎉 Initial release. 🎉
33
44
 
34
- [unreleased]: https://github.com/kddnewton/psych-pure/compare/v0.1.3...HEAD
45
+ [unreleased]: https://github.com/kddnewton/psych-pure/compare/v0.2.0...HEAD
46
+ [0.2.0]: https://github.com/kddnewton/psych-pure/compare/v0.1.4...v0.2.0
47
+ [0.1.4]: https://github.com/kddnewton/psych-pure/compare/v0.1.3...v0.1.4
35
48
  [0.1.3]: https://github.com/kddnewton/psych-pure/compare/v0.1.2...v0.1.3
36
49
  [0.1.2]: https://github.com/kddnewton/psych-pure/compare/v0.1.1...v0.1.2
37
50
  [0.1.1]: https://github.com/kddnewton/psych-pure/compare/v0.1.0...v0.1.1
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Psych
4
4
  module Pure
5
- VERSION = "0.1.3"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
data/lib/psych/pure.rb CHANGED
@@ -176,6 +176,20 @@ module Psych
176
176
  def trailing_comment(comment)
177
177
  @trailing << comment
178
178
  end
179
+
180
+ # Execute the given block without the leading comments being visible. This
181
+ # is used when a node has already handled its child nodes' leading
182
+ # comments, so they should not be processed again.
183
+ def without_leading
184
+ leading = @leading
185
+
186
+ begin
187
+ @leading = []
188
+ yield
189
+ ensure
190
+ @leading = leading
191
+ end
192
+ end
179
193
  end
180
194
 
181
195
  # Wraps a Ruby object with its node from the source input.
@@ -187,15 +201,37 @@ module Psych
187
201
  # rely on the source formatting, and need to format it ourselves.
188
202
  attr_reader :dirty
189
203
 
190
- def initialize(object, psych_node, dirty = false)
204
+ def initialize(object, psych_node)
191
205
  super(object)
192
206
  @psych_node = psych_node
193
- @dirty = dirty
207
+ @dirty = false
194
208
  end
195
209
 
196
- def replace(psych_node)
197
- @psych_node = psych_node
198
- @dirty = true
210
+ def initialize_clone(obj, freeze: nil)
211
+ super
212
+ @psych_node = obj.psych_node.dup
213
+ end
214
+
215
+ def initialize_dup(obj)
216
+ super
217
+ @psych_node = obj.psych_node.dup
218
+ end
219
+
220
+ # Effectively implement the same method_missing as SimpleDelegator, but
221
+ # additionally track whether or not the object has been mutated.
222
+ ruby2_keywords def method_missing(name, *args, &block)
223
+ takes_block = false
224
+ target = self.__getobj__ { takes_block = true }
225
+
226
+ if !takes_block && target_respond_to?(target, name, false)
227
+ previous = target.dup
228
+ result = target.__send__(name, *args, &block)
229
+
230
+ @dirty = true unless previous.eql?(target)
231
+ result
232
+ else
233
+ super(name, *args, &block)
234
+ end
199
235
  end
200
236
  end
201
237
 
@@ -209,7 +245,11 @@ module Psych
209
245
  @value_node = value_node
210
246
  end
211
247
 
212
- def replace(value_node)
248
+ def replace_key(key_node)
249
+ @key_node = key_node
250
+ end
251
+
252
+ def replace_value(value_node)
213
253
  @value_node = value_node
214
254
  end
215
255
  end
@@ -217,49 +257,245 @@ module Psych
217
257
  # The node associated with the hash.
218
258
  attr_reader :psych_node
219
259
 
260
+ # The list of key/value pairs within the hash.
261
+ attr_reader :psych_keys
262
+
220
263
  def initialize(object, psych_node)
221
264
  super(object)
222
265
  @psych_node = psych_node
223
266
  @psych_keys = []
224
267
  end
225
268
 
226
- def psych_keys
227
- @psych_keys.map do |psych_key|
228
- [psych_key.key_node, psych_key.value_node]
229
- end
269
+ def set!(key_node, value_node)
270
+ @psych_keys << PsychKey.new(key_node, value_node)
271
+ __getobj__[key_node.__getobj__] = value_node
272
+ end
273
+
274
+ def join!(key_node, value_node)
275
+ @psych_keys << PsychKey.new(key_node, value_node)
276
+ merge!(value_node)
277
+ end
278
+
279
+ def psych_assocs
280
+ @psych_keys.map { |psych_key| [psych_key.key_node, psych_key.value_node] }
281
+ end
282
+
283
+ # Override Hash mutation methods to keep @psych_keys in sync.
284
+
285
+ def initialize_clone(obj, freeze: nil)
286
+ super
287
+ @psych_keys = obj.psych_keys.map(&:dup)
288
+ end
289
+
290
+ def initialize_dup(obj)
291
+ super
292
+ @psych_keys = obj.psych_keys.map(&:dup)
230
293
  end
231
294
 
232
295
  def []=(key, value)
233
- if begin
234
- @psych_keys.none? do |psych_key|
235
- key_node = psych_key.key_node
236
- key_node_inner =
237
- if key_node.is_a?(LoadedHash) || key_node.is_a?(LoadedObject)
238
- key_node.__getobj__
239
- else
240
- key_node
241
- end
296
+ super(key, value)
242
297
 
243
- if key_node_inner.eql?(key)
244
- psych_key.replace(value)
245
- true
298
+ if (psych_key = @psych_keys.reverse_each.find { |psych_key| psych_compare?(psych_key.key_node, key) })
299
+ psych_key.replace_value(value)
300
+ else
301
+ @psych_keys << PsychKey.new(key, value)
302
+ end
303
+
304
+ value
305
+ end
306
+
307
+ alias store []=
308
+
309
+ def clear
310
+ super
311
+ @psych_keys.clear
312
+
313
+ self
314
+ end
315
+
316
+ def compact!
317
+ mutated = false
318
+ @psych_keys.each do |psych_key|
319
+ if psych_unwrap(psych_key.value_node).nil?
320
+ mutated = true
321
+ delete(psych_unwrap(psych_key.key_node))
322
+ end
323
+ end
324
+
325
+ self if mutated
326
+ end
327
+
328
+ def compact
329
+ dup.compact!
330
+ end
331
+
332
+ def delete(key)
333
+ result = super
334
+ psych_delete(key)
335
+
336
+ result
337
+ end
338
+
339
+ def delete_if(&block)
340
+ super do |key, value|
341
+ yield(key, psych_unwrap(value)).tap do |result|
342
+ psych_delete(key) if result
343
+ end
344
+ end
345
+
346
+ self
347
+ end
348
+
349
+ def except(*keys)
350
+ dup.delete_if { |key, _| keys.include?(key) }
351
+ end
352
+
353
+ def filter!(&block)
354
+ mutated = false
355
+ super do |key, value|
356
+ yield(key, psych_unwrap(value)).tap do |result|
357
+ unless result
358
+ psych_delete(key)
359
+ mutated = true
246
360
  end
247
361
  end
248
- end then
249
- @psych_keys << PsychKey.new(key, value)
250
362
  end
251
363
 
252
- super(key, value)
364
+ self if mutated
253
365
  end
254
366
 
255
- def set!(key_node, value_node)
256
- @psych_keys << PsychKey.new(key_node, value_node)
257
- __getobj__[key_node.__getobj__] = value_node
367
+ def filter(&block)
368
+ dup.filter!(&block)
258
369
  end
259
370
 
260
- def join!(key_node, value_node)
261
- @psych_keys << PsychKey.new(key_node, value_node)
262
- merge!(value_node)
371
+ def invert
372
+ result = LoadedHash.new({}, @psych_node)
373
+ each { |key, value| result[psych_unwrap(value)] = key }
374
+ result
375
+ end
376
+
377
+ def keep_if(&block)
378
+ super do |key, value|
379
+ yield(key, psych_unwrap(value)).tap do |result|
380
+ psych_delete(key) unless result
381
+ end
382
+ end
383
+
384
+ self
385
+ end
386
+
387
+ alias select! keep_if
388
+
389
+ def merge!(*others)
390
+ super
391
+ others.each do |other|
392
+ other.each do |key, value|
393
+ psych_delete(key)
394
+ @psych_keys << PsychKey.new(key, value)
395
+ end
396
+ end
397
+
398
+ self
399
+ end
400
+
401
+ alias update merge!
402
+
403
+ def merge(*others)
404
+ dup.merge!(*others)
405
+ end
406
+
407
+ def reject!(&block)
408
+ mutated = false
409
+ super do |key, value|
410
+ yield(key, psych_unwrap(value)).tap do |result|
411
+ if result
412
+ psych_delete(key)
413
+ mutated = true
414
+ end
415
+ end
416
+ end
417
+
418
+ self if mutated
419
+ end
420
+
421
+ def reject(&block)
422
+ dup.reject!(&block)
423
+ end
424
+
425
+ def replace(other)
426
+ super
427
+
428
+ @psych_keys.clear
429
+ other.each { |key, value| @psych_keys << PsychKey.new(key, value) }
430
+
431
+ self
432
+ end
433
+
434
+ def shift
435
+ unless empty?
436
+ key, value = super
437
+ psych_delete(key)
438
+
439
+ [key, value]
440
+ end
441
+ end
442
+
443
+ def slice(*keys)
444
+ dup.select! { |key, _| keys.include?(key) }
445
+ end
446
+
447
+ def transform_keys!(&block)
448
+ super do |key|
449
+ yield(key).tap do |result|
450
+ @psych_keys
451
+ .reverse_each
452
+ .find { |psych_key| psych_compare?(psych_key.key_node, key) }
453
+ &.replace_key(result)
454
+ end
455
+ end
456
+
457
+ self
458
+ end
459
+
460
+ def transform_keys(&block)
461
+ dup.transform_keys!(&block)
462
+ end
463
+
464
+ def transform_values!(&block)
465
+ super do |value|
466
+ yield(psych_unwrap(value)).tap do |result|
467
+ @psych_keys
468
+ .reverse_each
469
+ .find { |psych_key| psych_compare?(psych_key.value_node, value) }
470
+ &.replace_value(result)
471
+ end
472
+ end
473
+ end
474
+
475
+ def transform_values(&block)
476
+ dup.transform_values!(&block)
477
+ end
478
+
479
+ private
480
+
481
+ def psych_compare?(psych_node, value)
482
+ if compare_by_identity?
483
+ psych_unwrap(psych_node).equal?(value)
484
+ else
485
+ psych_unwrap(psych_node).eql?(value)
486
+ end
487
+ end
488
+
489
+ def psych_delete(key)
490
+ @psych_keys.reject! { |psych_key| psych_compare?(psych_key.key_node, key) }
491
+ end
492
+
493
+ def psych_unwrap(node)
494
+ if node.is_a?(LoadedHash) || node.is_a?(LoadedObject)
495
+ node.__getobj__
496
+ else
497
+ node
498
+ end
263
499
  end
264
500
  end
265
501
 
@@ -762,9 +998,7 @@ module Psych
762
998
  @source = Source.new(yaml)
763
999
  @comments = {} if comments
764
1000
 
765
- raise_syntax_error("Parser failed to complete") unless parse_l_yaml_stream
766
- raise_syntax_error("Parser finished before end of input") unless @scanner.eos?
767
-
1001
+ parse_l_yaml_stream
768
1002
  @comments = nil if comments
769
1003
  true
770
1004
  end
@@ -1261,11 +1495,8 @@ module Psych
1261
1495
  # b-comment
1262
1496
  def parse_s_b_comment
1263
1497
  try do
1264
- try do
1265
- if parse_s_separate_in_line
1266
- parse_c_nb_comment_text(true)
1267
- true
1268
- end
1498
+ if parse_s_separate_in_line
1499
+ parse_c_nb_comment_text(true)
1269
1500
  end
1270
1501
 
1271
1502
  parse_b_comment
@@ -2064,12 +2295,9 @@ module Psych
2064
2295
  if parse_ns_flow_seq_entry(n, c)
2065
2296
  parse_s_separate(n, c)
2066
2297
 
2067
- try do
2068
- if match(",")
2069
- parse_s_separate(n, c)
2070
- parse_ns_s_flow_seq_entries(n, c)
2071
- true
2072
- end
2298
+ if match(",")
2299
+ parse_s_separate(n, c)
2300
+ parse_ns_s_flow_seq_entries(n, c)
2073
2301
  end
2074
2302
 
2075
2303
  true
@@ -3195,34 +3423,32 @@ module Psych
3195
3423
  def parse_l_yaml_stream
3196
3424
  events_push_flush_properties(StreamStart.new(Location.point(@source, @scanner.pos)))
3197
3425
 
3198
- if try {
3199
- if parse_l_document_prefix
3200
- @document_start_event = DocumentStart.new(Location.point(@source, @scanner.pos))
3201
- @tag_directives = @document_start_event.tag_directives
3202
- @document_end_event = nil
3426
+ star { parse_l_document_prefix }
3427
+ @document_start_event = DocumentStart.new(Location.point(@source, @scanner.pos))
3428
+ @tag_directives = @document_start_event.tag_directives
3429
+ @document_end_event = nil
3430
+ parse_l_any_document
3203
3431
 
3204
- parse_l_any_document
3205
- star do
3206
- try do
3207
- if parse_l_document_suffix
3208
- star { parse_l_document_prefix }
3209
- parse_l_any_document
3210
- true
3211
- end
3212
- end ||
3213
- try do
3214
- if parse_l_document_prefix
3215
- parse_l_explicit_document
3216
- true
3217
- end
3218
- end
3432
+ star do
3433
+ try do
3434
+ if parse_l_document_suffix
3435
+ star { parse_l_document_prefix }
3436
+ parse_l_any_document
3437
+ true
3438
+ end
3439
+ end ||
3440
+ try do
3441
+ if parse_l_document_prefix
3442
+ parse_l_explicit_document
3443
+ true
3219
3444
  end
3220
3445
  end
3221
- } then
3222
- document_end_event_flush
3223
- events_push_flush_properties(StreamEnd.new(Location.point(@source, @scanner.pos)))
3224
- true
3225
3446
  end
3447
+
3448
+ raise_syntax_error("Parser finished before end of input") unless @scanner.eos?
3449
+ document_end_event_flush
3450
+ events_push_flush_properties(StreamEnd.new(Location.point(@source, @scanner.pos)))
3451
+ true
3226
3452
  end
3227
3453
 
3228
3454
  # ------------------------------------------------------------------------
@@ -3298,6 +3524,9 @@ module Psych
3298
3524
 
3299
3525
  # Represents the nil value.
3300
3526
  class NilNode < Node
3527
+ def accept(visitor)
3528
+ raise "Visiting NilNode is not supported"
3529
+ end
3301
3530
  end
3302
3531
 
3303
3532
  # Represents a generic object that is not matched by any of the other node
@@ -3352,8 +3581,9 @@ module Psych
3352
3581
  # The visitor is responsible for walking the tree and generating the YAML
3353
3582
  # output.
3354
3583
  class Visitor
3355
- def initialize(q)
3584
+ def initialize(q, sequence_indent: false)
3356
3585
  @q = q
3586
+ @sequence_indent = sequence_indent
3357
3587
  end
3358
3588
 
3359
3589
  # Visit an AliasNode.
@@ -3468,12 +3698,38 @@ module Psych
3468
3698
  @q.breakable
3469
3699
  end
3470
3700
 
3471
- @q.seplist(contents, -> { @q.breakable }) do |element|
3701
+ current_line = nil
3702
+ contents.each_with_index do |element, index|
3703
+ psych_node = element.psych_node
3704
+ leading = psych_node&.comments&.leading
3705
+
3706
+ if index > 0
3707
+ @q.breakable
3708
+
3709
+ if current_line && psych_node
3710
+ start_line = (leading&.first || psych_node).start_line
3711
+ @q.breakable if start_line - current_line >= 2
3712
+ end
3713
+ end
3714
+
3715
+ current_line = psych_node&.end_line
3716
+ visit_leading_comments(leading) if leading&.any?
3717
+
3718
+ if psych_node && (trailing = psych_node.comments.trailing).any?
3719
+ current_line = trailing.last.end_line
3720
+ end
3721
+
3472
3722
  @q.text("-")
3473
3723
  next if element.is_a?(NilNode)
3474
3724
 
3475
3725
  @q.text(" ")
3476
- @q.nest(2) { visit(element) }
3726
+ @q.nest(2) do
3727
+ if psych_node
3728
+ psych_node.comments.without_leading { visit(element) }
3729
+ else
3730
+ visit(element)
3731
+ end
3732
+ end
3477
3733
  end
3478
3734
 
3479
3735
  @q.current_group.break
@@ -3544,6 +3800,11 @@ module Psych
3544
3800
  elsif inlined || value.anchor || value.tag || value.value.empty?
3545
3801
  @q.text(" ")
3546
3802
  @q.nest(2) { visit(value) }
3803
+ elsif @sequence_indent
3804
+ @q.nest(2) do
3805
+ @q.breakable
3806
+ visit(value)
3807
+ end
3547
3808
  else
3548
3809
  @q.breakable
3549
3810
  visit(value)
@@ -3629,23 +3890,29 @@ module Psych
3629
3890
  end
3630
3891
  end
3631
3892
 
3893
+ # Visit the leading comments for a node, printing them out with proper
3894
+ # line breaks.
3895
+ def visit_leading_comments(comments)
3896
+ line = nil
3897
+
3898
+ comments.each do |comment|
3899
+ while line && line < comment.start_line
3900
+ @q.breakable
3901
+ line += 1
3902
+ end
3903
+
3904
+ @q.text(comment.value)
3905
+ line = comment.end_line
3906
+ end
3907
+
3908
+ @q.breakable
3909
+ end
3910
+
3632
3911
  # Print out the leading and trailing comments of a node, as well as
3633
3912
  # yielding the value of the node to the block.
3634
3913
  def with_comments(node)
3635
3914
  if (comments = node.psych_node&.comments) && (leading = comments.leading).any?
3636
- line = nil
3637
-
3638
- leading.each do |comment|
3639
- while line && line < comment.start_line
3640
- @q.breakable
3641
- line += 1
3642
- end
3643
-
3644
- @q.text(comment.value)
3645
- line = comment.end_line
3646
- end
3647
-
3648
- @q.breakable
3915
+ visit_leading_comments(leading)
3649
3916
  end
3650
3917
 
3651
3918
  yield node.value
@@ -3767,7 +4034,7 @@ module Psych
3767
4034
  q.text(" ")
3768
4035
  end
3769
4036
 
3770
- node.accept(Visitor.new(q))
4037
+ node.accept(Visitor.new(q, sequence_indent: @options.fetch(:sequence_indent, false)))
3771
4038
  q.breakable
3772
4039
  q.current_group.break
3773
4040
  q.flush
@@ -3835,7 +4102,7 @@ module Psych
3835
4102
  when Hash
3836
4103
  contents =
3837
4104
  if base_object.is_a?(LoadedHash)
3838
- base_object.psych_keys.flat_map { |(key, value)| [dump(key), dump(value)] }
4105
+ base_object.psych_assocs.flat_map { |(key, value)| [dump(key), dump(value)] }
3839
4106
  else
3840
4107
  object.flat_map { |key, value| [dump(key), dump(value)] }
3841
4108
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: psych-pure
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Newton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-24 00:00:00.000000000 Z
11
+ date: 2025-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: psych
@@ -113,7 +113,7 @@ licenses:
113
113
  - MIT
114
114
  metadata:
115
115
  bug_tracker_uri: https://github.com/kddnewton/psych-pure/issues
116
- changelog_uri: https://github.com/kddnewton/psych-pure/blob/v0.1.3/CHANGELOG.md
116
+ changelog_uri: https://github.com/kddnewton/psych-pure/blob/v0.2.0/CHANGELOG.md
117
117
  source_code_uri: https://github.com/kddnewton/psych-pure
118
118
  rubygems_mfa_required: 'true'
119
119
  post_install_message: