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