praxis 2.0.pre.19 → 2.0.pre.22

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Praxis
4
+ module Mapper
5
+ module Resources
6
+ module Callbacks
7
+ extend ::ActiveSupport::Concern
8
+
9
+ included do
10
+ class_attribute :before_callbacks, :after_callbacks, :around_callbacks
11
+ self.before_callbacks = Hash.new { |h, method| h[method] = [] }
12
+ self.after_callbacks = Hash.new { |h, method| h[method] = [] }
13
+ self.around_callbacks = Hash.new { |h, method| h[method] = [] }
14
+ end
15
+
16
+ module ClassMethods
17
+ def before(method, function = nil, &block)
18
+ target = function ? function.to_sym : block
19
+ before_callbacks[method] << target
20
+ end
21
+
22
+ def after(method, function = nil, &block)
23
+ target = function ? function.to_sym : block
24
+ after_callbacks[method] << target
25
+ end
26
+
27
+ def around(method, function = nil, &block)
28
+ target = function ? function.to_sym : block
29
+ around_callbacks[method] << target
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Praxis
4
+ module Mapper
5
+ module Resources
6
+ module QueryMethods
7
+ extend ::ActiveSupport::Concern
8
+
9
+ # Includes some limited, but handy query methods so we can transparently
10
+ # use them from the resource layer, and get wrapped resources from it
11
+ module ClassMethods
12
+ def including(args)
13
+ QueryProxy.new(klass: self).including(args)
14
+ end
15
+
16
+ def all(args = {})
17
+ QueryProxy.new(klass: self).all(args)
18
+ end
19
+
20
+ def get(args)
21
+ QueryProxy.new(klass: self).get(args)
22
+ end
23
+
24
+ def get!(args)
25
+ QueryProxy.new(klass: self).get!(args)
26
+ end
27
+
28
+ def first
29
+ QueryProxy.new(klass: self).first
30
+ end
31
+
32
+ def last
33
+ QueryProxy.new(klass: self).last
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Praxis
4
+ module Mapper
5
+ module Resources
6
+ class QueryProxy
7
+ attr_reader :klass
8
+
9
+ def initialize(klass:)
10
+ @klass = klass
11
+ end
12
+
13
+ def including(includes)
14
+ @_includes = includes
15
+ self
16
+ end
17
+
18
+ # Can pass extra includes through :_includes
19
+ # TODO: We should not use the AR includes, but first pass them through the properties, cause
20
+ # we need to expand based on the resource methods, not the model methods
21
+ def get(condition)
22
+ base = klass.model._add_includes(klass.model, @_includes) # includes(nil) seems to have no effect
23
+ record = base._get(condition)
24
+
25
+ record.nil? ? nil : klass.wrap(record)
26
+ end
27
+
28
+ def get!(condition)
29
+ resource = get(condition)
30
+ # TODO: passing the :id if there is one in the condition...but can be more complex...think if we want to expose more
31
+ raise Praxis::Mapper::ResourceNotFound.new(type: @klass, id: condition[:id]) unless resource
32
+
33
+ resource
34
+ end
35
+
36
+ # Retrieve all or many wrapped resources
37
+ # .all -> returns them all
38
+ # .all(name: 'foo') -> returns all that match the name
39
+ def all(condition = {})
40
+ base = klass.model._add_includes(klass.model, @_includes) # includes(nil) seems to have no effect
41
+ records = base._all(condition)
42
+
43
+ klass.wrap(records)
44
+ end
45
+
46
+ def first
47
+ record = klass.model._first
48
+ record.nil? ? nil : klass.wrap(record)
49
+ end
50
+
51
+ def last
52
+ record = klass.model._last
53
+ record.nil? ? nil : klass.wrap(record)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Praxis
4
+ module Mapper
5
+ module Resources
6
+ # Error raised when trying to call a typed method and the validation of arguments fails
7
+ class IncompatibleTypeForMethodArguments < ::StandardError
8
+ attr_reader :errors, :method
9
+
10
+ def initialize(errors:, method:, klass:)
11
+ @errors = errors
12
+ super "Error validating/coercing arguments for call to method #{method} of class #{klass}:\n#{errors}"
13
+ end
14
+ end
15
+
16
+ module TypedMethods
17
+ extend ::ActiveSupport::Concern
18
+
19
+ included do
20
+ include Praxis::Mapper::Resources::Callbacks
21
+
22
+ class << self
23
+ attr_reader :signatures
24
+
25
+ def _finalize!
26
+ if @signatures
27
+ # Build the around callbacks for coercing the params for the methods with types defined
28
+ # Also, this needs to be before, so that we hit the coercion code before any other around callback
29
+ const_set(:MethodSignatures, Module.new)
30
+ @signatures.each do |method, type|
31
+ # Also add a constant pointing to the signature type inside Signatures (and substitute ! for Bang, as that's not allowed in a constant)
32
+ # For class Methods, also substitute .self for Self
33
+ # This helps with debugging, as we won't get anonymous struct classes, but we'll see these better names
34
+ cleaned_name = method.to_s.gsub(/!/, '_bang').to_s.gsub(/^self./, 'self_')
35
+ self::MethodSignatures.const_set(cleaned_name.camelize.to_sym, type)
36
+ coerce_params_for method, type
37
+ end
38
+ end
39
+
40
+ super
41
+ end
42
+
43
+ def signature(method_name, &block)
44
+ method = method_name.to_sym
45
+ @signatures ||= {}
46
+ if block_given?
47
+ type =
48
+ Class.new(Attributor::Struct) do
49
+ attributes do
50
+ instance_eval(&block)
51
+ end
52
+ end
53
+ @signatures[method] = type
54
+ else
55
+ @signatures[method]
56
+ end
57
+ end
58
+
59
+ # Sets up a specific around callback to a given method, where it'd pass the loaded/coerced type from the input
60
+ def coerce_params_for(method, type)
61
+ raise "Argument type for #{method} could not be found. Did you define a `signature` stanza for it?" unless type
62
+
63
+ if method.start_with?('self.')
64
+ simple_name = method.to_s.gsub(/^self./, '').to_sym
65
+ # Look for a Class method
66
+ raise "Error building typed method signature: Method #{method} is not defined in class #{name}" unless methods.include?(simple_name)
67
+
68
+ coerce_params_for_class(method(simple_name), type)
69
+ else
70
+ # Look for an instance method
71
+ raise "Error building typed method signature: Method #{method} is not defined in class #{name}" unless method_defined?(method)
72
+
73
+ coerce_params_for_instance(instance_method(method), type)
74
+ end
75
+ end
76
+
77
+ def coerce_params_for_instance(method, type)
78
+ around_method_name = "_coerce_params_for_#{method.name}"
79
+ instance_exec around_method_name: around_method_name,
80
+ orig_method: method,
81
+ type: type,
82
+ ctx: [to_s, method.name].freeze,
83
+ &CREATE_LOADER_METHOD
84
+
85
+ # Set an around callback to call the defined method above
86
+ around method.name, around_method_name
87
+ end
88
+
89
+ def coerce_params_for_class(method, type)
90
+ around_method_name = "_coerce_params_for_class_#{method.name}"
91
+ # Define an instance method in the eigenclass
92
+ singleton_class.instance_exec around_method_name: around_method_name,
93
+ orig_method: method,
94
+ type: type,
95
+ ctx: [to_s, method.name].freeze,
96
+ &CREATE_LOADER_METHOD
97
+
98
+ # Set an around callback to call the defined method above (the callbacks need self. for class interceptors)
99
+ class_method_name = "self.#{method.name}"
100
+ around class_method_name.to_sym, around_method_name
101
+ end
102
+
103
+ CREATE_LOADER_METHOD = proc do |around_method_name:, orig_method:, type:, ctx:|
104
+ has_args = orig_method.parameters.any? { |(argtype, _)| %i[req opt rest].include?(argtype) }
105
+ has_kwargs = orig_method.parameters.any? { |(argtype, _)| %i[keyreq keyrest].include?(argtype) }
106
+ raise "Typed signatures aren't supported for methods that have both kwargs and normal args: #{orig_method.name} of #{self.class}" if has_args && has_kwargs
107
+
108
+ if has_args
109
+ define_method(around_method_name) do |arg, &block|
110
+ loaded = type.load(arg, ctx)
111
+ errors = type.validate(loaded, ctx, nil)
112
+ raise IncompatibleTypeForMethodArguments.new(errors: errors, method: orig_method.name, klass: self) unless errors.empty?
113
+
114
+ # pass the struct object as a single arg
115
+ block.yield(loaded)
116
+ end
117
+ else
118
+ define_method(around_method_name) do |**args, &block|
119
+ loaded = type.load(args, ctx)
120
+ errors = type.validate(loaded, ctx, nil)
121
+ raise IncompatibleTypeForMethodArguments.new(errors: errors, method: orig_method.name, klass: self) unless errors.empty?
122
+
123
+ # Splat the args if it's a kwarg type method
124
+ block.yield(**loaded)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -110,8 +110,18 @@ module Praxis
110
110
  raise Exceptions::Validation, "Attempting to return a response with name #{response_name} " \
