psych-pure 0.2.0 → 0.3.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: 3b495922e35ffc07633da357273f8df93cc949fff0582eb5d6bf6d05c1e3ea19
4
- data.tar.gz: 4a42f95af624a0712a49ea6dddd3ab5d0e382f65fb48a4d5a6e8fcbdf0195f39
3
+ metadata.gz: 33f682a9e0bba7ec024cec2e1ca54aa791aa550e066ac708d7b9cf4e765e6d5a
4
+ data.tar.gz: 5a0b04e41b5700c481658a9604be9f942f41757231f339e8382d53f2a383b431
5
5
  SHA512:
6
- metadata.gz: fd5c511ad9ba206d9d7f012a80cb0924c07f3b9b0b0e860d296404ec7b4a39a5bc5cd1391ae207e213ba4b19d197dc341550bab944a4c384164e30021fdde9d0
7
- data.tar.gz: 92bbc9e614dbd526792c45dca5cb1e5b38e463f2def07e47bc8b419f97236dce6823ef6b2303036a8db59c755e463e34a47254052b36688a3eb9824062043830
6
+ metadata.gz: 164cb1c175e35e791e0206d14cfef2233817e90d2a6ebabb8f5b88ba59cad9c6fd946d651a13c7aab2437fa33e2e1a23a8d775597958e8d06d42ea21bcd6a1f5
7
+ data.tar.gz: dcab2936344a40ddd58495e6a699aebc77c0d5900c53f0692009b5e4b7383e09ae4896c9c76fa2d6b67599da72a313772613ec9440f659558a51d6598f9e4aca
data/CHANGELOG.md CHANGED
@@ -6,6 +6,10 @@ 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.3.0] - 2025-12-25
10
+
11
+ - Support Psych >= 5.3.0 by adding the `parse_symbols` option.
12
+
9
13
  ## [0.2.0] - 2025-11-11
10
14
 
11
15
  - Add `sequence_indent` option to `Psych::Pure.dump` to control whether sequence elements contained within mapping elements are indented.
@@ -42,7 +46,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
42
46
 
43
47
  - 🎉 Initial release. 🎉
44
48
 
45
- [unreleased]: https://github.com/kddnewton/psych-pure/compare/v0.2.0...HEAD
49
+ [unreleased]: https://github.com/kddnewton/psych-pure/compare/v0.3.0...HEAD
50
+ [0.3.0]: https://github.com/kddnewton/psych-pure/compare/v0.2.0...v0.3.0
46
51
  [0.2.0]: https://github.com/kddnewton/psych-pure/compare/v0.1.4...v0.2.0
47
52
  [0.1.4]: https://github.com/kddnewton/psych-pure/compare/v0.1.3...v0.1.4
48
53
  [0.1.3]: https://github.com/kddnewton/psych-pure/compare/v0.1.2...v0.1.3
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Psych
4
4
  module Pure
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/psych/pure.rb CHANGED
@@ -15,6 +15,17 @@ module Psych
15
15
  end
16
16
  end
17
17
 
18
+ # If Psych is older than 5.3, we need to modify the ScalarScanner to add the
19
+ # parse_symbols parameter so that we can use it consistently when we
20
+ # initialize it.
21
+ if Psych::VERSION < "5.3"
22
+ ScalarScanner.prepend(Module.new {
23
+ def initialize(class_loader, strict_integer: false, parse_symbols: true)
24
+ super(class_loader, strict_integer: strict_integer)
25
+ end
26
+ })
27
+ end
28
+
18
29
  # A YAML parser written in Ruby.
19
30
  module Pure
20
31
  # An internal exception is an exception that should not have occurred. It is
@@ -74,6 +85,10 @@ module Psych
74
85
  def column(offset)
75
86
  offset - @line_offsets[line(offset)]
76
87
  end
88
+
89
+ def point(offset)
90
+ "line #{line(offset) + 1} column #{column(offset)}"
91
+ end
77
92
  end
78
93
 
79
94
  # A location represents a range of bytes in the input string.
@@ -645,8 +660,8 @@ module Psych
645
660
  defined?(@comments)
