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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (235) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +54 -0
  3. data/.simplecov +3 -1
  4. data/.travis.yml +2 -1
  5. data/CHANGELOG.md +19 -0
  6. data/CONTRIBUTING.md +2 -79
  7. data/Gemfile +5 -1
  8. data/Guardfile +6 -4
  9. data/LICENSE +0 -2
  10. data/MAINTAINERS.md +1 -0
  11. data/README.md +15 -22
  12. data/Rakefile +4 -2
  13. data/bin/praxis +55 -58
  14. data/lib/praxis/action_definition/headers_dsl_compiler.rb +5 -6
  15. data/lib/praxis/action_definition.rb +65 -95
  16. data/lib/praxis/api_definition.rb +21 -29
  17. data/lib/praxis/api_general_info.rb +55 -66
  18. data/lib/praxis/application.rb +15 -32
  19. data/lib/praxis/blueprint.rb +80 -73
  20. data/lib/praxis/bootloader.rb +24 -33
  21. data/lib/praxis/bootloader_stages/environment.rb +5 -10
  22. data/lib/praxis/bootloader_stages/file_loader.rb +3 -6
  23. data/lib/praxis/bootloader_stages/plugin_config_load.rb +4 -6
  24. data/lib/praxis/bootloader_stages/plugin_config_prepare.rb +2 -2
  25. data/lib/praxis/bootloader_stages/plugin_loader.rb +3 -7
  26. data/lib/praxis/bootloader_stages/plugin_setup.rb +3 -3
  27. data/lib/praxis/bootloader_stages/routing.rb +5 -8
  28. data/lib/praxis/bootloader_stages/subgroup_loader.rb +2 -10
  29. data/lib/praxis/bootloader_stages/warn_unloaded_files.rb +15 -19
  30. data/lib/praxis/callbacks.rb +12 -11
  31. data/lib/praxis/collection.rb +11 -14
  32. data/lib/praxis/config.rb +17 -28
  33. data/lib/praxis/config_hash.rb +2 -1
  34. data/lib/praxis/controller.rb +7 -6
  35. data/lib/praxis/dispatcher.rb +34 -42
  36. data/lib/praxis/docs/open_api/info_object.rb +11 -8
  37. data/lib/praxis/docs/open_api/media_type_object.rb +18 -17
  38. data/lib/praxis/docs/open_api/operation_object.rb +7 -4
  39. data/lib/praxis/docs/open_api/parameter_object.rb +17 -14
  40. data/lib/praxis/docs/open_api/paths_object.rb +11 -9
  41. data/lib/praxis/docs/open_api/request_body_object.rb +14 -13
  42. data/lib/praxis/docs/open_api/response_object.rb +24 -18
  43. data/lib/praxis/docs/open_api/responses_object.rb +3 -1
  44. data/lib/praxis/docs/open_api/schema_object.rb +61 -29
  45. data/lib/praxis/docs/open_api/server_object.rb +5 -2
  46. data/lib/praxis/docs/open_api/tag_object.rb +9 -6
  47. data/lib/praxis/docs/open_api_generator.rb +114 -150
  48. data/lib/praxis/endpoint_definition.rb +60 -77
  49. data/lib/praxis/error_handler.rb +2 -2
  50. data/lib/praxis/exception.rb +2 -0
  51. data/lib/praxis/exceptions/config.rb +3 -1
  52. data/lib/praxis/exceptions/config_load.rb +2 -0
  53. data/lib/praxis/exceptions/config_validation.rb +3 -1
  54. data/lib/praxis/exceptions/invalid_configuration.rb +3 -1
  55. data/lib/praxis/exceptions/invalid_response.rb +3 -1
  56. data/lib/praxis/exceptions/invalid_trait.rb +3 -1
  57. data/lib/praxis/exceptions/stage_not_found.rb +3 -1
  58. data/lib/praxis/exceptions/validation.rb +4 -3
  59. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +163 -149
  60. data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +18 -13
  61. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +13 -9
  62. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +14 -11
  63. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +12 -9
  64. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +8 -5
  65. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +89 -65
  66. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +68 -62
  67. data/lib/praxis/extensions/attribute_filtering.rb +3 -1
  68. data/lib/praxis/extensions/field_expansion.rb +6 -4
  69. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +10 -8
  70. data/lib/praxis/extensions/field_selection/field_selector.rb +91 -92
  71. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +12 -12
  72. data/lib/praxis/extensions/field_selection.rb +3 -1
  73. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +6 -4
  74. data/lib/praxis/extensions/pagination/header_generator.rb +16 -11
  75. data/lib/praxis/extensions/pagination/ordering_params.rb +29 -28
  76. data/lib/praxis/extensions/pagination/pagination_handler.rb +44 -42
  77. data/lib/praxis/extensions/pagination/pagination_params.rb +29 -48
  78. data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +8 -7
  79. data/lib/praxis/extensions/pagination.rb +10 -15
  80. data/lib/praxis/extensions/rails_compat/request_methods.rb +3 -4
  81. data/lib/praxis/extensions/rails_compat.rb +2 -0
  82. data/lib/praxis/extensions/rendering.rb +12 -12
  83. data/lib/praxis/field_expander.rb +8 -9
  84. data/lib/praxis/file_group.rb +8 -12
  85. data/lib/praxis/finalizable.rb +1 -0
  86. data/lib/praxis/handlers/json.rb +5 -2
  87. data/lib/praxis/handlers/plain.rb +2 -1
  88. data/lib/praxis/handlers/www_form.rb +6 -3
  89. data/lib/praxis/handlers/{xml-sample.rb → xml_sample.rb} +26 -22
  90. data/lib/praxis/mapper/active_model_compat.rb +13 -10
  91. data/lib/praxis/mapper/resource.rb +196 -181
  92. data/lib/praxis/mapper/selector_generator.rb +106 -112
  93. data/lib/praxis/mapper/sequel_compat.rb +70 -67
  94. data/lib/praxis/media_type.rb +2 -2
  95. data/lib/praxis/media_type_identifier.rb +26 -22
  96. data/lib/praxis/middleware_app.rb +18 -15
  97. data/lib/praxis/multipart/parser.rb +46 -51
  98. data/lib/praxis/multipart/part.rb +78 -110
  99. data/lib/praxis/notifications.rb +2 -4
  100. data/lib/praxis/plugin.rb +11 -18
  101. data/lib/praxis/plugin_concern.rb +12 -15
  102. data/lib/praxis/plugins/mapper_plugin.rb +15 -13
  103. data/lib/praxis/plugins/pagination_plugin.rb +8 -6
  104. data/lib/praxis/plugins/rails_plugin.rb +33 -28
  105. data/lib/praxis/renderer.rb +11 -15
  106. data/lib/praxis/request.rb +48 -44
  107. data/lib/praxis/request_stages/action.rb +4 -6
  108. data/lib/praxis/request_stages/load_request.rb +2 -4
  109. data/lib/praxis/request_stages/request_stage.rb +19 -23
  110. data/lib/praxis/request_stages/response.rb +4 -6
  111. data/lib/praxis/request_stages/validate.rb +3 -5
  112. data/lib/praxis/request_stages/validate_params_and_headers.rb +15 -22
  113. data/lib/praxis/request_stages/validate_payload.rb +25 -28
  114. data/lib/praxis/request_superclassing.rb +3 -3
  115. data/lib/praxis/resource_definition.rb +1 -0
  116. data/lib/praxis/response.rb +24 -26
  117. data/lib/praxis/response_definition.rb +77 -122
  118. data/lib/praxis/response_template.rb +11 -15
  119. data/lib/praxis/responses/http.rb +23 -44
  120. data/lib/praxis/responses/internal_server_error.rb +18 -21
  121. data/lib/praxis/responses/multipart_ok.rb +4 -9
  122. data/lib/praxis/responses/validation_error.rb +8 -15
  123. data/lib/praxis/route.rb +8 -10
  124. data/lib/praxis/router/rack.rb +13 -7
  125. data/lib/praxis/router/simple.rb +10 -5
  126. data/lib/praxis/router.rb +27 -34
  127. data/lib/praxis/routing_config.rb +52 -29
  128. data/lib/praxis/simple_media_type.rb +5 -8
  129. data/lib/praxis/stage.rb +17 -25
  130. data/lib/praxis/tasks/api_docs.rb +17 -16
  131. data/lib/praxis/tasks/console.rb +3 -1
  132. data/lib/praxis/tasks/environment.rb +2 -0
  133. data/lib/praxis/tasks/routes.rb +26 -24
  134. data/lib/praxis/tasks.rb +3 -1
  135. data/lib/praxis/trait.rb +37 -46
  136. data/lib/praxis/types/fuzzy_hash.rb +13 -14
  137. data/lib/praxis/types/media_type_common.rb +11 -10
  138. data/lib/praxis/types/multipart_array/part_definition.rb +14 -17
  139. data/lib/praxis/types/multipart_array.rb +100 -115
  140. data/lib/praxis/validation_handler.rb +5 -3
  141. data/lib/praxis/version.rb +3 -1
  142. data/lib/praxis.rb +4 -5
  143. data/praxis.gemspec +22 -21
  144. data/spec/functional_spec.rb +44 -56
  145. data/spec/praxis/action_definition_spec.rb +39 -48
  146. data/spec/praxis/api_definition_spec.rb +45 -47
  147. data/spec/praxis/api_general_info_spec.rb +28 -29
  148. data/spec/praxis/application_spec.rb +18 -14
  149. data/spec/praxis/blueprint_spec.rb +33 -34
  150. data/spec/praxis/bootloader_spec.rb +32 -30
  151. data/spec/praxis/callbacks_spec.rb +37 -37
  152. data/spec/praxis/collection_spec.rb +18 -25
  153. data/spec/praxis/config_hash_spec.rb +5 -4
  154. data/spec/praxis/config_spec.rb +27 -26
  155. data/spec/praxis/controller_spec.rb +8 -9
  156. data/spec/praxis/endpoint_definition_spec.rb +25 -32
  157. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +171 -114
  158. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +22 -21
  159. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +112 -60
  160. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +37 -38
  161. data/spec/praxis/extensions/field_expansion_spec.rb +8 -10
  162. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +14 -13
  163. data/spec/praxis/extensions/field_selection/field_selector_spec.rb +9 -16
  164. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +50 -49
  165. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +32 -31
  166. data/spec/praxis/extensions/rendering_spec.rb +9 -9
  167. data/spec/praxis/extensions/support/spec_resources_active_model.rb +32 -49
  168. data/spec/praxis/extensions/support/spec_resources_sequel.rb +48 -48
  169. data/spec/praxis/field_expander_spec.rb +6 -5
  170. data/spec/praxis/file_group_spec.rb +3 -1
  171. data/spec/praxis/handlers/json_spec.rb +6 -5
  172. data/spec/praxis/mapper/resource_spec.rb +39 -29
  173. data/spec/praxis/mapper/selector_generator_spec.rb +80 -46
  174. data/spec/praxis/media_type_identifier_spec.rb +13 -10
  175. data/spec/praxis/media_type_spec.rb +12 -12
  176. data/spec/praxis/middleware_app_spec.rb +23 -22
  177. data/spec/praxis/multipart/parser_spec.rb +7 -9
  178. data/spec/praxis/notifications_spec.rb +4 -4
  179. data/spec/praxis/plugin_concern_spec.rb +5 -6
  180. data/spec/praxis/renderer_spec.rb +10 -9
  181. data/spec/praxis/request_spec.rb +38 -41
  182. data/spec/praxis/request_stages/action_spec.rb +14 -15
  183. data/spec/praxis/request_stages/request_stage_spec.rb +30 -41
  184. data/spec/praxis/request_stages/validate_spec.rb +3 -1
  185. data/spec/praxis/response_definition_spec.rb +79 -92
  186. data/spec/praxis/response_spec.rb +35 -40
  187. data/spec/praxis/responses/internal_server_error_spec.rb +6 -9
  188. data/spec/praxis/responses/validation_error_spec.rb +17 -18
  189. data/spec/praxis/route_spec.rb +4 -7
  190. data/spec/praxis/router_spec.rb +69 -79
  191. data/spec/praxis/routing_config_spec.rb +15 -14
  192. data/spec/praxis/stage_spec.rb +56 -53
  193. data/spec/praxis/trait_spec.rb +17 -17
  194. data/spec/praxis/types/fuzzy_hash_spec.rb +11 -9
  195. data/spec/praxis/types/multipart_array/part_definition_spec.rb +3 -2
  196. data/spec/praxis/types/multipart_array_spec.rb +33 -48
  197. data/spec/spec_app/app/concerns/authenticated.rb +5 -5
  198. data/spec/spec_app/app/concerns/basic_api.rb +3 -1
  199. data/spec/spec_app/app/concerns/log_wrapper.rb +5 -3
  200. data/spec/spec_app/app/controllers/base_class.rb +6 -5
  201. data/spec/spec_app/app/controllers/instances.rb +31 -34
  202. data/spec/spec_app/app/controllers/volumes.rb +6 -6
  203. data/spec/spec_app/app/responses/multipart.rb +1 -2
  204. data/spec/spec_app/app/responses/other_response.rb +2 -2
  205. data/spec/spec_app/config/environment.rb +19 -6
  206. data/spec/spec_app/config.ru +4 -3
  207. data/spec/spec_app/design/api.rb +13 -15
  208. data/spec/spec_app/design/media_types/instance.rb +6 -6
  209. data/spec/spec_app/design/media_types/volume.rb +2 -1
  210. data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -1
  211. data/spec/spec_app/design/resources/instances.rb +11 -17
  212. data/spec/spec_app/design/resources/volume_snapshots.rb +4 -5
  213. data/spec/spec_app/design/resources/volumes.rb +4 -5
  214. data/spec/spec_helper.rb +11 -13
  215. data/spec/support/be_deep_equal_matcher.rb +5 -0
  216. data/spec/support/spec_authorization_plugin.rb +7 -12
  217. data/spec/support/spec_blueprints.rb +5 -4
  218. data/spec/support/spec_complex_authentication_plugin.rb +17 -34
  219. data/spec/support/spec_endpoint_definitions.rb +2 -3
  220. data/spec/support/spec_media_types.rb +28 -35
  221. data/spec/support/spec_resources.rb +22 -16
  222. data/spec/support/spec_simple_authentication_plugin.rb +5 -9
  223. data/tasks/loader.thor +4 -2
  224. data/tasks/thor/app.rb +7 -5
  225. data/tasks/thor/example.rb +23 -22
  226. data/tasks/thor/model.rb +7 -7
  227. data/tasks/thor/scaffold.rb +23 -23
  228. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +0 -8
  229. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +1 -2
  230. metadata +72 -84
  231. data/MAINTAINERS +0 -2
  232. data/TODO.md +0 -25
  233. data/spec/praxis/api_resource_spec.rb +0 -0
  234. data/spec/praxis/dispatcher_spec.rb +0 -0
  235. data/spec/spec_app/app/responses/bulk_response.rb +0 -0
