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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9bdbabd51b8b6bb4a776ab4c728d36b3cf86f3b7a3423250e6658fa01653ba6a
4
- data.tar.gz: e9edc1eacc8ebf67d8f8b5fdda19cb010b587f3e2d0319cbcdeec62908752d8b
3
+ metadata.gz: 907548a79ff385c164356d493d6ed808933534a37037d55fd3ffa446317c2e16
4
+ data.tar.gz: a1bc7931b86f65d6091a5ec835cc1b7ea7dafa21ac13a5cf8a0fcafdfd00ba3f
5
5
  SHA512:
6
- metadata.gz: 174e74b7e4e55378edde0f623b4f2c49aa15409bfc64ae9f36aa8a0167794ed1c3b747331da5bdc66e672bcb58228cbaad89971689449d082d5080c009b7ad6c
7
- data.tar.gz: c7574f73697e2067e6f338d1c19bdc6447ca24620ed49a1ff94882392c8df987cbc598624323ce3228330c27ce0dda7b4f0228b68a4ad18dbf6c37c995869e84
6
+ metadata.gz: df103d23ba77341af3586865c294e4681ab4d3d8dcb89f5b78dc2f29c56e4edc84b755195e35965a0193635b9726cbfcfe96e1d13ee26597b10bd7fdc13011f4
7
+ data.tar.gz: 927034f7434cd8a9e3f710d151d5fd04b92db0939d116c4951888c2c8837e8029db723e3c1aadd40efdb9930f05aada870c0c1291a07c126630c3d4c1784bcb4
data/.coveralls.yml ADDED
@@ -0,0 +1,2 @@
1
+ # Coveralls recommends putting the token in the yaml file, for public repos
2
+ repo_token: qkOZ802q2tAFRaxcRPreAYzm8WpaKMCRE
data/.gitignore CHANGED
@@ -11,9 +11,4 @@ node_modules/
11
11
  *.css.map
12
12
  pkg
13
13
 
14
- lib/api_browser/node_modules/
15
- lib/api_browser/app/bower_components/
16
- lib/api_browser/.tmp/
17
- lib/api_browser/bower.json
18
-
19
14
  development.sqlite3
data/.travis.yml CHANGED
@@ -1,9 +1,8 @@
1
- sudo: false
2
1
  language: ruby
2
+ cache: bundler
3
3
  rvm:
4
- - 2.6
5
4
  - 2.7
6
- - 3.1 # Not available in TravisCI out of the box yet
5
+ - 3.1
7
6
  script:
8
7
  - bundle exec rspec spec
9
8
  branches:
data/Appraisals CHANGED
@@ -1,11 +1,13 @@
1
- appraise "active-6" do
1
+ # frozen_string_literal: true
2
+
3
+ appraise 'active-6' do
2
4
  # Just for the query selector/filtering/ordering extensions etc...
3
5
  gem 'activerecord', '> 4', '< 7'
4
6
  gem 'sequel', '~> 5'
5
7
  end
6
-
7
- appraise "active-7" do
8
+
9
+ appraise 'active-7' do
8
10
  # Just for the query selector/filtering/ordering extensions etc...
9
11
  gem 'activerecord', '>=7'
10
12
  gem 'sequel', '~> 5'
11
- end
13
+ end
data/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # Praxis Changelog
2
2
 
3
3
  ## next
4
+
5
+ ## 2.0.pre.34
6
+ - Allow filtering, ordering and pagination to freely use any attributes (potentially deep ones) when no block for the definition is provided. When continuing to define the allowed fields from within the block, those would still be enforced.
7
+ - Added the ability to easily control the hiding/displayability of MediaType attributes in responses.
8
+ - it introduces a :displayable custom attribute that accepts an array of opaque strings, which can represent some sort of 'privileges' required to have for it to be renderable.
9
+ - the expander tooling now, will use such `display_attribute?` method to decide if a given attribute needs to be expanded/displayed or not.
10
+ - The glue to all this requires defining a `display_attribute?` method in the controllers (with takes an array of string opaque privileges), which needs to return true/false to decide if a given attribute is displayable or now. This method will commonly be connected to the `authz` pieces of your app, and would look at the assigned 'privileges' of the currently logged in 'principal', and simply lookup if the required set of privileges received in the method, fall within the currently assigned ones.
11
+ - Cleanup in the OpenAPI generation for schemas, to more properly surface existing custom attributes into x-{name} attributes as well.
12
+
13
+ ## 2.0.pre.33
4
14
  - Better support for ordefing in newer versions of MySQL:
