core_ext 0.0.1
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/README.md +3 -0
- data/lib/core_ext/array/access.rb +76 -0
- data/lib/core_ext/array/conversions.rb +211 -0
- data/lib/core_ext/array/extract_options.rb +29 -0
- data/lib/core_ext/array/grouping.rb +116 -0
- data/lib/core_ext/array/inquiry.rb +17 -0
- data/lib/core_ext/array/prepend_and_append.rb +7 -0
- data/lib/core_ext/array/wrap.rb +46 -0
- data/lib/core_ext/array.rb +7 -0
- data/lib/core_ext/array_inquirer.rb +44 -0
- data/lib/core_ext/benchmark.rb +14 -0
- data/lib/core_ext/benchmarkable.rb +49 -0
- data/lib/core_ext/big_decimal/conversions.rb +14 -0
- data/lib/core_ext/big_decimal.rb +1 -0
- data/lib/core_ext/builder.rb +6 -0
- data/lib/core_ext/callbacks.rb +770 -0
- data/lib/core_ext/class/attribute.rb +128 -0
- data/lib/core_ext/class/attribute_accessors.rb +4 -0
- data/lib/core_ext/class/subclasses.rb +42 -0
- data/lib/core_ext/class.rb +2 -0
- data/lib/core_ext/concern.rb +142 -0
- data/lib/core_ext/configurable.rb +148 -0
- data/lib/core_ext/date/acts_like.rb +8 -0
- data/lib/core_ext/date/blank.rb +12 -0
- data/lib/core_ext/date/calculations.rb +143 -0
- data/lib/core_ext/date/conversions.rb +93 -0
- data/lib/core_ext/date/zones.rb +6 -0
- data/lib/core_ext/date.rb +5 -0
- data/lib/core_ext/date_and_time/calculations.rb +328 -0
- data/lib/core_ext/date_and_time/zones.rb +40 -0
- data/lib/core_ext/date_time/acts_like.rb +14 -0
- data/lib/core_ext/date_time/blank.rb +12 -0
- data/lib/core_ext/date_time/calculations.rb +177 -0
- data/lib/core_ext/date_time/conversions.rb +104 -0
- data/lib/core_ext/date_time/zones.rb +6 -0
- data/lib/core_ext/date_time.rb +5 -0
- data/lib/core_ext/deprecation/behaviors.rb +86 -0
- data/lib/core_ext/deprecation/instance_delegator.rb +24 -0
- data/lib/core_ext/deprecation/method_wrappers.rb +70 -0
- data/lib/core_ext/deprecation/proxy_wrappers.rb +149 -0
- data/lib/core_ext/deprecation/reporting.rb +105 -0
- data/lib/core_ext/deprecation.rb +43 -0
- data/lib/core_ext/digest/uuid.rb +51 -0
- data/lib/core_ext/duration.rb +157 -0
- data/lib/core_ext/enumerable.rb +106 -0
- data/lib/core_ext/file/atomic.rb +68 -0
- data/lib/core_ext/file.rb +1 -0
- data/lib/core_ext/hash/compact.rb +20 -0
- data/lib/core_ext/hash/conversions.rb +261 -0
- data/lib/core_ext/hash/deep_merge.rb +38 -0
- data/lib/core_ext/hash/except.rb +22 -0
- data/lib/core_ext/hash/indifferent_access.rb +23 -0
- data/lib/core_ext/hash/keys.rb +170 -0
- data/lib/core_ext/hash/reverse_merge.rb +22 -0
- data/lib/core_ext/hash/slice.rb +48 -0
- data/lib/core_ext/hash/transform_values.rb +29 -0
- data/lib/core_ext/hash.rb +9 -0
- data/lib/core_ext/hash_with_indifferent_access.rb +298 -0
- data/lib/core_ext/inflections.rb +70 -0
- data/lib/core_ext/inflector/inflections.rb +244 -0
- data/lib/core_ext/inflector/methods.rb +381 -0
- data/lib/core_ext/inflector/transliterate.rb +112 -0
- data/lib/core_ext/inflector.rb +7 -0
- data/lib/core_ext/integer/inflections.rb +29 -0
- data/lib/core_ext/integer/multiple.rb +10 -0
- data/lib/core_ext/integer/time.rb +29 -0
- data/lib/core_ext/integer.rb +3 -0
- data/lib/core_ext/json/decoding.rb +67 -0
- data/lib/core_ext/json/encoding.rb +127 -0
- data/lib/core_ext/json.rb +2 -0
- data/lib/core_ext/kernel/agnostics.rb +11 -0
- data/lib/core_ext/kernel/concern.rb +10 -0
- data/lib/core_ext/kernel/reporting.rb +41 -0
- data/lib/core_ext/kernel/singleton_class.rb +6 -0
- data/lib/core_ext/kernel.rb +4 -0
- data/lib/core_ext/load_error.rb +30 -0
- data/lib/core_ext/logger.rb +57 -0
- data/lib/core_ext/logger_silence.rb +24 -0
- data/lib/core_ext/marshal.rb +19 -0
- data/lib/core_ext/module/aliasing.rb +74 -0
- data/lib/core_ext/module/anonymous.rb +28 -0
- data/lib/core_ext/module/attr_internal.rb +36 -0
- data/lib/core_ext/module/attribute_accessors.rb +212 -0
- data/lib/core_ext/module/concerning.rb +135 -0
- data/lib/core_ext/module/delegation.rb +218 -0
- data/lib/core_ext/module/deprecation.rb +23 -0
- data/lib/core_ext/module/introspection.rb +62 -0
- data/lib/core_ext/module/method_transplanting.rb +3 -0
- data/lib/core_ext/module/qualified_const.rb +52 -0
- data/lib/core_ext/module/reachable.rb +8 -0
- data/lib/core_ext/module/remove_method.rb +35 -0
- data/lib/core_ext/module.rb +11 -0
- data/lib/core_ext/multibyte/chars.rb +231 -0
- data/lib/core_ext/multibyte/unicode.rb +388 -0
- data/lib/core_ext/multibyte.rb +21 -0
- data/lib/core_ext/name_error.rb +31 -0
- data/lib/core_ext/numeric/bytes.rb +64 -0
- data/lib/core_ext/numeric/conversions.rb +132 -0
- data/lib/core_ext/numeric/inquiry.rb +26 -0
- data/lib/core_ext/numeric/time.rb +74 -0
- data/lib/core_ext/numeric.rb +4 -0
- data/lib/core_ext/object/acts_like.rb +10 -0
- data/lib/core_ext/object/blank.rb +140 -0
- data/lib/core_ext/object/conversions.rb +4 -0
- data/lib/core_ext/object/deep_dup.rb +53 -0
- data/lib/core_ext/object/duplicable.rb +98 -0
- data/lib/core_ext/object/inclusion.rb +27 -0
- data/lib/core_ext/object/instance_variables.rb +28 -0
- data/lib/core_ext/object/json.rb +199 -0
- data/lib/core_ext/object/to_param.rb +1 -0
- data/lib/core_ext/object/to_query.rb +84 -0
- data/lib/core_ext/object/try.rb +146 -0
- data/lib/core_ext/object/with_options.rb +69 -0
- data/lib/core_ext/object.rb +14 -0
- data/lib/core_ext/option_merger.rb +25 -0
- data/lib/core_ext/ordered_hash.rb +48 -0
- data/lib/core_ext/ordered_options.rb +81 -0
- data/lib/core_ext/range/conversions.rb +34 -0
- data/lib/core_ext/range/each.rb +21 -0
- data/lib/core_ext/range/include_range.rb +23 -0
- data/lib/core_ext/range/overlaps.rb +8 -0
- data/lib/core_ext/range.rb +4 -0
- data/lib/core_ext/regexp.rb +5 -0
- data/lib/core_ext/rescuable.rb +119 -0
- data/lib/core_ext/securerandom.rb +23 -0
- data/lib/core_ext/security_utils.rb +20 -0
- data/lib/core_ext/string/access.rb +104 -0
- data/lib/core_ext/string/behavior.rb +6 -0
- data/lib/core_ext/string/conversions.rb +56 -0
- data/lib/core_ext/string/exclude.rb +11 -0
- data/lib/core_ext/string/filters.rb +102 -0
- data/lib/core_ext/string/indent.rb +43 -0
- data/lib/core_ext/string/inflections.rb +235 -0
- data/lib/core_ext/string/inquiry.rb +13 -0
- data/lib/core_ext/string/multibyte.rb +53 -0
- data/lib/core_ext/string/output_safety.rb +261 -0
- data/lib/core_ext/string/starts_ends_with.rb +4 -0
- data/lib/core_ext/string/strip.rb +23 -0
- data/lib/core_ext/string/zones.rb +14 -0
- data/lib/core_ext/string.rb +13 -0
- data/lib/core_ext/string_inquirer.rb +26 -0
- data/lib/core_ext/tagged_logging.rb +78 -0
- data/lib/core_ext/test_case.rb +88 -0
- data/lib/core_ext/testing/assertions.rb +99 -0
- data/lib/core_ext/testing/autorun.rb +12 -0
- data/lib/core_ext/testing/composite_filter.rb +54 -0
- data/lib/core_ext/testing/constant_lookup.rb +50 -0
- data/lib/core_ext/testing/declarative.rb +26 -0
- data/lib/core_ext/testing/deprecation.rb +36 -0
- data/lib/core_ext/testing/file_fixtures.rb +34 -0
- data/lib/core_ext/testing/isolation.rb +115 -0
- data/lib/core_ext/testing/method_call_assertions.rb +41 -0
- data/lib/core_ext/testing/setup_and_teardown.rb +50 -0
- data/lib/core_ext/testing/stream.rb +42 -0
- data/lib/core_ext/testing/tagged_logging.rb +25 -0
- data/lib/core_ext/testing/time_helpers.rb +134 -0
- data/lib/core_ext/time/acts_like.rb +8 -0
- data/lib/core_ext/time/calculations.rb +284 -0
- data/lib/core_ext/time/conversions.rb +66 -0
- data/lib/core_ext/time/zones.rb +95 -0
- data/lib/core_ext/time.rb +20 -0
- data/lib/core_ext/time_with_zone.rb +503 -0
- data/lib/core_ext/time_zone.rb +464 -0
- data/lib/core_ext/uri.rb +25 -0
- data/lib/core_ext/version.rb +3 -0
- data/lib/core_ext/xml_mini/jdom.rb +181 -0
- data/lib/core_ext/xml_mini/libxml.rb +79 -0
- data/lib/core_ext/xml_mini/libxmlsax.rb +85 -0
- data/lib/core_ext/xml_mini/nokogiri.rb +83 -0
- data/lib/core_ext/xml_mini/nokogirisax.rb +87 -0
- data/lib/core_ext/xml_mini/rexml.rb +130 -0
- data/lib/core_ext/xml_mini.rb +194 -0
- data/lib/core_ext.rb +3 -0
- metadata +310 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
require 'erb'
|
|
2
|
+
require 'core_ext/kernel/singleton_class'
|
|
3
|
+
|
|
4
|
+
class ERB
|
|
5
|
+
module Util
|
|
6
|
+
HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' }
|
|
7
|
+
JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' }
|
|
8
|
+
HTML_ESCAPE_REGEXP = /[&"'><]/
|
|
9
|
+
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/
|
|
10
|
+
JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u
|
|
11
|
+
|
|
12
|
+
# A utility method for escaping HTML tag characters.
|
|
13
|
+
# This method is also aliased as <tt>h</tt>.
|
|
14
|
+
#
|
|
15
|
+
# In your ERB templates, use this method to escape any unsafe content. For example:
|
|
16
|
+
# <%= h @person.name %>
|
|
17
|
+
#
|
|
18
|
+
# puts html_escape('is a > 0 & a < 10?')
|
|
19
|
+
# # => is a > 0 & a < 10?
|
|
20
|
+
def html_escape(s)
|
|
21
|
+
unwrapped_html_escape(s).html_safe
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Aliasing twice issues a warning "discarding old...". Remove first to avoid it.
|
|
25
|
+
remove_method(:h)
|
|
26
|
+
alias h html_escape
|
|
27
|
+
|
|
28
|
+
module_function :h
|
|
29
|
+
|
|
30
|
+
singleton_class.send(:remove_method, :html_escape)
|
|
31
|
+
module_function :html_escape
|
|
32
|
+
|
|
33
|
+
# HTML escapes strings but doesn't wrap them with an CoreExt::SafeBuffer.
|
|
34
|
+
# This method is not for public consumption! Seriously!
|
|
35
|
+
def unwrapped_html_escape(s) # :nodoc:
|
|
36
|
+
s = s.to_s
|
|
37
|
+
if s.html_safe?
|
|
38
|
+
s
|
|
39
|
+
else
|
|
40
|
+
CoreExt::Multibyte::Unicode.tidy_bytes(s).gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
module_function :unwrapped_html_escape
|
|
44
|
+
|
|
45
|
+
# A utility method for escaping HTML without affecting existing escaped entities.
|
|
46
|
+
#
|
|
47
|
+
# html_escape_once('1 < 2 & 3')
|
|
48
|
+
# # => "1 < 2 & 3"
|
|
49
|
+
#
|
|
50
|
+
# html_escape_once('<< Accept & Checkout')
|
|
51
|
+
# # => "<< Accept & Checkout"
|
|
52
|
+
def html_escape_once(s)
|
|
53
|
+
result = CoreExt::Multibyte::Unicode.tidy_bytes(s.to_s).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
|
|
54
|
+
s.html_safe? ? result.html_safe : result
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module_function :html_escape_once
|
|
58
|
+
|
|
59
|
+
# A utility method for escaping HTML entities in JSON strings. Specifically, the
|
|
60
|
+
# &, > and < characters are replaced with their equivalent unicode escaped form -
|
|
61
|
+
# \u0026, \u003e, and \u003c. The Unicode sequences \u2028 and \u2029 are also
|
|
62
|
+
# escaped as they are treated as newline characters in some JavaScript engines.
|
|
63
|
+
# These sequences have identical meaning as the original characters inside the
|
|
64
|
+
# context of a JSON string, so assuming the input is a valid and well-formed
|
|
65
|
+
# JSON value, the output will have equivalent meaning when parsed:
|
|
66
|
+
#
|
|
67
|
+
# json = JSON.generate({ name: "</script><script>alert('PWNED!!!')</script>"})
|
|
68
|
+
# # => "{\"name\":\"</script><script>alert('PWNED!!!')</script>\"}"
|
|
69
|
+
#
|
|
70
|
+
# json_escape(json)
|
|
71
|
+
# # => "{\"name\":\"\\u003C/script\\u003E\\u003Cscript\\u003Ealert('PWNED!!!')\\u003C/script\\u003E\"}"
|
|
72
|
+
#
|
|
73
|
+
# JSON.parse(json) == JSON.parse(json_escape(json))
|
|
74
|
+
# # => true
|
|
75
|
+
#
|
|
76
|
+
# The intended use case for this method is to escape JSON strings before including
|
|
77
|
+
# them inside a script tag to avoid XSS vulnerability:
|
|
78
|
+
#
|
|
79
|
+
# <script>
|
|
80
|
+
# var currentUser = <%= raw json_escape(current_user.to_json) %>;
|
|
81
|
+
# </script>
|
|
82
|
+
#
|
|
83
|
+
# It is necessary to +raw+ the result of +json_escape+, so that quotation marks
|
|
84
|
+
# don't get converted to <tt>"</tt> entities. +json_escape+ doesn't
|
|
85
|
+
# automatically flag the result as HTML safe, since the raw value is unsafe to
|
|
86
|
+
# use inside HTML attributes.
|
|
87
|
+
#
|
|
88
|
+
# If your JSON is being used downstream for insertion into the DOM, be aware of
|
|
89
|
+
# whether or not it is being inserted via +html()+. Most jQuery plugins do this.
|
|
90
|
+
# If that is the case, be sure to +html_escape+ or +sanitize+ any user-generated
|
|
91
|
+
# content returned by your JSON.
|
|
92
|
+
#
|
|
93
|
+
# If you need to output JSON elsewhere in your HTML, you can just do something
|
|
94
|
+
# like this, as any unsafe characters (including quotation marks) will be
|
|
95
|
+
# automatically escaped for you:
|
|
96
|
+
#
|
|
97
|
+
# <div data-user-info="<%= current_user.to_json %>">...</div>
|
|
98
|
+
#
|
|
99
|
+
# WARNING: this helper only works with valid JSON. Using this on non-JSON values
|
|
100
|
+
# will open up serious XSS vulnerabilities. For example, if you replace the
|
|
101
|
+
# +current_user.to_json+ in the example above with user input instead, the browser
|
|
102
|
+
# will happily eval() that string as JavaScript.
|
|
103
|
+
#
|
|
104
|
+
# The escaping performed in this method is identical to those performed in the
|
|
105
|
+
# Active Support JSON encoder when +CoreExt.escape_html_entities_in_json+ is
|
|
106
|
+
# set to true. Because this transformation is idempotent, this helper can be
|
|
107
|
+
# applied even if +CoreExt.escape_html_entities_in_json+ is already true.
|
|
108
|
+
#
|
|
109
|
+
# Therefore, when you are unsure if +CoreExt.escape_html_entities_in_json+
|
|
110
|
+
# is enabled, or if you are unsure where your JSON string originated from, it
|
|
111
|
+
# is recommended that you always apply this helper (other libraries, such as the
|
|
112
|
+
# JSON gem, do not provide this kind of protection by default; also some gems
|
|
113
|
+
# might override +to_json+ to bypass Active Support's encoder).
|
|
114
|
+
def json_escape(s)
|
|
115
|
+
result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE)
|
|
116
|
+
s.html_safe? ? result.html_safe : result
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
module_function :json_escape
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class Object
|
|
124
|
+
def html_safe?
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
class Numeric
|
|
130
|
+
def html_safe?
|
|
131
|
+
true
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
module CoreExt #:nodoc:
|
|
136
|
+
class SafeBuffer < String
|
|
137
|
+
UNSAFE_STRING_METHODS = %w(
|
|
138
|
+
capitalize chomp chop delete downcase gsub lstrip next reverse rstrip
|
|
139
|
+
slice squeeze strip sub succ swapcase tr tr_s upcase
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
alias_method :original_concat, :concat
|
|
143
|
+
private :original_concat
|
|
144
|
+
|
|
145
|
+
class SafeConcatError < StandardError
|
|
146
|
+
def initialize
|
|
147
|
+
super 'Could not concatenate to the buffer because it is not html safe.'
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def [](*args)
|
|
152
|
+
if args.size < 2
|
|
153
|
+
super
|
|
154
|
+
else
|
|
155
|
+
if html_safe?
|
|
156
|
+
new_safe_buffer = super
|
|
157
|
+
|
|
158
|
+
if new_safe_buffer
|
|
159
|
+
new_safe_buffer.instance_variable_set :@html_safe, true
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
new_safe_buffer
|
|
163
|
+
else
|
|
164
|
+
to_str[*args]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def safe_concat(value)
|
|
170
|
+
raise SafeConcatError unless html_safe?
|
|
171
|
+
original_concat(value)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def initialize(*)
|
|
175
|
+
@html_safe = true
|
|
176
|
+
super
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def initialize_copy(other)
|
|
180
|
+
super
|
|
181
|
+
@html_safe = other.html_safe?
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def clone_empty
|
|
185
|
+
self[0, 0]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def concat(value)
|
|
189
|
+
super(html_escape_interpolated_argument(value))
|
|
190
|
+
end
|
|
191
|
+
alias << concat
|
|
192
|
+
|
|
193
|
+
def prepend(value)
|
|
194
|
+
super(html_escape_interpolated_argument(value))
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def +(other)
|
|
198
|
+
dup.concat(other)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def %(args)
|
|
202
|
+
case args
|
|
203
|
+
when Hash
|
|
204
|
+
escaped_args = Hash[args.map { |k,arg| [k, html_escape_interpolated_argument(arg)] }]
|
|
205
|
+
else
|
|
206
|
+
escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
self.class.new(super(escaped_args))
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def html_safe?
|
|
213
|
+
defined?(@html_safe) && @html_safe
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def to_s
|
|
217
|
+
self
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def to_param
|
|
221
|
+
to_str
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def encode_with(coder)
|
|
225
|
+
coder.represent_object nil, to_str
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
UNSAFE_STRING_METHODS.each do |unsafe_method|
|
|
229
|
+
if unsafe_method.respond_to?(unsafe_method)
|
|
230
|
+
class_eval <<-EOT, __FILE__, __LINE__ + 1
|
|
231
|
+
def #{unsafe_method}(*args, &block) # def capitalize(*args, &block)
|
|
232
|
+
to_str.#{unsafe_method}(*args, &block) # to_str.capitalize(*args, &block)
|
|
233
|
+
end # end
|
|
234
|
+
|
|
235
|
+
def #{unsafe_method}!(*args) # def capitalize!(*args)
|
|
236
|
+
@html_safe = false # @html_safe = false
|
|
237
|
+
super # super
|
|
238
|
+
end # end
|
|
239
|
+
EOT
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
private
|
|
244
|
+
|
|
245
|
+
def html_escape_interpolated_argument(arg)
|
|
246
|
+
(!html_safe? || arg.html_safe?) ? arg :
|
|
247
|
+
arg.to_s.gsub(ERB::Util::HTML_ESCAPE_REGEXP, ERB::Util::HTML_ESCAPE)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
class String
|
|
253
|
+
# Marks a string as trusted safe. It will be inserted into HTML with no
|
|
254
|
+
# additional escaping performed. It is your responsibilty to ensure that the
|
|
255
|
+
# string contains no malicious content. This method is equivalent to the
|
|
256
|
+
# `raw` helper in views. It is recommended that you use `sanitize` instead of
|
|
257
|
+
# this method. It should never be called on user input.
|
|
258
|
+
def html_safe
|
|
259
|
+
CoreExt::SafeBuffer.new(self)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class String
|
|
2
|
+
# Strips indentation in heredocs.
|
|
3
|
+
#
|
|
4
|
+
# For example in
|
|
5
|
+
#
|
|
6
|
+
# if options[:usage]
|
|
7
|
+
# puts <<-USAGE.strip_heredoc
|
|
8
|
+
# This command does such and such.
|
|
9
|
+
#
|
|
10
|
+
# Supported options are:
|
|
11
|
+
# -h This message
|
|
12
|
+
# ...
|
|
13
|
+
# USAGE
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# the user would see the usage message aligned against the left margin.
|
|
17
|
+
#
|
|
18
|
+
# Technically, it looks for the least indented non-empty line
|
|
19
|
+
# in the whole string, and removes that amount of leading whitespace.
|
|
20
|
+
def strip_heredoc
|
|
21
|
+
gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, ''.freeze)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'core_ext/string/conversions'
|
|
2
|
+
require 'core_ext/time/zones'
|
|
3
|
+
|
|
4
|
+
class String
|
|
5
|
+
# Converts String to a TimeWithZone in the current zone if Time.zone or Time.zone_default
|
|
6
|
+
# is set, otherwise converts String to a Time via String#to_time
|
|
7
|
+
def in_time_zone(zone = ::Time.zone)
|
|
8
|
+
if zone
|
|
9
|
+
::Time.find_zone!(zone).parse(self)
|
|
10
|
+
else
|
|
11
|
+
to_time
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require 'core_ext/string/conversions'
|
|
2
|
+
require 'core_ext/string/filters'
|
|
3
|
+
require 'core_ext/string/multibyte'
|
|
4
|
+
require 'core_ext/string/starts_ends_with'
|
|
5
|
+
require 'core_ext/string/inflections'
|
|
6
|
+
require 'core_ext/string/access'
|
|
7
|
+
require 'core_ext/string/behavior'
|
|
8
|
+
require 'core_ext/string/output_safety'
|
|
9
|
+
require 'core_ext/string/exclude'
|
|
10
|
+
require 'core_ext/string/strip'
|
|
11
|
+
require 'core_ext/string/inquiry'
|
|
12
|
+
require 'core_ext/string/indent'
|
|
13
|
+
require 'core_ext/string/zones'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module CoreExt
|
|
2
|
+
# Wrapping a string in this class gives you a prettier way to test
|
|
3
|
+
# for equality. The value returned by <tt>Rails.env</tt> is wrapped
|
|
4
|
+
# in a StringInquirer object, so instead of calling this:
|
|
5
|
+
#
|
|
6
|
+
# Rails.env == 'production'
|
|
7
|
+
#
|
|
8
|
+
# you can call this:
|
|
9
|
+
#
|
|
10
|
+
# Rails.env.production?
|
|
11
|
+
class StringInquirer < String
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
15
|
+
method_name[-1] == '?'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def method_missing(method_name, *arguments)
|
|
19
|
+
if method_name[-1] == '?'
|
|
20
|
+
self == method_name[0..-2]
|
|
21
|
+
else
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
|
2
|
+
require 'active_support/core_ext/object/blank'
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'active_support/logger'
|
|
5
|
+
|
|
6
|
+
module ActiveSupport
|
|
7
|
+
# Wraps any standard Logger object to provide tagging capabilities.
|
|
8
|
+
#
|
|
9
|
+
# logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
|
|
10
|
+
# logger.tagged('BCX') { logger.info 'Stuff' } # Logs "[BCX] Stuff"
|
|
11
|
+
# logger.tagged('BCX', "Jason") { logger.info 'Stuff' } # Logs "[BCX] [Jason] Stuff"
|
|
12
|
+
# logger.tagged('BCX') { logger.tagged('Jason') { logger.info 'Stuff' } } # Logs "[BCX] [Jason] Stuff"
|
|
13
|
+
#
|
|
14
|
+
# This is used by the default Rails.logger as configured by Railties to make
|
|
15
|
+
# it easy to stamp log lines with subdomains, request ids, and anything else
|
|
16
|
+
# to aid debugging of multi-user production applications.
|
|
17
|
+
module TaggedLogging
|
|
18
|
+
module Formatter # :nodoc:
|
|
19
|
+
# This method is invoked when a log event occurs.
|
|
20
|
+
def call(severity, timestamp, progname, msg)
|
|
21
|
+
super(severity, timestamp, progname, "#{tags_text}#{msg}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def tagged(*tags)
|
|
25
|
+
new_tags = push_tags(*tags)
|
|
26
|
+
yield self
|
|
27
|
+
ensure
|
|
28
|
+
pop_tags(new_tags.size)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def push_tags(*tags)
|
|
32
|
+
tags.flatten.reject(&:blank?).tap do |new_tags|
|
|
33
|
+
current_tags.concat new_tags
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pop_tags(size = 1)
|
|
38
|
+
current_tags.pop size
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def clear_tags!
|
|
42
|
+
current_tags.clear
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def current_tags
|
|
46
|
+
# We use our object ID here to avoid conflicting with other instances
|
|
47
|
+
thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}".freeze
|
|
48
|
+
Thread.current[thread_key] ||= []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
def tags_text
|
|
53
|
+
tags = current_tags
|
|
54
|
+
if tags.any?
|
|
55
|
+
tags.collect { |tag| "[#{tag}] " }.join
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.new(logger)
|
|
61
|
+
# Ensure we set a default formatter so we aren't extending nil!
|
|
62
|
+
logger.formatter ||= ActiveSupport::Logger::SimpleFormatter.new
|
|
63
|
+
logger.formatter.extend Formatter
|
|
64
|
+
logger.extend(self)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
delegate :push_tags, :pop_tags, :clear_tags!, to: :formatter
|
|
68
|
+
|
|
69
|
+
def tagged(*tags)
|
|
70
|
+
formatter.tagged(*tags) { yield self }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def flush
|
|
74
|
+
clear_tags!
|
|
75
|
+
super if defined?(super)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
gem 'minitest' # make sure we get the gem, not stdlib
|
|
2
|
+
|
|
3
|
+
require 'minitest'
|
|
4
|
+
require 'core_ext/testing/tagged_logging'
|
|
5
|
+
require 'core_ext/testing/setup_and_teardown'
|
|
6
|
+
require 'core_ext/testing/assertions'
|
|
7
|
+
require 'core_ext/testing/deprecation'
|
|
8
|
+
require 'core_ext/testing/declarative'
|
|
9
|
+
require 'core_ext/testing/isolation'
|
|
10
|
+
require 'core_ext/testing/constant_lookup'
|
|
11
|
+
require 'core_ext/testing/time_helpers'
|
|
12
|
+
require 'core_ext/testing/file_fixtures'
|
|
13
|
+
require 'core_ext/testing/composite_filter'
|
|
14
|
+
require 'core_ext/kernel/reporting'
|
|
15
|
+
|
|
16
|
+
module CoreExt
|
|
17
|
+
class TestCase < ::Minitest::Test
|
|
18
|
+
Assertion = Minitest::Assertion
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# Sets the order in which test cases are run.
|
|
22
|
+
#
|
|
23
|
+
# CoreExt::TestCase.test_order = :random # => :random
|
|
24
|
+
#
|
|
25
|
+
# Valid values are:
|
|
26
|
+
# * +:random+ (to run tests in random order)
|
|
27
|
+
# * +:parallel+ (to run tests in parallel)
|
|
28
|
+
# * +:sorted+ (to run tests alphabetically by method name)
|
|
29
|
+
# * +:alpha+ (equivalent to +:sorted+)
|
|
30
|
+
def test_order=(new_order)
|
|
31
|
+
CoreExt.test_order = new_order
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the order in which test cases are run.
|
|
35
|
+
#
|
|
36
|
+
# CoreExt::TestCase.test_order # => :random
|
|
37
|
+
#
|
|
38
|
+
# Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+.
|
|
39
|
+
# Defaults to +:random+.
|
|
40
|
+
def test_order
|
|
41
|
+
CoreExt.test_order ||= :random
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run(reporter, options = {})
|
|
45
|
+
if options[:patterns] && options[:patterns].any? { |p| p =~ /:\d+/ }
|
|
46
|
+
options[:filter] = \
|
|
47
|
+
Testing::CompositeFilter.new(self, options[:filter], options[:patterns])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
alias_method :method_name, :name
|
|
55
|
+
|
|
56
|
+
include CoreExt::Testing::TaggedLogging
|
|
57
|
+
include CoreExt::Testing::SetupAndTeardown
|
|
58
|
+
include CoreExt::Testing::Assertions
|
|
59
|
+
include CoreExt::Testing::TimeHelpers
|
|
60
|
+
include CoreExt::Testing::FileFixtures
|
|
61
|
+
extend CoreExt::Testing::Declarative
|
|
62
|
+
|
|
63
|
+
# test/unit backwards compatibility methods
|
|
64
|
+
alias :assert_raise :assert_raises
|
|
65
|
+
alias :assert_not_empty :refute_empty
|
|
66
|
+
alias :assert_not_equal :refute_equal
|
|
67
|
+
alias :assert_not_in_delta :refute_in_delta
|
|
68
|
+
alias :assert_not_in_epsilon :refute_in_epsilon
|
|
69
|
+
alias :assert_not_includes :refute_includes
|
|
70
|
+
alias :assert_not_instance_of :refute_instance_of
|
|
71
|
+
alias :assert_not_kind_of :refute_kind_of
|
|
72
|
+
alias :assert_no_match :refute_match
|
|
73
|
+
alias :assert_not_nil :refute_nil
|
|
74
|
+
alias :assert_not_operator :refute_operator
|
|
75
|
+
alias :assert_not_predicate :refute_predicate
|
|
76
|
+
alias :assert_not_respond_to :refute_respond_to
|
|
77
|
+
alias :assert_not_same :refute_same
|
|
78
|
+
|
|
79
|
+
# Reveals the intention that the block should not raise any exception.
|
|
80
|
+
#
|
|
81
|
+
# assert_nothing_raised do
|
|
82
|
+
# ...
|
|
83
|
+
# end
|
|
84
|
+
def assert_nothing_raised(*args)
|
|
85
|
+
yield
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require 'core_ext/object/blank'
|
|
2
|
+
|
|
3
|
+
module CoreExt
|
|
4
|
+
module Testing
|
|
5
|
+
module Assertions
|
|
6
|
+
# Asserts that an expression is not truthy. Passes if <tt>object</tt> is
|
|
7
|
+
# +nil+ or +false+. "Truthy" means "considered true in a conditional"
|
|
8
|
+
# like <tt>if foo</tt>.
|
|
9
|
+
#
|
|
10
|
+
# assert_not nil # => true
|
|
11
|
+
# assert_not false # => true
|
|
12
|
+
# assert_not 'foo' # => Expected "foo" to be nil or false
|
|
13
|
+
#
|
|
14
|
+
# An error message can be specified.
|
|
15
|
+
#
|
|
16
|
+
# assert_not foo, 'foo should be false'
|
|
17
|
+
def assert_not(object, message = nil)
|
|
18
|
+
message ||= "Expected #{mu_pp(object)} to be nil or false"
|
|
19
|
+
assert !object, message
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Test numeric difference between the return value of an expression as a
|
|
23
|
+
# result of what is evaluated in the yielded block.
|
|
24
|
+
#
|
|
25
|
+
# assert_difference 'Article.count' do
|
|
26
|
+
# post :create, params: { article: {...} }
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# An arbitrary expression is passed in and evaluated.
|
|
30
|
+
#
|
|
31
|
+
# assert_difference 'Article.last.comments(:reload).size' do
|
|
32
|
+
# post :create, params: { comment: {...} }
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# An arbitrary positive or negative difference can be specified.
|
|
36
|
+
# The default is <tt>1</tt>.
|
|
37
|
+
#
|
|
38
|
+
# assert_difference 'Article.count', -1 do
|
|
39
|
+
# post :delete, params: { id: ... }
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# An array of expressions can also be passed in and evaluated.
|
|
43
|
+
#
|
|
44
|
+
# assert_difference [ 'Article.count', 'Post.count' ], 2 do
|
|
45
|
+
# post :create, params: { article: {...} }
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# A lambda or a list of lambdas can be passed in and evaluated:
|
|
49
|
+
#
|
|
50
|
+
# assert_difference ->{ Article.count }, 2 do
|
|
51
|
+
# post :create, params: { article: {...} }
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# assert_difference [->{ Article.count }, ->{ Post.count }], 2 do
|
|
55
|
+
# post :create, params: { article: {...} }
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
# An error message can be specified.
|
|
59
|
+
#
|
|
60
|
+
# assert_difference 'Article.count', -1, 'An Article should be destroyed' do
|
|
61
|
+
# post :delete, params: { id: ... }
|
|
62
|
+
# end
|
|
63
|
+
def assert_difference(expression, difference = 1, message = nil, &block)
|
|
64
|
+
expressions = Array(expression)
|
|
65
|
+
|
|
66
|
+
exps = expressions.map { |e|
|
|
67
|
+
e.respond_to?(:call) ? e : lambda { eval(e, block.binding) }
|
|
68
|
+
}
|
|
69
|
+
before = exps.map(&:call)
|
|
70
|
+
|
|
71
|
+
retval = yield
|
|
72
|
+
|
|
73
|
+
expressions.zip(exps).each_with_index do |(code, e), i|
|
|
74
|
+
error = "#{code.inspect} didn't change by #{difference}"
|
|
75
|
+
error = "#{message}.\n#{error}" if message
|
|
76
|
+
assert_equal(before[i] + difference, e.call, error)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
retval
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Assertion that the numeric result of evaluating an expression is not
|
|
83
|
+
# changed before and after invoking the passed in block.
|
|
84
|
+
#
|
|
85
|
+
# assert_no_difference 'Article.count' do
|
|
86
|
+
# post :create, params: { article: invalid_attributes }
|
|
87
|
+
# end
|
|
88
|
+
#
|
|
89
|
+
# An error message can be specified.
|
|
90
|
+
#
|
|
91
|
+
# assert_no_difference 'Article.count', 'An Article should not be created' do
|
|
92
|
+
# post :create, params: { article: invalid_attributes }
|
|
93
|
+
# end
|
|
94
|
+
def assert_no_difference(expression, message = nil, &block)
|
|
95
|
+
assert_difference expression, 0, message, &block
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'method_source'
|
|
2
|
+
|
|
3
|
+
module CoreExt
|
|
4
|
+
module Testing
|
|
5
|
+
class CompositeFilter # :nodoc:
|
|
6
|
+
def initialize(runnable, filter, patterns)
|
|
7
|
+
@runnable = runnable
|
|
8
|
+
@filters = [ derive_regexp(filter), *derive_line_filters(patterns) ].compact
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def ===(method)
|
|
12
|
+
@filters.any? { |filter| filter === method }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
def derive_regexp(filter)
|
|
17
|
+
filter =~ %r%/(.*)/% ? Regexp.new($1) : filter
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def derive_line_filters(patterns)
|
|
21
|
+
patterns.map do |file_and_line|
|
|
22
|
+
file, line = file_and_line.split(':')
|
|
23
|
+
Filter.new(@runnable, file, line) if file
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Filter # :nodoc:
|
|
28
|
+
def initialize(runnable, file, line)
|
|
29
|
+
@runnable, @file = runnable, File.expand_path(file)
|
|
30
|
+
@line = line.to_i if line
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ===(method)
|
|
34
|
+
return unless @runnable.method_defined?(method)
|
|
35
|
+
|
|
36
|
+
if @line
|
|
37
|
+
test_file, test_range = definition_for(@runnable.instance_method(method))
|
|
38
|
+
test_file == @file && test_range.include?(@line)
|
|
39
|
+
else
|
|
40
|
+
@runnable.instance_method(method).source_location.first == @file
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
def definition_for(method)
|
|
46
|
+
file, start_line = method.source_location
|
|
47
|
+
end_line = method.source.count("\n") + start_line - 1
|
|
48
|
+
|
|
49
|
+
return file, start_line..end_line
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|