shamu 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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