praxis 2.0.pre.8 → 2.0.pre.13

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 (242) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +1 -3
  5. data/CHANGELOG.md +33 -0
  6. data/TODO.md +1 -4
  7. data/bin/praxis +67 -12
  8. data/lib/praxis.rb +10 -3
  9. data/lib/praxis/action_definition.rb +15 -13
  10. data/lib/praxis/action_definition/headers_dsl_compiler.rb +0 -7
  11. data/lib/praxis/api_general_info.rb +1 -1
  12. data/lib/praxis/application.rb +6 -2
  13. data/lib/praxis/blueprint.rb +357 -0
  14. data/lib/praxis/bootloader.rb +9 -3
  15. data/lib/praxis/bootloader_stages/environment.rb +16 -13
  16. data/lib/praxis/collection.rb +1 -11
  17. data/lib/praxis/config_hash.rb +44 -0
  18. data/lib/praxis/docs/{openapi → open_api}/info_object.rb +18 -10
  19. data/lib/praxis/docs/{openapi → open_api}/media_type_object.rb +0 -0
  20. data/lib/praxis/docs/{openapi → open_api}/operation_object.rb +0 -0
  21. data/lib/praxis/docs/{openapi → open_api}/parameter_object.rb +0 -0
  22. data/lib/praxis/docs/{openapi → open_api}/paths_object.rb +0 -0
  23. data/lib/praxis/docs/{openapi → open_api}/request_body_object.rb +0 -0
  24. data/lib/praxis/docs/{openapi → open_api}/response_object.rb +0 -0
  25. data/lib/praxis/docs/{openapi → open_api}/responses_object.rb +0 -0
  26. data/lib/praxis/docs/{openapi → open_api}/schema_object.rb +0 -0
  27. data/lib/praxis/docs/{openapi → open_api}/server_object.rb +0 -0
  28. data/lib/praxis/docs/{openapi → open_api}/tag_object.rb +0 -0
  29. data/lib/praxis/docs/open_api_generator.rb +91 -6
  30. data/lib/praxis/endpoint_definition.rb +273 -0
  31. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +182 -58
  32. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
  33. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +47 -56
  34. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +153 -0
  35. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
  36. data/lib/praxis/extensions/field_expansion.rb +3 -36
  37. data/lib/praxis/extensions/pagination.rb +5 -32
  38. data/lib/praxis/extensions/pagination/ordering_params.rb +1 -1
  39. data/lib/praxis/extensions/pagination/pagination_params.rb +6 -4
  40. data/lib/praxis/field_expander.rb +90 -0
  41. data/lib/praxis/finalizable.rb +34 -0
  42. data/lib/praxis/mapper/active_model_compat.rb +4 -0
  43. data/lib/praxis/mapper/resource.rb +18 -2
  44. data/lib/praxis/mapper/selector_generator.rb +2 -1
  45. data/lib/praxis/mapper/sequel_compat.rb +7 -0
  46. data/lib/praxis/media_type.rb +3 -68
  47. data/lib/praxis/plugin_concern.rb +1 -1
  48. data/lib/praxis/plugins/mapper_plugin.rb +24 -15
  49. data/lib/praxis/plugins/pagination_plugin.rb +34 -4
  50. data/lib/praxis/renderer.rb +88 -0
  51. data/lib/praxis/request.rb +1 -1
  52. data/lib/praxis/resource_definition.rb +2 -311
  53. data/lib/praxis/response_definition.rb +2 -10
  54. data/lib/praxis/response_template.rb +3 -3
  55. data/lib/praxis/router.rb +2 -2
  56. data/lib/praxis/routing_config.rb +1 -1
  57. data/lib/praxis/tasks/api_docs.rb +17 -64
  58. data/lib/praxis/tasks/routes.rb +1 -1
  59. data/lib/praxis/types/media_type_common.rb +1 -11
  60. data/lib/praxis/version.rb +1 -1
  61. data/praxis.gemspec +0 -1
  62. data/spec/functional_spec.rb +5 -9
  63. data/spec/praxis/action_definition_spec.rb +12 -20
  64. data/spec/praxis/blueprint_spec.rb +373 -0
  65. data/spec/praxis/bootloader_spec.rb +10 -2
  66. data/spec/praxis/collection_spec.rb +0 -13
  67. data/spec/praxis/config_hash_spec.rb +64 -0
  68. data/spec/praxis/{resource_definition_spec.rb → endpoint_definition_spec.rb} +37 -64
  69. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +249 -168
  70. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
  71. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +190 -8
  72. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +140 -0
  73. data/spec/praxis/extensions/field_expansion_spec.rb +5 -24
  74. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
  75. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
  76. data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
  77. data/spec/praxis/field_expander_spec.rb +149 -0
  78. data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
  79. data/spec/praxis/media_type_identifier_spec.rb +5 -4
  80. data/spec/praxis/media_type_spec.rb +4 -93
  81. data/spec/praxis/renderer_spec.rb +188 -0
  82. data/spec/praxis/response_definition_spec.rb +0 -31
  83. data/spec/praxis/response_spec.rb +1 -1
  84. data/spec/praxis/router_spec.rb +8 -8
  85. data/spec/praxis/routing_config_spec.rb +3 -3
  86. data/spec/spec_app/app/controllers/instances.rb +13 -7
  87. data/spec/spec_app/design/media_types/instance.rb +1 -19
  88. data/spec/spec_app/design/media_types/volume.rb +1 -1
  89. data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -14
  90. data/spec/spec_app/design/resources/instances.rb +5 -8
  91. data/spec/spec_app/design/resources/volume_snapshots.rb +1 -1
  92. data/spec/spec_app/design/resources/volumes.rb +1 -1
  93. data/spec/support/spec_authorization_plugin.rb +1 -1
  94. data/spec/support/spec_blueprints.rb +72 -0
  95. data/spec/support/{spec_resource_definitions.rb → spec_endpoint_definitions.rb} +2 -2
  96. data/spec/support/spec_media_types.rb +6 -26
  97. data/tasks/thor/app.rb +8 -34
  98. data/tasks/thor/example.rb +51 -285
  99. data/tasks/thor/model.rb +40 -0
  100. data/tasks/thor/scaffold.rb +117 -0
  101. data/tasks/thor/templates/generator/empty_app/.gitignore +0 -1
  102. data/tasks/thor/templates/generator/empty_app/Gemfile +7 -23
  103. data/tasks/thor/templates/generator/empty_app/README.md +1 -1
  104. data/tasks/thor/templates/generator/empty_app/Rakefile +4 -13
  105. data/tasks/thor/templates/generator/empty_app/{design/response_templates → app/v1/resources}/.empty_directory +0 -0
  106. data/tasks/thor/templates/generator/empty_app/{design/response_templates → app/v1/resources}/.gitkeep +0 -0
  107. data/tasks/thor/templates/generator/empty_app/config/environment.rb +26 -17
  108. data/tasks/thor/templates/generator/empty_app/{design/v1/resources → config/initializers}/.empty_directory +0 -0
  109. data/tasks/thor/templates/generator/empty_app/{design/v1/resources → config/initializers}/.gitkeep +0 -0
  110. data/tasks/thor/templates/generator/empty_app/design/v1/endpoints/.empty_directory +0 -0
  111. data/tasks/thor/templates/generator/empty_app/design/v1/endpoints/.gitkeep +0 -0
  112. data/tasks/thor/templates/generator/empty_app/docs/.empty_directory +0 -0
  113. data/tasks/thor/templates/generator/empty_app/docs/.gitkeep +0 -0
  114. data/tasks/thor/templates/generator/empty_app/spec/spec_helper.rb +14 -9
  115. data/tasks/thor/templates/generator/example_app/.gitignore +1 -0
  116. data/tasks/thor/templates/generator/example_app/Gemfile +19 -0
  117. data/tasks/thor/templates/generator/example_app/Rakefile +61 -0
  118. data/tasks/thor/templates/generator/example_app/app/models/user.rb +6 -0
  119. data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
  120. data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +17 -0
  121. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +11 -0
  122. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +25 -0
  123. data/tasks/thor/templates/generator/example_app/config.ru +30 -0
  124. data/tasks/thor/templates/generator/example_app/config/environment.rb +41 -0
  125. data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +12 -0
  126. data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
  127. data/tasks/thor/templates/generator/example_app/design/api.rb +18 -0
  128. data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +37 -0
  129. data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +21 -0
  130. data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +20 -0
  131. data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +42 -0
  132. data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +37 -0
  133. data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
  134. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
  135. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
  136. data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
  137. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
  138. data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
  139. data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
  140. metadata +64 -136
  141. data/lib/api_browser/.bowerrc +0 -3
  142. data/lib/api_browser/.editorconfig +0 -21
  143. data/lib/api_browser/Gruntfile.js +0 -581
  144. data/lib/api_browser/app/index.html +0 -59
  145. data/lib/api_browser/app/js/app.js +0 -48
  146. data/lib/api_browser/app/js/controllers/action.js +0 -47
  147. data/lib/api_browser/app/js/controllers/controller.js +0 -10
  148. data/lib/api_browser/app/js/controllers/menu.js +0 -93
  149. data/lib/api_browser/app/js/controllers/trait.js +0 -10
  150. data/lib/api_browser/app/js/controllers/type.js +0 -24
  151. data/lib/api_browser/app/js/directives/attribute_description.js +0 -56
  152. data/lib/api_browser/app/js/directives/attribute_table.js +0 -28
  153. data/lib/api_browser/app/js/directives/conditional_requirements.js +0 -13
  154. data/lib/api_browser/app/js/directives/fixed_if_fits.js +0 -38
  155. data/lib/api_browser/app/js/directives/highlight.js +0 -14
  156. data/lib/api_browser/app/js/directives/menu_item.js +0 -59
  157. data/lib/api_browser/app/js/directives/no_container.js +0 -8
  158. data/lib/api_browser/app/js/directives/readable_list.js +0 -87
  159. data/lib/api_browser/app/js/directives/request_examples.js +0 -31
  160. data/lib/api_browser/app/js/directives/type_placeholder.js +0 -30
  161. data/lib/api_browser/app/js/directives/url.js +0 -15
  162. data/lib/api_browser/app/js/factories/Configuration.js +0 -12
  163. data/lib/api_browser/app/js/factories/Documentation.js +0 -61
  164. data/lib/api_browser/app/js/factories/Example.js +0 -51
  165. data/lib/api_browser/app/js/factories/PageInfo.js +0 -9
  166. data/lib/api_browser/app/js/factories/normalize_attributes.js +0 -20
  167. data/lib/api_browser/app/js/factories/prepare_template.js +0 -15
  168. data/lib/api_browser/app/js/factories/template_for.js +0 -128
  169. data/lib/api_browser/app/js/filters/attribute_name.js +0 -10
  170. data/lib/api_browser/app/js/filters/friendly_json.js +0 -5
  171. data/lib/api_browser/app/js/filters/has_requirement.js +0 -14
  172. data/lib/api_browser/app/js/filters/header_info.js +0 -9
  173. data/lib/api_browser/app/js/filters/is_empty.js +0 -8
  174. data/lib/api_browser/app/js/filters/markdown.js +0 -6
  175. data/lib/api_browser/app/js/filters/resource_name.js +0 -5
  176. data/lib/api_browser/app/js/filters/tag_requirement.js +0 -13
  177. data/lib/api_browser/app/sass/modules/_body.scss +0 -40
  178. data/lib/api_browser/app/sass/modules/_cloke.scss +0 -8
  179. data/lib/api_browser/app/sass/modules/_header.scss +0 -10
  180. data/lib/api_browser/app/sass/modules/_nav.scss +0 -7
  181. data/lib/api_browser/app/sass/modules/_sidebar.scss +0 -134
  182. data/lib/api_browser/app/sass/modules/_switch.scss +0 -55
  183. data/lib/api_browser/app/sass/modules/_table.scss +0 -13
  184. data/lib/api_browser/app/sass/praxis.scss +0 -70
  185. data/lib/api_browser/app/sass/variables/_bootstrap-variables.scss +0 -774
  186. data/lib/api_browser/app/views/action.html +0 -97
  187. data/lib/api_browser/app/views/builtin/field-selector.html +0 -24
  188. data/lib/api_browser/app/views/controller.html +0 -55
  189. data/lib/api_browser/app/views/directives/attribute_description.html +0 -2
  190. data/lib/api_browser/app/views/directives/attribute_description/default.html +0 -2
  191. data/lib/api_browser/app/views/directives/attribute_description/example.html +0 -13
  192. data/lib/api_browser/app/views/directives/attribute_description/headers.html +0 -8
  193. data/lib/api_browser/app/views/directives/attribute_description/member_options.html +0 -4
  194. data/lib/api_browser/app/views/directives/attribute_description/values.html +0 -14
  195. data/lib/api_browser/app/views/directives/attribute_table.html +0 -17
  196. data/lib/api_browser/app/views/directives/menu_item.html +0 -8
  197. data/lib/api_browser/app/views/directives/url.html +0 -3
  198. data/lib/api_browser/app/views/examples/general.html +0 -26
  199. data/lib/api_browser/app/views/home.html +0 -5
  200. data/lib/api_browser/app/views/layout.html +0 -8
  201. data/lib/api_browser/app/views/menu.html +0 -42
  202. data/lib/api_browser/app/views/navbar.html +0 -9
  203. data/lib/api_browser/app/views/trait.html +0 -13
  204. data/lib/api_browser/app/views/type.html +0 -6
  205. data/lib/api_browser/app/views/type/details.html +0 -33
  206. data/lib/api_browser/app/views/types/embedded/array.html +0 -2
  207. data/lib/api_browser/app/views/types/embedded/default.html +0 -12
  208. data/lib/api_browser/app/views/types/embedded/field-selector.html +0 -13
  209. data/lib/api_browser/app/views/types/embedded/links.html +0 -11
  210. data/lib/api_browser/app/views/types/embedded/requirements.html +0 -6
  211. data/lib/api_browser/app/views/types/embedded/single_req.html +0 -9
  212. data/lib/api_browser/app/views/types/embedded/struct.html +0 -14
  213. data/lib/api_browser/app/views/types/label/link.html +0 -1
  214. data/lib/api_browser/app/views/types/label/primitive.html +0 -1
  215. data/lib/api_browser/app/views/types/label/primitive_collection.html +0 -1
  216. data/lib/api_browser/app/views/types/label/type.html +0 -1
  217. data/lib/api_browser/app/views/types/label/type_collection.html +0 -1
  218. data/lib/api_browser/app/views/types/main/array.html +0 -22
  219. data/lib/api_browser/app/views/types/main/default.html +0 -23
  220. data/lib/api_browser/app/views/types/main/hash.html +0 -23
  221. data/lib/api_browser/app/views/types/standalone/array.html +0 -3
  222. data/lib/api_browser/app/views/types/standalone/default.html +0 -18
  223. data/lib/api_browser/app/views/types/standalone/struct.html +0 -2
  224. data/lib/api_browser/bower_template.json +0 -41
  225. data/lib/api_browser/package-lock.json +0 -7110
  226. data/lib/api_browser/package.json +0 -43
  227. data/lib/praxis/docs/generator.rb +0 -243
  228. data/lib/praxis/docs/link_builder.rb +0 -30
  229. data/lib/praxis/links.rb +0 -135
  230. data/lib/praxis/types/multipart.rb +0 -109
  231. data/spec/api_browser/directives/type_placeholder_spec.js +0 -134
  232. data/spec/api_browser/factories/configuration_spec.js +0 -32
  233. data/spec/api_browser/factories/documentation_spec.js +0 -100
  234. data/spec/api_browser/factories/normalize_attributes_spec.js +0 -92
  235. data/spec/api_browser/factories/template_for_spec.js +0 -67
  236. data/spec/api_browser/filters/attribute_name_spec.js +0 -23
  237. data/spec/praxis/types/multipart_spec.rb +0 -112
  238. data/tasks/thor/templates/generator/empty_app/.rspec +0 -1
  239. data/tasks/thor/templates/generator/empty_app/Guardfile +0 -3
  240. data/tasks/thor/templates/generator/empty_app/config/rainbows.rb +0 -57
  241. data/tasks/thor/templates/generator/empty_app/docs/app.js +0 -1
  242. data/tasks/thor/templates/generator/empty_app/docs/styles.scss +0 -3
