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,19 @@
1
+ ########################################################################
2
+ # File:: errors.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Include error management classes and data definitions.
6
+ # ----------------------------------------------------------------------
7
+ # 26-Jan-2015 (ADH): Split from top-level inclusion file.
8
+ ########################################################################
9
+
10
+ # Dependencies
11
+
12
+ require 'hoodoo/utilities'
13
+ require 'hoodoo/presenters'
14
+ require 'hoodoo/data'
15
+
16
+ # Error management
17
+
18
+ require 'hoodoo/errors/error_descriptions'
19
+ require 'hoodoo/errors/errors'
@@ -0,0 +1,229 @@
1
+ ########################################################################
2
+ # File:: error_descriptions.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Error descriptions - provide a DSL and a container for a
6
+ # list of known error codes and associated data. Defines a
7
+ # platform API's +platform+ and +generic+ domain codes by
8
+ # default. Services can declare additional errors.
9
+ # ----------------------------------------------------------------------
10
+ # 22-Sep-2014 (ADH): Created.
11
+ # 09-Oct-2014 (ADH): Updated for Preview Release 8.
12
+ # 16-Oct-2014 (TC): Added session error
13
+ ########################################################################
14
+
15
+ module Hoodoo
16
+
17
+ # A collection of error descriptions. API service implementations create one
18
+ # of these, which self-declares platform and generic error domain codes. A
19
+ # simple DSL is available to declare service-specific errors. Since the
20
+ # middleware is responsible for instantiating an error collection inside a
21
+ # response object which service implementations use to signal error
22
+ # conditions, the service's _interface_ class uses the interface description
23
+ # DSL to call through to here behind the scenes; for example:
24
+ #
25
+ # class TransactionImplementation < Hoodoo::Services::Implementation
26
+ # # ...
27
+ # end
28
+ #
29
+ # class TransactionInterface < Hoodoo::Services::Interface
30
+ # interface :Transaction do
31
+ # endpoint :transactions, TransactionImplementation
32
+ # errors_for 'transaction' do
33
+ # error 'duplicate_transaction', status: 409, message: 'Duplicate transaction', :required => [ :client_uid ]
34
+ # end
35
+ # end
36
+ # end
37
+ #
38
+ # The #errors_for method takes the domain of the error as a string - the
39
+ # part that comes before the "+.+" in error codes. Then a series of +error+
40
+ # calls describe the individual error codes. See
41
+ # Hoodoo::ErrorDescriptions::DomainDescriptions#error for details.
42
+ #
43
+ # An instance of the Hoodoo::ErrorDescriptions class gets built behind
44
+ # the scenes as part of the service interface description. This is found by
45
+ # the middleware and passed to a Hoodoo::Errors constructor. The result
46
+ # is stored in a Hoodoo::Services::Response instance and passed to handler
47
+ # methods in the service's Hoodoo::Services::Implementation subclass for each
48
+ # request. Service implementations access the errors collection through
49
+ # Hoodoo::Services::Response#errors and can then add errors using the generic
50
+ # or platform domains, or whatever additional custom domain(s) they defined
51
+ # in the service interface subclass.
52
+ #
53
+ # For direct callers (e.g. the middleware), there is a shorthand form to
54
+ # invoke the DSL where the constructor is used in the same way as
55
+ # #errors_for:
56
+ #
57
+ # ERROR_DESCRIPTIONS = Hoodoo::ErrorDescriptions.new( 'transaction' ) do
58
+ # error 'duplicate_transaction', status: 409, message: 'Duplicate transaction', :required => [ :client_uid ]
59
+ # end
60
+ #
61
+ # Either way,
62
+ #
63
+ # As per the example above, services can share an instance across requests
64
+ # (and threads) via a class's variable if the descriptions don't change. You
65
+ # would use the descriptions to inform a Hoodoo::Errors instance of the
66
+ # available codes and their requirements:
67
+ #
68
+ # @errors = Hoodoo::Errors.new( ERROR_DESCRIPTIONS )
69
+ #
70
+ class ErrorDescriptions
71
+
72
+ # Create an instance, self-declaring +platform+ and +generic+ domain
73
+ # errors. You can optionally call the constructor with an error domain
74
+ # and code block, to declare errors all in one go rather than making a
75
+ # separate call to #errors_for (but both approaches are valid).
76
+ #
77
+ # +domain+:: Optional domain, just as used in #errors_for
78
+ # &block:: Optional block, just as used in #errors_for
79
+ #
80
+ def initialize( domain = nil, &block )
81
+
82
+ @descriptions = {}
83
+
84
+ # Up to date at Preview Release 9, 2014-11-10.
85
+
86
+ errors_for 'platform' do
87
+ error 'not_found', status: 404, message: 'Not found', reference: [ :entity_name ]
88
+ error 'malformed', status: 422, message: 'Malformed request'
89
+ error 'invalid_session', status: 401, message: 'Invalid session'
90
+ error 'forbidden', status: 403, message: 'Action not authorized'
91
+ error 'method_not_allowed', status: 405, message: 'Method not allowed'
92
+ error 'timeout', status: 408, message: 'Request timeout'
93
+ error 'fault', status: 500, message: 'Internal error', reference: [ :exception ]
94
+ end
95
+
96
+ # Up to date at Preview Release 9, 2014-11-10.
97
+
98
+ errors_for 'generic' do
99
+ error 'not_found', status: 404, message: 'Resource not found', reference: [ :ident ]
100
+ error 'malformed', status: 422, message: 'Malformed payload'
101
+ error 'required_field_missing', status: 422, message: 'Required field missing', reference: [ :field_name ]
102
+ error 'invalid_string', status: 422, message: 'Invalid string format', reference: [ :field_name ]
103
+ error 'invalid_integer', status: 422, message: 'Invalid integer format', reference: [ :field_name ]
104
+ error 'invalid_float', status: 422, message: 'Invalid float format', reference: [ :field_name ]
105
+ error 'invalid_decimal', status: 422, message: 'Invalid decimal format', reference: [ :field_name ]
106
+ error 'invalid_boolean', status: 422, message: 'Invalid boolean format', reference: [ :field_name ]
107
+ error 'invalid_enum', status: 422, message: 'Invalid enumeration', reference: [ :field_name ]
108
+ error 'invalid_date', status: 422, message: 'Invalid date specifier', reference: [ :field_name ]
109
+ error 'invalid_time', status: 422, message: 'Invalid time specifier', reference: [ :field_name ]
110
+ error 'invalid_datetime', status: 422, message: 'Invalid date-time specifier', reference: [ :field_name ]
111
+ error 'invalid_uuid', status: 422, message: 'Invalid UUID', reference: [ :field_name ]
112
+ error 'invalid_array', status: 422, message: 'Invalid array', reference: [ :field_name ]
113
+ error 'invalid_object', status: 422, message: 'Invalid object', reference: [ :field_name ]
114
+ error 'invalid_hash', status: 422, message: 'Invalid hash', reference: [ :field_name ]
115
+ error 'invalid_duplication', status: 422, message: 'Duplicates not allowed', reference: [ :field_name ]
116
+ error 'invalid_state', status: 422, message: 'State transition not allowed', reference: [ :destination_state ]
117
+ error 'invalid_parameters', status: 422, message: 'Invalid parameters'
118
+ error 'mutually_exclusive', status: 422, message: 'Mutually exclusive parameters', reference: [ :field_names ]
119
+ end
120
+
121
+ # Add caller's custom errors for the shorthand form, if provided.
122
+
123
+ if ( domain != nil && domain != '' && block_given?() )
124
+ errors_for( domain, &block )
125
+ end
126
+ end
127
+
128
+ # Implement the collection's part of the small DSL used for error
129
+ # declaration. Call here, passing the error domain (usually the singular
130
+ # service name or resource name, e.g. "+transaction+" and defined by the
131
+ # part of the platform API the service is implementing) and a block. The
132
+ # block makes one or more "+error+" calls, which actually end up calling
133
+ # Hoodoo::ErrorDescriptions::DomainDescriptions#error behind the scenes.
134
+ #
135
+ # See the implementation of #initialize for a worked example.
136
+ #
137
+ # +domain+:: Error domain, e.g. +platform+, +transaction+
138
+ # &block:: Block which makes one or more calls to "+error+"
139
+ #
140
+ def errors_for( domain, &block )
141
+ domain_descriptions = Hoodoo::ErrorDescriptions::DomainDescriptions.new( domain )
142
+ domain_descriptions.instance_eval( &block )
143
+
144
+ @descriptions.merge!( domain_descriptions.descriptions )
145
+ end
146
+
147
+ # Is the given error code recognised? Returns +true+ if so, else +false+.
148
+ #
149
+ # +code+:: Error code in full, e.g. +generic.invalid_state'.
150
+ #
151
+ def recognised?( code )
152
+ @descriptions.has_key?( code )
153
+ end
154
+
155
+ # Return the options description hash, as passed to +error+ calls in the
156
+ # block given to #errors_for, for the given code.
157
+ #
158
+ # +code+:: Error code in full, e.g. +generic.invalid_state'.
159
+ #
160
+ def describe( code )
161
+ @descriptions[ code ]
162
+ end
163
+
164
+ # Contain a description of errors for a particular domain, where the domain
165
+ # is a grouping string such as "platform", "generic", or a short service
166
+ # name. Usually driven via Hoodoo::ErrorDescriptions, not directly.
167
+ #
168
+ class DomainDescriptions
169
+
170
+ # Domain name for this description instance (string).
171
+ #
172
+ attr_reader( :domain )
173
+
174
+ # Hash of all descriptions, keyed by full error code, with options
175
+ # hash data as values (see #error for details).
176
+ #
177
+ attr_reader( :descriptions )
178
+
179
+ # Initialize a new instance for the given domain.
180
+ #
181
+ # +domain+:: The domain string - for most service-based callers, usually
182
+ # a short service name like +members+ or +transactions+.
183
+ #
184
+ def initialize( domain )
185
+ @domain = domain
186
+ @descriptions = {}
187
+ end
188
+
189
+ # Describe an error.
190
+ #
191
+ # +name+:: The error name - the bit after the "+.+" in the code, e.g.
192
+ # +invalid_parameters+.
193
+ #
194
+ # +options+:: Options hash. See below.
195
+ #
196
+ # The options hash contains symbol keys named as follows, with values as
197
+ # described:
198
+ #
199
+ # +:status+:: The integer or string HTTP status code to be associated
200
+ # with this error
201
+ #
202
+ # +message+:: The +en-nz+ language human-readable error message used
203
+ # for developers.
204
+ #
205
+ # +reference+:: Optional array of required named references. When errors
206
+ # are added (via Hoodoo::Errors#add_error) to a
207
+ # collection, required reference(s) from this array must
208
+ # be provided by the error-adding caller else an exception
209
+ # will be raised. This ensures correct, fully qualified
210
+ # error data is logged and sent to clients.
211
+ #
212
+ def error( name, options )
213
+ options = Hoodoo::Utilities.stringify( options )
214
+ required_keys = [ 'status', 'message' ]
215
+
216
+ reference = options[ 'reference' ]
217
+ options[ 'reference' ] = reference.map( &:to_s ) if reference.is_a?( ::Array )
218
+
219
+ required_keys.each do | required_key |
220
+ unless options.has_key?( required_key )
221
+ raise "Error description options hash missing required key '#{ required_key }'"
222
+ end
223
+ end
224
+
225
+ @descriptions[ "#{ @domain }.#{ name }" ] = options
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,322 @@
1
+ ########################################################################
2
+ # File:: errors.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: A collection of error messages, starting empty, with one
6
+ # or more messages added to it as errors are encountered by
7
+ # some processing task. Errors are added according to codes
8
+ # described by Hoodoo::ErrorDescriptions instances.
9
+ # ----------------------------------------------------------------------
10
+ # 22-Sep-2014 (ADH): Created.
11
+ ########################################################################
12
+
13
+ module Hoodoo
14
+
15
+ # During request processing, API service implementations create an
16
+ # Hoodoo::Errors instance and add error(s) to the collection as they arise
17
+ # using #add_error. That same instance can then be returned for the on-error
18
+ # handling path of whatever wider service framework is in use by the service
19
+ # code in question. Services should use new instances for each request.
20
+ #
21
+ class Errors
22
+
23
+ # Custom exception thrown when an unknown error code is added to a
24
+ # collection.
25
+ #
26
+ class UnknownCode < RuntimeError
27
+ end
28
+
29
+ # Custom exception thrown when an error is added to a collection without
30
+ # including required reference data
31
+ #
32
+ class MissingReferenceData < RuntimeError
33
+ end
34
+
35
+ # Default Hoodoo::ErrorDescriptions instance, used if the instantiator
36
+ # provides no alternative.
37
+ #
38
+ DEFAULT_ERROR_DESCRIPTIONS = Hoodoo::ErrorDescriptions.new()
39
+
40
+ # Errors are manifestations of the Errors resource. They acquire a UUID
41
+ # when instantiated, even if the instance is never used or persisted.
42
+ #
43
+ attr_reader( :uuid )
44
+
45
+ # Array of error data - hashes with +code+, +message+ and +reference+
46
+ # fields giving the error codes, human-readable messages and
47
+ # machine-readable reference data, where appropriate.
48
+ #
49
+ attr_reader( :errors )
50
+
51
+ # HTTP status code associated with the first error in the #errors array,
52
+ # _as an Integer_.
53
+ #
54
+ attr_reader( :http_status_code )
55
+
56
+ # The Hoodoo::ErrorDescriptions instance associated with this error
57
+ # collection. Only error codes that the instance's
58
+ # Hoodoo::ErrorDescriptions#recognised? method says are recognised
59
+ # can be added to the error collection, else
60
+ # Hoodoo::Errors::UnknownCode will be raised.
61
+ #
62
+ attr_reader( :descriptions )
63
+
64
+ # Create an instance.
65
+ #
66
+ # +descriptions+:: (Optional) Hoodoo::ErrorDescriptions instance with
67
+ # service-domain-specific error descriptions added, or
68
+ # omit for a default instance describing +platform+ and
69
+ # +generic+ error domains only.
70
+ #
71
+ def initialize( descriptions = DEFAULT_ERROR_DESCRIPTIONS )
72
+ @uuid = Hoodoo::UUID.generate()
73
+ @descriptions = descriptions
74
+ @errors = []
75
+ @http_status_code = 200
76
+ @created_at = nil # See #persist!
77
+ end
78
+
79
+ # Add an error instance to this collection.
80
+ #
81
+ # +code+:: Error code in full, e.g. +generic.invalid_state'.
82
+ #
83
+ # +options+:: An options Hash, optional.
84
+ #
85
+ # The options hash contains symbol keys named as follows, with values as
86
+ # described:
87
+ #
88
+ # +reference+:: Reference data Hash, optionality depending upon the error
89
+ # code and the reference data its error description mandates.
90
+ # Provide key/value pairs where (symbol) keys are names from
91
+ # the array of description requirements and values are
92
+ # strings. All values are concatenated into a single string,
93
+ # comma-separated. Commas within values are escaped with a
94
+ # backslash; backslash is itself escaped with a backslash.
95
+ #
96
+ # You must provide that data at a minimum, but can provide
97
+ # additional keys too if you so wish. Required keys are
98
+ # always included first, in order of appearance in the
99
+ # requirements array of the error declaration, followed by
100
+ # any extra values in undefined order.
101
+ #
102
+ # See also Hoodoo::ErrorDescriptions::DomainDescriptions#error
103
+ #
104
+ # +message+:: Optional human-readable for-developer message, +en-nz+
105
+ # locale. Default messages are provided for all errors, but
106
+ # if you think you can provide something more informative,
107
+ # you can do so through this parameter.
108
+ #
109
+ # Example:
110
+ #
111
+ # errors.add_error(
112
+ # 'platform.not_found',
113
+ # :message => 'Optional custom message',
114
+ # :reference => { :entity_name => 'mandatory reference data' }
115
+ # )
116
+ #
117
+ # In the above example, the mandatory reference data +entity_name+ comes
118
+ # from the description for the 'platform.not_found' message - see the
119
+ # Hoodoo::ErrorDescriptions#initialize _implementation_ and Platform API.
120
+ #
121
+ def add_error( code, options = nil )
122
+
123
+ options = Hoodoo::Utilities.stringify( options || {} )
124
+ reference = options[ 'reference' ] || {}
125
+ message = options[ 'message' ]
126
+
127
+ # Make sure nobody uses an undeclared error code.
128
+
129
+ raise UnknownCode, "In \#add_error: Unknown error code '#{code}'" unless @descriptions.recognised?( code )
130
+
131
+ # If the error description specifies a list of required reference keys,
132
+ # make sure all are present and complain if not.
133
+
134
+ description = @descriptions.describe( code )
135
+
136
+ required_keys = description[ 'reference' ] || []
137
+ actual_keys = reference.keys
138
+ missing_keys = required_keys - actual_keys
139
+
140
+ unless missing_keys.empty?
141
+ raise MissingReferenceData, "In \#add_error: Reference hash missing required keys: '#{ missing_keys.join( ', ' ) }'"
142
+ end
143
+
144
+ # All good!
145
+
146
+ @http_status_code = ( description[ 'status' ] || 200 ).to_i if @errors.empty? # Use first in collection for overall HTTP status code
147
+
148
+ error = {
149
+ 'code' => code,
150
+ 'message' => message || description[ 'message' ] || code
151
+ }
152
+
153
+ ordered_keys = required_keys + ( actual_keys - required_keys )
154
+ ordered_values = ordered_keys.map { | key | escape_commas( reference[ key ].to_s ) }
155
+
156
+ # See #unjoin_and_unescape_commas to undo the join below.
157
+
158
+ error[ 'reference' ] = ordered_values.join( ',' ) unless ordered_values.empty?
159
+
160
+ @errors << error
161
+ end
162
+
163
+ # Add a precompiled error to the error collection. Pass error code,
164
+ # error message and reference data directly.
165
+ #
166
+ # In most cases you should be calling #add_error instead, *NOT* here.
167
+ #
168
+ # *No* *validation* is performed. You should only really call here if
169
+ # storing an error / errors from another, trusted source with assumed
170
+ # validity (e.g. another service called remotely with errors in the JSON
171
+ # response). It's possible to store invalid error data using this call,
172
+ # which means counter-to-documentation results could be returned to API
173
+ # clients. That is Very Bad.
174
+ #
175
+ # Pass optionally the HTTP status code to use if this happens to be the
176
+ # first stored error. If this is omitted, 500 is kept as the default.
177
+ #
178
+ def add_precompiled_error( code, message, reference, http_status = 500 )
179
+ @http_status_code = http_status.to_i if @errors.empty?
180
+
181
+ error = {
182
+ 'code' => code,
183
+ 'message' => message
184
+ }
185
+
186
+ error[ 'reference' ] = reference unless reference.nil? || reference.empty?
187
+
188
+ @errors << error
189
+ end
190
+
191
+ # Merge the contents of a source error object with this one, adding its
192
+ # errors to this collection. No checks are made for duplicates (in part
193
+ # because, depending on error code and source/target contexts, a
194
+ # duplicate may be a valid thing to have).
195
+ #
196
+ # +source+:: Hoodoo::Errors instance to merge into the error collection
197
+ # of 'this' target object.
198
+ #
199
+ # Returns +true+ if errors were merged, else +false+ (the source
200
+ # collection was empty).
201
+ #
202
+ def merge!( source )
203
+ source_errors = source.errors
204
+
205
+ source_errors.each do | hash |
206
+ add_precompiled_error(
207
+ hash[ 'code' ],
208
+ hash[ 'message' ],
209
+ hash[ 'reference' ],
210
+ source.http_status_code
211
+ )
212
+ end
213
+
214
+ return ! source_errors.empty?
215
+ end
216
+
217
+ # Does this instance have any errors added? Returns +true+ if so,
218
+ # else +false+.
219
+ #
220
+ def has_errors?
221
+ ! @errors.empty?
222
+ end
223
+
224
+ # Clear (delete) all previously added errors (if any). After calling here,
225
+ # #has_errors? would always return +false+.
226
+ #
227
+ def clear_errors
228
+ @errors = []
229
+ @http_status_code = 200
230
+ end
231
+
232
+ # Return a Hash rendered through the Hoodoo::Data::Resources::Errors
233
+ # collection representing the formalised resource.
234
+ #
235
+ # +interaction_id+:: Mandatory Interaction ID (UUID) to associate with
236
+ # the collection.
237
+ #
238
+ def render( interaction_id )
239
+ unless Hoodoo::UUID.valid?( interaction_id )
240
+ raise "Hoodoo::Errors\#render must be given a valid Interaction ID (got '#{ interaction_id.inspect }')"
241
+ end
242
+
243
+ @created_at ||= Time.now
244
+
245
+ Hoodoo::Data::Resources::Errors.render(
246
+ {
247
+ 'interaction_id' => interaction_id,
248
+ 'errors' => @errors
249
+ },
250
+ @uuid,
251
+ @created_at
252
+ )
253
+ end
254
+
255
+ # Make life easier for debugging on the console by having the object
256
+ # represent itself more concisely.
257
+ #
258
+ def inspect
259
+ @errors.to_s
260
+ end
261
+
262
+
263
+ # DEVELOPER: In the function comment below, RDoc escaping has to be done
264
+ # for RDocs to make sense. Read every "\\" as a single "\" (or read the
265
+ # generated docs instead of reading the source code comment below).
266
+
267
+
268
+ # When reference data is specified for errors, the reference values are
269
+ # concatenated together into a comma-separated string. Since reference
270
+ # values can themselves contain commas, comma is escaped with "\\," and
271
+ # "\\" escaped with "\\\\".
272
+ #
273
+ # Call here with such a string; return an array of 'unescaped' values.
274
+ #
275
+ # +str+:: Value-escaped ("\\\\" / "\\,") comma-separated string. Unescaped
276
+ # commas separate individual values.
277
+ #
278
+ def unjoin_and_unescape_commas( str )
279
+
280
+ # In Ruby regular expressions, '(?<!pat)' is a negative lookbehind
281
+ # assertion, making sure that the preceding characters do not match
282
+ # 'pat'. To split the string joined on ',' to an array but not splitting
283
+ # any escaped '\,', then, we can use this rather opaque split regexp:
284
+ #
285
+ # error[ 'reference' ].split( /(?<!\\),/ )
286
+ #
287
+ # I.e. split on ',', provided it is not preceded by a '\' (escaped in the
288
+ # regexp to '\\').
289
+
290
+ ary = str.split( /(?<!\\),/ )
291
+ ary.map { | entry | unescape_commas( entry ) }
292
+ end
293
+
294
+ private
295
+
296
+
297
+ # DEVELOPER: In the function comment below, RDoc escaping has to be done
298
+ # for RDocs to make sense. Read every "\\" as a single "\" (or read the
299
+ # generated docs instead of reading the source code comment below).
300
+
301
+
302
+ # Given a string, escape "," to "\\," and "\\" to "\\\\", returning the result.
303
+ #
304
+ # +str+:: String to escape.
305
+ #
306
+ def escape_commas( str )
307
+ # "\" in replacement strings gets evaluated twice, once for string
308
+ # literals (leaving '\\\\') then again for regexp group references like
309
+ # "\1" work (thus leaving '\\').
310
+ #
311
+ str.gsub( "\\", "\\\\\\\\" ).gsub( ",", "\\," )
312
+ end
313
+
314
+ # Given a string escaped via #escape_commas, unescape it.
315
+ #
316
+ # +str+:: String to escape.
317
+ #
318
+ def unescape_commas( str )
319
+ str.gsub( "\\,", "," ).gsub( "\\\\", "\\\\" )
320
+ end
321
+ end
322
+ end