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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +21 -0
- data/lib/praxis/blueprint.rb +1 -0
- data/lib/praxis/collection.rb +1 -1
- data/lib/praxis/docs/open_api/media_type_object.rb +2 -2
- data/lib/praxis/docs/open_api/operation_object.rb +1 -1
- data/lib/praxis/docs/open_api/schema_object.rb +46 -14
- data/lib/praxis/docs/open_api/tag_object.rb +4 -4
- data/lib/praxis/docs/open_api_generator.rb +37 -70
- data/lib/praxis/mapper/active_model_compat.rb +21 -0
- data/lib/praxis/mapper/resource.rb +150 -4
- data/lib/praxis/mapper/resources/callbacks.rb +35 -0
- data/lib/praxis/mapper/resources/query_methods.rb +39 -0
- data/lib/praxis/mapper/resources/query_proxy.rb +58 -0
- data/lib/praxis/mapper/resources/typed_methods.rb +133 -0
- data/lib/praxis/response.rb +11 -1
- data/lib/praxis/tasks/api_docs.rb +2 -1
- data/lib/praxis/version.rb +1 -1
- data/lib/praxis.rb +7 -0
- data/praxis.gemspec +2 -2
- data/spec/functional_spec.rb +4 -4
- data/spec/praxis/mapper/resource_spec.rb +53 -2
- data/spec/praxis/mapper/resources/callbacks_spec.rb +56 -0
- data/spec/praxis/mapper/resources/query_proxy_spec.rb +137 -0
- data/spec/praxis/mapper/resources/typed_methods_spec.rb +201 -0
- data/spec/praxis/response_spec.rb +8 -2
- data/spec/support/spec_resources.rb +132 -0
- metadata +16 -9
@@ -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
|
data/lib/praxis/response.rb
CHANGED
@@ -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.
|
10
|
+
generator = Praxis::Docs::OpenApiGenerator.instance
|
11
|
+
generator.configure_root(Dir.pwd)
|
11
12
|
generator.save!
|
12
13
|
end
|
13
14
|
|
data/lib/praxis/version.rb
CHANGED
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.
|
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
|
data/spec/functional_spec.rb
CHANGED
@@ -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(
|
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 '
|
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 '
|
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(
|
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)
|
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)
|
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
|