hoodoo 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/hoodoo +5 -0
- data/lib/hoodoo.rb +27 -0
- data/lib/hoodoo/active.rb +32 -0
- data/lib/hoodoo/active/active_model/uuid_validator.rb +45 -0
- data/lib/hoodoo/active/active_record/base.rb +81 -0
- data/lib/hoodoo/active/active_record/creator.rb +134 -0
- data/lib/hoodoo/active/active_record/dated.rb +343 -0
- data/lib/hoodoo/active/active_record/error_mapping.rb +351 -0
- data/lib/hoodoo/active/active_record/finder.rb +606 -0
- data/lib/hoodoo/active/active_record/search_helper.rb +189 -0
- data/lib/hoodoo/active/active_record/secure.rb +431 -0
- data/lib/hoodoo/active/active_record/support.rb +106 -0
- data/lib/hoodoo/active/active_record/translated.rb +87 -0
- data/lib/hoodoo/active/active_record/uuid.rb +80 -0
- data/lib/hoodoo/active/active_record/writer.rb +321 -0
- data/lib/hoodoo/client.rb +23 -0
- data/lib/hoodoo/client/augmented_array.rb +29 -0
- data/lib/hoodoo/client/augmented_base.rb +168 -0
- data/lib/hoodoo/client/augmented_hash.rb +23 -0
- data/lib/hoodoo/client/client.rb +354 -0
- data/lib/hoodoo/client/endpoint/endpoint.rb +427 -0
- data/lib/hoodoo/client/endpoint/endpoints/amqp.rb +180 -0
- data/lib/hoodoo/client/endpoint/endpoints/auto_session.rb +194 -0
- data/lib/hoodoo/client/endpoint/endpoints/http.rb +203 -0
- data/lib/hoodoo/client/endpoint/endpoints/http_based.rb +367 -0
- data/lib/hoodoo/client/endpoint/endpoints/not_found.rb +59 -0
- data/lib/hoodoo/client/headers.rb +269 -0
- data/lib/hoodoo/communicators.rb +23 -0
- data/lib/hoodoo/communicators/fast.rb +44 -0
- data/lib/hoodoo/communicators/pool.rb +601 -0
- data/lib/hoodoo/communicators/slow.rb +84 -0
- data/lib/hoodoo/data.rb +51 -0
- data/lib/hoodoo/data/resources/caller.rb +39 -0
- data/lib/hoodoo/data/resources/errors.rb +28 -0
- data/lib/hoodoo/data/resources/log.rb +31 -0
- data/lib/hoodoo/data/resources/session.rb +26 -0
- data/lib/hoodoo/data/types/error_primitive.rb +27 -0
- data/lib/hoodoo/data/types/permissions.rb +40 -0
- data/lib/hoodoo/data/types/permissions_defaults.rb +32 -0
- data/lib/hoodoo/data/types/permissions_full.rb +28 -0
- data/lib/hoodoo/data/types/permissions_resources.rb +31 -0
- data/lib/hoodoo/discovery.rb +20 -0
- data/lib/hoodoo/errors.rb +19 -0
- data/lib/hoodoo/errors/error_descriptions.rb +229 -0
- data/lib/hoodoo/errors/errors.rb +322 -0
- data/lib/hoodoo/generator.rb +139 -0
- data/lib/hoodoo/logger.rb +23 -0
- data/lib/hoodoo/logger/fast_writer.rb +27 -0
- data/lib/hoodoo/logger/flattener_mixin.rb +36 -0
- data/lib/hoodoo/logger/logger.rb +387 -0
- data/lib/hoodoo/logger/slow_writer.rb +49 -0
- data/lib/hoodoo/logger/writer_mixin.rb +52 -0
- data/lib/hoodoo/logger/writers/file_writer.rb +45 -0
- data/lib/hoodoo/logger/writers/log_entries_dot_com_writer.rb +64 -0
- data/lib/hoodoo/logger/writers/stream_writer.rb +43 -0
- data/lib/hoodoo/middleware.rb +33 -0
- data/lib/hoodoo/presenters.rb +45 -0
- data/lib/hoodoo/presenters/base.rb +281 -0
- data/lib/hoodoo/presenters/base_dsl.rb +519 -0
- data/lib/hoodoo/presenters/common_resource_fields.rb +31 -0
- data/lib/hoodoo/presenters/embedding.rb +232 -0
- data/lib/hoodoo/presenters/types/array.rb +118 -0
- data/lib/hoodoo/presenters/types/boolean.rb +26 -0
- data/lib/hoodoo/presenters/types/date.rb +26 -0
- data/lib/hoodoo/presenters/types/date_time.rb +26 -0
- data/lib/hoodoo/presenters/types/decimal.rb +47 -0
- data/lib/hoodoo/presenters/types/enum.rb +55 -0
- data/lib/hoodoo/presenters/types/field.rb +158 -0
- data/lib/hoodoo/presenters/types/float.rb +26 -0
- data/lib/hoodoo/presenters/types/hash.rb +361 -0
- data/lib/hoodoo/presenters/types/integer.rb +26 -0
- data/lib/hoodoo/presenters/types/object.rb +117 -0
- data/lib/hoodoo/presenters/types/string.rb +53 -0
- data/lib/hoodoo/presenters/types/tags.rb +24 -0
- data/lib/hoodoo/presenters/types/text.rb +26 -0
- data/lib/hoodoo/presenters/types/uuid.rb +54 -0
- data/lib/hoodoo/services.rb +34 -0
- data/lib/hoodoo/services/discovery/discoverers/by_consul.rb +66 -0
- data/lib/hoodoo/services/discovery/discoverers/by_convention.rb +173 -0
- data/lib/hoodoo/services/discovery/discoverers/by_drb/by_drb.rb +195 -0
- data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server.rb +166 -0
- data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server_start.rb +37 -0
- data/lib/hoodoo/services/discovery/discovery.rb +186 -0
- data/lib/hoodoo/services/discovery/results/for_amqp.rb +58 -0
- data/lib/hoodoo/services/discovery/results/for_http.rb +85 -0
- data/lib/hoodoo/services/discovery/results/for_local.rb +85 -0
- data/lib/hoodoo/services/discovery/results/for_remote.rb +57 -0
- data/lib/hoodoo/services/middleware/amqp_log_message.rb +186 -0
- data/lib/hoodoo/services/middleware/amqp_log_writer.rb +119 -0
- data/lib/hoodoo/services/middleware/endpoints/inter_resource_local.rb +130 -0
- data/lib/hoodoo/services/middleware/endpoints/inter_resource_remote.rb +202 -0
- data/lib/hoodoo/services/middleware/exception_reporting/base_reporter.rb +105 -0
- data/lib/hoodoo/services/middleware/exception_reporting/exception_reporting.rb +115 -0
- data/lib/hoodoo/services/middleware/exception_reporting/reporters/airbrake_reporter.rb +64 -0
- data/lib/hoodoo/services/middleware/exception_reporting/reporters/raygun_reporter.rb +63 -0
- data/lib/hoodoo/services/middleware/interaction.rb +127 -0
- data/lib/hoodoo/services/middleware/middleware.rb +2705 -0
- data/lib/hoodoo/services/middleware/rack_monkey_patch.rb +73 -0
- data/lib/hoodoo/services/services/context.rb +153 -0
- data/lib/hoodoo/services/services/implementation.rb +132 -0
- data/lib/hoodoo/services/services/interface.rb +934 -0
- data/lib/hoodoo/services/services/permissions.rb +250 -0
- data/lib/hoodoo/services/services/request.rb +189 -0
- data/lib/hoodoo/services/services/response.rb +316 -0
- data/lib/hoodoo/services/services/service.rb +141 -0
- data/lib/hoodoo/services/services/session.rb +729 -0
- data/lib/hoodoo/utilities.rb +12 -0
- data/lib/hoodoo/utilities/string_inquirer.rb +54 -0
- data/lib/hoodoo/utilities/utilities.rb +380 -0
- data/lib/hoodoo/utilities/uuid.rb +44 -0
- data/lib/hoodoo/version.rb +17 -0
- data/spec/active/active_record/base_spec.rb +57 -0
- data/spec/active/active_record/creator_spec.rb +88 -0
- data/spec/active/active_record/dated_spec.rb +248 -0
- data/spec/active/active_record/error_mapping_spec.rb +360 -0
- data/spec/active/active_record/finder_spec.rb +744 -0
- data/spec/active/active_record/search_helper_spec.rb +384 -0
- data/spec/active/active_record/secure_spec.rb +435 -0
- data/spec/active/active_record/support_spec.rb +225 -0
- data/spec/active/active_record/translated_spec.rb +19 -0
- data/spec/active/active_record/uuid_spec.rb +72 -0
- data/spec/active/active_record/writer_spec.rb +272 -0
- data/spec/alchemy/alchemy-amq.rb +33 -0
- data/spec/client/augmented_array_spec.rb +15 -0
- data/spec/client/augmented_base_spec.rb +50 -0
- data/spec/client/augmented_hash_spec.rb +15 -0
- data/spec/client/client_spec.rb +955 -0
- data/spec/client/endpoint/endpoint_spec.rb +70 -0
- data/spec/client/endpoint/endpoints/amqp_spec.rb +16 -0
- data/spec/client/endpoint/endpoints/auto_session_spec.rb +9 -0
- data/spec/client/endpoint/endpoints/http_based_spec.rb +9 -0
- data/spec/client/endpoint/endpoints/http_spec.rb +103 -0
- data/spec/client/endpoint/endpoints/not_found_spec.rb +35 -0
- data/spec/client/headers_spec.rb +172 -0
- data/spec/communicators/fast_spec.rb +9 -0
- data/spec/communicators/pool_spec.rb +339 -0
- data/spec/communicators/slow_spec.rb +15 -0
- data/spec/data/resources/caller_spec.rb +156 -0
- data/spec/data/resources/errors_spec.rb +22 -0
- data/spec/data/resources/log_spec.rb +20 -0
- data/spec/data/resources/session_spec.rb +15 -0
- data/spec/data/types/error_primitive_spec.rb +15 -0
- data/spec/data/types/permissions_defaults_spec.rb +25 -0
- data/spec/data/types/permissions_full_spec.rb +44 -0
- data/spec/data/types/permissions_resources_spec.rb +34 -0
- data/spec/data/types/permissions_spec.rb +37 -0
- data/spec/errors/error_descriptions_spec.rb +98 -0
- data/spec/errors/errors_spec.rb +346 -0
- data/spec/integration/service_actions_spec.rb +112 -0
- data/spec/logger/fast_writer_spec.rb +18 -0
- data/spec/logger/logger_spec.rb +259 -0
- data/spec/logger/slow_writer_spec.rb +144 -0
- data/spec/logger/writers/file_writer_spec.rb +37 -0
- data/spec/logger/writers/log_entries_dot_com_writer_spec.rb +29 -0
- data/spec/logger/writers/stream_writer_spec.rb +38 -0
- data/spec/presenters/base_dsl_spec.rb +111 -0
- data/spec/presenters/base_spec.rb +871 -0
- data/spec/presenters/common_resource_fields_spec.rb +30 -0
- data/spec/presenters/embedding_spec.rb +87 -0
- data/spec/presenters/types/array_spec.rb +249 -0
- data/spec/presenters/types/boolean_spec.rb +51 -0
- data/spec/presenters/types/date_spec.rb +57 -0
- data/spec/presenters/types/date_time_spec.rb +59 -0
- data/spec/presenters/types/decimal_spec.rb +58 -0
- data/spec/presenters/types/enum_spec.rb +71 -0
- data/spec/presenters/types/field_spec.rb +77 -0
- data/spec/presenters/types/float_spec.rb +50 -0
- data/spec/presenters/types/hash_spec.rb +1069 -0
- data/spec/presenters/types/integer_spec.rb +50 -0
- data/spec/presenters/types/object_spec.rb +177 -0
- data/spec/presenters/types/string_spec.rb +65 -0
- data/spec/presenters/types/tags_spec.rb +56 -0
- data/spec/presenters/types/text_spec.rb +50 -0
- data/spec/presenters/types/uuid_spec.rb +46 -0
- data/spec/presenters/walk_spec.rb +198 -0
- data/spec/services/discovery/discoverers/by_consul_spec.rb +29 -0
- data/spec/services/discovery/discoverers/by_convention_spec.rb +67 -0
- data/spec/services/discovery/discoverers/by_drb/by_drb_spec.rb +80 -0
- data/spec/services/discovery/discoverers/by_drb/drb_server_spec.rb +205 -0
- data/spec/services/discovery/discovery_spec.rb +73 -0
- data/spec/services/discovery/results/for_amqp_spec.rb +17 -0
- data/spec/services/discovery/results/for_http_spec.rb +37 -0
- data/spec/services/discovery/results/for_local_spec.rb +21 -0
- data/spec/services/discovery/results/for_remote_spec.rb +15 -0
- data/spec/services/middleware/amqp_log_message_spec.rb +60 -0
- data/spec/services/middleware/amqp_log_writer_spec.rb +95 -0
- data/spec/services/middleware/endpoints/inter_resource_local_spec.rb +9 -0
- data/spec/services/middleware/endpoints/inter_resource_remote_spec.rb +9 -0
- data/spec/services/middleware/exception_reporting/base_reporter_spec.rb +16 -0
- data/spec/services/middleware/exception_reporting/exception_reporting_spec.rb +92 -0
- data/spec/services/middleware/exception_reporting/reporters/airbrake_reporter_spec.rb +24 -0
- data/spec/services/middleware/exception_reporting/reporters/raygun_reporter_spec.rb +23 -0
- data/spec/services/middleware/middleware_cors_spec.rb +93 -0
- data/spec/services/middleware/middleware_create_update_spec.rb +489 -0
- data/spec/services/middleware/middleware_dated_at_spec.rb +186 -0
- data/spec/services/middleware/middleware_exotic_communication_spec.rb +560 -0
- data/spec/services/middleware/middleware_logging_spec.rb +356 -0
- data/spec/services/middleware/middleware_multi_local_spec.rb +1094 -0
- data/spec/services/middleware/middleware_multi_remote_spec.rb +1440 -0
- data/spec/services/middleware/middleware_permissions_spec.rb +1014 -0
- data/spec/services/middleware/middleware_public_spec.rb +238 -0
- data/spec/services/middleware/middleware_spec.rb +1569 -0
- data/spec/services/middleware/string_inquirer_spec.rb +30 -0
- data/spec/services/services/application_spec.rb +74 -0
- data/spec/services/services/context_spec.rb +48 -0
- data/spec/services/services/implementation_spec.rb +45 -0
- data/spec/services/services/interface_spec.rb +262 -0
- data/spec/services/services/permissions_spec.rb +249 -0
- data/spec/services/services/request_spec.rb +95 -0
- data/spec/services/services/response_spec.rb +250 -0
- data/spec/services/services/session_spec.rb +432 -0
- data/spec/spec_helper.rb +298 -0
- data/spec/utilities/utilities_spec.rb +537 -0
- data/spec/utilities/uuid_spec.rb +20 -0
- 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
|