praxis 2.0.pre.17 → 2.0.pre.21

Sign up to get free protection for your applications and to get access to all the features.
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