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
@@ -4,14 +4,15 @@ require 'praxis/extensions/attribute_filtering'
4
4
 
5
5
  describe Praxis::Extensions::AttributeFiltering::FilterTreeNode do
6
6
 
7
+ let(:dummy_object) { double("Fake NodeObject")}
7
8
  let(:filters) do
8
9
  [
9
- {name: 'one', specs: { op: '>', value: 1}},
10
- {name: 'one', specs: { op: '<', value: 10}},
11
- {name: 'rel1.a1', specs: { op: '=', value: 1}},
12
- {name: 'rel1.a2', specs: { op: '=', value: 2}},
13
- {name: 'rel1.rel2.b1', specs: { op: '=', value: 11}},
14
- {name: 'rel1.rel2.b2', specs: { op: '=', value: 12}}
10
+ {name: 'one', op: '>', value: 1, node_object: dummy_object},
11
+ {name: 'one', op: '<', value: 10},
12
+ {name: 'rel1.a1', op: '=', value: 1},
13
+ {name: 'rel1.a2', op: '=', value: 2},
14
+ {name: 'rel1.rel2.b1', op: '=', value: 11},
15
+ {name: 'rel1.rel2.b2', op: '=', value: 12, node_object: dummy_object}
15
16
  ]
16
17
  end
17
18
  context 'initialization' do
@@ -19,20 +20,38 @@ describe Praxis::Extensions::AttributeFiltering::FilterTreeNode do
19
20
  it 'holds the top conditions and the child in a TreeNode' do
20
21
  expect(subject.path).to eq([])
21
22
  expect(subject.conditions.size).to eq(2)
23
+ expect(subject.conditions.map{|i| i.slice(:name,:op,:value)}).to eq([
24
+ {name: 'one', op: '>', value: 1},
25
+ {name: 'one', op: '<', value: 10},
26
+ ])
22
27
  expect(subject.children.keys).to eq(['rel1'])
23
28
  expect(subject.children['rel1']).to be_kind_of(described_class)
24
29
  end
25
30
 
31
+ it 'passes on any node_object value at any level' do
32
+ expect(subject.conditions.first[:node_object]).to be(dummy_object)
33
+ expect(subject.conditions[1]).to_not have_key(:node_object)
34
+ expect(subject.children['rel1'].children['rel2'].conditions[1][:node_object]).to be(dummy_object)
35
+ end
36
+
26
37
  it 'recursively holds the conditions and the children of their children in a TreeNode' do
27
38
  rel1 = subject.children['rel1']
28
39
  expect(rel1.path).to eq(['rel1'])
29
40
  expect(rel1.conditions.size).to eq(2)
41
+ expect(rel1.conditions.map{|i| i.slice(:name,:op,:value)}).to eq([
42
+ {name: 'a1', op: '=', value: 1},
43
+ {name: 'a2', op: '=', value: 2},
44
+ ])
30
45
  expect(rel1.children.keys).to eq(['rel2'])
31
46
  expect(rel1.children['rel2']).to be_kind_of(described_class)
32
47
 
33
48
  rel1rel2 = rel1.children['rel2']
34
49
  expect(rel1rel2.path).to eq(['rel1','rel2'])
35
50
  expect(rel1rel2.conditions.size).to eq(2)
51
+ expect(rel1rel2.conditions.map{|i| i.slice(:name,:op,:value)}).to eq([
52
+ {name: 'b1', op: '=', value: 11},
53
+ {name: 'b2', op: '=', value: 12}
54
+ ])
36
55
  expect(rel1rel2.children.keys).to be_empty
37
56
  end
38
57
  end
@@ -6,28 +6,210 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
6
6
 
7
7
  context '.load' do
8
8
  subject { described_class.load(filters_string) }
