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.
Files changed (235) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +54 -0
  3. data/.simplecov +3 -1
  4. data/.travis.yml +2 -1
  5. data/CHANGELOG.md +19 -0
  6. data/CONTRIBUTING.md +2 -79
  7. data/Gemfile +5 -1
  8. data/Guardfile +6 -4
  9. data/LICENSE +0 -2
  10. data/MAINTAINERS.md +1 -0
  11. data/README.md +15 -22
  12. data/Rakefile +4 -2
  13. data/bin/praxis +55 -58
  14. data/lib/praxis/action_definition/headers_dsl_compiler.rb +5 -6
  15. data/lib/praxis/action_definition.rb +65 -95
  16. data/lib/praxis/api_definition.rb +21 -29
  17. data/lib/praxis/api_general_info.rb +55 -66
  18. data/lib/praxis/application.rb +15 -32
  19. data/lib/praxis/blueprint.rb +80 -73
  20. data/lib/praxis/bootloader.rb +24 -33
  21. data/lib/praxis/bootloader_stages/environment.rb +5 -10
  22. data/lib/praxis/bootloader_stages/file_loader.rb +3 -6
  23. data/lib/praxis/bootloader_stages/plugin_config_load.rb +4 -6
  24. data/lib/praxis/bootloader_stages/plugin_config_prepare.rb +2 -2
  25. data/lib/praxis/bootloader_stages/plugin_loader.rb +3 -7
  26. data/lib/praxis/bootloader_stages/plugin_setup.rb +3 -3
  27. data/lib/praxis/bootloader_stages/routing.rb +5 -8
  28. data/lib/praxis/bootloader_stages/subgroup_loader.rb +2 -10
  29. data/lib/praxis/bootloader_stages/warn_unloaded_files.rb +15 -19
  30. data/lib/praxis/callbacks.rb +12 -11
  31. data/lib/praxis/collection.rb +11 -14
  32. data/lib/praxis/config.rb +17 -28
  33. data/lib/praxis/config_hash.rb +2 -1
  34. data/lib/praxis/controller.rb +7 -6
  35. data/lib/praxis/dispatcher.rb +34 -42
  36. data/lib/praxis/docs/open_api/info_object.rb +11 -8
  37. data/lib/praxis/docs/open_api/media_type_object.rb +18 -17
  38. data/lib/praxis/docs/open_api/operation_object.rb +7 -4
  39. data/lib/praxis/docs/open_api/parameter_object.rb +17 -14
  40. data/lib/praxis/docs/open_api/paths_object.rb +11 -9
  41. data/lib/praxis/docs/open_api/request_body_object.rb +14 -13
  42. data/lib/praxis/docs/open_api/response_object.rb +24 -18
  43. data/lib/praxis/docs/open_api/responses_object.rb +3 -1
  44. data/lib/praxis/docs/open_api/schema_object.rb +61 -29
  45. data/lib/praxis/docs/open_api/server_object.rb +5 -2
  46. data/lib/praxis/docs/open_api/tag_object.rb +9 -6
  47. data/lib/praxis/docs/open_api_generator.rb +114 -150
  48. data/lib/praxis/endpoint_definition.rb +60 -77
  49. data/lib/praxis/error_handler.rb +2 -2
  50. data/lib/praxis/exception.rb +2 -0
  51. data/lib/praxis/exceptions/config.rb +3 -1
  52. data/lib/praxis/exceptions/config_load.rb +2 -0
  53. data/lib/praxis/exceptions/config_validation.rb +3 -1
  54. data/lib/praxis/exceptions/invalid_configuration.rb +3 -1
  55. data/lib/praxis/exceptions/invalid_response.rb +3 -1
  56. data/lib/praxis/exceptions/invalid_trait.rb +3 -1
  57. data/lib/praxis/exceptions/stage_not_found.rb +3 -1
  58. data/lib/praxis/exceptions/validation.rb +4 -3
  59. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +163 -149
  60. data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +18 -13
  61. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +13 -9
  62. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +14 -11
  63. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +12 -9
  64. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +8 -5
  65. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +89 -65
  66. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +68 -62
  67. data/lib/praxis/extensions/attribute_filtering.rb +3 -1
  68. data/lib/praxis/extensions/field_expansion.rb +6 -4
  69. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +10 -8
  70. data/lib/praxis/extensions/field_selection/field_selector.rb +91 -92
  71. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +12 -12
  72. data/lib/praxis/extensions/field_selection.rb +3 -1
  73. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +6 -4
  74. data/lib/praxis/extensions/pagination/header_generator.rb +16 -11
  75. data/lib/praxis/extensions/pagination/ordering_params.rb +29 -28
  76. data/lib/praxis/extensions/pagination/pagination_handler.rb +44 -42
  77. data/lib/praxis/extensions/pagination/pagination_params.rb +29 -48
  78. data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +8 -7
  79. data/lib/praxis/extensions/pagination.rb +10 -15
  80. data/lib/praxis/extensions/rails_compat/request_methods.rb +3 -4
  81. data/lib/praxis/extensions/rails_compat.rb +2 -0
  82. data/lib/praxis/extensions/rendering.rb +12 -12
  83. data/lib/praxis/field_expander.rb +8 -9
  84. data/lib/praxis/file_group.rb +8 -12
  85. data/lib/praxis/finalizable.rb +1 -0
  86. data/lib/praxis/handlers/json.rb +5 -2
  87. data/lib/praxis/handlers/plain.rb +2 -1
  88. data/lib/praxis/handlers/www_form.rb +6 -3
  89. data/lib/praxis/handlers/{xml-sample.rb → xml_sample.rb} +26 -22
  90. data/lib/praxis/mapper/active_model_compat.rb +13 -10
  91. data/lib/praxis/mapper/resource.rb +196 -181
  92. data/lib/praxis/mapper/selector_generator.rb +106 -112
  93. data/lib/praxis/mapper/sequel_compat.rb +70 -67
  94. data/lib/praxis/media_type.rb +2 -2
  95. data/lib/praxis/media_type_identifier.rb +26 -22
  96. data/lib/praxis/middleware_app.rb +18 -15
  97. data/lib/praxis/multipart/parser.rb +46 -51
  98. data/lib/praxis/multipart/part.rb +78 -110
  99. data/lib/praxis/notifications.rb +2 -4
  100. data/lib/praxis/plugin.rb +11 -18
  101. data/lib/praxis/plugin_concern.rb +12 -15
  102. data/lib/praxis/plugins/mapper_plugin.rb +15 -13
  103. data/lib/praxis/plugins/pagination_plugin.rb +8 -6
  104. data/lib/praxis/plugins/rails_plugin.rb +33 -28
  105. data/lib/praxis/renderer.rb +11 -15
  106. data/lib/praxis/request.rb +48 -44
  107. data/lib/praxis/request_stages/action.rb +4 -6
  108. data/lib/praxis/request_stages/load_request.rb +2 -4
  109. data/lib/praxis/request_stages/request_stage.rb +19 -23
  110. data/lib/praxis/request_stages/response.rb +4 -6
  111. data/lib/praxis/request_stages/validate.rb +3 -5
  112. data/lib/praxis/request_stages/validate_params_and_headers.rb +15 -22
  113. data/lib/praxis/request_stages/validate_payload.rb +25 -28
  114. data/lib/praxis/request_superclassing.rb +3 -3
  115. data/lib/praxis/resource_definition.rb +1 -0
  116. data/lib/praxis/response.rb +24 -26
  117. data/lib/praxis/response_definition.rb +77 -122
  118. data/lib/praxis/response_template.rb +11 -15
  119. data/lib/praxis/responses/http.rb +23 -44
  120. data/lib/praxis/responses/internal_server_error.rb +18 -21
  121. data/lib/praxis/responses/multipart_ok.rb +4 -9
  122. data/lib/praxis/responses/validation_error.rb +8 -15
  123. data/lib/praxis/route.rb +8 -10
  124. data/lib/praxis/router/rack.rb +13 -7
  125. data/lib/praxis/router/simple.rb +10 -5
  126. data/lib/praxis/router.rb +27 -34
  127. data/lib/praxis/routing_config.rb +52 -29
  128. data/lib/praxis/simple_media_type.rb +5 -8
  129. data/lib/praxis/stage.rb +17 -25
  130. data/lib/praxis/tasks/api_docs.rb +17 -16
  131. data/lib/praxis/tasks/console.rb +3 -1
  132. data/lib/praxis/tasks/environment.rb +2 -0
  133. data/lib/praxis/tasks/routes.rb +26 -24
  134. data/lib/praxis/tasks.rb +3 -1
  135. data/lib/praxis/trait.rb +37 -46
  136. data/lib/praxis/types/fuzzy_hash.rb +13 -14
  137. data/lib/praxis/types/media_type_common.rb +11 -10
  138. data/lib/praxis/types/multipart_array/part_definition.rb +14 -17
  139. data/lib/praxis/types/multipart_array.rb +100 -115
  140. data/lib/praxis/validation_handler.rb +5 -3
  141. data/lib/praxis/version.rb +3 -1
  142. data/lib/praxis.rb +4 -5
  143. data/praxis.gemspec +22 -21
  144. data/spec/functional_spec.rb +44 -56
  145. data/spec/praxis/action_definition_spec.rb +39 -48
  146. data/spec/praxis/api_definition_spec.rb +45 -47
  147. data/spec/praxis/api_general_info_spec.rb +28 -29
  148. data/spec/praxis/application_spec.rb +18 -14
  149. data/spec/praxis/blueprint_spec.rb +33 -34
  150. data/spec/praxis/bootloader_spec.rb +32 -30
  151. data/spec/praxis/callbacks_spec.rb +37 -37
  152. data/spec/praxis/collection_spec.rb +18 -25
  153. data/spec/praxis/config_hash_spec.rb +5 -4
  154. data/spec/praxis/config_spec.rb +27 -26
  155. data/spec/praxis/controller_spec.rb +8 -9
  156. data/spec/praxis/endpoint_definition_spec.rb +25 -32
  157. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +171 -114
  158. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +22 -21
  159. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +112 -60
  160. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +37 -38
  161. data/spec/praxis/extensions/field_expansion_spec.rb +8 -10
  162. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +14 -13
  163. data/spec/praxis/extensions/field_selection/field_selector_spec.rb +9 -16
  164. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +50 -49
  165. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +32 -31
  166. data/spec/praxis/extensions/rendering_spec.rb +9 -9
  167. data/spec/praxis/extensions/support/spec_resources_active_model.rb +32 -49
  168. data/spec/praxis/extensions/support/spec_resources_sequel.rb +48 -48
  169. data/spec/praxis/field_expander_spec.rb +6 -5
  170. data/spec/praxis/file_group_spec.rb +3 -1
  171. data/spec/praxis/handlers/json_spec.rb +6 -5
  172. data/spec/praxis/mapper/resource_spec.rb +39 -29
  173. data/spec/praxis/mapper/selector_generator_spec.rb +80 -46
  174. data/spec/praxis/media_type_identifier_spec.rb +13 -10
  175. data/spec/praxis/media_type_spec.rb +12 -12
  176. data/spec/praxis/middleware_app_spec.rb +23 -22
  177. data/spec/praxis/multipart/parser_spec.rb +7 -9
  178. data/spec/praxis/notifications_spec.rb +4 -4
  179. data/spec/praxis/plugin_concern_spec.rb +5 -6
  180. data/spec/praxis/renderer_spec.rb +10 -9
  181. data/spec/praxis/request_spec.rb +38 -41
  182. data/spec/praxis/request_stages/action_spec.rb +14 -15
  183. data/spec/praxis/request_stages/request_stage_spec.rb +30 -41
  184. data/spec/praxis/request_stages/validate_spec.rb +3 -1
  185. data/spec/praxis/response_definition_spec.rb +79 -92
  186. data/spec/praxis/response_spec.rb +35 -40
  187. data/spec/praxis/responses/internal_server_error_spec.rb +6 -9
  188. data/spec/praxis/responses/validation_error_spec.rb +17 -18
  189. data/spec/praxis/route_spec.rb +4 -7
  190. data/spec/praxis/router_spec.rb +69 -79
  191. data/spec/praxis/routing_config_spec.rb +15 -14
  192. data/spec/praxis/stage_spec.rb +56 -53
  193. data/spec/praxis/trait_spec.rb +17 -17
  194. data/spec/praxis/types/fuzzy_hash_spec.rb +11 -9
  195. data/spec/praxis/types/multipart_array/part_definition_spec.rb +3 -2
  196. data/spec/praxis/types/multipart_array_spec.rb +33 -48
  197. data/spec/spec_app/app/concerns/authenticated.rb +5 -5
  198. data/spec/spec_app/app/concerns/basic_api.rb +3 -1
  199. data/spec/spec_app/app/concerns/log_wrapper.rb +5 -3
  200. data/spec/spec_app/app/controllers/base_class.rb +6 -5
  201. data/spec/spec_app/app/controllers/instances.rb +31 -34
  202. data/spec/spec_app/app/controllers/volumes.rb +6 -6
  203. data/spec/spec_app/app/responses/multipart.rb +1 -2
  204. data/spec/spec_app/app/responses/other_response.rb +2 -2
  205. data/spec/spec_app/config/environment.rb +19 -6
  206. data/spec/spec_app/config.ru +4 -3
  207. data/spec/spec_app/design/api.rb +13 -15
  208. data/spec/spec_app/design/media_types/instance.rb +6 -6
  209. data/spec/spec_app/design/media_types/volume.rb +2 -1
  210. data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -1
  211. data/spec/spec_app/design/resources/instances.rb +11 -17
  212. data/spec/spec_app/design/resources/volume_snapshots.rb +4 -5
  213. data/spec/spec_app/design/resources/volumes.rb +4 -5
  214. data/spec/spec_helper.rb +11 -13
  215. data/spec/support/be_deep_equal_matcher.rb +5 -0
  216. data/spec/support/spec_authorization_plugin.rb +7 -12
  217. data/spec/support/spec_blueprints.rb +5 -4
  218. data/spec/support/spec_complex_authentication_plugin.rb +17 -34
  219. data/spec/support/spec_endpoint_definitions.rb +2 -3
  220. data/spec/support/spec_media_types.rb +28 -35
  221. data/spec/support/spec_resources.rb +22 -16
  222. data/spec/support/spec_simple_authentication_plugin.rb +5 -9
  223. data/tasks/loader.thor +4 -2
  224. data/tasks/thor/app.rb +7 -5
  225. data/tasks/thor/example.rb +23 -22
  226. data/tasks/thor/model.rb +7 -7
  227. data/tasks/thor/scaffold.rb +23 -23
  228. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +0 -8
  229. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +1 -2
  230. metadata +72 -84
  231. data/MAINTAINERS +0 -2
  232. data/TODO.md +0 -25
  233. data/spec/praxis/api_resource_spec.rb +0 -0
  234. data/spec/praxis/dispatcher_spec.rb +0 -0
  235. 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
