html_mockup 0.6.5 → 0.7.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.
- data/CHANGELOG.md +8 -0
- data/html_mockup.gemspec +1 -1
- data/lib/html_mockup/extractor.rb +31 -62
- data/lib/html_mockup/rack/html_mockup.rb +17 -22
- data/lib/html_mockup/release/scm/git.rb +2 -2
- data/lib/html_mockup/resolver.rb +95 -0
- data/lib/html_mockup/server.rb +1 -1
- data/lib/html_mockup/template.rb +18 -23
- metadata +11 -10
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## Version 0.7.0
|
4
|
+
* Replace --quiet with -s in as it's no longer supported in newer GIT versions
|
5
|
+
* Add support for ENV passing to the partials
|
6
|
+
* Add support for single file processing and env passing in the extractor (release)
|
7
|
+
* Refactor path and url resolving
|
8
|
+
* Allow `.html` files to be processed by ERB (both in release and serve)
|
9
|
+
* Pass "MOCKUP_PROJECT" variable to env (both in release and serve)
|
10
|
+
|
3
11
|
## Version 0.6.5
|
4
12
|
* Allow disabling of URL relativizing in the extractor with `release.extract :url_relativize => false`
|
5
13
|
* Add missing Hpricot dependency to gem
|
data/html_mockup.gemspec
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'hpricot'
|
2
|
+
require File.dirname(__FILE__) + '/resolver'
|
2
3
|
|
3
4
|
module HtmlMockup
|
4
5
|
class Extractor
|
@@ -12,21 +13,27 @@ module HtmlMockup
|
|
12
13
|
|
13
14
|
# @option options [Array] :url_attributes The element attributes to parse and relativize
|
14
15
|
# @option options [Array] :url_relativize Wether or not we should relativize
|
16
|
+
# @option options [Array] :env ENV variable to pass to template renderer.
|
15
17
|
def initialize(project, target_path, options={})
|
16
18
|
@project = project
|
17
19
|
@target_path = Pathname.new(target_path)
|
20
|
+
@resolver = Resolver.new(self.target_path)
|
21
|
+
|
18
22
|
|
19
23
|
@options = {
|
20
24
|
:url_attributes => %w{src href action},
|
21
|
-
:url_relativize => true
|
25
|
+
:url_relativize => true,
|
26
|
+
:env => {}
|
22
27
|
}
|
23
28
|
|
24
29
|
@options.update(options) if options
|
30
|
+
|
31
|
+
env.update("MOCKUP_PROJECT" => project)
|
25
32
|
end
|
26
33
|
|
27
34
|
def run!
|
28
35
|
target_path = self.target_path
|
29
|
-
source_path
|
36
|
+
source_path = self.project.html_path
|
30
37
|
|
31
38
|
|
32
39
|
filter = "**/*.html"
|
@@ -39,86 +46,48 @@ module HtmlMockup
|
|
39
46
|
cp_r(source_path.children, target_path)
|
40
47
|
|
41
48
|
Dir.chdir(source_path) do
|
42
|
-
Dir.glob(filter).each do |
|
43
|
-
|
44
|
-
|
45
|
-
if @options[:url_relativize]
|
46
|
-
source = relativize_urls(source, file_name)
|
47
|
-
end
|
48
|
-
|
49
|
-
File.open(target_path + file_name,"w"){|f| f.write(source) }
|
49
|
+
Dir.glob(filter).each do |file_path|
|
50
|
+
self.run_on_file!(file_path, @options[:env])
|
50
51
|
end
|
51
52
|
end
|
52
53
|
end
|
54
|
+
|
55
|
+
def run_on_file!(file_path, env = {})
|
56
|
+
source = self.extract_source_from_file(file_path, env)
|
57
|
+
File.open(target_path + file_path,"w"){|f| f.write(source) }
|
58
|
+
end
|
59
|
+
|
60
|
+
# Runs the extractor on a single file and return processed source.
|
61
|
+
def extract_source_from_file(file_path, env = {})
|
62
|
+
source = HtmlMockup::Template.open(file_path, :partial_path => self.project.partial_path).render(env)
|
63
|
+
|
64
|
+
if @options[:url_relativize]
|
65
|
+
source = relativize_urls(source, file_path)
|
66
|
+
end
|
53
67
|
|
68
|
+
source
|
69
|
+
end
|
70
|
+
|
54
71
|
|
55
72
|
protected
|
56
73
|
|
57
|
-
def relativize_urls(source,
|
58
|
-
cur_dir = Pathname.new(file_name).dirname
|
59
|
-
up_to_root = File.join([".."] * (file_name.split("/").size - 1))
|
60
|
-
|
74
|
+
def relativize_urls(source, file_path)
|
61
75
|
doc = Hpricot(source)
|
62
76
|
@options[:url_attributes].each do |attribute|
|
63
77
|
(doc/"*[@#{attribute}]").each do |tag|
|
64
|
-
converted_url =
|
78
|
+
converted_url = @resolver.url_to_relative_url(tag[attribute], file_path)
|
65
79
|
|
66
80
|
case converted_url
|
67
81
|
when String
|
68
82
|
tag[attribute] = converted_url
|
69
83
|
when nil
|
70
|
-
puts "Could not resolve link #{tag[attribute]} in #{
|
84
|
+
puts "Could not resolve link #{tag[attribute]} in #{file_path}"
|
71
85
|
end
|
72
86
|
end
|
73
87
|
end
|
74
88
|
|
75
89
|
doc.to_original_html
|
76
90
|
end
|
77
|
-
|
78
|
-
# @return [false, nil, String] False if it can't be converted, nil if it can't be resolved and the converted string if it can be resolved.
|
79
|
-
def convert_relative_url_to_absolute_url(url, cur_dir, up_to_root)
|
80
|
-
# Skip if the url doesn't start with a / (but not with //)
|
81
|
-
return false unless url =~ /\A\/[^\/]/
|
82
|
-
|
83
|
-
# Strip off anchors
|
84
|
-
anchor = nil
|
85
|
-
url.gsub!(/(#.+)\Z/) do |r|
|
86
|
-
anchor = r
|
87
|
-
""
|
88
|
-
end
|
89
|
-
|
90
|
-
# Strip off query strings
|
91
|
-
query = nil
|
92
|
-
url.gsub!(/(\?.+)\Z/) do |r|
|
93
|
-
query = r
|
94
|
-
""
|
95
|
-
end
|
96
|
-
|
97
|
-
if true_file = resolve_path(cur_dir + up_to_root + url.sub(/\A\//,""))
|
98
|
-
url = true_file.relative_path_from(cur_dir).to_s
|
99
|
-
url += query if query
|
100
|
-
url += anchor if anchor
|
101
|
-
url
|
102
|
-
else
|
103
|
-
nil
|
104
|
-
end
|
105
|
-
|
106
|
-
end
|
107
|
-
|
108
|
-
def resolve_path(path)
|
109
|
-
path = Pathname.new(path) unless path.kind_of?(Pathname)
|
110
|
-
# Append index.html/index.htm/index.rhtml if it's a diretory
|
111
|
-
if path.directory?
|
112
|
-
search_files = %w{.html .htm}.map!{|p| path + "index#{p}" }
|
113
|
-
# If it ends with a slash or does not contain a . and it's not a directory
|
114
|
-
# try to add .html/.htm to see if that exists.
|
115
|
-
elsif (path.to_s =~ /\/$/) || (path.to_s =~ /^[^.]+$/)
|
116
|
-
search_files = [path.to_s + ".html", path.to_s + ".htm"].map!{|p| Pathname.new(p) }
|
117
|
-
else
|
118
|
-
search_files = [path]
|
119
|
-
end
|
120
|
-
search_files.find{|p| p.exist? }
|
121
|
-
end
|
122
|
-
|
91
|
+
|
123
92
|
end
|
124
93
|
end
|
@@ -2,42 +2,37 @@ require 'rack/request'
|
|
2
2
|
require 'rack/response'
|
3
3
|
require 'rack/file'
|
4
4
|
|
5
|
+
require File.dirname(__FILE__) + '/../resolver'
|
6
|
+
|
5
7
|
module HtmlMockup
|
6
8
|
module Rack
|
9
|
+
|
7
10
|
class HtmlMockup
|
8
|
-
|
11
|
+
|
12
|
+
attr_reader :project
|
13
|
+
|
14
|
+
def initialize(project)
|
15
|
+
@project = project
|
16
|
+
root,partial_path = project.html_path, project.partial_path
|
17
|
+
|
9
18
|
@docroot = root
|
10
19
|
@partial_path = partial_path
|
11
20
|
@file_server = ::Rack::File.new(@docroot)
|
12
21
|
end
|
13
22
|
|
14
23
|
def call(env)
|
15
|
-
|
16
|
-
|
17
|
-
# TODO: Combine with Extractor#resolve_path
|
24
|
+
url = env["PATH_INFO"]
|
25
|
+
env["MOCKUP_PROJECT"] = project
|
18
26
|
|
19
|
-
|
20
|
-
if File.directory?(File.join(@docroot,path))
|
21
|
-
search_files = %w{.html .htm}.map!{|p| File.join(@docroot,path,"index#{p}")}
|
22
|
-
# If it's already a .html/.htm file, render that file
|
23
|
-
elsif (path =~ /\.html?$/)
|
24
|
-
search_files = [File.join(@docroot,path)]
|
25
|
-
# If it ends with a slash or does not contain a . and it's not a directory
|
26
|
-
# try to add .html/.htm to see if that exists.
|
27
|
-
elsif (path =~ /\/$/) || (path =~ /^[^.]+$/)
|
28
|
-
search_files = [path + ".html", path + ".htm"].map!{|p| File.join(@docroot,p) }
|
29
|
-
# Otherwise don't render anything at all.
|
30
|
-
else
|
31
|
-
search_files = []
|
32
|
-
end
|
27
|
+
resolver = Resolver.new(@docroot)
|
33
28
|
|
34
|
-
if template_path =
|
35
|
-
env["rack.errors"].puts "Rendering template #{template_path.inspect} (#{
|
29
|
+
if template_path = resolver.url_to_path(url)
|
30
|
+
env["rack.errors"].puts "Rendering template #{template_path.inspect} (#{url.inspect})"
|
36
31
|
begin
|
37
32
|
templ = ::HtmlMockup::Template.open(template_path, :partial_path => @partial_path)
|
38
33
|
resp = ::Rack::Response.new do |res|
|
39
34
|
res.status = 200
|
40
|
-
res.write templ.render
|
35
|
+
res.write templ.render(env)
|
41
36
|
end
|
42
37
|
resp.finish
|
43
38
|
rescue StandardError => e
|
@@ -49,7 +44,7 @@ module HtmlMockup
|
|
49
44
|
resp.finish
|
50
45
|
end
|
51
46
|
else
|
52
|
-
env["rack.errors"].puts "Invoking file handler for #{
|
47
|
+
env["rack.errors"].puts "Invoking file handler for #{url.inspect}"
|
53
48
|
@file_server.call(env)
|
54
49
|
end
|
55
50
|
end
|
@@ -61,7 +61,7 @@ module HtmlMockup::Release::Scm
|
|
61
61
|
|
62
62
|
if $?.to_i > 0
|
63
63
|
# HEAD is not a tagged verison, get the short SHA1 instead
|
64
|
-
@_version = `git --git-dir=#{git_dir} show #{ref} --format=format:"%h"
|
64
|
+
@_version = `git --git-dir=#{git_dir} show #{ref} --format=format:"%h" -s 2>&1`
|
65
65
|
else
|
66
66
|
# HEAD is a tagged version, if version is prefixed with "v" it will be stripped off
|
67
67
|
@_version.gsub!(/^v/,"")
|
@@ -69,7 +69,7 @@ module HtmlMockup::Release::Scm
|
|
69
69
|
@_version.strip!
|
70
70
|
|
71
71
|
# Get the date in epoch time
|
72
|
-
date = `git --git-dir=#{git_dir} show #{ref} --format=format:"%ct"
|
72
|
+
date = `git --git-dir=#{git_dir} show #{ref} --format=format:"%ct" -s 2>&1`
|
73
73
|
if date =~ /\d+/
|
74
74
|
@_date = Time.at(date.to_i)
|
75
75
|
else
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module HtmlMockup
|
2
|
+
class Resolver
|
3
|
+
|
4
|
+
def initialize(path)
|
5
|
+
@base = Pathname.new(path)
|
6
|
+
end
|
7
|
+
|
8
|
+
def url_to_path(url, exact_match = false)
|
9
|
+
path, qs, anch = strip_query_string_and_anchor(url.to_s)
|
10
|
+
|
11
|
+
extensions = %w{html htm}
|
12
|
+
|
13
|
+
# Append index.extension if it's a diretory
|
14
|
+
if File.directory?(File.join(@base,path))
|
15
|
+
search_files = extensions.map{|p| File.join(@base,path,"index.#{p}")}
|
16
|
+
# If it's already a .extension file, return that file
|
17
|
+
elsif extensions.detect{|e| path =~ /\.#{e}\Z/ }
|
18
|
+
search_files = [File.join(@base,path)]
|
19
|
+
# If it ends with a slash or does not contain a . and it's not a directory
|
20
|
+
# try to add extenstions to see if that exists.
|
21
|
+
elsif (path =~ /\/$/) || (path =~ /^[^.]+$/)
|
22
|
+
search_files = extensions.map{|e| File.join(@base,"#{path}.#{e}") }
|
23
|
+
# Otherwise don't return anything at all.
|
24
|
+
else
|
25
|
+
if exact_match
|
26
|
+
search_files = [File.join(@base,path)]
|
27
|
+
else
|
28
|
+
search_files = []
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if file = search_files.find{|p| File.exist?(p) }
|
33
|
+
Pathname.new(file)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Convert a disk path on file to an url
|
39
|
+
def path_to_url(path, relative_to = nil)
|
40
|
+
|
41
|
+
path = Pathname.new(path).relative_path_from(@base).cleanpath
|
42
|
+
|
43
|
+
if relative_to
|
44
|
+
if relative_to.to_s =~ /\A\//
|
45
|
+
relative_to = Pathname.new(File.dirname(relative_to.to_s)).relative_path_from(@base).cleanpath
|
46
|
+
else
|
47
|
+
relative_to = Pathname.new(File.dirname(relative_to.to_s))
|
48
|
+
end
|
49
|
+
path = Pathname.new("/" + path.to_s).relative_path_from(Pathname.new("/" + relative_to.to_s))
|
50
|
+
path.to_s
|
51
|
+
else
|
52
|
+
"/" + path.to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
def url_to_relative_url(url, relative_to_path)
|
58
|
+
# Skip if the url doesn't start with a / (but not with //)
|
59
|
+
return false unless url =~ /\A\/[^\/]/
|
60
|
+
|
61
|
+
path, qs, anch = strip_query_string_and_anchor(url)
|
62
|
+
|
63
|
+
# Get disk path
|
64
|
+
if true_path = self.url_to_path(path, true)
|
65
|
+
path = self.path_to_url(true_path, relative_to_path)
|
66
|
+
path += qs if qs
|
67
|
+
path += anch if anch
|
68
|
+
path
|
69
|
+
else
|
70
|
+
false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def strip_query_string_and_anchor(url)
|
75
|
+
url = url.dup
|
76
|
+
|
77
|
+
# Strip off anchors
|
78
|
+
anchor = nil
|
79
|
+
url.gsub!(/(#.+)\Z/) do |r|
|
80
|
+
anchor = r
|
81
|
+
""
|
82
|
+
end
|
83
|
+
|
84
|
+
# Strip off query strings
|
85
|
+
query = nil
|
86
|
+
url.gsub!(/(\?.+)\Z/) do |r|
|
87
|
+
query = r
|
88
|
+
""
|
89
|
+
end
|
90
|
+
|
91
|
+
[url, query, anchor]
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
data/lib/html_mockup/server.rb
CHANGED
@@ -65,7 +65,7 @@ module HtmlMockup
|
|
65
65
|
return @app if @app
|
66
66
|
|
67
67
|
@stack.use Rack::HtmlValidator if self.options[:validate]
|
68
|
-
@stack.run Rack::HtmlMockup.new(self.project
|
68
|
+
@stack.run Rack::HtmlMockup.new(self.project)
|
69
69
|
|
70
70
|
@app = @stack
|
71
71
|
end
|
data/lib/html_mockup/template.rb
CHANGED
@@ -2,6 +2,7 @@ require 'pathname'
|
|
2
2
|
require 'strscan'
|
3
3
|
require 'erb'
|
4
4
|
require 'cgi'
|
5
|
+
require 'tilt'
|
5
6
|
|
6
7
|
module HtmlMockup
|
7
8
|
|
@@ -10,7 +11,7 @@ module HtmlMockup
|
|
10
11
|
class Template
|
11
12
|
|
12
13
|
class << self
|
13
|
-
def open(filename,options={})
|
14
|
+
def open(filename, options={})
|
14
15
|
raise "Unknown file #{filename}" unless File.exist?(filename)
|
15
16
|
self.new(File.read(filename),options.update(:target_file => filename))
|
16
17
|
end
|
@@ -39,18 +40,17 @@ module HtmlMockup
|
|
39
40
|
|
40
41
|
# Create a new HtmlMockupTemplate
|
41
42
|
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
# options<Hash>:: See options
|
43
|
+
# @param [String] source The template to parse
|
44
|
+
# @param [Hash] options See options
|
45
45
|
#
|
46
|
-
#
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
@
|
46
|
+
# @option options [String] partial_path Path where the partials reside (default: $0/../../partials)
|
47
|
+
def initialize(source, options={})
|
48
|
+
defaults = {
|
49
|
+
:partial_path => File.dirname(__FILE__) + "/../../partials/"
|
50
|
+
}
|
51
|
+
@source = source
|
52
|
+
@template = Tilt::ERBTemplate.new{ @source }
|
52
53
|
@options = defaults.update(options)
|
53
|
-
@scanner = StringScanner.new(@template)
|
54
54
|
raise "Partial path '#{self.options[:partial_path]}' not found" unless File.exist?(self.options[:partial_path])
|
55
55
|
end
|
56
56
|
|
@@ -65,6 +65,7 @@ module HtmlMockup
|
|
65
65
|
# String:: The rendered template
|
66
66
|
#--
|
67
67
|
def render(env={})
|
68
|
+
@scanner = StringScanner.new(@template.render(Object.new, :env => env))
|
68
69
|
out = ""
|
69
70
|
while (partial = self.parse_partial_tag!) do
|
70
71
|
tag,params,scanned = partial
|
@@ -73,7 +74,7 @@ module HtmlMockup
|
|
73
74
|
|
74
75
|
# scan until end of tag
|
75
76
|
current_content = self.scanner.scan_until(/<!-- \[STOP:#{tag}\] -->/)
|
76
|
-
out << (render_partial(tag, params) || current_content)
|
77
|
+
out << (render_partial(tag, params, env) || current_content)
|
77
78
|
end
|
78
79
|
out << scanner.rest
|
79
80
|
end
|
@@ -115,24 +116,18 @@ module HtmlMockup
|
|
115
116
|
unless self.available_partials[tag]
|
116
117
|
raise MissingPartial.new("Could not find partial '#{tag}' in partial path '#{@options[:partial_path]}'")
|
117
118
|
end
|
118
|
-
template =
|
119
|
-
context = TemplateContext.new(params
|
120
|
-
"\n" + template.
|
119
|
+
template = Tilt::ERBTemplate.new{ self.available_partials[tag] }
|
120
|
+
context = TemplateContext.new(params)
|
121
|
+
"\n" + template.render(context, :env => env).rstrip + "\n<!-- [STOP:#{tag}] -->"
|
121
122
|
end
|
122
123
|
|
123
124
|
class TemplateContext
|
124
125
|
# Params will be set as instance variables
|
125
|
-
def initialize(params
|
126
|
+
def initialize(params)
|
126
127
|
params.each do |k,v|
|
127
128
|
self.instance_variable_set("@#{k}",v)
|
128
129
|
end
|
129
|
-
|
130
|
-
@_env = env;
|
131
|
-
end
|
132
|
-
|
133
|
-
def env; @_env; end
|
134
|
-
|
135
|
-
def get_binding; binding(); end
|
130
|
+
end
|
136
131
|
end
|
137
132
|
|
138
133
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: html_mockup
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,11 +10,11 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2013-01-22 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: thor
|
17
|
-
requirement: &
|
17
|
+
requirement: &70345877984660 !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
20
|
- - ~>
|
@@ -22,10 +22,10 @@ dependencies:
|
|
22
22
|
version: 0.16.0
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
|
-
version_requirements: *
|
25
|
+
version_requirements: *70345877984660
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: rack
|
28
|
-
requirement: &
|
28
|
+
requirement: &70345877984180 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - ! '>='
|
@@ -33,10 +33,10 @@ dependencies:
|
|
33
33
|
version: 1.0.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
36
|
+
version_requirements: *70345877984180
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
38
|
name: tilt
|
39
|
-
requirement: &
|
39
|
+
requirement: &70345877983700 !ruby/object:Gem::Requirement
|
40
40
|
none: false
|
41
41
|
requirements:
|
42
42
|
- - ! '>='
|
@@ -44,10 +44,10 @@ dependencies:
|
|
44
44
|
version: '0'
|
45
45
|
type: :runtime
|
46
46
|
prerelease: false
|
47
|
-
version_requirements: *
|
47
|
+
version_requirements: *70345877983700
|
48
48
|
- !ruby/object:Gem::Dependency
|
49
49
|
name: hpricot
|
50
|
-
requirement: &
|
50
|
+
requirement: &70345877983200 !ruby/object:Gem::Requirement
|
51
51
|
none: false
|
52
52
|
requirements:
|
53
53
|
- - ! '>='
|
@@ -55,7 +55,7 @@ dependencies:
|
|
55
55
|
version: 0.6.4
|
56
56
|
type: :runtime
|
57
57
|
prerelease: false
|
58
|
-
version_requirements: *
|
58
|
+
version_requirements: *70345877983200
|
59
59
|
description:
|
60
60
|
email:
|
61
61
|
- info@digitpaint.nl
|
@@ -101,6 +101,7 @@ files:
|
|
101
101
|
- lib/html_mockup/release/processors/yuicompressor.rb
|
102
102
|
- lib/html_mockup/release/scm.rb
|
103
103
|
- lib/html_mockup/release/scm/git.rb
|
104
|
+
- lib/html_mockup/resolver.rb
|
104
105
|
- lib/html_mockup/server.rb
|
105
106
|
- lib/html_mockup/template.rb
|
106
107
|
- lib/html_mockup/w3c_validator.rb
|