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.
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