- return
17
- elsif components.size == 1
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
- attr_reader :allowed_filters
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, enclosing_type = find_filter_attribute(components, media_type)
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
- raise "Error, you've requested to filter by field #{first} which does not exist in the #{type.name} mediatype!\n"
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
- arr << "#{filter_name}#{op}" # Do not add a value for the operators that don't take it
153
+ arr << "#{filter_name}#{op}" # Do not add a value for the operators that don't take it
140
154
  else
141
- attr_example = filter_components.inject(mt_example) do |last, name|
142
- # we can safely do sends, since we've verified the components are valid
143
- last.send(name)
144
- end
145
- arr << "#{filter_name}#{op}#{attr_example}"
146
- end
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 , fuzzy: spec[:fuzzies], node_object: spec[:node_object])
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
- errors << "Filtering by #{attr_name} is not allowed. You can filter by #{allowed_filters.keys.map(&:to_s).join(', ')}"
216
- next
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
- if item[:value].presence
226
- fuzzy_match = attr_filters[:fuzzy_match]
227
- if item[:fuzzy] && !item[:fuzzy].empty? && !fuzzy_match
228
- errors << "Fuzzy matching for #{attr_name} is not allowed (yet '*' was found in the value)"
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 do |filter|
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
- results = values.each do|e|
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, @fuzzies = [nil, nil]
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, @fuzzies = ['', nil]
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
- [raw_val[1..-2], :start_end]
60
- elsif starting
61
- [raw_val[1..-1], :start]
62
- elsif ending
63
- [raw_val[0..-2], :end]
64
- else
65
- [raw_val,nil]
66
- end
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
- '{*}' + val + '{*}'
86
+ "{*}#{val}{*}"
81
87
  when :start
