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,351 @@
1
+ ########################################################################
2
+ # File:: error_mapping.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Support mixin for models subclassed from ActiveRecord::Base
6
+ # providing a mapping between API level errors and model
7
+ # validation errors.
8
+ # ----------------------------------------------------------------------
9
+ # 17-Nov-2014 (ADH): Created.
10
+ ########################################################################
11
+
12
+ module Hoodoo
13
+
14
+ # Support mixins for models subclassed from ActiveRecord::Base. See:
15
+ #
16
+ # * http://guides.rubyonrails.org/active_record_basics.html
17
+ #
18
+ module ActiveRecord
19
+
20
+ # Support mixin for models subclassed from ActiveRecord::Base providing
21
+ # a mapping between ActiveRecord validation errors and platform errors
22
+ # via Hoodoo::ErrorDescriptions and Hoodoo::Errors. See individual
23
+ # module methods for examples, along with:
24
+ #
25
+ # * http://guides.rubyonrails.org/active_record_basics.html
26
+ #
27
+ # The error handling mechanism this mixin provides is intentionally
28
+ # analogous to that used for resource-to-resource calls through
29
+ # Hoodoo::Client::AugmentedBase.
30
+ #
31
+ module ErrorMapping
32
+
33
+ # Validates the model instance and adds mapped-to-platform errors to
34
+ # a given Hoodoo::Errors instance, if any validation errors occur.
35
+ # For ActiveRecord validation documentation, see:
36
+ #
37
+ # * http://guides.rubyonrails.org/active_record_validations.html
38
+ #
39
+ # Returns +true+ if any errors were added (model instance is invalid)
40
+ # else +false+ if everything is OK (model instance is valid).
41
+ #
42
+ # == Mapping ActiveRecord errors to API errors
43
+ #
44
+ # The method makes an idiomatic example for "check errors in the model,
45
+ # map them to platform errors in my service's response and return the
46
+ # result" very simple, at the expense of modifying the passed-in
47
+ # error collection contents (mutating a parameter is a risky pattern).
48
+ #
49
+ # Given this example model:
50
+ #
51
+ # class SomeModel < ActiveRecord::Base
52
+ # include Hoodoo::ActiveRecord::ErrorMapping
53
+ # # ...
54
+ # end
55
+ #
56
+ # ...then a service's #create method could do something like:
57
+ #
58
+ # def create( context )
59
+ #
60
+ # # Validate inbound creation data by e.g. schema through the
61
+ # # presenter layer - Hoodoo::Presenters::Base and
62
+ # # Hoodoo::Presenters::Base - then...
63
+ #
64
+ # model = SomeModel.new
65
+ # model.param_1 = 'something based on inbound creation data'
66
+ #
67
+ # # Ideally use the Writer mixin for concurrency-safe saving,
68
+ # # but in this simple example we'll just use #save directly;
69
+ # # unhandled database exceptions might be thrown:
70
+ #
71
+ # model.save()
72
+ #
73
+ # # Now exit, adding mapped errors to the response, if there
74
+ # # were validation failures when attempting to save.
75
+ #
76
+ # return if model.adds_errors_to?( context.response.errors )
77
+ #
78
+ # # ...else set 'context.response' data appropriately.
79
+ #
80
+ # end
81
+ #
82
+ # An alternative pattern which avoids mutating the input parameter
83
+ # uses the potentially less efficient, but conceptually cleaner method
84
+ # #platform_errors. Using #adds_errors_to? as per the above code is
85
+ # faster, but the above example's use of +save+, as per its comments,
86
+ # does not fully handle some concurrency edge cases.
87
+ #
88
+ # To win on both fronts use Hoodoo::ActiveRecord::Writer:
89
+ #
90
+ # def create( context )
91
+ #
92
+ # model = SomeModel.new
93
+ # model.param_1 = 'something based on inbound creation data'
94
+ #
95
+ # unless model.persist_in( context ) === :success
96
+ # context.response.add_errors( model.platform_errors )
97
+ # return
98
+ # end
99
+ #
100
+ # # ...else set 'context.response' data appropriately.
101
+ #
102
+ # end
103
+ #
104
+ # In this case, the less efficient #platform_errors call only happens
105
+ # when we know we are in an error recovery situation anyway, in which
106
+ # case it isn't as important to operate in as efficient a manner as
107
+ # possible - provided one assumes that the non-error path is the much
108
+ # more common case!
109
+ #
110
+ # == Associations
111
+ #
112
+ # When a model has associations and nested attributes are accepted for
113
+ # those associations, a validity query on an instance constructed with
114
+ # nested attributes will cause ActiveRecord to traverse all such
115
+ # attributes and aggregate specific errors on the parent object. This
116
+ # is specifically different from +validates_associated+, wherein
117
+ # associations constructed and attached through any means are validated
118
+ # independently, with validation errors independently added to those
119
+ # objects and the parent only gaining a generic "foo is invalid" error.
120
+ #
121
+ # In such cases, the error mapper will attempt to path-traverse the
122
+ # error's column references to determine the association's column type
123
+ # and produce a fully mapped error with a reference to the full path.
124
+ # Service authors are encouraged to use this approach if associations
125
+ # are involved, as it yields the most comprehensive mapped error
126
+ # collection.
127
+ #
128
+ # In the example below, note how the Child model does not need to
129
+ # include Hoodoo error mapping (though it can do so harmlessly if it so
130
+ # wishes) because it is the Parent model that drives the mapping of all
131
+ # the validations aggregated by ActiveRecord into an instance of Parent
132
+ # due to +accepts_nested_attributes_for+.
133
+ #
134
+ # So, given this:
135
+ #
136
+ # def Parent < ActiveRecord::Base
137
+ # include Hoodoo::ActiveRecord::ErrorMapping
138
+ #
139
+ # has_many :children
140
+ # accepts_nested_attributes_for :children
141
+ # end
142
+ #
143
+ # def Child < ActiveRecord::Base
144
+ # belongs_to :parent
145
+ #
146
+ # # ...then add ActiveRecord validations - e.g.:
147
+ #
148
+ # validates :some_child_field, :length => { :maximum => 5 }
149
+ # end
150
+ #
151
+ # ...then if a Parent were to be constructed thus:
152
+ #
153
+ # parent = Parent.new( {
154
+ # "parent_field_1" = "foo",
155
+ # "parent_field_2" = "bar",
156
+ # "children_attributes" = [
157
+ # { "some_child_field" = "child_1_foo" },
158
+ # { "some_child_field" = "child_2_foo" },
159
+ # # ...
160
+ # ],
161
+ # # ...
162
+ # } )
163
+ #
164
+ # ...then <tt>parent.adds_errors_to?( some_collection )</tt> could lead
165
+ # to +some_collection+ containing errors such as:
166
+ #
167
+ # {
168
+ # "code" => "generic.invalid_string",
169
+ # "message => "is too long (maximum is 5 characters)",
170
+ # "reference" => "children.some_child_field"
171
+ # }
172
+ #
173
+ # +collection+:: A Hoodoo::Errors instance, typically obtained
174
+ # from the Hoodoo::Services::Context instance passed to
175
+ # a service implementation in calls like
176
+ # Hoodoo::Services::Implementation#list or
177
+ # Hoodoo::Services::Implementation#show, via
178
+ # +context.response.errors+
179
+ # (i.e. Hoodoo::Services::Context#response /
180
+ # Hoodoo::Services::Response#errors). The collection you
181
+ # pass is updated if there are any errors recorded in
182
+ # the model, by adding equivalent structured errors to
183
+ # the collection.
184
+ #
185
+ def adds_errors_to?( collection )
186
+
187
+ self.validate()
188
+
189
+ self.errors.messages.each_pair do | attribute_name, message_array |
190
+ attribute_name = attribute_name.to_s
191
+
192
+ attribute_type = nz_co_loyalty_determine_deep_attribute_type( attribute_name )
193
+ attribute_name = 'model instance' if attribute_name == 'base'
194
+
195
+ message_array.each do | message |
196
+ error_code = case message
197
+ when 'has already been taken'
198
+ 'generic.invalid_duplication'
199
+ else
200
+ attribute_type == 'text' ? 'generic.invalid_string' : "generic.invalid_#{ attribute_type }"
201
+ end
202
+
203
+ unless collection.descriptions.recognised?( error_code )
204
+ error_code = 'generic.invalid_parameters'
205
+ end
206
+
207
+ collection.add_error(
208
+ error_code,
209
+ :message => message,
210
+ :reference => { :field_name => attribute_name }
211
+ )
212
+ end
213
+ end
214
+
215
+ return self.errors.any?
216
+ end
217
+
218
+ # Validate the model instance and return a Hoodoo::Errors instance
219
+ # which contains no platform errors if there are no model validation
220
+ # errors, else mapped-to-platform errors if validation errors are
221
+ # encountered. For ActiveRecord validation documentation, see:
222
+ #
223
+ # * http://guides.rubyonrails.org/active_record_validations.html
224
+ #
225
+ # This mixin method provides support for an alternative coding style to
226
+ # method #adds_errors_to?, by generating an Errors collection internally
227
+ # rather than modifying one passed by the caller. It is less efficient
228
+ # than calling #adds_errors_to? if you have an existing errors collection
229
+ # already constructed, but otherwise follows a cleaner design pattern.
230
+ #
231
+ # See #adds_errors_to? examples first, then compare the idiom shown
232
+ # there:
233
+ #
234
+ # return if model.adds_errors_to?( context.response.errors )
235
+ #
236
+ # ...with the idiomatic use of this method:
237
+ #
238
+ # context.response.add_errors( model.platform_errors )
239
+ # return if context.response.halt_processing?
240
+ #
241
+ # It is a little more verbose and in this example will run a little
242
+ # slower due to the construction of the internal Hoodoo::Errors
243
+ # instance followed by the addition to the +context.response+
244
+ # collection, but you may prefer the conceptually cleaner approach.
245
+ # You can lean on the return value of #add_errors and end up back at
246
+ # one line of (very slightly less obvious) code, too:
247
+ #
248
+ # return if context.response.add_errors( model.platform_errors )
249
+ #
250
+ def platform_errors
251
+ collection = Hoodoo::Errors.new
252
+ self.adds_errors_to?( collection )
253
+
254
+ return collection
255
+ end
256
+
257
+ private
258
+
259
+ # Given an attribute for this model as a string, return the column type
260
+ # associated with it.
261
+ #
262
+ # The attribute name intended for use here comes from validation and,
263
+ # when there are unsaved associations in an ActiveRecord graph that is
264
+ # being saved, ActiveRecord aggregates child object errors into the
265
+ # target parent being saved with the attribute names using a dot
266
+ # notation to indicate the path of methods to get from one instance to
267
+ # the next. This is resolved. For example:
268
+ #
269
+ # * <tt>address</tt> would look up the type of a column called
270
+ # "address" in 'this' model.
271
+ #
272
+ # * <tt>addresses.home</tt> would look up the type of a column called
273
+ # "home" in whatever is accessed by "model.addresses". If this gives
274
+ # an array, the first entry in the array is taken for column type
275
+ # retrieval.
276
+ #
277
+ # This path chasing will be done to an arbitrary depth. If at any point
278
+ # there is a failure to follow the path, the path follower exits and
279
+ # the top-level error is used instead, with a generic unknown column
280
+ # type returned.
281
+ #
282
+ # Parameters:
283
+ #
284
+ # +attribute_path+:: _String_ attribute path. Not a Symbol or Array!
285
+ #
286
+ # Return values are any ActiveRecord column type or these special
287
+ # values:
288
+ #
289
+ # * +unknown+ for any unrecognised attribute name or an attribute name
290
+ # that is a path (it has one or more "."s in it) but where the path
291
+ # cannot be followed.
292
+ #
293
+ # * +array+ for columns that appear to respond to the +array+ method.
294
+ #
295
+ # * +uuid+ for columns of any known but non-array type, where there is
296
+ # a UuidValidator present.
297
+ #
298
+ def nz_co_loyalty_determine_deep_attribute_type( attribute_path )
299
+
300
+ attribute_name = attribute_path
301
+ target_instance = self
302
+
303
+ # Descend a path of "foo.bar.baz" dereferencing associations from the
304
+ # field names in the dot-separated path until we're at the lowest leaf
305
+ # object with "baz" as its errant field.
306
+
307
+ if attribute_path.include?( '.' )
308
+
309
+ leaf_instance = target_instance
310
+ leaf_field = attribute_path
311
+
312
+ fields = attribute_path.split( '.' )
313
+ leaf_field = fields.pop() # (remove final entry - the leaf object's errant field)
314
+ reached_field = nil
315
+
316
+ fields.each do | field |
317
+ object_at_field = leaf_instance.send( field ) if leaf_instance.respond_to?( field )
318
+ object_at_field = object_at_field.first if object_at_field.respond_to?( :first )
319
+
320
+ break if object_at_field.nil?
321
+
322
+ leaf_instance = object_at_field
323
+ reached_field = field
324
+ end
325
+
326
+ if reached_field === fields.last
327
+ attribute_name = leaf_field
328
+ target_instance = leaf_instance
329
+ end
330
+ end
331
+
332
+ column = target_instance.class.columns_hash[ attribute_name ]
333
+
334
+ attribute_type = if column.nil?
335
+ 'unknown'
336
+ elsif column.respond_to?( :array ) && column.array
337
+ 'array'
338
+ elsif target_instance.class.validators_on( attribute_name ).any? { | v |
339
+ v.instance_of?( UuidValidator )
340
+ } # Considered a UUID since it uses the UUID validator
341
+ 'uuid'
342
+ else
343
+ column.type.to_s()
344
+ end
345
+
346
+ return attribute_type
347
+ end
348
+
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,606 @@
1
+ ########################################################################
2
+ # File:: finder.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Support mixin for models subclassed from ActiveRecord::Base
6
+ # providing enhanced find mechanisms for data retrieval,
7
+ # especially +show+ and +list+ action handling.
8
+ # ----------------------------------------------------------------------
9
+ # 25-Nov-2014 (ADH): Created.
10
+ ########################################################################
11
+
12
+ require 'hoodoo/active/active_record/search_helper'
13
+
14
+ module Hoodoo
15
+ module ActiveRecord
16
+
17
+ # Mixin for models subclassed from ActiveRecord::Base providing support
18
+ # methods to handle common +show+ and +list+ filtering actions based on
19
+ # inbound data and create instances in a request context aware fashion.
20
+ #
21
+ # It is _STRONGLY_ _RECOMMENDED_ that you use the likes of:
22
+ #
23
+ # * Hoodoo::ActiveRecord::Finder::ClassMethods::acquire_in
24
+ # * Hoodoo::ActiveRecord::Finder::ClassMethods::list_in
25
+ #
26
+ # ...to retrieve model data related to resource instances and participate
27
+ # "for free" in whatever plug-in ActiveRecord modules are mixed into the
28
+ # model classes, such as Hoodoo::ActiveRecord::Secure.
29
+ #
30
+ # See also:
31
+ #
32
+ # * http://guides.rubyonrails.org/active_record_basics.html
33
+ #
34
+ # Dependency Hoodoo::ActiveRecord::Secure is included automatically.
35
+ #
36
+ module Finder
37
+
38
+ # Instantiates this module when it is included:
39
+ #
40
+ # Example:
41
+ #
42
+ # class SomeModel < ActiveRecord::Base
43
+ # include Hoodoo::ActiveRecord::Finder
44
+ # # ...
45
+ # end
46
+ #
47
+ # +model+:: The ActiveRecord::Base descendant that is including
48
+ # this module.
49
+ #
50
+ def self.included( model )
51
+ model.class_attribute(
52
+ :nz_co_loyalty_hoodoo_show_id_fields,
53
+ :nz_co_loyalty_hoodoo_search_with,
54
+ :nz_co_loyalty_hoodoo_filter_with,
55
+ {
56
+ :instance_predicate => false,
57
+ :instance_accessor => false
58
+ }
59
+ )
60
+
61
+ unless model == Hoodoo::ActiveRecord::Base
62
+ model.send( :include, Hoodoo::ActiveRecord::Secure )
63
+ instantiate( model )
64
+ end
65
+
66
+ super( model )
67
+ end
68
+
69
+ # When instantiated in an ActiveRecord::Base subclass, all of the
70
+ # Hoodoo::ActiveRecord::Finder::ClassMethods methods are defined as
71
+ # class methods on the including class.
72
+ #
73
+ # This module depends upon Hoodoo::ActiveRecord::Secure, so that
74
+ # will be auto-included first if it isn't already.
75
+ #
76
+ # +model+:: The ActiveRecord::Base descendant that is including
77
+ # this module.
78
+ #
79
+ def self.instantiate( model )
80
+ model.extend( ClassMethods )
81
+ end
82
+
83
+ # Collection of class methods that get defined on an including class via
84
+ # Hoodoo::ActiveRecord::Finder::included.
85
+ #
86
+ module ClassMethods
87
+
88
+ # "Polymorphic" find - support for finding a model by fields other
89
+ # than just +:id+, based on a single unique identifier. Use #acquire
90
+ # just like you'd use +find_by_id+ and only bother with it if you
91
+ # support finding a resource instance by +id+ _and_ one or more
92
+ # other model fields. Otherwise, just use +find_by_id+.
93
+ #
94
+ # In the model, you declare the list of fields _in_ _addition_ _to_
95
+ # +id+ by calling #acquire_with thus:
96
+ #
97
+ # class SomeModel < ActiveRecord::Base
98
+ # include Hoodoo::ActiveRecord::Finder
99
+ # acquire_with ... # <list-of-other-fields>
100
+ # end
101
+ #
102
+ # For example, maybe you allow some resource to be looked up by fields
103
+ # +id+ or +code+, both of which are independently unique sets. Since
104
+ # +id+ is always automatically included, you only need to do this:
105
+ #
106
+ # class SomeModel < ActiveRecord::Base
107
+ # include Hoodoo::ActiveRecord::Finder
108
+ # acquire_with :code
109
+ # end
110
+ #
111
+ # Then, in a resource's implementation:
112
+ #
113
+ # def show( context )
114
+ # found = SomeModel.acquire( context.request.ident )
115
+ # return context.response.not_found( context.request.ident ) if found.nil?
116
+ #
117
+ # # ...map 'found' to whatever resource you're representing,
118
+ # # e.g. via a Hoodoo::Presenters::Base subclass with resource
119
+ # # schema and the subclass's Hoodoo::Presenters::Base::render
120
+ # # call, then...
121
+ #
122
+ # context.response.set_resource( resource_representation_of_found )
123
+ # end
124
+ #
125
+ # There is nothing magic "under the hood" - Hoodoo just tries to
126
+ # find records with a value matching the incoming identifier for
127
+ # each of the fields in turn. It starts with +id+ then runs through
128
+ # any other fields in the order given through #acquire_with.
129
+ #
130
+ # This can only be used <i>if your searched fields are strings</i> in
131
+ # the database. This includes, for example, the +id+ column; Hoodoo
132
+ # usually expects to be a string field holding a 32-character UUID. If
133
+ # any of the fields contain non-string types, attempts to use the
134
+ # #acquire mechanism (or a related one) may result in database errors
135
+ # due to type mismatches, depending upon the database engine in use.
136
+ #
137
+ # In more complex scenarious, you can just call #acquire at the end
138
+ # of any chain of AREL queries just as you would call ActiveRecord's
139
+ # own #find_by_id method, e.g.:
140
+ #
141
+ # SomeModel.where( :foo => :bar ).acquire( context.request.ident )
142
+ #
143
+ # Usually for convenience you should use #acquire_in instead, or only
144
+ # call #acquire with (say) a secure scope via for example a call to
145
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure. Other scopes may
146
+ # be needed depending on the mixins your model uses.
147
+ #
148
+ # +ident+:: The value to search for in the fields (attributes)
149
+ # specified via #acquire_with, matched using calls to
150
+ # <tt>where( attr => ident )</tt>.
151
+ #
152
+ # Returns a found model instance or +nil+ for no match.
153
+ #
154
+ def acquire( ident )
155
+ extra_fields = self.nz_co_loyalty_hoodoo_show_id_fields || []
156
+
157
+ id_fields = [ :id ] + extra_fields
158
+ id_fields.each do | field |
159
+
160
+ # This is fiddly.
161
+ #
162
+ # You must use a string with field substitution approach, rather
163
+ # than e.g. ".where( :field => :ident )". AREL/ActiveRecord will,
164
+ # in the latter case, compose rational SQL based on column data
165
+ # types. If you have an *integer* ID field, then, it'll try to
166
+ # convert a *string* ident to an integer. This can give Hilarious
167
+ # Consequences. Consider looking up on (integer) field "id" or
168
+ # (text) field "uuid", with a string ident of "1f294942..." - the
169
+ # text UUID would be fine, but the integer ID may end up with the
170
+ # UUID being "to_i"'d, yielding integer 1. If the ID field is
171
+ # looked at first, you're highly likely to find the wrong record.
172
+ #
173
+ # The solution is, as written, simple; just use the substitution
174
+ # approach rather than higher level AREL, causing a string-like SQL
175
+ # query on all adapters which SQL handles just fine for varying
176
+ # field data types.
177
+ #
178
+ # The caveat is that should the database object to mismatched type
179
+ # comparisons it will actually raise an error - we see this on
180
+ # for example PostgreSQL where a column is an integer but we try
181
+ # to match it against a string that cannot be cleanly converted to
182
+ # one (e.g. trying to find an integer column 'id' based on a text
183
+ # UUID value containing alphabetic characters). That is still
184
+ # preferable to looking up the wrong record!
185
+
186
+ checker = where( [ "\"#{ self.table_name }\".\"#{ field }\" = ?", ident ] )
187
+ return checker.first unless checker.count == 0
188
+ end
189
+
190
+ return nil
191
+ end
192
+
193
+ # Implicily secure, translated, dated etc. etc. version of #acquire,
194
+ # according to which modules are mixed into your model class. See
195
+ # Hoodoo::ActiveRecord::Support#full_scope_for to see the list of
196
+ # things that get included in the scope according to the mixins
197
+ # that are in use.
198
+ #
199
+ # For example, if you are using or at some point intend to mix in and
200
+ # use the mechanism described by the likes of
201
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure, call here as a
202
+ # convenience to both obtain a secure context and find a record
203
+ # (with or without additional find-by fields other than +id+) in one
204
+ # go. Building on the example from
205
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure, we might have an
206
+ # Audit model as follows:
207
+ #
208
+ # class Audit < ActiveRecord::Base
209
+ # include Hoodoo::ActiveRecord::Secure
210
+ #
211
+ # secure_with( {
212
+ # :creating_caller_uuid => :authorised_caller_uuid
213
+ # } )
214
+ #
215
+ # # Plus perhaps a call to "acquire_with"
216
+ # end
217
+ #
218
+ # Then, in a resource's implementation:
219
+ #
220
+ # def show( context )
221
+ # found = SomeModel.acquire_in( context )
222
+ # return context.response.not_found( context.request.ident ) if found.nil?
223
+ #
224
+ # # ...map 'found' to whatever resource you're representing,
225
+ # # e.g. via a Hoodoo::Presenters::Base subclass with resource
226
+ # # schema and the subclass's Hoodoo::Presenters::Base::render
227
+ # # call, then...
228
+ #
229
+ # context.response.set_resource( resource_representation_of_found )
230
+ # end
231
+ #
232
+ # The value of +found+ will be acquired within the secure context
233
+ # determined by the prevailing call context (and its session), so
234
+ # the data it finds is inherently correctly scoped - provided your
235
+ # model's Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with
236
+ # call describes things correctly.
237
+ #
238
+ # This method is for convenience and safety - you can't accidentally
239
+ # forget the secure scope:
240
+ #
241
+ # SomeModel.secure( context ).acquire( context.request.ident )
242
+ #
243
+ # # ...has the same result as...
244
+ #
245
+ # SomeModel.acquire_in( context )
246
+ #
247
+ # The same applies to forgetting dated scopes, translated scopes, or
248
+ # anything else that Hoodoo::ActiveRecord::Support#full_scope_for
249
+ # might include for you.
250
+ #
251
+ # Parameters:
252
+ #
253
+ # +context+:: Hoodoo::Services::Context instance describing a call
254
+ # context. This is typically a value passed to one of
255
+ # the Hoodoo::Services::Implementation instance methods
256
+ # that a resource subclass implements.
257
+ #
258
+ # Returns a found model instance or +nil+ for no match.
259
+ #
260
+ def acquire_in( context )
261
+ scope = Hoodoo::ActiveRecord::Support.full_scope_for( self, context )
262
+ return scope.acquire( context.request.ident )
263
+ end
264
+
265
+ # Describe the list of model fields _in_ _addition_ _to_ +id+ which
266
+ # are to be used to "find-by-identifier" through calls #acquire and
267
+ # #acquire_in. See those for more details.
268
+ #
269
+ # *args:: One or more field names as Strings or Symbols.
270
+ #
271
+ def acquire_with( *args )
272
+ self.nz_co_loyalty_hoodoo_show_id_fields = args
273
+ end
274
+
275
+ # Generate an ActiveRecord::Relation instance which can be used to
276
+ # count, retrieve or further refine a list of model instances from
277
+ # the database.
278
+ #
279
+ # Usually for convenience you should use #list_in instead, or only
280
+ # call #acquire with (say) a secure scope via for example a call to
281
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure. An example of
282
+ # this second option is shown below.
283
+ #
284
+ # Pass a Hoodoo::Services::Request::ListParameters instance, e.g.
285
+ # via the Hoodoo::Services::Context instance passed to resource
286
+ # endpoint implementations and accessor +context.request.list+. It
287
+ # takes into account the list offset, limit, sort key and sort
288
+ # direction automatically. In addition, it can do simple search and
289
+ # filter operations if search and filter mappings are set up via
290
+ # #search_with and #filter_with.
291
+ #
292
+ # For exampe, in a simple case where a model can be listed without
293
+ # any unusual constraints, we might do this:
294
+ #
295
+ # class SomeModel < ActiveRecord::Base
296
+ # include Hoodoo::ActiveRecord::Finder
297
+ #
298
+ # search_with # ...<field-to-search-info mapping>
299
+ # # ...and/or...
300
+ # filter_with # ...<field-to-search-info mapping>
301
+ # end
302
+ #
303
+ # # ...then, in the resource implementation...
304
+ #
305
+ # def list( context )
306
+ # finder = SomeModel.list( context.request.list )
307
+ # results = finder.all.map do | item |
308
+ # # ...map database objects to response objects...
309
+ # end
310
+ # context.response.set_resources( results, finder.dataset_size )
311
+ # end
312
+ #
313
+ # Note the use of helper method #dataset_size to count the total
314
+ # amount of results in the dataset without pagination.
315
+ #
316
+ # The service middleware enforces sane values for things like list
317
+ # offsets, sort keys and so-on according to service interface
318
+ # definitions, so if using the middleware you don't need to do any
319
+ # extra checking yourself.
320
+ #
321
+ # Since the returned object is just a relation, adding further
322
+ # constraints is easy - call things like +where+, +group+ and so-on
323
+ # as normal. You can also list in a secure context via the included
324
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure, assuming
325
+ # appropriate data is set in the model via
326
+ # Hoodoo::ActiveRecord::Secure::ClassMethods#secure_with:
327
+ #
328
+ # def list( context )
329
+ # finder = SomeModel.secure( context ).list( context.request.list )
330
+ # finder = finder.where( :additional_filter => 'some value' )
331
+ # results = finder.all.map do | item |
332
+ # # ...map database objects to response objects...
333
+ # end
334
+ # context.response.set_resources( results, finder.dataset_size )
335
+ # end
336
+ #
337
+ # Since it's just a chained scope, you can call in any order:
338
+ #
339
+ # SomeModel.secure( context ).list( context.request.list )
340
+ #
341
+ # # ...has the same result as...
342
+ #
343
+ # SomeModel.list( context.request.list ).secure( context )
344
+ #
345
+ # Any of the ActiveRecord::QueryMethods can be called on the returned
346
+ # value. See:
347
+ #
348
+ # http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html
349
+ #
350
+ # +list_parameters+:: Hoodoo::Services::Request::ListParameters
351
+ # instance, typically obtained from the
352
+ # Hoodoo::Services::Context instance passed to
353
+ # a service implementation in
354
+ # Hoodoo::Services::Implementation#list, via
355
+ # +context.request.list+ (i.e.
356
+ # Hoodoo::Services::Context#request
357
+ # / Hoodoo::Services::Request#list).
358
+ #
359
+ def list( list_parameters )
360
+ finder = all.offset( list_parameters.offset ).limit( list_parameters.limit )
361
+ finder = finder.order( list_parameters.sort_data )
362
+
363
+ # DRY up the 'each' loops below. Use a Proc not a method because any
364
+ # methods we define will end up being defined on the including Model,
365
+ # increasing the chance of a name collision.
366
+ #
367
+ dry_proc = Proc.new do | data, attr, proc |
368
+ value = data[ attr ]
369
+ proc.call( attr, value ) unless value.nil?
370
+ end
371
+
372
+ search_map = self.nz_co_loyalty_hoodoo_search_with
373
+
374
+ unless search_map.nil?
375
+ search_map.each do | attr, proc |
376
+ args = dry_proc.call( list_parameters.search_data, attr, proc )
377
+ finder = finder.where( *args ) unless args.nil?
378
+ end
379
+ end
380
+
381
+ filter_map = self.nz_co_loyalty_hoodoo_filter_with
382
+
383
+ unless filter_map.nil?
384
+ filter_map.each do | attr, proc |
385
+ args = dry_proc.call( list_parameters.filter_data, attr, proc )
386
+ finder = finder.where.not( *args ) unless args.nil?
387
+ end
388
+ end
389
+
390
+ return finder
391
+ end
392
+
393
+ # Implicily secure, translated, dated etc. etc. version of #list,
394
+ # according to which modules are mixed into your model class. See
395
+ # Hoodoo::ActiveRecord::Support#full_scope_for to see the list of
396
+ # things that get included in the scope according to the mixins
397
+ # that are in use.
398
+ #
399
+ # For example, if you have included Hoodoo::ActiveRecord::Secure,
400
+ # this method provides you with an implicitly secure query. Read the
401
+ # documentation on #acquire_in versus #acquire for information
402
+ # on the use of secure scopes; as with #acquire_in and the "Secure"
403
+ # mixin, this method becomes for convenience and safety - you
404
+ # can't accidentally forget the secure scope:
405
+ #
406
+ # SomeModel.secure( context ).list( context.request.list )
407
+ #
408
+ # # ...has the same result as...
409
+ #
410
+ # SomeModel.list_in( context )
411
+ #
412
+ # The same applies to forgetting dated scopes, translated scopes, or
413
+ # anything else that Hoodoo::ActiveRecord::Support#full_scope_for
414
+ # might include for you.
415
+ #
416
+ # +context+:: Hoodoo::Services::Context instance describing a call
417
+ # context. This is typically a value passed to one of
418
+ # the Hoodoo::Services::Implementation instance methods
419
+ # that a resource subclass implements.
420
+ #
421
+ # Returns a secure list scope, for either further modification with
422
+ # query methods like +where+ or fetching from the database with +all+.
423
+ #
424
+ def list_in( context )
425
+ scope = Hoodoo::ActiveRecord::Support.full_scope_for( self, context )
426
+ return scope.list( context.request.list )
427
+ end
428
+
429
+ # Given some scope - typically that obtained from a prior call to
430
+ # #list or #list_in, with possibly other query modifiers too - return
431
+ # the total dataset size. This is basically a +COUNT+ operation, but
432
+ # run without offset or limit considerations (ignoring pagination).
433
+ #
434
+ # This is particularly useful if you are calling
435
+ # Hoodoo::Services::Response#set_resources and want to fill in its
436
+ # +dataset_size+ parameter.
437
+ #
438
+ def dataset_size
439
+ return all.limit( nil ).offset( nil ).count
440
+ end
441
+
442
+ # Specify a search mapping for use by #list to automatically restrict
443
+ # list results.
444
+ #
445
+ # In the simplest case, search query string entries and model field
446
+ # (attribute) names are assumed to be the same; if you wanted to
447
+ # search for values of model attributes +name+ and +colour+ using
448
+ # query string entries of +name+ and +colour+ you would just do this:
449
+ #
450
+ # class SomeModel < ActiveRecord::Base
451
+ # search_with(
452
+ # :name => nil,
453
+ # :colour => nil
454
+ # )
455
+ # end
456
+ #
457
+ # The +nil+ values mean a default, case sensitive match is performed
458
+ # with the query string keys and values mapping directly to model
459
+ # query attribute names and values.
460
+ #
461
+ # More complex example where +colour+ is matched verbatim, but +name+
462
+ # is matched case-insensitive, assuming PostgreSQL's ILIKE is there:
463
+ #
464
+ # class SomeModel < ActiveRecord::Base
465
+ # search_with(
466
+ # :name => Proc.new { | attr, value |
467
+ # [ 'name ILIKE ?', value ]
468
+ # },
469
+ # :colour => nil
470
+ # )
471
+ # end
472
+ #
473
+ # Extending the above to use a single Proc that handles case
474
+ # insensitive matches across all attributes:
475
+ #
476
+ # class SomeModel < ActiveRecord::Base
477
+ # CI_MATCH = Proc.new { | attr, value |
478
+ # [ "#{ attr } ILIKE ?", value ]
479
+ # }
480
+ #
481
+ # search_with(
482
+ # :name => CI_MATCH,
483
+ # :colour => CI_MATCH
484
+ # )
485
+ # end
486
+ #
487
+ # If you wanted to match against an array of possible matches, something
488
+ # like this would work:
489
+ #
490
+ # ARRAY_MATCH = Proc.new { | attr, value |
491
+ # [ { attr => [ value ].flatten } ]
492
+ # }
493
+ #
494
+ # Note the returned *array* (see input parameter details) inside which
495
+ # the usual hash syntax for AREL +.where+-style queries is present.
496
+ #
497
+ # To help out with common cases other than just specifying +nil+, the
498
+ # Hoodoo::ActiveRecord::Finder::SearchHelper class provides a method
499
+ # chaining approach which builds up the Hash used by #search_with and
500
+ # filter_with. See that class's API documentation for details.
501
+ #
502
+ # *args:: A Hash. Keys are both search field names and model attribute
503
+ # names, unless overridden by values; values of +nil+ are used
504
+ # for simple cases - "where( { attr_name => value } )" will be
505
+ # the resulting query modification. Alternatively, pass a
506
+ # callable Proc/Lambda. This is pased the attribute under
507
+ # consideration (and so you can ignore that and query against
508
+ # one or more different-named model attributes) and the
509
+ # context-caller-supplied value to search for. Return *AN*
510
+ # *ARRAY* of parameters to pass to +where+. For parameters to
511
+ # +where+, see:
512
+ #
513
+ # http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-where
514
+ #
515
+ # The Hash keys giving the search attribute names can be
516
+ # specified as Strings or Symbols.
517
+ #
518
+ # See Hoodoo::ActiveRecord::Finder::SearchHelper for methods
519
+ # which assist with filling in non-nil values for this Hash.
520
+ #
521
+ def search_with( hash )
522
+ self.nz_co_loyalty_hoodoo_search_with = Hoodoo::ActiveRecord::Support.process_to_map( hash )
523
+ end
524
+
525
+ # As #search_with, but used in +where.not+ queries.
526
+ #
527
+ # <b><i>IMPORTANT:</i></b> Beware +null+ column values and filters
528
+ # given SQL's strange behaviour with such things. The search helpers
529
+ # in Hoodoo::ActiveRecord::Finder::SearchHelper class will work as
530
+ # logically expected ("field not 'foo'" will find fields with a null
531
+ # value), though if you're expecting SQL-like behaviour it might come
532
+ # as a surprise! Using <tt>...AND field IS NOT NULL</tt> in queries
533
+ # for +filter_with+ tends to work reasonably when the query is
534
+ # negated for filter use via <tt>...NOT(...)...</tt>. Examining the
535
+ # implementation of Hoodoo::ActiveRecord::Finder::SearchHelper may
536
+ # help if confused. See also:
537
+ #
538
+ # * https://en.wikipedia.org/wiki/Null_(SQL)
539
+ #
540
+ # +map+:: As #search_with.
541
+ #
542
+ def filter_with( hash )
543
+ self.nz_co_loyalty_hoodoo_filter_with = Hoodoo::ActiveRecord::Support.process_to_map( hash )
544
+ end
545
+
546
+ # Deprecated interface replaced by #acquire. Instead of:
547
+ #
548
+ # Model.polymorphic_find( foo, ident )
549
+ #
550
+ # ...use:
551
+ #
552
+ # foo.acquire( ident )
553
+ #
554
+ # This implementation is for legacy support and just calls through
555
+ # to #acquire.
556
+ #
557
+ # +finder+:: #acquire is called on this.
558
+ #
559
+ # +ident+:: Passed to #acquire.
560
+ #
561
+ # Returns a found model instance or +nil+ for no match.
562
+ #
563
+ def polymorphic_find( finder, ident )
564
+ $stderr.puts( 'Hoodoo:ActiveRecord::Finder#polymorphic_find is deprecated - use "foo.acquire( ident )" instead of "Model.polymorphic_find( foo, ident )"' )
565
+ finder.acquire( ident ) # Ignore 'finder'
566
+ end
567
+
568
+ # Deprecated interface replaced by #acquire_with (this is an alias).
569
+ #
570
+ # *args:: Passed to #acquire_with.
571
+ #
572
+ def polymorphic_id_fields( *args )
573
+ $stderr.puts( 'Hoodoo:ActiveRecord::Finder#polymorphic_id_fields is deprecated - rename call to "#acquire_with"' )
574
+ acquire_with( *args )
575
+ end
576
+
577
+ # Deprecated interface replaced by #list (this is an alias).
578
+ #
579
+ # +list_parameters+:: Passed to #list.
580
+ #
581
+ def list_finder( list_parameters )
582
+ $stderr.puts( 'Hoodoo:ActiveRecord::Finder#list_finder is deprecated - rename call to "#list"' )
583
+ return list( list_parameters )
584
+ end
585
+
586
+ # Deprecated interface replaced by #search_with (this is an alias).
587
+ #
588
+ # +map+:: Passed to #search_with.
589
+ #
590
+ def list_search_map( map )
591
+ $stderr.puts( 'Hoodoo:ActiveRecord::Finder#list_search_map is deprecated - rename call to "#search_with"' )
592
+ search_with( map )
593
+ end
594
+
595
+ # Deprecated interface replaced by #filter_with (this is an alias).
596
+ #
597
+ # +map+:: Passed to #filter_with.
598
+ #
599
+ def list_filter_map( map )
600
+ $stderr.puts( 'Hoodoo:ActiveRecord::Finder#list_filter_map is deprecated - rename call to "#filter_with"' )
601
+ filter_with( map )
602
+ end
603
+ end
604
+ end
605
+ end
606
+ end