hoodoo 1.0.2

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