@@ -1,21 +1,22 @@
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::FilteringParams do
6
-
7
8
  context '.load' do
8
9
  subject { described_class.load(filters_string) }
9
10
 
10
11
  context 'unescapes the URL encoded values' do
11
12
  it 'for single values' do
12
- str = "one=#{CGI.escape('*')}&two>#{CGI.escape('^%$#!st_uff')}|three<normal"
13
- parsed = [
14
- { name: :one, op: '=', value: '*'},
15
- { name: :two, op: '>', value: '^%$#!st_uff'},
16
- { name: :three, op: '<', value: 'normal'},
17
- ]
18
- expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
13
+ str = "one=#{CGI.escape('*')}&two>#{CGI.escape('^%$#!st_uff')}|three<normal"
14
+ parsed = [
15
+ { name: :one, op: '=', value: '*' },
16
+ { name: :two, op: '>', value: '^%$#!st_uff' },
17
+ { name: :three, op: '<', value: 'normal' }
18
+ ]
19
+ expect(described_class.load(str).parsed_array.map { |i| i.slice(:name, :op, :value) }).to eq(parsed)
19
20
  end
20
21
  it 'each of the multi-values' do
21
22
  escaped_one = [
@@ -25,55 +26,55 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
25
26
  ].join(',')
26
27
  str = "one=#{escaped_one}&two>normal"