82
- '{*}' + val
88
+ "{*}#{val}"
83
89
  when :end
84
- val +'{*}'
90
+ "#{val}{*}"
85
91
  end
86
92
  end
93
+
87
94
  def dump
88
95
  vals = if values.is_a? Array
89
- dumped = values.map.with_index{|val,i| _dump_value(val, @fuzzies[i])}
90
- "[#{dumped.join(',')}]" # Purposedly enclose in brackets to make sure we differentiate
91
- else
92
- (values == '') ? '""' : _dump_value(values,@fuzzies) # Dump the empty string explicitly with quotes if we've converted no value to empty string
93
- end
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
- unless node[:o]
107
- loaded = Condition.new(triad: node[:triad], parent_group: nil)
108
- else
109
- compactedl = compress_tree(node: node[:l], op: node[:o])
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 = (op.to_s == '&') ? :and : :or
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
- "( " + @items.map(&:dump).join(" #{type.upcase} ") + " )"
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
- accum + item.flattened_conditions
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:, op:)
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] == op
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], op: op)
153
- resultr = compress_tree(node: node[:r], op: op)
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, op: node[:o])
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) { str(',') }
167
- rule(:val_operator) { str('!=') | str('>=') | str('<=') | str('=') | str('<') | str('>')}
168
- rule(:noval_operator) { str('!!') | str('!')}
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 *args
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) { chars >> (comma >> chars ).repeat }
184
+ rule(:value) { chars >> (comma >> chars).repeat }
179
185
 
180
- rule(:triad) {
181
- (name.as(:name) >> val_operator.as(:op) >> value).as(:triad) |
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, [and_kw, 2, :left], [or_kw, 1, :right])
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'praxis/extensions/attribute_filtering/filtering_params'
2
4
  require 'praxis/extensions/attribute_filtering/filter_tree_node'
3
5
  module Praxis
@@ -12,4 +14,4 @@ module Praxis
12
14
  end
13
15
  end
14
16
  end
15
- end
17
+ 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.send(:include, ActionDefinitionExtension)
9
+ Praxis::ActionDefinition.include ActionDefinitionExtension
8
10
  end
9
11
 
10
12
  def expanded_fields
11
- @expansion ||= request.action.expanded_fields(self.request, self.media_type)
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 = self.params && self.params.attributes.key?(: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