5
15
  * Some versions will complain on an invalid query if you use an ORDER BY component that does not have the corresponding SELECT field
6
16
  - Use the newest Attributor gem, which revamps and hardnens the struct/collection definion DSL, and provides much better error messages and safety. It also
data/Gemfile CHANGED
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source "https://rubygems.org"
3
+ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
- gem "rubocop"
6
+ gem 'rubocop'
7
7
 
8
8
  group :test do
9
- gem "builder"
9
+ gem 'builder'
10
10
 
11
- gem "link_header"
12
- gem "oj"
13
- gem "parslet"
11
+ gem 'link_header'
12
+ gem 'oj'
13
+ gem 'parslet'
14
14
  end
@@ -1,16 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This file was generated by Appraisal
2
4
 
3
- source "https://rubygems.org"
5
+ source 'https://rubygems.org'
4
6
 
5
- gem "rubocop"
6
- gem "activerecord", "> 4", "< 7"
7
- gem "sequel", "~> 5"
7
+ gem 'activerecord', '> 4', '< 7'
8
+ gem 'rubocop'
9
+ gem 'sequel', '~> 5'
8
10
 
9
11
  group :test do
10
- gem "builder"
11
- gem "link_header"
12
- gem "oj"
13
- gem "parslet"
12
+ gem 'builder'
13
+ gem 'link_header'
14
+ gem 'oj'
15
+ gem 'parslet'
14
16
  end
15
17
 
16
- gemspec path: "../"
18
+ gemspec path: '../'
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- praxis (2.0.pre.32)
4
+ praxis (2.0.pre.34)
5
5
  activesupport (>= 3)
6
- attributor (>= 6.5)
6
+ attributor (>= 7.1)
7
7
  mime (~> 0)
8
8
  mustermann (>= 1.1)
9
9
  rack (>= 1)
@@ -29,7 +29,7 @@ GEM
29
29
  rake
30
30
  thor (>= 0.14.0)
31
31
  ast (2.4.2)
32
- attributor (6.5)
32
+ attributor (7.1)
33
33
  activesupport (>= 3)
34
34
  hashie (~> 3)
35
35
  randexp (~> 0)
@@ -39,12 +39,11 @@ GEM
39
39
  byebug (11.1.3)
40
40
  coderay (1.1.3)
41
41
  concurrent-ruby (1.2.2)
42
- coveralls (0.8.23)
43
- json (>= 1.8, < 3)
44
- simplecov (~> 0.16.1)
45
- term-ansicolor (~> 1.3)
46
- thor (>= 0.19.4, < 2.0)
47
- tins (~> 1.6)
42
+ coveralls_reborn (0.27.0)
43
+ simplecov (~> 0.22.0)
44
+ term-ansicolor (~> 1.7)
45
+ thor (~> 1.2)
46
+ tins (~> 1.32)
48
47
  debug_inspector (1.1.0)
49
48
  diff-lcs (1.5.0)
50
49
  docile (1.4.0)
@@ -82,6 +81,7 @@ GEM
82
81
  lumberjack (1.2.8)
83
82
  method_source (1.0.0)
84
83
  mime (0.4.4)
84
+ mini_portile2 (2.8.2)
85
85
  minitest (5.18.0)
86
86
  mustermann (3.0.0)
87
87
  ruby2_keywords (~> 0.0.1)
@@ -148,18 +148,22 @@ GEM
148
148
  ruby2_keywords (0.0.5)
149
149
  sequel (5.55.0)
150
150
  shellany (0.0.1)
151
- simplecov (0.16.1)
151
+ simplecov (0.22.0)
152
152
  docile (~> 1.1)
