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.
- 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
|