hoodoo 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (216) hide show
  1. checksums.yaml +7 -0
  2. data/bin/hoodoo +5 -0
  3. data/lib/hoodoo.rb +27 -0
  4. data/lib/hoodoo/active.rb +32 -0
  5. data/lib/hoodoo/active/active_model/uuid_validator.rb +45 -0
  6. data/lib/hoodoo/active/active_record/base.rb +81 -0
  7. data/lib/hoodoo/active/active_record/creator.rb +134 -0
  8. data/lib/hoodoo/active/active_record/dated.rb +343 -0
  9. data/lib/hoodoo/active/active_record/error_mapping.rb +351 -0
  10. data/lib/hoodoo/active/active_record/finder.rb +606 -0
  11. data/lib/hoodoo/active/active_record/search_helper.rb +189 -0
  12. data/lib/hoodoo/active/active_record/secure.rb +431 -0
  13. data/lib/hoodoo/active/active_record/support.rb +106 -0
  14. data/lib/hoodoo/active/active_record/translated.rb +87 -0
  15. data/lib/hoodoo/active/active_record/uuid.rb +80 -0
  16. data/lib/hoodoo/active/active_record/writer.rb +321 -0
  17. data/lib/hoodoo/client.rb +23 -0
  18. data/lib/hoodoo/client/augmented_array.rb +29 -0
  19. data/lib/hoodoo/client/augmented_base.rb +168 -0
  20. data/lib/hoodoo/client/augmented_hash.rb +23 -0
  21. data/lib/hoodoo/client/client.rb +354 -0
  22. data/lib/hoodoo/client/endpoint/endpoint.rb +427 -0
  23. data/lib/hoodoo/client/endpoint/endpoints/amqp.rb +180 -0
  24. data/lib/hoodoo/client/endpoint/endpoints/auto_session.rb +194 -0
  25. data/lib/hoodoo/client/endpoint/endpoints/http.rb +203 -0
  26. data/lib/hoodoo/client/endpoint/endpoints/http_based.rb +367 -0
  27. data/lib/hoodoo/client/endpoint/endpoints/not_found.rb +59 -0
  28. data/lib/hoodoo/client/headers.rb +269 -0
  29. data/lib/hoodoo/communicators.rb +23 -0
  30. data/lib/hoodoo/communicators/fast.rb +44 -0
  31. data/lib/hoodoo/communicators/pool.rb +601 -0
  32. data/lib/hoodoo/communicators/slow.rb +84 -0
  33. data/lib/hoodoo/data.rb +51 -0
  34. data/lib/hoodoo/data/resources/caller.rb +39 -0
  35. data/lib/hoodoo/data/resources/errors.rb +28 -0
  36. data/lib/hoodoo/data/resources/log.rb +31 -0
  37. data/lib/hoodoo/data/resources/session.rb +26 -0
  38. data/lib/hoodoo/data/types/error_primitive.rb +27 -0
  39. data/lib/hoodoo/data/types/permissions.rb +40 -0
  40. data/lib/hoodoo/data/types/permissions_defaults.rb +32 -0
  41. data/lib/hoodoo/data/types/permissions_full.rb +28 -0
  42. data/lib/hoodoo/data/types/permissions_resources.rb +31 -0
  43. data/lib/hoodoo/discovery.rb +20 -0
  44. data/lib/hoodoo/errors.rb +19 -0
  45. data/lib/hoodoo/errors/error_descriptions.rb +229 -0
  46. data/lib/hoodoo/errors/errors.rb +322 -0
  47. data/lib/hoodoo/generator.rb +139 -0
  48. data/lib/hoodoo/logger.rb +23 -0
  49. data/lib/hoodoo/logger/fast_writer.rb +27 -0
  50. data/lib/hoodoo/logger/flattener_mixin.rb +36 -0
  51. data/lib/hoodoo/logger/logger.rb +387 -0
  52. data/lib/hoodoo/logger/slow_writer.rb +49 -0
  53. data/lib/hoodoo/logger/writer_mixin.rb +52 -0
  54. data/lib/hoodoo/logger/writers/file_writer.rb +45 -0
  55. data/lib/hoodoo/logger/writers/log_entries_dot_com_writer.rb +64 -0
  56. data/lib/hoodoo/logger/writers/stream_writer.rb +43 -0
  57. data/lib/hoodoo/middleware.rb +33 -0
  58. data/lib/hoodoo/presenters.rb +45 -0
  59. data/lib/hoodoo/presenters/base.rb +281 -0
  60. data/lib/hoodoo/presenters/base_dsl.rb +519 -0
  61. data/lib/hoodoo/presenters/common_resource_fields.rb +31 -0
  62. data/lib/hoodoo/presenters/embedding.rb +232 -0
  63. data/lib/hoodoo/presenters/types/array.rb +118 -0
  64. data/lib/hoodoo/presenters/types/boolean.rb +26 -0
  65. data/lib/hoodoo/presenters/types/date.rb +26 -0
  66. data/lib/hoodoo/presenters/types/date_time.rb +26 -0
  67. data/lib/hoodoo/presenters/types/decimal.rb +47 -0
  68. data/lib/hoodoo/presenters/types/enum.rb +55 -0
  69. data/lib/hoodoo/presenters/types/field.rb +158 -0
  70. data/lib/hoodoo/presenters/types/float.rb +26 -0
  71. data/lib/hoodoo/presenters/types/hash.rb +361 -0
  72. data/lib/hoodoo/presenters/types/integer.rb +26 -0
  73. data/lib/hoodoo/presenters/types/object.rb +117 -0
  74. data/lib/hoodoo/presenters/types/string.rb +53 -0
  75. data/lib/hoodoo/presenters/types/tags.rb +24 -0
  76. data/lib/hoodoo/presenters/types/text.rb +26 -0
  77. data/lib/hoodoo/presenters/types/uuid.rb +54 -0
  78. data/lib/hoodoo/services.rb +34 -0
  79. data/lib/hoodoo/services/discovery/discoverers/by_consul.rb +66 -0
  80. data/lib/hoodoo/services/discovery/discoverers/by_convention.rb +173 -0
  81. data/lib/hoodoo/services/discovery/discoverers/by_drb/by_drb.rb +195 -0
  82. data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server.rb +166 -0
  83. data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server_start.rb +37 -0
  84. data/lib/hoodoo/services/discovery/discovery.rb +186 -0
  85. data/lib/hoodoo/services/discovery/results/for_amqp.rb +58 -0
  86. data/lib/hoodoo/services/discovery/results/for_http.rb +85 -0
  87. data/lib/hoodoo/services/discovery/results/for_local.rb +85 -0
  88. data/lib/hoodoo/services/discovery/results/for_remote.rb +57 -0
  89. data/lib/hoodoo/services/middleware/amqp_log_message.rb +186 -0
  90. data/lib/hoodoo/services/middleware/amqp_log_writer.rb +119 -0
  91. data/lib/hoodoo/services/middleware/endpoints/inter_resource_local.rb +130 -0
  92. data/lib/hoodoo/services/middleware/endpoints/inter_resource_remote.rb +202 -0
  93. data/lib/hoodoo/services/middleware/exception_reporting/base_reporter.rb +105 -0
  94. data/lib/hoodoo/services/middleware/exception_reporting/exception_reporting.rb +115 -0
  95. data/lib/hoodoo/services/middleware/exception_reporting/reporters/airbrake_reporter.rb +64 -0
  96. data/lib/hoodoo/services/middleware/exception_reporting/reporters/raygun_reporter.rb +63 -0
  97. data/lib/hoodoo/services/middleware/interaction.rb +127 -0
  98. data/lib/hoodoo/services/middleware/middleware.rb +2705 -0
  99. data/lib/hoodoo/services/middleware/rack_monkey_patch.rb +73 -0
  100. data/lib/hoodoo/services/services/context.rb +153 -0
  101. data/lib/hoodoo/services/services/implementation.rb +132 -0
  102. data/lib/hoodoo/services/services/interface.rb +934 -0
  103. data/lib/hoodoo/services/services/permissions.rb +250 -0
  104. data/lib/hoodoo/services/services/request.rb +189 -0
  105. data/lib/hoodoo/services/services/response.rb +316 -0
  106. data/lib/hoodoo/services/services/service.rb +141 -0
  107. data/lib/hoodoo/services/services/session.rb +729 -0
  108. data/lib/hoodoo/utilities.rb +12 -0
  109. data/lib/hoodoo/utilities/string_inquirer.rb +54 -0
  110. data/lib/hoodoo/utilities/utilities.rb +380 -0
  111. data/lib/hoodoo/utilities/uuid.rb +44 -0
  112. data/lib/hoodoo/version.rb +17 -0
  113. data/spec/active/active_record/base_spec.rb +57 -0
  114. data/spec/active/active_record/creator_spec.rb +88 -0
  115. data/spec/active/active_record/dated_spec.rb +248 -0
  116. data/spec/active/active_record/error_mapping_spec.rb +360 -0
  117. data/spec/active/active_record/finder_spec.rb +744 -0
  118. data/spec/active/active_record/search_helper_spec.rb +384 -0
  119. data/spec/active/active_record/secure_spec.rb +435 -0
  120. data/spec/active/active_record/support_spec.rb +225 -0
  121. data/spec/active/active_record/translated_spec.rb +19 -0
  122. data/spec/active/active_record/uuid_spec.rb +72 -0
  123. data/spec/active/active_record/writer_spec.rb +272 -0
  124. data/spec/alchemy/alchemy-amq.rb +33 -0
  125. data/spec/client/augmented_array_spec.rb +15 -0
  126. data/spec/client/augmented_base_spec.rb +50 -0
  127. data/spec/client/augmented_hash_spec.rb +15 -0
  128. data/spec/client/client_spec.rb +955 -0
  129. data/spec/client/endpoint/endpoint_spec.rb +70 -0
  130. data/spec/client/endpoint/endpoints/amqp_spec.rb +16 -0
  131. data/spec/client/endpoint/endpoints/auto_session_spec.rb +9 -0
  132. data/spec/client/endpoint/endpoints/http_based_spec.rb +9 -0
  133. data/spec/client/endpoint/endpoints/http_spec.rb +103 -0
  134. data/spec/client/endpoint/endpoints/not_found_spec.rb +35 -0
  135. data/spec/client/headers_spec.rb +172 -0
  136. data/spec/communicators/fast_spec.rb +9 -0
  137. data/spec/communicators/pool_spec.rb +339 -0
  138. data/spec/communicators/slow_spec.rb +15 -0
  139. data/spec/data/resources/caller_spec.rb +156 -0
  140. data/spec/data/resources/errors_spec.rb +22 -0
  141. data/spec/data/resources/log_spec.rb +20 -0
  142. data/spec/data/resources/session_spec.rb +15 -0
  143. data/spec/data/types/error_primitive_spec.rb +15 -0
  144. data/spec/data/types/permissions_defaults_spec.rb +25 -0
  145. data/spec/data/types/permissions_full_spec.rb +44 -0
  146. data/spec/data/types/permissions_resources_spec.rb +34 -0
  147. data/spec/data/types/permissions_spec.rb +37 -0
  148. data/spec/errors/error_descriptions_spec.rb +98 -0
  149. data/spec/errors/errors_spec.rb +346 -0
  150. data/spec/integration/service_actions_spec.rb +112 -0
  151. data/spec/logger/fast_writer_spec.rb +18 -0
  152. data/spec/logger/logger_spec.rb +259 -0
  153. data/spec/logger/slow_writer_spec.rb +144 -0
  154. data/spec/logger/writers/file_writer_spec.rb +37 -0
  155. data/spec/logger/writers/log_entries_dot_com_writer_spec.rb +29 -0
  156. data/spec/logger/writers/stream_writer_spec.rb +38 -0
  157. data/spec/presenters/base_dsl_spec.rb +111 -0
  158. data/spec/presenters/base_spec.rb +871 -0
  159. data/spec/presenters/common_resource_fields_spec.rb +30 -0
  160. data/spec/presenters/embedding_spec.rb +87 -0
  161. data/spec/presenters/types/array_spec.rb +249 -0
  162. data/spec/presenters/types/boolean_spec.rb +51 -0
  163. data/spec/presenters/types/date_spec.rb +57 -0
  164. data/spec/presenters/types/date_time_spec.rb +59 -0
  165. data/spec/presenters/types/decimal_spec.rb +58 -0
  166. data/spec/presenters/types/enum_spec.rb +71 -0
  167. data/spec/presenters/types/field_spec.rb +77 -0
  168. data/spec/presenters/types/float_spec.rb +50 -0
  169. data/spec/presenters/types/hash_spec.rb +1069 -0
  170. data/spec/presenters/types/integer_spec.rb +50 -0
  171. data/spec/presenters/types/object_spec.rb +177 -0
  172. data/spec/presenters/types/string_spec.rb +65 -0
  173. data/spec/presenters/types/tags_spec.rb +56 -0
  174. data/spec/presenters/types/text_spec.rb +50 -0
  175. data/spec/presenters/types/uuid_spec.rb +46 -0
  176. data/spec/presenters/walk_spec.rb +198 -0
  177. data/spec/services/discovery/discoverers/by_consul_spec.rb +29 -0
  178. data/spec/services/discovery/discoverers/by_convention_spec.rb +67 -0
  179. data/spec/services/discovery/discoverers/by_drb/by_drb_spec.rb +80 -0
  180. data/spec/services/discovery/discoverers/by_drb/drb_server_spec.rb +205 -0
  181. data/spec/services/discovery/discovery_spec.rb +73 -0
  182. data/spec/services/discovery/results/for_amqp_spec.rb +17 -0
  183. data/spec/services/discovery/results/for_http_spec.rb +37 -0
  184. data/spec/services/discovery/results/for_local_spec.rb +21 -0
  185. data/spec/services/discovery/results/for_remote_spec.rb +15 -0
  186. data/spec/services/middleware/amqp_log_message_spec.rb +60 -0
  187. data/spec/services/middleware/amqp_log_writer_spec.rb +95 -0
  188. data/spec/services/middleware/endpoints/inter_resource_local_spec.rb +9 -0
  189. data/spec/services/middleware/endpoints/inter_resource_remote_spec.rb +9 -0
  190. data/spec/services/middleware/exception_reporting/base_reporter_spec.rb +16 -0
  191. data/spec/services/middleware/exception_reporting/exception_reporting_spec.rb +92 -0
  192. data/spec/services/middleware/exception_reporting/reporters/airbrake_reporter_spec.rb +24 -0
  193. data/spec/services/middleware/exception_reporting/reporters/raygun_reporter_spec.rb +23 -0
  194. data/spec/services/middleware/middleware_cors_spec.rb +93 -0
  195. data/spec/services/middleware/middleware_create_update_spec.rb +489 -0
  196. data/spec/services/middleware/middleware_dated_at_spec.rb +186 -0
  197. data/spec/services/middleware/middleware_exotic_communication_spec.rb +560 -0
  198. data/spec/services/middleware/middleware_logging_spec.rb +356 -0
  199. data/spec/services/middleware/middleware_multi_local_spec.rb +1094 -0
  200. data/spec/services/middleware/middleware_multi_remote_spec.rb +1440 -0
  201. data/spec/services/middleware/middleware_permissions_spec.rb +1014 -0
  202. data/spec/services/middleware/middleware_public_spec.rb +238 -0
  203. data/spec/services/middleware/middleware_spec.rb +1569 -0
  204. data/spec/services/middleware/string_inquirer_spec.rb +30 -0
  205. data/spec/services/services/application_spec.rb +74 -0
  206. data/spec/services/services/context_spec.rb +48 -0
  207. data/spec/services/services/implementation_spec.rb +45 -0
  208. data/spec/services/services/interface_spec.rb +262 -0
  209. data/spec/services/services/permissions_spec.rb +249 -0
  210. data/spec/services/services/request_spec.rb +95 -0
  211. data/spec/services/services/response_spec.rb +250 -0
  212. data/spec/services/services/session_spec.rb +432 -0
  213. data/spec/spec_helper.rb +298 -0
  214. data/spec/utilities/utilities_spec.rb +537 -0
  215. data/spec/utilities/uuid_spec.rb +20 -0
  216. metadata +615 -0
@@ -0,0 +1,12 @@
1
+ ########################################################################
2
+ # File:: utilities.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Include various generalised bits of Hoodoo utility code.
6
+ # ----------------------------------------------------------------------
7
+ # 26-Jan-2015 (ADH): Split from top-level inclusion file.
8
+ ########################################################################
9
+
10
+ require 'hoodoo/utilities/utilities'
11
+ require 'hoodoo/utilities/string_inquirer'
12
+ require 'hoodoo/utilities/uuid'
@@ -0,0 +1,54 @@
1
+ ########################################################################
2
+ # File:: string_inquirer.rb
3
+ #
4
+ # Purpose:: StringInquirer class copied from ActiveSupport 4.1.6, to
5
+ # avoid dragging in that huge dependency for this one thing.
6
+ # ----------------------------------------------------------------------
7
+ # 02-Oct-2014 (ADH): Copied from ActiveSupport 4.1.6.
8
+ ########################################################################
9
+
10
+ module Hoodoo
11
+
12
+ # Given a string, provides an object that takes the string's value and
13
+ # turns it into a method "#{value}?", returning +true+; other methods
14
+ # all respond +false+.
15
+ #
16
+ # Example:
17
+ #
18
+ # greeting = Hoodoo::StringInquirer.new( 'hello' )
19
+ # greeting.hello? # => true
20
+ # greeting.hi? # => false
21
+ #
22
+ class StringInquirer < String
23
+
24
+ private
25
+
26
+ # Asks if this object can respond to a given method. In practice returns
27
+ # +true+ for any method name ending in "?".
28
+ #
29
+ # +method_name+:: Method name to ask about
30
+ # +include_private+:: If +true+ include private methods in the test,
31
+ # else if omitted or +false+, ignore them.
32
+ #
33
+ def respond_to_missing?( method_name, include_private = false )
34
+ method_name[ -1 ] == '?'
35
+ end
36
+
37
+ # Called when a String receives a message it cannot handle. This is where
38
+ # StringInquirer adds in its string-value-dependent "fake" boolean method.
39
+ # For any method name ending in "?", returns +true+ if the string value
40
+ # matches the name except for that "?", else +false+. If the method name
41
+ # does not end in "?", the call is passed to +super+.
42
+ #
43
+ # +method_name+:: Method name that wasn't found in object instance.
44
+ # *arguments:: List of arguments passed to the method.
45
+ #
46
+ def method_missing( method_name, *arguments )
47
+ if method_name[ -1 ] == '?'
48
+ self == method_name[ 0..-2 ]
49
+ else
50
+ super
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,380 @@
1
+ ########################################################################
2
+ # File:: utilities.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Miscellaneous useful functions.
6
+ # ----------------------------------------------------------------------
7
+ # 22-Sep-2014 (ADH): Created.
8
+ ########################################################################
9
+
10
+ require 'socket'
11
+
12
+ module Hoodoo
13
+
14
+ # Useful tools, especially for those working without Rails components.
15
+ #
16
+ module Utilities
17
+
18
+ # Validation regular expression for DateTime subset selection.
19
+ #
20
+ DATETIME_ISO8601_SUBSET_REGEXP = /(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(\.\d+)?(Z|[+-](\d{2})\:(\d{2}))/
21
+
22
+ # Validation regular expression for Date subset selection.
23
+ #
24
+ DATE_ISO8601_SUBSET_REGEXP = /(\d{4})-(\d{2})-(\d{2})/
25
+
26
+ # Given a hash, returns the same hash with keys converted to symbols.
27
+ # Works with nested hashes.
28
+ #
29
+ # +obj+:: Hash or Array of Hashes. Will recursively convert keys in Hashes
30
+ # to symbols. Hashes with values that are Arrays of Hashes will be
31
+ # dealt with properly. Does not modify other types (e.g. an Array
32
+ # of Strings would stay an Array of Strings).
33
+ #
34
+ # Returns a copy of your input object with keys converted to symbols.
35
+ #
36
+ def self.symbolize(obj)
37
+
38
+ # http://stackoverflow.com/questions/800122/best-way-to-convert-strings-to-symbols-in-hash
39
+ #
40
+ return obj.inject({}){|memo,(k,v)| memo[k.to_s.to_sym] = self.symbolize(v); memo} if obj.is_a?(::Hash)
41
+ return obj.inject([]){|memo,v | memo << self.symbolize(v); memo} if obj.is_a?(::Array)
42
+ return obj
43
+ end
44
+
45
+ # The keys-to-strings equivalent of ::symbolize.
46
+ #
47
+ # +obj+:: Hash or Array of Hashes. Will recursively convert keys in Hashes
48
+ # to strings. Hashes with values that are Arrays of Hashes will be
49
+ # dealt with properly. Does not modify other types (e.g. an Array
50
+ # of Symbols would stay an Array of Symbols).
51
+ #
52
+ # Returns a copy of your input object with keys converted to strings.
53
+ #
54
+ def self.stringify(obj)
55
+ return obj.inject({}){|memo,(k,v)| memo[k.to_s] = self.stringify(v); memo} if obj.is_a?(::Hash)
56
+ return obj.inject([]){|memo,v | memo << self.stringify(v); memo} if obj.is_a?(::Array)
57
+ return obj
58
+ end
59
+
60
+ # Thorough but slow deep duplication of any object (if it isn't
61
+ # duplicable, e.g. FixNum, you just get the same thing back). Usually
62
+ # used with Hashes or Arrays.
63
+ #
64
+ # +obj+:: Object to duplicate.
65
+ #
66
+ # Returns the duplicated object if duplicable, else returns the input
67
+ # parameter.
68
+ #
69
+ def self.deep_dup( obj )
70
+ duplicate = obj.dup rescue obj
71
+
72
+ result = if duplicate.is_a?( Hash )
73
+ duplicate.each { | k, v | duplicate[ k ] = deep_dup( v ) }
74
+ elsif duplicate.is_a?( Array )
75
+ duplicate.map { | entry | deep_dup( entry ) }
76
+ else
77
+ duplicate
78
+ end
79
+
80
+ return result
81
+ end
82
+
83
+ # Deep merge two hashes.
84
+ #
85
+ # Hash#merge/merge! only do a shallow merge. For example, without
86
+ # a block, when starting with this hash:
87
+ #
88
+ # { :one => { :two => { :three => 3 } } }
89
+ #
90
+ # ...and merging in this hash:
91
+ #
92
+ # { :one => { :two => { :and_four => 4 } } }
93
+ #
94
+ # ...the possibly unexpected result is this:
95
+ #
96
+ # { :one => { :two => { :and_four => 4 } } }
97
+ #
98
+ # Because the value for key ":one" in the original hash is simply
99
+ # overwritten with the value from the merged-in hash.
100
+ #
101
+ # Deep merging takes a target hash, into which an "inbound" source
102
+ # hash is merged and returns a new hash that is the deep merged
103
+ # result. Taking the above example:
104
+ #
105
+ # target_hash = { :one => { :two => { :three => 3 } } }
106
+ # inbound_hash = { :one => { :two => { :and_four => 4 } } }
107
+ # Hoodoo::Utilities.deep_merge_into( target_hash, inbound_hash )
108
+ #
109
+ # ...yields:
110
+ #
111
+ # { :one => { :two => { :three => 3, :and_four => 4 } } }
112
+ #
113
+ # For any same-named key with a non-hash value, the value in the
114
+ # inbound hash will overwrite the value in the target hash.
115
+ #
116
+ # Parameters:
117
+ #
118
+ # +target_hash+:: The hash into which something will be merged.
119
+ # +inbound_hash+:: The hash that will be merged into the target.
120
+ #
121
+ # Returns the merged result.
122
+ #
123
+ def self.deep_merge_into( target_hash, inbound_hash )
124
+
125
+ # http://stackoverflow.com/questions/9381553/ruby-merge-nested-hash
126
+ #
127
+ merger = proc { | key, v1, v2 |
128
+ Hash === v1 && Hash === v2 ? v1.merge( v2, &merger ) : v2.nil? ? v1 : v2
129
+ }
130
+
131
+ return target_hash.merge( inbound_hash, &merger )
132
+ end
133
+
134
+ # Deep diff two hashes.
135
+ #
136
+ # +hash1+:: "Left hand" hash for comparison.
137
+ # +hash2+:: "Right hand" hash for comparison.
138
+ #
139
+ # The returned result is a Hash itself, potentially nested, with any
140
+ # present key paths leading to an array describing the difference found
141
+ # at that key path. If the two input hashes had values at the path, the
142
+ # differing values are placed in the array ("left hand" value at index
143
+ # 0, "right hand" at index 1). If one input hash has a key leading to
144
+ # a value which the other omits, the array contains +nil+ for the
145
+ # omitted entry.
146
+ #
147
+ # Example:
148
+ #
149
+ # hash1 = { :foo => { :bar => 2 }, :baz => true, :boo => false }
150
+ # hash2 = { :foo => { :bar => 3 }, :boo => false }
151
+ #
152
+ # Hoodoo::Utilities.hash_diff( hash1, hash2 )
153
+ # # => { :foo => { :bar => [ 2, 3 ] }, :baz => [ true, nil ] }
154
+ #
155
+ # Hoodoo::Utilities.hash_diff( hash2, hash1 )
156
+ # # => { :foo => { :bar => [ 3, 2 ] }, :baz => [ nil, true ] }
157
+ #
158
+ # Bear in mind that the difference array contains values of everything
159
+ # different from the first part of the key path where things diverge. So
160
+ # in this case:
161
+ #
162
+ # hash1 = { :foo => { :bar => { :baz => [ 1, 2, 3 ] } } }
163
+ # hash2 = {}
164
+ #
165
+ # ...the difference starts all the way up at ":foo". The result is thus
166
+ # *not* a Hash where just the ":baz" array is picked out as a difference;
167
+ # the entire Hash sub-tree is picked out:
168
+ #
169
+ # diff = Hoodoo::Utilities.hash_diff( hash1, hash2 )
170
+ # # => { :foo => [ { :bar => { :baz => [ 1, 2, 3 ] } }, nil ] }
171
+ #
172
+ def self.hash_diff( hash1, hash2 )
173
+
174
+ # http://stackoverflow.com/questions/1766741/comparing-ruby-hashes
175
+ #
176
+ return ( hash1.keys | hash2.keys ).inject( {} ) do | memo, key |
177
+ unless hash1[ key ] == hash2[ key ]
178
+ if hash1[ key ].kind_of?( Hash ) && hash2[ key ].kind_of?( Hash )
179
+ memo[ key ] = hash_diff( hash1[ key ], hash2[ key ] )
180
+ else
181
+ memo[ key ] = [ hash1[ key ], hash2[ key ] ]
182
+ end
183
+ end
184
+
185
+ memo
186
+ end
187
+ end
188
+
189
+ # Convert a (potentially nested) Hash into an array of entries which
190
+ # represent its keys, with the notation "foo.bar.baz" for nested hashes.
191
+ #
192
+ # +hash+:: Input Hash.
193
+ #
194
+ # Example:
195
+ #
196
+ # hash = { :foo => 1, :bar => { :baz => 2, :boo => { :hello => :world } } }
197
+ #
198
+ # Hoodoo::Utilities.hash_key_paths( hash )
199
+ # # => [ 'foo', 'bar.baz', 'bar.boo.hello' ]
200
+ #
201
+ def self.hash_key_paths( hash )
202
+ return hash.map do | key, value |
203
+ if value.is_a?( Hash )
204
+ hash_key_paths( value ).map do | entry |
205
+ "#{ key }.#{ entry }"
206
+ end
207
+ else
208
+ key.to_s
209
+ end
210
+ end.flatten
211
+ end
212
+
213
+ # A very single-purpose method which converts an Array of specifc form
214
+ # into a Hash.
215
+ #
216
+ # The Hash class can already build itself from an Array of tuples, thus:
217
+ #
218
+ # array = [ [ :one, 1 ], [ :two, 2 ] ]
219
+ # hash = Hash[ array ]
220
+ # # => { :one => 1, :two => 2 }
221
+ #
222
+ # This is fine, but what if the array contains the same key twice?
223
+ #
224
+ # array = [ [ :one, 1 ], [ :two, 2 ], [ :one, 42 ] ]
225
+ # hash = Hash[ array ]
226
+ # # => { :one => 42, :two => 2 }
227
+ #
228
+ # The later duplicates simply override any former entries. This Array
229
+ # collation method is designed to instead take the tuples and set up a
230
+ # Hash where each key leads to an Array of unique values found in the
231
+ # original, thus:
232
+ #
233
+ # array = [ [ :one, 1 ], [ :two, 2 ], [ :one, 42 ] ]
234
+ # Hoodoo::Utilities.collated_hash_from( array )
235
+ # # => { :one => [ 1, 42 ], :two => [ 2 ] }
236
+ #
237
+ # Note that:
238
+ #
239
+ # * The Hash values are always Arrays, even if they only have one value.
240
+ # * The Array values are unique; duplicates are removed via +uniq!+.
241
+ #
242
+ # +array+:: Array of two-element Arrays. The first element becomes a key
243
+ # in the returned Hash. The last element is added to an Array
244
+ # of (unique) values associated with that key. An empty Array
245
+ # results in an empty Hash; +nil+ is not allowed.
246
+ #
247
+ # +dupes+:: Optional. If omitted, duplicates are removed as described;
248
+ # if present and +true+, duplicates are allowed.
249
+ #
250
+ # Returns a new Hash as described. The input Array is not modified.
251
+ #
252
+ def self.collated_hash_from( array, dupes = false )
253
+ hash_of_arrays = {}
254
+
255
+ array.reduce( hash_of_arrays ) do | memo, sub_array |
256
+ memo[ sub_array.first ] = ( memo[ sub_array.first ] || [] ) << sub_array.last
257
+ memo
258
+ end
259
+
260
+ hash_of_arrays.values.collect( &:uniq! ) unless dupes == true
261
+ return hash_of_arrays
262
+ end
263
+
264
+ # Is a parameter convertable to an integer cleanly? Returns the integer
265
+ # value if so, else +nil+.
266
+ #
267
+ # +value+:: Value to check, e.g. 2, "44", :'55' (yields 2, 44, 55) or
268
+ # "hello", Time.now (yields nil, nil).
269
+ #
270
+ def self.to_integer?( value )
271
+ value = value.to_s
272
+ value.to_i if value.to_i.to_s == value
273
+ end
274
+
275
+ # Return a spare TCP port on localhost. This is free at the instant of
276
+ # calling, though of course if you have anything in other local machine
277
+ # processes/threads running which might start using ports at any moment,
278
+ # there's a chance of the free port getting claimed in between you asking
279
+ # for it and it being returned. This utility method is usually therefore
280
+ # used for test environments only.
281
+ #
282
+ def self.spare_port
283
+
284
+ # http://stackoverflow.com/questions/5985822/how-do-you-find-a-random-open-port-in-ruby
285
+ #
286
+ socket = Socket.new( :INET, :STREAM, 0 )
287
+ socket.bind( Addrinfo.tcp( '127.0.0.1', 0 ) )
288
+ port = socket.local_address.ip_port
289
+ socket.close
290
+
291
+ return port
292
+ end
293
+
294
+ # Is the given String a valid ISO 8601 subset date and time as accepted by
295
+ # (for example) Hoodoo API calls?
296
+ #
297
+ # +str+:: Value to check
298
+ #
299
+ # Returns a DateTime instance holding the parsed result if a valid ISO
300
+ # 8601 subset date and time, else +false+.
301
+ #
302
+ def self.valid_iso8601_subset_datetime?( str )
303
+
304
+ # Relies on Ruby evaluation behaviour and operator precedence - "'foo'
305
+ # && true" => true, but "true && 'foo'" => 'foo'. Don't use "and" here!
306
+
307
+ value = begin
308
+ ( DATETIME_ISO8601_SUBSET_REGEXP =~ str.to_s ) == 0 &&
309
+ str.size > 10 &&
310
+ ::DateTime.parse( str )
311
+
312
+ rescue ArgumentError
313
+ end
314
+
315
+ return value.is_a?( ::DateTime ) && value
316
+ end
317
+
318
+ # Is the given String a valid ISO 8601 subset date (no time) as accepted
319
+ # by (for example) Hoodoo API calls?
320
+ #
321
+ # +str+:: Value to check
322
+ #
323
+ # Returns a Date instance holding the parsed result if a valid ISO 8601
324
+ # subset date, else +false+.
325
+ #
326
+ def self.valid_iso8601_subset_date?( str )
327
+
328
+ # Same reliance as 'valid_iso8601_subset_datetime'?.
329
+
330
+ value = begin
331
+ ( DATE_ISO8601_SUBSET_REGEXP =~ str.to_s ) == 0 &&
332
+ str.size == 10 &&
333
+ ::Date.parse( str )
334
+
335
+ rescue ArgumentError
336
+ end
337
+
338
+ return value.is_a?( ::Date ) && value
339
+ end
340
+
341
+ # Returns an ISO 8601 String equivalent of the given Time or DateTime
342
+ # instance, with nanosecond precision (subject to Ruby port / OS support).
343
+ # This is nothing more than a standardised central interface on calling
344
+ # Ruby's <tt>Time/DateTime#iso8601( 9 )</tt>, to avoid the risk of lots of
345
+ # variable length precision times floating around by authors picking their
346
+ # own arbitrary precision parameters.
347
+ #
348
+ # +date_time+:: Ruby Time or DateTime instance to convert to an ISO 8601
349
+ # String with nanosecond precision.
350
+ #
351
+ def self.nanosecond_iso8601( time_or_date_time )
352
+ time_or_date_time.iso8601( 9 )
353
+ end
354
+
355
+ # Turn a given value of various types into a DateTime instance or +nil+.
356
+ # If the input value is not +nil+, a DateTime instance, a Time instance
357
+ # or something that <tt>DateTime.parse</tt> can handle, the method will
358
+ # throw a RuntimeError exception.
359
+ #
360
+ # +input+:: A Time or DateTime instance, or a String that can be
361
+ # converted to a DateTime instance; in these cases, an
362
+ # equivalent DateTime is returned. If +nil+, returns +nil+.
363
+ #
364
+ def self.rationalise_datetime( input )
365
+ begin
366
+ if input.nil? || input.is_a?( DateTime )
367
+ input
368
+ elsif input.is_a?( Time )
369
+ input.to_datetime
370
+ else
371
+ DateTime.parse( input )
372
+ end
373
+
374
+ rescue
375
+ raise "Hoodoo::Utilities\#rationalise_datetime: Invalid parameter '#{ input }'"
376
+
377
+ end
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,44 @@
1
+ require "uuidtools"
2
+
3
+ module Hoodoo
4
+
5
+ # Class that handles generation and validation of UUIDs. Whenever you
6
+ # want to associate an identifier with something, you should use this
7
+ # class rather than (e.g.) relying on identifiers generated by a
8
+ # database. This allows you to cluster your database later on, should
9
+ # your application become big enough, without having to worry about
10
+ # ID synchronisation across instances.
11
+ #
12
+ class UUID
13
+
14
+ # A regexp which, as its name suggests, only matches a string that
15
+ # contains 16 pairs of hex digits (with upper or lower case A-F).
16
+ #
17
+ # http://stackoverflow.com/questions/287684/regular-expression-to-validate-hex-string
18
+ #
19
+ MATCH_16_PAIRS_OF_HEX_DIGITS = /^([[:xdigit:]]{2}){16}$/
20
+
21
+ # Generate a unique identifier. Returns a 32 character string.
22
+ #
23
+ def self.generate
24
+ UUIDTools::UUID.random_create().hexdigest()
25
+ end
26
+
27
+ # Checks if a UUID string is valid. Returns +true+ if so, else +false+.
28
+ #
29
+ # +uuid+:: UUID string to validate. Must be a String, 32 characters
30
+ # long (as 16 hex digit pairs) and parse to a valid result
31
+ # internally, according to internal UUID generation rules.
32
+ #
33
+ # Note that the validity of a UUID says nothing about where, if anywhere,
34
+ # it might have been used. So, just because a UUID is valid, doesn't mean
35
+ # you have (say) stored something with that UUID as the primary key in a
36
+ # row in a database.
37
+ #
38
+ def self.valid?( uuid )
39
+ uuid.is_a?( ::String ) &&
40
+ ( uuid =~ MATCH_16_PAIRS_OF_HEX_DIGITS ) != nil &&
41
+ UUIDTools::UUID.parse_hexdigest( uuid ).valid?()
42
+ end
43
+ end
44
+ end