153
- json (>= 1.8, < 3)
154
- simplecov-html (~> 0.10.0)
155
- simplecov-html (0.10.2)
153
+ simplecov-html (~> 0.11)
154
+ simplecov_json_formatter (~> 0.1)
155
+ simplecov-html (0.12.3)
156
+ simplecov_json_formatter (0.1.4)
157
+ sqlite3 (1.6.1)
158
+ mini_portile2 (~> 2.8.0)
156
159
  sqlite3 (1.6.1-x86_64-darwin)
160
+ sqlite3 (1.6.1-x86_64-linux)
157
161
  sync (0.5.0)
158
162
  term-ansicolor (1.7.1)
159
163
  tins (~> 1.0)
160
164
  terminal-table (1.6.0)
161
165
  thor (1.2.2)
162
- tins (1.31.0)
166
+ tins (1.32.1)
163
167
  sync
164
168
  tzinfo (2.0.6)
165
169
  concurrent-ruby (~> 1.0)
@@ -167,14 +171,16 @@ GEM
167
171
  zeitwerk (2.6.8)
168
172
 
169
173
  PLATFORMS
174
+ ruby
170
175
  x86_64-darwin-20
176
+ x86_64-linux
171
177
 
172
178
  DEPENDENCIES
173
179
  activerecord (> 4, < 7)
174
180
  appraisal
175
181
  builder
176
182
  bundler
177
- coveralls
183
+ coveralls_reborn (~> 0.27.0)
178
184
  fuubar (~> 2)
179
185
  guard (~> 2)
180
186
  guard-bundler (~> 2)
@@ -1,16 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This file was generated by Appraisal
2
4
 
3
- source "https://rubygems.org"
5
+ source 'https://rubygems.org'
4
6
 
5
- gem "rubocop"
6
- gem "activerecord", ">=7"
7
- gem "sequel", "~> 5"
7
+ gem 'activerecord', '>=7'
8
+ gem 'rubocop'
9
+ gem 'sequel', '~> 5'
8
10
 
9
11
  group :test do
10
- gem "builder"
11
- gem "link_header"
12
- gem "oj"
13
- gem "parslet"
12
+ gem 'builder'
13
+ gem 'link_header'
14
+ gem 'oj'
15
+ gem 'parslet'
14
16
  end
15
17
 
16
- gemspec path: "../"
18
+ gemspec path: '../'
@@ -1,9 +1,9 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- praxis (2.0.pre.32)
4
+ praxis (2.0.pre.34)
5
5
  activesupport (>= 3)
6
- attributor (>= 6.5)
6
+ attributor (>= 7.1)
7
7
  mime (~> 0)
8
8
  mustermann (>= 1.1)
9
9
  rack (>= 1)
@@ -28,7 +28,7 @@ GEM
28
28
  rake
29
29
  thor (>= 0.14.0)
30
30
  ast (2.4.2)
31
- attributor (6.5)
31
+ attributor (7.1)
32
32
  activesupport (>= 3)
33
33
  hashie (~> 3)
34
34
  randexp (~> 0)
@@ -38,12 +38,11 @@ GEM
38
38
  byebug (11.1.3)
39
39
  coderay (1.1.3)
40
40
  concurrent-ruby (1.2.2)
41
- coveralls (0.8.23)
42
- json (>= 1.8, < 3)
43
- simplecov (~> 0.16.1)
44
- term-ansicolor (~> 1.3)
45
- thor (>= 0.19.4, < 2.0)
46
- tins (~> 1.6)
41
+ coveralls_reborn (0.27.0)
42
+ simplecov (~> 0.22.0)
43
+ term-ansicolor (~> 1.7)
44
+ thor (~> 1.2)
45
+ tins (~> 1.32)
47
46
  debug_inspector (1.1.0)
48
47
  diff-lcs (1.5.0)
49
48
  docile (1.4.0)
@@ -81,6 +80,7 @@ GEM
81
80
  lumberjack (1.2.8)
82
81
  method_source (1.0.0)
83
82
  mime (0.4.4)
83
+ mini_portile2 (2.8.2)
84
84
  minitest (5.18.0)
85
85
  mustermann (3.0.0)
86
86
  ruby2_keywords (~> 0.0.1)
@@ -147,32 +147,38 @@ GEM
147
147
  ruby2_keywords (0.0.5)
148
148
  sequel (5.55.0)
149
149
  shellany (0.0.1)
