praxis 2.0.pre.16 → 2.0.pre.20

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 +22 -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 +187 -131
  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 +221 -106
  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 -47
  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 +12 -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,12 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
- require_relative '../support/spec_resources_active_model.rb'
5
+ require_relative '../support/spec_resources_active_model'
4
6
  require 'praxis/extensions/attribute_filtering'
5
7
  require 'praxis/extensions/attribute_filtering/active_record_filter_query_builder'
6
8
 
7
9
  describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder do
8
10
  let(:root_resource) { ActiveBookResource }
9
- let(:filters_map) { root_resource.instance_variable_get(:@_filters_map)}
11
+ let(:filters_map) { root_resource.instance_variable_get(:@_filters_map) }
10
12
  let(:base_model) { root_resource.model }
11
13
  let(:base_query) { base_model }
12
14
  let(:instance) { described_class.new(query: base_query, model: base_model, filters_map: filters_map) }
@@ -28,7 +30,7 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
28
30
  # Strip blank at the beggining (and end) of every line
29
31
  # ...and recompose it by adding an extra space at the beginning of each one instead
30
32
  exp = expected_sql.split(/\n/).map do |line|
31
- " " + line.strip
33
+ " #{line.strip}"
32
34
  end.join.strip
33
35
  expect(gen_sql).to eq(exp)
34
36
  end
@@ -44,7 +46,7 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
44
46
  end
45
47
  context 'generate' do
46
48
  subject { instance.generate(filters) }
47
- let(:filters) { Praxis::Types::FilteringParams.load(filters_string)}
49
+ let(:filters) { Praxis::Types::FilteringParams.load(filters_string) }
48
50
 
49
51
  context 'with no filters' do
50
52
  let(:filters_string) { '' }
@@ -67,29 +69,29 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
67
69
  context 'but if it is a field that does not exist in the model' do
68
70
  let(:filters_string) { 'nonexisting=valuehere' }
69
71
  it 'it blows up with the right error' do
70
- expect{subject}.to raise_error(/Filtering by nonexisting is not allowed/)
72
+ expect { subject }.to raise_error(/Filtering by nonexisting is not allowed/)
71
73
  end
72
74
  end
73
75
  end
74
76
  context 'that maps to a different name' do
75
- let(:filters_string) { 'name=Book1'}
77
+ let(:filters_string) { 'name=Book1' }
76
78
  it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
77
79
  end
78
80
  context 'that is mapped as a nested struct' do
79
- let(:filters_string) { 'fake_nested.name=Book1'}
81
+ let(:filters_string) { 'fake_nested.name=Book1' }
80
82
  it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
81
83
  end
82
84
  context 'passing multiple values' do
83
85
  context 'without fuzzy matching' do
84
86
  let(:filters_string) { 'category_uuid=deadbeef1,deadbeef2' }
85
- it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: ['deadbeef1','deadbeef2'])
87
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: %w[deadbeef1 deadbeef2])
86
88
  end
87
89
  context 'with fuzzy matching' do
88
90
  let(:filters_string) { 'category_uuid=*deadbeef1,deadbeef2*' }
89
91
  it 'is not supported' do
90
- expect{
92
+ expect do
91
93
  subject
92
- }.to raise_error(
94
+ end.to raise_error(
93
95
  Praxis::Extensions::AttributeFiltering::MultiMatchWithFuzzyNotAllowedByAdapter,
94
96
  /Please use multiple OR clauses instead/
95
97
  )
@@ -100,7 +102,7 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
100
102
 
101
103
  context 'by a field or a related model' do
102
104
  context 'for a belongs_to association' do
103
- let(:filters_string) { 'author.name=author2'}
105
+ let(:filters_string) { 'author.name=author2' }
104
106
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name' => 'author2')
105
107
  end
106
108
  context 'for a has_many association' do
@@ -111,101 +113,143 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
111
113
  let(:filters_string) { 'tags.name=blue' }
112
114
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:tags).where('active_tags.name' => 'blue')
113
115
  end
116
+
117
+ context 'by just an association filter condition' do
118
+ context 'for a belongs_to association with NO ROWS' do
119
+ let(:filters_string) { 'category!!' }
120
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where.missing(:category)
121
+ end
122
+
123
+ context 'for a direct has_many association asking for missing rows' do
124
+ let(:filters_string) { 'primary_tags!!' }
125
+ it_behaves_like 'subject_equivalent_to',
126
+ ActiveBook.where.missing(:primary_tags)
127
+ end
128
+ context 'for a direct has_many association asking for non-missing rows' do
129
+ let(:filters_string) { 'primary_tags!' }
130
+ it_behaves_like 'subject_equivalent_to',
131
+ ActiveBook.left_outer_joins(:primary_tags).where.not('primary_tags.id' => nil)
132
+ end
133
+
134
+ context 'for a has_many through association with NO ROWS' do
135
+ let(:filters_string) { 'tags!!' }
136
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where.missing(:tags)
137
+ end
138
+
139
+ context 'for a has_many through association with SOME ROWS' do
140
+ let(:filters_string) { 'tags!' }
141
+ it_behaves_like 'subject_equivalent_to', ActiveBook.left_outer_joins(:tags).where.not('tags.id' => nil)
142
+ end
143
+
144
+ context 'for a 3 levels deep has_many association with NO ROWS' do
145
+ let(:filters_string) { 'category.books.taggings!!' }
146
+ it_behaves_like 'subject_equivalent_to',
147
+ ActiveBook.left_outer_joins(category: { books: :taggings }).where('category.books.taggings.id' => nil)
148
+ end
149
+
150
+ context 'for a 3 levels deep has_many association WITH SIME ROWS' do
151
+ let(:filters_string) { 'category.books.taggings!' }
152
+ it_behaves_like 'subject_equivalent_to',
153
+ ActiveBook.left_outer_joins(category: { books: :taggings }).where.not('category.books.taggings.id' => nil)
154
+ end
155
+ end
114
156
  end
115
157
 
116
158
  # NOTE: apparently AR when conditions are build with strings in the where clauses (instead of names, etc)
117
159
  # it decides to parenthesize them, even when there's only 1 condition. Hence the silly parentization of
118
160
  # these SQL fragments here (and others)
119
161
  context 'by using all supported operators' do
162
+ # rubocop:disable Lint/ConstantDefinitionInBlock
120
163
  PREF = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
121
164
  COMMON_SQL_PREFIX = <<~SQL
122
- SELECT "active_books".* FROM "active_books"
123
- INNER JOIN
124
- "active_authors" "#{PREF}/author" ON "#{PREF}/author"."id" = "active_books"."author_id"
125
- SQL
165
+ SELECT "active_books".* FROM "active_books"
166
+ LEFT OUTER JOIN
167
+ "active_authors" "#{PREF}/author" ON "#{PREF}/author"."id" = "active_books"."author_id"
168
+ SQL
169
+ # rubocop:enable Lint/ConstantDefinitionInBlock
126
170
  context '=' do
127
- let(:filters_string) { 'author.id=11'}
171
+ let(:filters_string) { 'author.id=11' }
128
172
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id = 11')
129
173
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
130
- WHERE ("#{PREF}/author"."id" = 11)
131
- SQL
174
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" = 11)
175
+ SQL
132
176
  end
133
177
  context '= (with array)' do
134
- let(:filters_string) { 'author.id=11,22'}
178
+ let(:filters_string) { 'author.id=11,22' }
135
179
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IN (11,22)')
136
180
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
137
- WHERE ("#{PREF}/author"."id" IN (11,22))
138
- SQL
139
- end
181
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" IN (11,22))
182
+ SQL
183
+ end
140
184
  context '!=' do
141
- let(:filters_string) { 'author.id!=11'}
185
+ let(:filters_string) { 'author.id!=11' }
142
186
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <> 11')
143
187
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
144
- WHERE ("#{PREF}/author"."id" <> 11)
145
- SQL
188
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" <> 11)
189
+ SQL
146
190
  end
147
191
  context '!= (with array)' do
148
- let(:filters_string) { 'author.id!=11,888'}
192
+ let(:filters_string) { 'author.id!=11,888' }
149
193
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id NOT IN (11,888)')
150
194
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
151
- WHERE ("#{PREF}/author"."id" NOT IN (11,888))
152
- SQL
195
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" NOT IN (11,888))
196
+ SQL
153
197
  end
154
198
  context '>' do
155
- let(:filters_string) { 'author.id>1'}
199
+ let(:filters_string) { 'author.id>1' }
156
200
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id > 1')
157
201
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
158
- WHERE ("#{PREF}/author"."id" > 1)
159
- SQL
202
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" > 1)
203
+ SQL
160
204
  end
161
205
  context '<' do
162
- let(:filters_string) { 'author.id<22'}
206
+ let(:filters_string) { 'author.id<22' }
163
207
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id < 22')
164
208
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
165
- WHERE ("#{PREF}/author"."id" < 22)
166
- SQL
209
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" < 22)
210
+ SQL
167
211
  end
168
212
  context '>=' do
169
- let(:filters_string) { 'author.id>=22'}
213
+ let(:filters_string) { 'author.id>=22' }
170
214
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id >= 22')
171
215
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
172
- WHERE ("#{PREF}/author"."id" >= 22)
173
- SQL
216
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" >= 22)
217
+ SQL
174
218
  end
175
219
  context '<=' do
176
- let(:filters_string) { 'author.id<=22'}
220
+ let(:filters_string) { 'author.id<=22' }
177
221
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <= 22')
178
222
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
179
- WHERE ("#{PREF}/author"."id" <= 22)
180
- SQL
223
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" <= 22)
224
+ SQL
181
225
  end
182
226
  context '!' do
183
- let(:filters_string) { 'author.id!'}
227
+ let(:filters_string) { 'author.id!' }
184
228
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IS NOT NULL')
185
229
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
186
- WHERE ("#{PREF}/author"."id" IS NOT NULL)
187
- SQL
230
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" IS NOT NULL)
231
+ SQL
188
232
  end
189
233
  context '!!' do
190
- let(:filters_string) { 'author.name!!'}
234
+ let(:filters_string) { 'author.name!!' }
191
235
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name IS NULL')
192
236
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
193
- WHERE ("#{PREF}/author"."name" IS NULL)
194
- SQL
195
- end
237
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."name" IS NULL)
238
+ SQL
239
+ end
196
240
  context 'including LIKE fuzzy queries' do
197
241
  context 'LIKE' do
198
- let(:filters_string) { 'author.name=author*'}
242
+ let(:filters_string) { 'author.name=author*' }
199
243
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name LIKE "author%"')
200
244
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
201
- WHERE ("#{PREF}/author"."name" LIKE 'author%')
245
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."name" LIKE 'author%')
202
246
  SQL
203
247
  end
204
248
  context 'NOT LIKE' do
205
- let(:filters_string) { 'author.name!=foobar*'}
249
+ let(:filters_string) { 'author.name!=foobar*' }
206
250
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name NOT LIKE "foobar%"')
207
251
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
208
- WHERE ("#{PREF}/author"."name" NOT LIKE 'foobar%')
252
+ WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."name" NOT LIKE 'foobar%')
209
253
  SQL
210
254
  end
211
255
  end
@@ -223,28 +267,28 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
223
267
  end
224
268
  context 'multiple conditions on a nested relationship' do
225
269
  let(:filters_string) { 'category.books.taggings.tag_id=1&category.books.taggings.label=primary' }
226
- it_behaves_like 'subject_equivalent_to',
227
- ActiveBook.joins(category: { books: :taggings }).where('active_taggings.tag_id': 1).where('active_taggings.label': 'primary')
270
+ it_behaves_like 'subject_equivalent_to',
271
+ ActiveBook.joins(category: { books: :taggings }).where('active_taggings.tag_id': 1).where('active_taggings.label': 'primary')
228
272
  it_behaves_like 'subject_matches_sql', <<~SQL
229
- SELECT "active_books".* FROM "active_books"
230
- INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
231
- INNER JOIN "active_books" "books_active_categories" ON "books_active_categories"."category_uuid" = "active_categories"."uuid"
232
- INNER JOIN "active_taggings" "#{PREF}/category/books/taggings" ON "/category/books/taggings"."book_id" = "books_active_categories"."id"
233
- WHERE ("#{PREF}/category/books/taggings"."tag_id" = 1)
234
- AND ("#{PREF}/category/books/taggings"."label" = 'primary')
235
- SQL
273
+ SELECT "active_books".* FROM "active_books"
274
+ LEFT OUTER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
275
+ LEFT OUTER JOIN "active_books" "books_active_categories" ON "books_active_categories"."category_uuid" = "active_categories"."uuid"
276
+ LEFT OUTER JOIN "active_taggings" "#{PREF}/category/books/taggings" ON "/category/books/taggings"."book_id" = "books_active_categories"."id"
277
+ WHERE ("#{PREF}/category/books/taggings"."id" IS NOT NULL) AND ("#{PREF}/category/books/taggings"."tag_id" = 1)
278
+ AND ("#{PREF}/category/books/taggings"."label" = 'primary')
279
+ SQL
236
280
  end
237
281
  context 'that contain multiple joins to the same table' do
238
282
  let(:filters_string) { 'taggings.tag.taggings.tag_id=1' }
239
- it_behaves_like 'subject_equivalent_to',
240
- ActiveBook.joins(taggings: {tag: :taggings}).where('taggings_active_tags.tag_id=1')
283
+ it_behaves_like 'subject_equivalent_to',
284
+ ActiveBook.joins(taggings: { tag: :taggings }).where('taggings_active_tags.tag_id=1')
241
285
  end
242
286
  end
243
287
 
244
288
  context 'by multiple fields' do
245
289
  context 'adds the where clauses for the top model if fields belong to it' do
246
290
  let(:filters_string) { 'category_uuid=deadbeef1&name=Book1' }
247
- it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1', simple_name: 'Book1')
291
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1', simple_name: 'Book1')
248
292
  end
249
293
  context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
250
294
  let(:filters_string) { 'taggings.label=primary&taggings.tag_id=2' }
@@ -256,25 +300,24 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
256
300
  context 'when we have a join table condition that has the same field' do
257
301
  let(:filters_string) { 'name=Book1&category.books.name=Book3' }
258
302
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books)
259
- .where('simple_name': 'Book1')
260
- .where('books_active_categories.simple_name': 'Book3')
303
+ .where('simple_name': 'Book1')
304
+ .where('books_active_categories.simple_name': 'Book3')
261
305
  it_behaves_like 'subject_matches_sql', <<~SQL
262
- SELECT "active_books".* FROM "active_books"
263
- INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
264
- INNER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
265
- WHERE ("active_books"."simple_name" = 'Book1')
266
- AND ("#{PREF}/category/books"."simple_name" = 'Book3')
267
- SQL
306
+ SELECT "active_books".* FROM "active_books"
307
+ LEFT OUTER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
308
+ LEFT OUTER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"#{' '}
309
+ WHERE ("active_books"."simple_name" = 'Book1')
310
+ AND ("#{PREF}/category/books"."id" IS NOT NULL) AND ("#{PREF}/category/books"."simple_name" = 'Book3')
311
+ SQL
268
312
  end
269
313
 
270
- context 'it qualifis them even if there are no joined tables/conditions at all' do
271
- let(:filters_string) { 'id=11'}
314
+ context 'it qualifies them even if there are no joined tables/conditions at all' do
315
+ let(:filters_string) { 'id=11' }
272
316
  it_behaves_like 'subject_matches_sql', <<~SQL
273
317
  SELECT "active_books".* FROM "active_books"
274
318
  WHERE ("active_books"."id" = 11)
275
- SQL
319
+ SQL
276
320
  end
277
-
278
321
  end
279
322
  end
280
323
 
@@ -290,14 +333,14 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
290
333
  context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
291
334
  let(:filters_string) { 'taggings.label=primary|taggings.tag_id=2' }
292
335
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
293
- .or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
336
+ .or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
294
337
  end
295
338
  end
296
339
 
297
340
  context 'with combined AND and OR conditions' do
298
341
  let(:filters_string) { '(category_uuid=deadbeef1|category_uuid=deadbeef2)&(name=Book1|name=Book2)' }
299
342
  it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(category_uuid: 'deadbeef2'))
300
- .and(ActiveBook.where(simple_name: 'Book1').or(ActiveBook.where(simple_name: 'Book2')))
343
+ .and(ActiveBook.where(simple_name: 'Book1').or(ActiveBook.where(simple_name: 'Book2')))
301
344
  it_behaves_like 'subject_matches_sql', <<~SQL
302
345
  SELECT "active_books".* FROM "active_books"
303
346
  WHERE ("active_books"."category_uuid" = 'deadbeef1' OR "active_books"."category_uuid" = 'deadbeef2')
@@ -307,47 +350,63 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
307
350
  context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
308
351
  let(:filters_string) { 'taggings.label=primary|taggings.tag_id=2' }
309
352
  it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
310
- .or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
353
+ .or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
354
+ it_behaves_like 'subject_matches_sql', <<~SQL
355
+ SELECT "active_books".* FROM "active_books"
356
+ LEFT OUTER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
357
+ WHERE ("/taggings"."id" IS NOT NULL) AND ("/taggings"."label" = 'primary' OR "/taggings"."tag_id" = 2)
358
+ SQL
359
+ end
360
+
361
+ context 'works well with ORs at a parent table along with joined associations with no rows' do
362
+ let(:filters_string) { 'name=Book1005|category!!' }
363
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where.missing(:category)
364
+ .or(ActiveBook.where.missing(:category).where(simple_name: 'Book1005'))
311
365
  it_behaves_like 'subject_matches_sql', <<~SQL
312
366
  SELECT "active_books".* FROM "active_books"
313
- INNER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
314
- WHERE ("/taggings"."label" = 'primary' OR "/taggings"."tag_id" = 2)
367
+ LEFT OUTER JOIN "active_categories" "/category" ON "/category"."uuid" = "active_books"."category_uuid"
368
+ WHERE ("active_books"."simple_name" = 'Book1005' OR "/category"."uuid" IS NULL)
315
369
  SQL
316
370
  end
317
371
 
318
372
  context '3-deep AND and OR conditions' do
319
373
  let(:filters_string) { '(category.name=cat2|(taggings.label=primary&tags.name=red))&category_uuid=deadbeef1' }
