spec_views 1.1.0 → 3.0.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 +4 -4
- data/README.md +111 -5
- data/Rakefile +3 -1
- data/app/assets/config/spec_views_manifest.js +1 -0
- data/app/assets/javascripts/spec_views/diff.js +60 -0
- data/app/assets/javascripts/spec_views/jsdiff.js +1055 -0
- data/app/controllers/spec_views/views_controller.rb +35 -87
- data/app/models/spec_views/base_matcher.rb +65 -0
- data/app/models/spec_views/capybara_session_extractor.rb +30 -0
- data/app/models/spec_views/directory.rb +140 -0
- data/app/models/spec_views/html_matcher.rb +41 -0
- data/app/models/spec_views/http_response_extractor.rb +30 -0
- data/app/models/spec_views/mail_message_extractor.rb +31 -0
- data/app/models/spec_views/pdf_matcher.rb +53 -0
- data/app/models/spec_views/view_sanitizer.rb +20 -0
- data/app/views/layouts/spec_views.html.erb +261 -0
- data/app/views/spec_views/views/_actions.html.erb +11 -0
- data/app/views/spec_views/views/_directory_footer.html.erb +14 -0
- data/app/views/spec_views/views/compare.html.erb +11 -0
- data/app/views/spec_views/views/diff.html.erb +16 -0
- data/app/views/spec_views/views/index.html.erb +48 -0
- data/app/views/spec_views/views/preview.html.erb +32 -0
- data/config/routes.rb +3 -0
- data/lib/spec_views/configuration.rb +3 -2
- data/lib/spec_views/engine.rb +10 -0
- data/lib/spec_views/support.rb +61 -170
- data/lib/spec_views/version.rb +3 -1
- data/lib/spec_views.rb +3 -1
- data/lib/tasks/spec_views_tasks.rake +1 -0
- metadata +28 -15
- data/app/views/layouts/spec_views.html.haml +0 -200
- data/app/views/spec_views/views/compare.html.haml +0 -16
- data/app/views/spec_views/views/index.html.haml +0 -29
- data/app/views/spec_views/views/preview.html.haml +0 -19
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module SpecViews
|
4
4
|
class ViewsController < ApplicationController
|
5
|
-
skip_authorization_check
|
5
|
+
skip_authorization_check if respond_to?(:skip_authorization_check)
|
6
6
|
layout 'spec_views'
|
7
7
|
|
8
8
|
def index
|
@@ -19,16 +19,16 @@ module SpecViews
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def show
|
22
|
-
|
23
|
-
|
24
|
-
if pdf?
|
22
|
+
path = directory.champion_path
|
23
|
+
path = directory.challenger_path if params[:view] == 'challenger'
|
24
|
+
if directory.content_type.pdf?
|
25
25
|
send_data(
|
26
|
-
get_view(
|
26
|
+
get_view(path),
|
27
27
|
filename: 'a.pdf',
|
28
28
|
type: 'application/pdf', disposition: 'inline'
|
29
29
|
)
|
30
30
|
else
|
31
|
-
render html: get_view(
|
31
|
+
render html: get_view(path)
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
@@ -36,15 +36,25 @@ module SpecViews
|
|
36
36
|
@directory = directory
|
37
37
|
end
|
38
38
|
|
39
|
+
def diff
|
40
|
+
if directory.content_type.pdf?
|
41
|
+
redirect_to action: :compare
|
42
|
+
return
|
43
|
+
end
|
44
|
+
@champion = get_view(directory.champion_path, html_safe: false)
|
45
|
+
@challenger = get_view(directory.challenger_path, html_safe: false)
|
46
|
+
@directory = directory
|
47
|
+
end
|
48
|
+
|
39
49
|
def preview
|
40
50
|
@directory = directory
|
41
51
|
@next = directories[directories.index(@directory) + 1]
|
42
52
|
index = directories.index(@directory)
|
43
|
-
@previous = directories[index - 1] if index
|
53
|
+
@previous = directories[index - 1] if index.positive?
|
44
54
|
end
|
45
55
|
|
46
56
|
def accept
|
47
|
-
accept_directory(
|
57
|
+
accept_directory(directory)
|
48
58
|
redirect_to action: :index, challenger: :next
|
49
59
|
end
|
50
60
|
|
@@ -56,8 +66,7 @@ module SpecViews
|
|
56
66
|
end
|
57
67
|
|
58
68
|
def reject
|
59
|
-
|
60
|
-
FileUtils.remove_file(challenger)
|
69
|
+
FileUtils.remove_file(directory.challenger_path)
|
61
70
|
redirect_to action: :index, challenger: :next
|
62
71
|
end
|
63
72
|
|
@@ -72,66 +81,20 @@ module SpecViews
|
|
72
81
|
|
73
82
|
private
|
74
83
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
def initialize(path)
|
79
|
-
@path = path
|
80
|
-
end
|
81
|
-
|
82
|
-
def basename
|
83
|
-
path.basename
|
84
|
-
end
|
85
|
-
|
86
|
-
def to_param
|
87
|
-
basename.to_s
|
88
|
-
end
|
89
|
-
|
90
|
-
def name
|
91
|
-
basename
|
92
|
-
end
|
93
|
-
|
94
|
-
def challenger?
|
95
|
-
File.file?(path.join('challenger.html')) || File.file?(path.join('challenger.pdf'))
|
96
|
-
end
|
97
|
-
|
98
|
-
def controller_name
|
99
|
-
splitted_name.first.gsub(/Controller(_.*)$/, 'Controller').gsub(/Controller$/, '').gsub('_', '::')
|
100
|
-
end
|
101
|
-
|
102
|
-
def method
|
103
|
-
splitted_name.second
|
104
|
-
end
|
105
|
-
|
106
|
-
def description
|
107
|
-
splitted_name.third.humanize
|
108
|
-
end
|
109
|
-
|
110
|
-
def last_run
|
111
|
-
@last_run ||= begin
|
112
|
-
Time.zone.parse(File.read(path.join('last_run.txt')))
|
113
|
-
rescue Errno::ENOENT
|
114
|
-
Time.zone.at(0)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
def delete!
|
119
|
-
FileUtils.remove_dir(path)
|
84
|
+
def directories
|
85
|
+
@directories ||= Pathname.new(directory_path).children.select(&:directory?).map do |path|
|
86
|
+
SpecViews::Directory.new(path)
|
120
87
|
end
|
88
|
+
latest_run = @directories.map(&:last_run).max
|
121
89
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
90
|
+
@directories.sort_by! do |dir|
|
91
|
+
prefix = '3-'
|
92
|
+
prefix = '2-' if dir.last_run < latest_run
|
93
|
+
prefix = '1-' if dir.challenger?
|
94
|
+
"#{prefix}#{dir.basename}"
|
126
95
|
end
|
127
96
|
end
|
128
97
|
|
129
|
-
def directories
|
130
|
-
@directories ||= Pathname.new(directory_path).children.select(&:directory?).map do |path|
|
131
|
-
Directory.new(path)
|
132
|
-
end.sort_by(&:basename)
|
133
|
-
end
|
134
|
-
|
135
98
|
def directory_path
|
136
99
|
Rails.root.join(Rails.configuration.spec_views.directory)
|
137
100
|
end
|
@@ -140,32 +103,17 @@ module SpecViews
|
|
140
103
|
directories.detect { |dir| dir.to_param == params[:id] }
|
141
104
|
end
|
142
105
|
|
143
|
-
def get_view(
|
144
|
-
|
145
|
-
|
106
|
+
def get_view(path, html_safe: true)
|
107
|
+
content = File.read(path)
|
108
|
+
content = content.html_safe if html_safe
|
109
|
+
content
|
146
110
|
rescue Errno::ENOENT
|
147
111
|
''
|
148
112
|
end
|
149
113
|
|
150
|
-
def
|
151
|
-
|
152
|
-
|
153
|
-
directory_path.join(id, filename)
|
154
|
-
end
|
155
|
-
|
156
|
-
def filename(base = 'view')
|
157
|
-
pdf? ? "#{base}.pdf" : "#{base}.html"
|
158
|
-
end
|
159
|
-
|
160
|
-
def pdf?
|
161
|
-
params[:id] && params[:id].end_with?('__pdf')
|
162
|
-
end
|
163
|
-
|
164
|
-
def accept_directory(id)
|
165
|
-
champion = file_path(filename('view'), id: id)
|
166
|
-
challenger = file_path(filename('challenger'), id: id)
|
167
|
-
FileUtils.copy_file(challenger, champion)
|
168
|
-
FileUtils.remove_file(challenger)
|
114
|
+
def accept_directory(dir)
|
115
|
+
FileUtils.copy_file(dir.challenger_path, dir.champion_path)
|
116
|
+
FileUtils.remove_file(dir.challenger_path)
|
169
117
|
end
|
170
118
|
end
|
171
119
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class BaseMatcher
|
5
|
+
attr_reader :response, :directory, :failure_message, :sanitizer, :type
|
6
|
+
|
7
|
+
delegate :champion_html, to: :directory
|
8
|
+
|
9
|
+
def initialize(response, description, run_time:, expected_status: :ok, sanitizer: nil, type: :request)
|
10
|
+
@response = response
|
11
|
+
@type = type
|
12
|
+
@sanitizer = sanitizer
|
13
|
+
@extractor = extractor_class.new(response, expected_status: expected_status)
|
14
|
+
@directory = SpecViews::Directory.for_description(description, content_type: content_type)
|
15
|
+
@extractor_failure = @extractor.extractor_failure?
|
16
|
+
@match = !extractor_failure? && match_challenger
|
17
|
+
directory.write_meta(description, run_time, type, content_type) if champion_html
|
18
|
+
return if match?
|
19
|
+
|
20
|
+
if extractor_failure?
|
21
|
+
@failure_message = @extractor.failure_message
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
@failure_message = "#{subject_name} has changed."
|
26
|
+
@failure_message = "#{subject_name} has been added." if champion_html.nil?
|
27
|
+
|
28
|
+
directory.write_meta(description, run_time, type, content_type)
|
29
|
+
@directory.write_challenger(challenger_body)
|
30
|
+
end
|
31
|
+
|
32
|
+
def match?
|
33
|
+
@match
|
34
|
+
end
|
35
|
+
|
36
|
+
def extractor_failure?
|
37
|
+
@extractor_failure
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def subject_name
|
43
|
+
raise NotImplementedError
|
44
|
+
end
|
45
|
+
|
46
|
+
def challenger_body
|
47
|
+
raise NotImplementedError
|
48
|
+
end
|
49
|
+
|
50
|
+
def match_challenger
|
51
|
+
raise NotImplementedError
|
52
|
+
end
|
53
|
+
|
54
|
+
def content_type
|
55
|
+
raise NotImplementedError
|
56
|
+
end
|
57
|
+
|
58
|
+
def extractor_class
|
59
|
+
return CapybaraSessionExtractor if type == :feature
|
60
|
+
return MailMessageExtractor if type == :mailer
|
61
|
+
|
62
|
+
HttpResponseExtractor
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class CapybaraSessionExtractor
|
5
|
+
attr_reader :page, :failure_message
|
6
|
+
|
7
|
+
def initialize(page, expected_status:)
|
8
|
+
@page = page
|
9
|
+
@status_match = response_status_match?(page, expected_status)
|
10
|
+
return if @status_match
|
11
|
+
|
12
|
+
@failure_message = "Unexpected response status #{page.status_code}."
|
13
|
+
end
|
14
|
+
|
15
|
+
def extractor_failure?
|
16
|
+
failure_message.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
def body
|
20
|
+
page.source
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def response_status_match?(page, expected_status)
|
26
|
+
page.status_code == expected_status ||
|
27
|
+
Rack::Utils::SYMBOL_TO_STATUS_CODE.invert[page.status_code] == expected_status.to_sym
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class Directory
|
5
|
+
attr_reader :path
|
6
|
+
|
7
|
+
def self.for_description(description, content_type: :html)
|
8
|
+
dir_name = description.strip.gsub(/[^0-9A-Za-z.\-]/, '_').gsub('__', '_')
|
9
|
+
new(Rails.root.join(Rails.configuration.spec_views.directory, dir_name), content_type)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(path, content_type = nil)
|
13
|
+
@path = path
|
14
|
+
@content_type = ActiveSupport::StringInquirer.new(content_type.to_s) if content_type
|
15
|
+
end
|
16
|
+
|
17
|
+
def basename
|
18
|
+
path.basename
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_param
|
22
|
+
basename.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
basename
|
27
|
+
end
|
28
|
+
|
29
|
+
def controller_name
|
30
|
+
splitted_description.first.gsub(/Controller(_.*)$/, 'Controller').gsub(/Controller$/, '').gsub('_', '::')
|
31
|
+
end
|
32
|
+
|
33
|
+
def method
|
34
|
+
splitted_description.second[0, 4]
|
35
|
+
end
|
36
|
+
|
37
|
+
def description_tail
|
38
|
+
splitted_description.third
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete!
|
42
|
+
FileUtils.remove_dir(path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def champion_path
|
46
|
+
path.join("view.#{file_extension}")
|
47
|
+
end
|
48
|
+
|
49
|
+
def champion_html
|
50
|
+
File.read(champion_path)
|
51
|
+
rescue Errno::ENOENT
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def challenger_path
|
56
|
+
path.join("challenger.#{file_extension}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def challenger?
|
60
|
+
File.file?(challenger_path)
|
61
|
+
end
|
62
|
+
|
63
|
+
def write_challenger(content)
|
64
|
+
FileUtils.mkdir_p(path)
|
65
|
+
File.open(challenger_path, binary? ? 'wb' : 'w') { |f| f.write(content) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def meta_path
|
69
|
+
path.join('meta.txt')
|
70
|
+
end
|
71
|
+
|
72
|
+
def write_meta(description, run_time, spec_type, content_type)
|
73
|
+
FileUtils.mkdir_p(path)
|
74
|
+
lines = [
|
75
|
+
description.to_s.gsub("\n", ' '),
|
76
|
+
run_time.to_s,
|
77
|
+
spec_type.to_s,
|
78
|
+
content_type.to_s
|
79
|
+
]
|
80
|
+
File.write(meta_path, lines.join("\n"))
|
81
|
+
end
|
82
|
+
|
83
|
+
def meta
|
84
|
+
@meta ||= File.read(meta_path).lines.map(&:strip)
|
85
|
+
end
|
86
|
+
|
87
|
+
def description
|
88
|
+
meta[0]
|
89
|
+
rescue Errno::ENOENT
|
90
|
+
name
|
91
|
+
end
|
92
|
+
|
93
|
+
def last_run
|
94
|
+
Time.zone.parse(meta[1])
|
95
|
+
rescue Errno::ENOENT
|
96
|
+
Time.zone.at(0)
|
97
|
+
end
|
98
|
+
|
99
|
+
def spec_type
|
100
|
+
ActiveSupport::StringInquirer.new(meta[2])
|
101
|
+
rescue Errno::ENOENT
|
102
|
+
:response
|
103
|
+
end
|
104
|
+
|
105
|
+
def content_type
|
106
|
+
@content_type ||= begin
|
107
|
+
ActiveSupport::StringInquirer.new(meta[3])
|
108
|
+
rescue Errno::ENOENT
|
109
|
+
:response
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def binary?
|
114
|
+
content_type.pdf?
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def splitted_description
|
120
|
+
@splitted_description ||= begin
|
121
|
+
if spec_type.feature?
|
122
|
+
split = ['', 'FEAT', description]
|
123
|
+
elsif spec_type.mailer?
|
124
|
+
split = description.to_s.match(/(\A[a-zA-Z0-9:]+)(Mailer)(.*)/)
|
125
|
+
split = split[1..3]
|
126
|
+
split[0] += 'Mailer'
|
127
|
+
split[1] = 'MAIL'
|
128
|
+
else
|
129
|
+
split = description.to_s.split(/\s(DELETE|GET|PATCH|POST|PUT)\s/)
|
130
|
+
end
|
131
|
+
split = ['', '', split[0]] if split.size == 1
|
132
|
+
split
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def file_extension
|
137
|
+
content_type.pdf? ? 'pdf' : 'html'
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class HtmlMatcher < BaseMatcher
|
5
|
+
delegate :champion_html, to: :directory
|
6
|
+
|
7
|
+
protected
|
8
|
+
|
9
|
+
def subject_name
|
10
|
+
'View'
|
11
|
+
end
|
12
|
+
|
13
|
+
def content_type
|
14
|
+
:html
|
15
|
+
end
|
16
|
+
|
17
|
+
def challenger_body
|
18
|
+
sanitized_body
|
19
|
+
end
|
20
|
+
|
21
|
+
def match_challenger
|
22
|
+
champion_html == challenger_body
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def sanitized_body
|
28
|
+
body = remove_pack_digests_from_body(remove_digests_from_body(@extractor.body))
|
29
|
+
body = sanitizer.sanitize(body) if sanitizer
|
30
|
+
body
|
31
|
+
end
|
32
|
+
|
33
|
+
def remove_digests_from_body(body)
|
34
|
+
body.gsub(/(-[a-z0-9]{64})(\.css|\.js|\.ico|\.png|\.jpg|\.jpeg|\.svg|\.gif)/, '\2')
|
35
|
+
end
|
36
|
+
|
37
|
+
def remove_pack_digests_from_body(body)
|
38
|
+
body.gsub(%r{(packs.*/js/[a-z0-9_]+)(-[a-z0-9]{20})(\.js)}, '\1\3')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class HttpResponseExtractor
|
5
|
+
attr_reader :response, :failure_message
|
6
|
+
|
7
|
+
def initialize(response, expected_status:)
|
8
|
+
@response = response
|
9
|
+
@status_match = response_status_match?(response, expected_status)
|
10
|
+
return if @status_match
|
11
|
+
|
12
|
+
@failure_message = "Unexpected response status #{response.status}."
|
13
|
+
end
|
14
|
+
|
15
|
+
def extractor_failure?
|
16
|
+
failure_message.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
def body
|
20
|
+
response.body
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def response_status_match?(response, expected_status)
|
26
|
+
response.status == expected_status ||
|
27
|
+
response.message.parameterize.underscore == expected_status.to_s
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class MailMessageExtractor
|
5
|
+
attr_reader :mail, :failure_message, :body
|
6
|
+
|
7
|
+
def initialize(mail, expected_status:)
|
8
|
+
@mail = mail
|
9
|
+
@body = extract_body(mail)
|
10
|
+
return if body.present?
|
11
|
+
|
12
|
+
@failure_message = 'Failed to find mail part'
|
13
|
+
end
|
14
|
+
|
15
|
+
def extractor_failure?
|
16
|
+
failure_message.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
def extract_body(part)
|
20
|
+
return part.raw_source if part.respond_to?(:raw_source) && part.raw_source.present?
|
21
|
+
return extract_body(part.body) if part.respond_to?(:body)
|
22
|
+
return part if part.is_a?(String)
|
23
|
+
|
24
|
+
return if !part.respond_to?(:parts) && nil
|
25
|
+
|
26
|
+
part.parts.map do |inner_part|
|
27
|
+
extract_body(inner_part)
|
28
|
+
end.compact.first
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class PdfMatcher < BaseMatcher
|
5
|
+
protected
|
6
|
+
|
7
|
+
def subject_name
|
8
|
+
'PDF'
|
9
|
+
end
|
10
|
+
|
11
|
+
def content_type
|
12
|
+
:pdf
|
13
|
+
end
|
14
|
+
|
15
|
+
def challenger_body
|
16
|
+
sanitized_body
|
17
|
+
end
|
18
|
+
|
19
|
+
def match_challenger
|
20
|
+
champion_hash == challenger_hash
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def champion_hash
|
26
|
+
@champion_hash ||= begin
|
27
|
+
Digest::MD5.file(directory.champion_path).hexdigest
|
28
|
+
rescue Errno::ENOENT
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def challenger_hash
|
34
|
+
@challenger_hash ||= Digest::MD5.hexdigest(sanitized_body)
|
35
|
+
end
|
36
|
+
|
37
|
+
def sanitized_body
|
38
|
+
@sanitized_body ||= begin
|
39
|
+
body = remove_headers_from_pdf(@extractor.body)
|
40
|
+
body = sanitizer.sanitize(body) if sanitizer
|
41
|
+
body
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def remove_headers_from_pdf(body)
|
46
|
+
body
|
47
|
+
.force_encoding('BINARY')
|
48
|
+
.gsub(%r{^/CreationDate \(.*\)$}, '')
|
49
|
+
.gsub(%r{^/ModDate \(.*\)$}, '')
|
50
|
+
.gsub(%r{^/ID \[<.+><.+>\]$}, '')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module SpecViews
|
2
|
+
class ViewSanitizer
|
3
|
+
delegate :each, to: :@sanitizers
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@sanitizers = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def gsub(*args, &block)
|
10
|
+
@sanitizers << [:gsub, args, block]
|
11
|
+
end
|
12
|
+
|
13
|
+
def sanitize(string)
|
14
|
+
each do |sanitize_method, args, block|
|
15
|
+
string = string.send(sanitize_method, *args, &block)
|
16
|
+
end
|
17
|
+
string
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|