praxis 2.0.pre.8 → 2.0.pre.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.ruby-version +1 -1
- data/.travis.yml +1 -3
- data/CHANGELOG.md +33 -0
- data/TODO.md +1 -4
- data/bin/praxis +67 -12
- data/lib/praxis.rb +10 -3
- data/lib/praxis/action_definition.rb +15 -13
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +0 -7
- data/lib/praxis/api_general_info.rb +1 -1
- data/lib/praxis/application.rb +6 -2
- data/lib/praxis/blueprint.rb +357 -0
- data/lib/praxis/bootloader.rb +9 -3
- data/lib/praxis/bootloader_stages/environment.rb +16 -13
- data/lib/praxis/collection.rb +1 -11
- data/lib/praxis/config_hash.rb +44 -0
- data/lib/praxis/docs/{openapi → open_api}/info_object.rb +18 -10
- data/lib/praxis/docs/{openapi → open_api}/media_type_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/operation_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/parameter_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/paths_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/request_body_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/response_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/responses_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/schema_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/server_object.rb +0 -0
- data/lib/praxis/docs/{openapi → open_api}/tag_object.rb +0 -0
- data/lib/praxis/docs/open_api_generator.rb +91 -6
- data/lib/praxis/endpoint_definition.rb +273 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +182 -58
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +47 -56
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +153 -0
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
- data/lib/praxis/extensions/field_expansion.rb +3 -36
- data/lib/praxis/extensions/pagination.rb +5 -32
- data/lib/praxis/extensions/pagination/ordering_params.rb +1 -1
- data/lib/praxis/extensions/pagination/pagination_params.rb +6 -4
- data/lib/praxis/field_expander.rb +90 -0
- data/lib/praxis/finalizable.rb +34 -0
- data/lib/praxis/mapper/active_model_compat.rb +4 -0
- data/lib/praxis/mapper/resource.rb +18 -2
- data/lib/praxis/mapper/selector_generator.rb +2 -1
- data/lib/praxis/mapper/sequel_compat.rb +7 -0
- data/lib/praxis/media_type.rb +3 -68
- data/lib/praxis/plugin_concern.rb +1 -1
- data/lib/praxis/plugins/mapper_plugin.rb +24 -15
- data/lib/praxis/plugins/pagination_plugin.rb +34 -4
- data/lib/praxis/renderer.rb +88 -0
- data/lib/praxis/request.rb +1 -1
- data/lib/praxis/resource_definition.rb +2 -311
- data/lib/praxis/response_definition.rb +2 -10
- data/lib/praxis/response_template.rb +3 -3
- data/lib/praxis/router.rb +2 -2
- data/lib/praxis/routing_config.rb +1 -1
- data/lib/praxis/tasks/api_docs.rb +17 -64
- data/lib/praxis/tasks/routes.rb +1 -1
- data/lib/praxis/types/media_type_common.rb +1 -11
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +0 -1
- data/spec/functional_spec.rb +5 -9
- data/spec/praxis/action_definition_spec.rb +12 -20
- data/spec/praxis/blueprint_spec.rb +373 -0
- data/spec/praxis/bootloader_spec.rb +10 -2
- data/spec/praxis/collection_spec.rb +0 -13
- data/spec/praxis/config_hash_spec.rb +64 -0
- data/spec/praxis/{resource_definition_spec.rb → endpoint_definition_spec.rb} +37 -64
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +249 -168
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +190 -8
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +140 -0
- data/spec/praxis/extensions/field_expansion_spec.rb +5 -24
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
- data/spec/praxis/field_expander_spec.rb +149 -0
- data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
- data/spec/praxis/media_type_identifier_spec.rb +5 -4
- data/spec/praxis/media_type_spec.rb +4 -93
- data/spec/praxis/renderer_spec.rb +188 -0
- data/spec/praxis/response_definition_spec.rb +0 -31
- data/spec/praxis/response_spec.rb +1 -1
- data/spec/praxis/router_spec.rb +8 -8
- data/spec/praxis/routing_config_spec.rb +3 -3
- data/spec/spec_app/app/controllers/instances.rb +13 -7
- data/spec/spec_app/design/media_types/instance.rb +1 -19
- data/spec/spec_app/design/media_types/volume.rb +1 -1
- data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -14
- data/spec/spec_app/design/resources/instances.rb +5 -8
- data/spec/spec_app/design/resources/volume_snapshots.rb +1 -1
- data/spec/spec_app/design/resources/volumes.rb +1 -1
- data/spec/support/spec_authorization_plugin.rb +1 -1
- data/spec/support/spec_blueprints.rb +72 -0
- data/spec/support/{spec_resource_definitions.rb → spec_endpoint_definitions.rb} +2 -2
- data/spec/support/spec_media_types.rb +6 -26
- data/tasks/thor/app.rb +8 -34
- data/tasks/thor/example.rb +51 -285
- data/tasks/thor/model.rb +40 -0
- data/tasks/thor/scaffold.rb +117 -0
- data/tasks/thor/templates/generator/empty_app/.gitignore +0 -1
- data/tasks/thor/templates/generator/empty_app/Gemfile +7 -23
- data/tasks/thor/templates/generator/empty_app/README.md +1 -1
- data/tasks/thor/templates/generator/empty_app/Rakefile +4 -13
- data/tasks/thor/templates/generator/empty_app/{design/response_templates → app/v1/resources}/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/{design/response_templates → app/v1/resources}/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/config/environment.rb +26 -17
- data/tasks/thor/templates/generator/empty_app/{design/v1/resources → config/initializers}/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/{design/v1/resources → config/initializers}/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/design/v1/endpoints/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/design/v1/endpoints/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/docs/.empty_directory +0 -0
- data/tasks/thor/templates/generator/empty_app/docs/.gitkeep +0 -0
- data/tasks/thor/templates/generator/empty_app/spec/spec_helper.rb +14 -9
- data/tasks/thor/templates/generator/example_app/.gitignore +1 -0
- data/tasks/thor/templates/generator/example_app/Gemfile +19 -0
- data/tasks/thor/templates/generator/example_app/Rakefile +61 -0
- data/tasks/thor/templates/generator/example_app/app/models/user.rb +6 -0
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
- data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +17 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +11 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +25 -0
- data/tasks/thor/templates/generator/example_app/config.ru +30 -0
- data/tasks/thor/templates/generator/example_app/config/environment.rb +41 -0
- data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +12 -0
- data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
- data/tasks/thor/templates/generator/example_app/design/api.rb +18 -0
- data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +37 -0
- data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +21 -0
- data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +20 -0
- data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +42 -0
- data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +37 -0
- data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
- data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
- data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
- data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
- data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
- metadata +64 -136
- data/lib/api_browser/.bowerrc +0 -3
- data/lib/api_browser/.editorconfig +0 -21
- data/lib/api_browser/Gruntfile.js +0 -581
- data/lib/api_browser/app/index.html +0 -59
- data/lib/api_browser/app/js/app.js +0 -48
- data/lib/api_browser/app/js/controllers/action.js +0 -47
- data/lib/api_browser/app/js/controllers/controller.js +0 -10
- data/lib/api_browser/app/js/controllers/menu.js +0 -93
- data/lib/api_browser/app/js/controllers/trait.js +0 -10
- data/lib/api_browser/app/js/controllers/type.js +0 -24
- data/lib/api_browser/app/js/directives/attribute_description.js +0 -56
- data/lib/api_browser/app/js/directives/attribute_table.js +0 -28
- data/lib/api_browser/app/js/directives/conditional_requirements.js +0 -13
- data/lib/api_browser/app/js/directives/fixed_if_fits.js +0 -38
- data/lib/api_browser/app/js/directives/highlight.js +0 -14
- data/lib/api_browser/app/js/directives/menu_item.js +0 -59
- data/lib/api_browser/app/js/directives/no_container.js +0 -8
- data/lib/api_browser/app/js/directives/readable_list.js +0 -87
- data/lib/api_browser/app/js/directives/request_examples.js +0 -31
- data/lib/api_browser/app/js/directives/type_placeholder.js +0 -30
- data/lib/api_browser/app/js/directives/url.js +0 -15
- data/lib/api_browser/app/js/factories/Configuration.js +0 -12
- data/lib/api_browser/app/js/factories/Documentation.js +0 -61
- data/lib/api_browser/app/js/factories/Example.js +0 -51
- data/lib/api_browser/app/js/factories/PageInfo.js +0 -9
- data/lib/api_browser/app/js/factories/normalize_attributes.js +0 -20
- data/lib/api_browser/app/js/factories/prepare_template.js +0 -15
- data/lib/api_browser/app/js/factories/template_for.js +0 -128
- data/lib/api_browser/app/js/filters/attribute_name.js +0 -10
- data/lib/api_browser/app/js/filters/friendly_json.js +0 -5
- data/lib/api_browser/app/js/filters/has_requirement.js +0 -14
- data/lib/api_browser/app/js/filters/header_info.js +0 -9
- data/lib/api_browser/app/js/filters/is_empty.js +0 -8
- data/lib/api_browser/app/js/filters/markdown.js +0 -6
- data/lib/api_browser/app/js/filters/resource_name.js +0 -5
- data/lib/api_browser/app/js/filters/tag_requirement.js +0 -13
- data/lib/api_browser/app/sass/modules/_body.scss +0 -40
- data/lib/api_browser/app/sass/modules/_cloke.scss +0 -8
- data/lib/api_browser/app/sass/modules/_header.scss +0 -10
- data/lib/api_browser/app/sass/modules/_nav.scss +0 -7
- data/lib/api_browser/app/sass/modules/_sidebar.scss +0 -134
- data/lib/api_browser/app/sass/modules/_switch.scss +0 -55
- data/lib/api_browser/app/sass/modules/_table.scss +0 -13
- data/lib/api_browser/app/sass/praxis.scss +0 -70
- data/lib/api_browser/app/sass/variables/_bootstrap-variables.scss +0 -774
- data/lib/api_browser/app/views/action.html +0 -97
- data/lib/api_browser/app/views/builtin/field-selector.html +0 -24
- data/lib/api_browser/app/views/controller.html +0 -55
- data/lib/api_browser/app/views/directives/attribute_description.html +0 -2
- data/lib/api_browser/app/views/directives/attribute_description/default.html +0 -2
- data/lib/api_browser/app/views/directives/attribute_description/example.html +0 -13
- data/lib/api_browser/app/views/directives/attribute_description/headers.html +0 -8
- data/lib/api_browser/app/views/directives/attribute_description/member_options.html +0 -4
- data/lib/api_browser/app/views/directives/attribute_description/values.html +0 -14
- data/lib/api_browser/app/views/directives/attribute_table.html +0 -17
- data/lib/api_browser/app/views/directives/menu_item.html +0 -8
- data/lib/api_browser/app/views/directives/url.html +0 -3
- data/lib/api_browser/app/views/examples/general.html +0 -26
- data/lib/api_browser/app/views/home.html +0 -5
- data/lib/api_browser/app/views/layout.html +0 -8
- data/lib/api_browser/app/views/menu.html +0 -42
- data/lib/api_browser/app/views/navbar.html +0 -9
- data/lib/api_browser/app/views/trait.html +0 -13
- data/lib/api_browser/app/views/type.html +0 -6
- data/lib/api_browser/app/views/type/details.html +0 -33
- data/lib/api_browser/app/views/types/embedded/array.html +0 -2
- data/lib/api_browser/app/views/types/embedded/default.html +0 -12
- data/lib/api_browser/app/views/types/embedded/field-selector.html +0 -13
- data/lib/api_browser/app/views/types/embedded/links.html +0 -11
- data/lib/api_browser/app/views/types/embedded/requirements.html +0 -6
- data/lib/api_browser/app/views/types/embedded/single_req.html +0 -9
- data/lib/api_browser/app/views/types/embedded/struct.html +0 -14
- data/lib/api_browser/app/views/types/label/link.html +0 -1
- data/lib/api_browser/app/views/types/label/primitive.html +0 -1
- data/lib/api_browser/app/views/types/label/primitive_collection.html +0 -1
- data/lib/api_browser/app/views/types/label/type.html +0 -1
- data/lib/api_browser/app/views/types/label/type_collection.html +0 -1
- data/lib/api_browser/app/views/types/main/array.html +0 -22
- data/lib/api_browser/app/views/types/main/default.html +0 -23
- data/lib/api_browser/app/views/types/main/hash.html +0 -23
- data/lib/api_browser/app/views/types/standalone/array.html +0 -3
- data/lib/api_browser/app/views/types/standalone/default.html +0 -18
- data/lib/api_browser/app/views/types/standalone/struct.html +0 -2
- data/lib/api_browser/bower_template.json +0 -41
- data/lib/api_browser/package-lock.json +0 -7110
- data/lib/api_browser/package.json +0 -43
- data/lib/praxis/docs/generator.rb +0 -243
- data/lib/praxis/docs/link_builder.rb +0 -30
- data/lib/praxis/links.rb +0 -135
- data/lib/praxis/types/multipart.rb +0 -109
- data/spec/api_browser/directives/type_placeholder_spec.js +0 -134
- data/spec/api_browser/factories/configuration_spec.js +0 -32
- data/spec/api_browser/factories/documentation_spec.js +0 -100
- data/spec/api_browser/factories/normalize_attributes_spec.js +0 -92
- data/spec/api_browser/factories/template_for_spec.js +0 -67
- data/spec/api_browser/filters/attribute_name_spec.js +0 -23
- data/spec/praxis/types/multipart_spec.rb +0 -112
- data/tasks/thor/templates/generator/empty_app/.rspec +0 -1
- data/tasks/thor/templates/generator/empty_app/Guardfile +0 -3
- data/tasks/thor/templates/generator/empty_app/config/rainbows.rb +0 -57
- data/tasks/thor/templates/generator/empty_app/docs/app.js +0 -1
- 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',
|
10
|
-
{name: 'one',
|
11
|
-
{name: 'rel1.a1',
|
12
|
-
{name: 'rel1.a2',
|
13
|
-
{name: 'rel1.rel2.b1',
|
14
|
-
{name: 'rel1.rel2.b2',
|
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::
|
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=
|
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: '
|
23
|
-
{ name: :two, op: '!=', value: '
|
24
|
-
{ name: :three, op: '>=', value: '
|
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) { {
|
44
|
+
let(:test_attributes) { {} }
|
47
45
|
|
48
|
-
context '
|
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
|
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
|
-
|
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
|