320
- it_behaves_like('subject_equivalent_to', Proc.new do
321
- base=ActiveBook.joins(:category,:taggings,:tags)
374
+ it_behaves_like('subject_equivalent_to', proc do
375
+ base = ActiveBook.left_outer_joins(:category, :taggings, :tags)
322
376
 
323
- and1_or1 = base.where('category.name': 'cat2')
377
+ and1_or1 = base.where('category.name': 'cat2').where.not('category.uuid': nil)
324
378
 
325
- and1_or2_and1 = base.where('taggings.label': 'primary')
326
- and1_or2_and2 = base.where('tags.name': 'red')
379
+ and1_or2_and1 = base.where('taggings.label': 'primary').where.not('taggings.id': nil)
380
+ and1_or2_and2 = base.where('tags.name': 'red').where.not('tags.id': nil)
327
381
  and1_or2 = and1_or2_and1.and(and1_or2_and2)
328
382
 
329
383
  and1 = and1_or1.or(and1_or2)
330
- and2=base.where(category_uuid: 'deadbeef1')
384
+ and2 = base.where(category_uuid: 'deadbeef1')
331
385
 
332
- query = and1.and(and2)
386
+ and1.and(and2)
333
387
  end)
334
388
 
335
389
  it_behaves_like 'subject_matches_sql', <<~SQL
336
- SELECT "active_books".* FROM "active_books"
337
- INNER JOIN "active_categories" "/category" ON "/category"."uuid" = "active_books"."category_uuid"
338
- INNER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
339
- INNER JOIN "active_taggings" "taggings_active_books_join" ON "taggings_active_books_join"."book_id" = "active_books"."id"
340
- INNER JOIN "active_tags" "/tags" ON "/tags"."id" = "taggings_active_books_join"."tag_id"
341
- WHERE ("/category"."name" = 'cat2' OR ("/taggings"."label" = 'primary') AND ("/tags"."name" = 'red')) AND ("active_books"."category_uuid" = 'deadbeef1')
390
+ SELECT "active_books".* FROM "active_books"#{' '}
391
+ LEFT OUTER JOIN "active_categories" "/category" ON "/category"."uuid" = "active_books"."category_uuid"#{' '}
392
+ LEFT OUTER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"#{' '}
393
+ LEFT OUTER JOIN "active_tags" "/tags" ON "/tags"."id" = "/taggings"."tag_id"#{' '}
394
+ WHERE (("/category"."uuid" IS NOT NULL)
395
+ AND ("/category"."name" = 'cat2')
396
+ OR ("/taggings"."id" IS NOT NULL)
397
+ AND ("/taggings"."label" = 'primary')
398
+ AND ("/tags"."id" IS NOT NULL)
399
+ AND ("/tags"."name" = 'red'))
400
+ AND ("active_books"."category_uuid" = 'deadbeef1')#{' '}
342
401
  SQL
343
402
  end
344
403
  end
345
404
 
346
405
  context 'ActiveRecord continues to work as expected (with our patches)' do
347
406
  context 'using a deep join with repeated tables' do
348
- subject{ ActiveBook.joins(taggings: {tag: :taggings}).where('taggings_active_tags.tag_id=1') }
407
+ subject { ActiveBook.joins(taggings: { tag: :taggings }).where('taggings_active_tags.tag_id=1') }
349
408
  it 'performs query' do
350
- expect(subject.to_a).to_not be_empty
409
+ expect(subject.to_a).to_not be_empty
351
410
  end
352
411
  it_behaves_like 'subject_matches_sql', <<~SQL
353
412
  SELECT "active_books".* FROM "active_books"
@@ -358,19 +417,19 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
358
417
  SQL
359
418
  end
360
419
  context 'a deep join with repeated tables with the root AND the join, along with :through joins as well' do
361
- subject!{ ActiveBook.joins(tags: {books: {taggings: :book}}).where('books_active_taggings.simple_name="Book2"') }
420
+ subject! { ActiveBook.joins(tags: { books: { taggings: :book } }).where('books_active_taggings.simple_name="Book2"') }
362
421
  it 'performs query' do
363
- expect(subject.to_a).to_not be_empty
422
+ expect(subject.to_a).to_not be_empty
364
423
  end
365
424
  it_behaves_like 'subject_matches_sql', <<~SQL
366
- SELECT "active_books".* FROM "active_books"
425
+ SELECT "active_books".* FROM "active_books"#{' '}
367
426
  INNER JOIN "active_taggings" ON "active_taggings"."book_id" = "active_books"."id"
368
427
  INNER JOIN "active_tags" ON "active_tags"."id" = "active_taggings"."tag_id"
369
- INNER JOIN "active_taggings" "taggings_active_tags_join" ON "taggings_active_tags_join"."tag_id" = "active_tags"."id"
370
- INNER JOIN "active_books" "books_active_tags" ON "books_active_tags"."id" = "taggings_active_tags_join"."book_id"
371
- INNER JOIN "active_taggings" "taggings_active_books" ON "taggings_active_books"."book_id" = "books_active_tags"."id"
428
+ INNER JOIN "active_taggings" "taggings_active_tags_join" ON "taggings_active_tags_join"."tag_id" = "active_tags"."id"#{' '}
429
+ INNER JOIN "active_books" "books_active_tags" ON "books_active_tags"."id" = "taggings_active_tags_join"."book_id"#{' '}
430
+ INNER JOIN "active_taggings" "taggings_active_books" ON "taggings_active_books"."book_id" = "books_active_tags"."id"#{' '}
372
431
  INNER JOIN "active_books" "books_active_taggings" ON "books_active_taggings"."id" = "taggings_active_books"."book_id"
373
- WHERE (books_active_taggings.simple_name="Book2")
432
+ WHERE (books_active_taggings.simple_name="Book2")#{' '}
374
433
  SQL
375
434
  end
376
435
  end
@@ -378,8 +437,8 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
378
437
  context 'respects scopes' do
379
438
  context 'for a has_many through association' do
380
439
  let(:filters_string) { 'primary_tags.name=blue' }
381
- it_behaves_like 'subject_equivalent_to',
382
- ActiveBook.joins(:primary_tags).where('active_tags.name="blue"')
440
+ it_behaves_like 'subject_equivalent_to',
441
+ ActiveBook.joins(:primary_tags).where('active_tags.name="blue"')
383
442
 
384
443
  it 'adds the association scope clause to the join' do
385
444
  inner_join_pieces = subject.to_sql.split('INNER')
@@ -391,12 +450,68 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
391
450
  # This is slightly incorrect in AR 6.1+ (since the picked aliases for active_taggings tables vary)
392
451
  # it_behaves_like 'subject_matches_sql', <<~SQL
393
452
  # SELECT "active_books".* FROM "active_books"
394
- # INNER JOIN "active_taggings" ON "active_taggings"."label" = 'primary'
453
+ # LEFT OUTER JOIN "active_taggings" ON "active_taggings"."label" = 'primary'
395
454
  # AND "active_taggings"."book_id" = "active_books"."id"
396
- # INNER JOIN "active_tags" "/primary_tags" ON "/primary_tags"."id" = "active_taggings"."tag_id"
455
+ # LEFT OUTER JOIN "active_tags" "/primary_tags" ON "/primary_tags"."id" = "active_taggings"."tag_id"
397
456
  # WHERE ("/primary_tags"."name" = 'blue')
398
457
  # SQL
399
458
  end
400
459
  end
401
460
  end
461
+
462
+ context '.valid_path?' do
463
+ it 'suceeds for reachable model columns' do
464
+ expect(described_class.valid_path?(ActiveBook, ['added_column'])).to be_truthy
465
+ expect(described_class.valid_path?(ActiveBook, %w[author books added_column])).to be_truthy
466
+ expect(described_class.valid_path?(ActiveBook, %w[author books simple_name])).to be_truthy
467
+ end
468
+ it 'suceeds for reachable leaf associations' do
469
+ expect(described_class.valid_path?(ActiveBook, ['author'])).to be_truthy
470
+ expect(described_class.valid_path?(ActiveBook, %w[author books])).to be_truthy
471
+ end
472
+ it 'returns false for invalid model columns' do
473
+ expect(described_class.valid_path?(ActiveBook, ['not_a_column'])).to be_falsy
474
+ expect(described_class.valid_path?(ActiveBook, %w[author books not_here])).to be_falsy
475
+ expect(described_class.valid_path?(ActiveBook, %w[author books name])).to be_falsy
476
+ end
477
+ end
478
+
479
+ context '_mapped_filter' do
480
+ let(:root_resource) { ActiveBookResource }
481
+ let(:filters_map) { root_resource.instance_variable_get(:@_filters_map) }
482
+
483
+ context 'for explicitly mapped values' do
484
+ %i[id name name_is_not author.name category.books.taggings.label]
485
+ .each do |name|
486
+ it "suceeds for #{name}" do
487
+ mapped_value = filters_map[name]
488
+ expect(mapped_value).to_not be_nil
489
+ expect(instance.send(:_mapped_filter, name)).to eq(mapped_value)
490
+ end
491
+ end
492
+ end
493
+
494
+ context 'for not mapped values' do
495
+ context 'that are valid model columns/associations paths' do
496
+ %i[added_column author.books.added_column author.books].each do |name|
497
+ it "returns (and caches) the same valid path for #{name}" do
498
+ expect(filters_map[name]).to be_nil
499
+ expect(instance.send(:_mapped_filter, name)).to eq(name)
500
+
501
+ expect(filters_map[name]).to eq(name)
502
+ end
503
+ end
504
+ end
505
+ context 'that are not model columns/associations paths' do
506
+ %i[not_a_column author.books.not_here].each do |name|
507
+ it "returns nil (and does not cache) for #{name}" do
508
+ expect(filters_map[name]).to be_nil
509
+ expect(instance.send(:_mapped_filter, name)).to eq(nil)
510
+
511
+ expect(filters_map[name]).to eq(nil)
512
+ end
513
+ end
514
+ end
515
+ end
516
+ end
402
517
  end
@@ -1,18 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  require 'praxis/extensions/attribute_filtering'
4
6
 
5
7
  describe Praxis::Extensions::AttributeFiltering::FilterTreeNode do
6
-
7
- let(:dummy_object) { double("Fake NodeObject")}
8
+ let(:dummy_object) { double('Fake NodeObject') }
8
9
  let(:filters) do
9
10
  [
10
- {name: 'one', op: '>', value: 1, node_object: dummy_object},
11
- {name: 'one', op: '<', value: 10},
12
- {name: 'rel1.a1', op: '=', value: 1},
13
- {name: 'rel1.a2', op: '=', value: 2},
14
- {name: 'rel1.rel2.b1', op: '=', value: 11},
15
- {name: 'rel1.rel2.b2', op: '=', value: 12, node_object: dummy_object}
11
+ { name: 'one', op: '>', value: 1, node_object: dummy_object },
12
+ { name: 'one', op: '<', value: 10 },
13
+ { name: 'rel1.a1', op: '=', value: 1 },
14
+ { name: 'rel1.a2', op: '=', value: 2 },
15
+ { name: 'rel1.rel2.b1', op: '=', value: 11 },
16
+ { name: 'rel1.rel2.b2', op: '=', value: 12, node_object: dummy_object }
16
17
  ]
17
18
  end
18
19
  context 'initialization' do
@@ -20,10 +21,10 @@ describe Praxis::Extensions::AttributeFiltering::FilterTreeNode do
20
21
  it 'holds the top conditions and the child in a TreeNode' do
21
22
  expect(subject.path).to eq([])
22
23
  expect(subject.conditions.size).to eq(2)
23
- expect(subject.conditions.map{|i| i.slice(:name,:op,:value)}).to eq([
24
- {name: 'one', op: '>', value: 1},
25
- {name: 'one', op: '<', value: 10},
26
- ])
24
+ expect(subject.conditions.map { |i| i.slice(:name, :op, :value) }).to eq([
25
+ { name: 'one', op: '>', value: 1 },
26
+ { name: 'one', op: '<', value: 10 }
27
+ ])
27
28
  expect(subject.children.keys).to eq(['rel1'])
28
29
  expect(subject.children['rel1']).to be_kind_of(described_class)
29
30
  end
@@ -38,20 +39,20 @@ describe Praxis::Extensions::AttributeFiltering::FilterTreeNode do
38
39
  rel1 = subject.children['rel1']
39
40
  expect(rel1.path).to eq(['rel1'])
40
41
  expect(rel1.conditions.size).to eq(2)
41
- expect(rel1.conditions.map{|i| i.slice(:name,:op,:value)}).to eq([
42
- {name: 'a1', op: '=', value: 1},
43
- {name: 'a2', op: '=', value: 2},
44
- ])
42
+ expect(rel1.conditions.map { |i| i.slice(:name, :op, :value) }).to eq([
43
+ { name: 'a1', op: '=', value: 1 },
44
+ { name: 'a2', op: '=', value: 2 }
45
+ ])
45
46
  expect(rel1.children.keys).to eq(['rel2'])
46
47
  expect(rel1.children['rel2']).to be_kind_of(described_class)
47
48
 
48
49
  rel1rel2 = rel1.children['rel2']
49
- expect(rel1rel2.path).to eq(['rel1','rel2'])
50
+ expect(rel1rel2.path).to eq(%w[rel1 rel2])
50
51
  expect(rel1rel2.conditions.size).to eq(2)
51
- expect(rel1rel2.conditions.map{|i| i.slice(:name,:op,:value)}).to eq([
52
- {name: 'b1', op: '=', value: 11},
53
- {name: 'b2', op: '=', value: 12}
54
- ])
52
+ expect(rel1rel2.conditions.map { |i| i.slice(:name, :op, :value) }).to eq([
53
+ { name: 'b1', op: '=', value: 11 },
54
+ { name: 'b2', op: '=', value: 12 }
55
+ ])
55
56
  expect(rel1rel2.children.keys).to be_empty
56
57
  end
57
58
  end