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,4 +1,4 @@
1
-
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Praxis
4
4
  module Extensions
@@ -10,13 +10,14 @@ module Praxis
10
10
  # This is necessary as (the latest AR code):
11
11
  # * does not carry over "references" in joins if they are not SqlLiterals
12
12
  # * but, at the same time, it indexes the references using the .to_sym value (which is really expected to be the normal string, without quotes)
13
- # If we pass a normal SqlLiteral, instead of our wrapper, without quoting the table, the current AR code will never quote it to form the
13
+ # If we pass a normal SqlLiteral, instead of our wrapper, without quoting the table, the current AR code will never quote it to form the
14
14
  # SQL string, as it's already a literal...so our "/" type separators as names won't work without quoting.
15
15
  class QuasiSqlLiteral < Arel::Nodes::SqlLiteral
16
16
  def initialize(quoted:, symbolized:)
17
17
  @symbolized = symbolized
18
18
  super(quoted)
19
19
  end
20
+
20
21
  def to_sym
21
22
  @symbolized
22
23
  end
@@ -27,67 +28,61 @@ module Praxis
27
28
  attr_reader :model, :filters_map
28
29
 
29
30
  # Base query to build upon
30
- def initialize(query: , model:, filters_map:, debug: false)
31
- # Note: Do not make the initial_query an attr reader to make sure we don't count/leak on modifying it. Easier to mostly use class methods
31
+ def initialize(query:, model:, filters_map:, debug: false)
32
+ # NOTE: Do not make the initial_query an attr reader to make sure we don't count/leak on modifying it. Easier to mostly use class methods
32
33
  @initial_query = query
33
34
  @model = model
34
35
  @filters_map = filters_map
35
- @logger = debug ? Logger.new(STDOUT) : nil
36
+ @logger = debug ? Logger.new($stdout) : nil
36
37
  @active_record_version_maj = ActiveRecord.gem_version.segments[0]
37
38
  end
38
-
39
+
39
40
  def debug_query(msg, query)
40
- @logger.info(msg + query.to_sql) if @logger
41
+ @logger&.info(msg + query.to_sql)
41
42
  end
42
43
 
43
44
  def generate(filters)
44
45
  # Resolve the names and values first, based on filters_map
45
46
  root_node = _convert_to_treenode(filters)
46
47
  crafted = craft_filter_query(root_node, for_model: @model)
47
- debug_query("SQL due to filters: ", crafted.all)
48
+ debug_query('SQL due to filters: ', crafted.all)
48
49
  crafted
49
50
  end
50
51
 
51
52
  def craft_filter_query(nodetree, for_model:)
52
53
  result = _compute_joins_and_conditions_data(nodetree, model: for_model, parent_reflection: nil)
53
54
  return @initial_query if result[:conditions].empty?
54
-
55
+
55
56
  # Find the root group (usually an AND group) but can be an OR group, or nil if there's only 1 condition
56
57
  root_parent_group = result[:conditions].first[:node_object].parent_group || result[:conditions].first[:node_object]
57
- while root_parent_group.parent_group != nil
58
- root_parent_group = root_parent_group.parent_group
59
- end
58
+ root_parent_group = root_parent_group.parent_group until root_parent_group.parent_group.nil?
60
59
 
61
60
  # Process the joins
62
61
  query_with_joins = result[:associations_hash].empty? ? @initial_query : @initial_query.left_outer_joins(result[:associations_hash])
63
62
 
64
63
  # Proc to apply a single condition
65
- apply_single_condition = Proc.new do |condition, associated_query|
64
+ apply_single_condition = proc do |condition, associated_query|
66
65
  colo = condition[:model].columns_hash[condition[:name].to_s]
67
66
  column_prefix = condition[:column_prefix]
68
67
  association_key_column = \
69
- if ref = condition[:parent_reflection]
68
+ if (ref = condition[:parent_reflection])
70
69
  # get the target model of the association(where the assoc pk is)
71
- target_model = condition[:parent_reflection].klass
72
- target_model.columns_hash[condition[:parent_reflection].association_primary_key]
73
- else
74
- nil
70
+ target_model = ref.klass
71
+ target_model.columns_hash[ref.association_primary_key]
75
72
  end
76
73
 