150
- simplecov (0.16.1)
150
+ simplecov (0.22.0)
151
151
  docile (~> 1.1)
152
- json (>= 1.8, < 3)
153
- simplecov-html (~> 0.10.0)
154
- simplecov-html (0.10.2)
152
+ simplecov-html (~> 0.11)
153
+ simplecov_json_formatter (~> 0.1)
154
+ simplecov-html (0.12.3)
155
+ simplecov_json_formatter (0.1.4)
156
+ sqlite3 (1.6.1)
157
+ mini_portile2 (~> 2.8.0)
155
158
  sqlite3 (1.6.1-x86_64-darwin)
159
+ sqlite3 (1.6.1-x86_64-linux)
156
160
  sync (0.5.0)
157
161
  term-ansicolor (1.7.1)
158
162
  tins (~> 1.0)
159
163
  terminal-table (1.6.0)
160
164
  thor (1.2.2)
161
- tins (1.31.0)
165
+ tins (1.32.1)
162
166
  sync
163
167
  tzinfo (2.0.6)
164
168
  concurrent-ruby (~> 1.0)
165
169
  unicode-display_width (2.4.2)
166
170
 
167
171
  PLATFORMS
172
+ ruby
168
173
  x86_64-darwin-20
174
+ x86_64-linux
169
175
 
170
176
  DEPENDENCIES
171
177
  activerecord (>= 7)
172
178
  appraisal
173
179
  builder
174
180
  bundler
175
- coveralls
181
+ coveralls_reborn (~> 0.27.0)
176
182
  fuubar (~> 2)
177
183
  guard (~> 2)
178
184
  guard-bundler (~> 2)
@@ -5,6 +5,9 @@ require 'mustermann'
5
5
  require 'logger'
6
6
 
7
7
  module Praxis
8
+ # Setup the option early, since things like MediaTypes and EndpointDefinitions might need it
9
+ Attributor::Attribute.custom_option :displayable, Types::SplattableStringArray.of(String)
10
+
8
11
  class Application
9
12
  include Singleton
10
13
 
@@ -11,7 +11,7 @@ module Praxis
11
11
  # It takes the name of the group, and passes the attributes block that needs to be a subset of the MediaType where the group resides
12
12
  def group(name, **options, &block)
13
13
  # Pass the reference to the target type by default. But allow overriding it if needed
14
- attribute(name, Praxis::BlueprintAttributeGroup.for(target), **{reference: target}.merge(options), &block)
14
+ attribute(name, Praxis::BlueprintAttributeGroup.for(target), **{ reference: target }.merge(options), &block)
15
15
  end
16
16
  end
17
17
 
@@ -284,6 +284,7 @@ module Praxis
284
284
  attributes.each do |name, attr|
285
285
  the_type = attr.type < Attributor::Collection ? attr.type.member_type : attr.type
286
286
  next if the_type < Blueprint
287
+
287
288
  # TODO: Allow groups in the default fieldset?? or perhaps better to make people explicitly define them?
288
289
  # next if (the_type < Blueprint && !(the_type < BlueprintAttributeGroup))
289
290
 
@@ -32,7 +32,7 @@ module Praxis
32
32
  id = resource.id
33
33
  # fill in the paths hash with a key for each path for each action/route
34
34
  resource.actions.each do |action_name, action|
35
- params_example = action.params ? action.params.example(nil) : nil
35
+ params_example = action.params&.example(nil)
36
36
  url = ActionDefinition.url_description(route: action.route, params: action.params, params_example: params_example)
37
37
 
38
38
  verb = url[:verb].downcase
@@ -5,68 +5,79 @@ 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, :attribute
9
9
 
10
10
  def initialize(info:)
11
- @attribute_options = {}
12
-
13
11
  # info could be an attribute ... or a type
12
+ # We will always try to work with the attribute if it is there, otherwise, we'll use type underneath
14
13
  if info.is_a?(Attributor::Attribute)
15
14
  @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
15
+ @attribute = info
18
16
  else
19
17
  @type = info
20
18
  end
21
19
 
22
- # Mediatypes have the description method, lower types don't
23
- @attribute_options[:description] = @type.description if @type.respond_to?(:description)
24
20
  @collection = type.respond_to?(:member_type)
