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,73 @@
1
+ ########################################################################
2
+ # File:: rack_monkey_patch.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Heyre Be Dragyns.
6
+ #
7
+ # For local development, the service middleware needs to know
8
+ # where other resource endpoints are in terms of HTTP host and
9
+ # port, so that remote inter-resource calls can work without
10
+ # up-front static configuration of service host/port data. To
11
+ # have to manage a fixed list of local development ports in
12
+ # the face of arbitrary resource endpoint divisions would be a
13
+ # big pain and cause developers much frustration.
14
+ #
15
+ # This means that whenever a service starts up, it needs to
16
+ # know the HTTP host and port under which it is running, then
17
+ # tell the middleware about it.
18
+ #
19
+ # In the absence of a formal interface in Rack for this, then
20
+ # we could do still that relatively nicely by looking up the
21
+ # Rack server in ObjectSpace and asking it for its options -
22
+ # except some of the web server adapters do really dumb things
23
+ # like "options.delete(:Host)" to read items out, destroying
24
+ # the info we need.
25
+ #
26
+ # So instead, we have to monkey patch :-(
27
+ # ----------------------------------------------------------------------
28
+ # 11-Nov-2014 (ADH): Split out from service_middleware.rb.
29
+ ########################################################################
30
+
31
+ if defined?( Rack ) && defined?( Rack::Server )
32
+
33
+ # Part of the Rack monkey patch. See file
34
+ # "rack_monkey_path.rb"'s documentation for details.
35
+ #
36
+ module Rack
37
+
38
+ # Part of the Rack monkey patch. See file
39
+ # "rack_monkey_path.rb"'s documentation for details.
40
+ #
41
+ class Server
42
+
43
+ class << self
44
+
45
+ # Part of the Rack monkey patch. See file
46
+ # "rack_monkey_path.rb"'s documentation for details.
47
+ #
48
+ # This method is aliased in place of Rack::Server::start and reads
49
+ # the passed-in options hash to attempt to determine the host name
50
+ # and port number under which a Rack based service is running. It
51
+ # then calls through to Rack's original ::start implementation.
52
+ #
53
+ # +options+:: Options (see original Rack::Server documentation).
54
+ #
55
+ def start_and_record_host_and_port( options = nil )
56
+ Hoodoo::Services::Middleware.record_host_and_port( options )
57
+ racks_original_start( options )
58
+ end
59
+
60
+ # Part of the Rack monkey patch. Alias for the original
61
+ # Rack::Server::start.
62
+ #
63
+ alias racks_original_start start
64
+
65
+ # Part of the Rack monkey patch. See ::start_and_record_host_and_port.
66
+ #
67
+ # +options+:: See ::start_and_record_host_and_port.
68
+ #
69
+ alias start start_and_record_host_and_port
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,153 @@
1
+ ########################################################################
2
+ # File:: context.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Container for information about the context of a call to
6
+ # a service, including session, request and response.
7
+ # ----------------------------------------------------------------------
8
+ # 03-Oct-2014 (ADH): Created.
9
+ ########################################################################
10
+
11
+ module Hoodoo; module Services
12
+
13
+ # A collection of objects which describe the context in which a service is
14
+ # being called. The service reads session and request information and returns
15
+ # results of its processing via the associated response object.
16
+ #
17
+ class Context
18
+
19
+ public
20
+
21
+ # The Hoodoo::Services::Session instance describing the authorised call
22
+ # context. If a resource implementation is handling a public action this
23
+ # may be +nil+, else it will be a valid instance.
24
+ #
25
+ attr_reader :session
26
+
27
+ # The Hoodoo::Services::Request instance giving details about the
28
+ # inbound request. Relevant information will depend upon the endpoint
29
+ # service implementation action being addressed.
30
+ #
31
+ attr_reader :request
32
+
33
+ # The Hoodoo::Services::Response instance that a service implementation
34
+ # updates with results of its processing.
35
+ #
36
+ attr_reader :response
37
+
38
+ # The Hoodoo::Services::Middleware::Interaction instance for which this
39
+ # context exists (the 'owning' instance). Generally speaking this is
40
+ # only needed internally as part of the inter-resource call mechanism.
41
+ #
42
+ attr_reader :owning_interaction
43
+
44
+ # Create a new instance. There is almost certainly never any need to
45
+ # call this unless you're the Hoodoo::Services::Middleware::Interaction
46
+ # constructor! If you want to build a context for (say) test purposes,
47
+ # it's probably best to construct an interaction instance and use the
48
+ # context instance this provides.
49
+ #
50
+ # +session+:: See #session.
51
+ # +request+:: See #request.
52
+ # +response+:: See #response.
53
+ # +owning_interaction+:: See #interaction.
54
+ #
55
+ def initialize( session, request, response, owning_interaction )
56
+ @session = session
57
+ @request = request
58
+ @response = response
59
+ @owning_interaction = owning_interaction
60
+ end
61
+
62
+ # Request (and lazy-initialize) a new resource endpoint instance for
63
+ # talking to a resource's interface. See Hoodoo::Client::Endpoint.
64
+ #
65
+ # You can request an endpoint for any resource name, whether or not an
66
+ # implementation actually exists for it. Until you try and talk to the
67
+ # interface through the endpoint instance, you won't know if it is
68
+ # there. All endpoint methods return instances of classes that mix in
69
+ # Hoodoo::Client::AugmentedBase; these
70
+ # mixin methods provide error handling options to detect a "not found"
71
+ # error (equivanent to HTTP status code 404) returned when a resource
72
+ # implementation turns out to not actually be present.
73
+ #
74
+ # The idiomatic call sequence is something like the following, where
75
+ # you get hold of an endpoint, make a call and handle the response:
76
+ #
77
+ # clock = context.resource( :Clock, 2 ) # v2 of 'Clock' resource
78
+ # time = clock.show( 'now' )
79
+ #
80
+ # return if time.adds_errors_to?( context.response.errors )
81
+ #
82
+ # ...or alternatively:
83
+ #
84
+ # clock = context.resource( :Clock, 2 ) # v2 of 'Clock' resource
85
+ # time = clock.show( 'now' )
86
+ #
87
+ # context.response.add_errors( time.platform_errors )
88
+ # return if context.response.halt_processing?
89
+ #
90
+ # The return value of calls made to the endpoint is an Array or Hash
91
+ # that mixes in Hoodoo::Client::AugmentedBase;
92
+ # see this class's documentation for details of the two alternative
93
+ # error handling approaches shown above.
94
+ #
95
+ # +resource+:: Resource name for the endpoint, e.g. +:Purchase+. String
96
+ # or symbol.
97
+ #
98
+ # +version+:: Optional required implemented version for the endpoint,
99
+ # as an Integer - defaults to 1.
100
+ #
101
+ # +options+:: Optional options Hash (see below).
102
+ #
103
+ # The options Hash key/values are as follows:
104
+ #
105
+ # +locale+:: Locale string for request/response, e.g. "en-gb". Optional.
106
+ # If omitted, defaults to the locale set in this Client
107
+ # instance's constructor.
108
+ #
109
+ # Others:: See Hoodoo::Client::Headers' +HEADER_TO_PROPERTY+.
110
+ # For any options in that map which describe themselves as
111
+ # being automatically transferred from one endpoint to
112
+ # another, you can prevent this by explicitly pasisng a
113
+ # +nil+ value for the option; otherwise, _OMIT_ the option
114
+ # for normal behaviour. Non-auto-transfer properties can be
115
+ # specified as +nil+ or omitted with no change in behaviour.
116
+ #
117
+ def resource( resource, version = 1, options = {} )
118
+ middleware = @owning_interaction.owning_middleware_instance
119
+ endpoint = middleware.inter_resource_endpoint_for(
120
+ resource,
121
+ version,
122
+ @owning_interaction
123
+ )
124
+
125
+ endpoint.locale = options[ :locale ] unless options[ :locale ].nil?
126
+
127
+ Hoodoo::Client::Headers::HEADER_TO_PROPERTY.each do | rack_header, description |
128
+ property = description[ :property ]
129
+ property_writer = description[ :property_writer ]
130
+ auto_transfer = description[ :auto_transfer ]
131
+
132
+ # For automatically transferred options there's no way to stop the
133
+ # auto transfer unless explicitly stating 'nil' to overwrite any
134
+ # existing value, so here, only write the value into the endpoint if
135
+ # the property specifically exists in the inbound options hash.
136
+ #
137
+ # For other properties, 'nil' has no meaning and there's no need to
138
+ # override anything, so use "unless nil?" in that case.
139
+
140
+ value = options[ property ]
141
+
142
+ if auto_transfer == true
143
+ endpoint.send( property_writer, value ) if options.has_key?( property )
144
+ else
145
+ endpoint.send( property_writer, value ) unless value.nil?
146
+ end
147
+ end
148
+
149
+ return endpoint
150
+ end
151
+
152
+ end
153
+ end; end
@@ -0,0 +1,132 @@
1
+ ########################################################################
2
+ # File:: implementation.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Service authors create subclasses of
6
+ # Hoodoo::Services::Service, which lists the one or more
7
+ # subclasses of Hoodoo::Services::Interface the service author
8
+ # writes; each of those declares an interface which refers,
9
+ # for each interface endpoint, to a subclass of the class
10
+ # described here. Service authors create the body of their
11
+ # service implementation within the subclass.
12
+ #
13
+ # This file, then, does very little beyond describing the
14
+ # method framework that service authors use.
15
+ # ----------------------------------------------------------------------
16
+ # 24-Sep-2014 (ADH): Created.
17
+ ########################################################################
18
+
19
+ module Hoodoo; module Services
20
+
21
+ # Service authors subclass this to produce the body of their service
22
+ # interface implementation. It defines a series of methods that must be
23
+ # implemented in order to service requests.
24
+ #
25
+ # A Hoodoo::Services::Implementation subclass is selected by the platform
26
+ # middleware because a Hoodoo::Services::Interface subclass tells it about
27
+ # the implementation class through the Hoodoo::Services::Interface::interface
28
+ # DSL; the interface class is referenced from an
29
+ # Hoodoo::Services::Service subclass through the
30
+ # Hoodoo::Services::Service::comprised_of DSL; and the application class
31
+ # is run by Rack by being passed to a call to +run+ in +config.ru+.
32
+ #
33
+ class Implementation
34
+
35
+ # Implement a "list" action (paginated, sorted list of resources).
36
+ #
37
+ # +context+:: Hoodoo::Services::Context instance describing authorised
38
+ # session information, inbound request information and holding
39
+ # the response object that the service updates with the results
40
+ # of its processing of this action.
41
+ #
42
+ def list( context )
43
+ raise "Hoodoo::Services::Implementation subclasses must implement 'list'"
44
+ end
45
+
46
+ # Implement a "show" action (represent one existing resource instance).
47
+ #
48
+ # +context+:: Hoodoo::Services::Context instance describing authorised
49
+ # session information, inbound request information and holding
50
+ # the response object that the service updates with the results
51
+ # of its processing of this action.
52
+ #
53
+ def show( context )
54
+ raise "Hoodoo::Services::Implementation subclasses must implement 'show'"
55
+ end
56
+
57
+ # Implement a "create" action (store one new resource instance).
58
+ #
59
+ # +context+:: Hoodoo::Services::Context instance describing authorised
60
+ # session information, inbound request information and holding
61
+ # the response object that the service updates with the results
62
+ # of its processing of this action.
63
+ #
64
+ def create( context )
65
+ raise "Hoodoo::Services::Implementation subclasses must implement 'create'"
66
+ end
67
+
68
+ # Implement a "update" action (modify one existing resource instance).
69
+ #
70
+ # +context+:: Hoodoo::Services::Context instance describing authorised
71
+ # session information, inbound request information and holding
72
+ # the response object that the service updates with the results
73
+ # of its processing of this action.
74
+ #
75
+ def update( context )
76
+ raise "Hoodoo::Services::Implementation subclasses must implement 'update'"
77
+ end
78
+
79
+ # Implement a "delete" action (delete one existing resource instance).
80
+ #
81
+ # +context+:: Hoodoo::Services::Context instance describing authorised
82
+ # session information, inbound request information and holding
83
+ # the response object that the service updates with the results
84
+ # of its processing of this action.
85
+ #
86
+ def delete( context )
87
+ raise "Hoodoo::Services::Implementation subclasses must implement 'delete'"
88
+ end
89
+
90
+ # Optional verification to allow or deny authorisation for a particular
91
+ # action on a call-by-call basis.
92
+ #
93
+ # The middleware calls this method if a session
94
+ # (Hoodoo::Services::Session) has associated permissions
95
+ # (Hoodoo::Services::Permissions) which say that the resource's
96
+ # implementation should be asked via constant
97
+ # (Hoodoo::Services::Permissions::ASK).
98
+ #
99
+ # +context+:: Hoodoo::Services::Context instance as for action methods
100
+ # such as #show, #list and so forth.
101
+ #
102
+ # +action+:: The action that the caller is trying to perform, as a
103
+ # Symbol from the list in
104
+ # Hoodoo::Services::Middleware::ALLOWED_ACTIONS.
105
+ #
106
+ # Your implementation *MUST* return either
107
+ # Hoodoo::Services::Permissions::ALLOW, to allow the action, or
108
+ # Hoodoo::Services::Permissions::DENY, to block the action.
109
+ #
110
+ # * If a session's permissions indicate that a resource endpoint should
111
+ # be asked, but that interface does not define its own #verify method,
112
+ # then the default implementation herein will _deny_ the request.
113
+ #
114
+ # * If a buggy verification method returns an unexpected value, the
115
+ # middleware will ignore it and again _deny_ the request.
116
+ #
117
+ # Whether or not any of your implementations ever need to write a custom
118
+ # verification method will depend entirely upon your API, whether or not
119
+ # it has a meaningful definition of per-request assessment to allow or
120
+ # deny access and whether or not any sessions can exist with an 'ask'
121
+ # permission inside in the first place. If using the Hoodoo authorisation
122
+ # and authentication mechanism, this would come down to whether or not
123
+ # any Hoodoo::Data::Resources::Caller instances existed with the
124
+ # relevant permission value defined somewhere inside.
125
+ #
126
+ def verify( context, action )
127
+ return Hoodoo::Services::Permissions::DENY
128
+ end
129
+
130
+ end
131
+
132
+ end; end
@@ -0,0 +1,934 @@
1
+ ########################################################################
2
+ # File:: interface.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Define a class (and some namespace-nested related support
6
+ # classes) that are subclassed by service authors and used to
7
+ # declare the nature of the interface the service implements
8
+ # via a small DSL.
9
+ # ----------------------------------------------------------------------
10
+ # 23-Sep-2014 (ADH): Created.
11
+ ########################################################################
12
+
13
+ require 'set'
14
+
15
+ module Hoodoo; module Services
16
+
17
+ # Service implementation authors subclass this to describe the interface that
18
+ # they implement for a particular Resource, as documented in the Loyalty
19
+ # Platform API.
20
+ #
21
+ # See class method ::interface for details.
22
+ #
23
+ class Interface
24
+
25
+ ###########################################################################
26
+
27
+ # A class containing a series of accessors that describe allowed parameters
28
+ # in a "list" call for a service implementation. The middleware uses this
29
+ # validate incoming query strings for lists and reject requests that ask
30
+ # for unsupported things. When instantiated the class sets itself up with
31
+ # defaults that match those described by the your platform's API. When
32
+ # passed to a Hoodoo::Services::Interface::ToListDSL instance, the DSL
33
+ # methods, if called, update the values stored herein.
34
+ #
35
+ class ToList
36
+
37
+ # Limit value; an integer that limits page size in lists.
38
+ #
39
+ attr_reader :limit
40
+
41
+ # Sort hash. Keys are supported sort fields, values are arrays of
42
+ # supported sort directions. The first array entry is the default sort
43
+ # order for the sort field.
44
+ #
45
+ attr_reader :sort
46
+
47
+ # Default sort key.
48
+ #
49
+ attr_reader :default_sort_key
50
+
51
+ # Default sort direction.
52
+ #
53
+ def default_sort_direction
54
+ @sort[ default_sort_key() ].first
55
+ end
56
+
57
+ # Array of supported search keys as Strings; empty for none defined.
58
+ #
59
+ attr_reader :search
60
+
61
+ # Array of supported filter keys as Strings; empty for none defined.
62
+ #
63
+ attr_reader :filter
64
+
65
+ # Create an instance with default settings.
66
+ #
67
+ def initialize
68
+
69
+ # Remember, these are defaults for the "to_list" object of an
70
+ # interface only. For interface-wide top level defaults, use the
71
+ # embedded calls to the DSL in Interface::interface.
72
+
73
+ @limit = 50
74
+ @sort = { 'created_at' => Set.new( [ 'desc', 'asc' ] ) }
75
+ @default_sort_key = 'created_at'
76
+ @search = []
77
+ @filter = []
78
+ end
79
+
80
+ private
81
+
82
+ # Private writer - see #limit - but there's a special contract with
83
+ # Hoodoo::Services::Interface::ToListDSL which permits it to call here
84
+ # bypassing +private+ via +send()+.
85
+ #
86
+ attr_writer :limit
87
+
88
+ # Private writer - see #sort - but there's a special contract with
89
+ # Hoodoo::Services::Interface::ToListDSL which permits it to call here
90
+ # bypassing +private+ via +send()+.
91
+ #
92
+ attr_writer :sort
93
+
94
+ # Private writer - see #default_sort_key - but there's a special
95
+ # contract with Hoodoo::Services::Interface::ToListDSL which permits it
96
+ # to call here bypassing +private+ via +send()+.
97
+ #
98
+ attr_writer :default_sort_key
99
+
100
+ # Private writer - see #search - but there's a special contract with
101
+ # Hoodoo::Services::Interface::ToListDSL which permits it to call here
102
+ # bypassing +private+ via +send()+.
103
+ #
104
+ attr_writer :search
105
+
106
+ # Private writer - see #filter - but there's a special contract with
107
+ # Hoodoo::Services::Interface::ToListDSL which permits it to call here
108
+ # bypassing +private+ via +send()+.
109
+ #
110
+ attr_writer :filter
111
+
112
+ end # 'class ToList'
113
+
114
+ ###########################################################################
115
+
116
+ # Implementation of the DSL that's written inside a block passed to
117
+ # Hoodoo::Services::Interface#to_list. This is an internal implementation
118
+ # class. Instantiate with a Hoodoo::Services::Interface::ToList instance,
119
+ # the data in which is updated as the DSL methods run.
120
+ #
121
+ class ToListDSL
122
+
123
+ # Initialize an instance and run the DSL methods.
124
+ #
125
+ # +hoodoo_interface_to_list_instance+:: Instance of
126
+ # Hoodoo::Services::Interface::ToList
127
+ # to update with data
128
+ # from DSL method calls.
129
+ #
130
+ # &block:: Block of code that makes calls to the DSL herein.
131
+ #
132
+ # On exit, the DSL is run and the Hoodoo::Services::Interface::ToList has
133
+ # been updated.
134
+ #
135
+ def initialize( hoodoo_interface_to_list_instance, &block )
136
+ @tl = hoodoo_interface_to_list_instance # Shorthand!
137
+
138
+ unless @tl.instance_of?( Hoodoo::Services::Interface::ToList )
139
+ raise "Hoodoo::Services::Interface::ToListDSL\#initialize requires a Hoodoo::Services::Interface::ToList instance - got '#{ @tl.class }'"
140
+ end
141
+
142
+ self.instance_eval( &block )
143
+ end
144
+
145
+ # Specify the page size (limit) for lists.
146
+ #
147
+ # +limit+:: Page size (integer).
148
+ #
149
+ # Example:
150
+ #
151
+ # limit 100
152
+ #
153
+ def limit( limit )
154
+ unless limit.is_a?( ::Integer )
155
+ raise "Hoodoo::Services::Interface::ToListDSL\#limit requires an Integer - got '#{ limit.class }'"
156
+ end
157
+
158
+ @tl.send( :limit=, limit )
159
+ end
160
+
161
+ # Specify extra sort keys and orders that add with whatever platform
162
+ # common defaults are already in place.
163
+ #
164
+ # +sort+:: Hash of sort keys, with values that are an array of supported
165
+ # sort directions. The first array entry is used as the default
166
+ # direction if no direction is specified in the client caller's
167
+ # query string. Use strings or symbols.
168
+ #
169
+ # To specify that a sort key should be the new default for the
170
+ # interface in question, wrap it in a call to the #default
171
+ # DSL method.
172
+ #
173
+ # Example - add sort key '+code+' with directions +:asc+ and +:desc+,
174
+ # plus sort key +:member+ which only supports direction +:asc+.
175
+ #
176
+ # sort :code => [ :asc, :desc ],
177
+ # :member => [ :asc ]
178
+ #
179
+ def sort( sort )
180
+ unless sort.is_a?( ::Hash )
181
+ raise "Hoodoo::Services::Interface::ToListDSL\#sort requires a Hash - got '#{ sort.class }'"
182
+ end
183
+
184
+ # Convert Hash keys to Strings and Arrays to Sets of Strings too.
185
+
186
+ sort = sort.inject( {} ) do | memo, ( k, v ) |
187
+ memo[ k.to_s ] = Set.new( v.map do | entry |
188
+ entry.to_s
189
+ end )
190
+ memo
191
+ end
192
+
193
+ merged = @tl.sort().merge( sort )
194
+ @tl.send( :sort=, merged )
195
+ end
196
+
197
+ # Used in conjunction with #sort. Specifies that a sort key should be
198
+ # the default sort order for the interface.
199
+ #
200
+ # Example - add sort key '+code+' with directions +:asc+ and +:desc+,
201
+ # plus sort key +:member+ which only supports direction +:asc+. Say that
202
+ # '+code+' is to be the default sort order.
203
+ #
204
+ # sort default( :code ) => [ :asc, :desc ],
205
+ # :member => [ :asc ]
206
+ #
207
+ def default( sort_key )
208
+ unless sort_key.is_a?( ::String ) || sort_key.is_a?( ::Symbol )
209
+ raise "Hoodoo::Services::Interface::ToListDSL\#default requires a String or Symbol - got '#{ sort_key.class }'"
210
+ end
211
+
212
+ @tl.send( :default_sort_key=, sort_key.to_s )
213
+ return sort_key
214
+ end
215
+
216
+ # Specify supported search keys in an array. The middleware will make
217
+ # sure the interface implementation is only called with search keys in
218
+ # that list. If a client attempts a search on an unsupported key, their
219
+ # request will be rejected by the middleware.
220
+ #
221
+ # If a service wants to do its own search validation, it should not list
222
+ # call here. Note also that only the keys are specified and validated;
223
+ # value escaping and validation, if necessary, is up to the service
224
+ # implementation.
225
+ #
226
+ # +search+:: Array of permitted search keys, as symbols or strings.
227
+ # The order of array entries is arbitrary.
228
+ #
229
+ # Example - allow searches specifying +first_name+ and +last_name+ keys:
230
+ #
231
+ # search :first_name, :last_name
232
+ #
233
+ def search( *search )
234
+ @tl.send( :search=, search.map { | item | item.to_s } )
235
+ end
236
+
237
+ # As #search, but for filtering.
238
+ #
239
+ # +filter+:: Array of permitted filter keys, as symbols or strings.
240
+ # The order of array entries is arbitrary.
241
+ #
242
+ def filter( *filter )
243
+ @tl.send( :filter=, filter.map { | item | item.to_s } )
244
+ end
245
+ end # 'class ToListDSL'
246
+
247
+ ###########################################################################
248
+
249
+ # Mandatory part of the interface DSL. Declare the interface's URL endpoint
250
+ # and the Hoodoo::Services::Implementation subclass to be invoked when
251
+ # client requests are sent to a URL matching the endpoint.
252
+ #
253
+ # No two interfaces can use the same endpoint within a service application,
254
+ # unless the describe a different interface version - see #version.
255
+ #
256
+ # Example:
257
+ #
258
+ # endpoint :estimations, PurchaseImplementation
259
+ #
260
+ # +uri_path_fragment+:: Path fragment to match at the start of a URL path,
261
+ # as a symbol or string, excluding leading "/". The
262
+ # URL path matches the fragment if the path starts
263
+ # with a "/", then matches the fragment exactly, then
264
+ # is followed by either ".", another "/", or the
265
+ # end of the path string. For example, a fragment of
266
+ # +:products+ matches all paths out of +/products+,
267
+ # +/products.json+ or +/products/22+, but does not
268
+ # match +/products_and_things+.
269
+ #
270
+ # +implementation_class+:: The Hoodoo::Services::Implementation subclass
271
+ # (the class itself, not an instance of it) that
272
+ # should be used when a request matching the
273
+ # path fragment is received.
274
+ #
275
+ def endpoint( uri_path_fragment, implementation_class )
276
+
277
+ # http://www.ruby-doc.org/core-2.2.3/Module.html#method-i-3C
278
+ #
279
+ unless implementation_class < Hoodoo::Services::Implementation
280
+ raise "Hoodoo::Services::Interface#endpoint must provide Hoodoo::Services::Implementation subclasses, but '#{ implementation_class }' was given instead"
281
+ end
282
+
283
+ self.class.send( :endpoint=, uri_path_fragment )
284
+ self.class.send( :implementation=, implementation_class )
285
+ end
286
+
287
+ # Declare the _major_ version of the interface being implemented. All
288
+ # service endpoints appear at "/v{version}/{endpoint}" relative to whatever
289
+ # root an edge layer defines. If a service interface does not specifiy its
290
+ # version, +1+ is assumed.
291
+ #
292
+ # Two interfaces can exist on the same endpoint provided their versions are
293
+ # different since the resulting route to reach them will be different too.
294
+ #
295
+ # +version+:: Integer major version number, e.g +2+.
296
+ #
297
+ def version( major_version )
298
+ self.class.send( :version=, major_version.to_s.to_i )
299
+ end
300
+
301
+ # List the actions that the service implementation supports. If you don't
302
+ # call this, the middleware assumes that all actions are available; else it
303
+ # only calls for supported actions. If you declared an empty array, your
304
+ # implementation would never be called.
305
+ #
306
+ # *supported_actions:: One or more from +:list+, +:show+, +:create+,
307
+ # +:update+ and +:delete+. Always use symbols, not
308
+ # strings. An exception is raised if unrecognised
309
+ # actions are given.
310
+ #
311
+ # Example:
312
+ #
313
+ # actions :list, :show
314
+ #
315
+ def actions( *supported_actions )
316
+ supported_actions.map! { | item | item.to_sym }
317
+ invalid = supported_actions - Hoodoo::Services::Middleware::ALLOWED_ACTIONS
318
+
319
+ unless invalid.empty?
320
+ raise "Hoodoo::Services::Interface#actions does not recognise one or more actions: '#{ invalid.join( ', ' ) }'"
321
+ end
322
+
323
+ self.class.send( :actions=, Set.new( supported_actions ) )
324
+ end
325
+
326
+ # List any actions which are public - NOT PROTECTED BY SESSIONS. For
327
+ # public actions, no X-Session-ID or similar header is consulted and
328
+ # no session data will be associated with your
329
+ # Hoodoo::Services::Context instance when action methods are called.
330
+ #
331
+ # Use with great care!
332
+ #
333
+ # Note that if the implementation of a public action needs to call
334
+ # other resources, it can only ever call them if those actions in
335
+ # those other resources are also public. The implementation of a
336
+ # public action is prohibited from making calls to protected actions
337
+ # in other resources.
338
+ #
339
+ # *public_actions:: One or more from +:list+, +:show+, +:create+,
340
+ # +:update+ and +:delete+. Always use symbols, not
341
+ # strings. An exception is raised if unrecognised
342
+ # actions are given.
343
+ #
344
+ def public_actions( *public_actions )
345
+ public_actions.map! { | item | item.to_sym }
346
+ invalid = public_actions - Hoodoo::Services::Middleware::ALLOWED_ACTIONS
347
+
348
+ unless invalid.empty?
349
+ raise "Hoodoo::Services::Interface#public_actions does not recognise one or more actions: '#{ invalid.join( ', ' ) }'"
350
+ end
351
+
352
+ self.class.send( :public_actions=, Set.new( public_actions ) )
353
+ end
354
+
355
+ # Set secure log actions.
356
+ #
357
+ # +secure_log_actions+:: A Hash, described below.
358
+ #
359
+ # The given Hash keys are names of actions as Symbols: +:list+,
360
+ # +:show+, +:create+, +:update+ or +:delete+. Values are +:request+,
361
+ # +:response+ or +:both+. For a given action targeted at this resource:
362
+ #
363
+ # * A key of +:request+ means that API call-related Hoodoo automatic
364
+ # logging will _exclude_ body data for the _inbound_ _request_, but
365
+ # still include body data in the response. Example: A POST to a Login
366
+ # resource includes a password which you don't want logged, but the
367
+ # response data doesn't quote the password back so is "safe". The
368
+ # secure log actions Hash for the Login resource's interface would
369
+ # include <tt>:create => :request</tt>.
370
+ #
371
+ # * A key of +:response+ means that API call-related Hoodoo automatic
372
+ # logging will _exclude_ body data for the _outbound_ _response_,
373
+ # but still include body data in the request. Example: A POST to a
374
+ # Caller resource creates a Caller with a generated authentication
375
+ # secret that's only exposed in the POST's response. The inbound
376
+ # data used to create that Caller can be safely logged, but the
377
+ # authentication secret is sensitive and shouldn't be recorded. The
378
+ # secure log actions Hash for the Caller resource's interface would
379
+ # include <tt>:create => :response</tt>.
380
+ #
381
+ # _ERROR_ _RESPONSES_ _ARE_ _STILL_ _LOGGED_ because that's useful data;
382
+ # so make sure that if you generate any custom errors in your service
383
+ # that secure data is not contained within them.
384
+ #
385
+ # * A key of +both+ has the same result as both +:request+ and
386
+ # +:response+, so body data is never logged. It's hard to come up
387
+ # with good examples of resources where both the incoming data is
388
+ # sensitive and the outgoing data is sensitive but the option is
389
+ # included for competion, as someone out there will need it.
390
+ #
391
+ # Example: The request body data sent by a caller into a resource's
392
+ # +:create+ action will not be logged:
393
+ #
394
+ # secure_log_for( { :create => :request } )
395
+ #
396
+ # Example: Neither the request data sent by a caller, nor the
397
+ # response data sent back, will be logged for an +:update+ action:
398
+ #
399
+ # secure_log_for( { :update => :both } )
400
+ #
401
+ # The default is an empty Hash; all actions have both inbound request
402
+ # body data and outbound response body data logged by Hoodoo.
403
+ #
404
+ def secure_log_for( secure_log_actions = {} )
405
+ secure_log_actions = Hoodoo::Utilities.symbolize( secure_log_actions )
406
+ invalid = secure_log_actions.keys - Hoodoo::Services::Middleware::ALLOWED_ACTIONS
407
+
408
+ unless invalid.empty?
409
+ raise "Hoodoo::Services::Interface#secure_log_for does not recognise one or more actions: '#{ invalid.join( ', ' ) }'"
410
+ end
411
+
412
+ self.class.send( :secure_log_for=, secure_log_actions )
413
+ end
414
+
415
+ # An array of supported embed keys (as per documentation, so singular or
416
+ # plural as per resource interface descriptions in the Loyalty Platform
417
+ # API). Things which can be embedded can also be referenced, via the
418
+ # <tt>_embed</tt> and <tt>_reference</tt> query string keys.
419
+ #
420
+ # The middleware uses the list to reject requests from clients which
421
+ # ask for embedded or referenced entities that were not listed by the
422
+ # interface. If you don't call here, or call here with an empty array,
423
+ # no embedding or referencing will be allowed for calls to the service
424
+ # implementation.
425
+ #
426
+ # +embed+:: Array of permitted embeddable entity names, as symbols or
427
+ # strings. The order of array entries is arbitrary.
428
+ #
429
+ # Example: An interface permits lists that request embedding or
430
+ # referencing of "vouchers", "balances" and "member":
431
+ #
432
+ # embed :vouchers, :balances, :member
433
+ #
434
+ # As a result, #embeds would return:
435
+ #
436
+ # [ 'vouchers', 'balances', 'member' ]
437
+ #
438
+ def embeds( *embeds )
439
+ self.class.send( :embeds=, embeds.map { | item | item.to_s } )
440
+ end
441
+
442
+ # Specify parameters related to common index parameters. The block contains
443
+ # calls to the DSL described by Hoodoo::Services::Interface::ToListDSL. The
444
+ # default values should be described by your platform's API - hard-coded at
445
+ # the time of writing as:
446
+ #
447
+ # limit 50
448
+ # sort :created_at => [ :desc, :asc ]
449
+ # search nil
450
+ # filter nil
451
+ #
452
+ def to_list( &block )
453
+ Hoodoo::Services::Interface::ToListDSL.new(
454
+ self.class.instance_variable_get( '@to_list' ),
455
+ &block
456
+ )
457
+ end
458
+
459
+ # Optional description of the JSON parameters (schema) that the interface's
460
+ # implementation requires for calls creating resource instances. The block
461
+ # uses the DSL from Hoodoo::Presenters::Object, so you can specify
462
+ # basic object things like +string+, or higher level things like +type+ or
463
+ # +resource+.
464
+ #
465
+ # If a call comes into the middleware from a client which contains body
466
+ # data that doesn't validate according to your schema, it'll be rejected
467
+ # before even getting as far as your interface implementation.
468
+ #
469
+ # Default values for fields where present are for _rendering_ _only_; they
470
+ # are not injected into the inbound body for (say) persistence at database
471
+ # levels. A returned, rendered representation based on the same schema
472
+ # would have the default values present only. If you need default values
473
+ # at the persistence layer too, define them there too with whatever
474
+ # mechanism is most appropriate for your chosen persistence approach.
475
+ #
476
+ # The Hoodoo::Presenters::Object#internationalised DSL method can be
477
+ # called within your block harmlessly, but it has no side effects. Any
478
+ # resource interface that can take internationalised data for creation (or
479
+ # modification) must already have an internationalised representation, so
480
+ # the standard resources in the Hoodoo::Data::Resources collection will
481
+ # already have declared that internationalisation applies.
482
+ #
483
+ # Example 1:
484
+ #
485
+ # to_create do
486
+ # string :name, :length => 32, :required => true
487
+ # text :description
488
+ # end
489
+ #
490
+ # Example 2: With a resource
491
+ #
492
+ # to_create do
493
+ # resource Product # Fields are *inline*
494
+ # end
495
+ #
496
+ # &block:: Block, passed to Hoodoo::Presenters::Object, describing
497
+ # the fields used for resource creation.
498
+ #
499
+ def to_create( &block )
500
+ obj = Class.new( Hoodoo::Presenters::Base )
501
+ obj.schema( &block )
502
+
503
+ self.class.send( :to_create=, obj )
504
+ end
505
+
506
+ # As #to_create, but applies when modifying existing resource instances.
507
+ # To avoid repeating yourself, if your modification and creation parameter
508
+ # requirements are identical, call #update_same_as_create.
509
+ #
510
+ # The "required" flag is ignored for updates, because an omitted field for
511
+ # an update to an existing resource instance simply means "do not change
512
+ # the current value". As with #to_create, default values have relevance
513
+ # to the rendering stage only and have no effect here.
514
+ #
515
+ # &block:: Block, passed to Hoodoo::Presenters::Object, describing
516
+ # the fields used for resource modification.
517
+ #
518
+ def to_update( &block )
519
+ obj = Class.new( Hoodoo::Presenters::Base )
520
+ obj.schema( &block )
521
+
522
+ # When updating, 'required' fields in schema aren't required; you just
523
+ # omit a field to avoid changing its value. Walk the to-update schema
524
+ # graph stripping out any such problematic attributes.
525
+ #
526
+ obj.walk do | property |
527
+ property.required = false
528
+ end
529
+
530
+ self.class.send( :to_update=, obj )
531
+ end
532
+
533
+ # Declares that the expected JSON fields described in a #to_create call are
534
+ # the same as those required for modifying resources too.
535
+ #
536
+ # Example:
537
+ #
538
+ # update_same_as_create
539
+ #
540
+ # ...and that's all. There are no parameters or blocks needed.
541
+ #
542
+ def update_same_as_create
543
+ self.send( :to_update, & self.class.to_create().get_schema_definition() )
544
+ end
545
+
546
+ # Declares custom errors that are part of this defined interface. This
547
+ # calls directly through to Hoodoo::ErrorDescriptions#errors_for, so
548
+ # see that for details.
549
+ #
550
+ # A service should usually define only a single domain of error using one
551
+ # call to #errors_for, but techncially can make as many calls for as many
552
+ # domains as required. Definitions are merged.
553
+ #
554
+ # +domain+:: Domain, e.g. 'purchase', 'transaction' - see
555
+ # Hoodoo::ErrorDescriptions#errors_for for details.
556
+ #
557
+ # &block:: Code block making Hoodoo::ErrorDescriptions DSL calls.
558
+ #
559
+ # Example:
560
+ #
561
+ # errors_for 'transaction' do
562
+ # error 'duplicate_transaction', status: 409, message: 'Duplicate transaction', :required => [ :client_uid ]
563
+ # end
564
+ #
565
+ def errors_for( domain, &block )
566
+ descriptions = self.class.errors_for
567
+
568
+ if descriptions.nil?
569
+ descriptions = self.class.send( :errors_for=, Hoodoo::ErrorDescriptions.new )
570
+ end
571
+
572
+ descriptions.errors_for( domain, &block )
573
+ end
574
+
575
+ # Declare additional permissions that you require for a given action.
576
+ #
577
+ # If the implementation of a resource endpoint involves making calls out
578
+ # to other resources, then you need to consider how authorisation is
579
+ # granted to those other resources.
580
+ #
581
+ # The Hoodoo::Services::Session instance for the inbound external caller
582
+ # carries a Hoodoo::Services::Permission instance describing the actions
583
+ # that the caller is permitted to do. The middleware enforces these
584
+ # permissions, so that a resource implementation won't be called at all
585
+ # unless the caller has permission to do so.
586
+ #
587
+ # These permissions continue to apply during inter-resource calls. The
588
+ # wider session context is always applied. So, if one resource calls
589
+ # another resource, either:
590
+ #
591
+ # * The inbound API caller's session must have all necessary permissions
592
+ # for both the resource it is actually directly calling, and for any
593
+ # actions in any resources that the called resource in turn calls (and
594
+ # so-on, for any chain of resources).
595
+ #
596
+ # ...or...
597
+ #
598
+ # * The resource uses this +additional_permissions_for+ method to declare
599
+ # up-front that it will require the described permissions when a
600
+ # particular action is performed on it. When an inter-resource call is
601
+ # made, a temporary internal-only session is constructed that merges
602
+ # the permissions of the inbound caller with the additional permissions
603
+ # requested by the resource. The downstream called resource needs no
604
+ # special case code at all - it just sees a valid session with valid
605
+ # permissions and does what the upstream resource asked of it.
606
+ #
607
+ # For example, suppose a resource Clock returns both a time and a date,
608
+ # by calling out to the Time and Date resources. One option is that the
609
+ # inbound caller must have +show+ action permissions for all of Clock,
610
+ # Time and Date; if any of those are missing, then an attempt to call
611
+ # +show+ on the Clock resource would result in a 403 response.
612
+ #
613
+ # The other option is for Clock's interface to declare its requirements:
614
+ #
615
+ # additional_permissions_for( :show ) do | p |
616
+ # p.set_resource( :Time, :show, Hoodoo::Services::Permissions::ALLOW )
617
+ # p.set_resource( :Date, :show, Hoodoo::Services::Permissions::ALLOW )
618
+ # end
619
+ #
620
+ # Suppose you could create Clock instances for some reason, but there
621
+ # was an audit trail for this; Clock must create an Audit entry itself,
622
+ # but you don't want to expose this ability to external callers through
623
+ # their session permissions; so, just declare your additional
624
+ # permissions for that specific inter-service case:
625
+ #
626
+ # additional_permissions_for( :create ) do | p |
627
+ # p.set_resource( :Audit, :create, Hoodoo::Services::Permissions::ALLOW )
628
+ # end
629
+ #
630
+ # The call says which action in _the_ _declaring_ _interface's_ _resource_
631
+ # is a target. The block takes a single parameter; this is a default
632
+ # initialisation Hoodoo::Services::Permissions instance. Use that
633
+ # object's methods to set up whatever permissions you need in other
634
+ # resources, to successfully process the action in question. You only
635
+ # need to describe the resources you immediately call, not the whole
636
+ # chain - if "this" resource calls another, then it's up to the other
637
+ # resource to in turn describe additional permissions should it make its
638
+ # own set of downstream calls to further resource endpoints.
639
+ #
640
+ # Setting default permissions or especially the default permission
641
+ # fallback inside the block is possible but *VERY* *STRONGLY*
642
+ # *DISCOURAGED*. Instead, precisely describe the downstream resources,
643
+ # actions and permissions that are required.
644
+ #
645
+ # Note an important restriction - public actions (see ::public_actions)
646
+ # cannot be augmented in this way. A public action in one resource can
647
+ # only ever call public actions in other resources. This is because no
648
+ # session is needed _at_ _all_ to call a public action; calling into a
649
+ # protected action in another resource from this context would require
650
+ # invention of a full caller context which would be entirely invented
651
+ # and could represent an accidental (and significant) security hole.
652
+ #
653
+ # If you call this method for the same action more than once, the last
654
+ # call will be the one that takes effect - each call overwrites the
655
+ # results of any previous call made for the same action.
656
+ #
657
+ # Parameters are:
658
+ #
659
+ # +action+:: The action in this interface which will require the
660
+ # additional permissions to be described. Pass a Symbol or
661
+ # equivalent String from the list in
662
+ # Hoodoo::Services::Middleware::ALLOWED_ACTIONS.
663
+ #
664
+ # &block:: Block which is passed a new, default state
665
+ # Hoodoo::Services::Permissions instance; make method calls
666
+ # on this instance to describe the required permissions.
667
+ #
668
+ def additional_permissions_for( action, &block )
669
+ action = action.to_s
670
+
671
+ unless block_given?
672
+ raise 'Hoodoo::Services::Interface#additional_permissions_for must be passed a block'
673
+ end
674
+
675
+ p = Hoodoo::Services::Permissions.new
676
+ yield( p )
677
+
678
+ additional_permissions = self.class.additional_permissions() || {}
679
+ additional_permissions[ action ] = p
680
+ self.class.send( :additional_permissions=, additional_permissions )
681
+ end
682
+
683
+ protected
684
+
685
+ # Define the subclass Service's interface. A DSL is used with methods
686
+ # documented in the Hoodoo::Services::InterfaceDSL class.
687
+ #
688
+ # The absolute bare minimum interface description just states that a
689
+ # particular implementation class is used when requests are made to a
690
+ # particular URL endpoint, which is implementing an interface for a
691
+ # particular given resource. For a hypothetical Magic resource interface:
692
+ #
693
+ # class MagicImplementation < Hoodoo::Services::Implementation
694
+ # # ...implementation code goes here...
695
+ # end
696
+ #
697
+ # class MagicInterface < Hoodoo::Services::Interface
698
+ # interface :Magic do
699
+ # endpoint :paul_daniels, MagicImplementation
700
+ # end
701
+ # end
702
+ #
703
+ # This would cause all calls to URLs at '/paul_daniels[...]' to be routed to
704
+ # an instance of the MagicImplementation class.
705
+ #
706
+ # Addtional DSL facilities allow the interface to say what HTTP methods
707
+ # it supports (in terms of the action methods that it supports inside its
708
+ # implementation class), describe any extra sort, search or filter data it
709
+ # allows beyond the common fields and describe the expected JSON fields for
710
+ # creation and/or modification actions. By specifing these, the service
711
+ # middleware code is able to do extra validation and sanitisation of client
712
+ # requests, but they're entirely optional if the implementation class wants
713
+ # to take over all of that itself.
714
+ #
715
+ # +resource+:: Name of the resource that the interface is for, as a String
716
+ # or Symbol (e.g. +:Purchase+).
717
+ #
718
+ # &block:: Block that calls the Hoodoo::Services::InterfaceDSL methods;
719
+ # #endpoint is the only mandatory call.
720
+ #
721
+ def self.interface( resource, &block )
722
+
723
+ if @to_list.nil?
724
+ @to_list = Hoodoo::Services::Interface::ToList.new
725
+ else
726
+ raise "Hoodoo::Services::Interface subclass unexpectedly ran ::interface more than once"
727
+ end
728
+
729
+ self.resource = resource.to_sym
730
+
731
+ interface = self.new
732
+ interface.instance_eval do
733
+ version 1
734
+ embeds # Nothing
735
+ actions *Hoodoo::Services::Middleware::ALLOWED_ACTIONS
736
+ public_actions # None
737
+ secure_log_for # None
738
+ end
739
+
740
+ interface.instance_eval( &block )
741
+
742
+ if self.endpoint.nil?
743
+ raise "Hoodoo::Services::Interface subclasses must always call the 'endpoint' DSL method in their interface descriptions"
744
+ end
745
+
746
+ end
747
+
748
+ # Define various class instance variable (sic.) accessors.
749
+ #
750
+ # * Instance variable: things set on individual "Foo" instances ("Foo.new")
751
+ # * Class instance variables: things set on the "Foo" class only
752
+ # * Class variables: things set on the "Foo" class _and_ _all_ _subclasses_
753
+ #
754
+ class << self
755
+
756
+ public
757
+
758
+ # Endpoint path as declared by service, without preceding "/", possibly
759
+ # as a symbol - e.g. +:products+ for "/products[...]" as an implied
760
+ # endpoint.
761
+ #
762
+ attr_reader :endpoint
763
+
764
+ # Major version of interface as an integer. All service endpoint routes
765
+ # have "v{version}/" as a prefix, e.g. "/v1/products[...]".
766
+ #
767
+ attr_reader :version
768
+
769
+ # Name of the resource the interface addresses as a symbol, e.g.
770
+ # +:Product+.
771
+ #
772
+ attr_reader :resource
773
+
774
+ # Implementation class for the service. An
775
+ # Hoodoo::Services::Implementation subclass - the class, not an
776
+ # instance of it.
777
+ #
778
+ attr_reader :implementation
779
+
780
+ # Supported action methods as a Set of symbols with one or more of
781
+ # +:list+, +:show+, +:create+, +:update+ or +:delete+. The presence of
782
+ # a Symbol indicates a supported action. If empty, no actions are
783
+ # supported. The default is for all actions to be present in the Set.
784
+ #
785
+ attr_reader :actions
786
+
787
+ # Public action methods as a Set of symbols with one or more of
788
+ # +:list+, +:show+, +:create+, +:update+ or +:delete+. The presence
789
+ # of a Symbol indicates an action open to the public and not subject
790
+ # to session security. If empty, all actions are protected by session
791
+ # security. The default is an empty Set.
792
+ #
793
+ attr_reader :public_actions
794
+
795
+ # Secure log actions set by #secure_log_for - see that call for
796
+ # details. The default is an empty Hash.
797
+ #
798
+ attr_reader :secure_log_for
799
+
800
+ # Array of strings listing allowed embeddable things. Each string
801
+ # matches the split up comma-separated value for query string
802
+ # <tt>_embed</tt> or <tt>_reference</tt> keys. For example:
803
+ #
804
+ # ...&_embed=foo,bar
805
+ #
806
+ # ...would be valid provided there was an embedding declaration
807
+ # such as:
808
+ #
809
+ # embeds :foo, :bar
810
+ #
811
+ # ...which would in turn lead this accessor to return:
812
+ #
813
+ # [ 'foo', 'bar' ]
814
+ #
815
+ attr_reader :embeds
816
+
817
+ # A Hoodoo::Services::Interface::ToList instance describing the list
818
+ # parameters for the interface as a Set of Strings. See also
819
+ # Hoodoo::Services::Interface::ToListDSL.
820
+ #
821
+ def to_list
822
+ @to_list ||= Hoodoo::Services::Interface::ToList.new
823
+ @to_list
824
+ end
825
+
826
+ # A Hoodoo::Presenters::Object instance describing the schema
827
+ # for client JSON coming in for calls that create instances of the
828
+ # resource that the service's interface is addressing. If +nil+,
829
+ # arbitrary data is acceptable (the implementation becomes entirely
830
+ # responsible for data validation).
831
+ #
832
+ attr_reader :to_create
833
+
834
+ # A Hoodoo::Presenters::Object instance describing the schema
835
+ # for client JSON coming in for calls that modify instances of the
836
+ # resource that the service's interface is addressing. If +nil+,
837
+ # arbitrary data is acceptable (the implementation becomes entirely
838
+ # responsible for data validation).
839
+ #
840
+ attr_reader :to_update
841
+
842
+ # A Hoodoo::ErrorDescriptions instance describing all errors that
843
+ # the interface might return, including the default set of platform
844
+ # and generic errors. If nil, there are no additional error codes
845
+ # beyond the default set.
846
+ #
847
+ attr_reader :errors_for
848
+
849
+ # A Hash, keyed by String equivalents of the Symbols in
850
+ # Hoodoo::Services::Middleware::ALLOWED_ACTIONS, where the values
851
+ # are Hoodoo::Services::Permissions instances describing extended
852
+ # permissions for the related action. See
853
+ # ::additional_permissions_for.
854
+ #
855
+ attr_reader :additional_permissions
856
+
857
+ private
858
+
859
+ # Private property writer allows instances running the DSL to set
860
+ # values on the class for querying using the public readers.
861
+ # See ::endpoint.
862
+ #
863
+ attr_writer :endpoint
864
+
865
+ # Private property writer allows instances running the DSL to set
866
+ # values on the class for querying using the public readers.
867
+ # See ::version.
868
+ #
869
+ attr_writer :version
870
+
871
+ # Private property writer allows instances running the DSL to set
872
+ # values on the class for querying using the public readers.
873
+ # See ::resource.
874
+ #
875
+ attr_writer :resource
876
+
877
+ # Private property writer allows instances running the DSL to set
878
+ # values on the class for querying using the public readers.
879
+ # See ::implementation.
880
+ #
881
+ attr_writer :implementation
882
+
883
+ # Private property writer allows instances running the DSL to set
884
+ # values on the class for querying using the public readers.
885
+ # See ::actions.
886
+ #
887
+ attr_writer :actions
888
+
889
+ # Private property writer allows instances running the DSL to set
890
+ # values on the class for querying using the public readers.
891
+ # See ::public_actions.
892
+ #
893
+ attr_writer :public_actions
894
+
895
+ # Private property writer allows instances running the DSL to set
896
+ # values on the class for querying using the public readers.
897
+ # See ::secure_log_for.
898
+ #
899
+ attr_writer :secure_log_for
900
+
901
+ # Private property writer allows instances running the DSL to set
902
+ # values on the class for querying using the public readers.
903
+ # See ::embeds.
904
+ #
905
+ attr_writer :embeds
906
+
907
+ # Private property writer allows instances running the DSL to set
908
+ # values on the class for querying using the public readers.
909
+ # See ::to_create.
910
+ #
911
+ attr_writer :to_create
912
+
913
+ # Private property writer allows instances running the DSL to set
914
+ # values on the class for querying using the public readers.
915
+ # See ::to_update.
916
+ #
917
+ attr_writer :to_update
918
+
919
+ # Private property writer allows instances running the DSL to set
920
+ # values on the class for querying using the public readers.
921
+ # See ::errors_for.
922
+ #
923
+ attr_writer :errors_for
924
+
925
+ # Private property writer allows instances running the DSL to set
926
+ # values on the class for querying using the public readers.
927
+ # See ::additional_permissions.
928
+ #
929
+ attr_writer :additional_permissions
930
+
931
+ end # 'class << self'
932
+ end # 'class Interface'
933
+
934
+ end; end # 'module Hoodoo; module Services'