praxis 2.0.pre.18 → 2.0.pre.19
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +54 -0
- data/.simplecov +3 -1
- data/.travis.yml +2 -1
- data/CHANGELOG.md +6 -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 +64 -94
- 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 +64 -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 +10 -13
- 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 +6 -3
- 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 +16 -16
- data/lib/praxis/docs/open_api/server_object.rb +5 -2
- data/lib/praxis/docs/open_api/tag_object.rb +6 -3
- data/lib/praxis/docs/open_api_generator.rb +92 -95
- 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 +171 -180
- 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 +46 -47
- 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 -16
- data/lib/praxis/request_stages/validate_payload.rb +25 -27
- data/lib/praxis/request_superclassing.rb +3 -3
- data/lib/praxis/resource_definition.rb +1 -0
- data/lib/praxis/response.rb +13 -25
- 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 +15 -15
- 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 +88 -112
- 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 +40 -52
- data/spec/praxis/action_definition_spec.rb +36 -46
- 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 +27 -30
- data/spec/praxis/mapper/selector_generator_spec.rb +50 -50
- 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 +28 -39
- 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 -18
- 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 +5 -5
- 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 +9 -15
- 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 +2 -1
- 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 +20 -18
- 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
|