25
21
  end
26
22
 
27
23
  def dump_example
28
- ex = type.example
24
+ ex = (attribute || type).example
29
25
  ex.respond_to?(:dump) ? ex.dump : ex
30
26
  end
31
27
 
32
28
  def dump_schema(shallow: false, allow_ref: false)
29
+ important_options = %i[description null]
30
+ # Compile all options from the underlying tye and attribute (if any), translating them
31
+ # to OpenAPI schema options with the x- prefix for our custom ones
32
+ base_options = _slice_options(type, important_options)
33
+ base_options.merge! _slice_options(type, Attributor::Attribute.custom_options, prefix: 'x')
34
+ if attribute
35
+ base_options.merge! _slice_options(attribute, important_options, prefix: 'x')
36
+ base_options.merge! _slice_options(attribute, Attributor::Attribute.custom_options, prefix: 'x')
37
+ end
38
+ # Tag on OpenAPI specific requirements that aren't already added in the underlying JSON schema model
39
+ # Nullable: (it seems we need to ensure there is a null option to the enum, if there is one)
40
+ base_options[:nullable] = !base_options.delete(:null).nil? if base_options.key?(:null)
41
+
33
42
  # We will dump schemas for mediatypes by simply creating a reference to the components' section
34
43
  if type < Attributor::Container && !(type < Praxis::Types::MultipartArray)
35
44
  if (type < Praxis::Blueprint || type < Attributor::Model) && allow_ref && !type.anonymous?
36
- # TODO: Technically OpenAPI/JSON schema support passing a description when pointing to a $ref (to override it)
45
+ # NOTE: Technically OpenAPI/JSON schema support passing a description when pointing to a $ref (to override it)
37
46
  # However, it seems that UI browsers like redoc or elements have bugs where if that's done, they get into a loop and crash
38
47
  # so for now, we're gonna avoid overriding the description until that is solved
39
- # h = @attribute_options[:description] ? { 'description' => @attribute_options[:description] } : {}
40
- h = {}
48
+ base_options.delete(:description)
41
49
  Praxis::Docs::OpenApiGenerator.instance.register_seen_component(type)
42
- h.merge!('$ref' => "#/components/schemas/#{type.id}")
50
+ base_options.merge!('$ref' => "#/components/schemas/#{type.id}")
43
51
  elsif @collection
44
52
  items = OpenApi::SchemaObject.new(info: type.member_type).dump_schema(allow_ref: allow_ref, shallow: false)
45
- h = @attribute_options[:description] ? { 'description' => @attribute_options[:description] } : {}
46
- h.merge!(type: 'array', items: items)
53
+ base_options.merge!(type: 'array', items: items)
47
54
  else # Attributor::Struct, etc
48
- required_attributes = (type.describe[:requirements] || []).filter { |r| r[:type] == :all }.map { |r| r[:attributes] }.flatten.compact.uniq
49
- props = type.attributes.transform_values do |definition|
50
- # if type has an attribute in its requirements all, then it should be marked as required here
55
+ # Requirements are reported at the outter schema layer, we we need to gather them from the description here
56
+ reqs = type < Praxis::Blueprint ? type.attribute.type.requirements : type.requirements
57
+ # Full requirements specified at the struct level that apply to all are considered required attributes
58
+ required_attributes = (reqs || []).filter { |r| r.type == :all }.map(&:attr_names).flatten.compact
59
+ # Also, if any inner attribute has the required: true option, that, obviously means required as well
60
+ sub_attributes = (attribute || type).attributes
61
+ direct_required = sub_attributes ? sub_attributes.select { |_, a| a.options[:required] == true }.keys : []
62
+ required_attributes.concat(direct_required)
63
+ required_attributes.uniq!
64
+ props = sub_attributes.transform_values do |definition|
51
65
  OpenApi::SchemaObject.new(info: definition).dump_schema(allow_ref: true, shallow: shallow)
52
66
  end
53
- h = { type: :object }
54
- h[:properties] = props if props.presence
55
- h[:required] = required_attributes unless required_attributes.empty?
67
+
68
+ base_options.merge!(type: :object)
69
+ base_options[:properties] = props if props.presence
70
+ base_options[:required] = required_attributes unless required_attributes.empty?
56
71
  end
57
72
  else
73
+ desc = (attribute || type).as_json_schema(shallow: shallow, example: nil)
58
74
  # OpenApi::SchemaObject.new(info:target).dump_schema(allow_ref: allow_ref, shallow: shallow)
59
75
  # TODO...we need to make sure we can use refs in the underlying components after the first level...
60
76
  # ... maybe we need to loop over the attributes if it's an object/struct?...
61
- h = type.as_json_schema(shallow: shallow, example: nil, attribute_options: @attribute_options)
77
+ base_options.merge!(desc)
62
78
  end
63
79
 
64
- # Tag on OpenAPI specific requirements that aren't already added in the underlying JSON schema model
65
- # Nullable: (it seems we need to ensure there is a null option to the enum, if there is one)
66
- is_nullable = @attribute_options[:null]
67
- h[:nullable] = true if is_nullable
68
- h
69
-
80
+ base_options
70
81
  # # TODO: FIXME: return a generic object type if the passed info was weird.
71
82
  # return { type: :object } unless info
72
83
 
@@ -116,6 +127,16 @@ module Praxis
116
127
  raise "Unknown praxis family type: #{praxis_type[:family]}"
117
128
  end
118
129
  end
130
+
131
+ def _slice_options(object, names, prefix: nil)
132
+ subset = object.options.slice(*names)
133
+ return subset if prefix.nil?
134
+
135
+ subset.transform_keys do |key|
136
+ "#{prefix}-#{key}".to_sym
137
+ end
138
+ subset
139
+ end
119
140
  end
120
141
  end
121
142
  end
@@ -110,13 +110,13 @@ module Praxis
110
110
  info_object = OpenApi::InfoObject.new(version: version, api_definition_info: @infos[version])
111
111
  # We only support a server in Praxis ... so we'll use the base path
112
112
  server_params = {}
113
- if(server_info = @infos[version].server)
113
+ if (server_info = @infos[version].server)
114
114
  server_params[:url] = server_info[:url]
115
115
  server_params[:variables] = server_info[:variables] if server_info[:variables]
116
+ server_params[:description] = server_info[:description] if server_info[:description]
116
117
  else
117
118
  server_params[:url] = @infos[version].base_path
118
119
  end
119
- server_params[:description] = server_info[:description] if server_info[:description]
120
120
  server_object = OpenApi::ServerObject.new(**server_params)
121
121
 
122
122
  paths_object = OpenApi::PathsObject.new(resources: resources_by_version[version])
@@ -163,18 +163,18 @@ module Praxis
163
163
  if (version_with = @infos[version].version_with)
164
164
  common_params = {}
165
165
  if version_with.include?(:header)