77
74
  # Mark where clause referencing the appropriate alias IF it's not the root table, as there is no association to reference
78
- # If we added root table as a reference, we better make sure it is not quoted, as it actually makes AR to see it as an
75
+ # If we added root table as a reference, we better make sure it is not quoted, as it actually makes AR to see it as an
79
76
  # unmatched reference and eager loads the whole association (it means eager load ALL the things). Not good.
80
- unless for_model.table_name == column_prefix
81
- associated_query = associated_query.references(build_reference_value(column_prefix, query: associated_query))
82
- end
77
+ associated_query = associated_query.references(build_reference_value(column_prefix, query: associated_query)) unless for_model.table_name == column_prefix
83
78
  self.class.add_clause(
84
- query: associated_query,
85
- column_prefix: column_prefix,
86
- column_object: colo,
87
- op: condition[:op],
79
+ query: associated_query,
80
+ column_prefix: column_prefix,
81
+ column_object: colo,
82
+ op: condition[:op],
88
83
  value: condition[:value],
89
84
  fuzzy: condition[:fuzzy],
90
- association_key_column: association_key_column,
85
+ association_key_column: association_key_column
91
86
  )
92
87
  end
93
88
 
@@ -97,7 +92,7 @@ module Praxis
97
92
  if root_parent_group.is_a?(FilteringParams::Condition)
98
93
  # A Single condition it is easy to handle
99
94
  apply_single_condition.call(result[:conditions].first, query_with_joins)
100
- elsif root_parent_group.items.all?{|i| i.is_a?(FilteringParams::Condition)}
95
+ elsif root_parent_group.items.all? { |i| i.is_a?(FilteringParams::Condition) }
101
96
  # Only 1 top level root, with only with simple condition items
102
97
  if root_parent_group.type == :and
103
98
  result[:conditions].reverse.inject(query_with_joins) do |accum, condition|
@@ -113,115 +108,40 @@ module Praxis
113
108
  end
114
109
  end
115
110
  else
116
- raise "Mixing AND and OR conditions is not supported for ActiveRecord <6."
111
+ raise 'Mixing AND and OR conditions is not supported for ActiveRecord <6.'
117
112
  end
118
113
  else # ActiveRecord 6+
119
114
  # Process the conditions in a depth-first order, and return the resulting query
120
115
  _depth_first_traversal(
121
- root_query: query_with_joins,
122
- root_node: root_parent_group,
123
- conditions: result[:conditions],
116
+ root_query: query_with_joins,
117
+ root_node: root_parent_group,
118
+ conditions: result[:conditions],
124
119
  &apply_single_condition
125
120
  )
126
121
  end
127
122
  end
128
123
 