646
661
  end
647
662
 
648
- def to_ruby(symbolize_names: false, freeze: false, strict_integer: false, comments: false)
649
- Visitors::ToRuby.create(symbolize_names: symbolize_names, freeze: freeze, strict_integer: strict_integer, comments: comments).accept(self)
663
+ def to_ruby(symbolize_names: false, freeze: false, strict_integer: false, parse_symbols: true, comments: false)
664
+ Visitors::ToRuby.create(symbolize_names: symbolize_names, freeze: freeze, strict_integer: strict_integer, parse_symbols: parse_symbols, comments: comments).accept(self)
650
665
  end
651
666
  end
652
667
 
@@ -732,9 +747,9 @@ module Psych
732
747
 
733
748
  # Extend the ToRuby singleton to be able to pass the comments option.
734
749
  module ToRubySingleton
735
- def create(symbolize_names: false, freeze: false, strict_integer: false, comments: false)
750
+ def create(symbolize_names: false, freeze: false, strict_integer: false, parse_symbols: true, comments: false)
736
751
  class_loader = ClassLoader.new
737
- scanner = ScalarScanner.new(class_loader, strict_integer: strict_integer)
752
+ scanner = ScalarScanner.new(class_loader, strict_integer: strict_integer, parse_symbols: parse_symbols)
738
753
  new(scanner, class_loader, symbolize_names: symbolize_names, freeze: freeze, comments: comments)
739
754
  end
740
755
  end
@@ -931,6 +946,123 @@ module Psych
931
946
  # The parser is responsible for taking a YAML string and converting it into
932
947
  # a series of events that can be used by the consumer.
933
948
  class Parser
