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