129
- private
130
- def _depth_first_traversal(root_query:, root_node:, conditions:, &block)
131
- # Save the associated query for non-leaves
132
- root_node.associated_query = root_query if root_node.is_a?(FilteringParams::ConditionGroup)
133
-
134
- if root_node.is_a?(FilteringParams::Condition)
135
- matching_condition = conditions.find {|cond| cond[:node_object] == root_node }
136
-
137
- # The simplified case of a single top level condition (without a wrapping group)
138
- # will need to pass the root query itself
139
- associated_query = root_node.parent_group ? root_node.parent_group.associated_query : root_query
140
- return yield matching_condition, associated_query
141
- else
142
- first_query, *rest_queries = root_node.items.map do |child|
143
- _depth_first_traversal(root_query: root_query, root_node: child, conditions: conditions, &block)
144
- end
145
-
146
- rest_queries.each.inject(first_query) do |q, a_query|
147
- root_node.type == :and ? q.and(a_query) : q.or(a_query)
148
- end
149
- end
150
- end
151
-
152
- def _mapped_filter(name)
153
- target = @filters_map[name]
154
- unless target
155
- filter_name = name.to_s
156
- if (@model.attribute_names + @model.reflections.keys).include?(filter_name)
157
- # Cache it in the filters mapping (to avoid later lookups), and return it.
158
- @filters_map[name] = name
159
- target = name
160
- end
161
- end
162
- return target
163
- end
164
-
165
- # Resolve and convert from filters, to a more manageable and param-type-independent structure
166
- def _convert_to_treenode(filters)
167
- # Resolve the names and values first, based on filters_map
168
- resolved_array = []
169
- filters.parsed_array.each do |filter|
170
- mapped_value = _mapped_filter(filter[:name])
171
- unless mapped_value
172
- msg = "Filtering by #{filter[:name]} is not allowed. No implementation mapping defined for it has been found \
173
- and there is not a model attribute with this name either.\n" \
174
- "Please add a mapping for #{filter[:name]} in the `filters_mapping` method of the appropriate Resource class"
175
- raise msg
176
- end
177
- bindings_array = \
178
- if mapped_value.is_a?(Proc)
179
- result = mapped_value.call(filter)
180
- # Result could be an array of hashes (each hash has name/op/value to identify a condition)
181
- result_from_proc = result.is_a?(Array) ? result : [result]
182
- # Make sure we tack on the node object associated with the filter
183
- result_from_proc.map{|hash| hash.merge(node_object: filter[:node_object])}
184
- else
185
- # For non-procs there's only 1 filter and 1 value (we're just overriding the mapped value)
186
- [filter.merge( name: mapped_value)]
187
- end
188
- resolved_array = resolved_array + bindings_array
189
- end
190
- FilterTreeNode.new(resolved_array, path: [ALIAS_TABLE_PREFIX])
191
- end
192
-
193
- # Calculate join tree and conditions array for the nodetree object and its children
194
- def _compute_joins_and_conditions_data(nodetree, model:, parent_reflection:)
195
- h = {}
196
- conditions = []
197
- nodetree.children.each do |name, child|
198
- child_reflection = model.reflections[name.to_s]
199
- result = _compute_joins_and_conditions_data(child, model: child_reflection.klass, parent_reflection: child_reflection)
200
- h[name] = result[:associations_hash]
201
-
202
- conditions += result[:conditions]
203
- end
204
-
205
- column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join(REFERENCES_STRING_SEPARATOR)
206
- nodetree.conditions.each do |condition|
207
- # If it's a final ! or !! operation on an association from the parent, it means we need to add a condition
208
- # on the existence (or lack of) of the whole associated table
209
- ref = model.reflections[condition[:name].to_s]
210
- if ref && ['!','!!'].include?(condition[:op])
211
- cp = (nodetree.path + [condition[:name].to_s]).join(REFERENCES_STRING_SEPARATOR)
212
- conditions += [condition.merge(column_prefix: cp, model: model, parent_reflection: ref)]
213
- h[condition[:name]] = {}
214
- else
215
- # Save the parent reflection where the condition applies as well (used later to get assoc keys)
216
- conditions += [condition.merge(column_prefix: column_prefix, model: model, parent_reflection: parent_reflection)]
124
+ # not in filters....checks if it's a valid path
125
+ # array of strings
126
+ def self.valid_path?(model, path)
127
+ first_component, *rest = path
128
+ if model.attribute_names.include?(first_component)
129
+ true
130
+ elsif model.reflections.keys.include?(first_component)
131
+ if rest.empty?
132
+ true # Allow associations as a leaf too (as they can have the ! and !! operator)
133
+ else # Follow the association
134
+ nested_model = model.reflections[first_component].klass
135
+ valid_path?(nested_model, rest)
217
136
  end
218
-
137
+ else
138
+ false
219
139
  end
220
- {associations_hash: h, conditions: conditions}
221
140
  end
222
141
 
223
- def self.add_clause(query:, column_prefix:, column_object:, op:, value:,fuzzy:, association_key_column:)
224
- likeval = get_like_value(value,fuzzy)
142
+ # rubocop:disable Metrics/ParameterLists,Naming/MethodParameterName
143
+ def self.add_clause(query:, column_prefix:, column_object:, op:, value:, fuzzy:, association_key_column:)
144
+ likeval = get_like_value(value, fuzzy)
225
145
 
226
146
  association_op = nil
227
147
  case op
@@ -236,7 +156,7 @@ module Praxis
236
156
  end
237
157
 
238
158
  if association_op
239
- neg = association_op == :not_null ? true : false
159
+ neg = association_op == :not_null
240
160
  qr = quote_right_part(query: query, value: nil, column_object: association_key_column, negative: neg)
241
161
  return query.where("#{quote_column_path(query: query, prefix: column_prefix, column_object: association_key_column)} #{qr}")
