praxis 2.0.pre.10 → 2.0.pre.15
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.
- 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
|
|