shamu 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (207) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +26 -0
  3. data/.gitignore +2 -1
  4. data/.rubocop.yml +89 -30
  5. data/.yardopts +4 -5
  6. data/Gemfile +24 -12
  7. data/Guardfile +5 -0
  8. data/LABELS.md +22 -0
  9. data/README.md +41 -0
  10. data/Rakefile +12 -0
  11. data/circle.yml +7 -3
  12. data/config.ru +7 -0
  13. data/lib/shamu/active_record.rb +7 -0
  14. data/lib/shamu/attributes/assignment.rb +114 -0
  15. data/lib/shamu/attributes/equality.rb +40 -0
  16. data/lib/shamu/attributes/fluid_assignment.rb +49 -0
  17. data/lib/shamu/attributes/validation.rb +74 -0
  18. data/lib/shamu/attributes.rb +255 -0
  19. data/lib/shamu/auditing/README.md +0 -0
  20. data/lib/shamu/auditing/audit_record.rb +32 -0
  21. data/lib/shamu/auditing/auditing_service.rb +32 -0
  22. data/lib/shamu/auditing/list_scope.rb +22 -0
  23. data/lib/shamu/auditing/logging_auditing_service.rb +16 -0
  24. data/lib/shamu/auditing/support.rb +75 -0
  25. data/lib/shamu/auditing/transaction.rb +58 -0
  26. data/lib/shamu/auditing.rb +12 -0
  27. data/lib/shamu/entities/README.md +1 -0
  28. data/lib/shamu/entities/active_record.rb +123 -0
  29. data/lib/shamu/entities/active_record_soft_destroy.rb +91 -0
  30. data/lib/shamu/entities/entity.rb +196 -0
  31. data/lib/shamu/entities/entity_path.rb +87 -0
  32. data/lib/shamu/entities/identity_cache.rb +64 -0
  33. data/lib/shamu/entities/list.rb +54 -0
  34. data/lib/shamu/entities/list_scope/dates.rb +57 -0
  35. data/lib/shamu/entities/list_scope/paging.rb +51 -0
  36. data/lib/shamu/entities/list_scope/scoped_paging.rb +65 -0
  37. data/lib/shamu/entities/list_scope/sorting.rb +76 -0
  38. data/lib/shamu/entities/list_scope.rb +105 -0
  39. data/lib/shamu/entities/null_entity.rb +88 -0
  40. data/lib/shamu/entities.rb +11 -0
  41. data/lib/shamu/error.rb +23 -5
  42. data/lib/shamu/events/README.md +0 -0
  43. data/lib/shamu/events/active_record/channel.rb +36 -0
  44. data/lib/shamu/events/active_record/message.rb +52 -0
  45. data/lib/shamu/events/active_record/migration.rb +49 -0
  46. data/lib/shamu/events/active_record/runner.rb +28 -0
  47. data/lib/shamu/events/active_record/service.rb +174 -0
  48. data/lib/shamu/events/active_record.rb +13 -0
  49. data/lib/shamu/events/channel_stats.rb +23 -0
  50. data/lib/shamu/events/error.rb +24 -0
  51. data/lib/shamu/events/events_service.rb +136 -0
  52. data/lib/shamu/events/in_memory/async_service.rb +48 -0
  53. data/lib/shamu/events/in_memory/service.rb +97 -0
  54. data/lib/shamu/events/in_memory.rb +10 -0
  55. data/lib/shamu/events/message.rb +38 -0
  56. data/lib/shamu/events/support.rb +60 -0
  57. data/lib/shamu/events.rb +12 -0
  58. data/lib/shamu/features/README.md +0 -0
  59. data/lib/shamu/features/conditions/condition.rb +39 -0
  60. data/lib/shamu/features/conditions/env.rb +37 -0
  61. data/lib/shamu/features/conditions/hosts.rb +25 -0
  62. data/lib/shamu/features/conditions/matching.rb +16 -0
  63. data/lib/shamu/features/conditions/not_matching.rb +16 -0
  64. data/lib/shamu/features/conditions/percentage.rb +44 -0
  65. data/lib/shamu/features/conditions/proc.rb +54 -0
  66. data/lib/shamu/features/conditions/roles.rb +23 -0
  67. data/lib/shamu/features/conditions/schedule_at.rb +27 -0
  68. data/lib/shamu/features/conditions.rb +18 -0
  69. data/lib/shamu/features/config_service.rb +10 -0
  70. data/lib/shamu/features/context.rb +80 -0
  71. data/lib/shamu/features/env_store.rb +88 -0
  72. data/lib/shamu/features/errors.rb +29 -0
  73. data/lib/shamu/features/features_service.rb +168 -0
  74. data/lib/shamu/features/list_scope.rb +30 -0
  75. data/lib/shamu/features/selector.rb +50 -0
  76. data/lib/shamu/features/support.rb +51 -0
  77. data/lib/shamu/features/toggle.rb +149 -0
  78. data/lib/shamu/features/toggle_codec.rb +69 -0
  79. data/lib/shamu/features.rb +16 -0
  80. data/lib/shamu/locale/en.yml +22 -2
  81. data/lib/shamu/logger.rb +13 -0
  82. data/lib/shamu/rack/README.md +0 -0
  83. data/lib/shamu/rack/cookies.rb +115 -0
  84. data/lib/shamu/rack/cookies_middleware.rb +26 -0
  85. data/lib/shamu/rack/query_params.rb +41 -0
  86. data/lib/shamu/rack/query_params_middleware.rb +24 -0
  87. data/lib/shamu/rack.rb +12 -0
  88. data/lib/shamu/rails/controller.rb +131 -0
  89. data/lib/shamu/rails/entity.rb +168 -0
  90. data/lib/shamu/rails/features.rb +13 -0
  91. data/lib/shamu/rails/railtie.rb +30 -0
  92. data/lib/shamu/rails.rb +10 -0
  93. data/lib/shamu/rspec/matchers.rb +44 -0
  94. data/lib/shamu/rspec.rb +1 -0
  95. data/lib/shamu/security/README.md +0 -0
  96. data/lib/shamu/security/active_record_policy.rb +106 -0
  97. data/lib/shamu/security/error.rb +65 -0
  98. data/lib/shamu/security/hashed_value.rb +71 -0
  99. data/lib/shamu/security/no_policy.rb +15 -0
  100. data/lib/shamu/security/policy.rb +289 -0
  101. data/lib/shamu/security/policy_refinement.rb +50 -0
  102. data/lib/shamu/security/policy_rule.rb +59 -0
  103. data/lib/shamu/security/principal.rb +72 -0
  104. data/lib/shamu/security/roles.rb +62 -0
  105. data/lib/shamu/security/roles_service.rb +30 -0
  106. data/lib/shamu/security/support.rb +83 -0
  107. data/lib/shamu/security.rb +43 -0
  108. data/lib/shamu/services/README.md +2 -0
  109. data/lib/shamu/services/active_record.rb +58 -0
  110. data/lib/shamu/services/active_record_crud.rb +378 -0
  111. data/lib/shamu/services/error.rb +24 -0
  112. data/lib/shamu/services/lazy_association.rb +31 -0
  113. data/lib/shamu/services/lazy_transform.rb +97 -0
  114. data/lib/shamu/services/request.rb +122 -0
  115. data/lib/shamu/services/request_support.rb +124 -0
  116. data/lib/shamu/services/result.rb +75 -0
  117. data/lib/shamu/services/service.rb +355 -0
  118. data/lib/shamu/services.rb +12 -0
  119. data/lib/shamu/sessions/README.md +2 -0
  120. data/lib/shamu/sessions/cookie_store.rb +79 -0
  121. data/lib/shamu/sessions/session_store.rb +42 -0
  122. data/lib/shamu/sessions.rb +8 -0
  123. data/lib/shamu/to_bool_extension.rb +57 -0
  124. data/lib/shamu/to_model_id_extension.rb +50 -0
  125. data/lib/shamu/version.rb +10 -4
  126. data/lib/shamu.rb +18 -6
  127. data/shamu.gemspec +21 -10
  128. data/spec/internal/README.md +4 -0
  129. data/spec/internal/config/database.yml +3 -0
  130. data/spec/internal/config/routes.rb +3 -0
  131. data/spec/internal/db/schema.rb +3 -0
  132. data/spec/internal/log/.gitignore +1 -0
  133. data/spec/internal/public/favicon.ico +0 -0
  134. data/spec/lib/shamu/active_record_support.rb +32 -0
  135. data/spec/lib/shamu/attributes/assignment_spec.rb +129 -0
  136. data/spec/lib/shamu/attributes/equality_spec.rb +63 -0
  137. data/spec/lib/shamu/attributes/fluid_assignment_spec.rb +31 -0
  138. data/spec/lib/shamu/attributes/validation_spec.rb +53 -0
  139. data/spec/lib/shamu/attributes_spec.rb +331 -0
  140. data/spec/lib/shamu/auditing/logging_auditing_service_spec.rb +18 -0
  141. data/spec/lib/shamu/auditing/support_spec.rb +41 -0
  142. data/spec/lib/shamu/entities/active_record_soft_destroy_spec.rb +82 -0
  143. data/spec/lib/shamu/entities/active_record_spec.rb +66 -0
  144. data/spec/lib/shamu/entities/entity_path_spec.rb +40 -0
  145. data/spec/lib/shamu/entities/entity_spec.rb +56 -0
  146. data/spec/lib/shamu/entities/identity_cache_spec.rb +69 -0
  147. data/spec/lib/shamu/entities/list_scope/dates_spec.rb +47 -0
  148. data/spec/lib/shamu/entities/list_scope/paging_spec.rb +41 -0
  149. data/spec/lib/shamu/entities/list_scope/scoped_paging_spec.rb +40 -0
  150. data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +59 -0
  151. data/spec/lib/shamu/entities/list_scope_spec.rb +127 -0
  152. data/spec/lib/shamu/entities/list_spec.rb +60 -0
  153. data/spec/lib/shamu/entities/null_entity_spec.rb +94 -0
  154. data/spec/lib/shamu/events/active_record/migration_spec.rb +11 -0
  155. data/spec/lib/shamu/events/active_record/service_spec.rb +139 -0
  156. data/spec/lib/shamu/events/events_service_spec.rb +57 -0
  157. data/spec/lib/shamu/events/in_memory/async_service_spec.rb +37 -0
  158. data/spec/lib/shamu/events/in_memory/service_spec.rb +36 -0
  159. data/spec/lib/shamu/events/message_spec.rb +7 -0
  160. data/spec/lib/shamu/events/support_spec.rb +44 -0
  161. data/spec/lib/shamu/features/conditions/condition_spec.rb +8 -0
  162. data/spec/lib/shamu/features/conditions/env_spec.rb +29 -0
  163. data/spec/lib/shamu/features/conditions/hosts_spec.rb +21 -0
  164. data/spec/lib/shamu/features/conditions/matching_spec.rb +23 -0
  165. data/spec/lib/shamu/features/conditions/percentage_spec.rb +71 -0
  166. data/spec/lib/shamu/features/conditions/proc_spec.rb +28 -0
  167. data/spec/lib/shamu/features/env_store_spec.rb +48 -0
  168. data/spec/lib/shamu/features/features.yml +34 -0
  169. data/spec/lib/shamu/features/features_service_spec.rb +109 -0
  170. data/spec/lib/shamu/features/secondary.yml +5 -0
  171. data/spec/lib/shamu/features/selector_spec.rb +17 -0
  172. data/spec/lib/shamu/features/support_spec.rb +45 -0
  173. data/spec/lib/shamu/features/toggle_codec_spec.rb +28 -0
  174. data/spec/lib/shamu/features/toggle_spec.rb +42 -0
  175. data/spec/lib/shamu/rack/cookies_middleware_spec.rb +33 -0
  176. data/spec/lib/shamu/rack/cookies_spec.rb +43 -0
  177. data/spec/lib/shamu/rack/query_params_middleware_spec.rb +33 -0
  178. data/spec/lib/shamu/rack/query_params_spec.rb +23 -0
  179. data/spec/lib/shamu/rails/controller_spec.rb +74 -0
  180. data/spec/lib/shamu/rails/entity_spec.rb +150 -0
  181. data/spec/lib/shamu/rails/features.yml +13 -0
  182. data/spec/lib/shamu/rails/features_spec.rb +45 -0
  183. data/spec/lib/shamu/security/active_record_policy_spec.rb +38 -0
  184. data/spec/lib/shamu/security/hashed_value_spec.rb +41 -0
  185. data/spec/lib/shamu/security/policy_refinement_spec.rb +61 -0
  186. data/spec/lib/shamu/security/policy_rule_spec.rb +60 -0
  187. data/spec/lib/shamu/security/policy_spec.rb +158 -0
  188. data/spec/lib/shamu/security/roles_spec.rb +46 -0
  189. data/spec/lib/shamu/services/active_record_crud_spec.rb +460 -0
  190. data/spec/lib/shamu/services/active_record_spec.rb +92 -0
  191. data/spec/lib/shamu/services/lazy_association_spec.rb +31 -0
  192. data/spec/lib/shamu/services/lazy_transform_spec.rb +96 -0
  193. data/spec/lib/shamu/services/request_spec.rb +58 -0
  194. data/spec/lib/shamu/services/request_support_spec.rb +129 -0
  195. data/spec/lib/shamu/services/result_spec.rb +37 -0
  196. data/spec/lib/shamu/services/service_spec.rb +307 -0
  197. data/spec/lib/shamu/sessions/cookie_store_spec.rb +44 -0
  198. data/spec/lib/shamu/to_bool_extension_spec.rb +67 -0
  199. data/spec/lib/shamu/to_model_id_extension_spec.rb +54 -0
  200. data/spec/rails_helper.rb +13 -0
  201. data/spec/spec_helper.rb +17 -12
  202. data/spec/support/active_record.rb +17 -0
  203. data/spec/support/database.rb +14 -0
  204. data/spec/support/logger.rb +0 -0
  205. metadata +383 -9
  206. data/spec/lib/shamu_spec.rb +0 -5
  207. /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