242
162
  end
@@ -278,11 +198,14 @@ module Praxis
278
198
  raise "Unsupported Operator!!! #{op}"
279
199
  end
280
200
  end
201
+ # rubocop:enable Metrics/ParameterLists,Naming/MethodParameterName
281
202
 
203
+ # rubocop:disable Naming/MethodParameterName
282
204
  def self.add_safe_where(query:, tab:, col:, op:, value:)
283
- quoted_value = query.connection.quote_default_expression(value,col)
284
- query.where("#{self.quote_column_path(query: query, prefix: tab, column_object: col)} #{op} #{quoted_value}")
205
+ quoted_value = query.connection.quote_default_expression(value, col)
206
+ query.where("#{quote_column_path(query: query, prefix: tab, column_object: col)} #{op} #{quoted_value}")
285
207
  end
208
+ # rubocop:enable Naming/MethodParameterName
286
209
 
287
210
  def self.quote_column_path(query:, prefix:, column_object:)
288
211
  c = query.connection
@@ -299,44 +222,135 @@ module Praxis
299
222
  conn = query.connection
300
223
  if value.nil?
301
224
  no = negative ? ' NOT' : ''
302
- "IS#{no} #{conn.quote_default_expression(value,column_object)}"
225
+ "IS#{no} #{conn.quote_default_expression(value, column_object)}"
303
226
  elsif value.is_a?(Array)
304
227
  no = negative ? 'NOT ' : ''
305
- list = value.map{|v| conn.quote_default_expression(v,column_object)}
228
+ list = value.map { |v| conn.quote_default_expression(v, column_object) }
306
229
  "#{no}IN (#{list.join(',')})"
307
- elsif value && value.is_a?(Range)
308
- raise "TODO!"
230
+ elsif value.is_a?(Range)
231
+ raise 'TODO!'
309
232
  else
310
233
  op = negative ? '<>' : '='
311
- "#{op} #{conn.quote_default_expression(value,column_object)}"
234
+ "#{op} #{conn.quote_default_expression(value, column_object)}"
312
235
  end
313
236
  end
314
237
 
315
238
  # Returns nil if the value was not a fuzzzy pattern
316
- def self.get_like_value(value,fuzzy)
239
+ def self.get_like_value(value, fuzzy)
317
240
  is_fuzzy = fuzzy.is_a?(Array) ? !fuzzy.compact.empty? : fuzzy
318
- if is_fuzzy
319
- unless value.is_a?(String)
320
- raise MultiMatchWithFuzzyNotAllowedByAdapter.new
241
+ return unless is_fuzzy
242
+
243
+ raise MultiMatchWithFuzzyNotAllowedByAdapter unless value.is_a?(String)
244
+
245
+ case fuzzy
246
+ when :start_end
247
+ "%#{value}%"
248
+ when :start
249
+ "%#{value}"
250
+ when :end
251
+ "#{value}%"
252
+ end
253
+ end
254
+
255
+ private
256
+
257
+ def _depth_first_traversal(root_query:, root_node:, conditions:, &block)
258
+ # Save the associated query for non-leaves
259
+ root_node.associated_query = root_query if root_node.is_a?(FilteringParams::ConditionGroup)
260
+
261
+ if root_node.is_a?(FilteringParams::Condition)
262
+ matching_condition = conditions.find { |cond| cond[:node_object] == root_node }
263
+
264
+ # The simplified case of a single top level condition (without a wrapping group)
265
+ # will need to pass the root query itself
266
+ associated_query = root_node.parent_group ? root_node.parent_group.associated_query : root_query
267
+ yield matching_condition, associated_query
268
+ else
269
+ first_query, *rest_queries = root_node.items.map do |child|
270
+ _depth_first_traversal(root_query: root_query, root_node: child, conditions: conditions, &block)
321
271
  end
322
- case fuzzy
323
- when :start_end
324
- '%'+value+'%'
325
- when :start
326
- '%'+value
327
- when :end
328
- value+'%'
272
+
273
+ rest_queries.each.inject(first_query) do |q, a_query|
274
+ root_node.type == :and ? q.and(a_query) : q.or(a_query)
275
+ end
276
+ end
277
+ end
278
+
279
+ def _mapped_filter(name)
280
+ target = @filters_map[name]
281
+ unless target
282
+ path = name.to_s.split('.')
283
+ if self.class.valid_path?(@model, path)
284
+ # Cache it in the filters mapping (to avoid later lookups), and return it.
285
+ @filters_map[name] = name
286
+ target = name
287
+ end
288
+ end
289
+ target
290
+ end
291
+
292
+ # Resolve and convert from filters, to a more manageable and param-type-independent structure
293
+ def _convert_to_treenode(filters)
294
+ # Resolve the names and values first, based on filters_map
295
+ resolved_array = []
296
+ filters.parsed_array.each do |filter|
297
+ mapped_value = _mapped_filter(filter[:name])
298
+ unless mapped_value
299
+ msg = "Filtering by #{filter[:name]} is not allowed. No implementation mapping defined for it has been found \
300
+ and there is not a model attribute with this name either.\n" \
301
+ "Please add a mapping for #{filter[:name]} in the `filters_mapping` method of the appropriate Resource class"
302
+ raise msg
303
+ end
304
+ bindings_array = \
305
+ if mapped_value.is_a?(Proc)
306
+ result = mapped_value.call(filter)
307
+ # Result could be an array of hashes (each hash has name/op/value to identify a condition)
308
+ result_from_proc = result.is_a?(Array) ? result : [result]
309
+ # Make sure we tack on the node object associated with the filter
310
+ result_from_proc.map { |hash| hash.merge(node_object: filter[:node_object]) }
311
+ else
312
+ # For non-procs there's only 1 filter and 1 value (we're just overriding the mapped value)
313
+ [filter.merge(name: mapped_value)]
314
+ end
315
+ resolved_array += bindings_array
316
+ end
317
+ FilterTreeNode.new(resolved_array, path: [ALIAS_TABLE_PREFIX])
318
+ end
319
+
320
+ # Calculate join tree and conditions array for the nodetree object and its children
321
+ def _compute_joins_and_conditions_data(nodetree, model:, parent_reflection:)
322
+ h = {}
323
+ conditions = []
324
+ nodetree.children.each do |name, child|
325
+ child_reflection = model.reflections[name.to_s]
326
+ result = _compute_joins_and_conditions_data(child, model: child_reflection.klass, parent_reflection: child_reflection)
327
+ h[name] = result[:associations_hash]
328
+
329
+ conditions += result[:conditions]
330
+ end
331
+
332
+ column_prefix = nodetree.path == [ALIAS_TABLE_PREFIX] ? model.table_name : nodetree.path.join(REFERENCES_STRING_SEPARATOR)
333
+ nodetree.conditions.each do |condition|
334
+ # If it's a final ! or !! operation on an association from the parent, it means we need to add a condition
335
+ # on the existence (or lack of) of the whole associated table
336
+ ref = model.reflections[condition[:name].to_s]
337
+ if ref && ['!', '!!'].include?(condition[:op])
338
+ cp = (nodetree.path + [condition[:name].to_s]).join(REFERENCES_STRING_SEPARATOR)
339
+ conditions += [condition.merge(column_prefix: cp, model: model, parent_reflection: ref)]
340
+ h[condition[:name]] = {}
341
+ else
342
+ # Save the parent reflection where the condition applies as well (used later to get assoc keys)
343
+ conditions += [condition.merge(column_prefix: column_prefix, model: model, parent_reflection: parent_reflection)]
329
344
  end
330
- else
331
- nil
332
345
  end
346
+ { associations_hash: h, conditions: conditions }
333
347
  end
334
348
 
335
349
  # The value that we need to stick in the references method is different in the latest Rails
336
- maj, min, _ = ActiveRecord.gem_version.segments
337
- if maj == 5 || (maj == 6 && min == 0)
350
+ maj, min, = ActiveRecord.gem_version.segments
351
+ if maj == 5 || (maj == 6 && min.zero?)
338
352
  # In AR 6 (and 6.0) the references are simple strings
339
- def build_reference_value(column_prefix, query: nil)
353
+ def build_reference_value(column_prefix, **_args)
340
354
  column_prefix
341
355
  end
342
356
  else
@@ -350,4 +364,4 @@ module Praxis
350
364
  end
351
365
  end
352
366
  end