27
28
  parsed = [
28
- { name: :one, op: '=', value: ['fun!','Times','~!@#$%^&*()_+-={}|[]\:";\'<>?,./`']},
29
- { name: :two, op: '>', value: 'normal'},
29
+ { name: :one, op: '=', value: ['fun!', 'Times', '~!@#$%^&*()_+-={}|[]\:";\'<>?,./`'] },
30
+ { name: :two, op: '>', value: 'normal' }
30
31
  ]
31
- expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
32
+ expect(described_class.load(str).parsed_array.map { |i| i.slice(:name, :op, :value) }).to eq(parsed)
32
33
  end
33
34
  it 'does not handle badly escaped values that contain reserved chars ()|&,' do
34
35
  badly_escaped = 'val('
35
36
  str = "one=#{badly_escaped}&(two>normal|three!)"
36
- expect{
37
+ expect do
37
38
  described_class.load(str)
38
- }.to raise_error(Parslet::ParseFailed)
39
+ end.to raise_error(Parslet::ParseFailed)
39
40
  end
40
41
  end
41
42
  context 'parses for operator' do
42
43
  described_class::VALUE_OPERATORS.each do |op|
43
- it "#{op}" do
44
+ it op.to_s do
44
45
  str = "thename#{op}thevalue"
45
- parsed = [{ name: :thename, op: op, value: 'thevalue'}]
46
- expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
46
+ parsed = [{ name: :thename, op: op, value: 'thevalue' }]
47
+ expect(described_class.load(str).parsed_array.map { |i| i.slice(:name, :op, :value) }).to eq(parsed)
47
48
  end
