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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +54 -0
  3. data/.simplecov +3 -1
  4. data/.travis.yml +2 -1
  5. data/CHANGELOG.md +19 -0
  6. data/CONTRIBUTING.md +2 -79
  7. data/Gemfile +5 -1
  8. data/Guardfile +6 -4
  9. data/LICENSE +0 -2
  10. data/MAINTAINERS.md +1 -0
  11. data/README.md +15 -22
  12. data/Rakefile +4 -2
  13. data/bin/praxis +55 -58
  14. data/lib/praxis/action_definition/headers_dsl_compiler.rb +5 -6
  15. data/lib/praxis/action_definition.rb +65 -95
  16. data/lib/praxis/api_definition.rb +21 -29
  17. data/lib/praxis/api_general_info.rb +55 -66
  18. data/lib/praxis/application.rb +15 -32
  19. data/lib/praxis/blueprint.rb +80 -73
  20. data/lib/praxis/bootloader.rb +24 -33
  21. data/lib/praxis/bootloader_stages/environment.rb +5 -10
  22. data/lib/praxis/bootloader_stages/file_loader.rb +3 -6
  23. data/lib/praxis/bootloader_stages/plugin_config_load.rb +4 -6
  24. data/lib/praxis/bootloader_stages/plugin_config_prepare.rb +2 -2
  25. data/lib/praxis/bootloader_stages/plugin_loader.rb +3 -7
  26. data/lib/praxis/bootloader_stages/plugin_setup.rb +3 -3
  27. data/lib/praxis/bootloader_stages/routing.rb +5 -8
  28. data/lib/praxis/bootloader_stages/subgroup_loader.rb +2 -10
  29. data/lib/praxis/bootloader_stages/warn_unloaded_files.rb +15 -19
  30. data/lib/praxis/callbacks.rb +12 -11
  31. data/lib/praxis/collection.rb +11 -14
  32. data/lib/praxis/config.rb +17 -28
  33. data/lib/praxis/config_hash.rb +2 -1
  34. data/lib/praxis/controller.rb +7 -6
  35. data/lib/praxis/dispatcher.rb +34 -42
  36. data/lib/praxis/docs/open_api/info_object.rb +11 -8
  37. data/lib/praxis/docs/open_api/media_type_object.rb +18 -17
  38. data/lib/praxis/docs/open_api/operation_object.rb +7 -4
  39. data/lib/praxis/docs/open_api/parameter_object.rb +17 -14
  40. data/lib/praxis/docs/open_api/paths_object.rb +11 -9
  41. data/lib/praxis/docs/open_api/request_body_object.rb +14 -13
  42. data/lib/praxis/docs/open_api/response_object.rb +24 -18
  43. data/lib/praxis/docs/open_api/responses_object.rb +3 -1
  44. data/lib/praxis/docs/open_api/schema_object.rb +61 -29
  45. data/lib/praxis/docs/open_api/server_object.rb +5 -2
  46. data/lib/praxis/docs/open_api/tag_object.rb +9 -6
  47. data/lib/praxis/docs/open_api_generator.rb +114 -150
  48. data/lib/praxis/endpoint_definition.rb +60 -77
  49. data/lib/praxis/error_handler.rb +2 -2
  50. data/lib/praxis/exception.rb +2 -0
  51. data/lib/praxis/exceptions/config.rb +3 -1
  52. data/lib/praxis/exceptions/config_load.rb +2 -0
  53. data/lib/praxis/exceptions/config_validation.rb +3 -1
  54. data/lib/praxis/exceptions/invalid_configuration.rb +3 -1
  55. data/lib/praxis/exceptions/invalid_response.rb +3 -1
  56. data/lib/praxis/exceptions/invalid_trait.rb +3 -1
  57. data/lib/praxis/exceptions/stage_not_found.rb +3 -1
  58. data/lib/praxis/exceptions/validation.rb +4 -3
  59. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +163 -149
  60. data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +18 -13
  61. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +13 -9
  62. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +14 -11
  63. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +12 -9
  64. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +8 -5
  65. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +89 -65
  66. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +68 -62
  67. data/lib/praxis/extensions/attribute_filtering.rb +3 -1
  68. data/lib/praxis/extensions/field_expansion.rb +6 -4
  69. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +10 -8
  70. data/lib/praxis/extensions/field_selection/field_selector.rb +91 -92
  71. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +12 -12
  72. data/lib/praxis/extensions/field_selection.rb +3 -1
  73. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +6 -4
  74. data/lib/praxis/extensions/pagination/header_generator.rb +16 -11
  75. data/lib/praxis/extensions/pagination/ordering_params.rb +29 -28
  76. data/lib/praxis/extensions/pagination/pagination_handler.rb +44 -42
  77. data/lib/praxis/extensions/pagination/pagination_params.rb +29 -48
  78. data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +8 -7
  79. data/lib/praxis/extensions/pagination.rb +10 -15
  80. data/lib/praxis/extensions/rails_compat/request_methods.rb +3 -4
  81. data/lib/praxis/extensions/rails_compat.rb +2 -0
  82. data/lib/praxis/extensions/rendering.rb +12 -12
  83. data/lib/praxis/field_expander.rb +8 -9
  84. data/lib/praxis/file_group.rb +8 -12
  85. data/lib/praxis/finalizable.rb +1 -0
  86. data/lib/praxis/handlers/json.rb +5 -2
  87. data/lib/praxis/handlers/plain.rb +2 -1
  88. data/lib/praxis/handlers/www_form.rb +6 -3
  89. data/lib/praxis/handlers/{xml-sample.rb → xml_sample.rb} +26 -22
  90. data/lib/praxis/mapper/active_model_compat.rb +13 -10
  91. data/lib/praxis/mapper/resource.rb +196 -181
  92. data/lib/praxis/mapper/selector_generator.rb +106 -112
  93. data/lib/praxis/mapper/sequel_compat.rb +70 -67
  94. data/lib/praxis/media_type.rb +2 -2
  95. data/lib/praxis/media_type_identifier.rb +26 -22
  96. data/lib/praxis/middleware_app.rb +18 -15
  97. data/lib/praxis/multipart/parser.rb +46 -51
  98. data/lib/praxis/multipart/part.rb +78 -110
  99. data/lib/praxis/notifications.rb +2 -4
  100. data/lib/praxis/plugin.rb +11 -18
  101. data/lib/praxis/plugin_concern.rb +12 -15
  102. data/lib/praxis/plugins/mapper_plugin.rb +15 -13
  103. data/lib/praxis/plugins/pagination_plugin.rb +8 -6
  104. data/lib/praxis/plugins/rails_plugin.rb +33 -28
  105. data/lib/praxis/renderer.rb +11 -15
  106. data/lib/praxis/request.rb +48 -44
  107. data/lib/praxis/request_stages/action.rb +4 -6
  108. data/lib/praxis/request_stages/load_request.rb +2 -4
  109. data/lib/praxis/request_stages/request_stage.rb +19 -23
  110. data/lib/praxis/request_stages/response.rb +4 -6
  111. data/lib/praxis/request_stages/validate.rb +3 -5
  112. data/lib/praxis/request_stages/validate_params_and_headers.rb +15 -22
  113. data/lib/praxis/request_stages/validate_payload.rb +25 -28
  114. data/lib/praxis/request_superclassing.rb +3 -3
  115. data/lib/praxis/resource_definition.rb +1 -0
  116. data/lib/praxis/response.rb +24 -26
  117. data/lib/praxis/response_definition.rb +77 -122
  118. data/lib/praxis/response_template.rb +11 -15
  119. data/lib/praxis/responses/http.rb +23 -44
  120. data/lib/praxis/responses/internal_server_error.rb +18 -21
  121. data/lib/praxis/responses/multipart_ok.rb +4 -9
  122. data/lib/praxis/responses/validation_error.rb +8 -15
  123. data/lib/praxis/route.rb +8 -10
  124. data/lib/praxis/router/rack.rb +13 -7
  125. data/lib/praxis/router/simple.rb +10 -5
  126. data/lib/praxis/router.rb +27 -34
  127. data/lib/praxis/routing_config.rb +52 -29
  128. data/lib/praxis/simple_media_type.rb +5 -8
  129. data/lib/praxis/stage.rb +17 -25
  130. data/lib/praxis/tasks/api_docs.rb +17 -16
  131. data/lib/praxis/tasks/console.rb +3 -1
  132. data/lib/praxis/tasks/environment.rb +2 -0
  133. data/lib/praxis/tasks/routes.rb +26 -24
  134. data/lib/praxis/tasks.rb +3 -1
  135. data/lib/praxis/trait.rb +37 -46
  136. data/lib/praxis/types/fuzzy_hash.rb +13 -14
  137. data/lib/praxis/types/media_type_common.rb +11 -10
  138. data/lib/praxis/types/multipart_array/part_definition.rb +14 -17
  139. data/lib/praxis/types/multipart_array.rb +100 -115
  140. data/lib/praxis/validation_handler.rb +5 -3
  141. data/lib/praxis/version.rb +3 -1
  142. data/lib/praxis.rb +4 -5
  143. data/praxis.gemspec +22 -21
  144. data/spec/functional_spec.rb +44 -56
  145. data/spec/praxis/action_definition_spec.rb +39 -48
  146. data/spec/praxis/api_definition_spec.rb +45 -47
  147. data/spec/praxis/api_general_info_spec.rb +28 -29
  148. data/spec/praxis/application_spec.rb +18 -14
  149. data/spec/praxis/blueprint_spec.rb +33 -34
  150. data/spec/praxis/bootloader_spec.rb +32 -30
  151. data/spec/praxis/callbacks_spec.rb +37 -37
  152. data/spec/praxis/collection_spec.rb +18 -25
  153. data/spec/praxis/config_hash_spec.rb +5 -4
  154. data/spec/praxis/config_spec.rb +27 -26
  155. data/spec/praxis/controller_spec.rb +8 -9
  156. data/spec/praxis/endpoint_definition_spec.rb +25 -32
  157. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +171 -114
  158. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +22 -21
  159. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +112 -60
  160. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +37 -38
  161. data/spec/praxis/extensions/field_expansion_spec.rb +8 -10
  162. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +14 -13
  163. data/spec/praxis/extensions/field_selection/field_selector_spec.rb +9 -16
  164. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +50 -49
  165. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +32 -31
  166. data/spec/praxis/extensions/rendering_spec.rb +9 -9
  167. data/spec/praxis/extensions/support/spec_resources_active_model.rb +32 -49
  168. data/spec/praxis/extensions/support/spec_resources_sequel.rb +48 -48
  169. data/spec/praxis/field_expander_spec.rb +6 -5
  170. data/spec/praxis/file_group_spec.rb +3 -1
  171. data/spec/praxis/handlers/json_spec.rb +6 -5
  172. data/spec/praxis/mapper/resource_spec.rb +39 -29
  173. data/spec/praxis/mapper/selector_generator_spec.rb +80 -46
  174. data/spec/praxis/media_type_identifier_spec.rb +13 -10
  175. data/spec/praxis/media_type_spec.rb +12 -12
  176. data/spec/praxis/middleware_app_spec.rb +23 -22
  177. data/spec/praxis/multipart/parser_spec.rb +7 -9
  178. data/spec/praxis/notifications_spec.rb +4 -4
  179. data/spec/praxis/plugin_concern_spec.rb +5 -6
  180. data/spec/praxis/renderer_spec.rb +10 -9
  181. data/spec/praxis/request_spec.rb +38 -41
  182. data/spec/praxis/request_stages/action_spec.rb +14 -15
  183. data/spec/praxis/request_stages/request_stage_spec.rb +30 -41
  184. data/spec/praxis/request_stages/validate_spec.rb +3 -1
  185. data/spec/praxis/response_definition_spec.rb +79 -92
  186. data/spec/praxis/response_spec.rb +35 -40
  187. data/spec/praxis/responses/internal_server_error_spec.rb +6 -9
  188. data/spec/praxis/responses/validation_error_spec.rb +17 -18
  189. data/spec/praxis/route_spec.rb +4 -7
  190. data/spec/praxis/router_spec.rb +69 -79
  191. data/spec/praxis/routing_config_spec.rb +15 -14
  192. data/spec/praxis/stage_spec.rb +56 -53
  193. data/spec/praxis/trait_spec.rb +17 -17
  194. data/spec/praxis/types/fuzzy_hash_spec.rb +11 -9
  195. data/spec/praxis/types/multipart_array/part_definition_spec.rb +3 -2
  196. data/spec/praxis/types/multipart_array_spec.rb +33 -48
  197. data/spec/spec_app/app/concerns/authenticated.rb +5 -5
  198. data/spec/spec_app/app/concerns/basic_api.rb +3 -1
  199. data/spec/spec_app/app/concerns/log_wrapper.rb +5 -3
  200. data/spec/spec_app/app/controllers/base_class.rb +6 -5
  201. data/spec/spec_app/app/controllers/instances.rb +31 -34
  202. data/spec/spec_app/app/controllers/volumes.rb +6 -6
  203. data/spec/spec_app/app/responses/multipart.rb +1 -2
  204. data/spec/spec_app/app/responses/other_response.rb +2 -2
  205. data/spec/spec_app/config/environment.rb +19 -6
  206. data/spec/spec_app/config.ru +4 -3
  207. data/spec/spec_app/design/api.rb +13 -15
  208. data/spec/spec_app/design/media_types/instance.rb +6 -6
  209. data/spec/spec_app/design/media_types/volume.rb +2 -1
  210. data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -1
  211. data/spec/spec_app/design/resources/instances.rb +11 -17
  212. data/spec/spec_app/design/resources/volume_snapshots.rb +4 -5
  213. data/spec/spec_app/design/resources/volumes.rb +4 -5
  214. data/spec/spec_helper.rb +11 -13
  215. data/spec/support/be_deep_equal_matcher.rb +5 -0
  216. data/spec/support/spec_authorization_plugin.rb +7 -12
  217. data/spec/support/spec_blueprints.rb +5 -4
  218. data/spec/support/spec_complex_authentication_plugin.rb +17 -34
  219. data/spec/support/spec_endpoint_definitions.rb +2 -3
  220. data/spec/support/spec_media_types.rb +28 -35
  221. data/spec/support/spec_resources.rb +22 -16
  222. data/spec/support/spec_simple_authentication_plugin.rb +5 -9
  223. data/tasks/loader.thor +4 -2
  224. data/tasks/thor/app.rb +7 -5
  225. data/tasks/thor/example.rb +23 -22
  226. data/tasks/thor/model.rb +7 -7
  227. data/tasks/thor/scaffold.rb +23 -23
  228. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +0 -8
  229. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +1 -2
  230. metadata +72 -84
  231. data/MAINTAINERS +0 -2
  232. data/TODO.md +0 -25
  233. data/spec/praxis/api_resource_spec.rb +0 -0
  234. data/spec/praxis/dispatcher_spec.rb +0 -0
  235. data/spec/spec_app/app/responses/bulk_response.rb +0 -0
@@ -1,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