From a56472cd12ba7e7fec9b98cea092d1ed5cfa7c92 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Fri, 19 Apr 2024 15:00:33 +0200 Subject: [PATCH 1/6] Use #each_line instead of #lines The block form of #lines has been deprecated in Ruby 2.4 --- lib/vcardigan/vcard.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vcardigan/vcard.rb b/lib/vcardigan/vcard.rb index 8fd6df6..86abb38 100644 --- a/lib/vcardigan/vcard.rb +++ b/lib/vcardigan/vcard.rb @@ -174,7 +174,7 @@ def unfold(card) unfolded = [] prior_line = nil - card.lines do |line| + card.each_line do |line| line.chomp! # If it's a continuation line, add it to the last. # If it's an empty line, drop it from the input. From 7641534a9ae1890a4a91ce4c4360ba535dc8cb06 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Fri, 19 Apr 2024 15:07:56 +0200 Subject: [PATCH 2/6] Update to RSpec 3 RSpec 2 fails to run on Ruby 3 --- Gemfile.lock | 30 +++++++++++++++++++----------- vcardigan.gemspec | 2 +- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6d3bcf5..858b0e5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,26 +1,31 @@ PATH remote: . specs: - vcardigan (0.0.8) + vcardigan (0.0.9) GEM remote: https://rubygems.org/ specs: coderay (1.1.0) - diff-lcs (1.1.3) + diff-lcs (1.5.1) method_source (0.8.2) pry (0.9.12.6) coderay (~> 1.0) method_source (~> 0.8) slop (~> 3.4) - rspec (2.12.0) - rspec-core (~> 2.12.0) - rspec-expectations (~> 2.12.0) - rspec-mocks (~> 2.12.0) - rspec-core (2.12.2) - rspec-expectations (2.12.1) - diff-lcs (~> 1.1.3) - rspec-mocks (2.12.1) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.1) slop (3.4.7) PLATFORMS @@ -28,5 +33,8 @@ PLATFORMS DEPENDENCIES pry - rspec (~> 2.0) + rspec (~> 3.0) vcardigan! + +BUNDLED WITH + 2.5.7 diff --git a/vcardigan.gemspec b/vcardigan.gemspec index 9a62b89..fae6e12 100644 --- a/vcardigan.gemspec +++ b/vcardigan.gemspec @@ -12,6 +12,6 @@ spec = Gem::Specification.new do |s| s.files = Dir['lib/**/*.rb'] s.test_files = Dir['spec/**/*.rb'] s.has_rdoc = false - s.add_development_dependency 'rspec', '~> 2.0' + s.add_development_dependency 'rspec', '~> 3.0' s.add_development_dependency 'pry' end From 5b836fbc0d1a37571c39da591818bed1d95f4a61 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Fri, 19 Apr 2024 17:33:10 +0200 Subject: [PATCH 3/6] Add strict parsing mode The default parser doesn't consider begin, end, version, or fn fields while parsing. This leads to odd behaviour if you give it an invalid vCard - such as one without an begin or end line; without a version or full name - where it parses it as if it were valid. This problem is most obvious if you give the parser a file that contains multiple cards, it will merge all the cards into a single large card. --- lib/vcardigan.rb | 4 ++ lib/vcardigan/errors.rb | 32 ++++++++++- lib/vcardigan/vcard.rb | 37 ++++++++++--- spec/examples/vcard_spec.rb | 96 +++++++++++++++++++++++++++++++++ spec/helpers/multiple_begin.vcf | 7 +++ spec/helpers/no_begin.vcf | 5 ++ spec/helpers/no_end.vcf | 5 ++ spec/helpers/no_fullname.vcf | 5 ++ spec/helpers/no_version.vcf | 5 ++ spec/helpers/scrambeled_joe.vcf | 6 +++ 10 files changed, 194 insertions(+), 8 deletions(-) create mode 100644 spec/helpers/multiple_begin.vcf create mode 100644 spec/helpers/no_begin.vcf create mode 100644 spec/helpers/no_end.vcf create mode 100644 spec/helpers/no_fullname.vcf create mode 100644 spec/helpers/no_version.vcf create mode 100644 spec/helpers/scrambeled_joe.vcf diff --git a/lib/vcardigan.rb b/lib/vcardigan.rb index 0cfd4b1..de42f33 100644 --- a/lib/vcardigan.rb +++ b/lib/vcardigan.rb @@ -16,6 +16,10 @@ def parse(*args) VCardigan::VCard.new.parse(*args) end + def parse!(*args) + VCardigan::VCard.new.parse(*args, strict: true) + end + end end diff --git a/lib/vcardigan/errors.rb b/lib/vcardigan/errors.rb index 5ebf7cc..0c60b17 100644 --- a/lib/vcardigan/errors.rb +++ b/lib/vcardigan/errors.rb @@ -1,3 +1,33 @@ module VCardigan - EncodingError = Class.new(StandardError) + class Error < StandardError + class << self + attr_accessor :default_message + end + + def initialize(message = nil) + super(message || self.class.default_message) + end + end + + class EncodingError < Error; end + + class MissingBeginError < EncodingError + self.default_message = "vCards must start with a BEGIN:VCARD line" + end + + class MissingEndError < EncodingError + self.default_message = "vCards must end with an END:VCARD line" + end + + class MissingVersionError < EncodingError + self.default_message = "vCards must include a VERSION field" + end + + class MissingFullNameError < EncodingError + self.default_message = "vCards must include an FN field" + end + + class UnexpectedBeginError < EncodingError + self.default_message = "vCard has more than one BEGIN:VCARD line" + end end diff --git a/lib/vcardigan/vcard.rb b/lib/vcardigan/vcard.rb index 86abb38..85186d2 100644 --- a/lib/vcardigan/vcard.rb +++ b/lib/vcardigan/vcard.rb @@ -24,13 +24,18 @@ def initialize(options = {}) @group = nil end - def parse(data) - lines = unfold(data) + def parse!(data) + parse(data, strict: true) + end + + def parse(data, strict: false) + self.version = nil if strict + lines = unfold(data, strict: strict) # Add the parsed properties to this vCard lines.each do |line| if line =~ /^VERSION:(.+)/ - @version = $1 + self.version = $1 next end @@ -38,6 +43,9 @@ def parse(data) add_prop(property) end + raise VCardigan::MissingVersionError if strict && self.version.nil? + raise VCardigan::MissingFullNameError if strict && !@fields.has_key?('fn') + self end @@ -170,27 +178,43 @@ def fullname(*args) # lines to be inserted for readability - it does this by dropping zero-length # lines. # Borrowed from https://github.com/qoobaa/vcard - def unfold(card) + def unfold(card, strict: true) unfolded = [] + passed_begining = 0 + passed_ending = 0 + prior_line = nil card.each_line do |line| line.chomp! # If it's a continuation line, add it to the last. # If it's an empty line, drop it from the input. if line =~ /^[ \t]/ + next if strict && passed_begining.zero? unfolded[-1] << line[1, line.size-1] - elsif line =~ /(^BEGIN:VCARD$)|(^END:VCARD$)/ + elsif line =~ /^BEGIN:VCARD$/ + passed_begining += 1 + raise VCardigan::UnexpectedBeginError if strict && passed_begining > 1 + elsif line =~ /^END:VCARD$/ + passed_ending += 1 + break if strict elsif prior_line && (prior_line =~ UNTERMINATED_QUOTED_PRINTABLE) + next if strict && passed_begining.zero? # Strip the trailing = off prior line, then append current line unfolded[-1] = prior_line[0, prior_line.length-1] + line elsif line =~ /^$/ else + next if strict && passed_begining.zero? unfolded << line end prior_line = unfolded[-1] end + if strict + raise VCardigan::MissingBeginError if passed_begining == 0 + raise VCardigan::MissingEndError if passed_ending == 0 + end + unfolded end @@ -225,8 +249,7 @@ def add_prop(property) def validate unless @fields['fn'] - raise VCardigan::EncodingError, - "vCards must include an FN field" + raise VCardigan::MissingFullNameError end end diff --git a/spec/examples/vcard_spec.rb b/spec/examples/vcard_spec.rb index 767e640..540a6f1 100644 --- a/spec/examples/vcard_spec.rb +++ b/spec/examples/vcard_spec.rb @@ -357,4 +357,100 @@ end end end + + describe '#parse!' do + context 'invalid vCard' do + let(:data) { File.read(File.dirname(__FILE__) + '/../helpers/joe.vcf') } + let(:vcard) { VCardigan.parse!(data) } + let(:fields) { vcard.instance_variable_get(:@fields) } + + context 'when the fields are out of order' do + let(:data) { File.read(File.dirname(__FILE__) + '/../helpers/scrambeled_joe.vcf') } + + it 'ignores all fields before the begin line and after the end line' do + expect(fields).not_to have_key('n') + expect(fields).not_to have_key('email') + expect(fields).to have_key('fn') + expect(vcard.fullname.first.value).to eq('Joe Strummer') + expect(vcard.email).to be_nil + end + end + + context 'when the version is missing' do + let(:data) { File.read(File.dirname(__FILE__) + '/../helpers/no_version.vcf') } + + it 'raises an error' do + expect { vcard }.to raise_error(VCardigan::MissingVersionError) + end + end + + context 'when the begin line is missing' do + let(:data) { File.read(File.dirname(__FILE__) + '/../helpers/no_begin.vcf') } + + it 'raises an error' do + expect { vcard }.to raise_error(VCardigan::MissingBeginError) + end + end + + context 'when the end line is missing' do + let(:data) { File.read(File.dirname(__FILE__) + '/../helpers/no_end.vcf') } + + it 'raises an error' do + expect { vcard }.to raise_error(VCardigan::MissingEndError) + end + end + + context 'when the full name is missing' do + let(:data) { File.read(File.dirname(__FILE__) + '/../helpers/no_fullname.vcf') } + + it 'raises an error' do + expect { vcard }.to raise_error(VCardigan::MissingFullNameError) + end + end + + context 'when there are multiple begin lines' do + let(:data) { File.read(File.dirname(__FILE__) + '/../helpers/multiple_begin.vcf') } + + it 'raises an error' do + expect { vcard }.to raise_error(VCardigan::UnexpectedBeginError) + end + end + end + + context 'valid 4.0 vCard' do + let(:data) { File.read(File.dirname(__FILE__) + '/../helpers/joe.vcf') } + let(:vcard) { VCardigan.parse!(data) } + let(:fields) { vcard.instance_variable_get(:@fields) } + + it 'should set the version' do + vcard.version.should == '4.0' + end + + it 'should only have one version property' do + vcard.to_s.lines.count {|l| l =~ /^VERSION:/ }.should == 1 + end + + it 'should add the properties to the fields array' do + fields.should have_key('n') + fields.should have_key('fn') + end + end + + context 'google 3.0 vCard' do + let(:data) { File.read(File.dirname(__FILE__) + '/../helpers/google.vcf') } + let(:vcard) { VCardigan.parse!(data) } + let(:fields) { vcard.instance_variable_get(:@fields) } + + it 'should set the version' do + vcard.version.should == '3.0' + end + + it 'should add the properties to the fields array' do + fields.should have_key('n') + fields.should have_key('fn') + fields.should have_key('photo') + fields.should have_key('x-socialprofile') + end + end + end end diff --git a/spec/helpers/multiple_begin.vcf b/spec/helpers/multiple_begin.vcf new file mode 100644 index 0000000..83d4f0d --- /dev/null +++ b/spec/helpers/multiple_begin.vcf @@ -0,0 +1,7 @@ +BEGIN:VCARD +VERSION:4.0 +FN:Joe Strummer +N:Strummer;Joe;;; +BEGIN:VCARD +EMAIL;type=INTERNET;type=HOME:joe@example.com +END:VCARD diff --git a/spec/helpers/no_begin.vcf b/spec/helpers/no_begin.vcf new file mode 100644 index 0000000..351ab88 --- /dev/null +++ b/spec/helpers/no_begin.vcf @@ -0,0 +1,5 @@ +VERSION:4.0 +FN:Joe Strummer +N:Strummer;Joe;;; +EMAIL;type=INTERNET;type=HOME:joe@example.com +END:VCARD diff --git a/spec/helpers/no_end.vcf b/spec/helpers/no_end.vcf new file mode 100644 index 0000000..bfc1636 --- /dev/null +++ b/spec/helpers/no_end.vcf @@ -0,0 +1,5 @@ +BEGIN:VCARD +VERSION:4.0 +FN:Joe Strummer +N:Strummer;Joe;;; +EMAIL;type=INTERNET;type=HOME:joe@example.com diff --git a/spec/helpers/no_fullname.vcf b/spec/helpers/no_fullname.vcf new file mode 100644 index 0000000..ff25f9b --- /dev/null +++ b/spec/helpers/no_fullname.vcf @@ -0,0 +1,5 @@ +BEGIN:VCARD +VERSION:4.0 +N:Strummer;Joe;;; +EMAIL;type=INTERNET;type=HOME:joe@example.com +END:VCARD diff --git a/spec/helpers/no_version.vcf b/spec/helpers/no_version.vcf new file mode 100644 index 0000000..0a88060 --- /dev/null +++ b/spec/helpers/no_version.vcf @@ -0,0 +1,5 @@ +BEGIN:VCARD +FN:Joe Strummer +N:Strummer;Joe;;; +EMAIL;type=INTERNET;type=HOME:joe@example.com +END:VCARD diff --git a/spec/helpers/scrambeled_joe.vcf b/spec/helpers/scrambeled_joe.vcf new file mode 100644 index 0000000..0b2584d --- /dev/null +++ b/spec/helpers/scrambeled_joe.vcf @@ -0,0 +1,6 @@ +EMAIL;type=INTERNET;type=HOME:joe@example.com +BEGIN:VCARD +VERSION:4.0 +FN:Joe Strummer +END:VCARD +N:Strummer;Joe;;; From 84efff98ba3ba0531662e6b3dce153944d517306 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Fri, 19 Apr 2024 18:48:41 +0200 Subject: [PATCH 4/6] Add ability to parse multiple vCards contained within a single file --- lib/vcardigan.rb | 4 ++ lib/vcardigan/vcard.rb | 26 +++++++++ spec/examples/vcard_spec.rb | 75 ++++++++++++++++++++++++++ spec/helpers/doe_family.vcf | 10 ++++ spec/helpers/scrambeled_doe_family.vcf | 10 ++++ 5 files changed, 125 insertions(+) create mode 100644 spec/helpers/doe_family.vcf create mode 100644 spec/helpers/scrambeled_doe_family.vcf diff --git a/lib/vcardigan.rb b/lib/vcardigan.rb index de42f33..1baa9c3 100644 --- a/lib/vcardigan.rb +++ b/lib/vcardigan.rb @@ -20,6 +20,10 @@ def parse!(*args) VCardigan::VCard.new.parse(*args, strict: true) end + def parse_all!(io, skip_invalid: false, &block) + VCardigan::VCard.parse_all(io, skip_invalid: skip_invalid, &block) + end + end end diff --git a/lib/vcardigan/vcard.rb b/lib/vcardigan/vcard.rb index 85186d2..f5f0737 100644 --- a/lib/vcardigan/vcard.rb +++ b/lib/vcardigan/vcard.rb @@ -1,3 +1,5 @@ +require 'stringio' + module VCardigan class VCard @@ -6,6 +8,30 @@ class VCard # it's not terminated UNTERMINATED_QUOTED_PRINTABLE = /ENCODING=QUOTED-PRINTABLE:.*=$/ + class << self + def parse_all(io, skip_invalid: false, &block) + io = StringIO.new(io.to_s) unless io.is_a?(IO) + + enumerator = Enumerator.new do |yielder| + loop do + begin + yielder << new.parse(io, strict: true) + rescue VCardigan::EncodingError => e + raise e unless skip_invalid + end + + break if io.eof? + end + end + + if block_given? + enumerator.each(&block) + else + enumerator + end + end + end + attr_accessor :version attr_accessor :chars diff --git a/spec/examples/vcard_spec.rb b/spec/examples/vcard_spec.rb index 540a6f1..e21cb54 100644 --- a/spec/examples/vcard_spec.rb +++ b/spec/examples/vcard_spec.rb @@ -453,4 +453,79 @@ end end end + + describe '.parse_all!' do + context 'with a valid 4.0 vCard string' do + it 'should return an enumerator of vCards' do + data = File.read(File.dirname(__FILE__) + '/../helpers/doe_family.vcf') + vcards = VCardigan.parse_all!(data) + + expect(vcards).to be_an_instance_of(Enumerator) + + vcards = vcards.to_a + expect(vcards.size).to eq(2) + expect(vcards).to all(be_an_instance_of(VCardigan::VCard)) + + vcards = [] + VCardigan.parse_all!(data) do |vcard| + vcards << vcard + end + + expect(vcards.size).to eq(2) + expect(vcards).to all(be_an_instance_of(VCardigan::VCard)) + end + end + + context 'with a valid 4.0 vCard io' do + it 'should return an enumerator of vCards' do + data = File.open(File.dirname(__FILE__) + '/../helpers/doe_family.vcf', 'r') + vcards = VCardigan.parse_all!(data) + + expect(vcards).to be_an_instance_of(Enumerator) + + vcards = vcards.to_a + expect(vcards.size).to eq(2) + expect(vcards).to all(be_an_instance_of(VCardigan::VCard)) + + vcards = [] + data.rewind + VCardigan.parse_all!(data) do |vcard| + vcards << vcard + end + + expect(vcards.size).to eq(2) + expect(vcards).to all(be_an_instance_of(VCardigan::VCard)) + end + end + + context 'with an invalid vCard' do + it 'parses what it can and then rasies an error' do + data = File.open(File.dirname(__FILE__) + '/../helpers/scrambeled_doe_family.vcf', 'r') + + vcards = [] + + expect do + VCardigan.parse_all!(data) do |vcard| + vcards << vcard + end + end.to raise_error(VCardigan::EncodingError) + + expect(vcards.size).to eq(1) + expect(vcards.first).to be_an_instance_of(VCardigan::VCard) + end + + it 'parses what it can when passed skip_invalid: true' do + data = File.open(File.dirname(__FILE__) + '/../helpers/scrambeled_doe_family.vcf', 'r') + + vcards = [] + + VCardigan.parse_all!(data, skip_invalid: true) do |vcard| + vcards << vcard + end + + expect(vcards.size).to eq(1) + expect(vcards.first).to be_an_instance_of(VCardigan::VCard) + end + end + end end diff --git a/spec/helpers/doe_family.vcf b/spec/helpers/doe_family.vcf new file mode 100644 index 0000000..9b7e3ac --- /dev/null +++ b/spec/helpers/doe_family.vcf @@ -0,0 +1,10 @@ +BEGIN:VCARD +VERSION:4.0 +N:Doe;John;;; +FN:John Doe +END:VCARD +BEGIN:VCARD +VERSION:4.0 +N:Doe;Jane;;; +FN:Jane Doe +END:VCARD diff --git a/spec/helpers/scrambeled_doe_family.vcf b/spec/helpers/scrambeled_doe_family.vcf new file mode 100644 index 0000000..c96a17f --- /dev/null +++ b/spec/helpers/scrambeled_doe_family.vcf @@ -0,0 +1,10 @@ +VERSION:4.0 +N:Doe;John;;; +FN:John Doe +BEGIN:VCARD +VERSION:4.0 +N:Doe;Jane;;; +FN:Jane Doe +END:VCARD +BEGIN:VCARD +BEGIN:VCARD From 65c41da7e0bad73da784b990e6958dfa445a316f Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Sat, 20 Apr 2024 18:03:56 +0200 Subject: [PATCH 5/6] Fix crash on trailing blank lines --- lib/vcardigan/errors.rb | 4 ---- lib/vcardigan/vcard.rb | 15 +++++++-------- spec/examples/vcard_spec.rb | 8 -------- spec/helpers/doe_family.vcf | 5 +++++ 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/lib/vcardigan/errors.rb b/lib/vcardigan/errors.rb index 0c60b17..d19e94c 100644 --- a/lib/vcardigan/errors.rb +++ b/lib/vcardigan/errors.rb @@ -11,10 +11,6 @@ def initialize(message = nil) class EncodingError < Error; end - class MissingBeginError < EncodingError - self.default_message = "vCards must start with a BEGIN:VCARD line" - end - class MissingEndError < EncodingError self.default_message = "vCards must end with an END:VCARD line" end diff --git a/lib/vcardigan/vcard.rb b/lib/vcardigan/vcard.rb index f5f0737..0bf7eb0 100644 --- a/lib/vcardigan/vcard.rb +++ b/lib/vcardigan/vcard.rb @@ -15,7 +15,10 @@ def parse_all(io, skip_invalid: false, &block) enumerator = Enumerator.new do |yielder| loop do begin - yielder << new.parse(io, strict: true) + vcard = new.parse(io, strict: true) + break if io.eof? && vcard.nil? + + yielder << vcard if vcard rescue VCardigan::EncodingError => e raise e unless skip_invalid end @@ -50,13 +53,10 @@ def initialize(options = {}) @group = nil end - def parse!(data) - parse(data, strict: true) - end - def parse(data, strict: false) self.version = nil if strict lines = unfold(data, strict: strict) + return nil if lines.empty? && strict # Add the parsed properties to this vCard lines.each do |line| @@ -236,9 +236,8 @@ def unfold(card, strict: true) prior_line = unfolded[-1] end - if strict - raise VCardigan::MissingBeginError if passed_begining == 0 - raise VCardigan::MissingEndError if passed_ending == 0 + if strict && passed_begining.positive? && passed_ending.zero? + raise VCardigan::MissingEndError end unfolded diff --git a/spec/examples/vcard_spec.rb b/spec/examples/vcard_spec.rb index e21cb54..af70976 100644 --- a/spec/examples/vcard_spec.rb +++ b/spec/examples/vcard_spec.rb @@ -384,14 +384,6 @@ end end - context 'when the begin line is missing' do - let(:data) { File.read(File.dirname(__FILE__) + '/../helpers/no_begin.vcf') } - - it 'raises an error' do - expect { vcard }.to raise_error(VCardigan::MissingBeginError) - end - end - context 'when the end line is missing' do let(:data) { File.read(File.dirname(__FILE__) + '/../helpers/no_end.vcf') } diff --git a/spec/helpers/doe_family.vcf b/spec/helpers/doe_family.vcf index 9b7e3ac..c68993b 100644 --- a/spec/helpers/doe_family.vcf +++ b/spec/helpers/doe_family.vcf @@ -8,3 +8,8 @@ VERSION:4.0 N:Doe;Jane;;; FN:Jane Doe END:VCARD + + + + + From ef4021bf05b091fb8d1e46ec8d3195cc809b83e4 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Sat, 20 Apr 2024 18:49:10 +0200 Subject: [PATCH 6/6] Accept IO like objects --- lib/vcardigan/vcard.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/vcardigan/vcard.rb b/lib/vcardigan/vcard.rb index 0bf7eb0..ff4327c 100644 --- a/lib/vcardigan/vcard.rb +++ b/lib/vcardigan/vcard.rb @@ -10,20 +10,18 @@ class VCard class << self def parse_all(io, skip_invalid: false, &block) - io = StringIO.new(io.to_s) unless io.is_a?(IO) + io = StringIO.new(io.to_s) unless io_like?(io) enumerator = Enumerator.new do |yielder| loop do + break if io.eof? + begin vcard = new.parse(io, strict: true) - break if io.eof? && vcard.nil? - yielder << vcard if vcard rescue VCardigan::EncodingError => e raise e unless skip_invalid end - - break if io.eof? end end @@ -33,6 +31,12 @@ def parse_all(io, skip_invalid: false, &block) enumerator end end + + private + + def io_like?(obj) + obj.respond_to?(:each_line) && obj.respond_to?(:eof?) + end end attr_accessor :version