praxis 2.0.pre.33 → 2.0.pre.34
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +2 -0
- data/.gitignore +0 -5
- data/.travis.yml +2 -3
- data/Appraisals +6 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile +6 -6
- data/gemfiles/active_6.gemfile +11 -9
- data/gemfiles/active_6.gemfile.lock +21 -15
- data/gemfiles/active_7.gemfile +11 -9
- data/gemfiles/active_7.gemfile.lock +21 -15
- data/lib/praxis/application.rb +3 -0
- data/lib/praxis/blueprint.rb +2 -1
- data/lib/praxis/docs/open_api/paths_object.rb +1 -1
- data/lib/praxis/docs/open_api/schema_object.rb +48 -27
- data/lib/praxis/docs/open_api_generator.rb +6 -6
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +0 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +5 -0
- data/lib/praxis/extensions/field_expansion.rb +7 -4
- data/lib/praxis/extensions/pagination/ordering_params.rb +0 -4
- data/lib/praxis/field_expander.rb +18 -3
- data/lib/praxis/handlers/xml_sample.rb +1 -1
- data/lib/praxis/mapper/resource.rb +1 -1
- data/lib/praxis/mapper/selector_generator.rb +1 -1
- data/lib/praxis/multipart/parser.rb +1 -1
- data/lib/praxis/types/splattable_string_array.rb +13 -0
- data/lib/praxis/version.rb +1 -1
- data/lib/praxis.rb +1 -1
- data/praxis.gemspec +3 -3
- data/spec/functional_library_spec.rb +91 -28
- data/spec/praxis/application_spec.rb +7 -2
- data/spec/praxis/blueprint_spec.rb +53 -55
- data/spec/praxis/controller_spec.rb +4 -1
- data/spec/praxis/extensions/field_expansion_spec.rb +2 -2
- data/spec/praxis/extensions/pagination/ordering_params_spec.rb +0 -2
- data/spec/praxis/field_expander_spec.rb +46 -0
- data/spec/spec_app/app/controllers/base_class.rb +12 -0
- data/spec/spec_app/app/resources/book.rb +9 -0
- data/spec/spec_app/config.ru +0 -1
- data/spec/spec_app/design/media_types/book.rb +2 -0
- data/spec/spec_app/design/resources/authors.rb +3 -7
- data/spec/support/spec_blueprints.rb +15 -0
- data/tasks/thor/templates/generator/example_app/Gemfile +1 -1
- data/tasks/thor/templates/generator/example_app/app/models/user.rb +0 -1
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +0 -1
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +10 -4
- data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +24 -0
- data/tasks/thor/templates/generator/example_app/design/api.rb +12 -1
- data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +1 -1
- data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +1 -1
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +1 -1
- metadata +18 -17
- data/.simplecov +0 -9
@@ -13,6 +13,52 @@ describe Praxis::FieldExpander do
|
|
13
13
|
AddressBlueprint.default_fieldset
|
14
14
|
end
|
15
15
|
|
16
|
+
context '.expand' do
|
17
|
+
let(:display_attribute_filter) { ->(required) { (required & allowed) == required } }
|
18
|
+
let(:all_fields) { { id: true, secret_data: true, pii_data: true } }
|
19
|
+
subject { described_class.expand(object_type, all_fields, display_attribute_filter) }
|
20
|
+
context 'with a displayable attribute at the top' do
|
21
|
+
let(:object_type) { RestrictedBlueprint }
|
22
|
+
context 'when it has the right permissions for the top and inner ones' do
|
23
|
+
let(:allowed) { ['restricted#read', 'pii#read'] }
|
24
|
+
it 'calls the underlying expander instance (i.e., expands it all)' do
|
25
|
+
expect(subject).to eq(id: true, secret_data: true, pii_data: true)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
context 'when it has the right permissions for the top, but not the inner' do
|
29
|
+
let(:allowed) { ['restricted#read'] }
|
30
|
+
it 'calls the underlying expander instance (i.e., expands it all)' do
|
31
|
+
expect(subject).to eq(id: true, secret_data: true)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
context 'when it does NOT have the right permissions on the top' do
|
35
|
+
let(:allowed) { ['pii#read'] } # Yet it would have the inner one
|
36
|
+
it 'directly returns empty hash (i.e., nothing is expanded)' do
|
37
|
+
expect(subject).to eq({})
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'with type that has an attribute that points to another type with a displayable attribute at the top' do
|
43
|
+
let(:object_type) { PseudoRestrictedBlueprint }
|
44
|
+
let(:all_fields) { { id: true, restricted: true } }
|
45
|
+
context 'when it has the right permissions for the top of the inner one' do
|
46
|
+
let(:allowed) { ['restricted#read'] }
|
47
|
+
it 'calls the underlying expander instance including the inner type' do
|
48
|
+
expect(subject).to eq(id: true, restricted: { id: true, secret_data: true })
|
49
|
+
expect(subject[:restricted].keys).to_not include(:pii_data)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
context 'when it does NOT have the right permissions for the top of the inner one' do
|
53
|
+
let(:allowed) { ['another#read'] }
|
54
|
+
it 'does not expand it' do
|
55
|
+
expect(subject).to eq(id: true)
|
56
|
+
expect(subject.keys).to_not include(:restricted)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
16
62
|
context 'expanding attributes of a PersonBlueprint blueprint' do
|
17
63
|
it 'with fields=true, expands all fields on the default fieldset' do
|
18
64
|
expect(field_expander.expand(PersonBlueprint, true)).to eq(expanded_person_default_fieldset)
|
@@ -12,4 +12,16 @@ class BaseClass
|
|
12
12
|
# I.e., you can use class inheritance in cases where it makes sense from an OO point of view
|
13
13
|
# but for the most part, you can probably share code through modules/concerns too.
|
14
14
|
def this_is_shared; end
|
15
|
+
|
16
|
+
# Just a dummy method to use as if it was an Authenticated concern for the current request...
|
17
|
+
def self.current_user_privs
|
18
|
+
['special#read']
|
19
|
+
end
|
20
|
+
|
21
|
+
def display_attribute?(privs)
|
22
|
+
# Do not expand, unless the current user has all the required privileges
|
23
|
+
return nil unless (self.class.current_user_privs & privs) == privs
|
24
|
+
|
25
|
+
true
|
26
|
+
end
|
15
27
|
end
|
@@ -39,5 +39,14 @@ module Resources
|
|
39
39
|
end
|
40
40
|
|
41
41
|
property :grouped_moar_tags, as: :tags
|
42
|
+
|
43
|
+
property :special, dependencies: [:simple_name]
|
44
|
+
def special
|
45
|
+
record.simple_name.reverse # just to make it different
|
46
|
+
end
|
47
|
+
property :multi, dependencies: [:simple_name]
|
48
|
+
def multi
|
49
|
+
record.simple_name.upcase # just to make it different
|
50
|
+
end
|
42
51
|
end
|
43
52
|
end
|
data/spec/spec_app/config.ru
CHANGED
@@ -12,6 +12,8 @@ class Book < Praxis::MediaType
|
|
12
12
|
attribute :category_uuid, String
|
13
13
|
attribute :author, Author
|
14
14
|
attribute :tags, Praxis::Collection.of(Tag)
|
15
|
+
attribute :special, String, displayable: 'special#read'
|
16
|
+
attribute :multi, String, displayable: ['special#read', 'normal#read']
|
15
17
|
|
16
18
|
group :grouped do
|
17
19
|
attribute :id
|
@@ -11,13 +11,9 @@ module ApiResources
|
|
11
11
|
routing { get '' }
|
12
12
|
params do
|
13
13
|
attribute :fields, Praxis::Types::FieldSelector.for(Author), description: 'Fields with which to render the result.'
|
14
|
-
attribute :filters, Praxis::Types::FilteringParams.for(Author)
|
15
|
-
|
16
|
-
|
17
|
-
end
|
18
|
-
attribute :order, Praxis::Extensions::Pagination::OrderingParams.for(Author) do
|
19
|
-
by_fields :id, :name, 'books.name'
|
20
|
-
end
|
14
|
+
attribute :filters, Praxis::Types::FilteringParams.for(Author) # No block for allowing any filtering
|
15
|
+
attribute :order, Praxis::Extensions::Pagination::OrderingParams.for(Author) # No block for allowing any sorting
|
16
|
+
attribute :pagination, Praxis::Types::PaginationParams.for(Author) # No block for allowing any field pagination
|
21
17
|
end
|
22
18
|
response :ok, media_type: Praxis::Collection.of(Author)
|
23
19
|
end
|
@@ -73,3 +73,18 @@ class SimpleHashCollection < Attributor::Model
|
|
73
73
|
attribute :hash_collection, Attributor::Collection.of(Hash)
|
74
74
|
end
|
75
75
|
end
|
76
|
+
|
77
|
+
class RestrictedBlueprint < Praxis::Blueprint
|
78
|
+
attributes(displayable: 'restricted#read') do
|
79
|
+
attribute :id, Integer
|
80
|
+
attribute :secret_data, String
|
81
|
+
attribute :pii_data, String, displayable: 'pii#read'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class PseudoRestrictedBlueprint < Praxis::Blueprint
|
86
|
+
attributes do
|
87
|
+
attribute :id, Integer
|
88
|
+
attribute :restricted, RestrictedBlueprint
|
89
|
+
end
|
90
|
+
end
|
@@ -14,13 +14,19 @@ module V1
|
|
14
14
|
end
|
15
15
|
|
16
16
|
module ClassMethods
|
17
|
+
mutex = Mutex.new
|
18
|
+
|
17
19
|
def endpoint_path_template
|
18
20
|
# memoize a templated path for an endpoint, like
|
19
|
-
# /
|
20
|
-
return @endpoint_path_template if @endpoint_path_template
|
21
|
+
# /users/%{id}
|
22
|
+
return @endpoint_path_template if @endpoint_path_template # rubocop:disable ThreadSafety/InstanceVariableInClassMethod
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
+
mutex.synchronize do
|
25
|
+
return @endpoint_path_template if @endpoint_path_template # rubocop:disable ThreadSafety/InstanceVariableInClassMethod
|
26
|
+
|
27
|
+
path = self.base_module.const_get(:Endpoints).const_get(model.name.split(':').last.pluralize).canonical_path.route.path
|
28
|
+
@endpoint_path_template = path.names.inject(path.to_s) { |p, name| p.sub(':' + name, "%{#{name}}") }
|
29
|
+
end
|
24
30
|
end
|
25
31
|
end
|
26
32
|
|
@@ -5,6 +5,13 @@ module V1
|
|
5
5
|
class User < Base
|
6
6
|
model ::User
|
7
7
|
|
8
|
+
# Define the name mapping from API filter params, to model attribute/associations
|
9
|
+
# when they aren't 1:1 the same
|
10
|
+
# filters_mapping(
|
11
|
+
# 'label': 'association.label_name'
|
12
|
+
# )
|
13
|
+
|
14
|
+
# Add dependencies for resource attributes to other attributes and/or model associations
|
8
15
|
# To compute the full_name (method below) we need to load first and last names from the DB
|
9
16
|
property :full_name, dependencies: %i[first_name last_name]
|
10
17
|
|
@@ -12,6 +19,23 @@ module V1
|
|
12
19
|
def full_name
|
13
20
|
[first_name, last_name].join(' ')
|
14
21
|
end
|
22
|
+
|
23
|
+
|
24
|
+
def self.create(payload)
|
25
|
+
# Assuming the API field names directly map the the model attributes. Massage if appropriate.
|
26
|
+
self.new(model.create(**payload.to_h))
|
27
|
+
end
|
28
|
+
|
29
|
+
def update(payload:)
|
30
|
+
# Assuming the API field names directly map the the model attributes. Massage if appropriate.
|
31
|
+
record.update(**payload.to_h)
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete
|
36
|
+
record.destroy
|
37
|
+
self
|
38
|
+
end
|
15
39
|
end
|
16
40
|
end
|
17
41
|
end
|
@@ -7,6 +7,17 @@ Praxis::ApiDefinition.define do
|
|
7
7
|
# Attributes for OpenAPI docs
|
8
8
|
termsOfService 'https://mysitehere.com'
|
9
9
|
contact name: 'API Info', email: 'info@mysitehere.com'
|
10
|
+
server(
|
11
|
+
url: 'https://{host}',
|
12
|
+
description: 'My Fancy API Service',
|
13
|
+
variables: {
|
14
|
+
host: {
|
15
|
+
default: 'localhost',
|
16
|
+
description: 'Host environment where to point at',
|
17
|
+
enum: %w[localhost mysitehere.com],
|
18
|
+
},
|
19
|
+
},
|
20
|
+
)
|
10
21
|
end
|
11
22
|
|
12
23
|
# Trait that when included will require a Bearer authorization header to be passed in.
|
@@ -15,4 +26,4 @@ Praxis::ApiDefinition.define do
|
|
15
26
|
key "Authorization", String, regexp: /^.*Bearer\s/, required: true
|
16
27
|
end
|
17
28
|
end
|
18
|
-
end
|
29
|
+
end
|
@@ -5,7 +5,7 @@ module <%= version_module %>
|
|
5
5
|
class <%= singular_class %> < Praxis::MediaType
|
6
6
|
identifier 'application/json'
|
7
7
|
|
8
|
-
domain_model 'Resources::<%= singular_class %>'
|
8
|
+
domain_model '<%= version_module %>::Resources::<%= singular_class %>'
|
9
9
|
description 'Structural definition of a <%= singular_class %>'
|
10
10
|
|
11
11
|
attributes do
|
@@ -60,7 +60,7 @@ module <%= version_module %>
|
|
60
60
|
resource = Resources::<%= singular_class %>.get(id: id)
|
61
61
|
return Praxis::Responses::NotFound.new unless resource
|
62
62
|
|
63
|
-
resource.delete
|
63
|
+
resource.delete
|
64
64
|
Praxis::Responses::NoContent.new
|
65
65
|
end
|
66
66
|
<%- end -%>
|
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.34
|
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: 2023-
|
12
|
+
date: 2023-06-14 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: '7.
|
34
|
+
version: '7.1'
|
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: '7.
|
41
|
+
version: '7.1'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: mime
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -110,7 +110,7 @@ dependencies:
|
|
110
110
|
- !ruby/object:Gem::Version
|
111
111
|
version: '0'
|
112
112
|
- !ruby/object:Gem::Dependency
|
113
|
-
name:
|
113
|
+
name: appraisal
|
114
114
|
requirement: !ruby/object:Gem::Requirement
|
115
115
|
requirements:
|
116
116
|
- - ">="
|
@@ -124,33 +124,33 @@ dependencies:
|
|
124
124
|
- !ruby/object:Gem::Version
|
125
125
|
version: '0'
|
126
126
|
- !ruby/object:Gem::Dependency
|
127
|
-
name:
|
127
|
+
name: bundler
|
128
128
|
requirement: !ruby/object:Gem::Requirement
|
129
129
|
requirements:
|
130
130
|
- - ">="
|
131
131
|
- !ruby/object:Gem::Version
|
132
|
-
version:
|
132
|
+
version: '0'
|
133
133
|
type: :development
|
134
134
|
prerelease: false
|
135
135
|
version_requirements: !ruby/object:Gem::Requirement
|
136
136
|
requirements:
|
137
137
|
- - ">="
|
138
138
|
- !ruby/object:Gem::Version
|
139
|
-
version:
|
139
|
+
version: '0'
|
140
140
|
- !ruby/object:Gem::Dependency
|
141
|
-
name:
|
141
|
+
name: rake
|
142
142
|
requirement: !ruby/object:Gem::Requirement
|
143
143
|
requirements:
|
144
144
|
- - ">="
|
145
145
|
- !ruby/object:Gem::Version
|
146
|
-
version:
|
146
|
+
version: 12.3.3
|
147
147
|
type: :development
|
148
148
|
prerelease: false
|
149
149
|
version_requirements: !ruby/object:Gem::Requirement
|
150
150
|
requirements:
|
151
151
|
- - ">="
|
152
152
|
- !ruby/object:Gem::Version
|
153
|
-
version:
|
153
|
+
version: 12.3.3
|
154
154
|
- !ruby/object:Gem::Dependency
|
155
155
|
name: pry
|
156
156
|
requirement: !ruby/object:Gem::Requirement
|
@@ -208,19 +208,19 @@ dependencies:
|
|
208
208
|
- !ruby/object:Gem::Version
|
209
209
|
version: '1'
|
210
210
|
- !ruby/object:Gem::Dependency
|
211
|
-
name:
|
211
|
+
name: coveralls_reborn
|
212
212
|
requirement: !ruby/object:Gem::Requirement
|
213
213
|
requirements:
|
214
|
-
- - "
|
214
|
+
- - "~>"
|
215
215
|
- !ruby/object:Gem::Version
|
216
|
-
version:
|
216
|
+
version: 0.27.0
|
217
217
|
type: :development
|
218
218
|
prerelease: false
|
219
219
|
version_requirements: !ruby/object:Gem::Requirement
|
220
220
|
requirements:
|
221
|
-
- - "
|
221
|
+
- - "~>"
|
222
222
|
- !ruby/object:Gem::Version
|
223
|
-
version:
|
223
|
+
version: 0.27.0
|
224
224
|
- !ruby/object:Gem::Dependency
|
225
225
|
name: fuubar
|
226
226
|
requirement: !ruby/object:Gem::Requirement
|
@@ -342,11 +342,11 @@ executables:
|
|
342
342
|
extensions: []
|
343
343
|
extra_rdoc_files: []
|
344
344
|
files:
|
345
|
+
- ".coveralls.yml"
|
345
346
|
- ".gitignore"
|
346
347
|
- ".rspec"
|
347
348
|
- ".rubocop.yml"
|
348
349
|
- ".ruby-version"
|
349
|
-
- ".simplecov"
|
350
350
|
- ".travis.yml"
|
351
351
|
- Appraisals
|
352
352
|
- CHANGELOG.md
|
@@ -496,6 +496,7 @@ files:
|
|
496
496
|
- lib/praxis/types/media_type_common.rb
|
497
497
|
- lib/praxis/types/multipart_array.rb
|
498
498
|
- lib/praxis/types/multipart_array/part_definition.rb
|
499
|
+
- lib/praxis/types/splattable_string_array.rb
|
499
500
|
- lib/praxis/validation_handler.rb
|
500
501
|
- lib/praxis/version.rb
|
501
502
|
- praxis.gemspec
|