@@ -44,7 +44,11 @@ describe Praxis::Bootloader do
44
44
  expect(bootloader.before(stage.name)).to eq('before!')
45
45
  end
46
46
 
47
- it "raises when given an invalid stage name"
47
+ it "raises when given an invalid stage name" do
48
+ expect{
49
+ bootloader.before('nope!')
50
+ }.to raise_error(Praxis::Exceptions::StageNotFound,/Error running a before block for stage nope!/)
51
+ end
48
52
  end
49
53
 
50
54
  context ".after" do
@@ -54,7 +58,11 @@ describe Praxis::Bootloader do
54
58
  expect(bootloader.after(stage.name)).to eq('after!')
55
59
  end
56
60
 
57
- it "raises when given an invalid stage name"
61
+ it "raises when given an invalid stage name" do
62
+ expect{
63
+ bootloader.after('nope!')
64
+ }.to raise_error(Praxis::Exceptions::StageNotFound,/Error running an after block for stage nope!/)
65
+ end
58
66
  end
59
67
 
60
68
  context ".use" do
@@ -51,19 +51,6 @@ describe Praxis::Collection do
51
51
  its(:identifier) { should eq Person.identifier + "; type=collection" }
52
52
  end
53
53
 
54
- context '.views' do
55
- subject(:views) { collection.views }
56
- its(:keys) { should match_array(member_type.views.keys)}
57
-
58
- it 'generates CollectionViews from the member views' do
59
- collection.views.each do |name, view|
60
- expect(view.name).to be name
61
- expect(view.schema).to be member_type
62
- expect(view.contents).to eq member_type.views[name].contents
63
- end
64
- end
65
- end
66
-
67
54
  context '.load' do
