core_ext 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|