shamu 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +26 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +89 -30
- data/.yardopts +4 -5
- data/Gemfile +24 -12
- data/Guardfile +5 -0
- data/LABELS.md +22 -0
- data/README.md +41 -0
- data/Rakefile +12 -0
- data/circle.yml +7 -3
- data/config.ru +7 -0
- data/lib/shamu/active_record.rb +7 -0
- data/lib/shamu/attributes/assignment.rb +114 -0
- data/lib/shamu/attributes/equality.rb +40 -0
- data/lib/shamu/attributes/fluid_assignment.rb +49 -0
- data/lib/shamu/attributes/validation.rb +74 -0
- data/lib/shamu/attributes.rb +255 -0
- data/lib/shamu/auditing/README.md +0 -0
- data/lib/shamu/auditing/audit_record.rb +32 -0
- data/lib/shamu/auditing/auditing_service.rb +32 -0
- data/lib/shamu/auditing/list_scope.rb +22 -0
- data/lib/shamu/auditing/logging_auditing_service.rb +16 -0
- data/lib/shamu/auditing/support.rb +75 -0
- data/lib/shamu/auditing/transaction.rb +58 -0
- data/lib/shamu/auditing.rb +12 -0
- data/lib/shamu/entities/README.md +1 -0
- data/lib/shamu/entities/active_record.rb +123 -0
- data/lib/shamu/entities/active_record_soft_destroy.rb +91 -0
- data/lib/shamu/entities/entity.rb +196 -0
- data/lib/shamu/entities/entity_path.rb +87 -0
- data/lib/shamu/entities/identity_cache.rb +64 -0
- data/lib/shamu/entities/list.rb +54 -0
- data/lib/shamu/entities/list_scope/dates.rb +57 -0
- data/lib/shamu/entities/list_scope/paging.rb +51 -0
- data/lib/shamu/entities/list_scope/scoped_paging.rb +65 -0
- data/lib/shamu/entities/list_scope/sorting.rb +76 -0
- data/lib/shamu/entities/list_scope.rb +105 -0
- data/lib/shamu/entities/null_entity.rb +88 -0
- data/lib/shamu/entities.rb +11 -0
- data/lib/shamu/error.rb +23 -5
- data/lib/shamu/events/README.md +0 -0
- data/lib/shamu/events/active_record/channel.rb +36 -0
- data/lib/shamu/events/active_record/message.rb +52 -0
- data/lib/shamu/events/active_record/migration.rb +49 -0
- data/lib/shamu/events/active_record/runner.rb +28 -0
- data/lib/shamu/events/active_record/service.rb +174 -0
- data/lib/shamu/events/active_record.rb +13 -0
- data/lib/shamu/events/channel_stats.rb +23 -0
- data/lib/shamu/events/error.rb +24 -0
- data/lib/shamu/events/events_service.rb +136 -0
- data/lib/shamu/events/in_memory/async_service.rb +48 -0
- data/lib/shamu/events/in_memory/service.rb +97 -0
- data/lib/shamu/events/in_memory.rb +10 -0
- data/lib/shamu/events/message.rb +38 -0
- data/lib/shamu/events/support.rb +60 -0
- data/lib/shamu/events.rb +12 -0
- data/lib/shamu/features/README.md +0 -0
- data/lib/shamu/features/conditions/condition.rb +39 -0
- data/lib/shamu/features/conditions/env.rb +37 -0
- data/lib/shamu/features/conditions/hosts.rb +25 -0
- data/lib/shamu/features/conditions/matching.rb +16 -0
- data/lib/shamu/features/conditions/not_matching.rb +16 -0
- data/lib/shamu/features/conditions/percentage.rb +44 -0
- data/lib/shamu/features/conditions/proc.rb +54 -0
- data/lib/shamu/features/conditions/roles.rb +23 -0
- data/lib/shamu/features/conditions/schedule_at.rb +27 -0
- data/lib/shamu/features/conditions.rb +18 -0
- data/lib/shamu/features/config_service.rb +10 -0
- data/lib/shamu/features/context.rb +80 -0
- data/lib/shamu/features/env_store.rb +88 -0
- data/lib/shamu/features/errors.rb +29 -0
- data/lib/shamu/features/features_service.rb +168 -0
- data/lib/shamu/features/list_scope.rb +30 -0
- data/lib/shamu/features/selector.rb +50 -0
- data/lib/shamu/features/support.rb +51 -0
- data/lib/shamu/features/toggle.rb +149 -0
- data/lib/shamu/features/toggle_codec.rb +69 -0
- data/lib/shamu/features.rb +16 -0
- data/lib/shamu/locale/en.yml +22 -2
- data/lib/shamu/logger.rb +13 -0
- data/lib/shamu/rack/README.md +0 -0
- data/lib/shamu/rack/cookies.rb +115 -0
- data/lib/shamu/rack/cookies_middleware.rb +26 -0
- data/lib/shamu/rack/query_params.rb +41 -0
- data/lib/shamu/rack/query_params_middleware.rb +24 -0
- data/lib/shamu/rack.rb +12 -0
- data/lib/shamu/rails/controller.rb +131 -0
- data/lib/shamu/rails/entity.rb +168 -0
- data/lib/shamu/rails/features.rb +13 -0
- data/lib/shamu/rails/railtie.rb +30 -0
- data/lib/shamu/rails.rb +10 -0
- data/lib/shamu/rspec/matchers.rb +44 -0
- data/lib/shamu/rspec.rb +1 -0
- data/lib/shamu/security/README.md +0 -0
- data/lib/shamu/security/active_record_policy.rb +106 -0
- data/lib/shamu/security/error.rb +65 -0
- data/lib/shamu/security/hashed_value.rb +71 -0
- data/lib/shamu/security/no_policy.rb +15 -0
- data/lib/shamu/security/policy.rb +289 -0
- data/lib/shamu/security/policy_refinement.rb +50 -0
- data/lib/shamu/security/policy_rule.rb +59 -0
- data/lib/shamu/security/principal.rb +72 -0
- data/lib/shamu/security/roles.rb +62 -0
- data/lib/shamu/security/roles_service.rb +30 -0
- data/lib/shamu/security/support.rb +83 -0
- data/lib/shamu/security.rb +43 -0
- data/lib/shamu/services/README.md +2 -0
- data/lib/shamu/services/active_record.rb +58 -0
- data/lib/shamu/services/active_record_crud.rb +378 -0
- data/lib/shamu/services/error.rb +24 -0
- data/lib/shamu/services/lazy_association.rb +31 -0
- data/lib/shamu/services/lazy_transform.rb +97 -0
- data/lib/shamu/services/request.rb +122 -0
- data/lib/shamu/services/request_support.rb +124 -0
- data/lib/shamu/services/result.rb +75 -0
- data/lib/shamu/services/service.rb +355 -0
- data/lib/shamu/services.rb +12 -0
- data/lib/shamu/sessions/README.md +2 -0
- data/lib/shamu/sessions/cookie_store.rb +79 -0
- data/lib/shamu/sessions/session_store.rb +42 -0
- data/lib/shamu/sessions.rb +8 -0
- data/lib/shamu/to_bool_extension.rb +57 -0
- data/lib/shamu/to_model_id_extension.rb +50 -0
- data/lib/shamu/version.rb +10 -4
- data/lib/shamu.rb +18 -6
- data/shamu.gemspec +21 -10
- data/spec/internal/README.md +4 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/schema.rb +3 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/lib/shamu/active_record_support.rb +32 -0
- data/spec/lib/shamu/attributes/assignment_spec.rb +129 -0
- data/spec/lib/shamu/attributes/equality_spec.rb +63 -0
- data/spec/lib/shamu/attributes/fluid_assignment_spec.rb +31 -0
- data/spec/lib/shamu/attributes/validation_spec.rb +53 -0
- data/spec/lib/shamu/attributes_spec.rb +331 -0
- data/spec/lib/shamu/auditing/logging_auditing_service_spec.rb +18 -0
- data/spec/lib/shamu/auditing/support_spec.rb +41 -0
- data/spec/lib/shamu/entities/active_record_soft_destroy_spec.rb +82 -0
- data/spec/lib/shamu/entities/active_record_spec.rb +66 -0
- data/spec/lib/shamu/entities/entity_path_spec.rb +40 -0
- data/spec/lib/shamu/entities/entity_spec.rb +56 -0
- data/spec/lib/shamu/entities/identity_cache_spec.rb +69 -0
- data/spec/lib/shamu/entities/list_scope/dates_spec.rb +47 -0
- data/spec/lib/shamu/entities/list_scope/paging_spec.rb +41 -0
- data/spec/lib/shamu/entities/list_scope/scoped_paging_spec.rb +40 -0
- data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +59 -0
- data/spec/lib/shamu/entities/list_scope_spec.rb +127 -0
- data/spec/lib/shamu/entities/list_spec.rb +60 -0
- data/spec/lib/shamu/entities/null_entity_spec.rb +94 -0
- data/spec/lib/shamu/events/active_record/migration_spec.rb +11 -0
- data/spec/lib/shamu/events/active_record/service_spec.rb +139 -0
- data/spec/lib/shamu/events/events_service_spec.rb +57 -0
- data/spec/lib/shamu/events/in_memory/async_service_spec.rb +37 -0
- data/spec/lib/shamu/events/in_memory/service_spec.rb +36 -0
- data/spec/lib/shamu/events/message_spec.rb +7 -0
- data/spec/lib/shamu/events/support_spec.rb +44 -0
- data/spec/lib/shamu/features/conditions/condition_spec.rb +8 -0
- data/spec/lib/shamu/features/conditions/env_spec.rb +29 -0
- data/spec/lib/shamu/features/conditions/hosts_spec.rb +21 -0
- data/spec/lib/shamu/features/conditions/matching_spec.rb +23 -0
- data/spec/lib/shamu/features/conditions/percentage_spec.rb +71 -0
- data/spec/lib/shamu/features/conditions/proc_spec.rb +28 -0
- data/spec/lib/shamu/features/env_store_spec.rb +48 -0
- data/spec/lib/shamu/features/features.yml +34 -0
- data/spec/lib/shamu/features/features_service_spec.rb +109 -0
- data/spec/lib/shamu/features/secondary.yml +5 -0
- data/spec/lib/shamu/features/selector_spec.rb +17 -0
- data/spec/lib/shamu/features/support_spec.rb +45 -0
- data/spec/lib/shamu/features/toggle_codec_spec.rb +28 -0
- data/spec/lib/shamu/features/toggle_spec.rb +42 -0
- data/spec/lib/shamu/rack/cookies_middleware_spec.rb +33 -0
- data/spec/lib/shamu/rack/cookies_spec.rb +43 -0
- data/spec/lib/shamu/rack/query_params_middleware_spec.rb +33 -0
- data/spec/lib/shamu/rack/query_params_spec.rb +23 -0
- data/spec/lib/shamu/rails/controller_spec.rb +74 -0
- data/spec/lib/shamu/rails/entity_spec.rb +150 -0
- data/spec/lib/shamu/rails/features.yml +13 -0
- data/spec/lib/shamu/rails/features_spec.rb +45 -0
- data/spec/lib/shamu/security/active_record_policy_spec.rb +38 -0
- data/spec/lib/shamu/security/hashed_value_spec.rb +41 -0
- data/spec/lib/shamu/security/policy_refinement_spec.rb +61 -0
- data/spec/lib/shamu/security/policy_rule_spec.rb +60 -0
- data/spec/lib/shamu/security/policy_spec.rb +158 -0
- data/spec/lib/shamu/security/roles_spec.rb +46 -0
- data/spec/lib/shamu/services/active_record_crud_spec.rb +460 -0
- data/spec/lib/shamu/services/active_record_spec.rb +92 -0
- data/spec/lib/shamu/services/lazy_association_spec.rb +31 -0
- data/spec/lib/shamu/services/lazy_transform_spec.rb +96 -0
- data/spec/lib/shamu/services/request_spec.rb +58 -0
- data/spec/lib/shamu/services/request_support_spec.rb +129 -0
- data/spec/lib/shamu/services/result_spec.rb +37 -0
- data/spec/lib/shamu/services/service_spec.rb +307 -0
- data/spec/lib/shamu/sessions/cookie_store_spec.rb +44 -0
- data/spec/lib/shamu/to_bool_extension_spec.rb +67 -0
- data/spec/lib/shamu/to_model_id_extension_spec.rb +54 -0
- data/spec/rails_helper.rb +13 -0
- data/spec/spec_helper.rb +17 -12
- data/spec/support/active_record.rb +17 -0
- data/spec/support/database.rb +14 -0
- data/spec/support/logger.rb +0 -0
- metadata +383 -9
- data/spec/lib/shamu_spec.rb +0 -5
- /data/{spec/internal/log/test.log → lib/shamu/attributes/README.md} +0 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Services
|
3
|
+
|
4
|
+
# Helper methods useful for services that interact with {ActiveRecord::Base}
|
5
|
+
# models.
|
6
|
+
module ActiveRecord
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
# @!visibility public
|
11
|
+
#
|
12
|
+
# Watch for ActiveRecord::RecordNotFound errors and rethrow as a
|
13
|
+
# {Shamu::NotFoundError}.
|
14
|
+
def wrap_not_found( &block )
|
15
|
+
yield
|
16
|
+
rescue ::ActiveRecord::RecordNotFound
|
17
|
+
raise Shamu::NotFoundError
|
18
|
+
end
|
19
|
+
|
20
|
+
# @!visibility public
|
21
|
+
#
|
22
|
+
# Wrap all the changes to any ActiveRecord resource in a transaction.
|
23
|
+
# @param [Hash] options to pass to
|
24
|
+
# ActiveRecord::Transactions.transaction.
|
25
|
+
# @yieldreturn [Result] the validation sources for the transaction. See
|
26
|
+
# {Service#with_result}.
|
27
|
+
# @return [Result]
|
28
|
+
def with_transaction( options = {}, &block )
|
29
|
+
result = nil
|
30
|
+
|
31
|
+
::ActiveRecord::Base.transaction options do
|
32
|
+
result = yield
|
33
|
+
raise ::ActiveRecord::Rollback if result && !result.valid?
|
34
|
+
end
|
35
|
+
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
# @!visibility public
|
40
|
+
#
|
41
|
+
# Apply the filters specified in `list_scope` to the `relation`.
|
42
|
+
#
|
43
|
+
# @param [ActiveRecord::Relation] relation to filter.
|
44
|
+
# @param [Entities::ListScope] list_scope to apply.
|
45
|
+
# @return [ActiveRecord::Relation] the scoped relation.
|
46
|
+
def scope_relation( relation, list_scope )
|
47
|
+
return unless relation
|
48
|
+
|
49
|
+
if relation.respond_to?( :by_list_scope )
|
50
|
+
relation.by_list_scope( list_scope )
|
51
|
+
else
|
52
|
+
fail "Can't scope a #{ relation.klass }. Add `scope :by_list_scope, ->(list_scope) { ... }` or include Shamu::Entities::ActiveRecord." # rubocop:disable Metrics/LineLength
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,378 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Services
|
3
|
+
|
4
|
+
# Adds standard CRUD builders to an {ActiveRecordService} to reduce
|
5
|
+
# boilerplate for common methods.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
#
|
9
|
+
# class UsersService < Shamu::Services::Service
|
10
|
+
# include Shamu::Services::Crud
|
11
|
+
#
|
12
|
+
# # Define the resource that the service will manage
|
13
|
+
# resource UserEntity, Models::User
|
14
|
+
#
|
15
|
+
# # Define finder methods #find, #list and #lookup using the given
|
16
|
+
# # default scope.
|
17
|
+
# finders Models::User.active
|
18
|
+
#
|
19
|
+
# # Define change methods
|
20
|
+
# create
|
21
|
+
# update
|
22
|
+
#
|
23
|
+
# # Common update/change behavior for #create and #update
|
24
|
+
# change do |request, model|
|
25
|
+
# model.last_updated_at = Time.now
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# # Standard destroy method
|
29
|
+
# destroy
|
30
|
+
#
|
31
|
+
# # Build the entity class from the given record.
|
32
|
+
# build_entity do |record, records = nil|
|
33
|
+
# parent = lookup_association( record.parent_id, self ) do
|
34
|
+
# records.pluck( :parent_id ) if records
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# scorpion.fetch UserEntity, { parent: parent }, {}
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
module ActiveRecordCrud
|
41
|
+
extend ActiveSupport::Concern
|
42
|
+
|
43
|
+
# Known DSL methods defined by {ActiveRecordCrud}.
|
44
|
+
DSL_METHODS = %i( create update change destroy find list lookup finders ).freeze
|
45
|
+
|
46
|
+
included do |base|
|
47
|
+
base.include Shamu::Services::RequestSupport
|
48
|
+
base.include Shamu::Services::ActiveRecord
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def model_class
|
54
|
+
self.class.model_class
|
55
|
+
end
|
56
|
+
|
57
|
+
def entity_class
|
58
|
+
self.class.model_class
|
59
|
+
end
|
60
|
+
|
61
|
+
# @!visibility public
|
62
|
+
#
|
63
|
+
# Hook to allow a security module to authorize actions taken by the
|
64
|
+
# standard CRUD methods. If authorization is not granted, then an
|
65
|
+
# exception should be raised. Default behavior is a no-op.
|
66
|
+
#
|
67
|
+
# @param [Symbol] method on the service that was invoked.
|
68
|
+
# @param [Entities::Entity, Class, Symbol] resource the entity, class or
|
69
|
+
# arbitrary symbol describing the resource that the service method
|
70
|
+
# applies to.
|
71
|
+
# @param [Object] additional_context that the security module might
|
72
|
+
# consider when authorizing the transaction.
|
73
|
+
# @return [resource] the resource given to authorize.
|
74
|
+
def authorize!( method, resource, additional_context = nil )
|
75
|
+
resource
|
76
|
+
end
|
77
|
+
|
78
|
+
# @!visibility public
|
79
|
+
#
|
80
|
+
# Hook to allow a security module to pre-filter ActiveRecord queries
|
81
|
+
# for the standard crud methods. Default behavior is a no-op.
|
82
|
+
#
|
83
|
+
# @param [Symbol] method on the service that was invoked.
|
84
|
+
# @param [ActiveRecord::Relation] relation to filter
|
85
|
+
# @param [Object] additional_context that the security module might
|
86
|
+
# consider when authorizing the transaction.
|
87
|
+
#
|
88
|
+
# @return [relation] the filtered relation.
|
89
|
+
def authorize_relation( method, relation, additional_context = nil )
|
90
|
+
relation
|
91
|
+
end
|
92
|
+
|
93
|
+
class_methods do
|
94
|
+
|
95
|
+
# Declare the entity and resource classes used by the service.
|
96
|
+
#
|
97
|
+
# Creates instance and class level methods `entity_class` and
|
98
|
+
# `model_class`.
|
99
|
+
#
|
100
|
+
# See {.build_entity} for build_entity block details.
|
101
|
+
#
|
102
|
+
# @param [Class] entity_class the {Entities::Entity} class that will be
|
103
|
+
# returned by finders and mutator methods.
|
104
|
+
# @param [Class] model_class the {ActiveRecord::Base} model
|
105
|
+
# @param [Array<Symbol>] methods the {DSL_METHODS DSL methods} to
|
106
|
+
# include (eg :create, :update, :find, etc.)
|
107
|
+
# @yield (record, records = nil )
|
108
|
+
# @yieldparam [ActiveRecord::Base] record to build an {Entities::Entity}
|
109
|
+
# for.
|
110
|
+
# @yieldparam [ActiveRecord::Relation] records that are all being built
|
111
|
+
# @yieldreturn [Entities::Entity] the entity projection for the given
|
112
|
+
# record.
|
113
|
+
# @return [void]
|
114
|
+
def resource( entity_class, model_class, methods: nil, &block )
|
115
|
+
private define_method( :entity_class ) { entity_class }
|
116
|
+
define_singleton_method( :entity_class ) { entity_class }
|
117
|
+
|
118
|
+
private define_method( :model_class ) { model_class }
|
119
|
+
define_singleton_method( :model_class ) { model_class }
|
120
|
+
|
121
|
+
( Array( methods ) & DSL_METHODS ).each do |method|
|
122
|
+
send method
|
123
|
+
end
|
124
|
+
|
125
|
+
build_entity( &block )
|
126
|
+
end
|
127
|
+
|
128
|
+
# @return [Class] the {Entities::Entity} class that the service will
|
129
|
+
# return from it's methods.
|
130
|
+
def entity_class
|
131
|
+
resource_not_configured
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return [Class] the {ActiveRecord::Base} class used to store the data
|
135
|
+
# managed by the service.
|
136
|
+
def model_class
|
137
|
+
resource_not_configured
|
138
|
+
end
|
139
|
+
|
140
|
+
# Define a `#create` method on the service that takes a single {Request}
|
141
|
+
# parameter.
|
142
|
+
#
|
143
|
+
# See {.apply_changes} for details.
|
144
|
+
# @yield (see .apply_changes)
|
145
|
+
# @yieldparam (see .apply_changes)
|
146
|
+
# @return [void]
|
147
|
+
def create( &block )
|
148
|
+
define_method :create do |params = nil|
|
149
|
+
with_request params, request_class( :create ) do |request|
|
150
|
+
authorize! :create, entity_class, request
|
151
|
+
|
152
|
+
record = request.apply_to( model_class.new )
|
153
|
+
if block
|
154
|
+
yield( request, record )
|
155
|
+
elsif respond_to? :apply_changes
|
156
|
+
apply_changes( request, record )
|
157
|
+
end
|
158
|
+
|
159
|
+
next record unless record.save
|
160
|
+
build_entity record
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Define an change `method` on the service that takes the id of the
|
166
|
+
# resource to modify and a corresponding {Request} parameter.
|
167
|
+
#
|
168
|
+
# See {.apply_changes} for details.
|
169
|
+
# @yield (see .apply_changes)
|
170
|
+
# @yieldparam (see .apply_changes)
|
171
|
+
# @return [Result] the result of the request.
|
172
|
+
# @return [void]
|
173
|
+
def change( method = :update, &block )
|
174
|
+
define_method method do |id, params = nil|
|
175
|
+
with_request params, request_class( method ) do |request|
|
176
|
+
record = model_class.find( id.to_model_id )
|
177
|
+
authorize! method, build_entity( record ), request
|
178
|
+
|
179
|
+
request.apply_to( record )
|
180
|
+
|
181
|
+
if block
|
182
|
+
yield( request, record )
|
183
|
+
elsif respond_to? :apply_changes
|
184
|
+
apply_changes( request, record )
|
185
|
+
end
|
186
|
+
|
187
|
+
next record unless record.save
|
188
|
+
build_entity record
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Define an `update` method on the service that takes the id of the
|
194
|
+
# resource to update and a {Request} parameter. After applying the
|
195
|
+
# changes the record is persisted and the updated entity result is
|
196
|
+
# returned.
|
197
|
+
#
|
198
|
+
# See {.apply_changes} for details.
|
199
|
+
# @yield (see .apply_changes)
|
200
|
+
# @yieldparam (see .apply_changes)
|
201
|
+
# @return [void]
|
202
|
+
def update( &block )
|
203
|
+
change :update, &block
|
204
|
+
end
|
205
|
+
|
206
|
+
# Define a private method `apply_changes` on the service used by the
|
207
|
+
# {.create} and {.change} defined methods to apply changes in a
|
208
|
+
# {Request} to the model.
|
209
|
+
#
|
210
|
+
# @yield ( request, record ) a block that applies changes in the
|
211
|
+
# `request` to the `record`.
|
212
|
+
# @yieldparam [Request] request the {Request} containing all the changes
|
213
|
+
# that should be applied to the `record`.
|
214
|
+
# @yieldparam [ActiveRecord::Base] record the record to be updated.
|
215
|
+
# @yieldreturn [void]
|
216
|
+
# @return [void]
|
217
|
+
def apply_changes( &block )
|
218
|
+
define_method :apply_changes, &block
|
219
|
+
private :apply_changes
|
220
|
+
end
|
221
|
+
|
222
|
+
# Define the standard finder methods {.find}, {.lookup} and {.list}.
|
223
|
+
#
|
224
|
+
# @param [ActiveRecord::Relation] default_scope to use when finding
|
225
|
+
# records.
|
226
|
+
# @return [void]
|
227
|
+
def finders( default_scope = model_class.all, only: nil, except: nil )
|
228
|
+
methods = Array( only || [ :find, :lookup, :list ] )
|
229
|
+
methods -= Array( except ) if except
|
230
|
+
|
231
|
+
methods.each do |method|
|
232
|
+
send method, default_scope
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Define a `find( id )` method on the service that returns the entity
|
237
|
+
# with the given id if found or raises a {Shamu::NotFoundError} if the
|
238
|
+
# entity does not exist.
|
239
|
+
#
|
240
|
+
# @param [ActiveRecord::Relation] default_scope to use when finding
|
241
|
+
# records.
|
242
|
+
# @yield (id)
|
243
|
+
# @yieldreturn (ActiveRecord::Base) the found record.
|
244
|
+
# @return [void]
|
245
|
+
def find( default_scope = model_class.all, &block )
|
246
|
+
if block_given?
|
247
|
+
define_method :find do |id|
|
248
|
+
wrap_not_found do
|
249
|
+
record = yield( id )
|
250
|
+
authorize! :read, build_entity( record )
|
251
|
+
end
|
252
|
+
end
|
253
|
+
else
|
254
|
+
define_method :find do |id|
|
255
|
+
authorize! :read, find_by_lookup( id )
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Define a `lookup( *ids )` method that takes a list of entity ids to
|
261
|
+
# find. Calls {#build_entity} for each found record, or constructs a
|
262
|
+
# {Entities::NullEntity} for ids that were not found.
|
263
|
+
#
|
264
|
+
# @param [ActiveRecord::Relation] default_scope to use when finding
|
265
|
+
# records.
|
266
|
+
# @yield (uncached_ids)
|
267
|
+
# @yieldparam [Array<Object>] ids that need to be fetched from the
|
268
|
+
# underlying resource.
|
269
|
+
# @yieldreturn [ActiveRecord::Relation] records for ids found in the
|
270
|
+
# underlying resource.
|
271
|
+
# @return [void]
|
272
|
+
def lookup( default_scope = model_class.all, &block )
|
273
|
+
define_method :lookup do |*ids|
|
274
|
+
cached_lookup( ids ) do |uncached_ids|
|
275
|
+
records = block_given? ? yield( uncached_ids ) : default_scope.where( id: uncached_ids )
|
276
|
+
records = authorize_relation :read, records
|
277
|
+
entity_lookup_list records, uncached_ids, entity_class.null_entity
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Define a `list( params = nil )` method that takes a
|
283
|
+
# {Entities::ListScope} and returns all the entities selected by that
|
284
|
+
# scope.
|
285
|
+
#
|
286
|
+
# @param [ActiveRecord::Relation] default_scope to use when finding
|
287
|
+
# records.
|
288
|
+
# @yield (scope)
|
289
|
+
# @yieldparam [ListScope] scope to apply.
|
290
|
+
# @yieldreturn [ActiveRecord::Relation] records matching the given scope.
|
291
|
+
# @return [void]
|
292
|
+
def list( default_scope = model_class.all, &block )
|
293
|
+
define_method :list do |params = nil|
|
294
|
+
list_scope = Entities::ListScope.for( entity_class ).coerce( params )
|
295
|
+
authorize! :list, entity_class, list_scope
|
296
|
+
|
297
|
+
records = block_given? ? yield( scope ) : scope_relation( default_scope, list_scope )
|
298
|
+
records = authorize_relation( :read, records, list_scope )
|
299
|
+
|
300
|
+
entity_list records
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# Define a `destroy( id )` method that takes an {Entities::Entity} {Entities::Entity#id}
|
305
|
+
# and destroys the resource.
|
306
|
+
#
|
307
|
+
# @param [ActiveRecord::Relation] default_scope to use when finding
|
308
|
+
# records.
|
309
|
+
# @return [void]
|
310
|
+
def destroy( default_scope = model_class.all )
|
311
|
+
define_method :destroy do |id|
|
312
|
+
wrap_not_found do
|
313
|
+
record = default_scope.find( id.to_model_id )
|
314
|
+
authorize! :destroy, build_entity( record )
|
315
|
+
record.destroy
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# Define a private `build_entity( record, records = nil )` method that
|
321
|
+
# constructs an {Entities::Entity} from the given `record`. The optional
|
322
|
+
# `records` argument is used when constructing a list of entities so
|
323
|
+
# that associations can all be fetched once and cached while building
|
324
|
+
# the list of entities.
|
325
|
+
#
|
326
|
+
# If no block is given, creates a simple builder that simply constructs
|
327
|
+
# an instance of the {.entity_class} passing `record: record` to the
|
328
|
+
# initializer.
|
329
|
+
#
|
330
|
+
# See {Service#lookup_association} for details on association caching.
|
331
|
+
#
|
332
|
+
# @yield (record, records = nil )
|
333
|
+
# @yieldparam [ActiveRecord::Base] record to build an {Entities::Entity}
|
334
|
+
# for.
|
335
|
+
# @yieldparam [ActiveRecord::Relation] records that are all being built.
|
336
|
+
# @yieldreturn [Entities::Entity] the entity projection for the given
|
337
|
+
# record.
|
338
|
+
# @return [void]
|
339
|
+
def build_entity( &block )
|
340
|
+
if block_given?
|
341
|
+
define_method :build_entity_instance, &block
|
342
|
+
else
|
343
|
+
define_method :build_entity_instance do |record, _ = nil|
|
344
|
+
scorpion.fetch( entity_class, { record: record }, {} )
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
define_method :build_entity do |record, records = nil|
|
349
|
+
authorize! :read, build_entity_instance( record, records )
|
350
|
+
end
|
351
|
+
|
352
|
+
private :build_entity
|
353
|
+
private :build_entity_instance
|
354
|
+
end
|
355
|
+
|
356
|
+
private
|
357
|
+
|
358
|
+
def resource_not_configured
|
359
|
+
raise IncompleteSetupError, "Resource has not been defined. Add `resource #{ inferred_namespace }#{ inferred_resource_name }Entity, #{ inferred_namespace }Models::#{ inferred_resource_name }` to #{ name }." # rubocop:disable Metrics/LineLength
|
360
|
+
end
|
361
|
+
|
362
|
+
def inferred_resource_name
|
363
|
+
inferred = name || "Resource"
|
364
|
+
inferred.split( "::" ).last.sub /Service/, ""
|
365
|
+
end
|
366
|
+
|
367
|
+
def inferred_namespace
|
368
|
+
parts = ( name || "Resource" ).split( "::" )
|
369
|
+
parts.pop
|
370
|
+
return "" if parts.empty?
|
371
|
+
parts.join( "::" ) << "::"
|
372
|
+
end
|
373
|
+
|
374
|
+
end
|
375
|
+
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "i18n"
|
2
|
+
|
3
|
+
module Shamu
|
4
|
+
|
5
|
+
module Services
|
6
|
+
# A generic error class for problems with shamu services.
|
7
|
+
class Error < Shamu::Error
|
8
|
+
private
|
9
|
+
|
10
|
+
def translation_scope
|
11
|
+
super.dup.insert( 1, :services )
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
# The service has included a module that requires some setup or
|
17
|
+
# configuration but it hasn't been setup properly.
|
18
|
+
class IncompleteSetupError < Error
|
19
|
+
def initialize( message = :incomplete_setup )
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Services
|
3
|
+
|
4
|
+
# Lazily look up an associated resource
|
5
|
+
class LazyAssociation < Delegator
|
6
|
+
|
7
|
+
# ============================================================================
|
8
|
+
# @!group Attributes
|
9
|
+
#
|
10
|
+
|
11
|
+
# @!attribute
|
12
|
+
# @return [Object] the primary key id of the association. Not delegated so
|
13
|
+
# it is safe to use and will not trigger an unnecessary fetch.
|
14
|
+
attr_reader :id
|
15
|
+
|
16
|
+
#
|
17
|
+
# @!endgroup Attributes
|
18
|
+
|
19
|
+
def initialize( id, &block )
|
20
|
+
@id = id
|
21
|
+
@block = block
|
22
|
+
end
|
23
|
+
|
24
|
+
def __getobj__
|
25
|
+
return @association if defined? @association
|
26
|
+
|
27
|
+
@association = @block.call
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Services
|
3
|
+
|
4
|
+
# Lazily transform one enumerable to another with shortcuts for common
|
5
|
+
# collection methods such as first, count, etc.
|
6
|
+
class LazyTransform
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# @param [Enumerable] source enumerable to transform.
|
10
|
+
# @yieldparam [Object] object the original value.
|
11
|
+
# @yieldreturn the transformed value.
|
12
|
+
# @yield (object)
|
13
|
+
def initialize( source, &transformer )
|
14
|
+
@transformer = transformer
|
15
|
+
@source = source
|
16
|
+
end
|
17
|
+
|
18
|
+
# Yields each transformed value from the original source to the block.
|
19
|
+
#
|
20
|
+
# @yield (object)
|
21
|
+
# @yieldparam [Object] object
|
22
|
+
# @return [self]
|
23
|
+
def each( &block )
|
24
|
+
transformed.each( &block )
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# (see Enumerable#count)
|
29
|
+
# @return [Integer]
|
30
|
+
def count( *args )
|
31
|
+
if args.any? || block_given?
|
32
|
+
super
|
33
|
+
else
|
34
|
+
source.count
|
35
|
+
end
|
36
|
+
end
|
37
|
+
alias_method :size, :count
|
38
|
+
alias_method :length, :count
|
39
|
+
|
40
|
+
# Get the first transformed value without transforming the entire list.
|
41
|
+
# @overload first(n)
|
42
|
+
# @overload first
|
43
|
+
# @return [Object]
|
44
|
+
def first( *args )
|
45
|
+
if args.any?
|
46
|
+
super
|
47
|
+
else
|
48
|
+
return @first if defined? @first
|
49
|
+
@first = begin
|
50
|
+
value = source.first
|
51
|
+
transformer.call( value ) unless value.nil?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Boolean] true if there are no source values.
|
57
|
+
def empty?
|
58
|
+
source.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param [Integer] n number of source entries to take.
|
62
|
+
# @return [LazyTransform] a new {LazyTransform} taking only `n` source
|
63
|
+
# entries.
|
64
|
+
def take( n )
|
65
|
+
if transformed?
|
66
|
+
super
|
67
|
+
else
|
68
|
+
self.class.new( source.take( n ), &transformer )
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# @param [Integer] n number of source entries to skip.
|
73
|
+
# @return [LazyTransform] a new {LazyTransform} skipping `n` source
|
74
|
+
# entries.
|
75
|
+
def drop( n )
|
76
|
+
if transformed?
|
77
|
+
super
|
78
|
+
else
|
79
|
+
self.class.new( source.drop( n ), &transformer )
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
attr_reader :source
|
86
|
+
attr_reader :transformer
|
87
|
+
|
88
|
+
def transformed
|
89
|
+
@transformed ||= source.map( &transformer )
|
90
|
+
end
|
91
|
+
|
92
|
+
def transformed?
|
93
|
+
!!@transformed
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|