353
- end
367
+ end
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable all
3
+
1
4
  require 'active_record'
2
5
 
3
6
  module ActiveRecord
@@ -5,7 +8,7 @@ module ActiveRecord
5
8
  class Relation
6
9
  def construct_join_dependency
7
10
  including = eager_load_values + includes_values
8
- # Praxis: inject references into the join dependency
11
+ # Praxis: inject references into the join dependency
9
12
  ActiveRecord::Associations::JoinDependency.new(
10
13
  klass, table, including, references: references_values
11
14
  )
@@ -34,18 +37,20 @@ module ActiveRecord
34
37
 
35
38
  alias_tracker.aliases
36
39
  end
37
-
38
40
  end
41
+
39
42
  module Associations
40
43
  class JoinDependency
41
44
  attr_accessor :references
45
+
42
46
  private
43
- def initialize(base, table, associations, references: )
47
+
48
+ def initialize(base, table, associations, references:)
44
49
  tree = self.class.make_tree associations
45
50
  @references = references # Save the references values into the instance (to use during build)
46
51
  @join_root = JoinBase.new(base, table, build(tree, base))
47
52
  end
48
-
53
+
49
54
  # Praxis: table aliases for is shared for 5x and 6.0
50
55
  def table_aliases_for(parent, node)
51
56
  node.reflection.chain.map do |reflection|
@@ -57,8 +62,8 @@ module ActiveRecord
57
62
  )
58
63
  # through tables do not need a special alias_path alias (as they shouldn't really referenced by the client)
59
64
  if is_root_reflection && node.alias_path
60
- table = table.left if table.is_a?(Arel::Nodes::TableAlias) #un-alias it if necessary
61
- table = table.alias(node.alias_path.join('/'))
65
+ table = table.left if table.is_a?(Arel::Nodes::TableAlias) # un-alias it if necessary
66
+ table = table.alias(node.alias_path.join('/'))
62
67
  end
63
68
  table
64
69
  end
@@ -71,20 +76,20 @@ module ActiveRecord
71
76
  reflection.check_validity!
72
77
  reflection.check_eager_loadable!
73
78
 
74
- if reflection.polymorphic?
75
- raise EagerLoadPolymorphicError.new(reflection)
76
- end
79
+ raise EagerLoadPolymorphicError, reflection if reflection.polymorphic?
80
+
77
81
  # Praxis: set an alias_path in the JoinAssociation if its path matches a requested reference
78
- child_path = (path && !path.empty?) ? path + [name] : nil
82
+ child_path = path && !path.empty? ? path + [name] : nil
79
83
  association = JoinAssociation.new(reflection, build(right, reflection.klass, path: child_path))
80
84
  association.alias_path = child_path if references.include?(child_path.join('/'))
81
- association
85
+ association
82
86
  end
83
87
  end
84
-
85
88
  end
89
+
86
90
  class ActiveRecord::Associations::JoinDependency::JoinAssociation
87
91
  attr_accessor :alias_path
88
92
  end
89
93
  end
90
- end
94
+ end
95
+ # rubocop:enable all
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable all
3
+
1
4
  # FOR AR < 6.1
2
5
  module ActiveRecord
3
6
  PRAXIS_JOIN_ALIAS_PREFIX = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
@@ -15,6 +18,7 @@ module ActiveRecord
15
18
  attr_accessor :references
16
19
 
17
20
  private
21
+
18
22
  def initialize(base, table, associations, join_type, references: nil)
19
23
  tree = self.class.make_tree associations
20
24
  @references = references # Save the references values into the instance (to use during build)
@@ -35,8 +39,8 @@ module ActiveRecord
35
39
  )
36
40
  # through tables do not need a special alias_path alias (as they shouldn't really referenced by the client)
37
41
  if is_root_reflection && node.alias_path
38
- table = table.left if table.is_a?(Arel::Nodes::TableAlias) #un-alias it if necessary
39
- table = table.alias(node.alias_path.join('/'))
42
+ table = table.left if table.is_a?(Arel::Nodes::TableAlias) # un-alias it if necessary
43
+ table = table.alias(node.alias_path.join('/'))
40
44
  end
41
45
  table
42
46
  end
@@ -49,20 +53,20 @@ module ActiveRecord
49
53
  reflection.check_validity!
