prosereflect 0.1.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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rake.yml +15 -0
  3. data/.github/workflows/release.yml +25 -0
  4. data/.gitignore +12 -0
  5. data/.rubocop.yml +1 -0
  6. data/.rubocop_todo.yml +228 -0
  7. data/CODE_OF_CONDUCT.md +132 -0
  8. data/Gemfile +10 -0
  9. data/README.adoc +268 -0
  10. data/Rakefile +12 -0
  11. data/debug_loading.rb +34 -0
  12. data/lib/prosereflect/document.rb +63 -0
  13. data/lib/prosereflect/hard_break.rb +22 -0
  14. data/lib/prosereflect/node.rb +77 -0
  15. data/lib/prosereflect/paragraph.rb +50 -0
  16. data/lib/prosereflect/parser.rb +56 -0
  17. data/lib/prosereflect/table.rb +64 -0
  18. data/lib/prosereflect/table_cell.rb +37 -0
  19. data/lib/prosereflect/table_row.rb +32 -0
  20. data/lib/prosereflect/text.rb +37 -0
  21. data/lib/prosereflect/version.rb +5 -0
  22. data/lib/prosereflect.rb +17 -0
  23. data/prosereflect.gemspec +33 -0
  24. data/sig/prosemirror.rbs +4 -0
  25. data/spec/fixtures/ituob-1000/ituob-1000-DP.json +366 -0
  26. data/spec/fixtures/ituob-1000/ituob-1000-DP.yaml +182 -0
  27. data/spec/fixtures/ituob-1000/ituob-1000-E118_IIN.json +381 -0
  28. data/spec/fixtures/ituob-1000/ituob-1000-E118_IIN.yaml +182 -0
  29. data/spec/fixtures/ituob-1000/ituob-1000-E164_ACN.json +203 -0
  30. data/spec/fixtures/ituob-1000/ituob-1000-E164_ACN.yaml +98 -0
  31. data/spec/fixtures/ituob-1000/ituob-1000-E212_MNC.json +202 -0
  32. data/spec/fixtures/ituob-1000/ituob-1000-E212_MNC.yaml +101 -0
  33. data/spec/fixtures/ituob-1000/ituob-1000-F32_TDI.json +6948 -0
  34. data/spec/fixtures/ituob-1000/ituob-1000-F32_TDI.yaml +3519 -0
  35. data/spec/fixtures/ituob-1000/ituob-1000-M1400_ICC.json +529 -0
  36. data/spec/fixtures/ituob-1000/ituob-1000-M1400_ICC.yaml +263 -0
  37. data/spec/fixtures/ituob-1000/ituob-1000-NNP.json +288 -0
  38. data/spec/fixtures/ituob-1000/ituob-1000-NNP.yaml +152 -0
  39. data/spec/fixtures/ituob-1000/ituob-1000-Q708_ISPC.json +1534 -0
  40. data/spec/fixtures/ituob-1000/ituob-1000-Q708_ISPC.yaml +789 -0
  41. data/spec/fixtures/ituob-1000/ituob-1000-Q708_SANC.json +252 -0
  42. data/spec/fixtures/ituob-1000/ituob-1000-Q708_SANC.yaml +123 -0
  43. data/spec/fixtures/ituob-1000/ituob-1000-R_SP_LM.V.json +428 -0
  44. data/spec/fixtures/ituob-1000/ituob-1000-R_SP_LM.V.yaml +208 -0
  45. data/spec/fixtures/ituob-1000/ituob-1000-T35_NA.json +621 -0
  46. data/spec/fixtures/ituob-1000/ituob-1000-T35_NA.yaml +317 -0
  47. data/spec/fixtures/ituob-1001/ituob-1001-DP.json +532 -0
  48. data/spec/fixtures/ituob-1001/ituob-1001-DP.yaml +266 -0
  49. data/spec/fixtures/ituob-1001/ituob-1001-E118_IIN.json +1093 -0
  50. data/spec/fixtures/ituob-1001/ituob-1001-E118_IIN.yaml +519 -0
  51. data/spec/fixtures/ituob-1001/ituob-1001-E164_ACN.json +449 -0
  52. data/spec/fixtures/ituob-1001/ituob-1001-E164_ACN.yaml +214 -0
  53. data/spec/fixtures/ituob-1001/ituob-1001-E164_CC.json +271 -0
  54. data/spec/fixtures/ituob-1001/ituob-1001-E164_CC.yaml +136 -0
  55. data/spec/fixtures/ituob-1001/ituob-1001-E212_MNC.json +199 -0
  56. data/spec/fixtures/ituob-1001/ituob-1001-E212_MNC.yaml +99 -0
  57. data/spec/fixtures/ituob-1001/ituob-1001-NNP.json +288 -0
  58. data/spec/fixtures/ituob-1001/ituob-1001-NNP.yaml +152 -0
  59. data/spec/fixtures/ituob-1001/ituob-1001-Q708_ISPC.json +960 -0
  60. data/spec/fixtures/ituob-1001/ituob-1001-Q708_ISPC.yaml +487 -0
  61. data/spec/prosereflect/document_spec.rb +149 -0
  62. data/spec/prosereflect/hard_break_spec.rb +51 -0
  63. data/spec/prosereflect/node_spec.rb +256 -0
  64. data/spec/prosereflect/paragraph_spec.rb +152 -0
  65. data/spec/prosereflect/parser_spec.rb +129 -0
  66. data/spec/prosereflect/table_cell_spec.rb +114 -0
  67. data/spec/prosereflect/table_row_spec.rb +77 -0
  68. data/spec/prosereflect/table_spec.rb +144 -0
  69. data/spec/prosereflect/text_spec.rb +116 -0
  70. data/spec/prosereflect/version_spec.rb +11 -0
  71. data/spec/prosereflect_spec.rb +57 -0
  72. data/spec/spec_helper.rb +21 -0
  73. data/spec/support/matchers.rb +131 -0
  74. data/spec/support/shared_examples.rb +220 -0
  75. metadata +133 -0
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Prosereflect::TableRow do
6
+ describe 'initialization' do
7
+ it 'initializes as a table_row node' do
8
+ row = described_class.new({ 'type' => 'table_row' })
9
+ expect(row.type).to eq('table_row')
10
+ end
11
+ end
12
+
13
+ describe '.create' do
14
+ it 'creates an empty table row' do
15
+ row = described_class.create
16
+ expect(row).to be_a(described_class)
17
+ expect(row.type).to eq('table_row')
18
+ expect(row.content).to be_empty
19
+ end
20
+
21
+ it 'creates a table row with attributes' do
22
+ attrs = { 'background' => '#f5f5f5' }
23
+ row = described_class.create(attrs)
24
+ expect(row.attrs).to eq(attrs)
25
+ end
26
+ end
27
+
28
+ describe '#cells' do
29
+ it 'returns all cells in the row' do
30
+ row = described_class.create
31
+ row.add_cell('Cell 1')
32
+ row.add_cell('Cell 2')
33
+
34
+ expect(row.cells.size).to eq(2)
35
+ expect(row.cells).to all(be_a(Prosereflect::TableCell))
36
+ end
37
+
38
+ it 'returns empty array for row with no cells' do
39
+ row = described_class.create
40
+ expect(row.cells).to eq([])
41
+ end
42
+ end
43
+
44
+ describe '#add_cell' do
45
+ it 'adds a cell with text content' do
46
+ row = described_class.create
47
+ cell = row.add_cell('Test content')
48
+
49
+ expect(row.cells.size).to eq(1)
50
+ expect(cell).to be_a(Prosereflect::TableCell)
51
+ expect(cell.text_content).to eq('Test content')
52
+ end
53
+
54
+ it 'adds an empty cell' do
55
+ row = described_class.create
56
+ cell = row.add_cell
57
+
58
+ expect(row.cells.size).to eq(1)
59
+ expect(cell).to be_a(Prosereflect::TableCell)
60
+ expect(cell.text_content).to eq('')
61
+ end
62
+ end
63
+
64
+ describe 'serialization' do
65
+ it 'converts to hash representation' do
66
+ row = described_class.create
67
+ row.add_cell('Cell 1')
68
+ row.add_cell('Cell 2')
69
+
70
+ hash = row.to_h
71
+ expect(hash['type']).to eq('table_row')
72
+ expect(hash['content'].size).to eq(2)
73
+ expect(hash['content'][0]['type']).to eq('table_cell')
74
+ expect(hash['content'][1]['type']).to eq('table_cell')
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Prosereflect::Table do
6
+ describe 'initialization' do
7
+ it 'initializes as a table node' do
8
+ table = described_class.new({ 'type' => 'table' })
9
+ expect(table.type).to eq('table')
10
+ end
11
+ end
12
+
13
+ describe '.create' do
14
+ it 'creates an empty table' do
15
+ table = described_class.create
16
+ expect(table).to be_a(described_class)
17
+ expect(table.type).to eq('table')
18
+ expect(table.content).to be_empty
19
+ end
20
+
21
+ it 'creates a table with attributes' do
22
+ attrs = { 'width' => '100%' }
23
+ table = described_class.create(attrs)
24
+ expect(table.attrs).to eq(attrs)
25
+ end
26
+ end
27
+
28
+ describe 'row access methods' do
29
+ let(:table) do
30
+ t = described_class.create
31
+ t.add_header(['Header 1', 'Header 2'])
32
+ t.add_row(['Data 1', 'Data 2'])
33
+ t.add_row(['Data 3', 'Data 4'])
34
+ t
35
+ end
36
+
37
+ describe '#rows' do
38
+ it 'returns all rows' do
39
+ expect(table.rows.size).to eq(3)
40
+ expect(table.rows).to all(be_a(Prosereflect::TableRow))
41
+ end
42
+ end
43
+
44
+ describe '#header_row' do
45
+ it 'returns the first row' do
46
+ expect(table.header_row).to be_a(Prosereflect::TableRow)
47
+ expect(table.header_row.cells.first.text_content).to eq('Header 1')
48
+ end
49
+ end
50
+
51
+ describe '#data_rows' do
52
+ it 'returns all rows except the header' do
53
+ expect(table.data_rows.size).to eq(2)
54
+ expect(table.data_rows).to all(be_a(Prosereflect::TableRow))
55
+ expect(table.data_rows.first.cells.first.text_content).to eq('Data 1')
56
+ end
57
+
58
+ it 'returns empty array if there are no data rows' do
59
+ table = described_class.create
60
+ table.add_header(['Header'])
61
+ expect(table.data_rows).to eq([])
62
+ end
63
+ end
64
+
65
+ describe '#cell_at' do
66
+ it 'returns cell at specified position' do
67
+ cell = table.cell_at(0, 1)
68
+ expect(cell).to be_a(Prosereflect::TableCell)
69
+ expect(cell.text_content).to eq('Data 2')
70
+ end
71
+
72
+ it 'returns nil for out of bounds indices' do
73
+ expect(table.cell_at(-1, 0)).to be_nil
74
+ expect(table.cell_at(0, -1)).to be_nil
75
+ expect(table.cell_at(5, 0)).to be_nil
76
+ expect(table.cell_at(0, 5)).to be_nil
77
+ end
78
+ end
79
+ end
80
+
81
+ describe 'table building methods' do
82
+ describe '#add_header' do
83
+ it 'adds a header row with cells' do
84
+ table = described_class.create
85
+ header = table.add_header(['Col 1', 'Col 2'])
86
+
87
+ expect(table.rows.size).to eq(1)
88
+ expect(header).to be_a(Prosereflect::TableRow)
89
+ expect(header.cells.size).to eq(2)
90
+ expect(header.cells.map(&:text_content)).to eq(['Col 1', 'Col 2'])
91
+ end
92
+ end
93
+
94
+ describe '#add_row' do
95
+ it 'adds a row with cells' do
96
+ table = described_class.create
97
+ row = table.add_row(['Data 1', 'Data 2'])
98
+
99
+ expect(table.rows.size).to eq(1)
100
+ expect(row).to be_a(Prosereflect::TableRow)
101
+ expect(row.cells.size).to eq(2)
102
+ expect(row.cells.map(&:text_content)).to eq(['Data 1', 'Data 2'])
103
+ end
104
+
105
+ it 'adds an empty row when no data provided' do
106
+ table = described_class.create
107
+ row = table.add_row
108
+
109
+ expect(table.rows.size).to eq(1)
110
+ expect(row.cells).to be_empty
111
+ end
112
+ end
113
+
114
+ describe '#add_rows' do
115
+ it 'adds multiple rows at once' do
116
+ table = described_class.create
117
+ table.add_rows([
118
+ ['Row 1, Cell 1', 'Row 1, Cell 2'],
119
+ ['Row 2, Cell 1', 'Row 2, Cell 2']
120
+ ])
121
+
122
+ expect(table.rows.size).to eq(2)
123
+ expect(table.rows[0].cells.size).to eq(2)
124
+ expect(table.rows[1].cells.size).to eq(2)
125
+ expect(table.rows[0].cells.first.text_content).to eq('Row 1, Cell 1')
126
+ expect(table.rows[1].cells.first.text_content).to eq('Row 2, Cell 1')
127
+ end
128
+ end
129
+ end
130
+
131
+ describe 'serialization' do
132
+ it 'converts to hash representation' do
133
+ table = described_class.create
134
+ table.add_header(['Col 1', 'Col 2'])
135
+ table.add_row(['Data 1', 'Data 2'])
136
+
137
+ hash = table.to_h
138
+ expect(hash['type']).to eq('table')
139
+ expect(hash['content'].size).to eq(2)
140
+ expect(hash['content'][0]['type']).to eq('table_row')
141
+ expect(hash['content'][1]['type']).to eq('table_row')
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Prosereflect::Text do
4
+ describe 'initialization' do
5
+ it 'initializes as a text node' do
6
+ text = described_class.new({ 'type' => 'text', 'text' => 'Hello' })
7
+ expect(text.type).to eq('text')
8
+ expect(text.text).to eq('Hello')
9
+ end
10
+
11
+ it 'initializes with empty text' do
12
+ text = described_class.new({ 'type' => 'text' })
13
+ expect(text.text).to eq('')
14
+ end
15
+
16
+ it 'initializes with marks' do
17
+ marks = [{ 'type' => 'bold' }, { 'type' => 'italic' }]
18
+ text = described_class.new({
19
+ 'type' => 'text',
20
+ 'text' => 'Formatted text',
21
+ 'marks' => marks
22
+ })
23
+
24
+ expect(text.marks).to eq(marks)
25
+ end
26
+ end
27
+
28
+ describe '.create' do
29
+ it 'creates a text node with content' do
30
+ text = described_class.create('Hello world')
31
+ expect(text.type).to eq('text')
32
+ expect(text.text).to eq('Hello world')
33
+ end
34
+
35
+ it 'creates a text node with marks' do
36
+ marks = [{ 'type' => 'bold' }]
37
+ text = described_class.create('Bold text', marks)
38
+
39
+ expect(text.text).to eq('Bold text')
40
+ expect(text.marks).to eq(marks)
41
+ end
42
+ end
43
+
44
+ describe '#text_content' do
45
+ it 'returns the text content' do
46
+ text = described_class.new({ 'type' => 'text', 'text' => 'Sample text' })
47
+ expect(text.text_content).to eq('Sample text')
48
+ end
49
+
50
+ it 'returns empty string when text is nil' do
51
+ text = described_class.new({ 'type' => 'text' })
52
+ expect(text.text_content).to eq('')
53
+ end
54
+ end
55
+
56
+ describe '#to_h' do
57
+ it 'creates a hash representation with text' do
58
+ text = described_class.new({ 'type' => 'text', 'text' => 'Hello' })
59
+ hash = text.to_h
60
+
61
+ expect(hash['type']).to eq('text')
62
+ expect(hash['text']).to eq('Hello')
63
+ end
64
+
65
+ it 'includes marks in hash representation when present' do
66
+ marks = [{ 'type' => 'bold' }]
67
+ text = described_class.new({
68
+ 'type' => 'text',
69
+ 'text' => 'Bold text',
70
+ 'marks' => marks
71
+ })
72
+
73
+ hash = text.to_h
74
+ expect(hash['marks']).to eq(marks)
75
+ end
76
+ end
77
+
78
+ describe 'inheritance' do
79
+ it 'is a Node' do
80
+ text = described_class.new({ 'type' => 'text', 'text' => 'Test' })
81
+ expect(text).to be_a(Prosereflect::Node)
82
+ end
83
+ end
84
+
85
+ describe 'with marks' do
86
+ it 'can have multiple marks' do
87
+ marks = [
88
+ { 'type' => 'bold' },
89
+ { 'type' => 'italic' },
90
+ { 'type' => 'underline' }
91
+ ]
92
+
93
+ text = described_class.new({
94
+ 'type' => 'text',
95
+ 'text' => 'Formatted text',
96
+ 'marks' => marks
97
+ })
98
+
99
+ expect(text.marks.size).to eq(3)
100
+ end
101
+
102
+ it 'can have marks with attributes' do
103
+ marks = [
104
+ { 'type' => 'link', 'attrs' => { 'href' => 'https://example.com' } }
105
+ ]
106
+
107
+ text = described_class.new({
108
+ 'type' => 'text',
109
+ 'text' => 'Link text',
110
+ 'marks' => marks
111
+ })
112
+
113
+ expect(text.marks[0]['attrs']['href']).to eq('https://example.com')
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Prosereflect::VERSION do
6
+ it 'has a version number' do
7
+ expect(Prosereflect::VERSION).not_to be nil
8
+ expect(Prosereflect::VERSION).to be_a(String)
9
+ expect(Prosereflect::VERSION).to match(/\d+\.\d+\.\d+/)
10
+ end
11
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Prosereflect do
6
+ let(:fixtures_path) { File.join(__dir__, 'fixtures') }
7
+
8
+ it 'has a version number' do
9
+ expect(Prosereflect::VERSION).not_to be nil
10
+ end
11
+
12
+ # Test YAML parsing for different fixtures
13
+ Dir.glob(File.join(__dir__, 'fixtures', '*/*.yaml')).each do |yaml_file|
14
+ context "with YAML fixture #{File.basename(yaml_file)}" do
15
+ let(:file_content) { File.read(yaml_file) }
16
+
17
+ include_examples 'a parsable format', :yaml
18
+
19
+ # Test table functionality for DP fixtures
20
+ if yaml_file.include?('DP')
21
+ include_examples 'a document with tables'
22
+ include_examples 'document traversal'
23
+ include_examples 'text content extraction'
24
+ end
25
+ end
26
+ end
27
+
28
+ # Test JSON parsing for different fixtures
29
+ Dir.glob(File.join(__dir__, 'fixtures', '*/*.json')).each do |json_file|
30
+ context "with JSON fixture #{File.basename(json_file)}" do
31
+ let(:file_content) { File.read(json_file) }
32
+
33
+ include_examples 'a parsable format', :json
34
+
35
+ # Test table functionality for DP fixtures
36
+ if json_file.include?('DP')
37
+ include_examples 'a document with tables'
38
+ include_examples 'document traversal'
39
+ include_examples 'text content extraction'
40
+ end
41
+ end
42
+ end
43
+
44
+ describe 'Document creation' do
45
+ include_examples 'document creation'
46
+ end
47
+
48
+ describe 'Round-trip format conversion' do
49
+ context 'with YAML format' do
50
+ include_examples 'format round-trip', :yaml
51
+ end
52
+
53
+ context 'with JSON format' do
54
+ include_examples 'format round-trip', :json
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'yaml'
5
+ require 'json'
6
+ require 'prosereflect'
7
+
8
+ # Load shared examples
9
+ Dir[File.join(__dir__, 'support', '**', '*.rb')].sort.each { |f| require f }
10
+
11
+ RSpec.configure do |config|
12
+ # Enable flags like --only-failures and --next-failure
13
+ config.example_status_persistence_file_path = '.rspec_status'
14
+
15
+ # Disable RSpec exposing methods globally on `Module` and `main`
16
+ config.disable_monkey_patching!
17
+
18
+ config.expect_with :rspec do |c|
19
+ c.syntax = :expect
20
+ end
21
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'json'
5
+
6
+ # Custom RSpec matcher for comparing JSON structures
7
+ RSpec::Matchers.define :be_equivalent_json do |expected|
8
+ match do |actual|
9
+ # Parse strings if needed
10
+ expected_hash = expected.is_a?(String) ? JSON.parse(expected) : expected
11
+ actual_hash = actual.is_a?(String) ? JSON.parse(actual) : actual
12
+
13
+ # Compare the parsed structures
14
+ expected_hash == actual_hash
15
+ end
16
+
17
+ failure_message do |actual|
18
+ expected_hash = expected.is_a?(String) ? JSON.parse(expected) : expected
19
+ actual_hash = actual.is_a?(String) ? JSON.parse(actual) : actual
20
+
21
+ "Expected JSON to be equivalent to #{expected_hash.inspect}, but got #{actual_hash.inspect}"
22
+ end
23
+ end
24
+
25
+ # Custom RSpec matcher for comparing YAML structures
26
+ RSpec::Matchers.define :be_equivalent_yaml do |expected|
27
+ match do |actual|
28
+ # Parse strings if needed
29
+ expected_hash = expected.is_a?(String) ? YAML.safe_load(expected) : expected
30
+ actual_hash = actual.is_a?(String) ? YAML.safe_load(actual) : actual
31
+
32
+ # Compare the parsed structures
33
+ expected_hash == actual_hash
34
+ end
35
+
36
+ failure_message do |actual|
37
+ expected_hash = expected.is_a?(String) ? YAML.safe_load(expected) : expected
38
+ actual_hash = actual.is_a?(String) ? YAML.safe_load(actual) : actual
39
+
40
+ "Expected YAML to be equivalent to #{expected_hash.inspect}, but got #{actual_hash.inspect}"
41
+ end
42
+ end
43
+
44
+ # Helper method to recursively compare hashes and arrays
45
+ def deep_compare(expected, actual)
46
+ return expected == actual unless expected.is_a?(Hash) || expected.is_a?(Array)
47
+
48
+ if expected.is_a?(Hash) && actual.is_a?(Hash)
49
+ return false unless expected.keys.sort == actual.keys.sort
50
+
51
+ expected.all? { |key, value| deep_compare(value, actual[key]) }
52
+ elsif expected.is_a?(Array) && actual.is_a?(Array)
53
+ return false unless expected.length == actual.length
54
+
55
+ expected.zip(actual).all? { |e, a| deep_compare(e, a) }
56
+ else
57
+ false
58
+ end
59
+ end
60
+
61
+ # More flexible matcher that handles structural equivalence
62
+ RSpec::Matchers.define :have_equivalent_structure do |expected|
63
+ match do |actual|
64
+ # Parse strings if needed
65
+ expected_data = case expected
66
+ when String
67
+ if expected.strip.start_with?('{', '[')
68
+ begin
69
+ JSON.parse(expected)
70
+ rescue StandardError
71
+ YAML.safe_load(expected)
72
+ end
73
+ else
74
+ YAML.safe_load(expected)
75
+ end
76
+ else
77
+ expected
78
+ end
79
+
80
+ actual_data = case actual
81
+ when String
82
+ if actual.strip.start_with?('{', '[')
83
+ begin
84
+ JSON.parse(actual)
85
+ rescue StandardError
86
+ YAML.safe_load(actual)
87
+ end
88
+ else
89
+ YAML.safe_load(actual)
90
+ end
91
+ else
92
+ actual
93
+ end
94
+
95
+ deep_compare(expected_data, actual_data)
96
+ end
97
+
98
+ failure_message do |actual|
99
+ expected_data = case expected
100
+ when String
101
+ if expected.strip.start_with?('{', '[')
102
+ begin
103
+ JSON.parse(expected)
104
+ rescue StandardError
105
+ YAML.safe_load(expected)
106
+ end
107
+ else
108
+ YAML.safe_load(expected)
109
+ end
110
+ else
111
+ expected
112
+ end
113
+
114
+ actual_data = case actual
115
+ when String
116
+ if actual.strip.start_with?('{', '[')
117
+ begin
118
+ JSON.parse(actual)
119
+ rescue StandardError
120
+ YAML.safe_load(actual)
121
+ end
122
+ else
123
+ YAML.safe_load(actual)
124
+ end
125
+ else
126
+ actual
127
+ end
128
+
129
+ "Expected structures to be equivalent:\nExpected: #{expected_data.inspect}\nActual: #{actual_data.inspect}"
130
+ end
131
+ end