shamu 0.0.9 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +5 -3
  4. data/bin/rake +17 -0
  5. data/bin/rspec +17 -0
  6. data/lib/shamu/attributes.rb +3 -1
  7. data/lib/shamu/events/active_record/migration.rb +6 -6
  8. data/lib/shamu/json_api/builder_methods/identifier.rb +18 -4
  9. data/lib/shamu/json_api/context.rb +3 -1
  10. data/lib/shamu/json_api/error.rb +7 -1
  11. data/lib/shamu/json_api/presenter.rb +23 -1
  12. data/lib/shamu/json_api/rails/controller.rb +195 -62
  13. data/lib/shamu/locale/en.yml +3 -1
  14. data/lib/shamu/rails/controller.rb +5 -2
  15. data/lib/shamu/rails/entity.rb +29 -15
  16. data/lib/shamu/rails/railtie.rb +12 -7
  17. data/lib/shamu/services/active_record.rb +2 -2
  18. data/lib/shamu/services/active_record_crud.rb +36 -38
  19. data/lib/shamu/services/error.rb +11 -1
  20. data/lib/shamu/services/lazy_transform.rb +9 -4
  21. data/lib/shamu/services/request_support.rb +5 -2
  22. data/lib/shamu/services/result.rb +40 -7
  23. data/lib/shamu/services/service.rb +17 -8
  24. data/lib/shamu/services/service_call_failed_error.rb +4 -0
  25. data/lib/shamu/version.rb +2 -2
  26. data/shamu.gemspec +4 -4
  27. data/spec/lib/shamu/json_api/builder_methods/identifier_spec.rb +45 -0
  28. data/spec/lib/shamu/json_api/rails/controller_spec.rb +141 -7
  29. data/spec/lib/shamu/json_api/rails/responder_spec.rb +9 -9
  30. data/spec/lib/shamu/rails/controller_spec.rb +4 -4
  31. data/spec/lib/shamu/rails/entity_spec.rb +34 -16
  32. data/spec/lib/shamu/rails/features_spec.rb +6 -6
  33. data/spec/lib/shamu/services/active_record_crud_spec.rb +12 -7
  34. data/spec/lib/shamu/services/lazy_transform_spec.rb +23 -14
  35. data/spec/lib/shamu/services/request_support_spec.rb +15 -1
  36. data/spec/lib/shamu/services/result_spec.rb +37 -1
  37. data/spec/lib/shamu/services/service_spec.rb +25 -14
  38. data/spec/spec_helper.rb +1 -1
  39. metadata +23 -17
@@ -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.send :include, Shamu::Rails::Controller
22
- ::ActionController::Base.send :include, Shamu::Rails::Entity
23
- ::ActionController::Base.send :include, Shamu::Rails::Features
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 "Scorpion::Rack::Middleware"
36
- app.config.middleware.use "Shamu::Rack::CookiesMiddleware"
37
- app.config.middleware.use "Shamu::Rack::QueryParamsMiddleware"
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 include Shamu::Entities::ActiveRecord." # rubocop:disable Metrics/LineLength
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
- # build_entity do |record, records = nil|
33
- # parent = lookup_association( record.parent_id, self ) do
34
- # records.pluck( :parent_id ) if records
35
- # end
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
- # scorpion.fetch UserEntity, { parent: parent }, {}
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 {.build_entity} for build_entity block details.
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 (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
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
- build_entity( &block )
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
- with_request params, request_class( method ) do |request|
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, build_entity( record )
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 {#build_entity} for each found record, or constructs a
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 `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
+ # 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 (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.
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 build_entity( &block )
339
+ def build_entities( &block )
340
340
  if block_given?
341
- define_method :build_entity_instance, &block
341
+ define_method :build_entities, &block
342
342
  else
343
- define_method :build_entity_instance do |record, _ = nil|
344
- scorpion.fetch( entity_class, { record: record }, {} )
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
- 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
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
@@ -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] object the original value.
11
- # @yieldreturn the transformed value.
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 ||= source.map( &transformer )
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)?$/, "Request" ).constantize
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<#errors>] validation_sources an array of objects that respond to `#errors`
24
- # returning a {ActiveModel::Errors} object.
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 `validation_sources`.
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 {Request} object found in the `validation_sources`.
29
- def initialize( *validation_sources, request: :not_set, entity: :not_set )
30
- validation_sources.each do |source|
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 `build_entity(record,
89
- # records=nil)` where `record` is the individual record to be
90
- # transformed and `records` is the original collection or database query
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 build_entity( record, records = nil )` to #{ self.class.name }." unless respond_to?( :build_entity, true ) # rubocop:disable Metrics/LineLength
103
- transformer ||= ->( record ) { build_entity( record, records ) }
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( *validation_sources, request: nil, entity: nil )
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
@@ -0,0 +1,4 @@
1
+ module Shamu
2
+ module Services
3
+
4
+ class
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.9".freeze
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", "~> 4.2"
24
- spec.add_dependency "activesupport", "~> 4.2"
25
- spec.add_dependency "scorpion-ioc", "~> 0.5.16"
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", "~> 1"
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