50
54
  reflection.check_eager_loadable!
51
55
 
52
- if reflection.polymorphic?
53
- raise EagerLoadPolymorphicError.new(reflection)
54
- end
56
+ raise EagerLoadPolymorphicError, reflection if reflection.polymorphic?
57
+
55
58
  # Praxis: set an alias_path in the JoinAssociation if its path matches a requested reference
56
- child_path = (path && !path.empty?) ? path + [name] : nil
59
+ child_path = path && !path.empty? ? path + [name] : nil
57
60
  association = JoinAssociation.new(reflection, build(right, reflection.klass, path: child_path))
58
61
  association.alias_path = child_path if references.include?(child_path.join('/'))
59
- association
62
+ association
60
63
  end
61
64
  end
62
-
63
65
  end
66
+
64
67
  class ActiveRecord::Associations::JoinDependency::JoinAssociation
65
68
  attr_accessor :alias_path
66
69
  end
67
70
  end
68
- end
71
+ end
72
+ # rubocop:enable all
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable all
3
+
1
4
  # FOR AR >= 6.1
2
5
  module ActiveRecord
3
6
  PRAXIS_JOIN_ALIAS_PREFIX = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
4
7
  module Associations
5
8
  class JoinDependency
6
-
7
9
  private
10
+
8
11
  def make_constraints(parent, child, join_type)
9
12
  foreign_table = parent.table
10
13
  foreign_klass = parent.base_klass
@@ -19,7 +22,7 @@ module ActiveRecord
19
22
 
20
23
  table_name = @references[reflection.name.to_sym]
21
24
  # Praxis: set an alias_path in the JoinAssociation if its path matches a requested reference
22
- table_name = @references[child&.alias_path.join('/').to_sym] unless table_name
25
+ table_name ||= @references[child&.alias_path.join('/').to_sym]
23
26
 
24
27
  table = alias_tracker.aliased_table_for(reflection.klass.arel_table, table_name) do
25
28
  name = reflection.alias_candidate(parent.table_name)
@@ -38,21 +41,21 @@ module ActiveRecord
38
41
  reflection.check_validity!
39
42
  reflection.check_eager_loadable!
40
43
 
41
- if reflection.polymorphic?
42
- raise EagerLoadPolymorphicError.new(reflection)
43
- end
44
+ raise EagerLoadPolymorphicError, reflection if reflection.polymorphic?
45
+
44
46
  # Praxis: set an alias_path in the JoinAssociation if its path matches a requested reference
45
- child_path = (path && !path.empty?) ? path + [name] : nil
47
+ child_path = path && !path.empty? ? path + [name] : nil
46
48
  association = JoinAssociation.new(reflection, build(right, reflection.klass, path: child_path))
47
- #association.alias_path = child_path if references.include?(child_path.join('/'))
49
+ # association.alias_path = child_path if references.include?(child_path.join('/'))
48
50
  association.alias_path = child_path # ??? should be the line above no?
49
- association
51
+ association
50
52
  end
51
- end
53
+ end
52
54
  end
53
-
55
+
54
56
  class ActiveRecord::Associations::JoinDependency::JoinAssociation
55
57
  attr_accessor :alias_path
56
58
  end
57
59
  end
58
- end
60
+ end
61
+ # rubocop:enable all
@@ -1,15 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
 
3
- maj, min, _ = ActiveRecord.gem_version.segments
5
+ maj, min, = ActiveRecord.gem_version.segments
4
6
 
5
- if maj == 5
6
- require_relative 'active_record_patches/5x.rb'
7
- elsif maj == 6
8
- if min == 0
9
- require_relative 'active_record_patches/6_0.rb'
7
+ case maj
8
+ when 5
9
+ require_relative 'active_record_patches/5x'
10
+ when 6
11
+ if min.zero?
12
+ require_relative 'active_record_patches/6_0'
10
13
  else
11
- require_relative 'active_record_patches/6_1_plus.rb'
14
+ require_relative 'active_record_patches/6_1_plus'
12
15
  end
13
16
  else
14
- raise "Filtering only supported for ActiveRecord >= 5 && <= 6"
15
- end
17
+ raise 'Filtering only supported for ActiveRecord >= 5 && <= 6'
18
+ end