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,157 @@
|
|
|
1
|
+
require 'core_ext/array/conversions'
|
|
2
|
+
require 'core_ext/object/acts_like'
|
|
3
|
+
|
|
4
|
+
module CoreExt
|
|
5
|
+
# Provides accurate date and time measurements using Date#advance and
|
|
6
|
+
# Time#advance, respectively. It mainly supports the methods on Numeric.
|
|
7
|
+
#
|
|
8
|
+
# 1.month.ago # equivalent to Time.now.advance(months: -1)
|
|
9
|
+
class Duration
|
|
10
|
+
attr_accessor :value, :parts
|
|
11
|
+
|
|
12
|
+
def initialize(value, parts) #:nodoc:
|
|
13
|
+
@value, @parts = value, parts
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Adds another Duration or a Numeric to this Duration. Numeric values
|
|
17
|
+
# are treated as seconds.
|
|
18
|
+
def +(other)
|
|
19
|
+
if Duration === other
|
|
20
|
+
Duration.new(value + other.value, @parts + other.parts)
|
|
21
|
+
else
|
|
22
|
+
Duration.new(value + other, @parts + [[:seconds, other]])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Subtracts another Duration or a Numeric from this Duration. Numeric
|
|
27
|
+
# values are treated as seconds.
|
|
28
|
+
def -(other)
|
|
29
|
+
self + (-other)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def -@ #:nodoc:
|
|
33
|
+
Duration.new(-value, parts.map { |type,number| [type, -number] })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def is_a?(klass) #:nodoc:
|
|
37
|
+
Duration == klass || value.is_a?(klass)
|
|
38
|
+
end
|
|
39
|
+
alias :kind_of? :is_a?
|
|
40
|
+
|
|
41
|
+
def instance_of?(klass) # :nodoc:
|
|
42
|
+
Duration == klass || value.instance_of?(klass)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns +true+ if +other+ is also a Duration instance with the
|
|
46
|
+
# same +value+, or if <tt>other == value</tt>.
|
|
47
|
+
def ==(other)
|
|
48
|
+
if Duration === other
|
|
49
|
+
other.value == value
|
|
50
|
+
else
|
|
51
|
+
other == value
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns the amount of seconds a duration covers as a string.
|
|
56
|
+
# For more information check to_i method.
|
|
57
|
+
#
|
|
58
|
+
# 1.day.to_s # => "86400"
|
|
59
|
+
def to_s
|
|
60
|
+
@value.to_s
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the number of seconds that this Duration represents.
|
|
64
|
+
#
|
|
65
|
+
# 1.minute.to_i # => 60
|
|
66
|
+
# 1.hour.to_i # => 3600
|
|
67
|
+
# 1.day.to_i # => 86400
|
|
68
|
+
#
|
|
69
|
+
# Note that this conversion makes some assumptions about the
|
|
70
|
+
# duration of some periods, e.g. months are always 30 days
|
|
71
|
+
# and years are 365.25 days:
|
|
72
|
+
#
|
|
73
|
+
# # equivalent to 30.days.to_i
|
|
74
|
+
# 1.month.to_i # => 2592000
|
|
75
|
+
#
|
|
76
|
+
# # equivalent to 365.25.days.to_i
|
|
77
|
+
# 1.year.to_i # => 31557600
|
|
78
|
+
#
|
|
79
|
+
# In such cases, Ruby's core
|
|
80
|
+
# Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
|
|
81
|
+
# Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision
|
|
82
|
+
# date and time arithmetic.
|
|
83
|
+
def to_i
|
|
84
|
+
@value.to_i
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns +true+ if +other+ is also a Duration instance, which has the
|
|
88
|
+
# same parts as this one.
|
|
89
|
+
def eql?(other)
|
|
90
|
+
Duration === other && other.value.eql?(value)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def hash
|
|
94
|
+
@value.hash
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.===(other) #:nodoc:
|
|
98
|
+
other.is_a?(Duration)
|
|
99
|
+
rescue ::NoMethodError
|
|
100
|
+
false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Calculates a new Time or Date that is as far in the future
|
|
104
|
+
# as this Duration represents.
|
|
105
|
+
def since(time = ::Time.current)
|
|
106
|
+
sum(1, time)
|
|
107
|
+
end
|
|
108
|
+
alias :from_now :since
|
|
109
|
+
|
|
110
|
+
# Calculates a new Time or Date that is as far in the past
|
|
111
|
+
# as this Duration represents.
|
|
112
|
+
def ago(time = ::Time.current)
|
|
113
|
+
sum(-1, time)
|
|
114
|
+
end
|
|
115
|
+
alias :until :ago
|
|
116
|
+
|
|
117
|
+
def inspect #:nodoc:
|
|
118
|
+
parts.
|
|
119
|
+
reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }.
|
|
120
|
+
sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}.
|
|
121
|
+
map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}.
|
|
122
|
+
to_sentence(locale: ::I18n.default_locale)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def as_json(options = nil) #:nodoc:
|
|
126
|
+
to_i
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def respond_to_missing?(method, include_private=false) #:nodoc:
|
|
130
|
+
@value.respond_to?(method, include_private)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
delegate :<=>, to: :value
|
|
134
|
+
|
|
135
|
+
protected
|
|
136
|
+
|
|
137
|
+
def sum(sign, time = ::Time.current) #:nodoc:
|
|
138
|
+
parts.inject(time) do |t,(type,number)|
|
|
139
|
+
if t.acts_like?(:time) || t.acts_like?(:date)
|
|
140
|
+
if type == :seconds
|
|
141
|
+
t.since(sign * number)
|
|
142
|
+
else
|
|
143
|
+
t.advance(type => sign * number)
|
|
144
|
+
end
|
|
145
|
+
else
|
|
146
|
+
raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def method_missing(method, *args, &block) #:nodoc:
|
|
154
|
+
value.send(method, *args, &block)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module Enumerable
|
|
2
|
+
# Calculates a sum from the elements.
|
|
3
|
+
#
|
|
4
|
+
# payments.sum { |p| p.price * p.tax_rate }
|
|
5
|
+
# payments.sum(&:price)
|
|
6
|
+
#
|
|
7
|
+
# The latter is a shortcut for:
|
|
8
|
+
#
|
|
9
|
+
# payments.inject(0) { |sum, p| sum + p.price }
|
|
10
|
+
#
|
|
11
|
+
# It can also calculate the sum without the use of a block.
|
|
12
|
+
#
|
|
13
|
+
# [5, 15, 10].sum # => 30
|
|
14
|
+
# ['foo', 'bar'].sum # => "foobar"
|
|
15
|
+
# [[1, 2], [3, 1, 5]].sum => [1, 2, 3, 1, 5]
|
|
16
|
+
#
|
|
17
|
+
# The default sum of an empty list is zero. You can override this default:
|
|
18
|
+
#
|
|
19
|
+
# [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
|
|
20
|
+
def sum(identity = 0, &block)
|
|
21
|
+
if block_given?
|
|
22
|
+
map(&block).sum(identity)
|
|
23
|
+
else
|
|
24
|
+
inject { |sum, element| sum + element } || identity
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Convert an enumerable to a hash.
|
|
29
|
+
#
|
|
30
|
+
# people.index_by(&:login)
|
|
31
|
+
# => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...}
|
|
32
|
+
# people.index_by { |person| "#{person.first_name} #{person.last_name}" }
|
|
33
|
+
# => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...}
|
|
34
|
+
def index_by
|
|
35
|
+
if block_given?
|
|
36
|
+
Hash[map { |elem| [yield(elem), elem] }]
|
|
37
|
+
else
|
|
38
|
+
to_enum(:index_by) { size if respond_to?(:size) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns +true+ if the enumerable has more than 1 element. Functionally
|
|
43
|
+
# equivalent to <tt>enum.to_a.size > 1</tt>. Can be called with a block too,
|
|
44
|
+
# much like any?, so <tt>people.many? { |p| p.age > 26 }</tt> returns +true+
|
|
45
|
+
# if more than one person is over 26.
|
|
46
|
+
def many?
|
|
47
|
+
cnt = 0
|
|
48
|
+
if block_given?
|
|
49
|
+
any? do |element|
|
|
50
|
+
cnt += 1 if yield element
|
|
51
|
+
cnt > 1
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
any? { (cnt += 1) > 1 }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The negative of the <tt>Enumerable#include?</tt>. Returns +true+ if the
|
|
59
|
+
# collection does not include the object.
|
|
60
|
+
def exclude?(object)
|
|
61
|
+
!include?(object)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns a copy of the enumerable without the specified elements.
|
|
65
|
+
#
|
|
66
|
+
# ["David", "Rafael", "Aaron", "Todd"].without "Aaron", "Todd"
|
|
67
|
+
# => ["David", "Rafael"]
|
|
68
|
+
#
|
|
69
|
+
# {foo: 1, bar: 2, baz: 3}.without :bar
|
|
70
|
+
# => {foo: 1, baz: 3}
|
|
71
|
+
def without(*elements)
|
|
72
|
+
reject { |element| elements.include?(element) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Convert an enumerable to an array based on the given key.
|
|
76
|
+
#
|
|
77
|
+
# [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name)
|
|
78
|
+
# => ["David", "Rafael", "Aaron"]
|
|
79
|
+
#
|
|
80
|
+
# [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name)
|
|
81
|
+
# => [[1, "David"], [2, "Rafael"]]
|
|
82
|
+
def pluck(*keys)
|
|
83
|
+
if keys.many?
|
|
84
|
+
map { |element| keys.map { |key| element[key] } }
|
|
85
|
+
else
|
|
86
|
+
map { |element| element[keys.first] }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class Range #:nodoc:
|
|
92
|
+
# Optimize range sum to use arithmetic progression if a block is not given and
|
|
93
|
+
# we have a range of numeric values.
|
|
94
|
+
def sum(identity = 0)
|
|
95
|
+
if block_given? || !(first.is_a?(Integer) && last.is_a?(Integer))
|
|
96
|
+
super
|
|
97
|
+
else
|
|
98
|
+
actual_last = exclude_end? ? (last - 1) : last
|
|
99
|
+
if actual_last >= first
|
|
100
|
+
(actual_last - first + 1) * (actual_last + first) / 2
|
|
101
|
+
else
|
|
102
|
+
identity
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
|
|
3
|
+
class File
|
|
4
|
+
# Write to a file atomically. Useful for situations where you don't
|
|
5
|
+
# want other processes or threads to see half-written files.
|
|
6
|
+
#
|
|
7
|
+
# File.atomic_write('important.file') do |file|
|
|
8
|
+
# file.write('hello')
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# This method needs to create a temporary file. By default it will create it
|
|
12
|
+
# in the same directory as the destination file. If you don't like this
|
|
13
|
+
# behavior you can provide a different directory but it must be on the
|
|
14
|
+
# same physical filesystem as the file you're trying to write.
|
|
15
|
+
#
|
|
16
|
+
# File.atomic_write('/data/something.important', '/data/tmp') do |file|
|
|
17
|
+
# file.write('hello')
|
|
18
|
+
# end
|
|
19
|
+
def self.atomic_write(file_name, temp_dir = dirname(file_name))
|
|
20
|
+
require 'tempfile' unless defined?(Tempfile)
|
|
21
|
+
|
|
22
|
+
Tempfile.open(".#{basename(file_name)}", temp_dir) do |temp_file|
|
|
23
|
+
temp_file.binmode
|
|
24
|
+
return_val = yield temp_file
|
|
25
|
+
temp_file.close
|
|
26
|
+
|
|
27
|
+
old_stat = if exist?(file_name)
|
|
28
|
+
# Get original file permissions
|
|
29
|
+
stat(file_name)
|
|
30
|
+
elsif temp_dir != dirname(file_name)
|
|
31
|
+
# If not possible, probe which are the default permissions in the
|
|
32
|
+
# destination directory.
|
|
33
|
+
probe_stat_in(dirname(file_name))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if old_stat
|
|
37
|
+
# Set correct permissions on new file
|
|
38
|
+
begin
|
|
39
|
+
chown(old_stat.uid, old_stat.gid, temp_file.path)
|
|
40
|
+
# This operation will affect filesystem ACL's
|
|
41
|
+
chmod(old_stat.mode, temp_file.path)
|
|
42
|
+
rescue Errno::EPERM, Errno::EACCES
|
|
43
|
+
# Changing file ownership failed, moving on.
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Overwrite original file with temp file
|
|
48
|
+
rename(temp_file.path, file_name)
|
|
49
|
+
return_val
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Private utility method.
|
|
54
|
+
def self.probe_stat_in(dir) #:nodoc:
|
|
55
|
+
basename = [
|
|
56
|
+
'.permissions_check',
|
|
57
|
+
Thread.current.object_id,
|
|
58
|
+
Process.pid,
|
|
59
|
+
rand(1000000)
|
|
60
|
+
].join('.')
|
|
61
|
+
|
|
62
|
+
file_name = join(dir, basename)
|
|
63
|
+
FileUtils.touch(file_name)
|
|
64
|
+
stat(file_name)
|
|
65
|
+
ensure
|
|
66
|
+
FileUtils.rm_f(file_name) if file_name
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'core_ext/file/atomic'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class Hash
|
|
2
|
+
# Returns a hash with non +nil+ values.
|
|
3
|
+
#
|
|
4
|
+
# hash = { a: true, b: false, c: nil}
|
|
5
|
+
# hash.compact # => { a: true, b: false}
|
|
6
|
+
# hash # => { a: true, b: false, c: nil}
|
|
7
|
+
# { c: nil }.compact # => {}
|
|
8
|
+
def compact
|
|
9
|
+
self.select { |_, value| !value.nil? }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Replaces current hash with non +nil+ values.
|
|
13
|
+
#
|
|
14
|
+
# hash = { a: true, b: false, c: nil}
|
|
15
|
+
# hash.compact! # => { a: true, b: false}
|
|
16
|
+
# hash # => { a: true, b: false}
|
|
17
|
+
def compact!
|
|
18
|
+
self.reject! { |_, value| value.nil? }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
require 'core_ext/xml_mini'
|
|
2
|
+
require 'core_ext/time'
|
|
3
|
+
require 'core_ext/object/blank'
|
|
4
|
+
require 'core_ext/object/to_param'
|
|
5
|
+
require 'core_ext/object/to_query'
|
|
6
|
+
require 'core_ext/array/wrap'
|
|
7
|
+
require 'core_ext/hash/reverse_merge'
|
|
8
|
+
require 'core_ext/string/inflections'
|
|
9
|
+
|
|
10
|
+
class Hash
|
|
11
|
+
# Returns a string containing an XML representation of its receiver:
|
|
12
|
+
#
|
|
13
|
+
# { foo: 1, bar: 2 }.to_xml
|
|
14
|
+
# # =>
|
|
15
|
+
# # <?xml version="1.0" encoding="UTF-8"?>
|
|
16
|
+
# # <hash>
|
|
17
|
+
# # <foo type="integer">1</foo>
|
|
18
|
+
# # <bar type="integer">2</bar>
|
|
19
|
+
# # </hash>
|
|
20
|
+
#
|
|
21
|
+
# To do so, the method loops over the pairs and builds nodes that depend on
|
|
22
|
+
# the _values_. Given a pair +key+, +value+:
|
|
23
|
+
#
|
|
24
|
+
# * If +value+ is a hash there's a recursive call with +key+ as <tt>:root</tt>.
|
|
25
|
+
#
|
|
26
|
+
# * If +value+ is an array there's a recursive call with +key+ as <tt>:root</tt>,
|
|
27
|
+
# and +key+ singularized as <tt>:children</tt>.
|
|
28
|
+
#
|
|
29
|
+
# * If +value+ is a callable object it must expect one or two arguments. Depending
|
|
30
|
+
# on the arity, the callable is invoked with the +options+ hash as first argument
|
|
31
|
+
# with +key+ as <tt>:root</tt>, and +key+ singularized as second argument. The
|
|
32
|
+
# callable can add nodes by using <tt>options[:builder]</tt>.
|
|
33
|
+
#
|
|
34
|
+
# 'foo'.to_xml(lambda { |options, key| options[:builder].b(key) })
|
|
35
|
+
# # => "<b>foo</b>"
|
|
36
|
+
#
|
|
37
|
+
# * If +value+ responds to +to_xml+ the method is invoked with +key+ as <tt>:root</tt>.
|
|
38
|
+
#
|
|
39
|
+
# class Foo
|
|
40
|
+
# def to_xml(options)
|
|
41
|
+
# options[:builder].bar 'fooing!'
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# { foo: Foo.new }.to_xml(skip_instruct: true)
|
|
46
|
+
# # =>
|
|
47
|
+
# # <hash>
|
|
48
|
+
# # <bar>fooing!</bar>
|
|
49
|
+
# # </hash>
|
|
50
|
+
#
|
|
51
|
+
# * Otherwise, a node with +key+ as tag is created with a string representation of
|
|
52
|
+
# +value+ as text node. If +value+ is +nil+ an attribute "nil" set to "true" is added.
|
|
53
|
+
# Unless the option <tt>:skip_types</tt> exists and is true, an attribute "type" is
|
|
54
|
+
# added as well according to the following mapping:
|
|
55
|
+
#
|
|
56
|
+
# XML_TYPE_NAMES = {
|
|
57
|
+
# "Symbol" => "symbol",
|
|
58
|
+
# "Fixnum" => "integer",
|
|
59
|
+
# "Bignum" => "integer",
|
|
60
|
+
# "BigDecimal" => "decimal",
|
|
61
|
+
# "Float" => "float",
|
|
62
|
+
# "TrueClass" => "boolean",
|
|
63
|
+
# "FalseClass" => "boolean",
|
|
64
|
+
# "Date" => "date",
|
|
65
|
+
# "DateTime" => "dateTime",
|
|
66
|
+
# "Time" => "dateTime"
|
|
67
|
+
# }
|
|
68
|
+
#
|
|
69
|
+
# By default the root node is "hash", but that's configurable via the <tt>:root</tt> option.
|
|
70
|
+
#
|
|
71
|
+
# The default XML builder is a fresh instance of <tt>Builder::XmlMarkup</tt>. You can
|
|
72
|
+
# configure your own builder with the <tt>:builder</tt> option. The method also accepts
|
|
73
|
+
# options like <tt>:dasherize</tt> and friends, they are forwarded to the builder.
|
|
74
|
+
def to_xml(options = {})
|
|
75
|
+
require 'core_ext/builder' unless defined?(Builder)
|
|
76
|
+
|
|
77
|
+
options = options.dup
|
|
78
|
+
options[:indent] ||= 2
|
|
79
|
+
options[:root] ||= 'hash'
|
|
80
|
+
options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
|
|
81
|
+
|
|
82
|
+
builder = options[:builder]
|
|
83
|
+
builder.instruct! unless options.delete(:skip_instruct)
|
|
84
|
+
|
|
85
|
+
root = CoreExt::XmlMini.rename_key(options[:root].to_s, options)
|
|
86
|
+
|
|
87
|
+
builder.tag!(root) do
|
|
88
|
+
each { |key, value| CoreExt::XmlMini.to_tag(key, value, options) }
|
|
89
|
+
yield builder if block_given?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class << self
|
|
94
|
+
# Returns a Hash containing a collection of pairs when the key is the node name and the value is
|
|
95
|
+
# its content
|
|
96
|
+
#
|
|
97
|
+
# xml = <<-XML
|
|
98
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
99
|
+
# <hash>
|
|
100
|
+
# <foo type="integer">1</foo>
|
|
101
|
+
# <bar type="integer">2</bar>
|
|
102
|
+
# </hash>
|
|
103
|
+
# XML
|
|
104
|
+
#
|
|
105
|
+
# hash = Hash.from_xml(xml)
|
|
106
|
+
# # => {"hash"=>{"foo"=>1, "bar"=>2}}
|
|
107
|
+
#
|
|
108
|
+
# +DisallowedType+ is raised if the XML contains attributes with <tt>type="yaml"</tt> or
|
|
109
|
+
# <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to
|
|
110
|
+
# parse this XML.
|
|
111
|
+
#
|
|
112
|
+
# Custom +disallowed_types+ can also be passed in the form of an
|
|
113
|
+
# array.
|
|
114
|
+
#
|
|
115
|
+
# xml = <<-XML
|
|
116
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
|
117
|
+
# <hash>
|
|
118
|
+
# <foo type="integer">1</foo>
|
|
119
|
+
# <bar type="string">"David"</bar>
|
|
120
|
+
# </hash>
|
|
121
|
+
# XML
|
|
122
|
+
#
|
|
123
|
+
# hash = Hash.from_xml(xml, ['integer'])
|
|
124
|
+
# # => CoreExt::XMLConverter::DisallowedType: Disallowed type attribute: "integer"
|
|
125
|
+
#
|
|
126
|
+
# Note that passing custom disallowed types will override the default types,
|
|
127
|
+
# which are Symbol and YAML.
|
|
128
|
+
def from_xml(xml, disallowed_types = nil)
|
|
129
|
+
CoreExt::XMLConverter.new(xml, disallowed_types).to_h
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Builds a Hash from XML just like <tt>Hash.from_xml</tt>, but also allows Symbol and YAML.
|
|
133
|
+
def from_trusted_xml(xml)
|
|
134
|
+
from_xml xml, []
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
module CoreExt
|
|
140
|
+
class XMLConverter # :nodoc:
|
|
141
|
+
class DisallowedType < StandardError
|
|
142
|
+
def initialize(type)
|
|
143
|
+
super "Disallowed type attribute: #{type.inspect}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
DISALLOWED_TYPES = %w(symbol yaml)
|
|
148
|
+
|
|
149
|
+
def initialize(xml, disallowed_types = nil)
|
|
150
|
+
@xml = normalize_keys(XmlMini.parse(xml))
|
|
151
|
+
@disallowed_types = disallowed_types || DISALLOWED_TYPES
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def to_h
|
|
155
|
+
deep_to_h(@xml)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
def normalize_keys(params)
|
|
160
|
+
case params
|
|
161
|
+
when Hash
|
|
162
|
+
Hash[params.map { |k,v| [k.to_s.tr('-', '_'), normalize_keys(v)] } ]
|
|
163
|
+
when Array
|
|
164
|
+
params.map { |v| normalize_keys(v) }
|
|
165
|
+
else
|
|
166
|
+
params
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def deep_to_h(value)
|
|
171
|
+
case value
|
|
172
|
+
when Hash
|
|
173
|
+
process_hash(value)
|
|
174
|
+
when Array
|
|
175
|
+
process_array(value)
|
|
176
|
+
when String
|
|
177
|
+
value
|
|
178
|
+
else
|
|
179
|
+
raise "can't typecast #{value.class.name} - #{value.inspect}"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def process_hash(value)
|
|
184
|
+
if value.include?('type') && !value['type'].is_a?(Hash) && @disallowed_types.include?(value['type'])
|
|
185
|
+
raise DisallowedType, value['type']
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if become_array?(value)
|
|
189
|
+
_, entries = Array.wrap(value.detect { |k,v| not v.is_a?(String) })
|
|
190
|
+
if entries.nil? || value['__content__'].try(:empty?)
|
|
191
|
+
[]
|
|
192
|
+
else
|
|
193
|
+
case entries
|
|
194
|
+
when Array
|
|
195
|
+
entries.collect { |v| deep_to_h(v) }
|
|
196
|
+
when Hash
|
|
197
|
+
[deep_to_h(entries)]
|
|
198
|
+
else
|
|
199
|
+
raise "can't typecast #{entries.inspect}"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
elsif become_content?(value)
|
|
203
|
+
process_content(value)
|
|
204
|
+
|
|
205
|
+
elsif become_empty_string?(value)
|
|
206
|
+
''
|
|
207
|
+
elsif become_hash?(value)
|
|
208
|
+
xml_value = Hash[value.map { |k,v| [k, deep_to_h(v)] }]
|
|
209
|
+
|
|
210
|
+
# Turn { files: { file: #<StringIO> } } into { files: #<StringIO> } so it is compatible with
|
|
211
|
+
# how multipart uploaded files from HTML appear
|
|
212
|
+
xml_value['file'].is_a?(StringIO) ? xml_value['file'] : xml_value
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def become_content?(value)
|
|
217
|
+
value['type'] == 'file' || (value['__content__'] && (value.keys.size == 1 || value['__content__'].present?))
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def become_array?(value)
|
|
221
|
+
value['type'] == 'array'
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def become_empty_string?(value)
|
|
225
|
+
# { "string" => true }
|
|
226
|
+
# No tests fail when the second term is removed.
|
|
227
|
+
value['type'] == 'string' && value['nil'] != 'true'
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def become_hash?(value)
|
|
231
|
+
!nothing?(value) && !garbage?(value)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def nothing?(value)
|
|
235
|
+
# blank or nil parsed values are represented by nil
|
|
236
|
+
value.blank? || value['nil'] == 'true'
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def garbage?(value)
|
|
240
|
+
# If the type is the only element which makes it then
|
|
241
|
+
# this still makes the value nil, except if type is
|
|
242
|
+
# an XML node(where type['value'] is a Hash)
|
|
243
|
+
value['type'] && !value['type'].is_a?(::Hash) && value.size == 1
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def process_content(value)
|
|
247
|
+
content = value['__content__']
|
|
248
|
+
if parser = CoreExt::XmlMini::PARSING[value['type']]
|
|
249
|
+
parser.arity == 1 ? parser.call(content) : parser.call(content, value)
|
|
250
|
+
else
|
|
251
|
+
content
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def process_array(value)
|
|
256
|
+
value.map! { |i| deep_to_h(i) }
|
|
257
|
+
value.length > 1 ? value : value.first
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
end
|
|
261
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class Hash
|
|
2
|
+
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
|
3
|
+
#
|
|
4
|
+
# h1 = { a: true, b: { c: [1, 2, 3] } }
|
|
5
|
+
# h2 = { a: false, b: { x: [3, 4, 5] } }
|
|
6
|
+
#
|
|
7
|
+
# h1.deep_merge(h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
|
|
8
|
+
#
|
|
9
|
+
# Like with Hash#merge in the standard library, a block can be provided
|
|
10
|
+
# to merge values:
|
|
11
|
+
#
|
|
12
|
+
# h1 = { a: 100, b: 200, c: { c1: 100 } }
|
|
13
|
+
# h2 = { b: 250, c: { c1: 200 } }
|
|
14
|
+
# h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
|
|
15
|
+
# # => { a: 100, b: 450, c: { c1: 300 } }
|
|
16
|
+
def deep_merge(other_hash, &block)
|
|
17
|
+
dup.deep_merge!(other_hash, &block)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Same as +deep_merge+, but modifies +self+.
|
|
21
|
+
def deep_merge!(other_hash, &block)
|
|
22
|
+
other_hash.each_pair do |current_key, other_value|
|
|
23
|
+
this_value = self[current_key]
|
|
24
|
+
|
|
25
|
+
self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash)
|
|
26
|
+
this_value.deep_merge(other_value, &block)
|
|
27
|
+
else
|
|
28
|
+
if block_given? && key?(current_key)
|
|
29
|
+
block.call(current_key, this_value, other_value)
|
|
30
|
+
else
|
|
31
|
+
other_value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class Hash
|
|
2
|
+
# Returns a hash that includes everything except given keys.
|
|
3
|
+
# hash = { a: true, b: false, c: nil }
|
|
4
|
+
# hash.except(:c) # => { a: true, b: false }
|
|
5
|
+
# hash.except(:a, :b) # => { c: nil }
|
|
6
|
+
# hash # => { a: true, b: false, c: nil }
|
|
7
|
+
#
|
|
8
|
+
# This is useful for limiting a set of parameters to everything but a few known toggles:
|
|
9
|
+
# @person.update(params[:person].except(:admin))
|
|
10
|
+
def except(*keys)
|
|
11
|
+
dup.except!(*keys)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Removes the given keys from hash and returns it.
|
|
15
|
+
# hash = { a: true, b: false, c: nil }
|
|
16
|
+
# hash.except!(:c) # => { a: true, b: false }
|
|
17
|
+
# hash # => { a: true, b: false }
|
|
18
|
+
def except!(*keys)
|
|
19
|
+
keys.each { |key| delete(key) }
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'core_ext/hash_with_indifferent_access'
|
|
2
|
+
|
|
3
|
+
class Hash
|
|
4
|
+
|
|
5
|
+
# Returns an <tt>CoreExt::HashWithIndifferentAccess</tt> out of its receiver:
|
|
6
|
+
#
|
|
7
|
+
# { a: 1 }.with_indifferent_access['a'] # => 1
|
|
8
|
+
def with_indifferent_access
|
|
9
|
+
CoreExt::HashWithIndifferentAccess.new(self)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Called when object is nested under an object that receives
|
|
13
|
+
# #with_indifferent_access. This method will be called on the current object
|
|
14
|
+
# by the enclosing object and is aliased to #with_indifferent_access by
|
|
15
|
+
# default. Subclasses of Hash may overwrite this method to return +self+ if
|
|
16
|
+
# converting to an <tt>CoreExt::HashWithIndifferentAccess</tt> would not be
|
|
17
|
+
# desirable.
|
|
18
|
+
#
|
|
19
|
+
# b = { b: 1 }
|
|
20
|
+
# { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access
|
|
21
|
+
# # => {"b"=>1}
|
|
22
|
+
alias nested_under_indifferent_access with_indifferent_access
|
|
23
|
+
end
|