68
55
  let(:volume_data) do
69
56
  {
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
3
+
4
+ describe Praxis::ConfigHash do
5
+ subject(:instance) { Praxis::ConfigHash.new(hash, &block) }
6
+ let(:hash) { { one: ['existing'], two: 'dos' } }
7
+ let(:block) do
8
+ proc { 'abc' }
9
+ end
10
+
11
+ context 'initialization' do
12
+ it 'saves the passed hash' do
13
+ expect(subject.hash).to be(hash)
14
+ end
15
+ end
16
+
17
+ context '.from' do
18
+ subject(:instance) { Praxis::ConfigHash.from(hash, &block) }
19
+ it 'returns an instance' do
20
+ expect(subject).to be_kind_of(Praxis::ConfigHash)
21
+ expect(subject.hash).to be(hash)
22
+ end
23
+ end
24
+
25
+ context '#to_hash' do
26
+ let(:block) do
27
+ proc { hash['i_was'] = 'here' }
28
+ end
29
+ it 'evaluates the block and returns the resulting hash' do
30
+ expect(subject.to_hash).to eq(subject.hash)
31
+ expect(subject.hash['i_was']).to eq('here')
32
+ end
33
+ end
34
+
35
+ context '#method_missing' do
36
+ context 'when keys do not exist in the hash key' do
37
+ it 'sets a single value to the hash' do
38
+ subject.some_name 'someval'
39
+ expect(subject.hash[:some_name]).to eq('someval')
40
+ end
41
+ it 'sets a multiple values to the hash key' do
42
+ subject.some_name 'someval', 'other1', 'other2'
43
+ expect(subject.hash[:some_name]).to include('someval', 'other1', 'other2')
44
+ end
45
+ end
46
+ context 'when keys already exist in the hash key' do
47
+ it 'adds one value to the hash' do
48
+ subject.one'newval'
49
+ expect(subject.hash[:one]).to match_array(%w(existing newval))
50
+ end
51
+ it 'adds multiple values to the hash key' do
52
+ subject.one 'newval', 'other1', 'other2'
53
+ expect(subject.hash[:one]).to match_array(%w(existing newval other1 other2))
54
+ end
55
+ context 'when passing a value and a block' do
56
+ let(:my_block) { proc {} }
57
+ it 'adds the tuple to the hash key' do
58
+ subject.one 'val', &my_block
59
+ expect(subject.hash[:one]).to include(['val', my_block])
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Praxis::ResourceDefinition do
4
- subject(:resource_definition) { PeopleResource }
3
+ describe Praxis::EndpointDefinition do
4
+ subject(:endpoint_definition) { PeopleResource }
5
5
 
6
6
  its(:description) { should eq('People resource') }
7
7
  its(:media_type) { should eq(Person) }
@@ -14,10 +14,10 @@ describe Praxis::ResourceDefinition do
14
14
  its(:metadata) { should_not have_key(:doc_visibility) }
15
15
 
16
16
  context '.describe' do
17
- subject(:describe) { resource_definition.describe }
17
+ subject(:describe) { endpoint_definition.describe }
18
18
 
19
- its([:description]) { should eq(resource_definition.description) }
20
- its([:media_type]) { should eq(resource_definition.media_type.describe(true)) }
19
+ its([:description]) { should eq(endpoint_definition.description) }
20
+ its([:media_type]) { should eq(endpoint_definition.media_type.describe(true)) }
21
21
 
22
22
  its([:actions]) { should have(2).items }
23
23
  its([:metadata]) { should be_kind_of(Hash) }
@@ -25,7 +25,7 @@ describe Praxis::ResourceDefinition do
25
25
  it { should_not have_key(:parent)}
26
26
 
27
27
  context 'for a resource with a parent' do
28
- let(:resource_definition) { ApiResources::VolumeSnapshots}
28
+ let(:endpoint_definition) { ApiResources::VolumeSnapshots}
29
29
 
30
30
  its([:parent]) { should eq ApiResources::Volumes.id }
31
31
  end
@@ -34,29 +34,29 @@ describe Praxis::ResourceDefinition do
34
34
 
35
35
 
36
36
  context '.routing_prefix' do
37
- subject(:resource_definition) { ApiResources::VolumeSnapshots }
37
+ subject(:endpoint_definition) { ApiResources::VolumeSnapshots }
38
38
  it do
39
- expect(resource_definition.routing_prefix).to eq('/clouds/:cloud_id/volumes/:volume_id/snapshots')
39
+ expect(endpoint_definition.routing_prefix).to eq('/clouds/:cloud_id/volumes/:volume_id/snapshots')
40
40
  end
41
41
  end
42
42
 
43
43
  context '.parent_prefix' do
44
- subject(:resource_definition) { ApiResources::VolumeSnapshots }
44
+ subject(:endpoint_definition) { ApiResources::VolumeSnapshots }
45
45
  let(:base_path){ Praxis::ApiDefinition.instance.info.base_path }
46
46
  its(:parent_prefix){ should eq('/clouds/:cloud_id/volumes/:volume_id') }
47
47
  it do
48
- expect(resource_definition.parent_prefix).to_not match(/^#{base_path}/)
48
+ expect(endpoint_definition.parent_prefix).to_not match(/^#{base_path}/)
49
49
  end
50
50
  end
51
51
 
52
52
 
53
53
  context '.action' do
54
54
  it 'requires a block' do
55
- expect { resource_definition.action(:something)
55
+ expect { endpoint_definition.action(:something)
56
56
  }.to raise_error(ArgumentError)
57
57
  end
58
58
  it 'creates an ActionDefinition for actions' do
59
- index = resource_definition.actions[:index]
59
+ index = endpoint_definition.actions[:index]
60
60
  expect(index).to be_kind_of(Praxis::ActionDefinition)
61
61
  expect(index.description).to eq("index description")
62
62
  end
@@ -64,7 +64,7 @@ describe Praxis::ResourceDefinition do
64
64
  it 'complains if action names are not symbols' do
65
65
  expect do
66
66
  Class.new do
67
- include Praxis::ResourceDefinition
67
+ include Praxis::EndpointDefinition
68
68
  action "foo" do
69
69
  end
70
70
  end
@@ -73,9 +73,9 @@ describe Praxis::ResourceDefinition do
73
73
  end
74
74
 
75
75
  context 'action_defaults' do
76
- let(:resource_definition) do
76
+ let(:endpoint_definition) do
77
77
  Class.new do
78
- include Praxis::ResourceDefinition
78
+ include Praxis::EndpointDefinition
79
79
  media_type Person
80
80
 
81
81
  version '1.0'
@@ -135,13 +135,13 @@ describe Praxis::ResourceDefinition do
135
135
  end
136
136
 
137
137
  it 'are applied to actions' do
138
- action = resource_definition.actions[:show]
138
+ action = endpoint_definition.actions[:show]
139
139
  expect(action.params.attributes).to have_key(:id)
140
140
  expect(action.route.path.to_s).to eq '/api/:base_param/people/:id'
141
141
  end
142
142
 
143
143
  context 'includes base_params from the APIDefinition' do
144
- let(:show_action_params){ resource_definition.actions[:show].params }
144
+ let(:show_action_params){ endpoint_definition.actions[:show].params }
145
145
 
146
146
  it 'including globally defined' do
147
147
  expect(show_action_params.attributes).to have_key(:base_param)
@@ -159,15 +159,15 @@ describe Praxis::ResourceDefinition do
159
159
  end
160
160
 
161
161
  context 'setting other values' do
162
- subject(:resource_definition) { Class.new {include Praxis::ResourceDefinition } }
162
+ subject(:endpoint_definition) { Class.new {include Praxis::EndpointDefinition } }
163
163
 
164
164
  let(:some_proc) { Proc.new {} }
165
165
  let(:some_hash) { Hash.new }
166
166
 
167
167
  it 'accepts a string as media_type' do
168
- resource_definition.media_type('Something')
169
- expect(resource_definition.media_type).to be_kind_of(Praxis::SimpleMediaType)
170
- expect(resource_definition.media_type.identifier).to eq('Something')
168
+ endpoint_definition.media_type('Something')
169
+ expect(endpoint_definition.media_type).to be_kind_of(Praxis::SimpleMediaType)
170
+ expect(endpoint_definition.media_type.identifier).to eq('Something')
171
171
  end
172
172
 
173
173
  its(:version_options){ should be_kind_of(Hash) }
@@ -176,57 +176,30 @@ describe Praxis::ResourceDefinition do
176
176
 
177
177
 
178
178
  context '.trait' do
179
- subject(:resource_definition) { Class.new {include Praxis::ResourceDefinition } }
179
+ subject(:endpoint_definition) { Class.new {include Praxis::EndpointDefinition } }
180
180
  it 'raises an error for missing traits' do
181
- expect { resource_definition.use(:stuff) }.to raise_error(Praxis::Exceptions::InvalidTrait)
181
+ expect { endpoint_definition.trait(:stuff) }.to raise_error(Praxis::Exceptions::InvalidTrait)
182
182
  end
183
- it 'has a spec for actually using a trait'
184
- end
185
-
186
-
187
- context 'deprecated action methods' do
188
- subject(:resource_definition) do
189
- Class.new do
190
- include Praxis::ResourceDefinition
191
-
192
- def self.name
193
- 'FooBar'
194
- end
183
+ it 'adds it to its list when it is available in the APIDefinition instance' do
184
+ trait_name = :test
185
+ expect(endpoint_definition.traits).to_not include(trait_name)
195
186
 
196
- silence_warnings do
197
- payload { attribute :inherited_payload, String }
198
- headers { key "Inherited-Header", String }
199
- params { attribute :inherited_params, String }
200
- response :not_found
201
- end
202
-
203
- action :index do
204
- end
205
- end
187
+ endpoint_definition.trait(trait_name)
188
+ expect(endpoint_definition.traits).to include(trait_name)
206
189
  end
207
-
208
- let(:action) { resource_definition.actions[:index] }
209
-
210
- it 'are applied to the action' do
211
- expect(action.payload.attributes).to have_key(:inherited_payload)
212
- expect(action.headers.attributes).to have_key("Inherited-Header")
213
- expect(action.params.attributes).to have_key(:inherited_params)
214
- expect(action.responses).to have_key(:not_found)
215
- end
216
-
217
190
  end
218
191
 
219
192
  context 'with nodoc! called' do
220
193
  before do
221
- resource_definition.nodoc!
194
+ endpoint_definition.nodoc!
222
195
  end
223
196
 
224
197
  it 'has the :doc_visibility option set' do
225
- expect(resource_definition.metadata[:doc_visibility]).to be(:none)
198
+ expect(endpoint_definition.metadata[:doc_visibility]).to be(:none)
226
199
  end
227
200
 
228
201
  it 'is exposed in the describe' do
229
- expect(resource_definition.describe[:metadata][:doc_visibility]).to be(:none)
202
+ expect(endpoint_definition.describe[:metadata][:doc_visibility]).to be(:none)
230
203
  end
231
204
 
232
205
  end
@@ -238,14 +211,14 @@ describe Praxis::ResourceDefinition do
238
211
  end
239
212
  it 'cannot be done if already been defined' do
240
213
  expect{
241
- resource_definition.canonical_path :reset
214
+ endpoint_definition.canonical_path :reset
242
215
  }.to raise_error(/'canonical_path' can only be defined once./)
243
216
  end
244
217
  end
245
218
  context 'if none specified' do
246
- subject(:resource_definition) do
219
+ subject(:endpoint_definition) do
247
220
  Class.new do
248
- include Praxis::ResourceDefinition
221
+ include Praxis::EndpointDefinition
249
222
  action :show do
250
223
  end
251
224
  end
@@ -255,9 +228,9 @@ describe Praxis::ResourceDefinition do
255
228
  end
256
229
  end
257
230
  context 'with an undefined action' do
258
- subject(:resource_definition) do
231
+ subject(:endpoint_definition) do
259
232
  Class.new do
260
- include Praxis::ResourceDefinition
233
+ include Praxis::EndpointDefinition
261
234
  canonical_path :non_existent
262
235
  end
263
236
  end
@@ -275,7 +248,7 @@ describe Praxis::ResourceDefinition do
275
248
  end
276
249
  end
277
250
  context '#parse_href' do
278
- let(:parsed){ resource_definition.parse_href("/people/1") }
251
+ let(:parsed){ endpoint_definition.parse_href("/people/1") }
279
252
  it 'accesses the path expansion functions of the primary route' do
280
253
  expect(parsed).to have_key(:id)
281
254
  end
@@ -14,6 +14,7 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
14
14
  shared_examples 'subject_equivalent_to' do |expected_result|
15
15
  it do
16
16
  loaded_ids = subject.all.map(&:id).sort
17
+ expected_result = expected_result.call if expected_result.is_a?(Proc)
17
18
  expected_ids = expected_result.all.map(&:id).sort
18
19
  expect(loaded_ids).to_not be_empty
19
20
  expect(loaded_ids).to eq(expected_ids)
@@ -23,12 +24,11 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
23
24
  # Poorman's way to compare SQL queries...
24
25
  shared_examples 'subject_matches_sql' do |expected_sql|
25
26
  it do
26
- # Remove parenthesis as our queries have WHERE clauses using them...
27
- gen_sql = subject.all.to_sql.gsub(/[()]/,'')
27
+ gen_sql = subject.all.to_sql
28
28
  # Strip blank at the beggining (and end) of every line
29
29
  # ...and recompose it by adding an extra space at the beginning of each one instead
30
30
  exp = expected_sql.split(/\n/).map do |line|
31
- " " + line.strip.gsub(/[()]/,'')
31
+ " " + line.strip
32
32
  end.join.strip
33
33
  expect(gen_sql).to eq(exp)
34
34
  end
@@ -37,9 +37,9 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
37
37
  context 'initialize' do
38
38
  it 'sets the right things to the instance' do
39
39
  instance
40
- expect(instance.query).to eq(base_query)
40
+ expect(instance.instance_variable_get(:@initial_query)).to eq(base_query)
41
41
  expect(instance.model).to eq(base_model)
42
- expect(instance.attr_to_column).to eq(filters_map)
42
+ expect(instance.filters_map).to eq(filters_map)
43
43
  end
44
44
  end
45
45
  context 'generate' do
@@ -52,197 +52,278 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
52
52
  expect(subject).to be(base_query)
53
53
  end
54
54
  end
55
- context 'by a simple field' do
56
- context 'that maps to the same name' do
57
- let(:filters_string) { 'category_uuid=deadbeef1' }
58
- it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
59
- end
60
- context 'that maps to a different name' do
61
- let(:filters_string) { 'name=Book1'}
62
- it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
63
- end
64
- context 'that is mapped as a nested struct' do
65
- let(:filters_string) { 'fake_nested.name=Book1'}
66
- it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
67
- end
68
- end
55
+ context 'with flat AND conditions' do
56
+ context 'by a simple field' do
57
+ context 'that maps to the same name' do
58
+ let(:filters_string) { 'category_uuid=deadbeef1' }
59
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
60
+ end
61
+ context 'same-name filter mapping works' do
62
+ context 'even if ther was not a filter explicitly defined for it' do
63
+ let(:filters_string) { 'category_uuid=deadbeef1' }
64
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
65
+ end
69
66
 
70
- context 'by a field or a related model' do
71
- context 'for a belongs_to association' do
72
- let(:filters_string) { 'author.name=author2'}
73
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name' => 'author2')
74
- end
75
- context 'for a has_many association' do
76
- let(:filters_string) { 'taggings.label=primary' }
77
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
78
- end
79
- context 'for a has_many through association' do
80
- let(:filters_string) { 'tags.name=blue' }
81
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:tags).where('active_tags.name' => 'blue')
67
+ context 'but if it is a field that does not exist in the model' do
68
+ let(:filters_string) { 'nonexisting=valuehere' }
69
+ it 'it blows up with the right error' do
70
+ expect{subject}.to raise_error(/Filtering by nonexisting is not allowed/)
71
+ end
72
+ end
73
+ end
74
+ context 'that maps to a different name' do
75
+ let(:filters_string) { 'name=Book1'}
76
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
77
+ end
78
+ context 'that is mapped as a nested struct' do
79
+ let(:filters_string) { 'fake_nested.name=Book1'}
80
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
81
+ end
82
82
  end
83
- end
84
83
 
85
- context 'by using all supported operators' do
86
- PREF = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
87
- COMMON_SQL_PREFIX = <<~SQL
88
- SELECT "active_books".* FROM "active_books"
89
- INNER JOIN
90
- "active_authors" "#{PREF}/author" ON "#{PREF}/author"."id" = "active_books"."author_id"
91
- SQL
92
- context '=' do
93
- let(:filters_string) { 'author.id=11'}
94
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id = 11')
95
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
96
- WHERE "#{PREF}/author"."id" = 11
97
- SQL
98
- end
99
- context '= (with array)' do
100
- let(:filters_string) { 'author.id=11,22'}
101
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IN (11,22)')
102
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
103
- WHERE "#{PREF}/author"."id" IN (11,22)
104
- SQL
105
- end
106
- context '!=' do
107
- let(:filters_string) { 'author.id!=11'}
108
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <> 11')
109
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
110
- WHERE "#{PREF}/author"."id" <> 11
111
- SQL
112
- end
113
- context '!= (with array)' do
114
- let(:filters_string) { 'author.id!=11,888'}
115
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id NOT IN (11,888)')
116
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
117
- WHERE "#{PREF}/author"."id" NOT IN (11,888)
118
- SQL
119
- end
120
- context '>' do
121
- let(:filters_string) { 'author.id>1'}
122
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id > 1')
123
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
124
- WHERE "#{PREF}/author"."id" > 1
125
- SQL
126
- end
127
- context '<' do
128
- let(:filters_string) { 'author.id<22'}
129
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id < 22')
130
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
131
- WHERE "#{PREF}/author"."id" < 22
132
- SQL
133
- end
134
- context '>=' do
135
- let(:filters_string) { 'author.id>=22'}
136
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id >= 22')
137
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
138
- WHERE "#{PREF}/author"."id" >= 22
139
- SQL
140
- end
141
- context '<=' do
142
- let(:filters_string) { 'author.id<=22'}
143
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <= 22')
144
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
145
- WHERE "#{PREF}/author"."id" <= 22
146
- SQL
147
- end
148
- context '!' do
149
- let(:filters_string) { 'author.id!'}
150
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IS NOT NULL')
151
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
152
- WHERE "#{PREF}/author"."id" IS NOT NULL
153
- SQL
84
+ context 'by a field or a related model' do
85
+ context 'for a belongs_to association' do
86
+ let(:filters_string) { 'author.name=author2'}
87
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name' => 'author2')
88
+ end
89
+ context 'for a has_many association' do
90
+ let(:filters_string) { 'taggings.label=primary' }
91
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
92
+ end
93
+ context 'for a has_many through association' do
94
+ let(:filters_string) { 'tags.name=blue' }
95
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:tags).where('active_tags.name' => 'blue')
96
+ end
154
97
  end
155
- context '!!' do
156
- let(:filters_string) { 'author.name!!'}
157
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name IS NULL')
158
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
159
- WHERE "#{PREF}/author"."name" IS NULL
160
- SQL
161
- end
162
- context 'including LIKE fuzzy queries' do
163
- context 'LIKE' do
164
- let(:filters_string) { 'author.name=author*'}
165
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name LIKE "author%"')
98
+
99
+ # NOTE: apparently AR when conditions are build with strings in the where clauses (instead of names, etc)
100
+ # it decides to parenthesize them, even when there's only 1 condition. Hence the silly parentization of
101
+ # these SQL fragments here (and others)
102
+ context 'by using all supported operators' do
103
+ PREF = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
104
+ COMMON_SQL_PREFIX = <<~SQL
105
+ SELECT "active_books".* FROM "active_books"
106
+ INNER JOIN
107
+ "active_authors" "#{PREF}/author" ON "#{PREF}/author"."id" = "active_books"."author_id"
108
+ SQL
109
+ context '=' do
110
+ let(:filters_string) { 'author.id=11'}
111
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id = 11')
112
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
113
+ WHERE ("#{PREF}/author"."id" = 11)
114
+ SQL
115
+ end
116
+ context '= (with array)' do
117
+ let(:filters_string) { 'author.id=11,22'}
118
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IN (11,22)')
119
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
120
+ WHERE ("#{PREF}/author"."id" IN (11,22))
121
+ SQL
122
+ end
123
+ context '!=' do
124
+ let(:filters_string) { 'author.id!=11'}
125
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <> 11')
126
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
127
+ WHERE ("#{PREF}/author"."id" <> 11)
128
+ SQL
129
+ end
130
+ context '!= (with array)' do
131
+ let(:filters_string) { 'author.id!=11,888'}
132
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id NOT IN (11,888)')
133
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
134
+ WHERE ("#{PREF}/author"."id" NOT IN (11,888))
135
+ SQL
136
+ end
137
+ context '>' do
138
+ let(:filters_string) { 'author.id>1'}
139
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id > 1')
140
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
141
+ WHERE ("#{PREF}/author"."id" > 1)
142
+ SQL
143
+ end
144
+ context '<' do
145
+ let(:filters_string) { 'author.id<22'}
146
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id < 22')
147
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
148
+ WHERE ("#{PREF}/author"."id" < 22)
149
+ SQL
150
+ end
151
+ context '>=' do
152
+ let(:filters_string) { 'author.id>=22'}
153
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id >= 22')
154
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
155
+ WHERE ("#{PREF}/author"."id" >= 22)
156
+ SQL
157
+ end
158
+ context '<=' do
159
+ let(:filters_string) { 'author.id<=22'}
160
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <= 22')
161
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
162
+ WHERE ("#{PREF}/author"."id" <= 22)
163
+ SQL
164
+ end
165
+ context '!' do
166
+ let(:filters_string) { 'author.id!'}
167
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IS NOT NULL')
166
168
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
167
- WHERE "#{PREF}/author"."name" LIKE 'author%'
168
- SQL
169
+ WHERE ("#{PREF}/author"."id" IS NOT NULL)
170
+ SQL
169
171
  end
170
- context 'NOT LIKE' do
171
- let(:filters_string) { 'author.name!=foobar*'}
172
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name NOT LIKE "foobar%"')
172
+ context '!!' do
173
+ let(:filters_string) { 'author.name!!'}
174
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name IS NULL')
173
175
  it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
174
- WHERE "#{PREF}/author"."name" NOT LIKE 'foobar%'
175
- SQL
176
+ WHERE ("#{PREF}/author"."name" IS NULL)
177
+ SQL
178
+ end
179
+ context 'including LIKE fuzzy queries' do
180
+ context 'LIKE' do
181
+ let(:filters_string) { 'author.name=author*'}
182
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name LIKE "author%"')
183
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
184
+ WHERE ("#{PREF}/author"."name" LIKE 'author%')
185
+ SQL
186
+ end
187
+ context 'NOT LIKE' do
188
+ let(:filters_string) { 'author.name!=foobar*'}
189
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name NOT LIKE "foobar%"')
190
+ it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
191
+ WHERE ("#{PREF}/author"."name" NOT LIKE 'foobar%')
192
+ SQL
193
+ end
176
194
  end
177
195
  end
178
- end
179
196
 
180
- context 'with a field mapping using a proc' do
181
- let(:filters_string) { 'name_is_not=Book1' }
182
- it_behaves_like 'subject_equivalent_to', ActiveBook.where.not(simple_name: 'Book1')
183
- end
197
+ context 'with a field mapping using a proc' do
198
+ let(:filters_string) { 'name_is_not=Book1' }
199
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where.not(simple_name: 'Book1')
200
+ end
184
201
 
185
- context 'with a deeply nested chains' do
186
- context 'of depth 2' do
187
- let(:filters_string) { 'category.books.name=Book2' }
188
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books).where('books_active_categories.simple_name': 'Book2')
202
+ context 'with a deeply nested chains' do
203
+ context 'of depth 2' do
204
+ let(:filters_string) { 'category.books.name=Book2' }
205
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books).where('books_active_categories.simple_name': 'Book2')
206
+ end
207
+ context 'multiple conditions on a nested relationship' do
208
+ let(:filters_string) { 'category.books.taggings.tag_id=1&category.books.taggings.label=primary' }
209
+ it_behaves_like 'subject_equivalent_to',
210
+ ActiveBook.joins(category: { books: :taggings }).where('active_taggings.tag_id': 1).where('active_taggings.label': 'primary')
211
+ it_behaves_like 'subject_matches_sql', <<~SQL
212
+ SELECT "active_books".* FROM "active_books"
213
+ INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
214
+ INNER JOIN "active_books" "books_active_categories" ON "books_active_categories"."category_uuid" = "active_categories"."uuid"
215
+ INNER JOIN "active_taggings" "#{PREF}/category/books/taggings" ON "/category/books/taggings"."book_id" = "books_active_categories"."id"
216
+ WHERE ("#{PREF}/category/books/taggings"."tag_id" = 1)
217
+ AND ("#{PREF}/category/books/taggings"."label" = 'primary')
218
+ SQL
219
+ end
220
+ context 'that contain multiple joins to the same table' do
221
+ let(:filters_string) { 'taggings.tag.taggings.tag_id=1' }
222
+ it_behaves_like 'subject_equivalent_to',
223
+ ActiveBook.joins(taggings: {tag: :taggings}).where('taggings_active_tags.tag_id=1')
224
+ end
189
225
  end