48
49
  end
49
50
  described_class::NOVALUE_OPERATORS.each do |op|
50
- it "#{op}" do
51
+ it op.to_s do
51
52
  str = "thename#{op}"
52
- parsed = [{ name: :thename, op: op, value: nil}]
53
- expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
53
+ parsed = [{ name: :thename, op: op, value: nil }]
54
+ expect(described_class.load(str).parsed_array.map { |i| i.slice(:name, :op, :value) }).to eq(parsed)
54
55
  end
55
56
  end
56
57
  it 'can parse multiple values for filter' do
57
- str="filtername=1,2,3"
58
- parsed = [{ name: :filtername, op: '=', value: ["1","2","3"]}]
59
- expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
58
+ str = 'filtername=1,2,3'
59
+ parsed = [{ name: :filtername, op: '=', value: %w[1 2 3] }]
60
+ expect(described_class.load(str).parsed_array.map { |i| i.slice(:name, :op, :value) }).to eq(parsed)
60
61
  end
61
62
  end
62
63
  context 'with all value operators at once for the same AND group' do
63
- let(:filters_string) { 'one=11&two!=22&three>=33&four<=4&five<5&six>6&seven!&eight!!'}
64
+ let(:filters_string) { 'one=11&two!=22&three>=33&four<=4&five<5&six>6&seven!&eight!!' }
64
65
  it do
