core_ext 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (175) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3 -0
  3. data/lib/core_ext/array/access.rb +76 -0
  4. data/lib/core_ext/array/conversions.rb +211 -0
  5. data/lib/core_ext/array/extract_options.rb +29 -0
  6. data/lib/core_ext/array/grouping.rb +116 -0
  7. data/lib/core_ext/array/inquiry.rb +17 -0
  8. data/lib/core_ext/array/prepend_and_append.rb +7 -0
  9. data/lib/core_ext/array/wrap.rb +46 -0
  10. data/lib/core_ext/array.rb +7 -0
  11. data/lib/core_ext/array_inquirer.rb +44 -0
  12. data/lib/core_ext/benchmark.rb +14 -0
  13. data/lib/core_ext/benchmarkable.rb +49 -0
  14. data/lib/core_ext/big_decimal/conversions.rb +14 -0
  15. data/lib/core_ext/big_decimal.rb +1 -0
  16. data/lib/core_ext/builder.rb +6 -0
  17. data/lib/core_ext/callbacks.rb +770 -0
  18. data/lib/core_ext/class/attribute.rb +128 -0
  19. data/lib/core_ext/class/attribute_accessors.rb +4 -0
  20. data/lib/core_ext/class/subclasses.rb +42 -0
  21. data/lib/core_ext/class.rb +2 -0
  22. data/lib/core_ext/concern.rb +142 -0
  23. data/lib/core_ext/configurable.rb +148 -0
  24. data/lib/core_ext/date/acts_like.rb +8 -0
  25. data/lib/core_ext/date/blank.rb +12 -0
  26. data/lib/core_ext/date/calculations.rb +143 -0
  27. data/lib/core_ext/date/conversions.rb +93 -0
  28. data/lib/core_ext/date/zones.rb +6 -0
  29. data/lib/core_ext/date.rb +5 -0
  30. data/lib/core_ext/date_and_time/calculations.rb +328 -0
  31. data/lib/core_ext/date_and_time/zones.rb +40 -0
  32. data/lib/core_ext/date_time/acts_like.rb +14 -0
  33. data/lib/core_ext/date_time/blank.rb +12 -0
  34. data/lib/core_ext/date_time/calculations.rb +177 -0
  35. data/lib/core_ext/date_time/conversions.rb +104 -0
  36. data/lib/core_ext/date_time/zones.rb +6 -0
  37. data/lib/core_ext/date_time.rb +5 -0
  38. data/lib/core_ext/deprecation/behaviors.rb +86 -0
  39. data/lib/core_ext/deprecation/instance_delegator.rb +24 -0
  40. data/lib/core_ext/deprecation/method_wrappers.rb +70 -0
  41. data/lib/core_ext/deprecation/proxy_wrappers.rb +149 -0
  42. data/lib/core_ext/deprecation/reporting.rb +105 -0
  43. data/lib/core_ext/deprecation.rb +43 -0
  44. data/lib/core_ext/digest/uuid.rb +51 -0
  45. data/lib/core_ext/duration.rb +157 -0
  46. data/lib/core_ext/enumerable.rb +106 -0
  47. data/lib/core_ext/file/atomic.rb +68 -0
  48. data/lib/core_ext/file.rb +1 -0
  49. data/lib/core_ext/hash/compact.rb +20 -0
  50. data/lib/core_ext/hash/conversions.rb +261 -0
  51. data/lib/core_ext/hash/deep_merge.rb +38 -0
  52. data/lib/core_ext/hash/except.rb +22 -0
  53. data/lib/core_ext/hash/indifferent_access.rb +23 -0
  54. data/lib/core_ext/hash/keys.rb +170 -0
  55. data/lib/core_ext/hash/reverse_merge.rb +22 -0
  56. data/lib/core_ext/hash/slice.rb +48 -0
  57. data/lib/core_ext/hash/transform_values.rb +29 -0
  58. data/lib/core_ext/hash.rb +9 -0
  59. data/lib/core_ext/hash_with_indifferent_access.rb +298 -0
  60. data/lib/core_ext/inflections.rb +70 -0
  61. data/lib/core_ext/inflector/inflections.rb +244 -0
  62. data/lib/core_ext/inflector/methods.rb +381 -0
  63. data/lib/core_ext/inflector/transliterate.rb +112 -0
  64. data/lib/core_ext/inflector.rb +7 -0
  65. data/lib/core_ext/integer/inflections.rb +29 -0
  66. data/lib/core_ext/integer/multiple.rb +10 -0
  67. data/lib/core_ext/integer/time.rb +29 -0
  68. data/lib/core_ext/integer.rb +3 -0
  69. data/lib/core_ext/json/decoding.rb +67 -0
  70. data/lib/core_ext/json/encoding.rb +127 -0
  71. data/lib/core_ext/json.rb +2 -0
  72. data/lib/core_ext/kernel/agnostics.rb +11 -0
  73. data/lib/core_ext/kernel/concern.rb +10 -0
  74. data/lib/core_ext/kernel/reporting.rb +41 -0
  75. data/lib/core_ext/kernel/singleton_class.rb +6 -0
  76. data/lib/core_ext/kernel.rb +4 -0
  77. data/lib/core_ext/load_error.rb +30 -0
  78. data/lib/core_ext/logger.rb +57 -0
  79. data/lib/core_ext/logger_silence.rb +24 -0
  80. data/lib/core_ext/marshal.rb +19 -0
  81. data/lib/core_ext/module/aliasing.rb +74 -0
  82. data/lib/core_ext/module/anonymous.rb +28 -0
  83. data/lib/core_ext/module/attr_internal.rb +36 -0
  84. data/lib/core_ext/module/attribute_accessors.rb +212 -0
  85. data/lib/core_ext/module/concerning.rb +135 -0
  86. data/lib/core_ext/module/delegation.rb +218 -0
  87. data/lib/core_ext/module/deprecation.rb +23 -0
  88. data/lib/core_ext/module/introspection.rb +62 -0
  89. data/lib/core_ext/module/method_transplanting.rb +3 -0
  90. data/lib/core_ext/module/qualified_const.rb +52 -0
  91. data/lib/core_ext/module/reachable.rb +8 -0
  92. data/lib/core_ext/module/remove_method.rb +35 -0
  93. data/lib/core_ext/module.rb +11 -0
  94. data/lib/core_ext/multibyte/chars.rb +231 -0
  95. data/lib/core_ext/multibyte/unicode.rb +388 -0
  96. data/lib/core_ext/multibyte.rb +21 -0
  97. data/lib/core_ext/name_error.rb +31 -0
  98. data/lib/core_ext/numeric/bytes.rb +64 -0
  99. data/lib/core_ext/numeric/conversions.rb +132 -0
  100. data/lib/core_ext/numeric/inquiry.rb +26 -0
  101. data/lib/core_ext/numeric/time.rb +74 -0
  102. data/lib/core_ext/numeric.rb +4 -0
  103. data/lib/core_ext/object/acts_like.rb +10 -0
  104. data/lib/core_ext/object/blank.rb +140 -0
  105. data/lib/core_ext/object/conversions.rb +4 -0
  106. data/lib/core_ext/object/deep_dup.rb +53 -0
  107. data/lib/core_ext/object/duplicable.rb +98 -0
  108. data/lib/core_ext/object/inclusion.rb +27 -0
  109. data/lib/core_ext/object/instance_variables.rb +28 -0
  110. data/lib/core_ext/object/json.rb +199 -0
  111. data/lib/core_ext/object/to_param.rb +1 -0
  112. data/lib/core_ext/object/to_query.rb +84 -0
  113. data/lib/core_ext/object/try.rb +146 -0
  114. data/lib/core_ext/object/with_options.rb +69 -0
  115. data/lib/core_ext/object.rb +14 -0
  116. data/lib/core_ext/option_merger.rb +25 -0
  117. data/lib/core_ext/ordered_hash.rb +48 -0
  118. data/lib/core_ext/ordered_options.rb +81 -0
  119. data/lib/core_ext/range/conversions.rb +34 -0
  120. data/lib/core_ext/range/each.rb +21 -0
  121. data/lib/core_ext/range/include_range.rb +23 -0
  122. data/lib/core_ext/range/overlaps.rb +8 -0
  123. data/lib/core_ext/range.rb +4 -0
  124. data/lib/core_ext/regexp.rb +5 -0
  125. data/lib/core_ext/rescuable.rb +119 -0
  126. data/lib/core_ext/securerandom.rb +23 -0
  127. data/lib/core_ext/security_utils.rb +20 -0
  128. data/lib/core_ext/string/access.rb +104 -0
  129. data/lib/core_ext/string/behavior.rb +6 -0
  130. data/lib/core_ext/string/conversions.rb +56 -0
  131. data/lib/core_ext/string/exclude.rb +11 -0
  132. data/lib/core_ext/string/filters.rb +102 -0
  133. data/lib/core_ext/string/indent.rb +43 -0
  134. data/lib/core_ext/string/inflections.rb +235 -0
  135. data/lib/core_ext/string/inquiry.rb +13 -0
  136. data/lib/core_ext/string/multibyte.rb +53 -0
  137. data/lib/core_ext/string/output_safety.rb +261 -0
  138. data/lib/core_ext/string/starts_ends_with.rb +4 -0
  139. data/lib/core_ext/string/strip.rb +23 -0
  140. data/lib/core_ext/string/zones.rb +14 -0
  141. data/lib/core_ext/string.rb +13 -0
  142. data/lib/core_ext/string_inquirer.rb +26 -0
  143. data/lib/core_ext/tagged_logging.rb +78 -0
  144. data/lib/core_ext/test_case.rb +88 -0
  145. data/lib/core_ext/testing/assertions.rb +99 -0
  146. data/lib/core_ext/testing/autorun.rb +12 -0
  147. data/lib/core_ext/testing/composite_filter.rb +54 -0
  148. data/lib/core_ext/testing/constant_lookup.rb +50 -0
  149. data/lib/core_ext/testing/declarative.rb +26 -0
  150. data/lib/core_ext/testing/deprecation.rb +36 -0
  151. data/lib/core_ext/testing/file_fixtures.rb +34 -0
  152. data/lib/core_ext/testing/isolation.rb +115 -0
  153. data/lib/core_ext/testing/method_call_assertions.rb +41 -0
  154. data/lib/core_ext/testing/setup_and_teardown.rb +50 -0
  155. data/lib/core_ext/testing/stream.rb +42 -0
  156. data/lib/core_ext/testing/tagged_logging.rb +25 -0
  157. data/lib/core_ext/testing/time_helpers.rb +134 -0
  158. data/lib/core_ext/time/acts_like.rb +8 -0
  159. data/lib/core_ext/time/calculations.rb +284 -0
  160. data/lib/core_ext/time/conversions.rb +66 -0
  161. data/lib/core_ext/time/zones.rb +95 -0
  162. data/lib/core_ext/time.rb +20 -0
  163. data/lib/core_ext/time_with_zone.rb +503 -0
  164. data/lib/core_ext/time_zone.rb +464 -0
  165. data/lib/core_ext/uri.rb +25 -0
  166. data/lib/core_ext/version.rb +3 -0
  167. data/lib/core_ext/xml_mini/jdom.rb +181 -0
  168. data/lib/core_ext/xml_mini/libxml.rb +79 -0
  169. data/lib/core_ext/xml_mini/libxmlsax.rb +85 -0
  170. data/lib/core_ext/xml_mini/nokogiri.rb +83 -0
  171. data/lib/core_ext/xml_mini/nokogirisax.rb +87 -0
  172. data/lib/core_ext/xml_mini/rexml.rb +130 -0
  173. data/lib/core_ext/xml_mini.rb +194 -0
  174. data/lib/core_ext.rb +3 -0
  175. 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