praxis 2.0.pre.11 → 2.0.pre.12

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.
@@ -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,46 +6,109 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
6
6
 
7
7
  context '.load' do
8
8
  subject { described_class.load(filters_string) }
9
+
10
+ it 'unescapes the URL encoded string' do
11
+ str = CGI.escape("one=fun!,Times,3&two>*|three<^%$#!st_uff")
12
+ parsed = [
13
+ { name: :one, op: '=', value: ["fun!","Times","3"]},
14
+ { name: :two, op: '>', value: "*"},
15
+ { name: :three, op: '<', value: "^%$#!st_uff"},
16
+ ]
17
+ expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
18
+ end
9
19
  context 'parses for operator' do
10
- described_class::AVAILABLE_OPERATORS.each do |op|
20
+ described_class::VALUE_OPERATORS.each do |op|
11
21
  it "#{op}" do
12
22
  str = "thename#{op}thevalue"
13
23
  parsed = [{ name: :thename, op: op, value: 'thevalue'}]
14
- expect(described_class.load(str).parsed_array).to eq(parsed)
24
+ expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
15
25
  end
16
26
  end
27
+ described_class::NOVALUE_OPERATORS.each do |op|
28
+ it "#{op}" do
29
+ str = "thename#{op}"
30
+ parsed = [{ name: :thename, op: op, value: nil}]
31
+ expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
32
+ end
33
+ end
34
+ it 'can parse multiple values for filter' do
35
+ str="filtername=1,2,3"
36
+ parsed = [{ name: :filtername, op: '=', value: ["1","2","3"]}]
37
+ expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
38
+ end
17
39
  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!!'}
40
+ context 'with all value operators at once for the same AND group' do
41
+ let(:filters_string) { 'one=11&two!=22&three>=33&four<=4&five<5&six>6&seven!&eight!!'}
20
42
  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'},
43
+ expect(subject.parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq([
44
+ { name: :one, op: '=', value: '11'},
45
+ { name: :two, op: '!=', value: '22'},
46
+ { name: :three, op: '>=', value: '33'},
25
47
  { name: :four, op: '<=', value: '4'},
26
48
  { name: :five, op: '<', value: '5'},
27
49
  { name: :six, op: '>', value: '6'},
28
50
  { name: :seven, op: '!', value: nil},
29
51
  { name: :eight, op: '!!', value: nil},
30
52
  ])
53
+ # And all have the same parent, which is an AND group
54
+ parent = subject.parsed_array.map{|i|i[:node_object].parent_group}.uniq
55
+ expect(parent.size).to eq(1)
56
+ expect(parent.first.type).to eq(:and)
57
+ expect(parent.first.parent_group).to be_nil
58
+ end
59
+ end
60
+
61
+ context 'with with nested precedence groups' do
62
+ let(:filters_string) { '(one=11)&(two!=22|three!!)&four<=4&five>5|six!'}
63
+ it do
64
+ parsed = subject.parsed_array
65
+ expect(parsed.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: nil},
69
+ { name: :four, op: '<=', value: '4'},
70
+ { name: :five, op: '>', value: '5'},
71
+ { name: :six, op: '!', value: nil},
72
+ ])
73
+ # Grouped appropriately
74
+ parent_of = parsed.each_with_object({}) do |item, hash|
75
+ hash[item[:name]] = item[:node_object].parent_group
76
+ end
77
+ # This is the expected tree grouping result
78
+ # OR -- six
79
+ # |--- AND --five
80
+ # |--- four
81
+ # |--- OR -- three
82
+ # | |--- two
83
+ # |--- one
84
+ # two and 3 are grouped together by an OR
85
+ expect(parent_of[:two]).to be(parent_of[:three])
86
+ expect(parent_of[:two].type).to eq(:or)
87
+
88
+ # one, two, four and the or from two/three are grouped together by an AND
89
+ expect([parent_of[:one],parent_of[:two].parent_group,parent_of[:four],parent_of[:five]]).to all(be(parent_of[:one]))
90
+ expect(parent_of[:one].type).to eq(:and)
91
+
92
+ # six and the whole group above are grouped together with an OR
93
+ expect(parent_of[:six]).to be(parent_of[:one].parent_group)
94
+ expect(parent_of[:six].type).to eq(:or)
31
95
  end
32
96
  end
33
97
 
34
- context 'with an associated MediaType' do
35
- let(:params_for_post_media_type) do
98
+ context 'value coercing when associated to a MediaType' do
99
+ let(:parsed) do
36
100
  # Note wrap the filter_params (.for) type in an attribute (which then we discard), so it will
37
101
  # construct it propertly by applying the block. Seems easier than creating the type alone, and
38
102
  # then manually apply the block
39
103
  Attributor::Attribute.new(described_class.for(Post)) do
40
104
  filter 'id', using: ['=', '!=', '!']
41
- end.type
105
+ end.type.load(str).parsed_array
42
106
  end
43
107
 
44
108
  context 'with a single value' do
45
109
  let(:str) { 'id=1' }
46
110
  it 'coerces its value to the associated mediatype attribute type' do
47
- parsed = params_for_post_media_type.load(str).parsed_array
48
- expect(parsed.first).to eq(:name=>:id, :op=>"=", :value=>1)
111
+ expect(parsed.first[:value]).to eq(1)
49
112
  expect(Post.attributes[:id].type.valid_type?(parsed.first[:value])).to be_truthy
50
113
  end
51
114
  end
@@ -53,8 +116,7 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
53
116
  context 'with multimatch' do
54
117
  let(:str) { 'id=1,2,3' }
55
118
  it 'coerces ALL csv values to the associated mediatype attribute type' do
56
- parsed = params_for_post_media_type.load(str).parsed_array
57
- expect(parsed.first).to eq(:name=>:id, :op=>"=", :value=>[1, 2, 3])
119
+ expect(parsed.first[:value]).to eq([1, 2, 3])
58
120
  parsed.first[:value].each do |val|
59
121
  expect(Post.attributes[:id].type.valid_type?(val)).to be_truthy
60
122
  end
@@ -64,8 +126,7 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
64
126
  context 'with a single value that is null' do
65
127
  let(:str) { 'id!' }
66
128
  it 'properly loads it as null' do
67
- parsed = params_for_post_media_type.load(str).parsed_array
68
- expect(parsed.first).to eq(:name=>:id, :op=>"!", :value=>nil)
129
+ expect(parsed.first[:value]).to be_nil
69
130
  end
70
131
  end
71
132
  end
@@ -0,0 +1,131 @@
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' 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
+ end
131
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: praxis
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.pre.11
4
+ version: 2.0.pre.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep M. Blanquer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-02-05 00:00:00.000000000 Z
12
+ date: 2021-02-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -446,6 +446,7 @@ files:
446
446
  - lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb
447
447
  - lib/praxis/extensions/attribute_filtering/filter_tree_node.rb
448
448
  - lib/praxis/extensions/attribute_filtering/filtering_params.rb
449
+ - lib/praxis/extensions/attribute_filtering/filters_parser.rb
449
450
  - lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb
450
451
  - lib/praxis/extensions/field_expansion.rb
451
452
  - lib/praxis/extensions/field_selection.rb
@@ -540,6 +541,7 @@ files:
540
541
  - spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb
541
542
  - spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb
542
543
  - spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb
544
+ - spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb
543
545
  - spec/praxis/extensions/field_expansion_spec.rb
544
546
  - spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb
545
547
  - spec/praxis/extensions/field_selection/field_selector_spec.rb