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

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.
@@ -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