praxis 2.0.pre.21 → 2.0.pre.22
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +11 -0
- data/lib/praxis/mapper/active_model_compat.rb +21 -0
- data/lib/praxis/mapper/resource.rb +123 -1
- 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/version.rb +1 -1
- data/lib/praxis.rb +7 -0
- data/praxis.gemspec +1 -1
- data/spec/praxis/mapper/resource_spec.rb +40 -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/support/spec_resources.rb +132 -0
- metadata +16 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fcf3359dc0ac9062c61efc934673d9bf5dfdda43e5abfbc9abc41d3030a7a00e
|
4
|
+
data.tar.gz: 04ffe07b27222f714b8da5e756cc9e1733b76d066a9e1bf5a3d15358dc7e6662
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f90af4ae85f4170a850d7d7637758c0c2d878271befa2fe437da8c994ac78657eebd28831bb0945a74ca48aa93a91d42e8b734fd70f3996984e427ad6850a7ae
|
7
|
+
data.tar.gz: b8eaa60ce2ba599b94f4b844339df8501ca295f3e1efb9a0f5dd38b9c4551aa64f108ba3a28913d06a22748dab11fee5c4bd8f9cfb8d2e85b1c456fb3284a5b3
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,17 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 2.0.pre.22
|
6
|
+
* Introduced Resource callbacks (an includeable concern). Callbacks allow you to define methods or blocks to be executed `before`, `after` or `around` any existing method in the resource. Class-level callbacks are defined with `self.xxxxx`. These methods will be executed within the instance of the resource (i.e., in the same context of the original) and must be defined with the same parameter signature. For around methods, only blocks can be used, and to call the original (inner) one, one needs to yield.
|
7
|
+
* Introduced QueryMethods for resources (an includeable concern). QueryMethods expose handy querying methods (`.get`, `.get!`, `.all`, `.first` and `.last` ) which will reach into the underlying ORM (i.e., right now, only ActiveModelCompat is supported) to perform the desired loading of data (and subsequent wrapping of results in resource instances).
|
8
|
+
* For ActiveRecord `.get` takes a condition hash that will translate to `.find_by`, and `.all` gets a condition hash that will translate to `.where`.
|
9
|
+
* `.get!` is a `.get` but that will raise a `Praxis::Mapper::ResourceNotFound` exception if nothing was found.
|
10
|
+
* There is an `.including(<spec>)` function that can be use to preload the underlying associations. I.e., the `<spec>` argument will translate to `.includes(<spec>)` in ActiveRecord.
|
11
|
+
* Introduced method signatures and validations for resources.
|
12
|
+
* One can define a method signature with the `signature(<name>)` stanza, passing a block defining Attributor parameters. For instance method signatures, the `<name>` is just a symbol with the name of the method. For class level methods use a string, and prepend `self.` to it (i.e., `self.create`).
|
13
|
+
* Signatures can only work for methods that either have a single argument (taken as a whole hash), or that have only keyword arguments (i.e., no mixed args and kwargs). It would be basically impossible to validate that combo against an Attributor Struct.
|
14
|
+
* The calls to typed methods will be intercepted (using an around callback), and the incoming parameters will be validated against the Attributor Struct defined in the siguature, coerced if necessary and passed onto the original method. If the incoming parameters fail validation, a `IncompatibleTypeForMethodArguments` exception will be thrown.
|
15
|
+
|
5
16
|
## 2.0.pre.21
|
6
17
|
* Fix nullable attribute in OpenApi generation
|
7
18
|
## 2.0.pre.20
|
@@ -80,6 +80,27 @@ module Praxis
|
|
80
80
|
end
|
81
81
|
end
|
82
82
|
|
83
|
+
# Compatible reader accessors
|
84
|
+
def _get(condition)
|
85
|
+
find_by(condition)
|
86
|
+
end
|
87
|
+
|
88
|
+
def _all(conditions = {})
|
89
|
+
where(conditions)
|
90
|
+
end
|
91
|
+
|
92
|
+
def _add_includes(base, includes)
|
93
|
+
base.includes(includes) # includes(nil) seems to have no effect
|
94
|
+
end
|
95
|
+
|
96
|
+
def _first
|
97
|
+
first
|
98
|
+
end
|
99
|
+
|
100
|
+
def _last
|
101
|
+
last
|
102
|
+
end
|
103
|
+
|
83
104
|
private
|
84
105
|
|
85
106
|
def local_columns_used_for_the_association(type, assoc_reflection)
|
@@ -4,6 +4,15 @@
|
|
4
4
|
# Once that is complete, the data set is iterated and a resultant view is generated.
|
5
5
|
module Praxis
|
6
6
|
module Mapper
|
7
|
+
class ResourceNotFound < RuntimeError
|
8
|
+
attr_reader :type, :id
|
9
|
+
|
10
|
+
def initialize(type:, id: nil)
|
11
|
+
@type = type
|
12
|
+
@id = id
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
7
16
|
class Resource
|
8
17
|
extend Praxis::Finalizable
|
9
18
|
|
@@ -58,6 +67,7 @@ module Praxis
|
|
58
67
|
finalize_resource_delegates
|
59
68
|
define_model_accessors
|
60
69
|
|
70
|
+
hookup_callbacks
|
61
71
|
super
|
62
72
|
end
|
63
73
|
|
@@ -79,6 +89,39 @@ module Praxis
|
|
79
89
|
end
|
80
90
|
end
|
81
91
|
|
92
|
+
def self.hookup_callbacks
|
93
|
+
return unless ancestors.include?(Praxis::Mapper::Resources::Callbacks)
|
94
|
+
|
95
|
+
instance_module = nil
|
96
|
+
class_module = nil
|
97
|
+
|
98
|
+
affected_methods = (before_callbacks.keys + after_callbacks.keys + around_callbacks.keys).uniq
|
99
|
+
affected_methods&.each do |method|
|
100
|
+
calls = {}
|
101
|
+
calls[:before] = before_callbacks[method] if before_callbacks.key?(method)
|
102
|
+
calls[:around] = around_callbacks[method] if around_callbacks.key?(method)
|
103
|
+
calls[:after] = after_callbacks[method] if after_callbacks.key?(method)
|
104
|
+
|
105
|
+
if method.start_with?('self.')
|
106
|
+
# Look for a Class method
|
107
|
+
simple_name = method.to_s.gsub(/^self./, '').to_sym
|
108
|
+
raise "Error building callback: Class-level method #{method} is not defined in class #{name}" unless methods.include?(simple_name)
|
109
|
+
|
110
|
+
class_module ||= Module.new
|
111
|
+
create_override_module(mod: class_module, method: method(simple_name), calls: calls)
|
112
|
+
else
|
113
|
+
# Look for an instance method
|
114
|
+
raise "Error building callback: Instance method #{method} is not defined in class #{name}" unless method_defined?(method)
|
115
|
+
|
116
|
+
instance_module ||= Module.new
|
117
|
+
create_override_module(mod: instance_module, method: instance_method(method), calls: calls)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
# Prepend the created instance and/or class modules if there were any functions in them
|
121
|
+
prepend instance_module if instance_module
|
122
|
+
singleton_class.send(:prepend, class_module) if class_module
|
123
|
+
end
|
124
|
+
|
82
125
|
def self.for_record(record)
|
83
126
|
return record._resource if record._resource
|
84
127
|
|
@@ -184,8 +227,11 @@ module Praxis
|
|
184
227
|
end
|
185
228
|
|
186
229
|
def self.define_accessor(name)
|
187
|
-
ivar_name =
|
230
|
+
ivar_name = case name.to_s
|
231
|
+
when /\?/
|
188
232
|
"is_#{name.to_s[0..-2]}"
|
233
|
+
when /!/
|
234
|
+
"#{name.to_s[0..-2]}_bang"
|
189
235
|
else
|
190
236
|
name.to_s
|
191
237
|
end
|
@@ -250,6 +296,7 @@ module Praxis
|
|
250
296
|
def reload
|
251
297
|
clear_memoization
|
252
298
|
reload_record
|
299
|
+
self
|
253
300
|
end
|
254
301
|
|
255
302
|
def clear_memoization
|
@@ -275,6 +322,81 @@ module Praxis
|
|
275
322
|
super
|
276
323
|
end
|
277
324
|
end
|
325
|
+
|
326
|
+
# Defines a 'proxy' method in the given module (mod), so it can then be prepended
|
327
|
+
# There are mostly 3 flavors, which dictate how to define the procs (to make sure we play nicely
|
328
|
+
# with ruby's arguments and all). Method with only args, with only kwords, and with both
|
329
|
+
# Note: if procs could be defined with the (...) syntax, this could be more DRY and simple...
|
330
|
+
def self.create_override_module(mod:, method:, calls:)
|
331
|
+
has_args = method.parameters.any? { |(type, _)| %i[req opt rest].include?(type) }
|
332
|
+
has_kwargs = method.parameters.any? { |(type, _)| %i[keyreq keyrest].include?(type) }
|
333
|
+
|
334
|
+
mod.class_eval do
|
335
|
+
if has_args && has_kwargs
|
336
|
+
# Setup the method to take both args and kwargs
|
337
|
+
define_method(method.name.to_sym) do |*args, **kwargs|
|
338
|
+
calls[:before]&.each do |target|
|
339
|
+
target.is_a?(Symbol) ? send(target, *args, **kwargs) : instance_exec(*args, **kwargs, &target)
|
340
|
+
end
|
341
|
+
|
342
|
+
orig_call = proc { |*a, **kw| super(*a, **kw) }
|
343
|
+
around_chain = calls[:around].inject(orig_call) do |inner, target|
|
344
|
+
proc { |*a, **kw| send(target, *a, **kw, &inner) }
|
345
|
+
end
|
346
|
+
result = if calls[:around].presence
|
347
|
+
around_chain.call(*args, **kwargs)
|
348
|
+
else
|
349
|
+
super(*args, **kwargs)
|
350
|
+
end
|
351
|
+
calls[:after]&.each do |target|
|
352
|
+
target.is_a?(Symbol) ? send(target, *args, **kwargs) : instance_exec(*args, **kwargs, &target)
|
353
|
+
end
|
354
|
+
result
|
355
|
+
end
|
356
|
+
elsif has_kwargs && !has_args
|
357
|
+
# Setup the method to only take kwargs
|
358
|
+
define_method(method.name.to_sym) do |**kwargs|
|
359
|
+
calls[:before]&.each do |target|
|
360
|
+
target.is_a?(Symbol) ? send(target, **kwargs) : instance_exec(**kwargs, &target)
|
361
|
+
end
|
362
|
+
orig_call = proc { |**kw| super(**kw) }
|
363
|
+
around_chain = calls[:around].inject(orig_call) do |inner, target|
|
364
|
+
proc { |**kw| send(target, **kw, &inner) }
|
365
|
+
end
|
366
|
+
result = if calls[:around].presence
|
367
|
+
around_chain.call(**kwargs)
|
368
|
+
else
|
369
|
+
super(**kwargs)
|
370
|
+
end
|
371
|
+
calls[:after]&.each do |target|
|
372
|
+
target.is_a?(Symbol) ? send(target, **kwargs) : instance_exec(**kwargs, &target)
|
373
|
+
end
|
374
|
+
result
|
375
|
+
end
|
376
|
+
else
|
377
|
+
# Setup the method to only take args
|
378
|
+
define_method(method.name.to_sym) do |*args|
|
379
|
+
calls[:before]&.each do |target|
|
380
|
+
target.is_a?(Symbol) ? send(target, *args) : instance_exec(*args, &target)
|
381
|
+
end
|
382
|
+
orig_call = proc { |*a| super(*a) }
|
383
|
+
around_chain = calls[:around].inject(orig_call) do |inner, target|
|
384
|
+
proc { |*a| send(target, *a, &inner) }
|
385
|
+
end
|
386
|
+
result = if calls[:around].presence
|
387
|
+
around_chain.call(*args)
|
388
|
+
else
|
389
|
+
super(*args)
|
390
|
+
end
|
391
|
+
calls[:after]&.each do |target|
|
392
|
+
target.is_a?(Symbol) ? send(target, *args) : instance_exec(*args, &target)
|
393
|
+
end
|
394
|
+
result
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
private_class_method :create_override_module
|
278
400
|
end
|
279
401
|
end
|
280
402
|
end
|
@@ -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/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'
|
@@ -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']) }
|
@@ -176,4 +180,38 @@ describe Praxis::Mapper::Resource do
|
|
176
180
|
expect(SimpleResource.wrap(record)).to be(OtherResource.wrap(record))
|
177
181
|
end
|
178
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
|
179
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
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Praxis::Mapper::Resources::TypedMethods do
|
6
|
+
let(:resource_class) { TypedResource }
|
7
|
+
|
8
|
+
context '._finalize!' do
|
9
|
+
# The class is already finalized by loading the TypedResource from the spec_resources
|
10
|
+
# So we will simply check that all the right things are created
|
11
|
+
it 'builds the MethodSignatures constant within the class' do
|
12
|
+
expect(TypedResource::MethodSignatures).to_not be_nil
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'builds the inner class type for the defined signatures' do
|
16
|
+
expect(TypedResource::MethodSignatures::UpdateBang).to_not be_nil
|
17
|
+
expect(TypedResource::MethodSignatures::UpdateBang).to be TypedResource.signature(:update!)
|
18
|
+
|
19
|
+
expect(TypedResource::MethodSignatures::SelfCreate).to_not be_nil
|
20
|
+
expect(TypedResource::MethodSignatures::SelfCreate).to be TypedResource.signature(:'self.create')
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'Subsitutes a ! for a Bang when creating the constant' do
|
24
|
+
expect(TypedResource::MethodSignatures::UpdateBang).to be TypedResource.signature(:update!)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'defines the coercing methods' do
|
28
|
+
expect(TypedResource.methods).to include(:_coerce_params_for_class_create)
|
29
|
+
expect(TypedResource.instance_methods).to include(:_coerce_params_for_update!)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context '.signature' do
|
34
|
+
# We are not creating more classes and signatures, simply checking that the ones created
|
35
|
+
# for TypedResource in the spec_resource_files are correctly processed
|
36
|
+
it 'defines it in the @signatures hash' do
|
37
|
+
expect(TypedResource.signatures.keys).to match_array(
|
38
|
+
%i[
|
39
|
+
self.create
|
40
|
+
self.singlearg_create
|
41
|
+
create
|
42
|
+
update!
|
43
|
+
singlearg_update!
|
44
|
+
]
|
45
|
+
)
|
46
|
+
expect(TypedResource.signature(:'self.create')).to be < Attributor::Struct
|
47
|
+
expect(TypedResource.signature(:create)).to be < Attributor::Struct
|
48
|
+
expect(TypedResource.signature(:update!)).to be < Attributor::Struct
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'holds the right definition for class create' do
|
52
|
+
definition = TypedResource.signature(:'self.create')
|
53
|
+
expect(definition.attributes.keys).to eq %i[name payload]
|
54
|
+
expect(definition.attributes[:payload].attributes.keys).to eq %i[string_param struct_param]
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'holds the right definition for instance create' do
|
58
|
+
definition = TypedResource.signature(:create)
|
59
|
+
expect(definition.attributes.keys).to eq %i[id]
|
60
|
+
end
|
61
|
+
it 'holds the right definition for update!' do
|
62
|
+
definition = TypedResource.signature(:update!)
|
63
|
+
expect(definition.attributes.keys).to eq %i[string_param struct_param]
|
64
|
+
expect(definition.attributes[:struct_param].attributes.keys).to eq %i[id]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'coerce_params_for' do
|
69
|
+
let(:resource_class) do
|
70
|
+
Class.new(Praxis::Mapper::Resource) do
|
71
|
+
include Praxis::Mapper::Resources::TypedMethods
|
72
|
+
def imethod_args(args)
|
73
|
+
args
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.cmethod_args(args)
|
77
|
+
args
|
78
|
+
end
|
79
|
+
|
80
|
+
def imethod_kwargs(**args)
|
81
|
+
args
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.cmethod_kwargs(**args)
|
85
|
+
args
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
let(:hook_coercer) { methods.each { |method| resource_class.coerce_params_for(method, type) } }
|
91
|
+
# Note, we're associating the same type signature for both imethod and cmethod signatures!
|
92
|
+
let(:type) do
|
93
|
+
Class.new(Attributor::Struct) do
|
94
|
+
attributes do
|
95
|
+
attribute :id, Integer, required: true
|
96
|
+
attribute :name, String, null: false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
before do
|
102
|
+
# None of our wrappers before invoking the function
|
103
|
+
our_wrappers = resource_class.methods.select { |m| m.to_s =~ /^_coerce_params_for_class_/ }
|
104
|
+
our_wrappers += resource_class.instance_methods.select { |m| m.to_s =~ /^_coerce_params_for_/ }
|
105
|
+
expect(our_wrappers).to be_empty
|
106
|
+
end
|
107
|
+
context 'instance methods' do
|
108
|
+
let(:methods) { %i[imethod_args imethod_kwargs] }
|
109
|
+
it 'creates the wrapper methods' do
|
110
|
+
hook_coercer
|
111
|
+
iwrappers = resource_class.instance_methods.select { |m| m.to_s =~ /^_coerce_params_for_/ }
|
112
|
+
expect(iwrappers).to match_array %i[_coerce_params_for_imethod_args _coerce_params_for_imethod_kwargs]
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'sets an around callback for them' do
|
116
|
+
hook_coercer
|
117
|
+
expect(resource_class.around_callbacks[:imethod_args]).to eq([:_coerce_params_for_imethod_args])
|
118
|
+
expect(resource_class.around_callbacks[:imethod_kwargs]).to eq([:_coerce_params_for_imethod_kwargs])
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'when hooking in the callbacks' do
|
122
|
+
before do
|
123
|
+
hook_coercer
|
124
|
+
resource_class._finalize!
|
125
|
+
end
|
126
|
+
context 'calls the wrapper to validate and load' do
|
127
|
+
it 'fails if invalid (id is required)' do
|
128
|
+
expect do
|
129
|
+
resource_class.new(nil).imethod_args({ name: 'Praxis' })
|
130
|
+
end.to raise_error(
|
131
|
+
Praxis::Mapper::Resources::IncompatibleTypeForMethodArguments,
|
132
|
+
/.imethod_args.id is required/
|
133
|
+
)
|
134
|
+
expect do
|
135
|
+
resource_class.new(nil).imethod_kwargs(name: 'Praxis')
|
136
|
+
end.to raise_error(
|
137
|
+
Praxis::Mapper::Resources::IncompatibleTypeForMethodArguments,
|
138
|
+
/.imethod_kwargs.id is required/
|
139
|
+
)
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'succeeds and returns the coerced struct if compatible' do
|
143
|
+
result = resource_class.new(nil).imethod_args({ id: '1', name: 'Praxis' })
|
144
|
+
expect(result[:id]).to eq(1) # Coerces to Integer!
|
145
|
+
expect(result[:name]).to eq('Praxis')
|
146
|
+
result = resource_class.new(nil).imethod_kwargs(id: '1', name: 'Praxis')
|
147
|
+
expect(result[:id]).to eq(1) # Coerces to Integer!
|
148
|
+
expect(result[:name]).to eq('Praxis')
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'class methods' do
|
155
|
+
let(:methods) { %i[self.cmethod_args self.cmethod_kwargs] }
|
156
|
+
it 'creates the wrapper methods' do
|
157
|
+
hook_coercer
|
158
|
+
cwrappers = resource_class.methods.select { |m| m.to_s =~ /^_coerce_params_for_class_/ }
|
159
|
+
expect(cwrappers).to match_array %i[_coerce_params_for_class_cmethod_args _coerce_params_for_class_cmethod_kwargs]
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'sets an around callback for them' do
|
163
|
+
hook_coercer
|
164
|
+
expect(resource_class.around_callbacks[:'self.cmethod_args']).to eq([:_coerce_params_for_class_cmethod_args])
|
165
|
+
expect(resource_class.around_callbacks[:'self.cmethod_kwargs']).to eq([:_coerce_params_for_class_cmethod_kwargs])
|
166
|
+
end
|
167
|
+
|
168
|
+
context 'when hooking in the callbacks' do
|
169
|
+
before do
|
170
|
+
hook_coercer
|
171
|
+
resource_class._finalize!
|
172
|
+
end
|
173
|
+
context 'calls the wrapper to validate and load' do
|
174
|
+
it 'fails if invalid (id is required)' do
|
175
|
+
expect do
|
176
|
+
resource_class.cmethod_args({ name: 'Praxis' })
|
177
|
+
end.to raise_error(
|
178
|
+
Praxis::Mapper::Resources::IncompatibleTypeForMethodArguments,
|
179
|
+
/.cmethod_args.id is required/
|
180
|
+
)
|
181
|
+
expect do
|
182
|
+
resource_class.cmethod_kwargs(name: 'Praxis')
|
183
|
+
end.to raise_error(
|
184
|
+
Praxis::Mapper::Resources::IncompatibleTypeForMethodArguments,
|
185
|
+
/.cmethod_kwargs.id is required/
|
186
|
+
)
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'succeeds and returns the coerced struct if compatible' do
|
190
|
+
result = resource_class.cmethod_args({ id: '1', name: 'Praxis' })
|
191
|
+
expect(result[:id]).to eq(1) # Coerces to Integer!
|
192
|
+
expect(result[:name]).to eq('Praxis')
|
193
|
+
result = resource_class.cmethod_kwargs(id: '1', name: 'Praxis')
|
194
|
+
expect(result[:id]).to eq(1) # Coerces to Integer!
|
195
|
+
expect(result[:name]).to eq('Praxis')
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -77,6 +77,12 @@ class YamlArrayModel < OpenStruct
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
|
80
|
+
class TypedModel < OpenStruct
|
81
|
+
def self._praxis_associations
|
82
|
+
{}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
80
86
|
# A set of resource classes for use in specs
|
81
87
|
class BaseResource < Praxis::Mapper::Resource
|
82
88
|
def href
|
@@ -100,6 +106,8 @@ class ParentResource < BaseResource
|
|
100
106
|
end
|
101
107
|
|
102
108
|
class SimpleResource < BaseResource
|
109
|
+
include Praxis::Mapper::Resources::Callbacks
|
110
|
+
|
103
111
|
model SimpleModel
|
104
112
|
|
105
113
|
resource_delegate other_model: [:other_attribute]
|
@@ -123,8 +131,132 @@ class SimpleResource < BaseResource
|
|
123
131
|
property :no_deps, dependencies: []
|
124
132
|
|
125
133
|
property :deep_nested_deps, dependencies: ['parent.simple_children.other_model.parent.display_name']
|
134
|
+
|
135
|
+
before(:update!, :do_before_update)
|
136
|
+
around(:update!, :do_around_update_nested)
|
137
|
+
around(:update!, :do_around_update)
|
138
|
+
# Define an after as a proc
|
139
|
+
after(:update!) do |number:|
|
140
|
+
_unused = number
|
141
|
+
record.after_count += 1
|
142
|
+
end
|
143
|
+
|
144
|
+
def do_before_update(number:)
|
145
|
+
_unused = number
|
146
|
+
record.before_count += 1
|
147
|
+
end
|
148
|
+
|
149
|
+
def do_around_update_nested(number:)
|
150
|
+
record.around_count += 100
|
151
|
+
yield(number: number)
|
152
|
+
end
|
153
|
+
|
154
|
+
def do_around_update(number:)
|
155
|
+
record.around_count += 50
|
156
|
+
yield(number: number)
|
157
|
+
end
|
158
|
+
|
159
|
+
around(:change_name, :do_around_change_name)
|
160
|
+
after(:change_name, :do_after_change_name)
|
161
|
+
# Define a before as a proc
|
162
|
+
before(:change_name) do |name, force:|
|
163
|
+
_unused = force
|
164
|
+
record.before_count += 1
|
165
|
+
record.name = name
|
166
|
+
record.force = false # Force always false in before
|
167
|
+
end
|
168
|
+
|
169
|
+
def do_after_change_name(name, force:)
|
170
|
+
_unused = force
|
171
|
+
record.after_count += 1
|
172
|
+
record.name += "-#{name}"
|
173
|
+
end
|
174
|
+
|
175
|
+
def do_around_change_name(name, force:)
|
176
|
+
record.around_count += 50
|
177
|
+
|
178
|
+
record.name += "-#{name}"
|
179
|
+
yield(name, force: force)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Appends the name and overrides the force
|
183
|
+
def change_name(name, force:)
|
184
|
+
record.name += "-#{name}"
|
185
|
+
record.force = force
|
186
|
+
self
|
187
|
+
end
|
188
|
+
|
189
|
+
around(:argsonly, :do_around_argsonly)
|
190
|
+
def do_around_argsonly(name)
|
191
|
+
record.around_count += 50
|
192
|
+
record.name += name.to_s
|
193
|
+
yield(name)
|
194
|
+
end
|
195
|
+
|
196
|
+
def argsonly(name)
|
197
|
+
record.name += "-#{name}"
|
198
|
+
self
|
199
|
+
end
|
200
|
+
|
201
|
+
# Adds 1000 to the around count, plus whatever has been accumulated in before_count
|
202
|
+
def update!(number:)
|
203
|
+
record.around_count += number + record.before_count
|
204
|
+
self
|
205
|
+
end
|
126
206
|
end
|
127
207
|
|
128
208
|
class YamlArrayResource < BaseResource
|
129
209
|
model YamlArrayModel
|
130
210
|
end
|
211
|
+
|
212
|
+
class TypedResource < BaseResource
|
213
|
+
include Praxis::Mapper::Resources::TypedMethods
|
214
|
+
|
215
|
+
model TypedModel
|
216
|
+
|
217
|
+
signature(:update!) do
|
218
|
+
attribute :string_param, String, null: false
|
219
|
+
attribute :struct_param do
|
220
|
+
attribute :id, Integer
|
221
|
+
end
|
222
|
+
end
|
223
|
+
def update!(**payload)
|
224
|
+
payload
|
225
|
+
end
|
226
|
+
|
227
|
+
signature(:singlearg_update!) do
|
228
|
+
attribute :string_param, String, null: false
|
229
|
+
attribute :struct_param do
|
230
|
+
attribute :id, Integer
|
231
|
+
end
|
232
|
+
end
|
233
|
+
def singlearg_update!(payload)
|
234
|
+
payload
|
235
|
+
end
|
236
|
+
|
237
|
+
# Instance method: create, to make sure we support both an instance and a class method signature
|
238
|
+
signature(:create) do
|
239
|
+
attribute :id, String
|
240
|
+
end
|
241
|
+
def create(id:)
|
242
|
+
id
|
243
|
+
end
|
244
|
+
|
245
|
+
signature('self.create') do
|
246
|
+
attribute :name, String, regexp: /Praxis/
|
247
|
+
attribute :payload, TypedResource.signature(:update!), required: true
|
248
|
+
end
|
249
|
+
|
250
|
+
def self.create(**payload)
|
251
|
+
payload
|
252
|
+
end
|
253
|
+
|
254
|
+
signature('self.singlearg_create') do
|
255
|
+
attribute :name, String, regexp: /Praxis/
|
256
|
+
attribute :payload, TypedResource.signature(:update!), required: true
|
257
|
+
end
|
258
|
+
|
259
|
+
def self.singlearg_create(payload)
|
260
|
+
payload
|
261
|
+
end
|
262
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: praxis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.pre.
|
4
|
+
version: 2.0.pre.22
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josep M. Blanquer
|
8
8
|
- Dane Jensen
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-05-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -31,14 +31,14 @@ dependencies:
|
|
31
31
|
requirements:
|
32
32
|
- - ">="
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: '6.
|
34
|
+
version: '6.2'
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
39
|
- - ">="
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: '6.
|
41
|
+
version: '6.2'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: mime
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -359,7 +359,7 @@ dependencies:
|
|
359
359
|
- - "~>"
|
360
360
|
- !ruby/object:Gem::Version
|
361
361
|
version: '5'
|
362
|
-
description:
|
362
|
+
description:
|
363
363
|
email:
|
364
364
|
- blanquer@gmail.com
|
365
365
|
- dane.jensen@gmail.com
|
@@ -463,6 +463,10 @@ files:
|
|
463
463
|
- lib/praxis/handlers/xml_sample.rb
|
464
464
|
- lib/praxis/mapper/active_model_compat.rb
|
465
465
|
- lib/praxis/mapper/resource.rb
|
466
|
+
- lib/praxis/mapper/resources/callbacks.rb
|
467
|
+
- lib/praxis/mapper/resources/query_methods.rb
|
468
|
+
- lib/praxis/mapper/resources/query_proxy.rb
|
469
|
+
- lib/praxis/mapper/resources/typed_methods.rb
|
466
470
|
- lib/praxis/mapper/selector_generator.rb
|
467
471
|
- lib/praxis/mapper/sequel_compat.rb
|
468
472
|
- lib/praxis/media_type.rb
|
@@ -543,6 +547,9 @@ files:
|
|
543
547
|
- spec/praxis/file_group_spec.rb
|
544
548
|
- spec/praxis/handlers/json_spec.rb
|
545
549
|
- spec/praxis/mapper/resource_spec.rb
|
550
|
+
- spec/praxis/mapper/resources/callbacks_spec.rb
|
551
|
+
- spec/praxis/mapper/resources/query_proxy_spec.rb
|
552
|
+
- spec/praxis/mapper/resources/typed_methods_spec.rb
|
546
553
|
- spec/praxis/mapper/selector_generator_spec.rb
|
547
554
|
- spec/praxis/media_type_identifier_spec.rb
|
548
555
|
- spec/praxis/media_type_spec.rb
|
@@ -660,7 +667,7 @@ homepage: https://github.com/praxis/praxis
|
|
660
667
|
licenses:
|
661
668
|
- MIT
|
662
669
|
metadata: {}
|
663
|
-
post_install_message:
|
670
|
+
post_install_message:
|
664
671
|
rdoc_options: []
|
665
672
|
require_paths:
|
666
673
|
- lib
|
@@ -675,8 +682,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
675
682
|
- !ruby/object:Gem::Version
|
676
683
|
version: 1.3.1
|
677
684
|
requirements: []
|
678
|
-
rubygems_version: 3.
|
679
|
-
signing_key:
|
685
|
+
rubygems_version: 3.3.7
|
686
|
+
signing_key:
|
680
687
|
specification_version: 4
|
681
688
|
summary: Building APIs the way you want it.
|
682
689
|
test_files: []
|