111
111
  'but no response definition with that name can be found'
112
112
  end
113
-
114
113
  response_definition.validate(self, validate_body: validate_body)
114
+ rescue Exceptions::Validation => e
115
+ ve = Application.instance.validation_handler.handle!(
116
+ summary: 'Error validating response',
117
+ exception: e,
118
+ request: request,
119
+ stage: 'response',
120
+ errors: e.errors
121
+ )
122
+ body = ve.format!
123
+
124
+ Responses::InternalServerError.new(body: body)
115
125
  end
116
126
  end
117
127
  end
@@ -7,7 +7,8 @@ namespace :praxis do
7
7
  require 'fileutils'
8
8
 
9
9
  Praxis::Blueprint.caching_enabled = false
10
- generator = Praxis::Docs::OpenApiGenerator.new(Dir.pwd)
10
+ generator = Praxis::Docs::OpenApiGenerator.instance
11
+ generator.configure_root(Dir.pwd)
11
12
  generator.save!
12
13
  end
13
14
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Praxis
4
- VERSION = '2.0.pre.19'
4
+ VERSION = '2.0.pre.22'
5
5
  end
data/lib/praxis.rb CHANGED
@@ -136,6 +136,13 @@ module Praxis
136
136
  module Mapper
137
137
  autoload :Resource, 'praxis/mapper/resource'