949
+ # A stack of contexts that the parser is currently within. We use this to
950
+ # decorate error messages with the context in which they occurred.
951
+ class Context
952
+ class BlockMapping
953
+ attr_reader :pos, :indent
954
+
955
+ def initialize(pos, indent)
956
+ @pos = pos
957
+ @indent = indent
958
+ end
959
+
960
+ def format(source)
961
+ "block mapping at #{source.point(pos)}#{indent == -1 ? "" : " (indent=#{indent})"}"
962
+ end
963
+ end
964
+
965
+ class BlockSequence
966
+ attr_reader :pos, :indent
967
+
968
+ def initialize(pos, indent)
969
+ @pos = pos
970
+ @indent = indent
971
+ end
972
+
973
+ def format(source)
974
+ "block sequence at #{source.point(pos)}#{indent == -1 ? "" : " (indent=#{indent})"}"
975
+ end
976
+ end
977
+
978
+ class DoubleQuotedScalar
979
+ attr_reader :pos
980
+
981
+ def initialize(pos)
982
+ @pos = pos
983
+ end
984
+
985
+ def format(source)
986
+ "double quoted scalar at #{source.point(pos)}"
987
+ end
988
+ end
989
+
990
+ class FlowMapping
991
+ attr_reader :pos, :context
992
+
993
+ def initialize(pos, context)
994
+ @pos = pos
995
+ @context = context
996
+ end
997
+
998
+ def format(source)
999
+ "flow mapping at #{source.point(pos)} (context=#{context})"
1000
+ end
1001
+ end
1002
+
1003
+ class FlowSequence
1004
+ attr_reader :pos, :context
1005
+
1006
+ def initialize(pos, context)
1007
+ @pos = pos
1008
+ @context = context
1009
+ end
1010
+
1011
+ def format(source)
1012
+ "flow sequence at #{source.point(pos)} (context=#{context})"
1013
+ end
1014
+ end
1015
+
1016
+ private_constant :BlockMapping, :BlockSequence, :DoubleQuotedScalar, :FlowMapping, :FlowSequence
1017
+
1018
+ def initialize
1019
+ @contexts = []
1020
+ @deepest = []
1021
+ end
1022
+
1023
+ def syntax_error(source, filename, pos, message)
1024
+ unless (stack = (@contexts.empty? ? @deepest : @contexts)).empty?
1025
+ pos = stack.last.pos
1026
+ message = "#{message}\nwithin:\n#{stack.map { |element| " #{element.format(source)}\n" }.join}"
1027
+ end
1028
+
1029
+ SyntaxError.new(filename, source.line(pos), source.column(pos), pos, message, nil)
1030
+ end
1031
+
1032
+ def within_block_mapping(pos, indent, &blk)
1033
+ within(BlockMapping.new(pos, indent), &blk)
1034
+ end
1035
+
1036
+ def within_block_sequence(pos, indent, &blk)
1037
+ within(BlockSequence.new(pos, indent), &blk)
1038
+ end
1039
+
1040
+ def within_double_quoted_scalar(pos, &blk)
1041
+ within(DoubleQuotedScalar.new(pos), &blk)
1042
+ end
1043
+
1044
+ def within_flow_mapping(pos, context, &blk)
1045
+ within(FlowMapping.new(pos, context), &blk)
1046
+ end
1047
+
1048
+ def within_flow_sequence(pos, context, &blk)
1049
+ within(FlowSequence.new(pos, context), &blk)
1050
+ end
1051
+
1052
+ private
1053
+
1054
+ def within(object)
1055
+ @contexts.push(object)
1056
+ @deepest = @contexts.dup if @contexts.length > @deepest.length
1057
+
1058
+ begin
1059
+ yield
1060
+ ensure
1061
+ @contexts.pop
1062
+ end
1063
+ end
1064
+ end
1065
+
934
1066
  # Initialize a new parser with the given source string.
935
1067
  def initialize(handler)
936
1068
  # These are used to track the current state of the parser.
@@ -974,6 +1106,11 @@ module Psych
974
1106
  # This parser can optionally parse comments and attach them to the
975
1107
  # resulting tree, if the option is passed.
976
1108
  @comments = nil
1109
+
1110
+ # The context of the parser at any given time, which is used to decorate
1111
+ # error messages to make it easier to find the specific location where
1112
+ # they occurred.
1113
+ @context = Context.new
977
1114
  end
978
1115
 
979
1116
  # Top-level parse function that starts the parsing process.
@@ -1007,9 +1144,7 @@ module Psych
1007
1144
 
1008
1145
  # Raise a syntax error with the given message.
1009
1146
  def raise_syntax_error(message)
1010
- line = @source.line(@scanner.pos)
1011
- column = @source.column(@scanner.pos)
1012
- raise SyntaxError.new(@filename, line, column, @scanner.pos, message, nil)
1147
+ raise @context.syntax_error(@source, @filename, @scanner.pos, message)
1013
1148
  end
1014
1149
 
1015
1150
  # ------------------------------------------------------------------------
@@ -1878,30 +2013,32 @@ module Psych
1878
2013
  def parse_c_double_quoted(n, c)
1879
2014
  pos_start = @scanner.pos
1880
2015
 
1881
- if try { match("\"") && parse_nb_double_text(n, c) && match("\"") }
1882
- end1 = "(?:\\\\\\r?\\n[ \\t]*)"
1883
- end2 = "(?:[ \\t]*\\r?\\n[ \\t]*)"
1884
- hex = "[0-9a-fA-F]"
1885
- hex2 = "(?:\\\\x(#{hex}{2}))"
1886
- hex4 = "(?:\\\\u(#{hex}{4}))"
1887
- hex8 = "(?:\\\\U(#{hex}{8}))"
1888
-
1889
- value = from(pos_start).byteslice(1...-1)
1890
- value.gsub!(%r{(?:\r\n|#{end1}|#{end2}+|#{hex2}|#{hex4}|#{hex8}|\\[\\ "/_0abefnrt\tvLNP])}) do |m|
1891
- case m
1892
- when /\A(?:#{hex2}|#{hex4}|#{hex8})\z/o
1893
- m[2..].to_i(16).chr(Encoding::UTF_8)
1894
- when /\A#{end1}\z/o
1895
- ""
1896
- when /\A#{end2}+\z/o
1897
- m.sub(/#{end2}/, "").gsub(/#{end2}/, "\n").then { |r| r.empty? ? " " : r }
1898
- else
1899
- C_DOUBLE_QUOTED_UNESCAPES.fetch(m, m)
2016
+ @context.within_double_quoted_scalar(@scanner.pos) do
2017
+ if try { match("\"") && parse_nb_double_text(n, c) && match("\"") }
2018
+ end1 = "(?:\\\\\\r?\\n[ \\t]*)"
2019
+ end2 = "(?:[ \\t]*\\r?\\n[ \\t]*)"
2020
+ hex = "[0-9a-fA-F]"
2021
+ hex2 = "(?:\\\\x(#{hex}{2}))"
2022
+ hex4 = "(?:\\\\u(#{hex}{4}))"
2023
+ hex8 = "(?:\\\\U(#{hex}{8}))"
2024
+
2025
+ value = from(pos_start).byteslice(1...-1)
2026
+ value.gsub!(%r{(?:\r\n|#{end1}|#{end2}+|#{hex2}|#{hex4}|#{hex8}|\\[\\ "/_0abefnrt\tvLNP])}) do |m|
2027
+ case m
2028
+ when /\A(?:#{hex2}|#{hex4}|#{hex8})\z/o
2029
+ m[2..].to_i(16).chr(Encoding::UTF_8)
2030
+ when /\A#{end1}\z/o
2031
+ ""
2032
+ when /\A#{end2}+\z/o
2033
+ m.sub(/#{end2}/, "").gsub(/#{end2}/, "\n").then { |r| r.empty? ? " " : r }
2034
+ else
2035
+ C_DOUBLE_QUOTED_UNESCAPES.fetch(m, m)
2036
+ end
1900
2037
  end
1901
- end
1902
2038
 
1903
- events_push_flush_properties(Scalar.new(Location.new(@source, pos_start, @scanner.pos), from(pos_start), value, Nodes::Scalar::DOUBLE_QUOTED))
1904
- true
2039
+ events_push_flush_properties(Scalar.new(Location.new(@source, pos_start, @scanner.pos), from(pos_start), value, Nodes::Scalar::DOUBLE_QUOTED))
2040
+ true
2041
+ end
1905
2042
  end
1906
2043
  end
1907
2044
 
@@ -2271,14 +2408,16 @@ module Psych
2271
2408
  def parse_c_flow_sequence(n, c)
2272
2409
  try do
2273
2410
  if match("[")
2274
- events_push_flush_properties(SequenceStart.new(Location.new(@source, @scanner.pos - 1, @scanner.pos), Nodes::Sequence::FLOW))
2411
+ @context.within_flow_sequence(@scanner.pos, c) do
2412
+ events_push_flush_properties(SequenceStart.new(Location.new(@source, @scanner.pos - 1, @scanner.pos), Nodes::Sequence::FLOW))
2275
2413
 
2276
- parse_s_separate(n, c)
2277
- parse_ns_s_flow_seq_entries(n, parse_in_flow(c))
2414
+ parse_s_separate(n, c)
2415
+ parse_ns_s_flow_seq_entries(n, parse_in_flow(c))
2278
2416
 
2279
- if match("]")
2280
- events_push_flush_properties(SequenceEnd.new(Location.new(@source, @scanner.pos - 1, @scanner.pos)))
2281
- true
2417
+ if match("]")
2418
+ events_push_flush_properties(SequenceEnd.new(Location.new(@source, @scanner.pos - 1, @scanner.pos)))
2419
+ true
2420
+ end
2282
2421
  end
2283
2422
  end
2284
2423
  end
@@ -2319,14 +2458,16 @@ module Psych
2319
2458
  def parse_c_flow_mapping(n, c)
2320
2459
  try do
2321
2460
  if match("{")
2322
- events_push_flush_properties(MappingStart.new(Location.new(@source, @scanner.pos - 1, @scanner.pos), Nodes::Mapping::FLOW))
2461
+ @context.within_flow_mapping(@scanner.pos, c) do
2462
+ events_push_flush_properties(MappingStart.new(Location.new(@source, @scanner.pos - 1, @scanner.pos), Nodes::Mapping::FLOW))
2323
2463
 
2324
- parse_s_separate(n, c)
2325
- parse_ns_s_flow_map_entries(n, parse_in_flow(c))
2464
+ parse_s_separate(n, c)
2465
+ parse_ns_s_flow_map_entries(n, parse_in_flow(c))
2326
2466
 
2327
- if match("}")
2328
- events_push_flush_properties(MappingEnd.new(Location.new(@source, @scanner.pos - 1, @scanner.pos)))
2329
- true
2467
+ if match("}")
2468
+ events_push_flush_properties(MappingEnd.new(Location.new(@source, @scanner.pos - 1, @scanner.pos)))
2469
+ true
2470
+ end
2330
2471
  end
2331
2472
  end
2332
2473
  end
@@ -3039,18 +3180,20 @@ module Psych
3039
3180
  def parse_l_block_sequence(n)
3040
3181
  return false if (m = detect_indent(n)) == 0
3041
3182
 
3042
- events_cache_push
3043
- events_push_flush_properties(SequenceStart.new(Location.point(@source, @scanner.pos), Nodes::Sequence::BLOCK))
3183
+ @context.within_block_sequence(@scanner.pos, n) do
3184
+ events_cache_push
3185
+ events_push_flush_properties(SequenceStart.new(Location.point(@source, @scanner.pos), Nodes::Sequence::BLOCK))
3044
3186
 
3045
- if try { plus { try { parse_s_indent(n + m) && parse_c_l_block_seq_entry(n + m) } } }
3046
- events_cache_flush
3047
- events_push_flush_properties(SequenceEnd.new(Location.point(@source, @scanner.pos)))
3048
- true
3049
- else
3050
- event = events_cache_pop[0]
3051
- @anchor = event.anchor
3052
- @tag = event.tag
3053
- false
3187
+ if try { plus { try { parse_s_indent(n + m) && parse_c_l_block_seq_entry(n + m) } } }
3188
+ events_cache_flush
3189
+ events_push_flush_properties(SequenceEnd.new(Location.point(@source, @scanner.pos)))
3190
+ true
3191
+ else
3192
+ event = events_cache_pop[0]
3193
+ @anchor = event.anchor
3194
+ @tag = event.tag
3195
+ false
3196
+ end
3054
3197
  end
3055
3198
  end
3056
3199
 
@@ -3115,16 +3258,18 @@ module Psych
3115
3258
  def parse_l_block_mapping(n)
3116
3259
  return false if (m = detect_indent(n)) == 0
3117
3260
 
3118
- events_cache_push
3119
- events_push_flush_properties(MappingStart.new(Location.point(@source, @scanner.pos), Nodes::Mapping::BLOCK))
3261
+ @context.within_block_mapping(@scanner.pos, n) do
3262
+ events_cache_push
3263
+ events_push_flush_properties(MappingStart.new(Location.point(@source, @scanner.pos), Nodes::Mapping::BLOCK))
3120
3264
 
3121
- if try { plus { try { parse_s_indent(n + m) && parse_ns_l_block_map_entry(n + m) } } }
3122
- events_cache_flush
3123
- events_push_flush_properties(MappingEnd.new(Location.point(@source, @scanner.pos)))
3124
- true
3125
- else
3126
- events_cache_pop
3127
- false
3265
+ if try { plus { try { parse_s_indent(n + m) && parse_ns_l_block_map_entry(n + m) } } }
3266
+ events_cache_flush
3267
+ events_push_flush_properties(MappingEnd.new(Location.point(@source, @scanner.pos)))
3268
+ true
3269
+ else
3270
+ events_cache_pop
3271
+ false
3272
+ end
3128
3273
  end
3129
3274
  end
3130
3275
 
@@ -4221,19 +4366,19 @@ module Psych
4221
4366
  end
4222
4367
  end
4223
4368
 
4224
- def self.unsafe_load(yaml, filename: nil, fallback: false, symbolize_names: false, freeze: false, strict_integer: false, comments: false)
4369
+ def self.unsafe_load(yaml, filename: nil, fallback: false, symbolize_names: false, freeze: false, strict_integer: false, parse_symbols: true, comments: false)
4225
4370
  result = parse(yaml, filename: filename, comments: comments)
4226
4371
  return fallback unless result
4227
4372
 
4228
- result.to_ruby(symbolize_names: symbolize_names, freeze: freeze, strict_integer: strict_integer, comments: comments)
4373
+ result.to_ruby(symbolize_names: symbolize_names, freeze: freeze, strict_integer: strict_integer, parse_symbols: parse_symbols, comments: comments)
4229
4374
  end
4230
4375
 
4231
- def self.safe_load(yaml, permitted_classes: [], permitted_symbols: [], aliases: false, filename: nil, fallback: nil, symbolize_names: false, freeze: false, strict_integer: false, comments: false)
4376
+ def self.safe_load(yaml, permitted_classes: [], permitted_symbols: [], aliases: false, filename: nil, fallback: nil, symbolize_names: false, freeze: false, strict_integer: false, parse_symbols: true, comments: false)
4232
4377
  result = parse(yaml, filename: filename, comments: comments)
4233
4378
  return fallback unless result
4234
4379
 
4235
4380
  class_loader = ClassLoader::Restricted.new(permitted_classes.map(&:to_s), permitted_symbols.map(&:to_s))
4236
- scanner = ScalarScanner.new(class_loader, strict_integer: strict_integer)
4381
+ scanner = ScalarScanner.new(class_loader, strict_integer: strict_integer, parse_symbols: parse_symbols)
4237
4382
  visitor =
4238
4383
  if aliases
4239
4384
  Visitors::ToRuby.new(scanner, class_loader, symbolize_names: symbolize_names, freeze: freeze, comments: comments)
@@ -4244,7 +4389,7 @@ module Psych
4244
4389
  visitor.accept(result)
4245
4390
  end
4246
4391
 
4247
- def self.load(yaml, permitted_classes: [Symbol], permitted_symbols: [], aliases: false, filename: nil, fallback: nil, symbolize_names: false, freeze: false, strict_integer: false, comments: false)
4392
+ def self.load(yaml, permitted_classes: [Symbol], permitted_symbols: [], aliases: false, filename: nil, fallback: nil, symbolize_names: false, freeze: false, strict_integer: false, parse_symbols: true, comments: false)
4248
4393
  safe_load(
4249
4394
  yaml,
4250
4395
  permitted_classes: permitted_classes,
@@ -4255,6 +4400,7 @@ module Psych
4255
4400
  symbolize_names: symbolize_names,
4256
4401
  freeze: freeze,
4257
4402
  strict_integer: strict_integer,
4403
+ parse_symbols: parse_symbols,
4258
4404
  comments: comments
4259
4405
  )
4260
4406
  end
@@ -4273,6 +4419,21 @@ module Psych
4273
4419
  result
4274
4420
  end
4275
4421
 
4422
+ def self.safe_load_stream(yaml, filename: nil, permitted_classes: [], aliases: false, comments: false)
4423
+ documents = parse_stream(yaml, filename: filename).children.map do |child|
4424
+ stream = Psych::Nodes::Stream.new
4425
+ stream.children << child
4426
+ safe_load(stream.to_yaml, permitted_classes: permitted_classes, aliases: aliases, comments: comments)
4427
+ end
4428
+
4429
+ if block_given?
4430
+ documents.each { |doc| yield doc }
4431
+ nil
4432
+ else
4433
+ documents
4434
+ end
4435
+ end
4436
+
4276
4437
  def self.unsafe_load_file(filename, **kwargs)
4277
4438
  File.open(filename, "r:bom|utf-8") do |f|
4278
4439
  self.unsafe_load(f, filename: filename, **kwargs)
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.2.0
4
+ version: 0.3.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-11-11 00:00:00.000000000 Z
11
+ date: 2025-12-25 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.2.0/CHANGELOG.md
116
+ changelog_uri: https://github.com/kddnewton/psych-pure/blob/v0.3.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: