praxis 2.0.pre.17 → 2.0.pre.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +54 -0
- data/.simplecov +3 -1
- data/.travis.yml +2 -1
- data/CHANGELOG.md +19 -0
- data/CONTRIBUTING.md +2 -79
- data/Gemfile +5 -1
- data/Guardfile +6 -4
- data/LICENSE +0 -2
- data/MAINTAINERS.md +1 -0
- data/README.md +15 -22
- data/Rakefile +4 -2
- data/bin/praxis +55 -58
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +5 -6
- data/lib/praxis/action_definition.rb +65 -95
- data/lib/praxis/api_definition.rb +21 -29
- data/lib/praxis/api_general_info.rb +55 -66
- data/lib/praxis/application.rb +15 -32
- data/lib/praxis/blueprint.rb +80 -73
- data/lib/praxis/bootloader.rb +24 -33
- data/lib/praxis/bootloader_stages/environment.rb +5 -10
- data/lib/praxis/bootloader_stages/file_loader.rb +3 -6
- data/lib/praxis/bootloader_stages/plugin_config_load.rb +4 -6
- data/lib/praxis/bootloader_stages/plugin_config_prepare.rb +2 -2
- data/lib/praxis/bootloader_stages/plugin_loader.rb +3 -7
- data/lib/praxis/bootloader_stages/plugin_setup.rb +3 -3
- data/lib/praxis/bootloader_stages/routing.rb +5 -8
- data/lib/praxis/bootloader_stages/subgroup_loader.rb +2 -10
- data/lib/praxis/bootloader_stages/warn_unloaded_files.rb +15 -19
- data/lib/praxis/callbacks.rb +12 -11
- data/lib/praxis/collection.rb +11 -14
- data/lib/praxis/config.rb +17 -28
- data/lib/praxis/config_hash.rb +2 -1
- data/lib/praxis/controller.rb +7 -6
- data/lib/praxis/dispatcher.rb +34 -42
- data/lib/praxis/docs/open_api/info_object.rb +11 -8
- data/lib/praxis/docs/open_api/media_type_object.rb +18 -17
- data/lib/praxis/docs/open_api/operation_object.rb +7 -4
- data/lib/praxis/docs/open_api/parameter_object.rb +17 -14
- data/lib/praxis/docs/open_api/paths_object.rb +11 -9
- data/lib/praxis/docs/open_api/request_body_object.rb +14 -13
- data/lib/praxis/docs/open_api/response_object.rb +24 -18
- data/lib/praxis/docs/open_api/responses_object.rb +3 -1
- data/lib/praxis/docs/open_api/schema_object.rb +61 -29
- data/lib/praxis/docs/open_api/server_object.rb +5 -2
- data/lib/praxis/docs/open_api/tag_object.rb +9 -6
- data/lib/praxis/docs/open_api_generator.rb +114 -150
- data/lib/praxis/endpoint_definition.rb +60 -77
- data/lib/praxis/error_handler.rb +2 -2
- data/lib/praxis/exception.rb +2 -0
- data/lib/praxis/exceptions/config.rb +3 -1
- data/lib/praxis/exceptions/config_load.rb +2 -0
- data/lib/praxis/exceptions/config_validation.rb +3 -1
- data/lib/praxis/exceptions/invalid_configuration.rb +3 -1
- data/lib/praxis/exceptions/invalid_response.rb +3 -1
- data/lib/praxis/exceptions/invalid_trait.rb +3 -1
- data/lib/praxis/exceptions/stage_not_found.rb +3 -1
- data/lib/praxis/exceptions/validation.rb +4 -3
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +163 -149
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +18 -13
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +13 -9
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +14 -11
- data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +12 -9
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +8 -5
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +89 -65
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +68 -62
- data/lib/praxis/extensions/attribute_filtering.rb +3 -1
- data/lib/praxis/extensions/field_expansion.rb +6 -4
- data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +10 -8
- data/lib/praxis/extensions/field_selection/field_selector.rb +91 -92
- data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +12 -12
- data/lib/praxis/extensions/field_selection.rb +3 -1
- data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +6 -4
- data/lib/praxis/extensions/pagination/header_generator.rb +16 -11
- data/lib/praxis/extensions/pagination/ordering_params.rb +29 -28
- data/lib/praxis/extensions/pagination/pagination_handler.rb +44 -42
- data/lib/praxis/extensions/pagination/pagination_params.rb +29 -48
- data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +8 -7
- data/lib/praxis/extensions/pagination.rb +10 -15
- data/lib/praxis/extensions/rails_compat/request_methods.rb +3 -4
- data/lib/praxis/extensions/rails_compat.rb +2 -0
- data/lib/praxis/extensions/rendering.rb +12 -12
- data/lib/praxis/field_expander.rb +8 -9
- data/lib/praxis/file_group.rb +8 -12
- data/lib/praxis/finalizable.rb +1 -0
- data/lib/praxis/handlers/json.rb +5 -2
- data/lib/praxis/handlers/plain.rb +2 -1
- data/lib/praxis/handlers/www_form.rb +6 -3
- data/lib/praxis/handlers/{xml-sample.rb → xml_sample.rb} +26 -22
- data/lib/praxis/mapper/active_model_compat.rb +13 -10
- data/lib/praxis/mapper/resource.rb +196 -181
- data/lib/praxis/mapper/selector_generator.rb +106 -112
- data/lib/praxis/mapper/sequel_compat.rb +70 -67
- data/lib/praxis/media_type.rb +2 -2
- data/lib/praxis/media_type_identifier.rb +26 -22
- data/lib/praxis/middleware_app.rb +18 -15
- data/lib/praxis/multipart/parser.rb +46 -51
- data/lib/praxis/multipart/part.rb +78 -110
- data/lib/praxis/notifications.rb +2 -4
- data/lib/praxis/plugin.rb +11 -18
- data/lib/praxis/plugin_concern.rb +12 -15
- data/lib/praxis/plugins/mapper_plugin.rb +15 -13
- data/lib/praxis/plugins/pagination_plugin.rb +8 -6
- data/lib/praxis/plugins/rails_plugin.rb +33 -28
- data/lib/praxis/renderer.rb +11 -15
- data/lib/praxis/request.rb +48 -44
- data/lib/praxis/request_stages/action.rb +4 -6
- data/lib/praxis/request_stages/load_request.rb +2 -4
- data/lib/praxis/request_stages/request_stage.rb +19 -23
- data/lib/praxis/request_stages/response.rb +4 -6
- data/lib/praxis/request_stages/validate.rb +3 -5
- data/lib/praxis/request_stages/validate_params_and_headers.rb +15 -22
- data/lib/praxis/request_stages/validate_payload.rb +25 -28
- data/lib/praxis/request_superclassing.rb +3 -3
- data/lib/praxis/resource_definition.rb +1 -0
- data/lib/praxis/response.rb +24 -26
- data/lib/praxis/response_definition.rb +77 -122
- data/lib/praxis/response_template.rb +11 -15
- data/lib/praxis/responses/http.rb +23 -44
- data/lib/praxis/responses/internal_server_error.rb +18 -21
- data/lib/praxis/responses/multipart_ok.rb +4 -9
- data/lib/praxis/responses/validation_error.rb +8 -15
- data/lib/praxis/route.rb +8 -10
- data/lib/praxis/router/rack.rb +13 -7
- data/lib/praxis/router/simple.rb +10 -5
- data/lib/praxis/router.rb +27 -34
- data/lib/praxis/routing_config.rb +52 -29
- data/lib/praxis/simple_media_type.rb +5 -8
- data/lib/praxis/stage.rb +17 -25
- data/lib/praxis/tasks/api_docs.rb +17 -16
- data/lib/praxis/tasks/console.rb +3 -1
- data/lib/praxis/tasks/environment.rb +2 -0
- data/lib/praxis/tasks/routes.rb +26 -24
- data/lib/praxis/tasks.rb +3 -1
- data/lib/praxis/trait.rb +37 -46
- data/lib/praxis/types/fuzzy_hash.rb +13 -14
- data/lib/praxis/types/media_type_common.rb +11 -10
- data/lib/praxis/types/multipart_array/part_definition.rb +14 -17
- data/lib/praxis/types/multipart_array.rb +100 -115
- data/lib/praxis/validation_handler.rb +5 -3
- data/lib/praxis/version.rb +3 -1
- data/lib/praxis.rb +4 -5
- data/praxis.gemspec +22 -21
- data/spec/functional_spec.rb +44 -56
- data/spec/praxis/action_definition_spec.rb +39 -48
- data/spec/praxis/api_definition_spec.rb +45 -47
- data/spec/praxis/api_general_info_spec.rb +28 -29
- data/spec/praxis/application_spec.rb +18 -14
- data/spec/praxis/blueprint_spec.rb +33 -34
- data/spec/praxis/bootloader_spec.rb +32 -30
- data/spec/praxis/callbacks_spec.rb +37 -37
- data/spec/praxis/collection_spec.rb +18 -25
- data/spec/praxis/config_hash_spec.rb +5 -4
- data/spec/praxis/config_spec.rb +27 -26
- data/spec/praxis/controller_spec.rb +8 -9
- data/spec/praxis/endpoint_definition_spec.rb +25 -32
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +171 -114
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +22 -21
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +112 -60
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +37 -38
- data/spec/praxis/extensions/field_expansion_spec.rb +8 -10
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +14 -13
- data/spec/praxis/extensions/field_selection/field_selector_spec.rb +9 -16
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +50 -49
- data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +32 -31
- data/spec/praxis/extensions/rendering_spec.rb +9 -9
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +32 -49
- data/spec/praxis/extensions/support/spec_resources_sequel.rb +48 -48
- data/spec/praxis/field_expander_spec.rb +6 -5
- data/spec/praxis/file_group_spec.rb +3 -1
- data/spec/praxis/handlers/json_spec.rb +6 -5
- data/spec/praxis/mapper/resource_spec.rb +39 -29
- data/spec/praxis/mapper/selector_generator_spec.rb +80 -46
- data/spec/praxis/media_type_identifier_spec.rb +13 -10
- data/spec/praxis/media_type_spec.rb +12 -12
- data/spec/praxis/middleware_app_spec.rb +23 -22
- data/spec/praxis/multipart/parser_spec.rb +7 -9
- data/spec/praxis/notifications_spec.rb +4 -4
- data/spec/praxis/plugin_concern_spec.rb +5 -6
- data/spec/praxis/renderer_spec.rb +10 -9
- data/spec/praxis/request_spec.rb +38 -41
- data/spec/praxis/request_stages/action_spec.rb +14 -15
- data/spec/praxis/request_stages/request_stage_spec.rb +30 -41
- data/spec/praxis/request_stages/validate_spec.rb +3 -1
- data/spec/praxis/response_definition_spec.rb +79 -92
- data/spec/praxis/response_spec.rb +35 -40
- data/spec/praxis/responses/internal_server_error_spec.rb +6 -9
- data/spec/praxis/responses/validation_error_spec.rb +17 -18
- data/spec/praxis/route_spec.rb +4 -7
- data/spec/praxis/router_spec.rb +69 -79
- data/spec/praxis/routing_config_spec.rb +15 -14
- data/spec/praxis/stage_spec.rb +56 -53
- data/spec/praxis/trait_spec.rb +17 -17
- data/spec/praxis/types/fuzzy_hash_spec.rb +11 -9
- data/spec/praxis/types/multipart_array/part_definition_spec.rb +3 -2
- data/spec/praxis/types/multipart_array_spec.rb +33 -48
- data/spec/spec_app/app/concerns/authenticated.rb +5 -5
- data/spec/spec_app/app/concerns/basic_api.rb +3 -1
- data/spec/spec_app/app/concerns/log_wrapper.rb +5 -3
- data/spec/spec_app/app/controllers/base_class.rb +6 -5
- data/spec/spec_app/app/controllers/instances.rb +31 -34
- data/spec/spec_app/app/controllers/volumes.rb +6 -6
- data/spec/spec_app/app/responses/multipart.rb +1 -2
- data/spec/spec_app/app/responses/other_response.rb +2 -2
- data/spec/spec_app/config/environment.rb +19 -6
- data/spec/spec_app/config.ru +4 -3
- data/spec/spec_app/design/api.rb +13 -15
- data/spec/spec_app/design/media_types/instance.rb +6 -6
- data/spec/spec_app/design/media_types/volume.rb +2 -1
- data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -1
- data/spec/spec_app/design/resources/instances.rb +11 -17
- data/spec/spec_app/design/resources/volume_snapshots.rb +4 -5
- data/spec/spec_app/design/resources/volumes.rb +4 -5
- data/spec/spec_helper.rb +11 -13
- data/spec/support/be_deep_equal_matcher.rb +5 -0
- data/spec/support/spec_authorization_plugin.rb +7 -12
- data/spec/support/spec_blueprints.rb +5 -4
- data/spec/support/spec_complex_authentication_plugin.rb +17 -34
- data/spec/support/spec_endpoint_definitions.rb +2 -3
- data/spec/support/spec_media_types.rb +28 -35
- data/spec/support/spec_resources.rb +22 -16
- data/spec/support/spec_simple_authentication_plugin.rb +5 -9
- data/tasks/loader.thor +4 -2
- data/tasks/thor/app.rb +7 -5
- data/tasks/thor/example.rb +23 -22
- data/tasks/thor/model.rb +7 -7
- data/tasks/thor/scaffold.rb +23 -23
- data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +0 -8
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +1 -2
- metadata +72 -84
- data/MAINTAINERS +0 -2
- data/TODO.md +0 -25
- data/spec/praxis/api_resource_spec.rb +0 -0
- data/spec/praxis/dispatcher_spec.rb +0 -0
- data/spec/spec_app/app/responses/bulk_response.rb +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Praxis
|
|
2
4
|
module Extensions
|
|
3
5
|
module AttributeFiltering
|
|
4
6
|
class FilterTreeNode
|
|
5
7
|
attr_reader :path, :conditions, :children
|
|
8
|
+
|
|
6
9
|
# Parsed_filters is an Array of {name: X, op: Y, value: Z} ... exactly the format of the FilteringParams.load method
|
|
7
10
|
# It can also contain a :node_object
|
|
8
11
|
def initialize(parsed_filters, path: [])
|
|
@@ -12,9 +15,9 @@ module Praxis
|
|
|
12
15
|
children_data = {} # Hash with keys as names of the first level component of the children nodes (and values as array of matching filters)
|
|
13
16
|
parsed_filters.map do |hash|
|
|
14
17
|
*components = hash[:name].to_s.split('.')
|
|
15
|
-
if components.empty?
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
next if components.empty?
|
|
19
|
+
|
|
20
|
+
if components.size == 1
|
|
18
21
|
@conditions << hash.slice(:name, :op, :value, :fuzzy, :node_object)
|
|
19
22
|
else
|
|
20
23
|
children_data[components.first] ||= []
|
|
@@ -27,10 +30,10 @@ module Praxis
|
|
|
27
30
|
_parent, *rest = item[:name].to_s.split('.')
|
|
28
31
|
item.merge(name: rest.join('.'))
|
|
29
32
|
end
|
|
30
|
-
hash[name] = self.class.new(sub_filters, path: path + [name]
|
|
33
|
+
hash[name] = self.class.new(sub_filters, path: path + [name])
|
|
31
34
|
end
|
|
32
35
|
end
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
end
|
|
36
|
-
end
|
|
39
|
+
end
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'praxis/extensions/attribute_filtering/filters_parser'
|
|
3
4
|
|
|
4
5
|
#
|
|
5
6
|
# Attributor type to define and handle the language to express filtering attributes in listings.
|
|
6
7
|
# Commonly used in a query string parameter value for listing calls.
|
|
7
|
-
#
|
|
8
|
+
#
|
|
8
9
|
# The type allows you to restrict the allowable fields (and their types) based on an existing Mediatype.
|
|
9
10
|
# It also alows you to define exacly what fields (from that MediaType) are allowed, an what operations are
|
|
10
11
|
# supported for each of them. Includes most in/equalities and fuzzy matching options(i.e., leading/trailing `*` )
|
|
@@ -18,6 +19,8 @@ require 'praxis/extensions/attribute_filtering/filters_parser'
|
|
|
18
19
|
# filter 'name', using: ['=', '!=', '!', '!!]
|
|
19
20
|
# filter 'children.created_at', using: ['>', '>=', '<', '<=']
|
|
20
21
|
# filter 'display_name', using: ['=', '!='], fuzzy: true
|
|
22
|
+
# # Or glob any single leaf attribute into one
|
|
23
|
+
# any 'updated_at', using: ['>', '>=', '<', '<=', '=']
|
|
21
24
|
# end
|
|
22
25
|
|
|
23
26
|
module Praxis
|
|
@@ -26,7 +29,7 @@ module Praxis
|
|
|
26
29
|
class FilteringParams
|
|
27
30
|
include Attributor::Type
|
|
28
31
|
include Attributor::Dumpable
|
|
29
|
-
|
|
32
|
+
|
|
30
33
|
attr_reader :parsed_array
|
|
31
34
|
|
|
32
35
|
class DSLCompiler < Attributor::DSLCompiler
|
|
@@ -37,97 +40,108 @@ module Praxis
|
|
|
37
40
|
def filter(name, using: nil, fuzzy: false)
|
|
38
41
|
target.add_filter(name.to_sym, operators: Set.new(using), fuzzy: fuzzy)
|
|
39
42
|
end
|
|
43
|
+
|
|
44
|
+
def any(name, using: nil, fuzzy: false)
|
|
45
|
+
target.add_any(name.to_sym, operators: Set.new(using), fuzzy: fuzzy)
|
|
46
|
+
end
|
|
40
47
|
end
|
|
41
|
-
|
|
48
|
+
|
|
42
49
|
VALUE_OPERATORS = Set.new(['!=', '>=', '<=', '=', '<', '>']).freeze
|
|
43
|
-
NOVALUE_OPERATORS = Set.new(['!','!!']).freeze
|
|
44
|
-
AVAILABLE_OPERATORS = Set.new(VALUE_OPERATORS+NOVALUE_OPERATORS).freeze
|
|
45
|
-
|
|
50
|
+
NOVALUE_OPERATORS = Set.new(['!', '!!']).freeze
|
|
51
|
+
AVAILABLE_OPERATORS = Set.new(VALUE_OPERATORS + NOVALUE_OPERATORS).freeze
|
|
52
|
+
|
|
46
53
|
# Abstract class, which needs to be used by subclassing it through the .for method, to set the allowed filters
|
|
47
54
|
# definition should be a hash, keyed by field name, which contains a hash that can have two pieces of metadata
|
|
48
55
|
# :operators => an array of operators allowed (if empty, means all)
|
|
49
56
|
# :value_type => a type class which the value should match
|
|
50
57
|
# :fuzzy_match => weather or not we allow a "like" type query (for prefix or suffix matching)
|
|
51
58
|
class << self
|
|
52
|
-
attr_reader :media_type
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
attr_reader :media_type, :allowed_filters, :allowed_leaves
|
|
60
|
+
|
|
55
61
|
def for(media_type, **_opts)
|
|
56
62
|
unless media_type < Praxis::MediaType
|
|
57
63
|
raise ArgumentError, "Invalid type: #{media_type.name} for Filters. " \
|
|
58
64
|
'Using the .for method for defining a filter, requires passing a subclass of a MediaType'
|
|
59
65
|
end
|
|
60
|
-
|
|
66
|
+
|
|
61
67
|
::Class.new(self) do
|
|
62
68
|
@media_type = media_type
|
|
63
69
|
@allowed_filters = {}
|
|
70
|
+
@allowed_leaves = {}
|
|
64
71
|
end
|
|
65
72
|
end
|
|
66
|
-
|
|
73
|
+
|
|
67
74
|
def json_schema_type
|
|
68
75
|
:string
|
|
69
76
|
end
|
|
70
|
-
|
|
77
|
+
|
|
71
78
|
def add_filter(name, operators:, fuzzy:)
|
|
72
79
|
components = name.to_s.split('.').map(&:to_sym)
|
|
73
|
-
attribute,
|
|
80
|
+
attribute, _enclosing_type = find_filter_attribute(components, media_type)
|
|
74
81
|
raise 'Invalid set of operators passed' unless AVAILABLE_OPERATORS.superset?(operators)
|
|
75
|
-
|
|
82
|
+
|
|
76
83
|
@allowed_filters[name] = {
|
|
77
84
|
value_type: attribute.type,
|
|
78
85
|
operators: operators,
|
|
79
86
|
fuzzy_match: fuzzy
|
|
80
87
|
}
|
|
81
88
|
end
|
|
89
|
+
|
|
90
|
+
def add_any(name, operators:, fuzzy:)
|
|
91
|
+
raise 'Invalid set of operators passed' unless AVAILABLE_OPERATORS.superset?(operators)
|
|
92
|
+
|
|
93
|
+
@allowed_leaves[name] = {
|
|
94
|
+
operators: operators,
|
|
95
|
+
fuzzy_match: fuzzy
|
|
96
|
+
}
|
|
97
|
+
end
|
|
82
98
|
end
|
|
83
|
-
|
|
99
|
+
|
|
84
100
|
def self.native_type
|
|
85
101
|
self
|
|
86
102
|
end
|
|
87
|
-
|
|
103
|
+
|
|
88
104
|
def self.name
|
|
89
105
|
'Praxis::Types::FilteringParams'
|
|
90
106
|
end
|
|
91
|
-
|
|
107
|
+
|
|
92
108
|
def self.display_name
|
|
93
109
|
'Filtering'
|
|
94
110
|
end
|
|
95
|
-
|
|
111
|
+
|
|
96
112
|
def self.family
|
|
97
113
|
'string'
|
|
98
114
|
end
|
|
99
|
-
|
|
115
|
+
|
|
100
116
|
def self.constructable?
|
|
101
117
|
true
|
|
102
118
|
end
|
|
103
|
-
|
|
119
|
+
|
|
104
120
|
def self.construct(definition, **options)
|
|
105
121
|
return self if definition.nil?
|
|
106
|
-
|
|
122
|
+
|
|
107
123
|
DSLCompiler.new(self, **options).parse(*definition)
|
|
108
124
|
self
|
|
109
125
|
end
|
|
110
|
-
|
|
126
|
+
|
|
111
127
|
def self.find_filter_attribute(name_components, type)
|
|
112
128
|
type = type.member_type if type < Attributor::Collection
|
|
113
129
|
first, *rest = name_components
|
|
114
130
|
first_attr = type.attributes[first]
|
|
115
|
-
unless first_attr
|
|
116
|
-
|
|
117
|
-
end
|
|
118
|
-
|
|
131
|
+
raise "Error, you've requested to filter by field '#{first}' which does not exist in the #{type.name} mediatype!\n" unless first_attr
|
|
132
|
+
|
|
119
133
|
return find_filter_attribute(rest, first_attr.type) if rest.present?
|
|
120
|
-
|
|
134
|
+
|
|
121
135
|
[first_attr, type] # Return the attribute and associated enclosing type
|
|
122
136
|
end
|
|
123
|
-
|
|
137
|
+
|
|
124
138
|
def self.example(_context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
|
125
139
|
fields = if media_type
|
|
126
140
|
mt_example = media_type.example
|
|
127
141
|
pickable_fields = mt_example.object.keys & allowed_filters.keys
|
|
128
142
|
pickable_fields.sample(2).each_with_object([]) do |filter_name, arr|
|
|
129
143
|
op = allowed_filters[filter_name][:operators].to_a.sample(1).first
|
|
130
|
-
|
|
144
|
+
|
|
131
145
|
# Switch this to pick the right example attribute from the mt example
|
|
132
146
|
filter_components = filter_name.to_s.split('.').map(&:to_sym)
|
|
133
147
|
mapped_attribute, _enclosing_type = find_filter_attribute(filter_components, media_type)
|
|
@@ -136,32 +150,32 @@ module Praxis
|
|
|
136
150
|
" MediaType #{media_type.name}"
|
|
137
151
|
end
|
|
138
152
|
if NOVALUE_OPERATORS.include?(op)
|
|
139
|
-
|
|
153
|
+
arr << "#{filter_name}#{op}" # Do not add a value for the operators that don't take it
|
|
140
154
|
else
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
155
|
+
attr_example = filter_components.inject(mt_example) do |last, name|
|
|
156
|
+
# we can safely do sends, since we've verified the components are valid
|
|
157
|
+
last.send(name)
|
|
158
|
+
end
|
|
159
|
+
arr << "#{filter_name}#{op}#{attr_example}"
|
|
160
|
+
end
|
|
147
161
|
end.join('&')
|
|
148
162
|
else
|
|
149
163
|
'name=Joe&date>2017-01-01'
|
|
150
164
|
end
|
|
151
165
|
load(fields)
|
|
152
166
|
end
|
|
153
|
-
|
|
167
|
+
|
|
154
168
|
def self.validate(value, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute = nil)
|
|
155
169
|
instance = load(value, context)
|
|
156
170
|
instance.validate(context)
|
|
157
171
|
end
|
|
158
|
-
|
|
172
|
+
|
|
159
173
|
def self.load(filters, _context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
|
160
174
|
return filters if filters.is_a?(native_type)
|
|
161
175
|
return new if filters.nil? || filters.blank?
|
|
162
176
|
|
|
163
177
|
parsed = Parser.new.parse(filters)
|
|
164
|
-
|
|
178
|
+
|
|
165
179
|
tree = ConditionGroup.load(parsed)
|
|
166
180
|
|
|
167
181
|
rr = tree.flattened_conditions
|
|
@@ -182,15 +196,15 @@ module Praxis
|
|
|
182
196
|
else
|
|
183
197
|
spec[:values]
|
|
184
198
|
end
|
|
185
|
-
accum.push(name: attr_name, op: spec[:op], value: coerced
|
|
199
|
+
accum.push(name: attr_name, op: spec[:op], value: coerced, fuzzy: spec[:fuzzies], node_object: spec[:node_object])
|
|
186
200
|
end
|
|
187
201
|
new(accum)
|
|
188
202
|
end
|
|
189
|
-
|
|
203
|
+
|
|
190
204
|
def self.dump(value, **_opts)
|
|
191
205
|
load(value).dump
|
|
192
206
|
end
|
|
193
|
-
|
|
207
|
+
|
|
194
208
|
def self.describe(_root = false, example: nil)
|
|
195
209
|
hash = super
|
|
196
210
|
if allowed_filters
|
|
@@ -199,38 +213,47 @@ module Praxis
|
|
|
199
213
|
accum[name][:fuzzy] = true if spec[:fuzzy_match]
|
|
200
214
|
end
|
|
201
215
|
end
|
|
202
|
-
|
|
216
|
+
|
|
203
217
|
hash
|
|
204
218
|
end
|
|
205
|
-
|
|
219
|
+
|
|
206
220
|
def initialize(parsed = [])
|
|
207
221
|
@parsed_array = parsed
|
|
208
222
|
end
|
|
209
|
-
|
|
223
|
+
|
|
224
|
+
def matching_leaf_filter(filter_string)
|
|
225
|
+
return nil unless allowed_leaves.keys.present?
|
|
226
|
+
|
|
227
|
+
last_component = filter_string.to_s.split('.').last.to_sym
|
|
228
|
+
allowed_leaves[last_component]
|
|
229
|
+
end
|
|
230
|
+
|
|
210
231
|
def validate(_context = Attributor::DEFAULT_ROOT_CONTEXT)
|
|
211
232
|
parsed_array.each_with_object([]) do |item, errors|
|
|
212
233
|
attr_name = item[:name]
|
|
213
234
|
attr_filters = allowed_filters[attr_name]
|
|
214
235
|
unless attr_filters
|
|
215
|
-
|
|
216
|
-
|
|
236
|
+
# does not match a complete filter, let's check if it matches an 'any' filter on the last component
|
|
237
|
+
attr_filters = matching_leaf_filter(attr_name)
|
|
238
|
+
unless attr_filters
|
|
239
|
+
msg = "Filtering by #{attr_name} is not allowed. You can filter by #{allowed_filters.keys.map(&:to_s).join(', ')}"
|
|
240
|
+
msg += " or leaf attributes matching #{allowed_leaves.keys.map(&:to_s).join(', ')}" if allowed_leaves.keys.presence
|
|
241
|
+
errors << msg
|
|
242
|
+
next
|
|
243
|
+
end
|
|
217
244
|
end
|
|
218
245
|
allowed_operators = attr_filters[:operators]
|
|
219
|
-
unless allowed_operators.include?(item[:op])
|
|
220
|
-
errors << "Operator #{item[:op]} not allowed for filter #{attr_name}"
|
|
221
|
-
end
|
|
246
|
+
errors << "Operator #{item[:op]} not allowed for filter #{attr_name}" unless allowed_operators.include?(item[:op])
|
|
222
247
|
value_type = attr_filters[:value_type]
|
|
223
248
|
next unless value_type == Attributor::String
|
|
224
249
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
end
|
|
230
|
-
end
|
|
250
|
+
next unless item[:value].presence
|
|
251
|
+
|
|
252
|
+
fuzzy_match = attr_filters[:fuzzy_match]
|
|
253
|
+
errors << "Fuzzy matching for #{attr_name} is not allowed (yet '*' was found in the value)" if item[:fuzzy] && !item[:fuzzy].empty? && !fuzzy_match
|
|
231
254
|
end
|
|
232
255
|
end
|
|
233
|
-
|
|
256
|
+
|
|
234
257
|
# Dump back string parseable form
|
|
235
258
|
def dump
|
|
236
259
|
parsed_array.each_with_object([]) do |item, arr|
|
|
@@ -238,17 +261,20 @@ module Praxis
|
|
|
238
261
|
arr << "#{field}#{item[:op]}#{item[:value]}"
|
|
239
262
|
end.join('&')
|
|
240
263
|
end
|
|
241
|
-
|
|
242
|
-
def each
|
|
243
|
-
parsed_array&.each
|
|
244
|
-
yield filter
|
|
245
|
-
end
|
|
264
|
+
|
|
265
|
+
def each(&block)
|
|
266
|
+
parsed_array&.each(&block)
|
|
246
267
|
end
|
|
247
|
-
|
|
268
|
+
|
|
248
269
|
def allowed_filters
|
|
249
270
|
# Class method defined by the subclassing Class (using .for)
|
|
250
271
|
self.class.allowed_filters
|
|
251
272
|
end
|
|
273
|
+
|
|
274
|
+
def allowed_leaves
|
|
275
|
+
# Class method defined by the subclassing Class (using .for)
|
|
276
|
+
self.class.allowed_leaves
|
|
277
|
+
end
|
|
252
278
|
end
|
|
253
279
|
end
|
|
254
280
|
end
|
|
@@ -260,5 +286,3 @@ module Praxis
|
|
|
260
286
|
FilteringParams = Praxis::Extensions::AttributeFiltering::FilteringParams
|
|
261
287
|
end
|
|
262
288
|
end
|
|
263
|
-
|
|
264
|
-
# rubocop:enable all
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'parslet'
|
|
2
4
|
|
|
3
5
|
module Praxis
|
|
@@ -21,15 +23,14 @@ module Praxis
|
|
|
21
23
|
@op = spec[:op].to_s
|
|
22
24
|
|
|
23
25
|
if values.empty?
|
|
24
|
-
@values =
|
|
26
|
+
@values = ''
|
|
25
27
|
@fuzzies = nil
|
|
26
28
|
elsif values.size == 1
|
|
27
|
-
raw_val = values.first[:value].to_s
|
|
28
29
|
@values, @fuzzies = _compute_fuzzy(values.first[:value].to_s)
|
|
29
30
|
else
|
|
30
31
|
@values = []
|
|
31
32
|
@fuzzies = []
|
|
32
|
-
|
|
33
|
+
values.each do |e|
|
|
33
34
|
val, fuz = _compute_fuzzy(e[:value].to_s)
|
|
34
35
|
@values.push val
|
|
35
36
|
@fuzzies.push fuz
|
|
@@ -38,59 +39,65 @@ module Praxis
|
|
|
38
39
|
else # No values for the operand
|
|
39
40
|
@name = triad[:name].to_sym
|
|
40
41
|
@op = triad[:op].to_s
|
|
41
|
-
if ['!','!!'].include?(@op)
|
|
42
|
-
@values
|
|
42
|
+
if ['!', '!!'].include?(@op)
|
|
43
|
+
@values = nil
|
|
44
|
+
@fuzzies = nil
|
|
43
45
|
else
|
|
44
46
|
# Value operand without value? => convert it to empty string
|
|
45
47
|
raise "Interesting, didn't know this could happen. Oops!" if triad[:value].is_a?(Array) && !triad[:value].empty?
|
|
48
|
+
|
|
46
49
|
if triad[:value] == []
|
|
47
|
-
@values
|
|
50
|
+
@values = ''
|
|
51
|
+
@fuzzies = nil
|
|
48
52
|
else
|
|
49
53
|
@values, @fuzzies = _compute_fuzzy(triad[:value].to_s)
|
|
50
54
|
end
|
|
51
55
|
end
|
|
52
56
|
end
|
|
53
57
|
end
|
|
58
|
+
|
|
54
59
|
# Takes a raw val, and spits out the output val (unescaped), and the fuzzy definition
|
|
55
60
|
def _compute_fuzzy(raw_val)
|
|
56
61
|
starting = raw_val[0] == '*'
|
|
57
62
|
ending = raw_val[-1] == '*'
|
|
58
63
|
newval, fuzzy = if starting && ending
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
[raw_val[1..-2], :start_end]
|
|
65
|
+
elsif starting
|
|
66
|
+
[raw_val[1..-1], :start]
|
|
67
|
+
elsif ending
|
|
68
|
+
[raw_val[0..-2], :end]
|
|
69
|
+
else
|
|
70
|
+
[raw_val, nil]
|
|
71
|
+
end
|
|
67
72
|
newval = CGI.unescape(newval) if newval
|
|
68
|
-
[newval,fuzzy]
|
|
73
|
+
[newval, fuzzy]
|
|
69
74
|
end
|
|
75
|
+
|
|
70
76
|
def flattened_conditions
|
|
71
|
-
[{name: @name, op: @op, values: @values, fuzzies: @fuzzies, node_object: self}]
|
|
77
|
+
[{ name: @name, op: @op, values: @values, fuzzies: @fuzzies, node_object: self }]
|
|
72
78
|
end
|
|
73
79
|
|
|
74
80
|
# Dumps the value, marking where the fuzzy might be, and removing the * to differentiate from literals
|
|
75
|
-
def _dump_value(val,fuzzy)
|
|
81
|
+
def _dump_value(val, fuzzy)
|
|
76
82
|
case fuzzy
|
|
77
83
|
when nil
|
|
78
84
|
val
|
|
79
85
|
when :start_end
|
|
80
|
-
|
|
86
|
+
"{*}#{val}{*}"
|
|
81
87
|
when :start
|
|
82
|
-
|
|
88
|
+
"{*}#{val}"
|
|
83
89
|
when :end
|
|
84
|
-
|
|
90
|
+
"#{val}{*}"
|
|
85
91
|
end
|
|
86
92
|
end
|
|
93
|
+
|
|
87
94
|
def dump
|
|
88
95
|
vals = if values.is_a? Array
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
dumped = values.map.with_index { |val, i| _dump_value(val, @fuzzies[i]) }
|
|
97
|
+
"[#{dumped.join(',')}]" # Purposedly enclose in brackets to make sure we differentiate
|
|
98
|
+
else
|
|
99
|
+
values == '' ? '""' : _dump_value(values, @fuzzies) # Dump the empty string explicitly with quotes if we've converted no value to empty string
|
|
100
|
+
end
|
|
94
101
|
"#{name}#{op}#{vals}"
|
|
95
102
|
end
|
|
96
103
|
end
|
|
@@ -99,24 +106,24 @@ module Praxis
|
|
|
99
106
|
# to be applied to its items children
|
|
100
107
|
class ConditionGroup
|
|
101
108
|
attr_reader :items, :type
|
|
102
|
-
attr_accessor :parent_group
|
|
103
|
-
attr_accessor :associated_query # Metadata to be used by whomever is manipulating this
|
|
109
|
+
attr_accessor :parent_group, :associated_query # Metadata to be used by whomever is manipulating this
|
|
104
110
|
|
|
105
111
|
def self.load(node)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
compactedr = compress_tree(node: node[:r], op: node[:o])
|
|
111
|
-
compacted = {op: node[:o], items: compactedl + compactedr }
|
|
112
|
+
if node[:o]
|
|
113
|
+
compactedl = compress_tree(node: node[:l], operator: node[:o])
|
|
114
|
+
compactedr = compress_tree(node: node[:r], operator: node[:o])
|
|
115
|
+
compacted = { op: node[:o], items: compactedl + compactedr }
|
|
112
116
|
|
|
113
|
-
loaded = ConditionGroup.new(**compacted, parent_group: nil)
|
|
117
|
+
loaded = ConditionGroup.new(**compacted, parent_group: nil)
|
|
118
|
+
else
|
|
119
|
+
loaded = Condition.new(triad: node[:triad], parent_group: nil)
|
|
114
120
|
end
|
|
115
121
|
loaded
|
|
116
122
|
end
|
|
117
123
|
|
|
124
|
+
# rubocop:disable Naming/MethodParameterName
|
|
118
125
|
def initialize(op:, items:, parent_group:)
|
|
119
|
-
@type =
|
|
126
|
+
@type = op.to_s == '&' ? :and : :or
|
|
120
127
|
@items = items.map do |item|
|
|
121
128
|
if item[:op]
|
|
122
129
|
ConditionGroup.new(**item, parent_group: self)
|
|
@@ -126,35 +133,34 @@ module Praxis
|
|
|
126
133
|
end
|
|
127
134
|
@parent_group = parent_group
|
|
128
135
|
end
|
|
136
|
+
# rubocop:enable Naming/MethodParameterName
|
|
129
137
|
|
|
130
138
|
def dump
|
|
131
|
-
"(
|
|
139
|
+
"( #{@items.map(&:dump).join(" #{type.upcase} ")} )"
|
|
132
140
|
end
|
|
133
141
|
|
|
134
142
|
# Returns an array with flat conditions from all child triad conditions
|
|
135
143
|
def flattened_conditions
|
|
136
144
|
@items.inject([]) do |accum, item|
|
|
137
|
-
|
|
145
|
+
accum + item.flattened_conditions
|
|
138
146
|
end
|
|
139
147
|
end
|
|
140
148
|
|
|
141
149
|
# Given a binary tree of operand conditions, transform it to a multi-leaf tree
|
|
142
150
|
# where a single condition node has potentially multiple subtrees for the same operation (instead of 2)
|
|
143
|
-
# For example (&, (&, a, b), (|, c, d)) => (&, a, b, (|, c, d))
|
|
144
|
-
def self.compress_tree(node:,
|
|
145
|
-
if node[:triad]
|
|
146
|
-
return [node]
|
|
147
|
-
end
|
|
151
|
+
# For example (&, (&, a, b), (|, c, d)) => (&, a, b, (|, c, d))
|
|
152
|
+
def self.compress_tree(node:, operator:)
|
|
153
|
+
return [node] if node[:triad]
|
|
148
154
|
|
|
149
155
|
# It is an op node
|
|
150
|
-
if node[:o] ==
|
|
156
|
+
if node[:o] == operator
|
|
151
157
|
# compatible op as parent, collect my compacted children and return them up skipping my op
|
|
152
|
-
resultl = compress_tree(node: node[:l],
|
|
153
|
-
resultr = compress_tree(node: node[:r],
|
|
154
|
-
resultl+resultr
|
|
158
|
+
resultl = compress_tree(node: node[:l], operator: operator)
|
|
159
|
+
resultr = compress_tree(node: node[:r], operator: operator)
|
|
160
|
+
resultl + resultr
|
|
155
161
|
else
|
|
156
|
-
collected = compress_tree(node: node,
|
|
157
|
-
[{op: node[:o], items: collected }]
|
|
162
|
+
collected = compress_tree(node: node, operator: node[:o])
|
|
163
|
+
[{ op: node[:o], items: collected }]
|
|
158
164
|
end
|
|
159
165
|
end
|
|
160
166
|
end
|
|
@@ -163,31 +169,31 @@ module Praxis
|
|
|
163
169
|
root :expression
|
|
164
170
|
rule(:lparen) { str('(') }
|
|
165
171
|
rule(:rparen) { str(')') }
|
|
166
|
-
rule(:comma)
|
|
167
|
-
rule(:val_operator)
|
|
168
|
-
rule(:noval_operator)
|
|
172
|
+
rule(:comma) { str(',') }
|
|
173
|
+
rule(:val_operator) { str('!=') | str('>=') | str('<=') | str('=') | str('<') | str('>') }
|
|
174
|
+
rule(:noval_operator) { str('!!') | str('!') }
|
|
169
175
|
rule(:and_kw) { str('&') }
|
|
170
176
|
rule(:or_kw) { str('|') }
|
|
171
177
|
|
|
172
|
-
def infix
|
|
178
|
+
def infix(*args)
|
|
173
179
|
Infix.new(*args)
|
|
174
180
|
end
|
|
175
|
-
|
|
181
|
+
|
|
176
182
|
rule(:name) { match('[a-zA-Z0-9_\.]').repeat(1) } # TODO: are these the only characters that we allow for names?
|
|
177
183
|
rule(:chars) { match('[^&|(),]').repeat(0).as(:value) }
|
|
178
|
-
rule(:value)
|
|
184
|
+
rule(:value) { chars >> (comma >> chars).repeat }
|
|
179
185
|
|
|
180
|
-
rule(:triad)
|
|
181
|
-
|
|
182
|
-
(name.as(:name) >> noval_operator.as(:op)).as(:triad) |
|
|
186
|
+
rule(:triad) do
|
|
187
|
+
(name.as(:name) >> val_operator.as(:op) >> value).as(:triad) |
|
|
188
|
+
(name.as(:name) >> noval_operator.as(:op)).as(:triad) |
|
|
183
189
|
lparen >> expression >> rparen
|
|
184
|
-
|
|
190
|
+
end
|
|
185
191
|
|
|
186
|
-
rule(:expression)
|
|
187
|
-
infix_expression(triad,
|
|
188
|
-
|
|
192
|
+
rule(:expression) do
|
|
193
|
+
infix_expression(triad, [and_kw, 2, :left], [or_kw, 1, :right])
|
|
194
|
+
end
|
|
189
195
|
end
|
|
190
196
|
end
|
|
191
197
|
end
|
|
192
198
|
end
|
|
193
|
-
end
|
|
199
|
+
end
|
|
@@ -1,24 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Praxis
|
|
2
4
|
module Extensions
|
|
3
5
|
module FieldExpansion
|
|
4
6
|
extend ActiveSupport::Concern
|
|
5
7
|
|
|
6
8
|
included do
|
|
7
|
-
Praxis::ActionDefinition.
|
|
9
|
+
Praxis::ActionDefinition.include ActionDefinitionExtension
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def expanded_fields
|
|
11
|
-
@
|
|
13
|
+
@expanded_fields ||= request.action.expanded_fields(request, media_type)
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
module ActionDefinitionExtension
|
|
15
17
|
extend ActiveSupport::Concern
|
|
16
18
|
|
|
17
19
|
def expanded_fields(request, media_type)
|
|
18
|
-
uses_fields =
|
|
20
|
+
uses_fields = params&.attributes&.key?(:fields)
|
|
19
21
|
fields = uses_fields ? request.params.fields.fields : true
|
|
20
22
|
|
|
21
|
-
Praxis::FieldExpander.expand(media_type,fields)
|
|
23
|
+
Praxis::FieldExpander.expand(media_type, fields)
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
26
|
end
|