138
138
  autoload :SelectorGenerator, 'praxis/mapper/selector_generator'
139
+
140
+ module Resources
141
+ autoload :Callbacks, 'praxis/mapper/resources/callbacks'
142
+ autoload :QueryMethods, 'praxis/mapper/resources/query_methods'
143
+ autoload :TypedMethods, 'praxis/mapper/resources/typed_methods'
144
+ autoload :QueryProxy, 'praxis/mapper/resources/query_proxy'
145
+ end
139
146
  end
140
147
 
141
148
  # Avoid loading responses (and templates) lazily as they need to be registered in time
data/praxis.gemspec CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.executables << 'praxis'
24
24
 
25
25
  spec.add_dependency 'activesupport', '>= 3'
26
- spec.add_dependency 'attributor', '>= 6.0'
26
+ spec.add_dependency 'attributor', '>= 6.2'
27
27
  spec.add_dependency 'mime', '~> 0'
28
28
  spec.add_dependency 'mustermann', '>=1.1', '<=2'
29
29
  spec.add_dependency 'rack', '>= 1'
@@ -51,6 +51,6 @@ Gem::Specification.new do |spec|
51
51
  spec.add_development_dependency 'rspec-collection_matchers', '~> 1'
52
52
  spec.add_development_dependency 'rspec-its', '~> 1'
53
53
  # Just for the query selector extensions etc...
54
- spec.add_development_dependency 'activerecord', '> 4','< 7'
54
+ spec.add_development_dependency 'activerecord', '> 4', '< 7'
55
55
  spec.add_development_dependency 'sequel', '~> 5'
56
56
  end
@@ -88,7 +88,7 @@ describe 'Functional specs' do
88
88
 
89
89
  it 'fails to validate the response' do
90
90
  get '/api/clouds/1/instances?response_content_type=somejunk&api_version=1.0', nil, 'HTTP_FOO' => 'bar', 'global_session' => session
91
- expect(last_response.status).to eq(400)
91
+ expect(last_response.status).to eq(500)
92
92
  response = JSON.parse(last_response.body)
93
93
 
94
94
  expect(response['name']).to eq('ValidationError')
@@ -104,7 +104,7 @@ describe 'Functional specs' do
104
104
  expect(Praxis::Application.instance.config).to receive(:praxis).and_return(praxis_config)
105
105
  end
106
106
 
107
- it 'fails to validate the response' do
107
+ it 'does not validate the response and succeeds' do
108
108
  expect do
109
109
  get '/api/clouds/1/instances?response_content_type=somejunk&api_version=1.0', nil, 'global_session' => session
110
110
  end.to_not raise_error
@@ -400,7 +400,7 @@ describe 'Functional specs' do
400
400
  its(['root_volume']) { should be(nil) }
401
401
  end
402
402
 
403
- context 'with an invalid name' do
403
+ context 'returning an invalid name' do
404
404
  let(:request_payload) { { name: 'Invalid Name' } }
405
405
 
406
406
  its(['name']) { should eq 'ValidationError' }
@@ -408,7 +408,7 @@ describe 'Functional specs' do
408
408
  its(['errors']) { should match_array [/\$\.name value \(Invalid Name\) does not match regexp/] }
409
409
 
410
410
  it 'returns a validation error' do
411
- expect(last_response.status).to eq(400)
411
+ expect(last_response.status).to eq(500)
412
412
  end
413
413
  end
414
414
  end
@@ -32,7 +32,9 @@ describe Praxis::Mapper::Resource do
32
32
  context 'retrieving resources' do
33
33
  context 'getting a single resource' do
34
34
  before do
35
- expect(SimpleModel).to receive(:get).with(name: 'george xvi').and_return(record)
35
+ expect(SimpleModel).to receive(:get) do |args|
36
+ expect(**args).to match(name: 'george xvi')
37
+ end.and_return(record)
36
38
  end
37
39
 
38
40
  subject(:resource) { SimpleResource.get(name: 'george xvi') }
@@ -44,7 +46,9 @@ describe Praxis::Mapper::Resource do
44
46
 
45
47
  context 'getting multiple resources' do
46
48
  before do
47
- expect(SimpleModel).to receive(:all).with(name: ['george xvi']).and_return([record])
49
+ expect(SimpleModel).to receive(:all) do |args|
50
+ expect(**args).to eq(name: ['george xvi'])
51
+ end.and_return([record])
48
52
  end
49
53
 
50
54
  subject(:resource_collection) { SimpleResource.all(name: ['george xvi']) }
@@ -133,6 +137,19 @@ describe Praxis::Mapper::Resource do
133
137
  allow(record).to receive(:other_model).and_return(other_record)
134
138
  expect(resource.other_resource).to be(SimpleResource.new(record).other_resource)
135
139
  end
140
+
141
+ it 'memoizes result of related associations' do
142
+ expect(record).to receive(:parent).once.and_return(parent_record)
143
+ expect(resource.parent).to be(resource.parent)
144
+ end
145
+
146
+ it 'can clear memoization' do
147
+ expect(record).to receive(:parent).twice.and_return(parent_record)
148
+
149
+ expect(resource.parent).to be(resource.parent) # One time only calling the record parent method
150
+ resource.clear_memoization
151
+ expect(resource.parent).to be(resource.parent) # One time only time calling the record parent method after the reset
152
+ end
136
153
  end
137
154
 
138
155
  context '.wrap' do
@@ -163,4 +180,38 @@ describe Praxis::Mapper::Resource do
163
180
  expect(SimpleResource.wrap(record)).to be(OtherResource.wrap(record))
164
181
  end
165
182
  end
183
+
184
+ context 'calling typed methods' do
185
+ let(:resource) { TypedResource }
186
+ context 'class level ones' do
187
+ it 'kwarg methods get their args splatted in the top level' do
188
+ arg = resource.create(name: 'Praxis-licious', payload: { struct_param: { id: 1 } })
189
+ # Top level args are a hash (cause the typed methods will splat them before calling)
190
+ expect(arg).to be_kind_of Hash
191
+ # But structs beyond that are just the loaded types (which we can splat if we want to keep calling)
192
+ expect(arg[:payload]).to be_kind_of Attributor::Struct
193
+ end
194
+
195
+ it 'non-kwarg methods get a single arg' do
196
+ arg = resource.singlearg_create({ name: 'Praxis-licious', payload: { struct_param: { id: 1 } } })
197
+ # Single argument, instance of an Attributor Struct
198
+ expect(arg).to be_kind_of Attributor::Struct
199
+ end
200
+ end
201
+ context 'instance level ones' do
202
+ it 'kwarg methods get their args splatted in the top level' do
203
+ arg = resource.new({}).update!(string_param: 'Stringer', struct_param: { id: 1 })
204
+ # Top level args are a hash (cause the typed methods will splat them before calling)
205
+ expect(arg).to be_kind_of Hash
206
+ # But structs beyond that are just the loaded types (which we can splat if we want to keep calling)
207
+ expect(arg[:struct_param]).to be_kind_of Attributor::Struct
208
+ end
209
+
210
+ it 'non-kwarg methods get a single arg' do
211
+ arg = resource.new({}).singlearg_update!({ string_param: 'Stringer', struct_param: { id: 1 } })
212
+ # Single argument, instance of an Attributor Struct
213
+ expect(arg).to be_kind_of Attributor::Struct
214
+ end
215
+ end
216
+ end
166
217
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Praxis::Mapper::Resources::Callbacks do
6
+ context 'callbacks' do
7
+ let(:double_model) do
8
+ SimpleModel.new(before_count: 0, after_count: 0, around_count: 0, name: '', force: false)
9
+ end
10
+ let(:resource) { SimpleResource.new(double_model) }
11
+ context 'using functions with args and kwargs' do
12
+ subject { resource.change_name('hi', force: true) }
13
+ it 'before' do
14
+ expect(subject.record.before_count).to eq(1) # 1 before hook
15
+ end
16
+ it 'after' do
17
+ expect(subject.record.after_count).to eq(1) # 1 after hook
18
+ end
19
+ it 'around' do
20
+ # 50, just for the only filter
21
+ expect(subject.record.around_count).to eq(50)
22
+ end
23
+ after do
24
+ # one for the before, the around filter, the actual method and the after filter
25
+ expect(subject.record.name).to eq('hi-hi-hi-hi')
26
+ expect(subject.record.force).to be_truthy
27
+ end
28
+ end
29
+
30
+ context 'using functions with only kwargs' do
31
+ subject { resource.update!(number: 1000) }
32
+ it 'before' do
33
+ expect(subject.record.before_count).to eq(1) # 1 before hook
34
+ end
35
+ it 'after' do
36
+ expect(subject.record.after_count).to eq(1) # 1 after hook
37
+ end
38
+ it 'around' do
39
+ # 1000 for the orig update+1 from before_count + 50+100 for the 2 around filters
40
+ expect(subject.record.around_count).to eq(1151)
41
+ end
42
+ end
43
+
44
+ context 'using functions only args' do
45
+ subject { resource.argsonly('hi') }
46
+ it 'around' do
47
+ # 50, just for the only filter
48
+ expect(subject.record.around_count).to eq(50)
49
+ end
50
+ after do
51
+ # one for the the around filter and one for the actual methodr
52
+ expect(subject.record.name).to eq('hi-hi')
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Praxis::Mapper::Resources::QueryProxy do
6
+ let(:instance) { described_class.new(klass: resource_class) }
7
+ let(:resource_class) { SimpleResource }
8
+ let(:the_includes) { nil }
9
+
10
+ context 'including' do
11
+ let(:the_includes) { %i[one two] }
12
+ it 'saves the includes' do
13
+ expect(instance.instance_variable_get(:@_includes)).to be_nil
14
+ result = instance.including(the_includes)
15
+ expect(instance.instance_variable_get(:@_includes)).to be(the_includes)
16
+ expect(result).to be instance
17
+ end
18
+ end
19
+
20
+ context 'get' do
21
+ subject { instance.get(id: 39) }
22
+ # Base responds to the underlying ORM method _get to retrieve a record given a condition
23
+ let(:base) { double('ORM Base', _get: model_instance) }
24
+
25
+ before do
26
+ expect(resource_class.model).to receive(:_add_includes) do |model, includes|
27
+ expect(model).to be SimpleModel
28
+ expect(includes).to be(the_includes)
29
+ end.and_return(base)
30
+ end
31
+ context 'when a model is not found' do
32
+ let(:model_instance) { nil }
33
+ it 'returns nil' do
34
+ expect(subject).to be nil
35
+ end
36
+ end
37
+
38
+ context 'with a found model' do
39
+ let(:model_instance) { SimpleModel.new }
40
+ it 'returns an instance of the resource, wrapping the record' do
41
+ expect(subject).to be_kind_of(SimpleResource)
42
+ expect(subject.record).to be(model_instance)
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'all' do
48
+ subject { instance.all(id: 39, name: 'foo') }
49
+ # Base responds to the underlying ORM method _all to retrieve a list of records given a condition
50
+ let(:base) { double('ORM Base', _all: model_instances) }
51
+
52
+ before do
53
+ expect(resource_class.model).to receive(:_add_includes) do |model, includes|
54
+ expect(model).to be SimpleModel
55
+ expect(includes).to be(the_includes)
56
+ end.and_return(base)
57
+ end
58
+ context 'when no records found' do
59
+ let(:model_instances) { nil }
60
+ it 'returns nil' do
61
+ expect(subject).to be_empty
62
+ end
63
+ end
64
+
65
+ context 'with found records' do
66
+ let(:model_instances) { [SimpleModel.new(id: 1), SimpleModel.new(id: 2)] }
67
+ it 'returns an array of resource instances, each wrapping their record' do
68
+ expect(subject).to be_kind_of(Array)
69
+ expect(subject.map(&:record)).to eq(model_instances)
70
+ end
71
+ end
72
+ end
73
+
74
+ context 'first' do
75
+ subject { instance.first }
76
+ before do
77
+ expect(resource_class.model).to receive(:_first).and_return(model_instance)
78
+ end
79
+ context 'when a model is not found' do
80
+ let(:model_instance) { nil }
81
+ it 'returns nil' do
82
+ expect(subject).to be nil
83
+ end
84
+ end
85
+
86
+ context 'with a found model' do
87
+ let(:model_instance) { SimpleModel.new }
88
+ it 'returns an instance of the resource, wrapping the record' do
89
+ expect(subject).to be_kind_of(SimpleResource)
90
+ expect(subject.record).to be(model_instance)
91
+ end
92
+ end
93
+ end
94
+
95
+ context 'last' do
96
+ subject { instance.last }
97
+ before do
98
+ expect(resource_class.model).to receive(:_last).and_return(model_instance)
99
+ end
100
+ context 'when a model is not found' do
101
+ let(:model_instance) { nil }
102
+ it 'returns nil' do
103
+ expect(subject).to be nil
104
+ end
105
+ end
106
+
107
+ context 'with a found model' do
108
+ let(:model_instance) { SimpleModel.new }
109
+ it 'returns an instance of the resource, wrapping the record' do
110
+ expect(subject).to be_kind_of(SimpleResource)
111
+ expect(subject.record).to be(model_instance)
112
+ end
113
+ end
114
+ end
115
+
116
+ context 'get!' do
117
+ subject { instance.get!(id: 39) }
118
+ before do
119
+ # Expects to always call the normal get function for the result
120
+ expect(instance).to receive(:get) do |args|
121
+ expect(args[:id]).to eq(39)
122
+ end.and_return(resource_instance)
123
+ end
124
+ context 'when no record is found' do
125
+ let(:resource_instance) { nil }
126
+ it 'raises ResourceNotFound' do
127
+ expect { subject }.to raise_error(Praxis::Mapper::ResourceNotFound)
128
+ end
129
+ end
130
+ context 'with a record found' do
131
+ let(:resource_instance) { double('ResourceInstance') }
132
+ it 'simply returns the result of get' do
133
+ expect(subject).to be(resource_instance)
134
+ end
135
+ end
136
+ end
137
+ end