hoodoo 1.0.2

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 (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,194 @@
1
+ ########################################################################
2
+ # File:: auto_session.rb
3
+ # (C):: Loyalty New Zealand 2015
4
+ #
5
+ # Purpose:: Resource endpoint definition.
6
+ # ----------------------------------------------------------------------
7
+ # 12-Mar-2015 (ADH): Created.
8
+ ########################################################################
9
+
10
+ module Hoodoo
11
+ class Client # Just used as a namespace here
12
+ class Endpoint # Just used as a namespace here
13
+
14
+ # This endpoint wraps something which does _actual_ communication but
15
+ # requires a session; it maintains a valid session for that wrapped
16
+ # endpoint automatically. It implements the following model:
17
+ #
18
+ # - It requires a Caller ID and Caller Authentication Secret to be
19
+ # instantiated (via the +options+ parameter).
20
+ #
21
+ # - If there is no session ID available, it creates one using the above
22
+ # details. Otherwise, it tries to use the given session.
23
+ #
24
+ # - If a particular request leads to an 'invalid session' response, the
25
+ # request is marked for retry. A new session is obtained first, the
26
+ # retry happens and if it fails this time, the failure is returned to
27
+ # the caller.
28
+ #
29
+ # Since it wraps another endpoint and requires Caller information to be
30
+ # able to build sessions, instantiation requirements are rather unusual
31
+ # - see #configure_with for details.
32
+ #
33
+ class AutoSession < Hoodoo::Client::Endpoint
34
+
35
+ protected
36
+
37
+ # See Hoodoo::Client::Endpoint#configure_with.
38
+ #
39
+ # Configuration option keys which _must_ be supplied are:
40
+ #
41
+ # +caller_id+:: The UUID of the Caller instance to be used
42
+ # for session creation.
43
+ #
44
+ # +caller_secret+:: The authentication secret of the Caller
45
+ # instance to be used for session creation.
46
+ #
47
+ # +session_endpoint:: A Hooodo::Client::Endpoint subclass which
48
+ # can be used for talking to the Session
49
+ # endpoint (for obvious reasons!).
50
+ #
51
+ # +discovery_result+:: A Hoodoo::Services::Discovery::ForRemote
52
+ # instance describing the required, remotely
53
+ # available resource endpoint.
54
+ #
55
+ # The pattern for creating and using this instance is:
56
+ #
57
+ # * Discover the location of the remote resource.
58
+ #
59
+ # * Use the discovery result to build an appropriate Endpoint
60
+ # subclass instance, e.g. Hoodoo::Client::Endpoint::HTTP.
61
+ #
62
+ # * Create a Hoodoo::Services::Discovery::ForRemote instance which
63
+ # describes the above endpoint via the +wrapped_endpoint+ option.
64
+ #
65
+ # * Build an instance of this auto-session Endpoint subclass,
66
+ # giving it the above object as the +discovery_result+.
67
+ #
68
+ # * Use this endpoint in the normal fashion. All the special
69
+ # mechanics of session management are handled here.
70
+ #
71
+ def configure_with( resource, version, options )
72
+ @caller_id = options[ :caller_id ]
73
+ @caller_secret = options[ :caller_secret ]
74
+ @session_endpoint = options[ :session_endpoint ]
75
+ @wrapped_endpoint = @discovery_result.wrapped_endpoint
76
+ end
77
+
78
+ public
79
+
80
+ # See Hoodoo::Client::Endpoint#list.
81
+ #
82
+ def list( query_hash = nil )
83
+ return auto_retry( :list, query_hash )
84
+ end
85
+
86
+ # See Hoodoo::Client::Endpoint#show.
87
+ #
88
+ def show( ident, query_hash = nil )
89
+ return auto_retry( :show, ident, query_hash )
90
+ end
91
+
92
+ # See Hoodoo::Client::Endpoint#create.
93
+ #
94
+ def create( body_hash, query_hash = nil )
95
+ return auto_retry( :create, body_hash, query_hash )
96
+ end
97
+
98
+ # See Hoodoo::Client::Endpoint#update.
99
+ #
100
+ def update( ident, body_hash, query_hash = nil )
101
+ return auto_retry( :update, ident, body_hash, query_hash )
102
+ end
103
+
104
+ # See Hoodoo::Client::Endpoint#delete.
105
+ #
106
+ def delete( ident, query_hash = nil )
107
+ return auto_retry( :delete, ident, query_hash )
108
+ end
109
+
110
+ private
111
+
112
+ # Try to perform an action through the wrapped endpoint, acquiring a
113
+ # session first if need be or if necessary reacquiring a session and
114
+ # retrying the request.
115
+ #
116
+ # +action+:: The name of the method to call in the wrapped endpoint
117
+ # - see Hoodoo::Services::Middleware::ALLOWED_ACTIONS.
118
+ #
119
+ # *args:: Any other arguments to pass to +action+.
120
+ #
121
+ def auto_retry( action, *args )
122
+
123
+ copy_updated_options_to( @wrapped_endpoint )
124
+
125
+ # We use the session endpoint as a session ID cache, in essence,
126
+ # storing the acquired ID there and passing it into the wrapped
127
+ # endpoint for the 'real' calls.
128
+
129
+ if @session_endpoint.session_id.nil?
130
+ session_creation_result = acquire_session_for( action )
131
+ return session_creation_result unless session_creation_result.nil?
132
+ else
133
+ @wrapped_endpoint.session_id = @session_endpoint.session_id
134
+ end
135
+
136
+ result = @wrapped_endpoint.send( action, *args )
137
+
138
+ if result.platform_errors.has_errors? &&
139
+ result.platform_errors.errors.size == 1 &&
140
+ result.platform_errors.errors[ 0 ][ 'code' ] == 'platform.invalid_session'
141
+
142
+ session_creation_result = acquire_session_for( action )
143
+ return session_creation_result unless session_creation_result.nil?
144
+ return @wrapped_endpoint.send( action, *args )
145
+ else
146
+ return result
147
+ end
148
+ end
149
+
150
+ # Acquire a sessino using the configured session endpoint. If this
151
+ # fails, the failure result is returned. If it seems to succeed but
152
+ # a session ID cannot be found, an internal 'generic.malformed'
153
+ # result is generated and returned.
154
+ #
155
+ # The returned data uses an appropriate response class for the
156
+ # action at hand - an augmented array for lists, else an augmented
157
+ # hash. It can be returned directly up to the calling layer.
158
+ #
159
+ # Returns +nil+ if all goes well; #session_id will be updated.
160
+ #
161
+ # +action+:: As given to #auto_retry.
162
+ #
163
+ def acquire_session_for( action )
164
+ session_creation_result = @session_endpoint.create(
165
+ 'caller_id' => @caller_id,
166
+ 'authentication_secret' => @caller_secret
167
+ )
168
+
169
+ if session_creation_result.platform_errors.has_errors?
170
+ data = response_class_for( action ).new
171
+ data.platform_errors.merge!( session_creation_result.platform_errors )
172
+ return data
173
+ end
174
+
175
+ @session_endpoint.session_id = session_creation_result[ 'id' ]
176
+
177
+ if @session_endpoint.session_id.nil? || @session_endpoint.session_id.empty?
178
+ data = response_class_for( action ).new
179
+ data.platform_errors.add_error(
180
+ 'generic.malformed',
181
+ 'message' => 'Received bad session description from Session endpoint despite "200" response code'
182
+ )
183
+
184
+ return data
185
+ else
186
+ @wrapped_endpoint.session_id = @session_endpoint.session_id
187
+ return nil
188
+ end
189
+ end
190
+
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,203 @@
1
+ ########################################################################
2
+ # File:: http.rb
3
+ # (C):: Loyalty New Zealand 2015
4
+ #
5
+ # Purpose:: Resource endpoint definition.
6
+ # ----------------------------------------------------------------------
7
+ # 05-Mar-2015 (ADH): Created.
8
+ ########################################################################
9
+
10
+ require 'net/http'
11
+ require 'net/https'
12
+
13
+ module Hoodoo
14
+ class Client # Just used as a namespace here
15
+ class Endpoint # Just used as a namespace here
16
+
17
+ # Talk to a resource that is contacted over HTTP or HTTPS.
18
+ #
19
+ # Configured with a Hoodoo::Services::Discovery::ForHTTP discovery
20
+ # result instance.
21
+ #
22
+ class HTTP < Hoodoo::Client::Endpoint::HTTPBased
23
+
24
+ protected
25
+
26
+ # See Hoodoo::Client::Endpoint#configure_with.
27
+ #
28
+ # Requires a Hoodoo::Services::Discovery::ForHTTP instance in the
29
+ # +discovery_result+ field of the +options+ Hash.
30
+ #
31
+ def configure_with( resource, version, options )
32
+ unless @discovery_result.is_a?( Hoodoo::Services::Discovery::ForHTTP )
33
+ raise "Hoodoo::Client::Endpoint::HTTP must be configured with a Hoodoo::Services::Discovery::ForHTTP instance - got '#{ @discovery_result.class.name }'"
34
+ end
35
+
36
+ @description = Hoodoo::Client::Endpoint::HTTPBased::DescriptionOfRequest.new
37
+ @description.discovery_result = @discovery_result
38
+ @description.endpoint_uri = @discovery_result.endpoint_uri
39
+ @description.proxy_uri = @discovery_result.proxy_uri
40
+ @description.ca_file = @discovery_result.ca_file
41
+ @description.http_timeout = @discovery_result.http_timeout
42
+ end
43
+
44
+ public
45
+
46
+ # See Hoodoo::Client::Endpoint#list.
47
+ #
48
+ def list( query_hash = nil )
49
+ d = @description.dup # This does NOT dup the objects to which @description points
50
+ d.action = :list
51
+ d.query_hash = query_hash
52
+
53
+ return do_http( d )
54
+ end
55
+
56
+ # See Hoodoo::Client::Endpoint#show.
57
+ #
58
+ def show( ident, query_hash = nil )
59
+ d = @description.dup
60
+ d.action = :show
61
+ d.ident = ident
62
+ d.query_hash = query_hash
63
+
64
+ return do_http( d )
65
+ end
66
+
67
+ # See Hoodoo::Client::Endpoint#create.
68
+ #
69
+ def create( body_hash, query_hash = nil )
70
+ d = @description.dup
71
+ d.action = :create
72
+ d.body_hash = body_hash
73
+ d.query_hash = query_hash
74
+
75
+ return do_http( d )
76
+ end
77
+
78
+ # See Hoodoo::Client::Endpoint#update.
79
+ #
80
+ def update( ident, body_hash, query_hash = nil )
81
+ d = @description.dup
82
+ d.action = :update
83
+ d.ident = ident
84
+ d.body_hash = body_hash
85
+ d.query_hash = query_hash
86
+
87
+ return do_http( d )
88
+ end
89
+
90
+ # See Hoodoo::Client::Endpoint#delete.
91
+ #
92
+ def delete( ident, query_hash = nil )
93
+ d = @description.dup
94
+ d.action = :delete
95
+ d.ident = ident
96
+ d.query_hash = query_hash
97
+
98
+ return do_http( d )
99
+ end
100
+
101
+ private
102
+
103
+ # Make real HTTP(S) request to a target resource and return the
104
+ # result as a Hoodoo::Client::AugmentedArray (for 'list' calls) or
105
+ # Hoodoo::Client::AugumentedHash (for all other calls) instance.
106
+ #
107
+ # +description_of_request+:: A Hoodoo::Client::Endpoint::HTTPBased::DescriptionOfRequest
108
+ # instance with all the request details
109
+ # set inside. The +discovery_data+ field
110
+ # must refer to a
111
+ # Hoodoo::Services::Discovery::ForHTTP
112
+ # instance (not re-checked internally).
113
+ #
114
+ def do_http( description_of_request )
115
+
116
+ data = get_data_for_request( description_of_request )
117
+
118
+ action = description_of_request.action
119
+ proxy = description_of_request.proxy_uri
120
+ ca_file = description_of_request.ca_file
121
+ http_timeout = description_of_request.http_timeout
122
+
123
+ proxy_host = :ENV
124
+ proxy_port = proxy_user = proxy_pass = nil
125
+
126
+ unless proxy.nil?
127
+ proxy_host = proxy.host
128
+ proxy_port = proxy.port
129
+ proxy_user = proxy.user
130
+ proxy_pass = proxy.password
131
+ end
132
+
133
+ http = Net::HTTP.new(
134
+ data.full_uri.host,
135
+ data.full_uri.port,
136
+ proxy_host,
137
+ proxy_port,
138
+ proxy_user,
139
+ proxy_pass
140
+ )
141
+
142
+ if data.full_uri.scheme == 'https'
143
+ http.use_ssl = true
144
+
145
+ # The verify_mode is *important* - VERIFY_PEER ensures that we always validate
146
+ # the connection, *and* that the presented SSL Certificate by the endpoint is
147
+ # verifiable through our CA certificate trust store.
148
+ #
149
+ # To use a self-signed cert, you may configure the ca_file to a CA that
150
+ # includes the self-signed cert, but the verify_mode setting should remain.
151
+ #
152
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
153
+
154
+ if ca_file
155
+ http.ca_file = ca_file
156
+ end
157
+ end
158
+
159
+ http.read_timeout = http_timeout unless http_timeout.nil?
160
+
161
+ request_class = {
162
+ :create => Net::HTTP::Post,
163
+ :update => Net::HTTP::Patch,
164
+ :delete => Net::HTTP::Delete
165
+ }[ action ] || Net::HTTP::Get
166
+
167
+ request = request_class.new( data.full_uri.request_uri() )
168
+ request.body = data.body_string unless data.body_string.empty?
169
+
170
+ request.initialize_http_header( data.header_hash )
171
+
172
+ description_of_response = DescriptionOfResponse.new
173
+ description_of_response.action = action
174
+ description_of_response.http_headers = {}
175
+
176
+ begin
177
+ http_response = http.request( request )
178
+
179
+ description_of_response.http_status_code = http_response.code.to_i
180
+ description_of_response.http_headers = http_response
181
+ description_of_response.raw_body_data = http_response.body
182
+
183
+ rescue Errno::ECONNREFUSED => e
184
+ description_of_response.http_status_code = 404
185
+ description_of_response.raw_body_data = ''
186
+
187
+ rescue Net::ReadTimeout => e
188
+ description_of_response.http_status_code = 408
189
+ description_of_response.raw_body_data = ''
190
+
191
+ rescue => e
192
+ description_of_response.http_status_code = 500
193
+ description_of_response.raw_body_data = e.message
194
+
195
+ end
196
+
197
+ return get_data_for_response( description_of_response )
198
+ end
199
+
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,367 @@
1
+ ########################################################################
2
+ # File:: http_based.rb
3
+ # (C):: Loyalty New Zealand 2015
4
+ #
5
+ # Purpose:: Resource endpoint definition.
6
+ # ----------------------------------------------------------------------
7
+ # 05-Mar-2015 (ADH): Created.
8
+ ########################################################################
9
+
10
+ require 'json'
11
+
12
+ module Hoodoo
13
+ class Client # Just used as a namespace here
14
+ class Endpoint # Just used as a namespace here
15
+
16
+ # Base class for endpoints that have an HTTP basis to their request
17
+ # and responses, even if the underlying transport is not HTTP. This
18
+ # is basically a collection of library-like routines useful to such
19
+ # classes and specifically excludes the part which actually makes
20
+ # an HTTP call (or AMQP call, or whatever) to a resource. That's up
21
+ # to the subclass.
22
+ #
23
+ # This must never be instantiated directly as an endpoint. Instead,
24
+ # instantiate a subclass such as Hoodoo::Client::Endpoint::HTTP or
25
+ # Hoodoo::Client::Endpoint::AMQP.
26
+ #
27
+ class HTTPBased < Hoodoo::Client::Endpoint
28
+
29
+ protected
30
+
31
+ # Describe a request for HTTP-like endpoints.
32
+ #
33
+ class DescriptionOfRequest
34
+
35
+ # The action to perform - a Symbol from
36
+ # Hoodoo::Services::Middleware::ALLOWED_ACTIONS.
37
+ #
38
+ attr_accessor :action
39
+
40
+ # A Hoodoo::Services::Discovery "For..." family member instance
41
+ # giving information required to 'find' the target resource. The
42
+ # required class instance depends upon the endpoint in use.
43
+ #
44
+ attr_accessor :discovery_result
45
+
46
+ # The full HTTP URI (or equivalent HTTP URI for HTTP-like, but
47
+ # non-HTTP systems like AMQP) at which the endpoint is found.
48
+ # Excludes any query string or resource identifier portion (it
49
+ # is the "list" action URI without query data, in essence)
50
+ #
51
+ attr_accessor :endpoint_uri
52
+
53
+ # Full URI (as a URI object) of an HTTP proxy to use as an
54
+ # override to <tt>ENV['HTTP_PROXY']</tt> which Ruby itself
55
+ # will otherwise read. Will be +nil+ for no proxy override.
56
+ #
57
+ attr_accessor :proxy_uri
58
+
59
+ # An optional String indicating a relative or absolute file
60
+ # path to the location of a .pem format Certificate
61
+ # Authority file (trust store), which may include multliple
62
+ # certificates. The certificates in the file will be used
63
+ # by Net::HTTP to validate the SSL Ceritificate Chain
64
+ # presented by remote servers, when calling endpoints over
65
+ # HTTPS with Hoodoo::Client.
66
+ #
67
+ # Default +nil+ value should be used in nearly all cases
68
+ # and uses Ruby OpenSSL defaults which are generally
69
+ # Operating System provided.
70
+ #
71
+ attr_accessor :ca_file
72
+
73
+ # Optional Float indicating the Net::HTTP read timeout value.
74
+ # This operates at the HTTP transport level and is independent
75
+ # of any timeouts set within the API providing server.
76
+ #
77
+ attr_accessor :http_timeout
78
+
79
+ # Optional Hash of query data.
80
+ #
81
+ attr_accessor :query_hash
82
+
83
+ # Optional Hash of body data for actions +:create+ and +:update+.
84
+ #
85
+ attr_accessor :body_hash
86
+
87
+ # Optional resource identifier for actions +:show+, +:update+ and
88
+ # +:delete+:
89
+ #
90
+ attr_accessor :ident
91
+
92
+ end
93
+
94
+ # Description of data that will be used for request - essentially a
95
+ # compilation of a DescriptionOfRequest instance produced via a call
96
+ # to #get_data_for_request.
97
+ #
98
+ class DataForRequest
99
+
100
+ # The full HTTP URI (or equivalent HTTP URI for HTTP-like, but
101
+ # non-HTTP systems like AMQP) for the call, including any resource
102
+ # identifier and query data.
103
+ #
104
+ attr_accessor :full_uri
105
+
106
+ # String of compiled body data for all actions (may be empty).
107
+ #
108
+ attr_accessor :body_string
109
+
110
+ # Hash of headers; keys are HTTP header names as a Strings (e.g.
111
+ # "Content-Type", "X-Interaction-ID"), values are header values
112
+ # as Strings.
113
+ #
114
+ attr_accessor :header_hash
115
+
116
+ # Hash of query; keys are query keys as Strings (e.g.
117
+ # "search"), values are query values as URL encoded Strings. (e.g.
118
+ # "outlet_id%3Dd32a0e15754a486989fdde2b0830fe12")
119
+ #
120
+ attr_accessor :query_hash
121
+
122
+ end
123
+
124
+ # Description of data describing an HTTP response. Used by
125
+ # #get_data_for_response to generate a response array or hash
126
+ # (see #response_class_for).
127
+ #
128
+ class DescriptionOfResponse
129
+
130
+ # The action that was performed - a Symbol from
131
+ # Hoodoo::Services::Middleware::ALLOWED_ACTIONS.
132
+ #
133
+ attr_accessor :action
134
+
135
+ # The HTTP status code _as an Integer_.
136
+ #
137
+ attr_accessor :http_status_code
138
+
139
+ # The raw ("unparsed") returned body data as a String.
140
+ #
141
+ attr_accessor :raw_body_data
142
+
143
+ # An object that will allow Hash-like lookup and iteration of
144
+ # key/value pairs via +each+. A raw Net::HTTPOK (sic.) instance
145
+ # from a successful response is an example of such an object.
146
+ #
147
+ # May be unset (+nil+) or empty, especially for error cases.
148
+ #
149
+ attr_accessor :http_headers
150
+
151
+ end
152
+
153
+ # Preprocess a high level request description, returning HTTP
154
+ # orientated compiled data as a DataForRequest instance.
155
+ #
156
+ # +description_of_request+:: DescriptionOfRequest instance.
157
+ #
158
+ def get_data_for_request( description_of_request )
159
+ body_hash = Hoodoo::Utilities.stringify( description_of_request.body_hash )
160
+ query_hash = Hoodoo::Utilities.stringify( description_of_request.query_hash )
161
+ ident = description_of_request.ident.to_s
162
+
163
+ body_data = body_hash.nil? ? '' : ::JSON.generate( body_hash )
164
+
165
+ # Amazingly, there's no fast way to deep clone a URI. Long story
166
+ # short - Marshal.load(Marshal.dump(uri)) takes, astonishingly,
167
+ # twice as long to execute as URI.parse(uri.to_s). I have no idea
168
+ # how that's possible. The Addressable gem is even slower.
169
+ #
170
+ # require 'benchmark'
171
+ # require 'addressable/uri' # Assuming gem is present
172
+ #
173
+ # s='http://user:password@pond.org.uk:9924/foo/bar.baz?thing=that'
174
+ # u=URI.parse(s)
175
+ # a=Addressable::URI.parse(s)
176
+ #
177
+ # Benchmark.realtime { 1000000.times { u2=URI.parse(u.to_s) } }
178
+ # # => 14.110195
179
+ # Benchmark.realtime { 1000000.times { a2=a.dup } }
180
+ # # => 26.530487
181
+ # Benchmark.realtime { 1000000.times { u2=Marshal.load(Marshal.dump(u)) } }
182
+ # # => 22.048637
183
+ #
184
+ # ...repeatably.
185
+ #
186
+ # TODO: Is it possible to improve this? It's truly awful, to the
187
+ # extent I'm almost motivated to write a URI handler gem.
188
+ # The core library URI API is tragically bad.
189
+
190
+ remote_uri = URI.parse( description_of_request.endpoint_uri.to_s )
191
+
192
+ # Now we've a copy, we can use high level URI methods to manipulate
193
+ # it to form the full request URI.
194
+
195
+ remote_uri.path << "/#{ URI::escape( ident ) }" unless ident.nil?
196
+
197
+ # Grey area over whether this encodes spaces as "%20" or "+", but
198
+ # so long as the middleware consistently uses the URI encode/decode
199
+ # calls, it should work out in the end anyway.
200
+
201
+ unless query_hash.nil?
202
+ query_hash = query_hash.dup
203
+ query_hash[ 'search' ] = URI.encode_www_form( query_hash[ 'search' ] ) if ( query_hash[ 'search' ].is_a?( ::Hash ) )
204
+ query_hash[ 'filter' ] = URI.encode_www_form( query_hash[ 'filter' ] ) if ( query_hash[ 'filter' ].is_a?( ::Hash ) )
205
+
206
+ query_hash[ '_embed' ] = query_hash[ '_embed' ].join( ',' ) if ( query_hash[ '_embed' ].is_a?( ::Array ) )
207
+ query_hash[ '_reference' ] = query_hash[ '_reference' ].join( ',' ) if ( query_hash[ '_reference' ].is_a?( ::Array ) )
208
+
209
+ query_hash.delete( 'search' ) if query_hash[ 'search' ].nil? || query_hash[ 'search' ].empty?
210
+ query_hash.delete( 'filter' ) if query_hash[ 'filter' ].nil? || query_hash[ 'filter' ].empty?
211
+ query_hash.delete( '_embed' ) if query_hash[ '_embed' ].nil? || query_hash[ '_embed' ].empty?
212
+ query_hash.delete( '_reference' ) if query_hash[ '_reference' ].nil? || query_hash[ '_reference' ].empty?
213
+ end
214
+
215
+ remote_uri.query = URI.encode_www_form( query_hash ) unless query_hash.nil? || query_hash.empty?
216
+
217
+ headers = {
218
+ 'Content-Type' => 'application/json; charset=utf-8',
219
+ 'Content-Language' => self.locale() || 'en-nz', # Locale comes from Endpoint superclass
220
+ 'Accept-Language' => self.locale() || 'en-nz'
221
+ }
222
+
223
+ # Interaction comes from Endpoint superclass.
224
+ #
225
+ # TODO: Can anything be done about inbound X-Interaction-ID
226
+ # headers or interaction ID values specified by the
227
+ # calling client which would be stripped by an Alchemy
228
+ # architecture but not by conventional HTTP servers?
229
+ #
230
+ unless self.interaction().nil?
231
+ headers[ 'X-Interaction-ID' ] = self.interaction().interaction_id
232
+ end
233
+
234
+ # Session ID comes from Endpoint superclass.
235
+ #
236
+ unless self.session_id().nil?
237
+ headers[ 'X-Session-ID'] = self.session_id()
238
+ end
239
+
240
+ # A suite of options is defined by a constant in the Endpoint
241
+ # superclass.
242
+ #
243
+ Hoodoo::Client::Headers::HEADER_TO_PROPERTY.each do | rack_header, description |
244
+ header_name = description[ :header ]
245
+ header_proc = description[ :header_proc ]
246
+ property = description[ :property ]
247
+
248
+ property_value = self.send( property )
249
+
250
+ unless property_value.nil?
251
+ headers[ header_name ] = header_proc.call( property_value )
252
+ end
253
+ end
254
+
255
+ data = DataForRequest.new
256
+ data.full_uri = remote_uri
257
+ data.body_string = body_data
258
+ data.header_hash = headers
259
+ data.query_hash = query_hash
260
+
261
+ return data
262
+ end
263
+
264
+ # Process a raw HTTP response description, returning an instance of
265
+ # Hoodoo::Client::AugmentedArray or Hoodoo::Client::AugmentedHash
266
+ # with either processed body data inside, or error data associated.
267
+ #
268
+ # +description_of_response+:: DescriptionOfResponse instance.
269
+ #
270
+ def get_data_for_response( description_of_response )
271
+ code = description_of_response.http_status_code
272
+ body = description_of_response.raw_body_data
273
+
274
+ begin
275
+ parsed = ::JSON.parse(
276
+ body,
277
+ :object_class => Hoodoo::Client::AugmentedHash,
278
+ :array_class => Hoodoo::Client::AugmentedArray
279
+ )
280
+
281
+ rescue => e
282
+ data = response_class_for( description_of_response.action ).new
283
+ data.response_options = Hoodoo::Client::Headers.x_header_to_options(
284
+ description_of_response.http_headers
285
+ )
286
+
287
+ case code
288
+ when 404
289
+ return generate_404_response_for( description_of_response.action )
290
+ when 408
291
+ data.platform_errors.add_error( 'platform.timeout' )
292
+ when 200
293
+ data.platform_errors.add_error(
294
+ 'platform.fault',
295
+ :message => 'Could not parse body data returned from inter-resource call despite receiving HTTP status code 200',
296
+ :reference => { :exception => RuntimeError.new( "#{ body }" ) }
297
+ )
298
+ when 204
299
+ if data.response_options[ 'deja_vu' ] != 'confirmed'
300
+ data.platform_errors.add_error(
301
+ 'platform.fault',
302
+ :message => "Unexpected raw HTTP status code 204 with 'X-Deja-Vu: confirmed' not present",
303
+ :reference => { :exception => RuntimeError.new( '204' ) }
304
+ )
305
+ end # Else do nothing; keep the empty 'data'
306
+ else
307
+ data.platform_errors.add_error(
308
+ 'platform.fault',
309
+ :message => "Unexpected raw HTTP status code #{ code } with non-JSON response",
310
+ :reference => { :exception => RuntimeError.new( "#{ body }" ) }
311
+ )
312
+ end
313
+
314
+ return data
315
+ end
316
+
317
+ # Just in case someone changes JSON parsers under us and the
318
+ # replacement doesn't support the options used above...
319
+
320
+ unless parsed.is_a?( Hoodoo::Client::AugmentedHash )
321
+ raise "Hoodoo::Services::Middleware: Incompatible JSON implementation in use which doesn't understand 'object_class' or 'array_class' options"
322
+ end
323
+
324
+ # If the parsed data wrapped an array, extract just the array
325
+ # part, else the hash part.
326
+
327
+ if ( parsed[ '_data' ].is_a?( ::Array ) )
328
+ size = parsed[ '_dataset_size' ]
329
+ parsed = parsed[ '_data' ]
330
+ parsed.dataset_size = size
331
+
332
+ elsif ( parsed[ 'kind' ] == 'Errors' )
333
+
334
+ # This isn't an array, it's an AugmentedHash describing errors.
335
+ # Turn this into a formal errors collection.
336
+
337
+ errors_from_resource = Hoodoo::Errors.new()
338
+
339
+ parsed[ 'errors' ].each do | error |
340
+ errors_from_resource.add_precompiled_error(
341
+ error[ 'code' ],
342
+ error[ 'message' ],
343
+ error[ 'reference' ],
344
+ code
345
+ )
346
+ end
347
+
348
+ # Use a 'clean' copy of the response class rather than keeping
349
+ # the originating data. People will not make assumptions about
350
+ # error payloads and trip over with the early return 404 stuff
351
+ # etc. this way.
352
+
353
+ parsed = response_class_for( description_of_response.action ).new
354
+ parsed.set_platform_errors( errors_from_resource )
355
+
356
+ end
357
+
358
+ parsed.response_options = Hoodoo::Client::Headers.x_header_to_options(
359
+ description_of_response.http_headers
360
+ )
361
+
362
+ return parsed
363
+ end
364
+ end
365
+ end
366
+ end
367
+ end