190
- context 'multiple conditions on a nested relationship' do
191
- let(:filters_string) { 'category.books.taggings.tag_id=1&category.books.taggings.label=primary' }
192
- it_behaves_like 'subject_equivalent_to',
193
- ActiveBook.joins(category: { books: :taggings }).where('active_taggings.tag_id': 1).where('active_taggings.label': 'primary')
194
- it_behaves_like 'subject_matches_sql', <<~SQL
195
- SELECT "active_books".* FROM "active_books"
196
- INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
197
- INNER JOIN "active_books" "books_active_categories" ON "books_active_categories"."category_uuid" = "active_categories"."uuid"
198
- INNER JOIN "active_taggings" "#{PREF}/category/books/taggings" ON "/category/books/taggings"."book_id" = "books_active_categories"."id"
199
- WHERE ("#{PREF}/category/books/taggings"."tag_id" = 1)
200
- AND ("#{PREF}/category/books/taggings"."label" = 'primary')
201
- SQL
226
+
227
+ context 'by multiple fields' do
228
+ context 'adds the where clauses for the top model if fields belong to it' do
229
+ let(:filters_string) { 'category_uuid=deadbeef1&name=Book1' }
230
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1', simple_name: 'Book1')
231
+ end
232
+ context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
233
+ let(:filters_string) { 'taggings.label=primary&taggings.tag_id=2' }
234
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary', 'active_taggings.tag_id' => 2)
235
+ end
202
236
  end