9
+
10
+ context 'unescapes the URL encoded values' do
11
+ 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)
19
+ end
20
+ it 'each of the multi-values' do
21
+ escaped_one = [
22
+ CGI.escape('fun!'),
23
+ CGI.escape('Times'),
24
+ CGI.escape('~!@#$%^&*()_+-={}|[]\:";\'<>?,./`')
25
+ ].join(',')
26
+ str = "one=#{escaped_one}&two>normal"
27
+ parsed = [
28
+ { name: :one, op: '=', value: ['fun!','Times','~!@#$%^&*()_+-={}|[]\:";\'<>?,./`']},
29
+ { name: :two, op: '>', value: 'normal'},
30
+ ]
31
+ expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
32
+ end
33
+ end
9
34
  context 'parses for operator' do
10
- described_class::AVAILABLE_OPERATORS.each do |op|
35
+ described_class::VALUE_OPERATORS.each do |op|
11
36
  it "#{op}" do
12
37
  str = "thename#{op}thevalue"
13
38
  parsed = [{ name: :thename, op: op, value: 'thevalue'}]
14
- expect(described_class.load(str).parsed_array).to eq(parsed)
39
+ expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
40
+ end
41
+ end
42
+ described_class::NOVALUE_OPERATORS.each do |op|
43
+ it "#{op}" do
44
+ str = "thename#{op}"
45
+ parsed = [{ name: :thename, op: op, value: nil}]
46
+ expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
15
47
  end
16
48
  end
49
+ it 'can parse multiple values for filter' do
50
+ str="filtername=1,2,3"
51
+ parsed = [{ name: :filtername, op: '=', value: ["1","2","3"]}]
52
+ expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
53
+ end
17
54
  end
18
- context 'with all operators at once' do
19
- let(:filters_string) { 'one=1&two!=2&three>=3&four<=4&five<5&six>6&seven!&eight!!'}
55
+ context 'with all value operators at once for the same AND group' do
56
+ let(:filters_string) { 'one=11&two!=22&three>=33&four<=4&five<5&six>6&seven!&eight!!'}
20
57
  it do