65
- expect(subject.parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq([
66
- { name: :one, op: '=', value: '11'},
67
- { name: :two, op: '!=', value: '22'},
68
- { name: :three, op: '>=', value: '33'},
69
- { name: :four, op: '<=', value: '4'},
70
- { name: :five, op: '<', value: '5'},
71
- { name: :six, op: '>', value: '6'},
72
- { name: :seven, op: '!', value: nil},
73
- { name: :eight, op: '!!', value: nil},
74
- ])
66
+ expect(subject.parsed_array.map { |i| i.slice(:name, :op, :value) }).to eq([
67
+ { name: :one, op: '=', value: '11' },
68
+ { name: :two, op: '!=', value: '22' },
69
+ { name: :three, op: '>=', value: '33' },
70
+ { name: :four, op: '<=', value: '4' },
71
+ { name: :five, op: '<', value: '5' },
72
+ { name: :six, op: '>', value: '6' },
73
+ { name: :seven, op: '!', value: nil },
74
+ { name: :eight, op: '!!', value: nil }
75
+ ])
75
76
  # And all have the same parent, which is an AND group
76
- parent = subject.parsed_array.map{|i|i[:node_object].parent_group}.uniq
77
+ parent = subject.parsed_array.map { |i| i[:node_object].parent_group }.uniq
77
78
  expect(parent.size).to eq(1)
78
79
  expect(parent.first.type).to eq(:and)
79
80
  expect(parent.first.parent_group).to be_nil
@@ -81,17 +82,17 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
81
82
  end
82
83
 
83
84
  context 'with with nested precedence groups' do
84
- let(:filters_string) { '(one=11)&(two!=22|three!!)&four<=4&five>5|six!'}
85
+ let(:filters_string) { '(one=11)&(two!=22|three!!)&four<=4&five>5|six!' }
85
86
  it do
86
87
  parsed = subject.parsed_array
87
- expect(parsed.map{|i| i.slice(:name,:op,:value)}).to eq([
88
- { name: :one, op: '=', value: '11'},
89
- { name: :two, op: '!=', value: '22'},
90
- { name: :three, op: '!!', value: nil},
91
- { name: :four, op: '<=', value: '4'},
92
- { name: :five, op: '>', value: '5'},
93
- { name: :six, op: '!', value: nil},
94
- ])
88
+ expect(parsed.map { |i| i.slice(:name, :op, :value) }).to eq([
89
+ { name: :one, op: '=', value: '11' },
90
+ { name: :two, op: '!=', value: '22' },
91
+ { name: :three, op: '!!', value: nil },
92
+ { name: :four, op: '<=', value: '4' },
93
+ { name: :five, op: '>', value: '5' },
94
+ { name: :six, op: '!', value: nil }
95
+ ])
95
96
  # Grouped appropriately
96
97
  parent_of = parsed.each_with_object({}) do |item, hash|
97
98
  hash[item[:name]] = item[:node_object].parent_group
@@ -106,9 +107,9 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
106
107
  # two and 3 are grouped together by an OR
107
108
  expect(parent_of[:two]).to be(parent_of[:three])
108
109
  expect(parent_of[:two].type).to eq(:or)
109
-
110
+
110
111
  # one, two, four and the or from two/three are grouped together by an AND
111
- expect([parent_of[:one],parent_of[:two].parent_group,parent_of[:four],parent_of[:five]]).to all(be(parent_of[:one]))
112
+ expect([parent_of[:one], parent_of[:two].parent_group, parent_of[:four], parent_of[:five]]).to all(be(parent_of[:one]))
112
113
  expect(parent_of[:one].type).to eq(:and)
113
114
 
114
115
  # six and the whole group above are grouped together with an OR
@@ -119,11 +120,12 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
119
120
 
120
121
  context 'value coercing when associated to a MediaType' do
121
122
  let(:parsed) do
122
- # Note wrap the filter_params (.for) type in an attribute (which then we discard), so it will
123
- # construct it propertly by applying the block. Seems easier than creating the type alone, and
123
+ # Note wrap the filter_params (.for) type in an attribute (which then we discard), so it will
124
+ # construct it propertly by applying the block. Seems easier than creating the type alone, and
124
125
  # then manually apply the block
125
126
  Attributor::Attribute.new(described_class.for(Post)) do
126
127
  filter 'id', using: ['=', '!=', '!']
128
+ any 'updated_at', using: ['>', '<', '=']
127
129
  end.type.load(str).parsed_array
128
130
  end
129
131
 
@@ -151,19 +153,41 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
151
153
  expect(parsed.first[:value]).to be_nil
152
154
  end
153
155
  end
154
- end
155
156
 
157
+ context 'for an ANY matcher' do
158
+ let(:str) { 'blog.timestamps.updated_at>=2001-01-01' }
159
+ it 'coerces its value to the associated mediatype attribute type' do
160
+ expect(parsed.first[:value]).to eq(DateTime.parse('2001-01-01'))
161
+ nested_updated_at_attr = Post.attributes[:blog].attributes[:timestamps].attributes[:updated_at]
162
+ expect(nested_updated_at_attr.type.valid_type?(parsed.first[:value])).to be_truthy
163
+ end
164
+ end
165
+
166
+ context 'for both normal and ANY type filters' do
167
+ let(:str) { 'blog.timestamps.updated_at>=2001-01-01&id=42' }
168
+ it 'properly parsed them both, and into their own types' do
169
+ expect(parsed.first[:value]).to eq(DateTime.parse('2001-01-01'))
170
+ nested_updated_at_attr = Post.attributes[:blog].attributes[:timestamps].attributes[:updated_at]
171
+ expect(nested_updated_at_attr.type.valid_type?(parsed.first[:value])).to be_truthy
172
+
173
+ expect(parsed.second[:value]).to eq(42)
174
+ id_attr = Post.attributes[:blog].attributes[:id]
175
+ expect(id_attr.type.valid_type?(parsed.second[:value])).to be_truthy
176
+ end
177
+ end
178
+ end
156
179
  end
157
180
 
158
181
  context '.validate' do
159
182
  let(:filtering_params_type) do
160
- # Note wrap the filter_params (.for) type in an attribute (which then we discard), so it will
161
- # construct it propertly by applying the block. Seems easier than creating the type alone, and
183
+ # Note wrap the filter_params (.for) type in an attribute (which then we discard), so it will
184
+ # construct it propertly by applying the block. Seems easier than creating the type alone, and
162
185
  # then manually apply the block
163
186
  Attributor::Attribute.new(described_class.for(Post)) do
164
187
  filter 'id', using: ['=', '!=', '!']
165
188
  filter 'title', using: ['=', '!='], fuzzy: true
166
189
  filter 'content', using: ['=', '!=', '!']
190
+ any 'updated_at', using: ['>', '<', '=']
167
191
  end.type
168
192
  end
169
193
  let(:loaded_params) { filtering_params_type.load(filters_string) }
@@ -171,23 +195,23 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
171
195
 
172
196
  context 'errors' do
173
197
  context 'given attributes that do not exist in the type' do
174
- let(:filters_string) { 'NotAnExistingAttribute=Foobar*'}
198
+ let(:filters_string) { 'NotAnExistingAttribute=Foobar*' }
175
199
  it 'raises an error' do
176
- expect{subject}.to raise_error(/NotAnExistingAttribute.*does not exist/)
200
+ expect { subject }.to raise_error(/NotAnExistingAttribute.*does not exist/)
177
201
  end
178
202
  end
179
203
 
180
204
  context 'given unallowed attributes' do
181
- let(:filters_string) { 'href=Foobar*'}
205
+ let(:filters_string) { 'href=Foobar*' }
182
206
  it 'raises an error' do
183
207
  expect(subject).to_not be_empty
184
- matches_error = subject.any? {|err| err =~ /Filtering by href is not allowed/}
208
+ matches_error = subject.any? { |err| err =~ /Filtering by href is not allowed.*leaf attributes matching updated_at/ }
185
209
  expect(matches_error).to be_truthy
186
210
  end
187
211
  end
188
212
 
189
213
  context 'given unallowed operator' do
190
- let(:filters_string) { 'title>Foobar*'}
214
+ let(:filters_string) { 'title>Foobar*' }
191
215
  it 'raises an error' do
192
216
  expect(subject).to_not be_empty
193
217
  expect(subject.first).to match(/Operator > not allowed for filter title/)
@@ -195,15 +219,43 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
195
219
  end
196
220
  end
197
221
 
222
+ context 'for any-type leaf filters' do
223
+ context 'with the correct operator' do
224
+ let(:filters_string) { 'timestamps.updated_at>2001-01-01' }
225
+ it 'succeeds' do
226
+ expect(subject).to be_empty
227
+ end
228
+ end
229
+ context 'with the correct operator on a nested mediatype' do
230
+ let(:filters_string) { 'blog.timestamps.updated_at>2001-01-01' }
231
+ it 'succeeds' do
232
+ expect(subject).to be_empty
233
+ end
234
+ end
235
+ context 'with an incorrect operator' do
236
+ let(:filters_string) { 'blog.timestamps.updated_at>=2001-01-01' }
237
+ it 'complains about it' do
238
+ expect(subject).to_not be_empty
239
+ expect(subject.first).to match(/Operator >= not allowed for filter blog.timestamps.updated_at/)
240
+ end
241
+ end
242
+ context 'with an correct operator but passing an unexistent attribute path' do
243
+ let(:filters_string) { 'some.not.true.path.updated_at>=2001-01-01' }
244
+ it 'complains about it' do
245
+ expect { subject }.to raise_error(/Error, you've requested to filter by field 'some'/)
246
+ end
247
+ end
248
+ end
249
+
198
250
  context 'non-valued operators' do
199
251
  context 'for string typed fields' do
200
- let(:filters_string) { 'content!'}
252
+ let(:filters_string) { 'content!' }
201
253
  it 'validates properly' do
202
254
  expect(subject).to be_empty
203
255
  end
204
256
  end
205
257
  context 'for non-string typed fields' do
206
- let(:filters_string) { 'id!'}
258
+ let(:filters_string) { 'id!' }
207
259
  it 'validates properly' do
208
260
  expect(subject).to be_empty
209
261
  end
@@ -212,7 +264,7 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
212
264
  context 'fuzzy matches' do
213
265
  context 'when allowed' do
214
266
  context 'given a fuzzy string' do
215
- let(:filters_string) { 'title=IAmAString*'}
267
+ let(:filters_string) { 'title=IAmAString*' }
216
268
  it 'validates properly' do
217
269
  expect(subject).to be_empty
218
270
  end
@@ -220,14 +272,14 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
220
272
  end
221
273
  context 'when NOT allowed' do
222
274
  context 'given a fuzzy string' do
223
- let(:filters_string) { 'content=IAmAString*'}
275
+ let(:filters_string) { 'content=IAmAString*' }
224
276
  it 'errors out' do
225
277
  expect(subject).to_not be_empty
226
278
  expect(subject.first).to match(/Fuzzy matching for content is not allowed/)
227
279
  end
228
280
  end
229
281
  context 'given a non-fuzzy string' do
230
- let(:filters_string) { 'content=IAmAString'}
282
+ let(:filters_string) { 'content=IAmAString' }
231
283
  it 'validates properly' do
232
284
  expect(subject).to be_empty
233
285
  end
@@ -1,22 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'praxis/extensions/attribute_filtering/filters_parser'
2
4
 
3
5
  describe Praxis::Extensions::AttributeFiltering::FilteringParams::Condition do
4
- end
6
+ end
5
7
 
6
8
  describe Praxis::Extensions::AttributeFiltering::FilteringParams::ConditionGroup do
7
- end
9
+ end
8
10
 
9
11
  describe Praxis::Extensions::AttributeFiltering::FilteringParams::Parser do
10
-
11
12
  context 'testing' do
12
13
  let(:expectations) do
13
- {
14
- 'one=11|two=22' => "( one=11 OR two=22 )"
14
+ {
15
+ 'one=11|two=22' => '( one=11 OR two=22 )'
15
16
  }
16
17
  end
17
18
  it 'parses and loads the parsed result into the tree objects' do
18
19
  expectations.each do |filters, dump_result|
19
-
20
20
  parsed = described_class.new.parse(filters)
21
21
  tree = Praxis::Extensions::AttributeFiltering::FilteringParams::ConditionGroup.load(parsed)
22
22
 
@@ -24,8 +24,7 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams::Parser do
24
24
  end
25
25
  end
26
26
  end
27
- context 'parses the grammar' do
28
-
27
+ context 'parses the grammar' do
29
28
  # Takes a hash with keys containing literal filters string, and values being the "dump format for Condition/Group"
30
29
  shared_examples 'round-trip-properly' do |expectations|
31
30
  it description do
@@ -38,24 +37,24 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams::Parser do
38
37
  end
39
38
 
40
39
  context 'single expression' do
41
- it_behaves_like 'round-trip-properly', {
40
+ it_behaves_like 'round-trip-properly', {
42
41
  'one=11' => 'one=11',
43
42
  '(one=11)' => 'one=11',
44
- 'one!' => "one!",
43
+ 'one!' => 'one!'
45
44
  }
46
45
  end
47
46
  context 'same expression operator' do
48
- it_behaves_like 'round-trip-properly', {
47
+ it_behaves_like 'round-trip-properly', {
49
48
  'one=11&two=22' => '( one=11 AND two=22 )',
50
49
  'one=11&two=22&three=3' => '( one=11 AND two=22 AND three=3 )',
51
50
  'one=1,2,3&two=4,5' => '( one=[1,2,3] AND two=[4,5] )',
52
51
  'one=11|two=22' => '( one=11 OR two=22 )',
53
- 'one=11|two=22|three=3' => '( one=11 OR two=22 OR three=3 )',
52
+ 'one=11|two=22|three=3' => '( one=11 OR two=22 OR three=3 )'
54
53
  }
55
54
  end
56
55
 
57
56
  context 'respects and/or precedence and parenthesis grouping' do
58
- it_behaves_like 'round-trip-properly', {
57
+ it_behaves_like 'round-trip-properly', {
59
58
  'a=1&b=2&z=9|c=3' => '( ( a=1 AND b=2 AND z=9 ) OR c=3 )',
60
59
  'a=1|b=2&c=3' => '( a=1 OR ( b=2 AND c=3 ) )',
61
60
  'a=1|b=2&c=3&d=4' => '( a=1 OR ( b=2 AND c=3 AND d=4 ) )',
@@ -65,56 +64,56 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams::Parser do
65
64
  'one=11&two=2|three=3' => '( ( one=11 AND two=2 ) OR three=3 )', # AND has higer precedence
66
65
  'one=11|two=2&three=3' => '( one=11 OR ( two=2 AND three=3 ) )', # AND has higer precedence
67
66
  'one=11&two=2|three=3&four=4' => '( ( one=11 AND two=2 ) OR ( three=3 AND four=4 ) )',
68
- '(one=11)&(two!=2|three=3)&four=4&five=5|six=6' =>
67
+ '(one=11)&(two!=2|three=3)&four=4&five=5|six=6' =>
69
68
  '( ( one=11 AND ( two!=2 OR three=3 ) AND four=4 AND five=5 ) OR six=6 )',
70
69
  '(one=11)&three=3' => '( one=11 AND three=3 )',
71
70
  '(one=11|two=2)&(three=3|four=4)' => '( ( one=11 OR two=2 ) AND ( three=3 OR four=4 ) )',
72
71
  '(category_uuid=deadbeef1|category_uuid=deadbeef2)&(name=Book1|name=Book2)' =>
73
72
  '( ( category_uuid=deadbeef1 OR category_uuid=deadbeef2 ) AND ( name=Book1 OR name=Book2 ) )',
74
73
  '(category_uuid=deadbeef1&name=Book1)|(category_uuid=deadbeef2&name=Book2)' =>
75
- '( ( category_uuid=deadbeef1 AND name=Book1 ) OR ( category_uuid=deadbeef2 AND name=Book2 ) )',
74
+ '( ( category_uuid=deadbeef1 AND name=Book1 ) OR ( category_uuid=deadbeef2 AND name=Book2 ) )'
76
75
  }
77
76
  end
78
77
 
79
78
  context 'empty values get converted to empty strings' do
80
- it_behaves_like 'round-trip-properly', {
79
+ it_behaves_like 'round-trip-properly', {
81
80
  'one=' => 'one=""',
82
- 'one=&two=2' => '( one="" AND two=2 )',
81
+ 'one=&two=2' => '( one="" AND two=2 )'
83
82
  }
84
83
  end
85
84
 
86
85
  context 'no value operands' do
87
- it_behaves_like 'round-trip-properly', {
88
- 'one!' => "one!",
89
- 'one!!' => "one!!"
86
+ it_behaves_like 'round-trip-properly', {
87
+ 'one!' => 'one!',
88
+ 'one!!' => 'one!!'
90
89
  }
91
90
 
92
91
  it 'fails if passing a value' do
93
- expect {
92
+ expect do
94
93
  described_class.new.parse('one!val')
95
- }.to raise_error(Parslet::ParseFailed)
96
- expect {
94
+ end.to raise_error(Parslet::ParseFailed)
95
+ expect do
97
96
  described_class.new.parse('one!!val')
98
- }.to raise_error(Parslet::ParseFailed)
99
- end
97
+ end.to raise_error(Parslet::ParseFailed)
98
+ end
100
99
  end
101
100
 
102
101
  context 'csv values result in multiple values for the operation' do
103
- it_behaves_like 'round-trip-properly', {
104
- 'multi=1,2' => "multi=[1,2]",
105
- 'multi=1,2,valuehere' => "multi=[1,2,valuehere]"
102
+ it_behaves_like 'round-trip-properly', {
103
+ 'multi=1,2' => 'multi=[1,2]',
104
+ 'multi=1,2,valuehere' => 'multi=[1,2,valuehere]'
106
105
  }
107
106
  end
108
107
 
109
108
  context 'supports [a-zA-Z0-9_\.] for filter names' do
110
- it_behaves_like 'round-trip-properly', {
111
- 'normal=1' => 'normal=1',
112
- 'cOmBo=1' => 'cOmBo=1',
113
- '1=2' => '1=2',
109
+ it_behaves_like 'round-trip-properly', {
110
+ 'normal=1' => 'normal=1',
111
+ 'cOmBo=1' => 'cOmBo=1',
112
+ '1=2' => '1=2',
114
113
  'aFew42Things=1' => 'aFew42Things=1',
115
114
  'under_scores=1' => 'under_scores=1',
116
115
  'several.dots.in.here=1' => 'several.dots.in.here=1',
117
- 'cOrN.00copia.of_thinGs.42_here=1' => 'cOrN.00copia.of_thinGs.42_here=1',
116
+ 'cOrN.00copia.of_thinGs.42_here=1' => 'cOrN.00copia.of_thinGs.42_here=1'
118
117
  }
119
118
  end
120
119
  context 'supports everything (except &|(),) for values (even without encoding..not allowed, but just to ensure the parser does not bomb)' do
@@ -123,7 +122,7 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams::Parser do
123
122
  'v=*foo*' => 'v={*}foo{*}',
124
123
  'v=*^%$#@!foo' => 'v={*}^%$#@!foo',
125
124
  'v=_-=\{}"?:><' => 'v=_-=\{}"?:><',
126
- 'v=_-=\{}"?:><,another_value!' => 'v=[_-=\{}"?:><,another_value!]',
125
+ 'v=_-=\{}"?:><,another_value!' => 'v=[_-=\{}"?:><,another_value!]'
127
126
  }
128
127
  end
129
128
  context 'properly detects and handles fuzzy matching encoded as {*} in the dump' do
@@ -132,7 +131,7 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams::Parser do
132
131
  'v=*foo*' => 'v={*}foo{*}',
133
132
  'v=foo*' => 'v=foo{*}',
134
133
  'v=*start,end*,*both*' => 'v=[{*}start,end{*},{*}both{*}]',
135
- "v=*#{CGI.escape('***')},#{CGI.escape('*')}" => 'v=[{*}***,*]', # Simple exact match on 2nd
134
+ "v=*#{CGI.escape('***')},#{CGI.escape('*')}" => 'v=[{*}***,*]' # Simple exact match on 2nd
136
135
  }
137
136
  end
138
137
  context 'properly handles url-encoded values' do
@@ -141,8 +140,8 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams::Parser do
141
140
  "v=*#{CGI.escape('foo')}*" => 'v={*}foo{*}',
142
141
  "v=*#{CGI.escape('^%$#@!foo')}" => 'v={*}^%$#@!foo',
143
142
  "v=#{CGI.escape('~!@#$%^&*()_+-={}|[]\:";\'<>?,./`')}" => 'v=~!@#$%^&*()_+-={}|[]\:";\'<>?,./`',
144
- "v=#{CGI.escape('_-+=\{}"?:><')},#{CGI.escape('another_value!')}" => 'v=[_-+=\{}"?:><,another_value!]',
143
+ "v=#{CGI.escape('_-+=\{}"?:><')},#{CGI.escape('another_value!')}" => 'v=[_-+=\{}"?:><,another_value!]'
145
144
  }
146
- end
145
+ end
147
146
  end
148
- end
147
+ end
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  require 'praxis/extensions/field_selection'
4
6
 
5
7
  describe Praxis::Extensions::FieldExpansion do
6
-
7
8
  # include the ActionDefinitionExtension module directly, as that's where the
8
9
  # bulk of lies, and by including this instead of the base FieldExpansion module
9
10
  # we avoid the side-effect of injecting the ActionDefinitionExtension into
@@ -24,8 +25,7 @@ describe Praxis::Extensions::FieldExpansion do
24
25
 
25
26
  let(:request_params) do
26
27
  double('params',
27
- fields: Praxis::Extensions::FieldSelection::FieldSelector.for(Person).load(fields)
28
- )
28
+ fields: Praxis::Extensions::FieldSelection::FieldSelector.for(Person).load(fields))
29
29
  end
30
30
 
31
31
  let(:request) { double('Praxis::Request', params: request_params) }
@@ -33,26 +33,25 @@ describe Praxis::Extensions::FieldExpansion do
33
33
 
34
34
  let(:fields) { nil }
35
35
 
36
- let(:test_attributes) { }
36
+ let(:test_attributes) {}
37
37
  let(:test_params) { double('test_params', attributes: test_attributes) }
38
38
 
39
- subject(:expansion) { test_instance.expanded_fields(request, media_type)}
39
+ subject(:expansion) { test_instance.expanded_fields(request, media_type) }
40
40
 
41
41
  context '#expanded_fields' do
42
-
43
42
  context 'with fields and view params defined' do
44
43
  let(:test_attributes) { {} }
45
44
 
46
45
  context 'with no fields provided' do
47
46
  it 'returns the fields for the default view' do
48
- expect(expansion).to eq({id: true, name: true})
47
+ expect(expansion).to eq({ id: true, name: true })
49
48
  end
50
49
  end
51
50
 
52
51
  context 'with a set of fields provided' do
53
52
  let(:fields) { 'id,name,owner{name}' }
54
53
  it 'returns the subset of fields' do
55
- expected = {id: true, name: true }
54
+ expected = { id: true, name: true }
56
55
  expect(expansion).to eq expected
57
56
  end
58
57
  end
@@ -61,9 +60,8 @@ describe Praxis::Extensions::FieldExpansion do
61
60
  context 'with an action with no params' do
62
61
  let(:test_params) { nil }
63
62
  it 'ignores incoming parameters and expands for the default fieldset' do
64
- expect(expansion).to eq({id: true, name: true})
63
+ expect(expansion).to eq({ id: true, name: true })
65
64
  end
66
65
  end
67
66
  end
68
-
69
67
  end
@@ -1,10 +1,12 @@
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
 
5
7
  describe Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector do
6
8
  let(:selector_fields) do
7
- {
9
+ {
8
10
  name: true,
9
11
  author: {
10
12
  id: true,
@@ -29,13 +31,13 @@ describe Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector do
29
31
  :id # We always load the primary keys
30
32
  ]
31
33
  end
32
- let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(ActiveBookResource,selector_fields).selectors }
33
- let(:debug){ false }
34
+ let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(ActiveBookResource, selector_fields).selectors }
35
+ let(:debug) { false }
34
36
 
35
- subject(:selector) {described_class.new(query: query, selectors: selector_node, debug: debug) }
37
+ subject(:selector) { described_class.new(query: query, selectors: selector_node, debug: debug) }
36
38
  context '#generate with a mocked' do
37
- let(:query) { double("Query") }
38
- it 'calls the select columns for the top level, and includes the right association hashes' do
39
+ let(:query) { double('Query') }
40
+ it 'calls the select columns for the top level, and includes the right association hashes' do
39
41
  expect(query).to receive(:select).with(*expected_select_from_to_query).and_return(query)
40
42
  expected_includes = {
41
43
  author: {
@@ -49,9 +51,9 @@ describe Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector do
49
51
  expect(query).to receive(:includes).with(expected_includes).and_return(query)
50
52
  expect(subject).to_not receive(:explain_query)
51
53
  subject.generate
52
- end
54
+ end
53
55
  context 'when debug is enabled' do
54
- let(:debug){ true }
56
+ let(:debug) { true }
55
57
  it 'calls the explain method' do
56
58
  expect(query).to receive(:select).and_return(query)
57
59
  expect(query).to receive(:includes).and_return(query)
@@ -74,7 +76,7 @@ describe Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector do
74
76
  },
75
77
  tags: {}
76
78
  }
77
- #expect(query).to receive(:includes).with(expected_includes).and_return(query)
79
+ # expect(query).to receive(:includes).with(expected_includes).and_return(query)
78
80
  expect(subject).to_not receive(:explain_query)
79
81
  final_query = subject.generate
80
82
  expect(final_query.select_values).to match_array(expected_select_from_to_query)
@@ -90,7 +92,7 @@ describe Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector do
90
92
  expect(book1.author.books.size).to eq 1
91
93
  expect(book1.author.books.map(&:simple_name)).to eq(['Book1'])
92
94
  expect(book1.category.name).to eq 'cat1'
93
- expect(book1.tags.map(&:name)).to match_array(['blue','red','green'])
95
+ expect(book1.tags.map(&:name)).to match_array(%w[blue red green])
94
96
 
95
97
  expect(book2.author.id).to eq 22
96
98
  expect(book2.author.books.size).to eq 1
@@ -104,7 +106,6 @@ describe Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector do
104
106
  # Actually make it run all the way...but suppressing the output
105
107
  subject.generate
106
108
  end
107
- end
109
+ end
108
110
  end
109
-
110
111
  end