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,141 @@
1
+ ########################################################################
2
+ # File:: service.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Define a class that service authors subclass and use to
6
+ # declare the component interfaces within the service via a
7
+ # very small DSL.
8
+ #
9
+ # This class is passed to Rack and treated like an endpoint
10
+ # Rack application, though the service middleware in practice
11
+ # does not pass on calls using the Rack interface; it uses the
12
+ # custom calls exposed by Hoodoo::Services::Implementation.
13
+ # Rack's involvement between the two is really limited to just
14
+ # passing an instance of the service application subclass to
15
+ # the middleware so it knows who to "talk to".
16
+ # ----------------------------------------------------------------------
17
+ # 23-Sep-2014 (ADH): Created.
18
+ ########################################################################
19
+
20
+ module Hoodoo; module Services
21
+
22
+ # Hoodoo::Services::Service is subclassed by people writing service
23
+ # implementations; the subclasses are the entrypoint for platform services.
24
+ #
25
+ # It's really just a container of one or more interface classes, which are
26
+ # all Hoodoo::Services::Interface subclasses. The Rack middleware in
27
+ # Hoodoo::Services::Middleware uses the Hoodoo::Services::Service to find
28
+ # out what interfaces it implements. Those interface classes nominate a Ruby
29
+ # class of the author's choice in which they've written the implementation
30
+ # for that interface. Interfaces also declare themselves to be available at
31
+ # a particular URL endpoint (as a path fragment); this is used by the
32
+ # middleware to route inbound requests to the correct implementation class.
33
+ #
34
+ # Suppose we defined a PurchaseInterface and RefundInterface which we wanted
35
+ # both to be available as a Shopping Service:
36
+ #
37
+ # class PurchaseImplementation < Hoodoo::Services::Implementation
38
+ # # ...
39
+ # end
40
+ #
41
+ # class PurchaseInterface < Hoodoo::Services::Interface
42
+ # interface :Purchase do
43
+ # endpoint :purchases, PurchaseImplementation
44
+ # # ...
45
+ # end
46
+ # end
47
+ #
48
+ # class RefundImplementation < Hoodoo::Services::Implementation
49
+ # # ...
50
+ # end
51
+ #
52
+ # class RefundInterface < Hoodoo::Services::Interface
53
+ # interface :Refund do
54
+ # endpoint :refunds, RefundImplementation
55
+ # # ...
56
+ # end
57
+ # end
58
+ #
59
+ # ...then the *entire* Service subclass for the Shopping Service
60
+ # could be as small as this:
61
+ #
62
+ # class ShoppingService < Hoodoo::Services::Service
63
+ # comprised_of PurchaseInterface,
64
+ # RefundInterface
65
+ # end
66
+ #
67
+ # Names of subclasses in the above examples are chosen for clarity and the
68
+ # naming approach indicated is recommended, but it's not mandatory. Choose
69
+ # choose whatever you feel best fits your code and style.
70
+ #
71
+ # Conceptually, one might just have a single interface per application for
72
+ # very small services, but you may want to logically group more interfaces in
73
+ # one service for code clarity/locality. More realistically, efficiency may
74
+ # dictate that certain interfaces have such heavy reliance and relationships
75
+ # between database contents that sharing the data models between those
76
+ # interface classes makes sense; you would group them under the same service
77
+ # application, sacrificing full decoupling. As a service author, the choice
78
+ # is yours.
79
+ #
80
+ class Service
81
+
82
+ # Return an array of the classes that make up the interfaces for this
83
+ # service. Each is a Hoodoo::Services::Interface subclass that was
84
+ # registered by the subclass through a call to #comprised_of.
85
+ #
86
+ def self.component_interfaces
87
+ @component_interfaces
88
+ end
89
+
90
+ # Instance method which calls through to ::component_interfaces and returns
91
+ # its result.
92
+ #
93
+ def component_interfaces
94
+ self.class.component_interfaces
95
+ end
96
+
97
+ # Since service implementations are not pure Rack apps but really service
98
+ # middleware clients, they shouldn't ever have "call" invoked directly.
99
+ # This method is not intended to be overridden and just complains if Rack
100
+ # ends up calling here directly by accident.
101
+ #
102
+ # +env+:: Rack environment (ignored).
103
+ #
104
+ def call( env )
105
+ raise "Hoodoo::Services::Implementation subclasses should only be called through the middleware - add 'use Hoodoo::Services::Middleware' to (e.g.) config.ru"
106
+ end
107
+
108
+ protected
109
+
110
+ # Called by subclasses listing one or more Hoodoo::Services::Interface
111
+ # subclasses that make up the service implementation as a whole.
112
+ #
113
+ # Example:
114
+ #
115
+ # class ShoppingService < Hoodoo::Services::Service
116
+ # comprised_of PurchaseInterface,
117
+ # RefundInterface
118
+ # end
119
+ #
120
+ # See this class's general Hoodoo::Services::Service documentation for
121
+ # more details.
122
+ #
123
+ def self.comprised_of( *classes )
124
+
125
+ # http://www.ruby-doc.org/core-2.2.3/Module.html#method-i-3C
126
+ #
127
+ classes.each do | klass |
128
+ unless klass < Hoodoo::Services::Interface
129
+ raise "Hoodoo::Services::Implementation::comprised_of expects Hoodoo::Services::Interface subclasses only - got '#{ klass }'"
130
+ end
131
+ end
132
+
133
+ # Add the classes from this call to any given in a previous call.
134
+
135
+ @component_interfaces ||= []
136
+ @component_interfaces += classes
137
+ @component_interfaces.uniq!
138
+ end
139
+ end
140
+
141
+ end; end
@@ -0,0 +1,729 @@
1
+ ########################################################################
2
+ # File:: session.rb
3
+ # (C):: Loyalty New Zealand 2015
4
+ #
5
+ # Purpose:: Container for information about the context in which a
6
+ # service is called.
7
+ # ----------------------------------------------------------------------
8
+ # 04-Feb-2015 (ADH): Created.
9
+ ########################################################################
10
+
11
+ require 'ostruct'
12
+ require 'dalli'
13
+
14
+ module Hoodoo
15
+ module Services
16
+
17
+ # A container for functionality related to a context session.
18
+ #
19
+ class Session
20
+
21
+ # Time To Live: Number of seconds for which a session remains valid
22
+ # after being saved. Only applicable from the save time onwards in
23
+ # stores that support TTL such as Memcached - see #save_to_memcached.
24
+ #
25
+ TTL = 172800 # 48 hours
26
+
27
+ # A Session must have its own UUID.
28
+ #
29
+ attr_accessor :session_id
30
+
31
+ # A Session must always refer to a Caller instance by UUID.
32
+ #
33
+ attr_accessor :caller_id
34
+
35
+ # Callers can change; if so, related sessions must be invalidated.
36
+ # This must be achieved by keeping a version count on the Caller. A
37
+ # session is associated with a particular Caller version and if the
38
+ # version changes, associated sessions are flushed.
39
+ #
40
+ # If you _change_ a Caller version in a Session, you _really_ should
41
+ # call #save_to_memcached as soon as possible afterwards so that the
42
+ # change gets recognised in Memcached.
43
+ #
44
+ attr_accessor :caller_version
45
+
46
+ # An OpenStruct instance with session-creator defined key/value pairs
47
+ # that define the _identity_ of the session holder. This is usually
48
+ # related to a Caller resource instance - see also
49
+ # Hoodoo::Data::Resources::Caller - and will often contain a Caller
50
+ # resource instance's UUID, amongst other data.
51
+ #
52
+ # The object describes "who the session's owner is".
53
+ #
54
+ attr_reader :identity
55
+
56
+ # Set the identity data via a Hash of key/value pairs - see also
57
+ # #identity.
58
+ #
59
+ def identity=( hash )
60
+ @identity = OpenStruct.new( hash )
61
+ end
62
+
63
+ # A Hoodoo::Services::Permissions instance.
64
+ #
65
+ # The instance describes "what the session is allowed to do".
66
+ #
67
+ attr_accessor :permissions
68
+
69
+ # An OpenStruct instance with session-creator defined values that
70
+ # describe the _scoping_, that is, visbility of data, for the session.
71
+ # Its contents relate to service resource interface descriptions (see
72
+ # the DSL for Hoodoo::Services::Interface) and may be partially or
73
+ # entirely supported by the ActiveRecord finder extensions in
74
+ # Hoodoo::ActiveRecord::Finder.
75
+ #
76
+ # The object describes the "data that the session can 'see'".
77
+ #
78
+ attr_accessor :scoping
79
+
80
+ # Set the scoping data via a Hash of key/value pairs - see also
81
+ # #scoping.
82
+ #
83
+ def scoping=( hash )
84
+ @scoping = OpenStruct.new( hash )
85
+ end
86
+
87
+ # The creation date of this session instance as a Time instance in
88
+ # UTC.
89
+ #
90
+ attr_reader :created_at
91
+
92
+ # The expiry date for this session - the session should be considered
93
+ # expired at or after this date. Some session stores may support
94
+ # automatic expiry of session data, but there may be a small window
95
+ # between the expiry date passing and the store expiring the data; so
96
+ # always check the expiry.
97
+ #
98
+ # Only set when the session is saved (or loaded from a representation
99
+ # that includes an existing expiry date). See e.g.:
100
+ #
101
+ # * #save_to_memcached
102
+ #
103
+ # The value is a Time instance in UTC. If +nil+, the session has not
104
+ # yet been saved.
105
+ #
106
+ attr_reader :expires_at
107
+
108
+ # Connection IP address/port String for Memcached.
109
+ #
110
+ # If you are using Memcached for a session store, you can set the
111
+ # Memcached connection host either through this accessor, or via the
112
+ # object's constructor.
113
+ #
114
+ attr_accessor :memcached_host
115
+
116
+ # Create a new instance.
117
+ #
118
+ # +options+:: Optional Hash of options, described below.
119
+ #
120
+ # Options are:
121
+ #
122
+ # +session_id+:: UUID of this session. If unset, a new UUID is
123
+ # generated for you. You can read the UUID with
124
+ # the #session_id accessor method.
125
+ #
126
+ # +caller_id+:: UUID of the Caller instance associated with this
127
+ # session. This can be set either now or later, but
128
+ # the session cannot be saved without it.
129
+ #
130
+ # +caller_version+:: Version of the Caller instance; defaults to zero.
131
+ #
132
+ # +memcached_host+:: Host for Memcached connections; required if you
133
+ # want to use the #load_from_memcached! or
134
+ # #save_to_memcached methods.
135
+ #
136
+ def initialize( options = {} )
137
+ @created_at = Time.now.utc
138
+
139
+ self.session_id = options[ :session_id ] || Hoodoo::UUID.generate()
140
+ self.memcached_host = options[ :memcached_host ]
141
+ self.caller_id = options[ :caller_id ]
142
+ self.caller_version = options[ :caller_version ] || 0
143
+ end
144
+
145
+ # Save this session to Memcached, in a manner that will allow it to
146
+ # be loaded by #load_from_memcached! later.
147
+ #
148
+ # A session can only be saved if it has a Caller ID - see #caller_id=
149
+ # or the options hash passed to the constructor.
150
+ #
151
+ # The Hoodoo::Services::Session::TTL constant determines how long the
152
+ # key lives in Memcached.
153
+ #
154
+ # Returns a symbol:
155
+ #
156
+ # * +:ok+: The session data was saved OK and is valid. There was either
157
+ # a Caller record with an earlier or matching value in Memcached, or
158
+ # no preexisting record of the Caller.
159
+ #
160
+ # * +:outdated+: The session data could not be saved because an existing
161
+ # Caller record was found in Memcached with a _newer_ version than
162
+ # 'this' session, implying that the session is already outdated.
163
+ #
164
+ # * +:fail+: The session data could not be saved (Memcached problem).
165
+ #
166
+ def save_to_memcached
167
+ if self.caller_id.nil?
168
+ raise 'Hoodoo::Services::Session\#save_to_memcached: Cannot save this session as it has no assigned Caller UUID'
169
+ end
170
+
171
+ begin
172
+ mclient = self.class.connect_to_memcached( self.memcached_host() )
173
+
174
+ # Try to update the Caller version in Memcached using this
175
+ # Session's data. If this fails, the Caller version is out of
176
+ # date or we couldn't talk to Memcached. Either way, bail out.
177
+
178
+ result = update_caller_version_in_memcached( self.caller_id,
179
+ self.caller_version,
180
+ mclient )
181
+
182
+ return result unless result === :ok
183
+
184
+ # Must set this before saving, even though the delay between
185
+ # setting this value and Memcached actually saving the value
186
+ # with a TTL will mean that Memcached expires the key slightly
187
+ # *after* the time we record.
188
+
189
+ @expires_at = ( ::Time.now + TTL ).utc()
190
+
191
+ return :ok if mclient.set( self.session_id,
192
+ self.to_h(),
193
+ TTL )
194
+
195
+ rescue Exception => exception
196
+
197
+ # Log error and return nil if the session can't be parsed
198
+ #
199
+ Hoodoo::Services::Middleware.logger.warn(
200
+ 'Hoodoo::Services::Session\#save_to_memcached: Session saving failed - connection fault or session corrupt',
201
+ exception.to_s
202
+ )
203
+
204
+ end
205
+
206
+ return :fail
207
+ end
208
+
209
+ # Load session data into this instance, overwriting instance values
210
+ # if the session is found. Raises an exception if there is a problem
211
+ # connecting to Memcached. A Memcached connection host must have been
212
+ # set through the constructor or #memcached_host accessor first.
213
+ #
214
+ # +sid+:: The Session UUID to look up.
215
+ #
216
+ # Returns a symbol:
217
+ #
218
+ # * +:ok+: The session data was loaded OK and is valid.
219
+ #
220
+ # * +:outdated+: The session data was loaded, but is outdated; either
221
+ # the session has expired, or its Caller version mismatches the
222
+ # associated stored Caller version in Memcached.
223
+ #
224
+ # * +:not_found+: The session was not found.
225
+ #
226
+ # * +:fail+: The session data could not be loaded (Memcached problem).
227
+ #
228
+ def load_from_memcached!( sid )
229
+ begin
230
+ mclient = self.class.connect_to_memcached( self.memcached_host() )
231
+ session_hash = mclient.get( sid )
232
+
233
+ if session_hash.nil?
234
+ return :not_found
235
+ else
236
+ self.from_h!( session_hash )
237
+ return :outdated if self.expired?
238
+
239
+ cv = load_caller_version_from_memcached( mclient, self.caller_id )
240
+
241
+ if cv == nil || cv > self.caller_version
242
+ return :outdated
243
+ else
244
+ return :ok
245
+ end
246
+ end
247
+
248
+ rescue Exception => exception
249
+
250
+ # Log error and return nil if the session can't be parsed
251
+ #
252
+ Hoodoo::Services::Middleware.logger.warn(
253
+ 'Hoodoo::Services::Session\#load_from_memcached!: Session loading failed - connection fault or session corrupt',
254
+ exception.to_s
255
+ )
256
+
257
+ end
258
+
259
+ return :fail
260
+ end
261
+
262
+ # Update the version of a given Caller in Memcached. This is done
263
+ # automatically when Sessions are saved to Memcached, but if external
264
+ # code alters any Callers independently, it *MUST* call here to keep
265
+ # Memcached records up to date.
266
+ #
267
+ # If no cached version is in Memcached for the Caller, the method
268
+ # assumes it is being called for the first time for that Caller and
269
+ # writes the version it has to hand, rather than considering it an
270
+ # error condition.
271
+ #
272
+ # +cid+:: Caller UUID of the Caller record to update.
273
+ #
274
+ # +cv+:: New version to store (an Integer).
275
+ #
276
+ # +mclient+:: Optional Dalli::Client instance to use for talking to
277
+ # Memcached. If omitted, a connection is established for
278
+ # you. This is mostly an optimisation parameter, used by
279
+ # code which already has established a connection and
280
+ # wants to avoid creating another unnecessarily.
281
+ #
282
+ # Returns a Symbol:
283
+ #
284
+ # * +:ok+: The Caller record was updated successfully.
285
+ #
286
+ # * +:outdated+: The Caller was already present in Memcached with a
287
+ # _higher version_ than the one you wanted to save. Your own local
288
+ # Caller data must therefore already be out of date.
289
+ #
290
+ # * +:fail+: The Caller could not be updated (Memcached problem).
291
+ #
292
+ def update_caller_version_in_memcached( cid, cv, mclient = nil )
293
+ begin
294
+ mclient ||= self.class.connect_to_memcached( self.memcached_host() )
295
+
296
+ cached_version = load_caller_version_from_memcached( mclient, cid )
297
+
298
+ if cached_version != nil && cached_version > cv
299
+ return :outdated
300
+ elsif save_caller_version_to_memcached( mclient, cid, cv ) == true
301
+ return :ok
302
+ end
303
+
304
+ rescue Exception => exception
305
+
306
+ # Log error and return nil if the session can't be parsed
307
+ #
308
+ Hoodoo::Services::Middleware.logger.warn(
309
+ 'Hoodoo::Services::Session\#update_caller_version_in_memcached: Client version update - connection fault or corrupt record',
310
+ exception.to_s
311
+ )
312
+
313
+ end
314
+
315
+ return :fail
316
+ end
317
+
318
+ # Delete this session from Memcached. The Session object is not
319
+ # modified.
320
+ #
321
+ # Returns a symbol:
322
+ #
323
+ # * +:ok+: The Session was deleted from Memcached successfully.
324
+ #
325
+ # * +:not_found+: This session was not found in Memcached.
326
+ #
327
+ # * +:fail+: The session data could not be deleted (Memcached problem).
328
+ #
329
+ def delete_from_memcached
330
+ begin
331
+
332
+ mclient = self.class.connect_to_memcached( self.memcached_host() )
333
+
334
+ if mclient.delete( self.session_id )
335
+ return :ok
336
+ else
337
+ return :not_found
338
+ end
339
+
340
+ rescue Exception => exception
341
+
342
+ # Log error and return nil if the session can't be parsed
343
+ #
344
+ Hoodoo::Services::Middleware.logger.warn(
345
+ 'Hoodoo::Services::Session\#delete_from_memcached: Session delete - connection fault',
346
+ exception.to_s
347
+ )
348
+
349
+ return :fail
350
+ end
351
+
352
+ end
353
+
354
+ # Has this session expired? Only valid if an expiry date is set;
355
+ # see #expires_at.
356
+ #
357
+ # Returns +true+ if the session has expired, or +false+ if it has
358
+ # either not expired, or has no expiry date set yet.
359
+ #
360
+ def expired?
361
+ exp = self.expires_at()
362
+ now = Time.now.utc
363
+
364
+ return ! ( exp.nil? || now < exp )
365
+ end
366
+
367
+ # Represent this session's data as a Hash, for uses such as
368
+ # storage in Memcached or loading into another session instance.
369
+ # See also #from_h!.
370
+ #
371
+ def to_h
372
+ hash = {}
373
+
374
+ %w(
375
+
376
+ session_id
377
+ caller_id
378
+ caller_version
379
+
380
+ ).each do | property |
381
+ value = self.send( property )
382
+ hash[ property ] = value unless value.nil?
383
+ end
384
+
385
+ %w(
386
+
387
+ created_at
388
+ expires_at
389
+
390
+ ).each do | property |
391
+ value = self.send( property )
392
+ hash[ property ] = value.iso8601() unless value.nil?
393
+ end
394
+
395
+ %w(
396
+
397
+ identity
398
+ scoping
399
+ permissions
400
+
401
+ ).each do | property |
402
+ value = self.send( property )
403
+ hash[ property ] = Hoodoo::Utilities.stringify( value.to_h() ) unless value.nil?
404
+ end
405
+
406
+ return hash
407
+ end
408
+
409
+ # Load session parameters from a given Hash, of the form set by
410
+ # #to_h.
411
+ #
412
+ # If appropriate Hash keys are present, will set any or all of
413
+ # #session_id, #identity, #scoping and #permissions.
414
+ #
415
+ def from_h!( hash )
416
+ hash = Hoodoo::Utilities.stringify( hash )
417
+
418
+ %w(
419
+
420
+ session_id
421
+ caller_id
422
+ caller_version
423
+
424
+ ).each do | property |
425
+ value = hash[ property ]
426
+ self.send( "#{ property }=", value ) unless value.nil?
427
+ end
428
+
429
+ %w(
430
+
431
+ created_at
432
+ expires_at
433
+
434
+ ).each do | property |
435
+ if hash.has_key?( property )
436
+ begin
437
+ instance_variable_set( "@#{ property }", Time.parse( hash[ property ] ).utc() )
438
+ rescue => e
439
+ # Invalid time given; keep existing date
440
+ end
441
+ end
442
+ end
443
+
444
+ %w(
445
+
446
+ identity
447
+ scoping
448
+
449
+ ).each do | property |
450
+ value = hash[ property ]
451
+ self.send( "#{ property }=", OpenStruct.new( value ) ) unless value.nil?
452
+ end
453
+
454
+ value = hash[ 'permissions' ]
455
+ self.permissions = Hoodoo::Services::Permissions.new( value ) unless value.nil?
456
+ end
457
+
458
+ # Speciality interface usually only called by the middleware, or
459
+ # components closely related to the middleware.
460
+ #
461
+ # Takes this session and creates a copy for an inter-resource call
462
+ # which adds any additional parameters that the calling interface
463
+ # says it needs in order to complete the currently handled action.
464
+ #
465
+ # Through calling this method, the middleware implements the access
466
+ # permission functionality described by
467
+ # Hoodoo::Services::Interface#additional_permissions_for.
468
+ #
469
+ # +interaction+:: Hoodoo::Services::Middleware::Interaction instance
470
+ # describing the current interaction. This is for the
471
+ # request that a resource implementation *is handling*
472
+ # at the point it wants to make an inter-resource call
473
+ # - it is *not* data related to the *target* of that
474
+ # call.
475
+ #
476
+ # Returns:
477
+ #
478
+ # * Hoodoo::Services::Session instance if everything works OK; this
479
+ # may be the same as, or different from, the input session depending
480
+ # on whether or not there were any permissions that needed adding.
481
+ #
482
+ # * +false+ if the session can't be saved due to a mismatched caller
483
+ # version - the session must have become invalid _during_ handling.
484
+ #
485
+ # If the augmented session cannot be saved due to a Memcached problem,
486
+ # an exception is raised and the generic handler will turn this into a
487
+ # 500 response for the caller. At this time, we really can't do much
488
+ # better than that since failure to save the augmented session means
489
+ # the inter-resource call cannot proceed; it's an internal fault.
490
+ #
491
+ def augment_with_permissions_for( interaction )
492
+
493
+ # Set up some convenience variables
494
+
495
+ interface = interaction.target_interface
496
+ action = interaction.requested_action
497
+
498
+ # If there are no additional permissions for this action, just return
499
+ # the current session back again.
500
+
501
+ action = action.to_s()
502
+ additional_permissions = ( interface.additional_permissions() || {} )[ action ]
503
+
504
+ return self if additional_permissions.nil?
505
+
506
+ # Otherwise, duplicate the session and its permissions (or generate
507
+ # defaults) and merge the additional permissions.
508
+
509
+ local_session = self.dup()
510
+ local_permissions = self.permissions ? self.permissions.dup() : Hoodoo::Services::Permissions.new
511
+
512
+ local_permissions.merge!( additional_permissions.to_h() )
513
+
514
+ # Make sure the new session has its own ID and set the updated
515
+ # permissions. Then try to save it and return the result.
516
+
517
+ local_session.session_id = Hoodoo::UUID.generate()
518
+ local_session.permissions = local_permissions
519
+
520
+ case local_session.save_to_memcached()
521
+ when :ok
522
+ return local_session
523
+
524
+ when :outdated
525
+ # Caller version mismatch; original session is now outdated and invalid
526
+ return false
527
+
528
+ else # Couldn't save it
529
+ raise "Unable to create interim session for inter-resource call from #{ interface.resource } / #{ action }"
530
+ end
531
+ end
532
+
533
+ private
534
+
535
+ # Connect to the Memcached server. Returns a new Dalli client
536
+ # instance. Raises an exception if no connection can be established.
537
+ #
538
+ # In test environments, returns a MockDalliClient instance.
539
+ #
540
+ # +host+:: Connection host (IP "address:port" String) for Memcached.
541
+ #
542
+ def self.connect_to_memcached( host )
543
+
544
+ if Hoodoo::Services::Middleware.environment.test? && MockDalliClient.bypass? == false
545
+ return MockDalliClient.new
546
+ end
547
+
548
+ if host.nil? || host.empty?
549
+ raise 'Hoodoo::Services::Session.connect_to_memcached: The Memcached connection host data is nil or empty'
550
+ end
551
+
552
+ stats = nil
553
+ mclient = nil
554
+
555
+ begin
556
+ @@dalli_clients ||= {}
557
+ @@dalli_clients[ host ] ||= ::Dalli::Client.new(
558
+ host,
559
+ {
560
+ :compress => false,
561
+ :serializer => JSON,
562
+ :namespace => :nz_co_loyalty_hoodoo_session_
563
+ }
564
+ )
565
+
566
+ stats = @@dalli_clients[ host ].stats()
567
+
568
+ rescue Exception => e
569
+ stats = nil
570
+
571
+ end
572
+
573
+ if stats.nil?
574
+ raise "Hoodoo::Services::Session.connect_to_memcached: Cannot connect to Memcached at '#{ host }'"
575
+ else
576
+ return @@dalli_clients[ host ]
577
+ end
578
+ end
579
+
580
+ # Try to read a cached Caller version from Memcached. Returns the
581
+ # cached version if available, or +nil+ if the record isn't found.
582
+ #
583
+ # May raise an exception for e.g. Memcached failures, via Dalli.
584
+ #
585
+ # TODO: As a temporary measure, compatibility bridge code in Authsome
586
+ # may call this private interface via ".send". Until that is
587
+ # decommissioned, the API shouldn't be changed without updating
588
+ # Authsome too.
589
+ #
590
+ # +mclient+:: A Dalli::Client instance to use for talking to
591
+ # Memcached.
592
+ #
593
+ # +cid+:: Caller UUID of interest.
594
+ #
595
+ def load_caller_version_from_memcached( mclient, cid )
596
+ version_hash = mclient.get( cid )
597
+ return version_hash.nil? ? nil : version_hash[ 'version' ]
598
+ end
599
+
600
+ # Save the Caller version for a given Caller ID to Memcached.
601
+ # Returns +true+ if successful, else +false+.
602
+ #
603
+ # May raise an exception for e.g. Memcached failures, via Dalli.
604
+ #
605
+ # Note that any existing record for the given Caller, if there
606
+ # is one, is unconditionally overwritten.
607
+ #
608
+ # +mclient+:: A Dalli::Client instance to use for talking to
609
+ # Memcached.
610
+ #
611
+ # +cid+:: Caller UUID of interest.
612
+ #
613
+ # +cv+:: Version to save for that Caller UUID.
614
+ #
615
+ def save_caller_version_to_memcached( mclient, cid, cv )
616
+ return !! mclient.set(
617
+ cid,
618
+ { 'version' => cv }
619
+ )
620
+ end
621
+
622
+ # Mock known uses of Dalli::Client with test implementations.
623
+ # Use explicitly, or as an RSpec implicit mock via something like
624
+ # this:
625
+ #
626
+ # allow( Dalli::Client ).to receive( :new ).and_return( Hoodoo::Services::Session::MockDalliClient.new )
627
+ #
628
+ # ...whenever you need to stub out real Memcached. You will
629
+ # probably want to add:
630
+ #
631
+ # before :all do # (or ":each")
632
+ # Hoodoo::Services::Session::MockDalliClient.reset()
633
+ # end
634
+ #
635
+ # ...to "clean out Memcached" before or between tests. You can
636
+ # check the contents of mock Memcached by examining ::store's
637
+ # hash of data.
638
+ #
639
+ class MockDalliClient
640
+ @@store = {}
641
+
642
+ # For test analysis, return the hash of 'memcached' mock data.
643
+ #
644
+ # Entries are referenced by the key you used to originally
645
+ # store them; values are hashes with ":expires_at" giving an
646
+ # expiry time or "nil" and ":value" giving your stored value.
647
+ #
648
+ def self.store
649
+ @@store
650
+ end
651
+
652
+ # Wipe out all saved data.
653
+ #
654
+ def self.reset
655
+ @@store = {}
656
+ end
657
+
658
+ # Pass +true+ to bypass the mock client (subject to the caller
659
+ # reading ::bypass?) to e.g. get test code coverage on real
660
+ # Memcached. Pass +false+ otherwise.
661
+ #
662
+ def self.bypass( bypass_boolean )
663
+ @@bypass = bypass_boolean
664
+ end
665
+
666
+ @@bypass = false
667
+
668
+ # If +true+, bypass this class and use real Dalli::Client; else
669
+ # don't. Default return value is +false+.
670
+ #
671
+ def self.bypass?
672
+ @@bypass
673
+ end
674
+
675
+ # Get the data stored under the given key. Returns +nil+ if
676
+ # not found / expired.
677
+ #
678
+ # +key+:: Key to look up (see #set).
679
+ #
680
+ def get( key )
681
+ data = @@store[ key ]
682
+ return nil if data.nil?
683
+
684
+ expires_at = data[ :expires_at ]
685
+ return nil unless expires_at.nil? || Time.now < expires_at
686
+
687
+ return data[ :value ]
688
+ end
689
+
690
+ # Set data for a given key.
691
+ #
692
+ # +key+:: Key under which to store data.
693
+ #
694
+ # +value+:: Data to store.
695
+ #
696
+ # +ttl+:: (Optional) time-to-live ('live' as in living, not as in
697
+ # 'live TV') - a value in seconds, after which the data is
698
+ # considered expired. If omitted, the data does not expire.
699
+ #
700
+ def set( key, value, ttl = nil )
701
+ data = {
702
+ :expires_at => ttl.nil? ? nil : Time.now.utc + ttl,
703
+ :value => value
704
+ }
705
+
706
+ @@store[ key ] = data
707
+ true
708
+ end
709
+
710
+ # Remove data for the given key.
711
+ #
712
+ def delete( key )
713
+ if @@store.has_key?( key )
714
+ @@store.delete( key )
715
+ true
716
+ else
717
+ false
718
+ end
719
+ end
720
+
721
+ # Mock 'stats' health check.
722
+ #
723
+ def stats
724
+ true
725
+ end
726
+ end
727
+ end
728
+ end
729
+ end