21
- expect(subject.parsed_array).to eq([
22
- { name: :one, op: '=', value: '1'},
23
- { name: :two, op: '!=', value: '2'},
24
- { name: :three, op: '>=', value: '3'},
58
+ expect(subject.parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq([
59
+ { name: :one, op: '=', value: '11'},
60
+ { name: :two, op: '!=', value: '22'},
61
+ { name: :three, op: '>=', value: '33'},
25
62
  { name: :four, op: '<=', value: '4'},
26
63
  { name: :five, op: '<', value: '5'},
27
64
  { name: :six, op: '>', value: '6'},
28
65
  { name: :seven, op: '!', value: nil},
29
66
  { name: :eight, op: '!!', value: nil},
30
67
  ])
68
+ # And all have the same parent, which is an AND group
69
+ parent = subject.parsed_array.map{|i|i[:node_object].parent_group}.uniq
70
+ expect(parent.size).to eq(1)
71
+ expect(parent.first.type).to eq(:and)
72
+ expect(parent.first.parent_group).to be_nil
73
+ end
74
+ end
75
+
76
+ context 'with with nested precedence groups' do
77
+ let(:filters_string) { '(one=11)&(two!=22|three!!)&four<=4&five>5|six!'}
78
+ it do
79
+ parsed = subject.parsed_array
80
+ expect(parsed.map{|i| i.slice(:name,:op,:value)}).to eq([
81
+ { name: :one, op: '=', value: '11'},
82
+ { name: :two, op: '!=', value: '22'},
83
+ { name: :three, op: '!!', value: nil},
84
+ { name: :four, op: '<=', value: '4'},
85
+ { name: :five, op: '>', value: '5'},
86
+ { name: :six, op: '!', value: nil},
87
+ ])
88
+ # Grouped appropriately
89
+ parent_of = parsed.each_with_object({}) do |item, hash|
90
+ hash[item[:name]] = item[:node_object].parent_group
91
+ end
92
+ # This is the expected tree grouping result
93
+ # OR -- six
94
+ # |--- AND --five
95
+ # |--- four
96
+ # |--- OR -- three
97
+ # | |--- two
98
+ # |--- one
99
+ # two and 3 are grouped together by an OR
100
+ expect(parent_of[:two]).to be(parent_of[:three])
101
+ expect(parent_of[:two].type).to eq(:or)
102
+
103
+ # one, two, four and the or from two/three are grouped together by an AND
104
+ expect([parent_of[:one],parent_of[:two].parent_group,parent_of[:four],parent_of[:five]]).to all(be(parent_of[:one]))
105
+ expect(parent_of[:one].type).to eq(:and)
106
+
107
+ # six and the whole group above are grouped together with an OR
108
+ expect(parent_of[:six]).to be(parent_of[:one].parent_group)
109
+ expect(parent_of[:six].type).to eq(:or)
110
+ end
111
+ end
112
+
113
+ context 'value coercing when associated to a MediaType' do
114
+ let(:parsed) do
115
+ # Note wrap the filter_params (.for) type in an attribute (which then we discard), so it will
116
+ # construct it propertly by applying the block. Seems easier than creating the type alone, and
117
+ # then manually apply the block
118
+ Attributor::Attribute.new(described_class.for(Post)) do
119
+ filter 'id', using: ['=', '!=', '!']
120
+ end.type.load(str).parsed_array
121
+ end
122
+
123
+ context 'with a single value' do
124
+ let(:str) { 'id=1' }
125
+ it 'coerces its value to the associated mediatype attribute type' do
126
+ expect(parsed.first[:value]).to eq(1)
127
+ expect(Post.attributes[:id].type.valid_type?(parsed.first[:value])).to be_truthy
128
+ end
129
+ end
130
+
131
+ context 'with multimatch' do
132
+ let(:str) { 'id=1,2,3' }
133
+ it 'coerces ALL csv values to the associated mediatype attribute type' do
134
+ expect(parsed.first[:value]).to eq([1, 2, 3])
135
+ parsed.first[:value].each do |val|
136
+ expect(Post.attributes[:id].type.valid_type?(val)).to be_truthy
137
+ end
138
+ end
139
+ end
140
+
141
+ context 'with a single value that is null' do
142
+ let(:str) { 'id!' }
143
+ it 'properly loads it as null' do
144
+ expect(parsed.first[:value]).to be_nil
145
+ end
146
+ end
147
+ end
148
+
149
+ end
150
+
151
+ context '.validate' do
152
+ let(:filtering_params_type) do
153
+ # Note wrap the filter_params (.for) type in an attribute (which then we discard), so it will
154
+ # construct it propertly by applying the block. Seems easier than creating the type alone, and
155
+ # then manually apply the block
156
+ Attributor::Attribute.new(described_class.for(Post)) do
157
+ filter 'id', using: ['=', '!=']
158
+ filter 'title', using: ['=', '!='], fuzzy: true
159
+ filter 'content', using: ['=', '!=']
160
+ end.type
161
+ end
162
+ let(:loaded_params) { filtering_params_type.load(filters_string) }
163
+ subject { loaded_params.validate(filters_string) }
164
+
165
+ context 'errors' do
166
+ context 'given attributes that do not exist in the type' do
167
+ let(:filters_string) { 'NotAnExistingAttribute=Foobar*'}
168
+ it 'raises an error' do
169
+ expect{subject}.to raise_error(/NotAnExistingAttribute.*does not exist/)
170
+ end
171
+ end
172
+
173
+ context 'given unallowed attributes' do
174
+ let(:filters_string) { 'href=Foobar*'}
175
+ it 'raises an error' do
176
+ expect(subject).to_not be_empty
177
+ matches_error = subject.any? {|err| err =~ /Filtering by href is not allowed/}
178
+ expect(matches_error).to be_truthy
179
+ end
180
+ end
181
+
182
+ context 'given unallowed operator' do
183
+ let(:filters_string) { 'title>Foobar*'}
184
+ it 'raises an error' do
185
+ expect(subject).to_not be_empty
186
+ expect(subject.first).to match(/Operator > not allowed for filter title/)
187
+ end
188
+ end
189
+ end
190
+ context 'fuzzy matches' do
191
+ context 'when allowed' do
192
+ context 'given a fuzzy string' do
193
+ let(:filters_string) { 'title=IAmAString*'}
194
+ it 'validates properly' do
195
+ expect(subject).to be_empty
196
+ end
197
+ end
198
+ end
199
+ context 'when NOT allowed' do
200
+ context 'given a fuzzy string' do
201
+ let(:filters_string) { 'content=IAmAString*'}
202
+ it 'errors out' do
203
+ expect(subject).to_not be_empty
204
+ expect(subject.first).to match(/Fuzzy matching for content is not allowed/)
205
+ end
206
+ end
207
+ context 'given a non-fuzzy string' do
208
+ let(:filters_string) { 'content=IAmAString'}
209
+ it 'validates properly' do
210
+ expect(subject).to be_empty
211
+ end
212
+ end
31
213
  end
32
214
  end
33
215
  end
@@ -0,0 +1,140 @@
1
+ require 'praxis/extensions/attribute_filtering/filters_parser'
2
+
3
+
4
+ describe Praxis::Extensions::AttributeFiltering::FilteringParams::Condition do
5
+ end
6
+
7
+ describe Praxis::Extensions::AttributeFiltering::FilteringParams::ConditionGroup do
8
+ end
9
+
10
+ describe Praxis::Extensions::AttributeFiltering::FilteringParams::Parser do
11
+
12
+ context 'testing' do
13
+ let(:expectations) do
14
+ {
15
+ 'one=11|two=22' => "( one=11 OR two=22 )"
16
+ }
17
+ end
18
+ it 'parses and loads the parsed result into the tree objects' do
19
+ expectations.each do |filters, dump_result|
20
+
21
+ parsed = described_class.new.parse(filters)
22
+ tree = Praxis::Extensions::AttributeFiltering::FilteringParams::ConditionGroup.load(parsed)
23
+
24
+ expect(tree.dump).to eq(dump_result)
25
+ end
26
+ end
27
+ end
28
+ context 'parses the grammar' do
29
+
30
+ # Takes a hash with keys containing literal filters string, and values being the "dump format for Condition/Group"
31
+ shared_examples 'round-trip-properly' do |expectations|
32
+ it description do
33
+ expectations.each do |filters, dump_result|
34
+ parsed = Praxis::Extensions::AttributeFiltering::FilteringParams::Parser.new.parse(filters)
35
+ tree = Praxis::Extensions::AttributeFiltering::FilteringParams::ConditionGroup.load(parsed)
36
+ expect(tree.dump).to eq(dump_result)
37
+ end
38
+ end
39
+ end
40
+
41
+ context 'single expression' do
42
+ it_behaves_like 'round-trip-properly', {
43
+ 'one=11' => 'one=11',
44
+ '(one=11)' => 'one=11',
45
+ 'one!' => "one!",
46
+ }
47
+ end
48
+ context 'same expression operator' do
49
+ it_behaves_like 'round-trip-properly', {
50
+ 'one=11&two=22' => '( one=11 AND two=22 )',
51
+ 'one=11&two=22&three=3' => '( one=11 AND two=22 AND three=3 )',
52
+ 'one=1,2,3&two=4,5' => '( one=[1,2,3] AND two=[4,5] )',
53
+ 'one=11|two=22' => '( one=11 OR two=22 )',
54
+ 'one=11|two=22|three=3' => '( one=11 OR two=22 OR three=3 )',
55
+ }
56
+ end
57
+
58
+ context 'respects and/or precedence and parenthesis grouping' do
59
+ it_behaves_like 'round-trip-properly', {
60
+ 'a=1&b=2&z=9|c=3' => '( ( a=1 AND b=2 AND z=9 ) OR c=3 )',
61
+ 'a=1|b=2&c=3' => '( a=1 OR ( b=2 AND c=3 ) )',
62
+ 'a=1|b=2&c=3&d=4' => '( a=1 OR ( b=2 AND c=3 AND d=4 ) )',
63
+ '(a=1|b=2)&c=3&d=4' => '( ( a=1 OR b=2 ) AND c=3 AND d=4 )',
64
+ 'a=1|a.b.c_c=1&b=2' => '( a=1 OR ( a.b.c_c=1 AND b=2 ) )',
65
+ 'a=1,2,3|b=4,5&c=one,two' => '( a=[1,2,3] OR ( b=[4,5] AND c=[one,two] ) )',
66
+ 'one=11&two=2|three=3' => '( ( one=11 AND two=2 ) OR three=3 )', # AND has higer precedence
67
+ 'one=11|two=2&three=3' => '( one=11 OR ( two=2 AND three=3 ) )', # AND has higer precedence
68
+ 'one=11&two=2|three=3&four=4' => '( ( one=11 AND two=2 ) OR ( three=3 AND four=4 ) )',
69
+ '(one=11)&(two!=2|three=3)&four=4&five=5|six=6' =>
70
+ '( ( one=11 AND ( two!=2 OR three=3 ) AND four=4 AND five=5 ) OR six=6 )',
71
+ '(one=11)&three=3' => '( one=11 AND three=3 )',
72
+ '(one=11|two=2)&(three=3|four=4)' => '( ( one=11 OR two=2 ) AND ( three=3 OR four=4 ) )',
73
+ '(category_uuid=deadbeef1|category_uuid=deadbeef2)&(name=Book1|name=Book2)' =>
74
+ '( ( category_uuid=deadbeef1 OR category_uuid=deadbeef2 ) AND ( name=Book1 OR name=Book2 ) )',
75
+ '(category_uuid=deadbeef1&name=Book1)|(category_uuid=deadbeef2&name=Book2)' =>
76
+ '( ( category_uuid=deadbeef1 AND name=Book1 ) OR ( category_uuid=deadbeef2 AND name=Book2 ) )',
77
+ }
78
+ end
79
+
80
+ context 'empty values get converted to empty strings' do
81
+ it_behaves_like 'round-trip-properly', {
82
+ 'one=' => 'one=""',
83
+ 'one=&two=2' => '( one="" AND two=2 )',
84
+ }
85
+ end
86
+
87
+ context 'no value operands' do
88
+ it_behaves_like 'round-trip-properly', {
89
+ 'one!' => "one!",
90
+ 'one!!' => "one!!"
91
+ }
92
+
93
+ it 'fails if passing a value' do
94
+ expect {
95
+ described_class.new.parse('one!val')
96
+ }.to raise_error(Parslet::ParseFailed)
97
+ expect {
98
+ described_class.new.parse('one!!val')
99
+ }.to raise_error(Parslet::ParseFailed)
100
+ end
101
+ end
102
+
103
+ context 'csv values result in multiple values for the operation' do
104
+ it_behaves_like 'round-trip-properly', {
105
+ 'multi=1,2' => "multi=[1,2]",
106
+ 'multi=1,2,valuehere' => "multi=[1,2,valuehere]"
107
+ }
108
+ end
109
+
110
+ context 'supports [a-zA-Z0-9_\.] for filter names' do
111
+ it_behaves_like 'round-trip-properly', {
112
+ 'normal=1' => 'normal=1',
113
+ 'cOmBo=1' => 'cOmBo=1',
114
+ '1=2' => '1=2',
115
+ 'aFew42Things=1' => 'aFew42Things=1',
116
+ 'under_scores=1' => 'under_scores=1',
117
+ 'several.dots.in.here=1' => 'several.dots.in.here=1',
118
+ 'cOrN.00copia.of_thinGs.42_here=1' => 'cOrN.00copia.of_thinGs.42_here=1',
119
+ }
120
+ end
121
+ context 'supports everything (except &|(),) for values (even without encoding..not allowed, but just to ensure the parser does not bomb)' do
122
+ it_behaves_like 'round-trip-properly', {
123
+ 'v=1123' => 'v=1123',
124
+ 'v=*foo*' => 'v=*foo*',
125
+ 'v=*^%$#@!foo' => 'v=*^%$#@!foo',
126
+ 'v=_-=\{}"?:><' => 'v=_-=\{}"?:><',
127
+ 'v=_-=\{}"?:><,another_value!' => 'v=[_-=\{}"?:><,another_value!]',
128
+ }
129
+ end
130
+ context 'properly handles url-encoded values' do
131
+ it_behaves_like 'round-trip-properly', {
132
+ "v=#{CGI.escape('1123')}" => 'v=1123',
133
+ "v=#{CGI.escape('*foo*')}" => 'v=*foo*',
134
+ "v=#{CGI.escape('*^%$#@!foo')}" => 'v=*^%$#@!foo',
135
+ "v=#{CGI.escape('~!@#$%^&*()_+-={}|[]\:";\'<>?,./`')}" => 'v=~!@#$%^&*()_+-={}|[]\:";\'<>?,./`',
136
+ "v=#{CGI.escape('_-+=\{}"?:><')},#{CGI.escape('another_value!')}" => 'v=[_-+=\{}"?:><,another_value!]',
137
+ }
138
+ end
139
+ end
140
+ end
@@ -24,8 +24,7 @@ describe Praxis::Extensions::FieldExpansion do
24
24
 
25
25
  let(:request_params) do
26
26
  double('params',
27
- fields: Praxis::Extensions::FieldSelection::FieldSelector.for(Person).load(fields),
28
- view: view
27
+ fields: Praxis::Extensions::FieldSelection::FieldSelector.for(Person).load(fields)
29
28
  )
30
29
  end
31
30
 
@@ -33,7 +32,6 @@ describe Praxis::Extensions::FieldExpansion do
33
32
  let(:media_type) { Person }
34
33
 
35
34
  let(:fields) { nil }
36
- let(:view) { nil }
37
35
 
38
36
  let(:test_attributes) { }
39
37
  let(:test_params) { double('test_params', attributes: test_attributes) }
@@ -43,43 +41,26 @@ describe Praxis::Extensions::FieldExpansion do
43
41
  context '#expanded_fields' do
44
42
 
45
43
  context 'with fields and view params defined' do
46
- let(:test_attributes) { {view: true, fields: true} }
44
+ let(:test_attributes) { {} }
47
45
 
48
- context 'and no fields provided' do
46
+ context 'with no fields provided' do
49
47
  it 'returns the fields for the default view' do
50
48
  expect(expansion).to eq({id: true, name: true})
51
49
  end
52
-
53
- pending 'and a view'
54
50
  end
55
51
 
56
52
  context 'with a set of fields provided' do
57
53
  let(:fields) { 'id,name,owner{name}' }
58
- it 'returns the subset of fields for the default view' do
54
+ it 'returns the subset of fields' do
59
55
  expected = {id: true, name: true }
60
56
  expect(expansion).to eq expected
61
57
  end
62
-
63
- pending 'and a view'
64
58
  end
65
59
  end
66
60
 
67
- context 'with only a view param defined' do
68
- let(:test_attributes) { {view: true} }
69
-
70
- it 'returns the fields for the default view' do
71
- expect(expansion).to eq({id: true, name: true})
72
- end
73
-
74
- pending 'and a view'
75
- end
76
-
77
-
78
61
  context 'with an action with no params' do
79
62
  let(:test_params) { nil }
80
- let(:fields){ nil }
81
- let(:view){ nil }
82
- it 'ignores incoming parameters and expands for the default view' do
63
+ it 'ignores incoming parameters and expands for the default fieldset' do
83
64
  expect(expansion).to eq({id: true, name: true})
84
65
  end
85
66
  end