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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +2 -0
  3. data/.gitignore +0 -5
  4. data/.travis.yml +2 -3
  5. data/Appraisals +6 -4
  6. data/CHANGELOG.md +10 -0
  7. data/Gemfile +6 -6
  8. data/gemfiles/active_6.gemfile +11 -9
  9. data/gemfiles/active_6.gemfile.lock +21 -15
  10. data/gemfiles/active_7.gemfile +11 -9
  11. data/gemfiles/active_7.gemfile.lock +21 -15
  12. data/lib/praxis/application.rb +3 -0
  13. data/lib/praxis/blueprint.rb +2 -1
  14. data/lib/praxis/docs/open_api/paths_object.rb +1 -1
  15. data/lib/praxis/docs/open_api/schema_object.rb +48 -27
  16. data/lib/praxis/docs/open_api_generator.rb +6 -6
  17. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +0 -1
  18. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +1 -1
  19. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +5 -0
  20. data/lib/praxis/extensions/field_expansion.rb +7 -4
  21. data/lib/praxis/extensions/pagination/ordering_params.rb +0 -4
  22. data/lib/praxis/field_expander.rb +18 -3
  23. data/lib/praxis/handlers/xml_sample.rb +1 -1
  24. data/lib/praxis/mapper/resource.rb +1 -1
  25. data/lib/praxis/mapper/selector_generator.rb +1 -1
  26. data/lib/praxis/multipart/parser.rb +1 -1
  27. data/lib/praxis/types/splattable_string_array.rb +13 -0
  28. data/lib/praxis/version.rb +1 -1
  29. data/lib/praxis.rb +1 -1
  30. data/praxis.gemspec +3 -3
  31. data/spec/functional_library_spec.rb +91 -28
  32. data/spec/praxis/application_spec.rb +7 -2
  33. data/spec/praxis/blueprint_spec.rb +53 -55
  34. data/spec/praxis/controller_spec.rb +4 -1
  35. data/spec/praxis/extensions/field_expansion_spec.rb +2 -2
  36. data/spec/praxis/extensions/pagination/ordering_params_spec.rb +0 -2
  37. data/spec/praxis/field_expander_spec.rb +46 -0
  38. data/spec/spec_app/app/controllers/base_class.rb +12 -0
  39. data/spec/spec_app/app/resources/book.rb +9 -0
  40. data/spec/spec_app/config.ru +0 -1
  41. data/spec/spec_app/design/media_types/book.rb +2 -0
  42. data/spec/spec_app/design/resources/authors.rb +3 -7
  43. data/spec/support/spec_blueprints.rb +15 -0
  44. data/tasks/thor/templates/generator/example_app/Gemfile +1 -1
  45. data/tasks/thor/templates/generator/example_app/app/models/user.rb +0 -1
  46. data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +0 -1
  47. data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +10 -4
  48. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +24 -0
  49. data/tasks/thor/templates/generator/example_app/design/api.rb +12 -1
  50. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +1 -1
  51. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +1 -1
  52. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +1 -1
  53. metadata +18 -17
  54. 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
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pp'
4
3
  require 'json'
5
4
 
6
5
  require 'bundler/setup'
@@ -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) do
15
- filter 'books.name', using: %w[= != !], fuzzy: true
16
- filter 'id', using: %w[= !=]
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- gem 'activerecord', '~> 6'
5
+ gem 'activerecord'
6
6
  gem 'link_header' # For pagination extensions
7
7
  gem 'oj' # For fast JSON de/serialization handlers
8
8
  gem 'parslet' # For field selection extension
@@ -2,5 +2,4 @@
2
2
  class User < ActiveRecord::Base
3
3
  # So it can be used in all the automatic query/filtering extensions
4
4
  include Praxis::Mapper::ActiveModelCompat
5
-
6
5
  end
@@ -7,7 +7,6 @@ module V1
7
7
  included do
8
8
  around :action do |controller, callee|
9
9
  begin
10
- # TODO: Support Sequel as well
11
10
  ActiveRecord::Base.transaction do
12
11
  callee.call
13
12
  res = controller.response
@@ -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
- # /im/contacts/%{id}
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
- path = self.base_module.const_get(:Endpoints).const_get(model.name.split(':').last.pluralize).canonical_path.route.path
23
- @endpoint_path_template = path.names.inject(path.to_s) { |p, name| p.sub(':' + name, "%{#{name}}") }
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(payload: request.payload)
63
+ resource.delete
64
64
  Praxis::Responses::NoContent.new
65
65
  end
66
66
  <%- end -%>
@@ -30,7 +30,7 @@ module <%= version_module %>
30
30
  <%- end -%>
31
31
 
32
32
  <%- if action_enabled?(:delete) -%>
33
- def self.delete(id:)
33
+ def delete
34
34
  record.destroy
35
35
  self
36
36
  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.33
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-05-23 00:00:00.000000000 Z
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.0'
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.0'
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: bundler
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: rake
127
+ name: bundler
128
128
  requirement: !ruby/object:Gem::Requirement
129
129
  requirements:
130
130
  - - ">="
131
131
  - !ruby/object:Gem::Version
132
- version: 12.3.3
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: 12.3.3
139
+ version: '0'
140
140
  - !ruby/object:Gem::Dependency
141
- name: appraisal
141
+ name: rake
142
142
  requirement: !ruby/object:Gem::Requirement
143
143
  requirements:
144
144
  - - ">="
145
145
  - !ruby/object:Gem::Version
146
- version: '0'
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: '0'
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: coveralls
211
+ name: coveralls_reborn
212
212
  requirement: !ruby/object:Gem::Requirement
213
213
  requirements:
214
- - - ">="
214
+ - - "~>"
215
215
  - !ruby/object:Gem::Version
216
- version: '0'
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: '0'
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
data/.simplecov DELETED
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- SimpleCov.profiles.define 'praxis' do
4
- add_filter '/config/'
5
- add_filter '/spec/'
6
-
7
- add_group 'lib', 'lib'
8
- add_group 'app', 'app'
9
- end