166
- common_params['ApiVersionHeader'] = {
166
+ common_params['ApiVersionHeader'] = {
167
167
  in: 'header',
168
168
  name: 'X-Api-Version',
169
- schema: { type: 'string', enum: [version]},
169
+ schema: { type: 'string', enum: [version] },
170
170
  required: version_with.size == 1
171
171
  }
172
172
  end
173
173
  if version_with.include?(:params)
174
- common_params['ApiVersionParam'] = {
174
+ common_params['ApiVersionParam'] = {
175
175
  in: :query,
176
176
  name: 'api_version',
177
- schema: { type: 'string', enum: [version]},
177
+ schema: { type: 'string', enum: [version] },
178
178
  required: version_with.size == 1
179
179
  }
180
180
  end
@@ -86,7 +86,6 @@ module Praxis
86
86
  )
87
87
  end
88
88
 
89
-
90
89
  if @active_record_version < Gem::Version.new('6')
91
90
  # ActiveRecord < 6 does not support '.and' so no nested things can be done
92
91
  # But we can still support the case of 1+ flat conditions of the same AND/OR type
@@ -9,4 +9,4 @@ elsif ActiveRecord.gem_version < Gem::Version.new('6.1')
9
9
  else
10
10
  # As of 7.0.4 our 6.1-plus patches still work
11
11
  require_relative 'active_record_patches/6_1_plus'
12
- end
12
+ end
@@ -229,6 +229,11 @@ module Praxis
229
229
  end
230
230
 
231
231
  def validate(_context = Attributor::DEFAULT_ROOT_CONTEXT)
232
+ # Treat a blank block definition for the filters, as a way to allow any valid filter, on any operator and fuz
233
+ # Obviously, the filter names need to be valid, but that's checked below.
234
+ # Also, in some circumstances, you'd need to make sure there is a filters_map entry for the ones that aren't directly translatable to query associations/columns
235
+ return [] if allowed_filters.blank? && allowed_leaves.blank?
236
+
232
237
  parsed_array.each_with_object([]) do |item, errors|
233
238
  attr_name = item[:name]
234
239
  attr_filters = allowed_filters[attr_name]
@@ -10,17 +10,20 @@ module Praxis
10
10
  end
11
11
 
12
12
  def expanded_fields
13
- @expanded_fields ||= request.action.expanded_fields(request, media_type)
13
+ @expanded_fields ||=
14
+ begin
15
+ expansion_filter = respond_to?(:display_attribute?) ? method(:display_attribute?) : nil
16
+ request.action.expanded_fields(request, media_type, expansion_filter)
17
+ end
14
18
  end
15
19
 
16
20
  module ActionDefinitionExtension
17
21
  extend ActiveSupport::Concern
18
22
 
19
- def expanded_fields(request, media_type)
23
+ def expanded_fields(request, media_type, expansion_filter)
20
24
  uses_fields = params&.attributes&.key?(:fields)
21
25
  fields = uses_fields ? request.params.fields.fields : true
22
-
23
- Praxis::FieldExpander.expand(media_type, fields)
26
+ Praxis::FieldExpander.expand(media_type, fields, expansion_filter)
24
27
  end
25
28
  end
26
29
  end
@@ -91,10 +91,6 @@ module Praxis
91
91
 
92
92
  ::Class.new(self) do
93
93
  @media_type = media_type
94
- if media_type
95
- # By default all fields in the mediatype are allowed (but defining a DSL block will override it to more specific ones)
96
- @fields_allowed = media_type.attributes.keys
97
- end
98
94
  # Default is to only enforce the allowed fields in the first ordering position (the one typicall uses an index if there)
99
95
  @enforce_all = OrderingParams.enforce_all_fields
100
96
  end
@@ -2,19 +2,27 @@
2
2
 
3
3
  module Praxis
4
4
  class FieldExpander
5
- def self.expand(object, fields = true)
6
- new.expand(object, fields)
5
+ def self.expand(object, fields = true, displayable_filter = nil)
6
+ # check the displayability of the whole type/attr object at the top as well, not just the inner ones
7
+ if (privs = object.options[:displayable])
8
+ raise 'Attempting to expand fields for a type that uses :displayable, but the system (or at least this controller) does not have a displayable_filter setup.' unless privs && displayable_filter
9
+ return {} unless displayable_filter.call(Array(privs))
10
+
11
+ end
12
+ new(displayable_filter: displayable_filter).expand(object, fields)
7
13
  end
8
14
 
9
15
  attr_reader :stack, :history
10
16
 
11
- def initialize
17
+ # displayable_filter is a proc that takes a set of strings (representing privileges of sort) and returns true if it should be displayed
18
+ def initialize(displayable_filter: nil)
12
19
  @stack = Hash.new do |hash, key|
13
20
  hash[key] = Set.new
14
21
  end
15
22
  @history = Hash.new do |hash, key|
16
23
  hash[key] = {}
17
24
  end
25
+ @displayable_filter = displayable_filter
18
26
  end
19
27
 
20
28
  def expand(object, fields = true)
@@ -50,6 +58,13 @@ module Praxis
50
58
  end
51
59
 
52
60
  attributes.each_with_object({}) do |(name, dumpable), hash|
61
+ # Filter out attributes that are not displayable (if a filter is provided and there is a displayable option)
62
+ if (privs = dumpable.options[:displayable])
63
+ raise "Found field named #{name} using :displayable, but the system (or at least this controller) does not have a displayable_filter setup." unless privs && @displayable_filter
64
+ next unless @displayable_filter.call(Array(privs))
65
+
66
+ end
67
+
53
68
  sub_fields = case fields
54
69
  when true
55
70
  true