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.
- checksums.yaml +7 -0
- data/.github/workflows/rake.yml +15 -0
- data/.github/workflows/release.yml +25 -0
- data/.gitignore +12 -0
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +228 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Gemfile +10 -0
- data/README.adoc +268 -0
- data/Rakefile +12 -0
- data/debug_loading.rb +34 -0
- data/lib/prosereflect/document.rb +63 -0
- data/lib/prosereflect/hard_break.rb +22 -0
- data/lib/prosereflect/node.rb +77 -0
- data/lib/prosereflect/paragraph.rb +50 -0
- data/lib/prosereflect/parser.rb +56 -0
- data/lib/prosereflect/table.rb +64 -0
- data/lib/prosereflect/table_cell.rb +37 -0
- data/lib/prosereflect/table_row.rb +32 -0
- data/lib/prosereflect/text.rb +37 -0
- data/lib/prosereflect/version.rb +5 -0
- data/lib/prosereflect.rb +17 -0
- data/prosereflect.gemspec +33 -0
- data/sig/prosemirror.rbs +4 -0
- data/spec/fixtures/ituob-1000/ituob-1000-DP.json +366 -0
- data/spec/fixtures/ituob-1000/ituob-1000-DP.yaml +182 -0
- data/spec/fixtures/ituob-1000/ituob-1000-E118_IIN.json +381 -0
- data/spec/fixtures/ituob-1000/ituob-1000-E118_IIN.yaml +182 -0
- data/spec/fixtures/ituob-1000/ituob-1000-E164_ACN.json +203 -0
- data/spec/fixtures/ituob-1000/ituob-1000-E164_ACN.yaml +98 -0
- data/spec/fixtures/ituob-1000/ituob-1000-E212_MNC.json +202 -0
- data/spec/fixtures/ituob-1000/ituob-1000-E212_MNC.yaml +101 -0
- data/spec/fixtures/ituob-1000/ituob-1000-F32_TDI.json +6948 -0
- data/spec/fixtures/ituob-1000/ituob-1000-F32_TDI.yaml +3519 -0
- data/spec/fixtures/ituob-1000/ituob-1000-M1400_ICC.json +529 -0
- data/spec/fixtures/ituob-1000/ituob-1000-M1400_ICC.yaml +263 -0
- data/spec/fixtures/ituob-1000/ituob-1000-NNP.json +288 -0
- data/spec/fixtures/ituob-1000/ituob-1000-NNP.yaml +152 -0
- data/spec/fixtures/ituob-1000/ituob-1000-Q708_ISPC.json +1534 -0
- data/spec/fixtures/ituob-1000/ituob-1000-Q708_ISPC.yaml +789 -0
- data/spec/fixtures/ituob-1000/ituob-1000-Q708_SANC.json +252 -0
- data/spec/fixtures/ituob-1000/ituob-1000-Q708_SANC.yaml +123 -0
- data/spec/fixtures/ituob-1000/ituob-1000-R_SP_LM.V.json +428 -0
- data/spec/fixtures/ituob-1000/ituob-1000-R_SP_LM.V.yaml +208 -0
- data/spec/fixtures/ituob-1000/ituob-1000-T35_NA.json +621 -0
- data/spec/fixtures/ituob-1000/ituob-1000-T35_NA.yaml +317 -0
- data/spec/fixtures/ituob-1001/ituob-1001-DP.json +532 -0
- data/spec/fixtures/ituob-1001/ituob-1001-DP.yaml +266 -0
- data/spec/fixtures/ituob-1001/ituob-1001-E118_IIN.json +1093 -0
- data/spec/fixtures/ituob-1001/ituob-1001-E118_IIN.yaml +519 -0
- data/spec/fixtures/ituob-1001/ituob-1001-E164_ACN.json +449 -0
- data/spec/fixtures/ituob-1001/ituob-1001-E164_ACN.yaml +214 -0
- data/spec/fixtures/ituob-1001/ituob-1001-E164_CC.json +271 -0
- data/spec/fixtures/ituob-1001/ituob-1001-E164_CC.yaml +136 -0
- data/spec/fixtures/ituob-1001/ituob-1001-E212_MNC.json +199 -0
- data/spec/fixtures/ituob-1001/ituob-1001-E212_MNC.yaml +99 -0
- data/spec/fixtures/ituob-1001/ituob-1001-NNP.json +288 -0
- data/spec/fixtures/ituob-1001/ituob-1001-NNP.yaml +152 -0
- data/spec/fixtures/ituob-1001/ituob-1001-Q708_ISPC.json +960 -0
- data/spec/fixtures/ituob-1001/ituob-1001-Q708_ISPC.yaml +487 -0
- data/spec/prosereflect/document_spec.rb +149 -0
- data/spec/prosereflect/hard_break_spec.rb +51 -0
- data/spec/prosereflect/node_spec.rb +256 -0
- data/spec/prosereflect/paragraph_spec.rb +152 -0
- data/spec/prosereflect/parser_spec.rb +129 -0
- data/spec/prosereflect/table_cell_spec.rb +114 -0
- data/spec/prosereflect/table_row_spec.rb +77 -0
- data/spec/prosereflect/table_spec.rb +144 -0
- data/spec/prosereflect/text_spec.rb +116 -0
- data/spec/prosereflect/version_spec.rb +11 -0
- data/spec/prosereflect_spec.rb +57 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/matchers.rb +131 -0
- data/spec/support/shared_examples.rb +220 -0
- 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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|