praxis 2.0.pre.27 → 2.0.pre.29
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -1
- data/lib/praxis/docs/open_api/media_type_object.rb +15 -8
- data/lib/praxis/docs/open_api/request_body_object.rb +8 -7
- data/lib/praxis/docs/open_api/schema_object.rb +6 -9
- data/lib/praxis/docs/open_api_generator.rb +2 -2
- data/lib/praxis/mapper/selector_generator.rb +18 -1
- data/lib/praxis/multipart/part.rb +5 -0
- data/lib/praxis/types/multipart_array.rb +5 -1
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +1 -1
- data/spec/praxis/mapper/selector_generator_spec.rb +49 -0
- data/spec/support/spec_resources.rb +3 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e8f0514dd2a155733de0efe5586dd4b0409c70a9b385822b15e39934a7b83ea2
|
4
|
+
data.tar.gz: ff66561cdaadb84a9c97b83c044c423561b6506e10a31cf72d032dce73dcb50c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9bcb89e0426fb2e9e8ea51e7794a42f0a1ec6969fce436196da8c9d32697b232a02bddb0008400d51c6dc8b2d475e1122283da8644ed78bb448d759fbf4f2363
|
7
|
+
data.tar.gz: 5c0a71be2bb1043ccc4269ccfa258d01e93b66d4dfa2f69938f5a495d8a296a305d2b8051c84a6683343672a69cd01e20f717dc87f4bf5ca4049b587438a3194
|
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,14 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
-
## 2.0.pre.
|
5
|
+
## 2.0.pre.29
|
6
|
+
* Assorted set of fixes to generate cleaner and more compliant OpenApi documents.
|
7
|
+
* Mostly in the area of multipart generation, and requirements and nullability for OpenApi 3.0
|
8
|
+
## 2.0.pre.28
|
9
|
+
* Enhance the mapper's Resource property to allow for a couple more powerful options using the `as:` keyword:
|
10
|
+
* `as: :self` will provide a way to map any further incoming fields on top of the already existing object. This is useful when we want to expose some properties for a resource, grouped within a sub structure, but that in reality they exist directly in the resource's underlying model (i.e., to organize the information of the model in a more structured/groupable way).
|
11
|
+
* `as: 'association1.association2'` allows us to traverse more than 1 association, and continue applying the incoming fields under that. This is commonly used when we want to expose a relationship on a resource, which is really coming from more than a single association level depth.
|
12
|
+
## 2.0.pre.27
|
6
13
|
* Introduce a new `as:` option for resource's `property`, to indicate that the underlying association method it is connected to, has a different name.
|
7
14
|
* This also will create a delegation function for the property name, that instead of calling the underlying association on the record, and wrapping the result with a resource instance, it will simply call the aliased method name (which is likely gonna hit the autogenerated code for that properyty, unless we have overriden it)
|
8
15
|
* With this change, the selector generator (i.e., the thing that looks at the incoming `fields` parameters and calculates which select and includes are necessary to query all the data we need), will be able to understand this aliasing cases, and properly pass along, and continue expanding any nested fields that are under the property name (before this, and further inner fields would be not included as soon as we hit a property that didn't have that direct association underneath).
|
@@ -45,15 +45,22 @@ module Praxis
|
|
45
45
|
|
46
46
|
if example_payload
|
47
47
|
examples_by_content_type = {}
|
48
|
-
|
48
|
+
# We don't need to provide top-level examples for a multipart payload...each part
|
49
|
+
# will provide its own example (and assembling a proper structured example for the whole
|
50
|
+
# body isn't necessarily trivial based on the spec)
|
51
|
+
# If we need to, someday, we can create the rendered_paylod by calling MultipartArray.self.dump_for_openapi
|
52
|
+
# and properly complete that code to generate the appropriate thing, including the encoding etc...
|
53
|
+
unless type < Praxis::Types::MultipartArray
|
54
|
+
rendered_payload = example_payload.dump
|
49
55
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
56
|
+
example_handlers.each do |spec|
|
57
|
+
content_type, handler_name = spec.first
|
58
|
+
handler = Praxis::Application.instance.handlers[handler_name]
|
59
|
+
# ReDoc is not happy to display json generated outputs when served as JSON...wtf?
|
60
|
+
generated = handler.generate(rendered_payload)
|
61
|
+
final = handler_name == 'json' ? JSON.parse(generated) : generated
|
62
|
+
examples_by_content_type[content_type] = final
|
63
|
+
end
|
57
64
|
end
|
58
65
|
end
|
59
66
|
|
@@ -23,13 +23,14 @@ module Praxis
|
|
23
23
|
# so we'll show all the supported MTs...but repeating the schema
|
24
24
|
# dumped_schema = SchemaObject.new(info: attribute).dump_schema
|
25
25
|
|
26
|
-
example_handlers =
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
26
|
+
example_handlers = \
|
27
|
+
if attribute.type < Praxis::Types::MultipartArray
|
28
|
+
ident = MediaTypeIdentifier.load('multipart/form-data')
|
29
|
+
[{ ident.to_s => 'json' }] # Multipart content type
|
30
|
+
else
|
31
|
+
# TODO: We could run it through other handlers I guess...if they're registered
|
32
|
+
[{ 'application/json' => 'json' }]
|
33
|
+
end
|
33
34
|
|
34
35
|
h[:content] = MediaTypeObject.create_content_attribute_helper(type: attribute.type,
|
35
36
|
example_payload: attribute.example(nil),
|
@@ -31,7 +31,7 @@ module Praxis
|
|
31
31
|
|
32
32
|
def dump_schema(shallow: false, allow_ref: false)
|
33
33
|
# We will dump schemas for mediatypes by simply creating a reference to the components' section
|
34
|
-
if type < Attributor::Container
|
34
|
+
if type < Attributor::Container && ! (type < Praxis::Types::MultipartArray)
|
35
35
|
if (type < Praxis::Blueprint || type < Attributor::Model) && allow_ref && !type.anonymous?
|
36
36
|
# TODO: Do we even need a description?
|
37
37
|
h = @attribute_options[:description] ? { 'description' => @attribute_options[:description] } : {}
|
@@ -47,10 +47,11 @@ module Praxis
|
|
47
47
|
props = type.attributes.transform_values.with_index do |definition, index|
|
48
48
|
# if type has an attribute in its requirements all, then it should be marked as required here
|
49
49
|
field_name = type.attributes.keys[index]
|
50
|
-
definition.options.merge!(required: true) if required_attributes.include?(field_name)
|
51
50
|
OpenApi::SchemaObject.new(info: definition).dump_schema(allow_ref: true, shallow: shallow)
|
52
51
|
end
|
53
|
-
h = { type: :object
|
52
|
+
h = { type: :object}
|
53
|
+
h[:properties] = props if props.presence
|
54
|
+
h[:required] = required_attributes unless required_attributes.empty?
|
54
55
|
end
|
55
56
|
else
|
56
57
|
# OpenApi::SchemaObject.new(info:target).dump_schema(allow_ref: allow_ref, shallow: shallow)
|
@@ -61,12 +62,8 @@ module Praxis
|
|
61
62
|
|
62
63
|
# Tag on OpenAPI specific requirements that aren't already added in the underlying JSON schema model
|
63
64
|
# Nullable: (it seems we need to ensure there is a null option to the enum, if there is one)
|
64
|
-
|
65
|
-
|
66
|
-
h[:enum] = h[:enum] + [nil] if h[:enum] && !h[:enum].include?(nil)
|
67
|
-
end
|
68
|
-
# Required: Mostly for request bodies
|
69
|
-
h[:required] = true if @attribute_options[:required]
|
65
|
+
is_nullable = @attribute_options[:null]
|
66
|
+
h[:nullable] = true if is_nullable
|
70
67
|
h
|
71
68
|
|
72
69
|
# # TODO: FIXME: return a generic object type if the passed info was weird.
|
@@ -117,10 +117,10 @@ module Praxis
|
|
117
117
|
openapi: '3.0.2',
|
118
118
|
info: info_object.dump,
|
119
119
|
servers: [server_object.dump],
|
120
|
-
paths: paths_object.dump
|
120
|
+
paths: paths_object.dump,
|
121
|
+
security: [] # NOTE: No security definitions in Praxis. Leave it empty, to not anger linters
|
121
122
|
# responses: {}, #TODO!! what do we get here? the templates?...need to transform to "Responses Definitions Object"
|
122
123
|
# securityDefinitions: {}, # NOTE: No security definitions in Praxis
|
123
|
-
# security: [], # NOTE: No security definitions in Praxis
|
124
124
|
}
|
125
125
|
|
126
126
|
# Create the top level tags by:
|
@@ -68,7 +68,24 @@ module Praxis
|
|
68
68
|
# Always add the underlying association if we're overriding the name...
|
69
69
|
if (praxis_compat_model = resource.model&.respond_to?(:_praxis_associations))
|
70
70
|
aliased_as = resource.properties[name][:as]
|
71
|
-
|
71
|
+
if aliased_as == :self
|
72
|
+
# Special keyword to add itself as the association, but still continue procesing the fields
|
73
|
+
# This is useful when we expose resource fields tucked inside another sub-struct, this way
|
74
|
+
# we can make sure that if the fields necessary to compute things inside the struct, they are preloaded
|
75
|
+
add(fields)
|
76
|
+
else
|
77
|
+
first, *rest = aliased_as.to_s.split('.').map(&:to_sym)
|
78
|
+
|
79
|
+
extended_fields = \
|
80
|
+
if rest.empty?
|
81
|
+
fields
|
82
|
+
else
|
83
|
+
rest.reverse.inject(fields) do |accum, prop|
|
84
|
+
{ prop => accum }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
add_association(first, extended_fields) if resource.model._praxis_associations[first]
|
88
|
+
end
|
72
89
|
end
|
73
90
|
dependencies&.each do |dependency|
|
74
91
|
# To detect recursion, let's allow mapping depending fields to the same name of the property
|
@@ -254,5 +254,10 @@ module Praxis
|
|
254
254
|
ensure
|
255
255
|
self.content_type = original_content_type
|
256
256
|
end
|
257
|
+
|
258
|
+
def self.dump_for_openapi(example_part)
|
259
|
+
# TODO: This needs to be structured as OpenAPI requires it
|
260
|
+
raise "dumping a part for open api not implemented yet"
|
261
|
+
end
|
257
262
|
end
|
258
263
|
end
|
@@ -197,7 +197,7 @@ module Praxis
|
|
197
197
|
encoding[key_to_use]['headers'] = headers_attribute.as_json_schema(example: part_example.headers)
|
198
198
|
end
|
199
199
|
|
200
|
-
hash[:properties] = props
|
200
|
+
hash[:properties] = props if props.presence
|
201
201
|
hash[:encoding] = encoding unless encoding.empty?
|
202
202
|
end
|
203
203
|
hash
|
@@ -370,6 +370,10 @@ module Praxis
|
|
370
370
|
all_entities = parts.join("\r\n--#{boundary}\r\n")
|
371
371
|
"--#{boundary}\r\n#{all_entities}\r\n--#{boundary}--\r\n"
|
372
372
|
end
|
373
|
+
|
374
|
+
def self.dump_for_openapi(example)
|
375
|
+
example.map {|part| MultipartPart.dump_for_openapi(part)}
|
376
|
+
end
|
373
377
|
end
|
374
378
|
end
|
375
379
|
end
|
data/lib/praxis/version.rb
CHANGED
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.5'
|
27
27
|
spec.add_dependency 'mime', '~> 0'
|
28
28
|
spec.add_dependency 'mustermann', '>=1.1'
|
29
29
|
spec.add_dependency 'rack', '>= 1'
|
@@ -236,6 +236,55 @@ describe Praxis::Mapper::SelectorGenerator do
|
|
236
236
|
end
|
237
237
|
it_behaves_like 'a proper selector'
|
238
238
|
end
|
239
|
+
context 'Deep aliased underlying associations also follows any nested fields at the end of the chain...' do
|
240
|
+
let(:fields) do
|
241
|
+
{
|
242
|
+
parent_id: true,
|
243
|
+
deep_aliased_association: {
|
244
|
+
name: true
|
245
|
+
}
|
246
|
+
}
|
247
|
+
end
|
248
|
+
let(:selectors) do
|
249
|
+
{
|
250
|
+
model: SimpleModel,
|
251
|
+
columns: %i[parent_id simple_name], # No added_column, as it does not follow the dotted reference as properties, just associations
|
252
|
+
tracks: {
|
253
|
+
parent: {
|
254
|
+
model: ParentModel,
|
255
|
+
columns: %i[id],
|
256
|
+
tracks: {
|
257
|
+
simple_children: {
|
258
|
+
model: SimpleModel,
|
259
|
+
columns: %i[parent_id simple_name]
|
260
|
+
}
|
261
|
+
}
|
262
|
+
}
|
263
|
+
}
|
264
|
+
}
|
265
|
+
end
|
266
|
+
it_behaves_like 'a proper selector'
|
267
|
+
end
|
268
|
+
context 'Using self for the underlying association: follows any nested fields skipping the association name and still applies dependencies' do
|
269
|
+
let(:fields) do
|
270
|
+
{
|
271
|
+
parent_id: true,
|
272
|
+
sub_struct: {
|
273
|
+
display_name: true
|
274
|
+
}
|
275
|
+
}
|
276
|
+
end
|
277
|
+
let(:selectors) do
|
278
|
+
{
|
279
|
+
model: SimpleModel,
|
280
|
+
# Parent_id is because we asked for it at the top
|
281
|
+
# display_name because we asked for it under sub_struct, but it is marked as :self
|
282
|
+
# alway_necessary_attribute because it is a dependency of sub_struct
|
283
|
+
columns: %i[parent_id display_name alway_necessary_attribute]
|
284
|
+
}
|
285
|
+
end
|
286
|
+
it_behaves_like 'a proper selector'
|
287
|
+
end
|
239
288
|
end
|
240
289
|
|
241
290
|
context 'string associations' do
|
@@ -154,6 +154,9 @@ class SimpleResource < BaseResource
|
|
154
154
|
property :deep_nested_deps, dependencies: ['parent.simple_children.other_model.parent.display_name']
|
155
155
|
property :aliased_association, as: :other_model, dependencies: [:name]
|
156
156
|
property :overriden_aliased_association, as: :other_model, dependencies: [:name]
|
157
|
+
property :deep_aliased_association, as: 'parent.simple_children' , dependencies: [:name]
|
158
|
+
|
159
|
+
property :sub_struct, as: :self, dependencies: [:alway_necessary_attribute]
|
157
160
|
|
158
161
|
before(:update!, :do_before_update)
|
159
162
|
around(:update!, :do_around_update_nested)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
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.29
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josep M. Blanquer
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2023-01-24 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.5'
|
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.5'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: mime
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|