praxis 2.0.pre.10 → 2.0.pre.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.travis.yml +1 -3
- data/CHANGELOG.md +26 -0
- data/bin/praxis +65 -2
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/bootloader_stages/environment.rb +1 -0
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/docs/open_api_generator.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering.rb +14 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +206 -66
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +45 -41
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
- data/lib/praxis/extensions/pagination.rb +5 -32
- 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 +1 -0
- data/lib/praxis/mapper/sequel_compat.rb +7 -0
- data/lib/praxis/media_type_identifier.rb +11 -1
- data/lib/praxis/plugins/mapper_plugin.rb +22 -13
- data/lib/praxis/plugins/pagination_plugin.rb +34 -4
- data/lib/praxis/response_definition.rb +46 -66
- data/lib/praxis/responses/http.rb +3 -1
- data/lib/praxis/tasks/api_docs.rb +4 -1
- data/lib/praxis/tasks/routes.rb +6 -6
- data/lib/praxis/version.rb +1 -1
- data/spec/praxis/action_definition_spec.rb +3 -1
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +267 -167
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +100 -17
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
- 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/mapper/selector_generator_spec.rb +1 -1
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/response_definition_spec.rb +37 -129
- data/tasks/thor/example.rb +12 -6
- data/tasks/thor/model.rb +40 -0
- data/tasks/thor/scaffold.rb +117 -0
- data/tasks/thor/templates/generator/empty_app/config/environment.rb +1 -0
- data/tasks/thor/templates/generator/example_app/Rakefile +9 -2
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
- data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +2 -2
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +15 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +7 -28
- data/tasks/thor/templates/generator/example_app/config.ru +1 -2
- data/tasks/thor/templates/generator/example_app/config/environment.rb +3 -2
- data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +3 -2
- data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
- data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +4 -4
- data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +1 -6
- data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +4 -2
- data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +2 -2
- data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +2 -2
- 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 +21 -6
@@ -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,46 +6,131 @@ 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
|
+
it 'does not handle badly escaped values that contain reserved chars ()|&,' do
|
34
|
+
badly_escaped = 'val('
|
35
|
+
str = "one=#{badly_escaped}&(two>normal|three!)"
|
36
|
+
expect{
|
37
|
+
described_class.load(str)
|
38
|
+
}.to raise_error(Parslet::ParseFailed)
|
39
|
+
end
|
40
|
+
end
|
9
41
|
context 'parses for operator' do
|
10
|
-
described_class::
|
42
|
+
described_class::VALUE_OPERATORS.each do |op|
|
11
43
|
it "#{op}" do
|
12
44
|
str = "thename#{op}thevalue"
|
13
45
|
parsed = [{ name: :thename, op: op, value: 'thevalue'}]
|
14
|
-
expect(described_class.load(str).parsed_array).to eq(parsed)
|
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
|
+
described_class::NOVALUE_OPERATORS.each do |op|
|
50
|
+
it "#{op}" do
|
51
|
+
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)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
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)
|
60
|
+
end
|
17
61
|
end
|
18
|
-
context 'with all operators at once' do
|
19
|
-
let(:filters_string) { 'one=
|
62
|
+
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!!'}
|
20
64
|
it do
|
21
|
-
expect(subject.parsed_array).to eq([
|
22
|
-
{ name: :one, op: '=', value: '
|
23
|
-
{ name: :two, op: '!=', value: '
|
24
|
-
{ name: :three, op: '>=', value: '
|
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'},
|
25
69
|
{ name: :four, op: '<=', value: '4'},
|
26
70
|
{ name: :five, op: '<', value: '5'},
|
27
71
|
{ name: :six, op: '>', value: '6'},
|
28
72
|
{ name: :seven, op: '!', value: nil},
|
29
73
|
{ name: :eight, op: '!!', value: nil},
|
30
74
|
])
|
75
|
+
# 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
|
+
expect(parent.size).to eq(1)
|
78
|
+
expect(parent.first.type).to eq(:and)
|
79
|
+
expect(parent.first.parent_group).to be_nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'with with nested precedence groups' do
|
84
|
+
let(:filters_string) { '(one=11)&(two!=22|three!!)&four<=4&five>5|six!'}
|
85
|
+
it do
|
86
|
+
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
|
+
])
|
95
|
+
# Grouped appropriately
|
96
|
+
parent_of = parsed.each_with_object({}) do |item, hash|
|
97
|
+
hash[item[:name]] = item[:node_object].parent_group
|
98
|
+
end
|
99
|
+
# This is the expected tree grouping result
|
100
|
+
# OR -- six
|
101
|
+
# |--- AND --five
|
102
|
+
# |--- four
|
103
|
+
# |--- OR -- three
|
104
|
+
# | |--- two
|
105
|
+
# |--- one
|
106
|
+
# two and 3 are grouped together by an OR
|
107
|
+
expect(parent_of[:two]).to be(parent_of[:three])
|
108
|
+
expect(parent_of[:two].type).to eq(:or)
|
109
|
+
|
110
|
+
# 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].type).to eq(:and)
|
113
|
+
|
114
|
+
# six and the whole group above are grouped together with an OR
|
115
|
+
expect(parent_of[:six]).to be(parent_of[:one].parent_group)
|
116
|
+
expect(parent_of[:six].type).to eq(:or)
|
31
117
|
end
|
32
118
|
end
|
33
119
|
|
34
|
-
context '
|
35
|
-
let(:
|
120
|
+
context 'value coercing when associated to a MediaType' do
|
121
|
+
let(:parsed) do
|
36
122
|
# Note wrap the filter_params (.for) type in an attribute (which then we discard), so it will
|
37
123
|
# construct it propertly by applying the block. Seems easier than creating the type alone, and
|
38
124
|
# then manually apply the block
|
39
125
|
Attributor::Attribute.new(described_class.for(Post)) do
|
40
126
|
filter 'id', using: ['=', '!=', '!']
|
41
|
-
end.type
|
127
|
+
end.type.load(str).parsed_array
|
42
128
|
end
|
43
129
|
|
44
130
|
context 'with a single value' do
|
45
131
|
let(:str) { 'id=1' }
|
46
132
|
it 'coerces its value to the associated mediatype attribute type' do
|
47
|
-
parsed
|
48
|
-
expect(parsed.first).to eq(:name=>:id, :op=>"=", :value=>1)
|
133
|
+
expect(parsed.first[:value]).to eq(1)
|
49
134
|
expect(Post.attributes[:id].type.valid_type?(parsed.first[:value])).to be_truthy
|
50
135
|
end
|
51
136
|
end
|
@@ -53,8 +138,7 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
|
|
53
138
|
context 'with multimatch' do
|
54
139
|
let(:str) { 'id=1,2,3' }
|
55
140
|
it 'coerces ALL csv values to the associated mediatype attribute type' do
|
56
|
-
parsed
|
57
|
-
expect(parsed.first).to eq(:name=>:id, :op=>"=", :value=>[1, 2, 3])
|
141
|
+
expect(parsed.first[:value]).to eq([1, 2, 3])
|
58
142
|
parsed.first[:value].each do |val|
|
59
143
|
expect(Post.attributes[:id].type.valid_type?(val)).to be_truthy
|
60
144
|
end
|
@@ -64,8 +148,7 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
|
|
64
148
|
context 'with a single value that is null' do
|
65
149
|
let(:str) { 'id!' }
|
66
150
|
it 'properly loads it as null' do
|
67
|
-
parsed
|
68
|
-
expect(parsed.first).to eq(:name=>:id, :op=>"!", :value=>nil)
|
151
|
+
expect(parsed.first[:value]).to be_nil
|
69
152
|
end
|
70
153
|
end
|
71
154
|
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'praxis/extensions/attribute_filtering/filters_parser'
|
2
|
+
|
3
|
+
describe Praxis::Extensions::AttributeFiltering::FilteringParams::Condition do
|
4
|
+
end
|
5
|
+
|
6
|
+
describe Praxis::Extensions::AttributeFiltering::FilteringParams::ConditionGroup do
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Praxis::Extensions::AttributeFiltering::FilteringParams::Parser do
|
10
|
+
|
11
|
+
context 'testing' do
|
12
|
+
let(:expectations) do
|
13
|
+
{
|
14
|
+
'one=11|two=22' => "( one=11 OR two=22 )"
|
15
|
+
}
|
16
|
+
end
|
17
|
+
it 'parses and loads the parsed result into the tree objects' do
|
18
|
+
expectations.each do |filters, dump_result|
|
19
|
+
|
20
|
+
parsed = described_class.new.parse(filters)
|
21
|
+
tree = Praxis::Extensions::AttributeFiltering::FilteringParams::ConditionGroup.load(parsed)
|
22
|
+
|
23
|
+
expect(tree.dump).to eq(dump_result)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
context 'parses the grammar' do
|
28
|
+
|
29
|
+
# Takes a hash with keys containing literal filters string, and values being the "dump format for Condition/Group"
|
30
|
+
shared_examples 'round-trip-properly' do |expectations|
|
31
|
+
it description do
|
32
|
+
expectations.each do |filters, dump_result|
|
33
|
+
parsed = Praxis::Extensions::AttributeFiltering::FilteringParams::Parser.new.parse(filters)
|
34
|
+
tree = Praxis::Extensions::AttributeFiltering::FilteringParams::ConditionGroup.load(parsed)
|
35
|
+
expect(tree.dump).to eq(dump_result)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'single expression' do
|
41
|
+
it_behaves_like 'round-trip-properly', {
|
42
|
+
'one=11' => 'one=11',
|
43
|
+
'(one=11)' => 'one=11',
|
44
|
+
'one!' => "one!",
|
45
|
+
}
|
46
|
+
end
|
47
|
+
context 'same expression operator' do
|
48
|
+
it_behaves_like 'round-trip-properly', {
|
49
|
+
'one=11&two=22' => '( one=11 AND two=22 )',
|
50
|
+
'one=11&two=22&three=3' => '( one=11 AND two=22 AND three=3 )',
|
51
|
+
'one=1,2,3&two=4,5' => '( one=[1,2,3] AND two=[4,5] )',
|
52
|
+
'one=11|two=22' => '( one=11 OR two=22 )',
|
53
|
+
'one=11|two=22|three=3' => '( one=11 OR two=22 OR three=3 )',
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'respects and/or precedence and parenthesis grouping' do
|
58
|
+
it_behaves_like 'round-trip-properly', {
|
59
|
+
'a=1&b=2&z=9|c=3' => '( ( a=1 AND b=2 AND z=9 ) OR c=3 )',
|
60
|
+
'a=1|b=2&c=3' => '( a=1 OR ( b=2 AND c=3 ) )',
|
61
|
+
'a=1|b=2&c=3&d=4' => '( a=1 OR ( b=2 AND c=3 AND d=4 ) )',
|
62
|
+
'(a=1|b=2)&c=3&d=4' => '( ( a=1 OR b=2 ) AND c=3 AND d=4 )',
|
63
|
+
'a=1|a.b.c_c=1&b=2' => '( a=1 OR ( a.b.c_c=1 AND b=2 ) )',
|
64
|
+
'a=1,2,3|b=4,5&c=one,two' => '( a=[1,2,3] OR ( b=[4,5] AND c=[one,two] ) )',
|
65
|
+
'one=11&two=2|three=3' => '( ( one=11 AND two=2 ) OR three=3 )', # AND has higer precedence
|
66
|
+
'one=11|two=2&three=3' => '( one=11 OR ( two=2 AND three=3 ) )', # AND has higer precedence
|
67
|
+
'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' =>
|
69
|
+
'( ( one=11 AND ( two!=2 OR three=3 ) AND four=4 AND five=5 ) OR six=6 )',
|
70
|
+
'(one=11)&three=3' => '( one=11 AND three=3 )',
|
71
|
+
'(one=11|two=2)&(three=3|four=4)' => '( ( one=11 OR two=2 ) AND ( three=3 OR four=4 ) )',
|
72
|
+
'(category_uuid=deadbeef1|category_uuid=deadbeef2)&(name=Book1|name=Book2)' =>
|
73
|
+
'( ( category_uuid=deadbeef1 OR category_uuid=deadbeef2 ) AND ( name=Book1 OR name=Book2 ) )',
|
74
|
+
'(category_uuid=deadbeef1&name=Book1)|(category_uuid=deadbeef2&name=Book2)' =>
|
75
|
+
'( ( category_uuid=deadbeef1 AND name=Book1 ) OR ( category_uuid=deadbeef2 AND name=Book2 ) )',
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'empty values get converted to empty strings' do
|
80
|
+
it_behaves_like 'round-trip-properly', {
|
81
|
+
'one=' => 'one=""',
|
82
|
+
'one=&two=2' => '( one="" AND two=2 )',
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'no value operands' do
|
87
|
+
it_behaves_like 'round-trip-properly', {
|
88
|
+
'one!' => "one!",
|
89
|
+
'one!!' => "one!!"
|
90
|
+
}
|
91
|
+
|
92
|
+
it 'fails if passing a value' do
|
93
|
+
expect {
|
94
|
+
described_class.new.parse('one!val')
|
95
|
+
}.to raise_error(Parslet::ParseFailed)
|
96
|
+
expect {
|
97
|
+
described_class.new.parse('one!!val')
|
98
|
+
}.to raise_error(Parslet::ParseFailed)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
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]"
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
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',
|
114
|
+
'aFew42Things=1' => 'aFew42Things=1',
|
115
|
+
'under_scores=1' => 'under_scores=1',
|
116
|
+
'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',
|
118
|
+
}
|
119
|
+
end
|
120
|
+
context 'supports everything (except &|(),) for values (even without encoding..not allowed, but just to ensure the parser does not bomb)' do
|
121
|
+
it_behaves_like 'round-trip-properly', {
|
122
|
+
'v=1123' => 'v=1123',
|
123
|
+
'v=*foo*' => 'v={*}foo{*}',
|
124
|
+
'v=*^%$#@!foo' => 'v={*}^%$#@!foo',
|
125
|
+
'v=_-=\{}"?:><' => 'v=_-=\{}"?:><',
|
126
|
+
'v=_-=\{}"?:><,another_value!' => 'v=[_-=\{}"?:><,another_value!]',
|
127
|
+
}
|
128
|
+
end
|
129
|
+
context 'properly detects and handles fuzzy matching encoded as {*} in the dump' do
|
130
|
+
it_behaves_like 'round-trip-properly', {
|
131
|
+
'v=*foo' => 'v={*}foo',
|
132
|
+
'v=*foo*' => 'v={*}foo{*}',
|
133
|
+
'v=foo*' => 'v=foo{*}',
|
134
|
+
'v=*start,end*,*both*' => 'v=[{*}start,end{*},{*}both{*}]',
|
135
|
+
"v=*#{CGI.escape('***')},#{CGI.escape('*')}" => 'v=[{*}***,*]', # Simple exact match on 2nd
|
136
|
+
}
|
137
|
+
end
|
138
|
+
context 'properly handles url-encoded values' do
|
139
|
+
it_behaves_like 'round-trip-properly', {
|
140
|
+
"v=#{CGI.escape('1123')}" => 'v=1123',
|
141
|
+
"v=*#{CGI.escape('foo')}*" => 'v={*}foo{*}',
|
142
|
+
"v=*#{CGI.escape('^%$#@!foo')}" => 'v={*}^%$#@!foo',
|
143
|
+
"v=#{CGI.escape('~!@#$%^&*()_+-={}|[]\:";\'<>?,./`')}" => 'v=~!@#$%^&*()_+-={}|[]\:";\'<>?,./`',
|
144
|
+
"v=#{CGI.escape('_-+=\{}"?:><')},#{CGI.escape('another_value!')}" => 'v=[_-+=\{}"?:><,another_value!]',
|
145
|
+
}
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -29,7 +29,7 @@ describe Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector do
|
|
29
29
|
:id # We always load the primary keys
|
30
30
|
]
|
31
31
|
end
|
32
|
-
let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(ActiveBookResource,selector_fields) }
|
32
|
+
let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(ActiveBookResource,selector_fields).selectors }
|
33
33
|
let(:debug){ false }
|
34
34
|
|
35
35
|
subject(:selector) {described_class.new(query: query, selectors: selector_node, debug: debug) }
|
@@ -64,7 +64,7 @@ describe Praxis::Extensions::FieldSelection::SequelQuerySelector do
|
|
64
64
|
]
|
65
65
|
end
|
66
66
|
|
67
|
-
let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(SequelSimpleResource,selector_fields) }
|
67
|
+
let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(SequelSimpleResource,selector_fields).selectors }
|
68
68
|
subject {described_class.new(query: query, selectors: selector_node, debug: debug) }
|
69
69
|
|
70
70
|
context 'generate' do
|
@@ -103,7 +103,7 @@ class ActiveBookResource < ActiveBaseResource
|
|
103
103
|
|
104
104
|
filters_mapping(
|
105
105
|
id: :id,
|
106
|
-
category_uuid: :category_uuid
|
106
|
+
# category_uuid: :category_uuid #NOTE: we do not need to define same-name-mappings if they exist
|
107
107
|
'fake_nested.name': 'simple_name',
|
108
108
|
'name': 'simple_name',
|
109
109
|
'name_is_not': lambda do |spec| # Silly way to use a proc, but good enough for testing
|
@@ -8,7 +8,7 @@ describe Praxis::Mapper::SelectorGenerator do
|
|
8
8
|
context '#add' do
|
9
9
|
let(:resource) { SimpleResource }
|
10
10
|
shared_examples 'a proper selector' do
|
11
|
-
it { expect(generator.add(resource, fields).dump).to be_deep_equal selectors }
|
11
|
+
it { expect(generator.add(resource, fields).selectors.dump).to be_deep_equal selectors }
|
12
12
|
end
|
13
13
|
|
14
14
|
context 'basic combos' do
|
@@ -218,7 +218,21 @@ describe Praxis::MediaTypeIdentifier do
|
|
218
218
|
|
219
219
|
it 'replaces suffix and parameters and adds new ones' do
|
220
220
|
expect(complex_subject + 'json; nuts=false; cherry=true').to \
|
221
|
-
|
221
|
+
eq(described_class.new('application/vnd.icecream+json; cherry=true; nuts=false'))
|
222
|
+
end
|
223
|
+
|
224
|
+
context 'does not add json for an already json identifier' do
|
225
|
+
it 'non-parameterized mediatypes simply ignore adding the suffix' do
|
226
|
+
plain_application_json = described_class.new('application/json')
|
227
|
+
|
228
|
+
expect(plain_application_json + 'json').to \
|
229
|
+
eq(plain_application_json)
|
230
|
+
end
|
231
|
+
it 'parameterized mediatypes still keeps them' do
|
232
|
+
parameterized_application_json = described_class.new('application/json; cherry=true; nuts=false')
|
233
|
+
expect(parameterized_application_json + 'json').to \
|
234
|
+
eq(parameterized_application_json)
|
235
|
+
end
|
222
236
|
end
|
223
237
|
end
|
224
238
|
end
|
@@ -8,7 +8,8 @@ describe Praxis::ResponseDefinition do
|
|
8
8
|
Proc.new do
|
9
9
|
status 200
|
10
10
|
description 'test description'
|
11
|
-
|
11
|
+
header( "X-Header", "value", description: 'Very nais header')
|
12
|
+
header( "Content-Type", "application/some-type" )
|
12
13
|
end
|
13
14
|
end
|
14
15
|
|
@@ -17,7 +18,7 @@ describe Praxis::ResponseDefinition do
|
|
17
18
|
its(:parts) { should be(nil) }
|
18
19
|
let(:response_status) { 200 }
|
19
20
|
let(:response_content_type) { "application/some-type" }
|
20
|
-
let(:response_headers) { { "X-Header" => "value", "Content-Type" => response_content_type} }
|
21
|
+
let(:response_headers) { { "X-Header" => "value", "Content-Type" => response_content_type, "Location" => '/somewhere/over/the/rainbow'} }
|
21
22
|
|
22
23
|
let(:response) { instance_double("Praxis::Response", status: response_status , headers: response_headers, content_type: response_content_type ) }
|
23
24
|
|
@@ -105,29 +106,6 @@ describe Praxis::ResponseDefinition do
|
|
105
106
|
end
|
106
107
|
end
|
107
108
|
|
108
|
-
context '#headers' do
|
109
|
-
it 'accepts a Hash' do
|
110
|
-
response_definition.headers Hash["X-Header" => "value", "Content-Type" => "application/some-type"]
|
111
|
-
expect(response_definition.headers).to be_a(Hash)
|
112
|
-
end
|
113
|
-
|
114
|
-
it 'accepts an Array' do
|
115
|
-
response_definition.headers ["X-Header: value", "Content-Type: application/some-type"]
|
116
|
-
expect(response_definition.headers).to be_a(Hash)
|
117
|
-
expect(response_definition.headers.keys).to include("X-Header: value", "Content-Type: application/some-type")
|
118
|
-
end
|
119
|
-
|
120
|
-
it 'accepts a String' do
|
121
|
-
response_definition.headers "X-Header: value"
|
122
|
-
expect(response_definition.headers).to be_a(Hash)
|
123
|
-
expect(response_definition.headers.keys).to include("X-Header: value")
|
124
|
-
end
|
125
|
-
|
126
|
-
it 'should return an error when headers are not a Hash, Array or String object' do
|
127
|
-
expect{ response_definition.headers Object.new }. to raise_error(Praxis::Exceptions::InvalidConfiguration)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
109
|
context '#parts' do
|
132
110
|
context 'with a :like argument (and no block)' do
|
133
111
|
before do
|
@@ -242,7 +220,6 @@ describe Praxis::ResponseDefinition do
|
|
242
220
|
|
243
221
|
it "calls all the validation sub-functions" do
|
244
222
|
expect(response_definition).to receive(:validate_status!).once
|
245
|
-
expect(response_definition).to receive(:validate_location!).once
|
246
223
|
expect(response_definition).to receive(:validate_headers!).once
|
247
224
|
expect(response_definition).to receive(:validate_content_type!).once
|
248
225
|
response_definition.validate(response)
|
@@ -272,112 +249,39 @@ describe Praxis::ResponseDefinition do
|
|
272
249
|
|
273
250
|
end
|
274
251
|
|
275
|
-
describe "#
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
let(:location) { /no_match/ }
|
283
|
-
|
284
|
-
it 'should raise an error' do
|
285
|
-
expect {
|
286
|
-
response_definition.validate_location!(response)
|
287
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
288
|
-
end
|
289
|
-
end
|
290
|
-
|
291
|
-
context 'for String' do
|
292
|
-
let(:location) { "no_match" }
|
293
|
-
it 'should raise error' do
|
294
|
-
expect {
|
295
|
-
response_definition.validate_location!(response)
|
296
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
297
|
-
end
|
252
|
+
describe "#validate_headers!" do
|
253
|
+
context 'when there are missing headers' do
|
254
|
+
it 'should raise error' do
|
255
|
+
response_definition.header('X-Unknown', 'test')
|
256
|
+
expect {
|
257
|
+
response_definition.validate_headers!(response)
|
258
|
+
}.to raise_error(Praxis::Exceptions::Validation)
|
298
259
|
end
|
299
|
-
|
300
260
|
end
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
let (:headers) { { 'X-some' => 'test' } }
|
308
|
-
it 'should raise error' do
|
309
|
-
expect {
|
310
|
-
response_definition.validate_headers!(response)
|
311
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
312
|
-
end
|
261
|
+
context 'when headers with same names are returned' do
|
262
|
+
it 'a simply required header should not raise error just by being there' do
|
263
|
+
response_definition.header('X-Header', nil)
|
264
|
+
expect {
|
265
|
+
response_definition.validate_headers!(response)
|
266
|
+
}.to_not raise_error
|
313
267
|
end
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
expect {
|
320
|
-
response_definition.validate_headers!(response)
|
321
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
|
-
context "and is not missing" do
|
326
|
-
let (:headers) { [ "X-Header" ] }
|
327
|
-
it 'should not raise error' do
|
328
|
-
expect {
|
329
|
-
response_definition.validate_headers!(response)
|
330
|
-
}.not_to raise_error
|
331
|
-
end
|
332
|
-
end
|
268
|
+
it 'an exact string header should not raise error if it fully matches' do
|
269
|
+
response_definition.header('X-Header', 'value')
|
270
|
+
expect {
|
271
|
+
response_definition.validate_headers!(response)
|
272
|
+
}.to_not raise_error
|
333
273
|
end
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
}
|
340
|
-
it 'should raise error' do
|
341
|
-
expect {
|
342
|
-
response_definition.validate_headers!(response)
|
343
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
context "and is not missing" do
|
348
|
-
let (:headers) {
|
349
|
-
[ { "X-Header" => "value" } ]
|
350
|
-
}
|
351
|
-
it 'should not raise error' do
|
352
|
-
expect {
|
353
|
-
response_definition.validate_headers!(response)
|
354
|
-
}.not_to raise_error
|
355
|
-
end
|
356
|
-
end
|
274
|
+
it 'a regexp header should not raise error if it matches the regexp' do
|
275
|
+
response_definition.header('X-Header', /value/)
|
276
|
+
expect {
|
277
|
+
response_definition.validate_headers!(response)
|
278
|
+
}.to_not raise_error
|
357
279
|
end
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
}
|
364
|
-
it 'should raise error' do
|
365
|
-
expect {
|
366
|
-
response_definition.validate_headers!(response)
|
367
|
-
}.to raise_error(Praxis::Exceptions::Validation)
|
368
|
-
end
|
369
|
-
end
|
370
|
-
|
371
|
-
context "and is not missing" do
|
372
|
-
let (:headers) {
|
373
|
-
[ { "X-Header" => "value" }, "Content-Type" ]
|
374
|
-
}
|
375
|
-
it 'should not raise error' do
|
376
|
-
expect {
|
377
|
-
response_definition.validate_headers!(response)
|
378
|
-
}.not_to raise_error
|
379
|
-
end
|
380
|
-
end
|
280
|
+
it 'a regexp header should raise error if it does not match the regexp' do
|
281
|
+
response_definition.header('X-Header', /anotherthing/)
|
282
|
+
expect {
|
283
|
+
response_definition.validate_headers!(response)
|
284
|
+
}.to raise_error(Praxis::Exceptions::Validation)
|
381
285
|
end
|
382
286
|
end
|
383
287
|
end
|
@@ -478,7 +382,10 @@ describe Praxis::ResponseDefinition do
|
|
478
382
|
if parts || parts_block
|
479
383
|
parts ? response.parts(nil, **parts, &parts_block) : response.parts(nil, &parts_block)
|
480
384
|
end
|
481
|
-
|
385
|
+
|
386
|
+
headers&.each do |(name, value)|
|
387
|
+
response.header(name, value)
|
388
|
+
end
|
482
389
|
end
|
483
390
|
|
484
391
|
context 'for a definition with a media type' do
|
@@ -520,8 +427,9 @@ describe Praxis::ResponseDefinition do
|
|
520
427
|
its([:location]){ should == {value: location.inspect ,type: :regexp} }
|
521
428
|
|
522
429
|
it 'should have a header defined with value and type keys' do
|
523
|
-
expect( output[:headers] ).to have(
|
430
|
+
expect( output[:headers] ).to have(2).keys
|
524
431
|
expect( output[:headers]['Header1'] ).to eq({value: 'Value1' ,type: :string })
|
432
|
+
expect( output[:headers]['Location'] ).to eq({value: "/\\/my\\/url\\//" ,type: :regexp })
|
525
433
|
end
|
526
434
|
end
|
527
435
|
|