shamu 0.0.9 → 0.0.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +5 -3
- data/bin/rake +17 -0
- data/bin/rspec +17 -0
- data/lib/shamu/attributes.rb +3 -1
- data/lib/shamu/events/active_record/migration.rb +6 -6
- data/lib/shamu/json_api/builder_methods/identifier.rb +18 -4
- data/lib/shamu/json_api/context.rb +3 -1
- data/lib/shamu/json_api/error.rb +7 -1
- data/lib/shamu/json_api/presenter.rb +23 -1
- data/lib/shamu/json_api/rails/controller.rb +195 -62
- data/lib/shamu/locale/en.yml +3 -1
- data/lib/shamu/rails/controller.rb +5 -2
- data/lib/shamu/rails/entity.rb +29 -15
- data/lib/shamu/rails/railtie.rb +12 -7
- data/lib/shamu/services/active_record.rb +2 -2
- data/lib/shamu/services/active_record_crud.rb +36 -38
- data/lib/shamu/services/error.rb +11 -1
- data/lib/shamu/services/lazy_transform.rb +9 -4
- data/lib/shamu/services/request_support.rb +5 -2
- data/lib/shamu/services/result.rb +40 -7
- data/lib/shamu/services/service.rb +17 -8
- data/lib/shamu/services/service_call_failed_error.rb +4 -0
- data/lib/shamu/version.rb +2 -2
- data/shamu.gemspec +4 -4
- data/spec/lib/shamu/json_api/builder_methods/identifier_spec.rb +45 -0
- data/spec/lib/shamu/json_api/rails/controller_spec.rb +141 -7
- data/spec/lib/shamu/json_api/rails/responder_spec.rb +9 -9
- data/spec/lib/shamu/rails/controller_spec.rb +4 -4
- data/spec/lib/shamu/rails/entity_spec.rb +34 -16
- data/spec/lib/shamu/rails/features_spec.rb +6 -6
- data/spec/lib/shamu/services/active_record_crud_spec.rb +12 -7
- data/spec/lib/shamu/services/lazy_transform_spec.rb +23 -14
- data/spec/lib/shamu/services/request_support_spec.rb +15 -1
- data/spec/lib/shamu/services/result_spec.rb +37 -1
- data/spec/lib/shamu/services/service_spec.rb +25 -14
- data/spec/spec_helper.rb +1 -1
- metadata +23 -17
data/lib/shamu/rails/railtie.rb
CHANGED
@@ -18,9 +18,14 @@ module Shamu
|
|
18
18
|
config.shamu.json_api.default_url_options = {}
|
19
19
|
|
20
20
|
if defined? ::ActionController
|
21
|
-
::ActionController::Base
|
22
|
-
::ActionController::
|
23
|
-
|
21
|
+
controller_classes = [ ::ActionController::Base ]
|
22
|
+
controller_classes << ::ActionController::API if defined? ::ActionController::API
|
23
|
+
|
24
|
+
controller_classes.each do |klass|
|
25
|
+
klass.send :include, Shamu::Rails::Controller
|
26
|
+
klass.send :include, Shamu::Rails::Entity
|
27
|
+
klass.send :include, Shamu::Rails::Features
|
28
|
+
end
|
24
29
|
|
25
30
|
Mime::Type.register Shamu::JsonApi::MIME_TYPE, :json_api
|
26
31
|
|
@@ -32,11 +37,11 @@ module Shamu
|
|
32
37
|
end
|
33
38
|
|
34
39
|
initializer "shamu.insert_middleware" do |app|
|
35
|
-
app.config.middleware.use
|
36
|
-
app.config.middleware.use
|
37
|
-
app.config.middleware.use
|
40
|
+
app.config.middleware.use Scorpion::Rack::Middleware
|
41
|
+
app.config.middleware.use Shamu::Rack::CookiesMiddleware
|
42
|
+
app.config.middleware.use Shamu::Rack::QueryParamsMiddleware
|
38
43
|
end
|
39
44
|
|
40
45
|
end
|
41
46
|
end
|
42
|
-
end
|
47
|
+
end
|
@@ -49,10 +49,10 @@ module Shamu
|
|
49
49
|
if relation.respond_to?( :by_list_scope )
|
50
50
|
relation.by_list_scope( list_scope )
|
51
51
|
else
|
52
|
-
fail "Can't scope a #{ relation.klass }. Add `scope :by_list_scope, ->(list_scope) { ... }` or
|
52
|
+
fail "Can't scope a #{ relation.klass }. Add `scope :by_list_scope, ->(list_scope) { ... }` or extend Shamu::Entities::ActiveRecord." # rubocop:disable Metrics/LineLength
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
56
|
end
|
57
57
|
end
|
58
|
-
end
|
58
|
+
end
|
@@ -29,12 +29,14 @@ module Shamu
|
|
29
29
|
# destroy
|
30
30
|
#
|
31
31
|
# # Build the entity class from the given record.
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
32
|
+
# build_entities do |records|
|
33
|
+
# records.map do |record|
|
34
|
+
# parent = lookup_association( record.parent_id, self ) do
|
35
|
+
# records.pluck( :parent_id )
|
36
|
+
# end
|
36
37
|
#
|
37
|
-
#
|
38
|
+
# scorpion.fetch UserEntity, { parent: parent }, {}
|
39
|
+
# end
|
38
40
|
# end
|
39
41
|
# end
|
40
42
|
module ActiveRecordCrud
|
@@ -97,17 +99,16 @@ module Shamu
|
|
97
99
|
# Creates instance and class level methods `entity_class` and
|
98
100
|
# `model_class`.
|
99
101
|
#
|
100
|
-
# See {.
|
102
|
+
# See {.build_entities} for build_entities block details.
|
101
103
|
#
|
102
104
|
# @param [Class] entity_class the {Entities::Entity} class that will be
|
103
105
|
# returned by finders and mutator methods.
|
104
106
|
# @param [Class] model_class the {ActiveRecord::Base} model
|
105
107
|
# @param [Array<Symbol>] methods the {DSL_METHODS DSL methods} to
|
106
108
|
# include (eg :create, :update, :find, etc.)
|
107
|
-
# @yield (
|
108
|
-
# @yieldparam [ActiveRecord::
|
109
|
-
#
|
110
|
-
# @yieldparam [ActiveRecord::Relation] records that are all being built
|
109
|
+
# @yield ( records )
|
110
|
+
# @yieldparam [ActiveRecord::Relation] records to be mapped to an
|
111
|
+
# entity.
|
111
112
|
# @yieldreturn [Entities::Entity] the entity projection for the given
|
112
113
|
# record.
|
113
114
|
# @return [void]
|
@@ -122,7 +123,7 @@ module Shamu
|
|
122
123
|
send method
|
123
124
|
end
|
124
125
|
|
125
|
-
|
126
|
+
build_entities( &block )
|
126
127
|
end
|
127
128
|
|
128
129
|
# @return [Class] the {Entities::Entity} class that the service will
|
@@ -172,7 +173,11 @@ module Shamu
|
|
172
173
|
# @return [void]
|
173
174
|
def change( method = :update, &block )
|
174
175
|
define_method method do |id, params = nil|
|
175
|
-
|
176
|
+
klass = request_class( method )
|
177
|
+
|
178
|
+
id, params = id.id, id if id.is_a?( klass ) && !params
|
179
|
+
|
180
|
+
with_request params, klass do |request|
|
176
181
|
record = model_class.find( id.to_model_id )
|
177
182
|
authorize! method, build_entity( record ), request
|
178
183
|
|
@@ -247,7 +252,7 @@ module Shamu
|
|
247
252
|
define_method :find do |id|
|
248
253
|
wrap_not_found do
|
249
254
|
record = yield( id )
|
250
|
-
authorize! :read,
|
255
|
+
authorize! :read, build_entities( record )
|
251
256
|
end
|
252
257
|
end
|
253
258
|
else
|
@@ -258,8 +263,8 @@ module Shamu
|
|
258
263
|
end
|
259
264
|
|
260
265
|
# Define a `lookup( *ids )` method that takes a list of entity ids to
|
261
|
-
# find. Calls {#
|
262
|
-
# {Entities::NullEntity} for ids that were not found.
|
266
|
+
# find. Calls {#build_entities} to map all found records to entities,
|
267
|
+
# or constructs a {Entities::NullEntity} for ids that were not found.
|
263
268
|
#
|
264
269
|
# @param [ActiveRecord::Relation] default_scope to use when finding
|
265
270
|
# records.
|
@@ -317,11 +322,8 @@ module Shamu
|
|
317
322
|
end
|
318
323
|
end
|
319
324
|
|
320
|
-
# Define a private `
|
321
|
-
# constructs an {Entities::Entity}
|
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
|
+
# Define a private `build_entities( records )` method that
|
326
|
+
# constructs an {Entities::Entity} for each of the given `records`.
|
325
327
|
#
|
326
328
|
# If no block is given, creates a simple builder that simply constructs
|
327
329
|
# an instance of the {.entity_class} passing `record: record` to the
|
@@ -329,28 +331,24 @@ module Shamu
|
|
329
331
|
#
|
330
332
|
# See {Service#lookup_association} for details on association caching.
|
331
333
|
#
|
332
|
-
# @yield (
|
333
|
-
# @yieldparam [ActiveRecord::
|
334
|
-
#
|
335
|
-
# @
|
336
|
-
# @yieldreturn [Entities::Entity] the entity projection for the given
|
337
|
-
# record.
|
334
|
+
# @yield ( records )
|
335
|
+
# @yieldparam [ActiveRecord::Relation] records to be mapped to
|
336
|
+
# entities.
|
337
|
+
# @yieldreturn [Array<Entities::Entity>] the projected entities.
|
338
338
|
# @return [void]
|
339
|
-
def
|
339
|
+
def build_entities( &block )
|
340
340
|
if block_given?
|
341
|
-
define_method :
|
341
|
+
define_method :build_entities, &block
|
342
342
|
else
|
343
|
-
define_method :
|
344
|
-
|
343
|
+
define_method :build_entities do |records|
|
344
|
+
records.map do |record|
|
345
|
+
entity = scorpion.fetch( entity_class, { record: record }, {} )
|
346
|
+
authorize! :read, entity
|
347
|
+
end
|
345
348
|
end
|
346
349
|
end
|
347
350
|
|
348
|
-
|
349
|
-
authorize! :read, build_entity_instance( record, records )
|
350
|
-
end
|
351
|
-
|
352
|
-
private :build_entity
|
353
|
-
private :build_entity_instance
|
351
|
+
private :build_entities
|
354
352
|
end
|
355
353
|
|
356
354
|
private
|
@@ -361,7 +359,7 @@ module Shamu
|
|
361
359
|
|
362
360
|
def inferred_resource_name
|
363
361
|
inferred = name || "Resource"
|
364
|
-
inferred.split( "::" ).last.sub /Service/, ""
|
362
|
+
inferred.split( "::" ).last.sub( /Service/, "" )
|
365
363
|
end
|
366
364
|
|
367
365
|
def inferred_namespace
|
@@ -375,4 +373,4 @@ module Shamu
|
|
375
373
|
|
376
374
|
end
|
377
375
|
end
|
378
|
-
end
|
376
|
+
end
|
data/lib/shamu/services/error.rb
CHANGED
@@ -20,5 +20,15 @@ module Shamu
|
|
20
20
|
super
|
21
21
|
end
|
22
22
|
end
|
23
|
+
|
24
|
+
class ServiceRequestFailedError < Error
|
25
|
+
attr_reader :result
|
26
|
+
|
27
|
+
def initialize( result )
|
28
|
+
@result = result
|
29
|
+
|
30
|
+
super translate( :service_request_failed, errors: result.errors.full_messages.join( ', ' ) )
|
31
|
+
end
|
32
|
+
end
|
23
33
|
end
|
24
|
-
end
|
34
|
+
end
|
@@ -7,8 +7,8 @@ module Shamu
|
|
7
7
|
include Enumerable
|
8
8
|
|
9
9
|
# @param [Enumerable] source enumerable to transform.
|
10
|
-
# @yieldparam [Object]
|
11
|
-
# @yieldreturn the transformed
|
10
|
+
# @yieldparam [Array<Object>] objects the original values.
|
11
|
+
# @yieldreturn the transformed values.
|
12
12
|
# @yield (object)
|
13
13
|
def initialize( source, &transformer )
|
14
14
|
@transformer = transformer
|
@@ -48,7 +48,7 @@ module Shamu
|
|
48
48
|
return @first if defined? @first
|
49
49
|
@first = begin
|
50
50
|
value = source.first
|
51
|
-
transformer.call( value ) unless value.nil?
|
51
|
+
raise_if_not_transformed( transformer.call( [ value ] ) ).first unless value.nil?
|
52
52
|
end
|
53
53
|
end
|
54
54
|
end
|
@@ -86,12 +86,17 @@ module Shamu
|
|
86
86
|
attr_reader :transformer
|
87
87
|
|
88
88
|
def transformed
|
89
|
-
@transformed ||=
|
89
|
+
@transformed ||= raise_if_not_transformed( transformer.call( source ) )
|
90
90
|
end
|
91
91
|
|
92
92
|
def transformed?
|
93
93
|
!!@transformed
|
94
94
|
end
|
95
|
+
|
96
|
+
def raise_if_not_transformed( transformed )
|
97
|
+
raise "Block to LazyTransform did not return an enumerable value" unless transformed.is_a? Enumerable
|
98
|
+
transformed
|
99
|
+
end
|
95
100
|
end
|
96
101
|
end
|
97
102
|
end
|
@@ -117,7 +117,10 @@ module Shamu
|
|
117
117
|
private
|
118
118
|
|
119
119
|
def request_class_namespace
|
120
|
-
@request_class_namespace ||= ( name || "" ).sub( /(Service)?$/, "
|
120
|
+
@request_class_namespace ||= ( name || "" ).sub( /(Service)?$/, "" )
|
121
|
+
.singularize
|
122
|
+
.concat( 'Request' )
|
123
|
+
.constantize
|
121
124
|
rescue NameError
|
122
125
|
self
|
123
126
|
end
|
@@ -145,4 +148,4 @@ module Shamu
|
|
145
148
|
end
|
146
149
|
end
|
147
150
|
end
|
148
|
-
end
|
151
|
+
end
|
@@ -7,6 +7,7 @@ module Shamu
|
|
7
7
|
class Result
|
8
8
|
extend ActiveModel::Translation
|
9
9
|
|
10
|
+
|
10
11
|
# ============================================================================
|
11
12
|
# @!group Attributes
|
12
13
|
#
|
@@ -17,17 +18,41 @@ module Shamu
|
|
17
18
|
# @return [Entities::Entity] the entity created or changed by the request.
|
18
19
|
attr_reader :entity
|
19
20
|
|
21
|
+
# @return [Entities::Entity] the entity created or changed by the request.
|
22
|
+
# @raise [ServiceRequestFailedError] if the result was not valid.
|
23
|
+
def entity!
|
24
|
+
valid!
|
25
|
+
entity
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Array<Object>] the values returned by the service call.
|
29
|
+
attr_reader :values
|
30
|
+
|
31
|
+
# @return [Object] the primary return value of the service call.
|
32
|
+
attr_reader :value
|
33
|
+
|
34
|
+
# @return [Object] the primary return value of the service call.
|
35
|
+
# @raise [ServiceRequestFailedError] if the result was not valid.
|
36
|
+
def value!
|
37
|
+
valid!
|
38
|
+
value
|
39
|
+
end
|
40
|
+
|
20
41
|
#
|
21
42
|
# @!endgroup Attributes
|
22
43
|
|
23
|
-
# @param [Array
|
24
|
-
#
|
44
|
+
# @param [Array<Object,#errors>] values an array of objects that
|
45
|
+
# represent the result of the service call. If they respond to `#errors`
|
46
|
+
# those errors will be included in {#errors} on the result object itself.
|
25
47
|
# @param [Request] request submitted to the service. If :not_set, uses
|
26
|
-
# the first {Request} object found in the `
|
48
|
+
# the first {Request} object found in the `values`.
|
27
49
|
# @param [Entities::Entity] entity submitted to the service. If :not_set,
|
28
|
-
# uses the first {
|
29
|
-
def initialize( *
|
30
|
-
|
50
|
+
# uses the first {Entity} object found in the `values`.
|
51
|
+
def initialize( *values, request: :not_set, entity: :not_set )
|
52
|
+
@values = values
|
53
|
+
@value = values.first
|
54
|
+
|
55
|
+
values.each do |source|
|
31
56
|
request = source if request == :not_set && source.is_a?( Services::Request )
|
32
57
|
entity = source if entity == :not_set && source.is_a?( Entities::Entity )
|
33
58
|
|
@@ -61,8 +86,16 @@ module Shamu
|
|
61
86
|
( request && request.model_name ) || ( entity && entity.model_name ) || ActiveModel::Name.new( self, nil, "Request" ) # rubocop:disable Metrics/LineLength
|
62
87
|
end
|
63
88
|
|
89
|
+
# @return [self]
|
90
|
+
# @raise [ServiceRequestFailedError] if the result was not valid.
|
91
|
+
def valid!
|
92
|
+
raise ServiceRequestFailedError.new( self ) unless valid?
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
64
96
|
private
|
65
97
|
|
98
|
+
|
66
99
|
def append_error_source( source )
|
67
100
|
return unless source.respond_to?( :errors )
|
68
101
|
|
@@ -72,4 +105,4 @@ module Shamu
|
|
72
105
|
end
|
73
106
|
end
|
74
107
|
end
|
75
|
-
end
|
108
|
+
end
|
@@ -78,6 +78,16 @@ module Shamu
|
|
78
78
|
|
79
79
|
private
|
80
80
|
|
81
|
+
# Maps a single record to an entity. Requires a `build_entities` method
|
82
|
+
# that maps an enumerable set of records to entities.
|
83
|
+
#
|
84
|
+
# @param [Object] record to map.
|
85
|
+
# @return [Entity] the mapped entity.
|
86
|
+
def build_entity( record )
|
87
|
+
mapped = build_entities( [ record ] )
|
88
|
+
mapped && mapped.first
|
89
|
+
end
|
90
|
+
|
81
91
|
# @!visibility public
|
82
92
|
# Takes a raw enumerable list of records and transforms them to a proper
|
83
93
|
# {Entities::List}.
|
@@ -85,10 +95,9 @@ module Shamu
|
|
85
95
|
# As simple as the method is, it also serves as a hook for mixins to add
|
86
96
|
# additional behavior when processing lists.
|
87
97
|
#
|
88
|
-
# If a block is not provided, looks for a method `
|
89
|
-
# records
|
90
|
-
#
|
91
|
-
# being transformed.
|
98
|
+
# If a block is not provided, looks for a method `build_entities(
|
99
|
+
# records )` that maps a set of records to their corresponding
|
100
|
+
# entities.
|
92
101
|
#
|
93
102
|
# @param [Enumerable] records the raw list of records.
|
94
103
|
# @yield (record)
|
@@ -99,8 +108,8 @@ module Shamu
|
|
99
108
|
def entity_list( records, &transformer )
|
100
109
|
return Entities::List.new [] unless records
|
101
110
|
unless transformer
|
102
|
-
fail "Either provide a block or add a private method `def
|
103
|
-
transformer ||=
|
111
|
+
fail "Either provide a block or add a private method `def build_entities( records )` to #{ self.class.name }." unless respond_to?( :build_entities, true ) # rubocop:disable Metrics/LineLength
|
112
|
+
transformer ||= method( :build_entities )
|
104
113
|
end
|
105
114
|
|
106
115
|
Entities::List.new LazyTransform.new( records, &transformer )
|
@@ -325,7 +334,7 @@ module Shamu
|
|
325
334
|
|
326
335
|
# @!visibility public
|
327
336
|
#
|
328
|
-
# @overload result( *
|
337
|
+
# @overload result( *values, request: nil, entity: nil )
|
329
338
|
# @param (see Result#initialize)
|
330
339
|
# @return [Result]
|
331
340
|
def result( *args )
|
@@ -354,4 +363,4 @@ module Shamu
|
|
354
363
|
end
|
355
364
|
end
|
356
365
|
end
|
357
|
-
end
|
366
|
+
end
|
data/lib/shamu/version.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Shamu
|
3
3
|
# The primary version number
|
4
|
-
VERSION_NUMBER = "0.0.
|
4
|
+
VERSION_NUMBER = "0.0.11".freeze
|
5
5
|
|
6
6
|
# Version suffix such as 'beta' or 'alpha'
|
7
7
|
VERSION_SUFFIX = "".freeze
|
8
8
|
|
9
9
|
# Published version number
|
10
10
|
VERSION = "#{ VERSION_NUMBER }#{ VERSION_SUFFIX }".freeze
|
11
|
-
end
|
11
|
+
end
|
data/shamu.gemspec
CHANGED
@@ -20,11 +20,11 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.required_ruby_version = ">= 2.2.0"
|
21
21
|
|
22
22
|
|
23
|
-
spec.add_dependency "activemodel", "
|
24
|
-
spec.add_dependency "activesupport", "
|
25
|
-
spec.add_dependency "scorpion-ioc", "~> 0.
|
23
|
+
spec.add_dependency "activemodel", ">= 5.0"
|
24
|
+
spec.add_dependency "activesupport", ">= 5.0"
|
25
|
+
spec.add_dependency "scorpion-ioc", "~> 0.6"
|
26
26
|
spec.add_dependency "multi_json", "~> 1.11.2"
|
27
|
-
spec.add_dependency "rack", "
|
27
|
+
spec.add_dependency "rack", ">= 1"
|
28
28
|
spec.add_dependency "listen", "~> 3"
|
29
29
|
spec.add_dependency "crc32", "~> 1"
|
30
30
|
spec.add_dependency "loofah", "~> 2"
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module BuilderMethodsIdentifierSpec
|
4
|
+
class Builder
|
5
|
+
include Shamu::JsonApi::BuilderMethods::Identifier
|
6
|
+
|
7
|
+
attr_reader :output
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@output = {}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe Shamu::JsonApi::BuilderMethods::Identifier do
|
16
|
+
let( :builder ) { BuilderMethodsIdentifierSpec::Builder.new }
|
17
|
+
|
18
|
+
it "it uses #json_type if available" do
|
19
|
+
type = double( json_type: "magic" )
|
20
|
+
|
21
|
+
builder.identifier( type )
|
22
|
+
expect( builder.output[ :type ] ).to eq "magic"
|
23
|
+
end
|
24
|
+
|
25
|
+
it "it uses #model_name if available" do
|
26
|
+
type = double( model_name: double( element: "record" ) )
|
27
|
+
|
28
|
+
builder.identifier( type )
|
29
|
+
expect( builder.output[ :type ] ).to eq "record"
|
30
|
+
end
|
31
|
+
|
32
|
+
it "it uses class name as last resort" do
|
33
|
+
builder.identifier( BuilderMethodsIdentifierSpec::Builder )
|
34
|
+
|
35
|
+
expect( builder.output[ :type ] ).to eq "builder"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "gets ID from type ifid not provided" do
|
39
|
+
resource = double( id: 56, json_type: "double" )
|
40
|
+
|
41
|
+
builder.identifier resource
|
42
|
+
expect( builder.output[ :id ] ).to eq 56
|
43
|
+
expect( builder.output[ :type ] ).to eq "double"
|
44
|
+
end
|
45
|
+
end
|