hoodoo 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (216) hide show
  1. checksums.yaml +7 -0
  2. data/bin/hoodoo +5 -0
  3. data/lib/hoodoo.rb +27 -0
  4. data/lib/hoodoo/active.rb +32 -0
  5. data/lib/hoodoo/active/active_model/uuid_validator.rb +45 -0
  6. data/lib/hoodoo/active/active_record/base.rb +81 -0
  7. data/lib/hoodoo/active/active_record/creator.rb +134 -0
  8. data/lib/hoodoo/active/active_record/dated.rb +343 -0
  9. data/lib/hoodoo/active/active_record/error_mapping.rb +351 -0
  10. data/lib/hoodoo/active/active_record/finder.rb +606 -0
  11. data/lib/hoodoo/active/active_record/search_helper.rb +189 -0
  12. data/lib/hoodoo/active/active_record/secure.rb +431 -0
  13. data/lib/hoodoo/active/active_record/support.rb +106 -0
  14. data/lib/hoodoo/active/active_record/translated.rb +87 -0
  15. data/lib/hoodoo/active/active_record/uuid.rb +80 -0
  16. data/lib/hoodoo/active/active_record/writer.rb +321 -0
  17. data/lib/hoodoo/client.rb +23 -0
  18. data/lib/hoodoo/client/augmented_array.rb +29 -0
  19. data/lib/hoodoo/client/augmented_base.rb +168 -0
  20. data/lib/hoodoo/client/augmented_hash.rb +23 -0
  21. data/lib/hoodoo/client/client.rb +354 -0
  22. data/lib/hoodoo/client/endpoint/endpoint.rb +427 -0
  23. data/lib/hoodoo/client/endpoint/endpoints/amqp.rb +180 -0
  24. data/lib/hoodoo/client/endpoint/endpoints/auto_session.rb +194 -0
  25. data/lib/hoodoo/client/endpoint/endpoints/http.rb +203 -0
  26. data/lib/hoodoo/client/endpoint/endpoints/http_based.rb +367 -0
  27. data/lib/hoodoo/client/endpoint/endpoints/not_found.rb +59 -0
  28. data/lib/hoodoo/client/headers.rb +269 -0
  29. data/lib/hoodoo/communicators.rb +23 -0
  30. data/lib/hoodoo/communicators/fast.rb +44 -0
  31. data/lib/hoodoo/communicators/pool.rb +601 -0
  32. data/lib/hoodoo/communicators/slow.rb +84 -0
  33. data/lib/hoodoo/data.rb +51 -0
  34. data/lib/hoodoo/data/resources/caller.rb +39 -0
  35. data/lib/hoodoo/data/resources/errors.rb +28 -0
  36. data/lib/hoodoo/data/resources/log.rb +31 -0
  37. data/lib/hoodoo/data/resources/session.rb +26 -0
  38. data/lib/hoodoo/data/types/error_primitive.rb +27 -0
  39. data/lib/hoodoo/data/types/permissions.rb +40 -0
  40. data/lib/hoodoo/data/types/permissions_defaults.rb +32 -0
  41. data/lib/hoodoo/data/types/permissions_full.rb +28 -0
  42. data/lib/hoodoo/data/types/permissions_resources.rb +31 -0
  43. data/lib/hoodoo/discovery.rb +20 -0
  44. data/lib/hoodoo/errors.rb +19 -0
  45. data/lib/hoodoo/errors/error_descriptions.rb +229 -0
  46. data/lib/hoodoo/errors/errors.rb +322 -0
  47. data/lib/hoodoo/generator.rb +139 -0
  48. data/lib/hoodoo/logger.rb +23 -0
  49. data/lib/hoodoo/logger/fast_writer.rb +27 -0
  50. data/lib/hoodoo/logger/flattener_mixin.rb +36 -0
  51. data/lib/hoodoo/logger/logger.rb +387 -0
  52. data/lib/hoodoo/logger/slow_writer.rb +49 -0
  53. data/lib/hoodoo/logger/writer_mixin.rb +52 -0
  54. data/lib/hoodoo/logger/writers/file_writer.rb +45 -0
  55. data/lib/hoodoo/logger/writers/log_entries_dot_com_writer.rb +64 -0
  56. data/lib/hoodoo/logger/writers/stream_writer.rb +43 -0
  57. data/lib/hoodoo/middleware.rb +33 -0
  58. data/lib/hoodoo/presenters.rb +45 -0
  59. data/lib/hoodoo/presenters/base.rb +281 -0
  60. data/lib/hoodoo/presenters/base_dsl.rb +519 -0
  61. data/lib/hoodoo/presenters/common_resource_fields.rb +31 -0
  62. data/lib/hoodoo/presenters/embedding.rb +232 -0
  63. data/lib/hoodoo/presenters/types/array.rb +118 -0
  64. data/lib/hoodoo/presenters/types/boolean.rb +26 -0
  65. data/lib/hoodoo/presenters/types/date.rb +26 -0
  66. data/lib/hoodoo/presenters/types/date_time.rb +26 -0
  67. data/lib/hoodoo/presenters/types/decimal.rb +47 -0
  68. data/lib/hoodoo/presenters/types/enum.rb +55 -0
  69. data/lib/hoodoo/presenters/types/field.rb +158 -0
  70. data/lib/hoodoo/presenters/types/float.rb +26 -0
  71. data/lib/hoodoo/presenters/types/hash.rb +361 -0
  72. data/lib/hoodoo/presenters/types/integer.rb +26 -0
  73. data/lib/hoodoo/presenters/types/object.rb +117 -0
  74. data/lib/hoodoo/presenters/types/string.rb +53 -0
  75. data/lib/hoodoo/presenters/types/tags.rb +24 -0
  76. data/lib/hoodoo/presenters/types/text.rb +26 -0
  77. data/lib/hoodoo/presenters/types/uuid.rb +54 -0
  78. data/lib/hoodoo/services.rb +34 -0
  79. data/lib/hoodoo/services/discovery/discoverers/by_consul.rb +66 -0
  80. data/lib/hoodoo/services/discovery/discoverers/by_convention.rb +173 -0
  81. data/lib/hoodoo/services/discovery/discoverers/by_drb/by_drb.rb +195 -0
  82. data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server.rb +166 -0
  83. data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server_start.rb +37 -0
  84. data/lib/hoodoo/services/discovery/discovery.rb +186 -0
  85. data/lib/hoodoo/services/discovery/results/for_amqp.rb +58 -0
  86. data/lib/hoodoo/services/discovery/results/for_http.rb +85 -0
  87. data/lib/hoodoo/services/discovery/results/for_local.rb +85 -0
  88. data/lib/hoodoo/services/discovery/results/for_remote.rb +57 -0
  89. data/lib/hoodoo/services/middleware/amqp_log_message.rb +186 -0
  90. data/lib/hoodoo/services/middleware/amqp_log_writer.rb +119 -0
  91. data/lib/hoodoo/services/middleware/endpoints/inter_resource_local.rb +130 -0
  92. data/lib/hoodoo/services/middleware/endpoints/inter_resource_remote.rb +202 -0
  93. data/lib/hoodoo/services/middleware/exception_reporting/base_reporter.rb +105 -0
  94. data/lib/hoodoo/services/middleware/exception_reporting/exception_reporting.rb +115 -0
  95. data/lib/hoodoo/services/middleware/exception_reporting/reporters/airbrake_reporter.rb +64 -0
  96. data/lib/hoodoo/services/middleware/exception_reporting/reporters/raygun_reporter.rb +63 -0
  97. data/lib/hoodoo/services/middleware/interaction.rb +127 -0
  98. data/lib/hoodoo/services/middleware/middleware.rb +2705 -0
  99. data/lib/hoodoo/services/middleware/rack_monkey_patch.rb +73 -0
  100. data/lib/hoodoo/services/services/context.rb +153 -0
  101. data/lib/hoodoo/services/services/implementation.rb +132 -0
  102. data/lib/hoodoo/services/services/interface.rb +934 -0
  103. data/lib/hoodoo/services/services/permissions.rb +250 -0
  104. data/lib/hoodoo/services/services/request.rb +189 -0
  105. data/lib/hoodoo/services/services/response.rb +316 -0
  106. data/lib/hoodoo/services/services/service.rb +141 -0
  107. data/lib/hoodoo/services/services/session.rb +729 -0
  108. data/lib/hoodoo/utilities.rb +12 -0
  109. data/lib/hoodoo/utilities/string_inquirer.rb +54 -0
  110. data/lib/hoodoo/utilities/utilities.rb +380 -0
  111. data/lib/hoodoo/utilities/uuid.rb +44 -0
  112. data/lib/hoodoo/version.rb +17 -0
  113. data/spec/active/active_record/base_spec.rb +57 -0
  114. data/spec/active/active_record/creator_spec.rb +88 -0
  115. data/spec/active/active_record/dated_spec.rb +248 -0
  116. data/spec/active/active_record/error_mapping_spec.rb +360 -0
  117. data/spec/active/active_record/finder_spec.rb +744 -0
  118. data/spec/active/active_record/search_helper_spec.rb +384 -0
  119. data/spec/active/active_record/secure_spec.rb +435 -0
  120. data/spec/active/active_record/support_spec.rb +225 -0
  121. data/spec/active/active_record/translated_spec.rb +19 -0
  122. data/spec/active/active_record/uuid_spec.rb +72 -0
  123. data/spec/active/active_record/writer_spec.rb +272 -0
  124. data/spec/alchemy/alchemy-amq.rb +33 -0
  125. data/spec/client/augmented_array_spec.rb +15 -0
  126. data/spec/client/augmented_base_spec.rb +50 -0
  127. data/spec/client/augmented_hash_spec.rb +15 -0
  128. data/spec/client/client_spec.rb +955 -0
  129. data/spec/client/endpoint/endpoint_spec.rb +70 -0
  130. data/spec/client/endpoint/endpoints/amqp_spec.rb +16 -0
  131. data/spec/client/endpoint/endpoints/auto_session_spec.rb +9 -0
  132. data/spec/client/endpoint/endpoints/http_based_spec.rb +9 -0
  133. data/spec/client/endpoint/endpoints/http_spec.rb +103 -0
  134. data/spec/client/endpoint/endpoints/not_found_spec.rb +35 -0
  135. data/spec/client/headers_spec.rb +172 -0
  136. data/spec/communicators/fast_spec.rb +9 -0
  137. data/spec/communicators/pool_spec.rb +339 -0
  138. data/spec/communicators/slow_spec.rb +15 -0
  139. data/spec/data/resources/caller_spec.rb +156 -0
  140. data/spec/data/resources/errors_spec.rb +22 -0
  141. data/spec/data/resources/log_spec.rb +20 -0
  142. data/spec/data/resources/session_spec.rb +15 -0
  143. data/spec/data/types/error_primitive_spec.rb +15 -0
  144. data/spec/data/types/permissions_defaults_spec.rb +25 -0
  145. data/spec/data/types/permissions_full_spec.rb +44 -0
  146. data/spec/data/types/permissions_resources_spec.rb +34 -0
  147. data/spec/data/types/permissions_spec.rb +37 -0
  148. data/spec/errors/error_descriptions_spec.rb +98 -0
  149. data/spec/errors/errors_spec.rb +346 -0
  150. data/spec/integration/service_actions_spec.rb +112 -0
  151. data/spec/logger/fast_writer_spec.rb +18 -0
  152. data/spec/logger/logger_spec.rb +259 -0
  153. data/spec/logger/slow_writer_spec.rb +144 -0
  154. data/spec/logger/writers/file_writer_spec.rb +37 -0
  155. data/spec/logger/writers/log_entries_dot_com_writer_spec.rb +29 -0
  156. data/spec/logger/writers/stream_writer_spec.rb +38 -0
  157. data/spec/presenters/base_dsl_spec.rb +111 -0
  158. data/spec/presenters/base_spec.rb +871 -0
  159. data/spec/presenters/common_resource_fields_spec.rb +30 -0
  160. data/spec/presenters/embedding_spec.rb +87 -0
  161. data/spec/presenters/types/array_spec.rb +249 -0
  162. data/spec/presenters/types/boolean_spec.rb +51 -0
  163. data/spec/presenters/types/date_spec.rb +57 -0
  164. data/spec/presenters/types/date_time_spec.rb +59 -0
  165. data/spec/presenters/types/decimal_spec.rb +58 -0
  166. data/spec/presenters/types/enum_spec.rb +71 -0
  167. data/spec/presenters/types/field_spec.rb +77 -0
  168. data/spec/presenters/types/float_spec.rb +50 -0
  169. data/spec/presenters/types/hash_spec.rb +1069 -0
  170. data/spec/presenters/types/integer_spec.rb +50 -0
  171. data/spec/presenters/types/object_spec.rb +177 -0
  172. data/spec/presenters/types/string_spec.rb +65 -0
  173. data/spec/presenters/types/tags_spec.rb +56 -0
  174. data/spec/presenters/types/text_spec.rb +50 -0
  175. data/spec/presenters/types/uuid_spec.rb +46 -0
  176. data/spec/presenters/walk_spec.rb +198 -0
  177. data/spec/services/discovery/discoverers/by_consul_spec.rb +29 -0
  178. data/spec/services/discovery/discoverers/by_convention_spec.rb +67 -0
  179. data/spec/services/discovery/discoverers/by_drb/by_drb_spec.rb +80 -0
  180. data/spec/services/discovery/discoverers/by_drb/drb_server_spec.rb +205 -0
  181. data/spec/services/discovery/discovery_spec.rb +73 -0
  182. data/spec/services/discovery/results/for_amqp_spec.rb +17 -0
  183. data/spec/services/discovery/results/for_http_spec.rb +37 -0
  184. data/spec/services/discovery/results/for_local_spec.rb +21 -0
  185. data/spec/services/discovery/results/for_remote_spec.rb +15 -0
  186. data/spec/services/middleware/amqp_log_message_spec.rb +60 -0
  187. data/spec/services/middleware/amqp_log_writer_spec.rb +95 -0
  188. data/spec/services/middleware/endpoints/inter_resource_local_spec.rb +9 -0
  189. data/spec/services/middleware/endpoints/inter_resource_remote_spec.rb +9 -0
  190. data/spec/services/middleware/exception_reporting/base_reporter_spec.rb +16 -0
  191. data/spec/services/middleware/exception_reporting/exception_reporting_spec.rb +92 -0
  192. data/spec/services/middleware/exception_reporting/reporters/airbrake_reporter_spec.rb +24 -0
  193. data/spec/services/middleware/exception_reporting/reporters/raygun_reporter_spec.rb +23 -0
  194. data/spec/services/middleware/middleware_cors_spec.rb +93 -0
  195. data/spec/services/middleware/middleware_create_update_spec.rb +489 -0
  196. data/spec/services/middleware/middleware_dated_at_spec.rb +186 -0
  197. data/spec/services/middleware/middleware_exotic_communication_spec.rb +560 -0
  198. data/spec/services/middleware/middleware_logging_spec.rb +356 -0
  199. data/spec/services/middleware/middleware_multi_local_spec.rb +1094 -0
  200. data/spec/services/middleware/middleware_multi_remote_spec.rb +1440 -0
  201. data/spec/services/middleware/middleware_permissions_spec.rb +1014 -0
  202. data/spec/services/middleware/middleware_public_spec.rb +238 -0
  203. data/spec/services/middleware/middleware_spec.rb +1569 -0
  204. data/spec/services/middleware/string_inquirer_spec.rb +30 -0
  205. data/spec/services/services/application_spec.rb +74 -0
  206. data/spec/services/services/context_spec.rb +48 -0
  207. data/spec/services/services/implementation_spec.rb +45 -0
  208. data/spec/services/services/interface_spec.rb +262 -0
  209. data/spec/services/services/permissions_spec.rb +249 -0
  210. data/spec/services/services/request_spec.rb +95 -0
  211. data/spec/services/services/response_spec.rb +250 -0
  212. data/spec/services/services/session_spec.rb +432 -0
  213. data/spec/spec_helper.rb +298 -0
  214. data/spec/utilities/utilities_spec.rb +537 -0
  215. data/spec/utilities/uuid_spec.rb +20 -0
  216. metadata +615 -0
@@ -0,0 +1,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