203
- context 'that contain multiple joins to the same table' do
204
- let(:filters_string) { 'taggings.tag.taggings.tag_id=1' }
205
- it_behaves_like 'subject_equivalent_to',
206
- ActiveBook.joins(taggings: {tag: :taggings}).where('taggings_active_tags.tag_id=1')
237
+
238
+ context 'uses fully qualified names for conditions (disambiguate fields)' do
239
+ context 'when we have a join table condition that has the same field' do
240
+ let(:filters_string) { 'name=Book1&category.books.name=Book3' }
241
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books)
242
+ .where('simple_name': 'Book1')
243
+ .where('books_active_categories.simple_name': 'Book3')
244
+ it_behaves_like 'subject_matches_sql', <<~SQL
245
+ SELECT "active_books".* FROM "active_books"
246
+ INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
247
+ INNER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
248
+ WHERE ("active_books"."simple_name" = 'Book1')
249
+ AND ("#{PREF}/category/books"."simple_name" = 'Book3')
250
+ SQL
251
+ end
252
+
253
+ context 'it qualifis them even if there are no joined tables/conditions at all' do
254
+ let(:filters_string) { 'id=11'}
255
+ it_behaves_like 'subject_matches_sql', <<~SQL
256
+ SELECT "active_books".* FROM "active_books"
257
+ WHERE ("active_books"."id" = 11)
258
+ SQL
259
+ end
260
+
207
261
  end
208
262
  end
209
263
 
210
- context 'by multiple fields' do
264
+ context 'with simple OR conditions' do
211
265
  context 'adds the where clauses for the top model if fields belong to it' do
212
- let(:filters_string) { 'category_uuid=deadbeef1&name=Book1' }
213
- it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1', simple_name: 'Book1')
266
+ let(:filters_string) { 'category_uuid=deadbeef1|name=Book1' }
267
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(simple_name: 'Book1'))
268
+ end
269
+ context 'supports top level parenthesis' do
270
+ let(:filters_string) { '(category_uuid=deadbeef1|name=Book1)' }
271
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(simple_name: 'Book1'))
214
272
  end
215
273
  context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
216
- let(:filters_string) { 'taggings.label=primary&taggings.tag_id=2' }
217
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary', 'active_taggings.tag_id' => 2)
274
+ let(:filters_string) { 'taggings.label=primary|taggings.tag_id=2' }
275
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
276
+ .or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
218
277
  end
219
278
  end
220
279
 
221
- context 'uses fully qualified names for conditions (disambiguate fields)' do
222
- context 'when we have a join table condition that has the same field' do
223
- COMMON_SQL_PREFIX = <<~SQL
280
+ context 'with combined AND and OR conditions' do
281
+ let(:filters_string) { '(category_uuid=deadbeef1|category_uuid=deadbeef2)&(name=Book1|name=Book2)' }
282
+ it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(category_uuid: 'deadbeef2'))
283
+ .and(ActiveBook.where(simple_name: 'Book1').or(ActiveBook.where(simple_name: 'Book2')))
284
+ it_behaves_like 'subject_matches_sql', <<~SQL
224
285
  SELECT "active_books".* FROM "active_books"
225
- INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
226
- INNER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
227
- SQL
228
- let(:filters_string) { 'name=Book1&category.books.name=Book3' }
229
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books)
230
- .where('simple_name': 'Book1')
231
- .where('books_active_categories.simple_name': 'Book3')
232
- it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
233
- WHERE ("#{PREF}/category/books"."simple_name" = 'Book3')
234
- AND ("active_books"."simple_name" = 'Book1')
235
- SQL
236
- end
286
+ WHERE ("active_books"."category_uuid" = 'deadbeef1' OR "active_books"."category_uuid" = 'deadbeef2')
287
+ AND ("active_books"."simple_name" = 'Book1' OR "active_books"."simple_name" = 'Book2')
288
+ SQL
237
289
 
238
- context 'it qualifis them even if there are no joined tables/conditions at all' do
239
- let(:filters_string) { 'id=11'}
290
+ context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
291
+ let(:filters_string) { 'taggings.label=primary|taggings.tag_id=2' }
292
+ 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))
240
294
  it_behaves_like 'subject_matches_sql', <<~SQL
241
295
  SELECT "active_books".* FROM "active_books"
242
- WHERE "active_books"."id" = 11
243
- SQL
296
+ INNER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
297
+ WHERE ("/taggings"."label" = 'primary' OR "/taggings"."tag_id" = 2)
298
+ SQL
244
299
  end
245
300
 
301
+ context '3-deep AND and OR conditions' do
302
+ let(:filters_string) { '(category.name=cat2|(taggings.label=primary&tags.name=red))&category_uuid=deadbeef1' }
303
+ it_behaves_like('subject_equivalent_to', Proc.new do
304
+ base=ActiveBook.joins(:category,:taggings,:tags)
305
+
306
+ and1_or1 = base.where('category.name': 'cat2')
307
+
308
+ and1_or2_and1 = base.where('taggings.label': 'primary')
309
+ and1_or2_and2 = base.where('tags.name': 'red')
310
+ and1_or2 = and1_or2_and1.and(and1_or2_and2)
311
+
312
+ and1 = and1_or1.or(and1_or2)
313
+ and2=base.where(category_uuid: 'deadbeef1')
314
+
315
+ query = and1.and(and2)
316
+ end)
317
+
318
+ it_behaves_like 'subject_matches_sql', <<~SQL
319
+ SELECT "active_books".* FROM "active_books"
320
+ INNER JOIN "active_categories" "/category" ON "/category"."uuid" = "active_books"."category_uuid"
321
+ INNER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
322
+ INNER JOIN "active_taggings" "taggings_active_books_join" ON "taggings_active_books_join"."book_id" = "active_books"."id"
323
+ INNER JOIN "active_tags" "/tags" ON "/tags"."id" = "taggings_active_books_join"."tag_id"
324
+ WHERE ("/category"."name" = 'cat2' OR ("/taggings"."label" = 'primary') AND ("/tags"."name" = 'red')) AND ("active_books"."category_uuid" = 'deadbeef1')
325
+ SQL
326
+ end
246
327
  end
247
328
 
248
329
  context 'ActiveRecord continues to work as expected (with our patches)' do