praxis 2.0.pre.19 → 2.0.pre.22
Sign up to get free protection for your applications and to get access to all the features.
- 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
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,27 @@
|
|
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
|
+
|
16
|
+
## 2.0.pre.21
|
17
|
+
* Fix nullable attribute in OpenApi generation
|
18
|
+
## 2.0.pre.20
|
19
|
+
* Changed the behavior of dev-mode when validate_responses. Now they return a 500 status code (instead of a 400) but with the same validation error format body.
|
20
|
+
* validate_responses is meant to catch the application returning non-compliant responses for development only. As such, a 500 is much more appropriate and clear, as the validation is done on the behavior of the server, and not on the information sent by the client (i.e., it is a server problem, not reacting the way the API is defined)
|
21
|
+
* Introduced a method to reload a Resouce (.reload), which will clear the memoized values and call record.reload as well
|
22
|
+
* Open API Generation enhancements:
|
23
|
+
* Fixed type discovery (where some types wouldn't be included in the output)
|
24
|
+
* Changed the generation to output named types into components, and use `$ref` to point to them whenever appropriate
|
25
|
+
* Report nullable attributes
|
5
26
|
## 2.0.pre.19
|
6
27
|
* Introduced a new DSL for the `FilteringParams` type that allows filters for common attributes in your Media Types:
|
7
28
|
* The new `any` DSL allows you to define which final leaf attribute to always allow, and with which operators and/or fuzzy restrictions.
|
data/lib/praxis/blueprint.rb
CHANGED
data/lib/praxis/collection.rb
CHANGED
@@ -27,9 +27,9 @@ module Praxis
|
|
27
27
|
return {} if type.is_a? SimpleMediaType # NOTE: skip if it's a SimpleMediaType?? ... is that correct?
|
28
28
|
|
29
29
|
the_schema = if type.anonymous? || !(type < Praxis::MediaType) # Avoid referencing custom/simple Types? (i.e., just MTs)
|
30
|
-
SchemaObject.new(info: type).dump_schema
|
30
|
+
SchemaObject.new(info: type).dump_schema(shallow: false, allow_ref: false)
|
31
31
|
else
|
32
|
-
|
32
|
+
SchemaObject.new(info: type).dump_schema(shallow: true, allow_ref: true)
|
33
33
|
end
|
34
34
|
|
35
35
|
if example_payload
|
@@ -23,7 +23,6 @@ module Praxis
|
|
23
23
|
all_tags = tags + action.traits
|
24
24
|
h = {
|
25
25
|
summary: action.name.to_s,
|
26
|
-
description: action.description,
|
27
26
|
# externalDocs: {}, # TODO/FIXME
|
28
27
|
operationId: id,
|
29
28
|
responses: ResponsesObject.new(responses: action.responses).dump
|
@@ -32,6 +31,7 @@ module Praxis
|
|
32
31
|
# security: [{}]
|
33
32
|
# servers: [{}]
|
34
33
|
}
|
34
|
+
h[:description] = action.description if action.description
|
35
35
|
h[:tags] = all_tags.uniq unless all_tags.empty?
|
36
36
|
h[:parameters] = all_parameters unless all_parameters.empty?
|
37
37
|
h[:requestBody] = RequestBodyObject.new(attribute: action.payload).dump if action.payload
|
@@ -5,33 +5,65 @@ module Praxis
|
|
5
5
|
module OpenApi
|
6
6
|
class SchemaObject
|
7
7
|
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schema-object
|
8
|
-
attr_reader :type
|
8
|
+
attr_reader :type
|
9
9
|
|
10
10
|
def initialize(info:)
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
@attribute_options = {}
|
12
|
+
|
13
|
+
# info could be an attribute ... or a type
|
14
|
+
if info.is_a?(Attributor::Attribute)
|
15
|
+
@type = info.type
|
16
|
+
# Save the options that might be attached to the attribute, to add them to the type schema later
|
17
|
+
@attribute_options = info.options
|
14
18
|
else
|
15
19
|
@type = info
|
16
20
|
end
|
21
|
+
|
22
|
+
# Mediatypes have the description method, lower types don't
|
23
|
+
@attribute_options[:description] = @type.description if @type.respond_to?(:description)
|
24
|
+
@collection = type.respond_to?(:member_type)
|
17
25
|
end
|
18
26
|
|
19
27
|
def dump_example
|
20
|
-
ex =
|
21
|
-
if attribute
|
22
|
-
attribute.example
|
23
|
-
else
|
24
|
-
type.example
|
25
|
-
end
|
28
|
+
ex = type.example
|
26
29
|
ex.respond_to?(:dump) ? ex.dump : ex
|
27
30
|
end
|
28
31
|
|
29
|
-
def dump_schema
|
30
|
-
|
31
|
-
|
32
|
+
def dump_schema(shallow: false, allow_ref: false)
|
33
|
+
# We will dump schemas for mediatypes by simply creating a reference to the components' section
|
34
|
+
if type < Attributor::Container
|
35
|
+
if (type < Praxis::Blueprint || type < Attributor::Model) && allow_ref && !type.anonymous?
|
36
|
+
# TODO: Do we even need a description?
|
37
|
+
h = @attribute_options[:description] ? { 'description' => @attribute_options[:description] } : {}
|
38
|
+
|
39
|
+
Praxis::Docs::OpenApiGenerator.instance.register_seen_component(type)
|
40
|
+
h.merge!('$ref' => "#/components/schemas/#{type.id}")
|
41
|
+
elsif @collection
|
42
|
+
items = OpenApi::SchemaObject.new(info: type.member_type).dump_schema(allow_ref: allow_ref, shallow: false)
|
43
|
+
h = @attribute_options[:description] ? { 'description' => @attribute_options[:description] } : {}
|
44
|
+
h.merge!(type: 'array', items: items)
|
45
|
+
else # Attributor::Struct, etc
|
46
|
+
props = type.attributes.transform_values do |definition|
|
47
|
+
OpenApi::SchemaObject.new(info: definition).dump_schema(allow_ref: true, shallow: shallow)
|
48
|
+
end
|
49
|
+
h = { type: :object, properties: props } # TODO: Example?
|
50
|
+
end
|
32
51
|
else
|
33
|
-
|
52
|
+
# OpenApi::SchemaObject.new(info:target).dump_schema(allow_ref: allow_ref, shallow: shallow)
|
53
|
+
# TODO...we need to make sure we can use refs in the underlying components after the first level...
|
54
|
+
# ... maybe we need to loop over the attributes if it's an object/struct?...
|
55
|
+
h = type.as_json_schema(shallow: shallow, example: nil, attribute_options: @attribute_options)
|
34
56
|
end
|
57
|
+
|
58
|
+
# Tag on OpenAPI specific requirements that aren't already added in the underlying JSON schema model
|
59
|
+
# Nullable: (it seems we need to ensure there is a null option to the enum, if there is one)
|
60
|
+
if @attribute_options[:null]
|
61
|
+
h[:nullable] = @attribute_options[:null]
|
62
|
+
h[:enum] = h[:enum] + [nil] if h[:enum] && !h[:enum].include?(nil)
|
63
|
+
end
|
64
|
+
|
65
|
+
h
|
66
|
+
|
35
67
|
# # TODO: FIXME: return a generic object type if the passed info was weird.
|
36
68
|
# return { type: :object } unless info
|
37
69
|
|
@@ -9,6 +9,7 @@ module Praxis
|
|
9
9
|
module Docs
|
10
10
|
class OpenApiGenerator
|
11
11
|
require 'active_support/core_ext/enumerable' # For index_by
|
12
|
+
include Singleton
|
12
13
|
|
13
14
|
API_DOCS_DIRNAME = 'docs/openapi'
|
14
15
|
EXCLUDED_TYPES_FROM_OUTPUT = Set.new([
|
@@ -26,7 +27,7 @@ module Praxis
|
|
26
27
|
Attributor::URI
|
27
28
|
]).freeze
|
28
29
|
|
29
|
-
attr_reader :resources_by_version, :
|
30
|
+
attr_reader :resources_by_version, :infos_by_version, :doc_root_dir
|
30
31
|
|
31
32
|
# substitutes ":params_like_so" for {params_like_so}
|
32
33
|
def self.templatize_url(string)
|
@@ -34,24 +35,37 @@ module Praxis
|
|
34
35
|
end
|
35
36
|
|
36
37
|
def save!
|
38
|
+
raise 'You need to configure the root directory before saving (configure_root(<dir>))' unless @root
|
39
|
+
|
37
40
|
initialize_directories
|
38
41
|
# Restrict the versions listed in the index file to the ones for which we have at least 1 resource
|
39
42
|
write_index_file(for_versions: resources_by_version.keys)
|
40
43
|
resources_by_version.each_key do |version|
|
44
|
+
@seen_components_for_current_version = Set.new
|
41
45
|
write_version_file(version)
|
42
46
|
end
|
43
47
|
end
|
44
48
|
|
45
|
-
def initialize
|
49
|
+
def initialize
|
46
50
|
require 'yaml'
|
47
|
-
|
51
|
+
|
48
52
|
@resources_by_version = Hash.new do |h, k|
|
49
53
|
h[k] = Set.new
|
50
54
|
end
|
51
|
-
|
55
|
+
# List of types that we have seen/marked as necessary to list in the components/schemas section
|
56
|
+
# These should contain any mediatype define in the versioned controllers plus any type
|
57
|
+
# for which we've explicitly rendered a $ref schema
|
58
|
+
@seen_components_for_current_version = Set.new
|
52
59
|
@infos = ApiDefinition.instance.infos
|
53
60
|
collect_resources
|
54
|
-
|
61
|
+
end
|
62
|
+
|
63
|
+
def configure_root(root)
|
64
|
+
@root = root
|
65
|
+
end
|
66
|
+
|
67
|
+
def register_seen_component(type)
|
68
|
+
@seen_components_for_current_version.add(type)
|
55
69
|
end
|
56
70
|
|
57
71
|
private
|
@@ -69,63 +83,16 @@ module Praxis
|
|
69
83
|
end
|
70
84
|
end
|
71
85
|
|
72
|
-
def collect_types
|
73
|
-
@types_by_id = ObjectSpace.each_object(Class).select do |obj|
|
74
|
-
obj < Attributor::Type
|
75
|
-
end.index_by(&:id)
|
76
|
-
end
|
77
|
-
|
78
86
|
def write_index_file(for_versions:)
|
79
87
|
# TODO. create a simple html file that can link to the individual versions available
|
80
88
|
end
|
81
89
|
|
82
|
-
|
83
|
-
|
84
|
-
|
90
|
+
# TODO: Change this function name to scan_default_mediatypes...
|
91
|
+
def collect_default_mediatypes(endpoints)
|
85
92
|
# We'll start by processing the rendered mediatypes
|
86
|
-
|
87
|
-
|
93
|
+
Set.new(endpoints.select do |endpoint|
|
94
|
+
endpoint.media_type && !endpoint.media_type.is_a?(Praxis::SimpleMediaType)
|
88
95
|
end.collect(&:media_type))
|
89
|
-
|
90
|
-
newfound = Set.new
|
91
|
-
found_media_types.each do |mt|
|
92
|
-
newfound += scan_dump_for_types({ type: mt }, processed_types)
|
93
|
-
end
|
94
|
-
# Then will process the rendered resources (noting)
|
95
|
-
newfound += scan_dump_for_types(dumped_resources, Set.new)
|
96
|
-
|
97
|
-
# At this point we've done a scan of the dumped resources and mediatypes.
|
98
|
-
# In that scan we've discovered a bunch of types, however, many of those might have appeared in the JSON
|
99
|
-
# rendered in just shallow mode, so it is not guaranteed that we've seen all the available types.
|
100
|
-
# For that we'll do a (non-shallow) dump of all the types we found, and scan them until the scans do not
|
101
|
-
# yield types we haven't seen before
|
102
|
-
until newfound.empty?
|
103
|
-
dumped = newfound.collect(&:describe)
|
104
|
-
processed_types += newfound
|
105
|
-
newfound = scan_dump_for_types(dumped, processed_types)
|
106
|
-
end
|
107
|
-
processed_types
|
108
|
-
end
|
109
|
-
|
110
|
-
def scan_dump_for_types(data, processed_types)
|
111
|
-
newfound_types = Set.new
|
112
|
-
case data
|
113
|
-
when Array
|
114
|
-
data.collect { |item| newfound_types += scan_dump_for_types(item, processed_types) }
|
115
|
-
when Hash
|
116
|
-
if data.key?(:type) && data[:type].is_a?(Hash) && (%i[id name family] - data[:type].keys).empty?
|
117
|
-
type_id = data[:type][:id]
|
118
|
-
unless type_id.nil? || type_id == Praxis::SimpleMediaType.id # SimpleTypes shouldn't be collected
|
119
|
-
unless types_by_id[type_id]
|
120
|
-
raise "Error! We have detected a reference to a 'Type' with id='#{type_id}' which is not derived from Attributor::Type" \
|
121
|
-
' Document generation cannot proceed.'
|
122
|
-
end
|
123
|
-
newfound_types << types_by_id[type_id] unless processed_types.include? types_by_id[type_id]
|
124
|
-
end
|
125
|
-
end
|
126
|
-
data.values.map { |item| newfound_types += scan_dump_for_types(item, processed_types) }
|
127
|
-
end
|
128
|
-
newfound_types
|
129
96
|
end
|
130
97
|
|
131
98
|
def write_version_file(version)
|
@@ -133,11 +100,11 @@ module Praxis
|
|
133
100
|
# # Hack, let's "inherit/copy" all traits of a version from the global definition
|
134
101
|
# # Eventually traits should be defined for a version (and inheritable from global) so we'll emulate that here
|
135
102
|
# version_info[:traits] = infos_by_version[:traits]
|
136
|
-
dumped_resources = dump_resources(resources_by_version[version])
|
137
|
-
processed_types = scan_types_for_version(version, dumped_resources)
|
138
103
|
|
104
|
+
# We'll for sure include any of the default mediatypes in the endpoints for this version
|
105
|
+
@seen_components_for_current_version.merge(collect_default_mediatypes(resources_by_version[version]))
|
139
106
|
# Here we have:
|
140
|
-
# processed types: which includes mediatypes
|
107
|
+
# processed types: which includes default mediatypes for the processed endpoints
|
141
108
|
# processed resources for this version: resources_by_version[version]
|
142
109
|
|
143
110
|
info_object = OpenApi::InfoObject.new(version: version, api_definition_info: @infos[version])
|
@@ -168,8 +135,8 @@ module Praxis
|
|
168
135
|
end
|
169
136
|
full_data[:tags] = full_data[:tags] + tags_for_traits unless tags_for_traits.empty?
|
170
137
|
|
171
|
-
# Include only MTs (i.e.,
|
172
|
-
component_schemas =
|
138
|
+
# Include only MTs and Blueprints (i.e., no simple types...)
|
139
|
+
component_schemas = add_component_schemas(@seen_components_for_current_version.clone, {})
|
173
140
|
|
174
141
|
# 3- Then adding all of the top level Mediatypes...so we can present them at the bottom, otherwise they don't show
|
175
142
|
tags_for_mts = component_schemas.map do |(name, _info)|
|
@@ -251,16 +218,16 @@ module Praxis
|
|
251
218
|
end
|
252
219
|
end
|
253
220
|
|
254
|
-
def
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
else # If it is a blueprint ... for now, it'd be through the attribute
|
260
|
-
type.attribute
|
261
|
-
end
|
262
|
-
accum[type.id] = the_type.as_json_schema(shallow: false)
|
221
|
+
def add_component_schemas(types_to_add, components_hash)
|
222
|
+
initial = @seen_components_for_current_version.dup
|
223
|
+
types_to_add.each_with_object(components_hash) do |(type), accum|
|
224
|
+
# For components, we want the first level to be fully dumped (only references below that)
|
225
|
+
accum[type.id] = OpenApi::SchemaObject.new(info: type).dump_schema(allow_ref: false, shallow: false)
|
263
226
|
end
|
227
|
+
newfound = @seen_components_for_current_version - initial
|
228
|
+
# Process the new types if they have discovered
|
229
|
+
add_component_schemas(newfound, components_hash) unless newfound.empty?
|
230
|
+
components_hash
|
264
231
|
end
|
265
232
|
|
266
233
|
def convert_to_parameter_object(params)
|
@@ -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
|
|
@@ -13,6 +22,8 @@ module Praxis
|
|
13
22
|
|
14
23
|
class << self
|
15
24
|
attr_reader :model_map, :properties
|
25
|
+
# Names of the memoizable things (without the @__ prefix)
|
26
|
+
attr_accessor :memoized_variables
|
16
27
|
end
|
17
28
|
|
18
29
|
# TODO: also support an attribute of sorts on the versioned resource module. ie, V1::Resources.api_version.
|
@@ -32,6 +43,7 @@ module Praxis
|
|
32
43
|
|
33
44
|
@properties = superclass.properties.clone
|
34
45
|
@_filters_map = {}
|
46
|
+
@memoized_variables = []
|
35
47
|
end
|
36
48
|
end
|
37
49
|
|
@@ -55,6 +67,7 @@ module Praxis
|
|
55
67
|
finalize_resource_delegates
|
56
68
|
define_model_accessors
|
57
69
|
|
70
|
+
hookup_callbacks
|
58
71
|
super
|
59
72
|
end
|
60
73
|
|
@@ -76,6 +89,39 @@ module Praxis
|
|
76
89
|
end
|
77
90
|
end
|
78
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
|
+
|
79
125
|
def self.for_record(record)
|
80
126
|
return record._resource if record._resource
|
81
127
|
|
@@ -130,10 +176,13 @@ module Praxis
|
|
130
176
|
|
131
177
|
return unless association_resource_class
|
132
178
|
|
179
|
+
memoized_variables << name
|
133
180
|
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
134
181
|
def #{name}
|
182
|
+
return @__#{name} if instance_variable_defined?("@__#{name}")
|
183
|
+
|
135
184
|
records = record.#{name}
|
136
|
-
|
185
|
+
return nil if records.nil?
|
137
186
|
@__#{name} ||= #{association_resource_class}.wrap(records)
|
138
187
|
end
|
139
188
|
RUBY
|
@@ -151,6 +200,7 @@ module Praxis
|
|
151
200
|
end
|
152
201
|
|
153
202
|
def self.define_delegation_for_related_attribute(resource_name, resource_attribute)
|
203
|
+
memoized_variables << resource_attribute
|
154
204
|
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
155
205
|
def #{resource_attribute}
|
156
206
|
@__#{resource_attribute} ||= if (rec = self.#{resource_name})
|
@@ -164,6 +214,7 @@ module Praxis
|
|
164
214
|
related_resource_class = model_map[related_association[:model]]
|
165
215
|
return unless related_resource_class
|
166
216
|
|
217
|
+
memoized_variables << resource_attribute
|
167
218
|
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
168
219
|
def #{resource_attribute}
|
169
220
|
@__#{resource_attribute} ||= if (rec = self.#{resource_name})
|
@@ -176,15 +227,18 @@ module Praxis
|
|
176
227
|
end
|
177
228
|
|
178
229
|
def self.define_accessor(name)
|
179
|
-
ivar_name =
|
230
|
+
ivar_name = case name.to_s
|
231
|
+
when /\?/
|
180
232
|
"is_#{name.to_s[0..-2]}"
|
233
|
+
when /!/
|
234
|
+
"#{name.to_s[0..-2]}_bang"
|
181
235
|
else
|
182
236
|
name.to_s
|
183
237
|
end
|
184
|
-
|
238
|
+
memoized_variables << ivar_name
|
185
239
|
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
186
240
|
def #{name}
|
187
|
-
return @__#{ivar_name} if
|
241
|
+
return @__#{ivar_name} if instance_variable_defined?("@__#{ivar_name}")
|
188
242
|
@__#{ivar_name} = record.#{name}
|
189
243
|
end
|
190
244
|
RUBY
|
@@ -239,6 +293,23 @@ module Praxis
|
|
239
293
|
@record = record
|
240
294
|
end
|
241
295
|
|
296
|
+
def reload
|
297
|
+
clear_memoization
|
298
|
+
reload_record
|
299
|
+
self
|
300
|
+
end
|
301
|
+
|
302
|
+
def clear_memoization
|
303
|
+
self.class.memoized_variables.each do |name|
|
304
|
+
ivar = "@__#{name}"
|
305
|
+
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def reload_record
|
310
|
+
record.reload
|
311
|
+
end
|
312
|
+
|
242
313
|
def respond_to_missing?(name, *)
|
243
314
|
@record.respond_to?(name) || super
|
244
315
|
end
|
@@ -251,6 +322,81 @@ module Praxis
|
|
251
322
|
super
|
252
323
|
end
|
253
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
|
254
400
|
end
|
255
401
|
end
|
256
402
|
end
|