praxis 2.0.pre.11 → 2.0.pre.16
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/CHANGELOG.md +22 -0
- data/bin/praxis +6 -0
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/extensions/attribute_filtering.rb +14 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +154 -63
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +46 -43
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
- data/lib/praxis/mapper/resource.rb +2 -2
- data/lib/praxis/media_type_identifier.rb +11 -1
- data/lib/praxis/response_definition.rb +46 -66
- data/lib/praxis/responses/http.rb +3 -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 +259 -172
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +117 -19
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
- data/spec/praxis/mapper/resource_spec.rb +3 -3
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/response_definition_spec.rb +37 -129
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +4 -0
- data/tasks/thor/templates/generator/example_app/config/environment.rb +1 -1
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +2 -2
- metadata +9 -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)
|
47
|
+
end
|
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)
|
15
54
|
end
|
16
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
|
31
80
|
end
|
32
81
|
end
|
33
82
|
|
34
|
-
context 'with
|
35
|
-
let(:
|
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)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
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
|
@@ -78,9 +161,9 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
|
|
78
161
|
# construct it propertly by applying the block. Seems easier than creating the type alone, and
|
79
162
|
# then manually apply the block
|
80
163
|
Attributor::Attribute.new(described_class.for(Post)) do
|
81
|
-
filter 'id', using: ['=', '!=']
|
164
|
+
filter 'id', using: ['=', '!=', '!']
|
82
165
|
filter 'title', using: ['=', '!='], fuzzy: true
|
83
|
-
filter 'content', using: ['=', '!=']
|
166
|
+
filter 'content', using: ['=', '!=', '!']
|
84
167
|
end.type
|
85
168
|
end
|
86
169
|
let(:loaded_params) { filtering_params_type.load(filters_string) }
|
@@ -111,6 +194,21 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
|
|
111
194
|
end
|
112
195
|
end
|
113
196
|
end
|
197
|
+
|
198
|
+
context 'non-valued operators' do
|
199
|
+
context 'for string typed fields' do
|
200
|
+
let(:filters_string) { 'content!'}
|
201
|
+
it 'validates properly' do
|
202
|
+
expect(subject).to be_empty
|
203
|
+
end
|
204
|
+
end
|
205
|
+
context 'for non-string typed fields' do
|
206
|
+
let(:filters_string) { 'id!'}
|
207
|
+
it 'validates properly' do
|
208
|
+
expect(subject).to be_empty
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
114
212
|
context 'fuzzy matches' do
|
115
213
|
context 'when allowed' do
|
116
214
|
context 'given a fuzzy string' do
|
@@ -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
|
@@ -14,15 +14,15 @@ describe Praxis::Mapper::Resource do
|
|
14
14
|
subject(:properties) { resource.properties }
|
15
15
|
|
16
16
|
it 'includes directly-set properties' do
|
17
|
-
expect(properties[:other_resource]).to eq(dependencies: [:other_model])
|
17
|
+
expect(properties[:other_resource]).to eq(dependencies: [:other_model], through: nil)
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'inherits from a superclass' do
|
21
|
-
expect(properties[:href]).to eq(dependencies: [:id])
|
21
|
+
expect(properties[:href]).to eq(dependencies: [:id], through: nil)
|
22
22
|
end
|
23
23
|
|
24
24
|
it 'properly overrides a property from the parent' do
|
25
|
-
expect(properties[:name]).to eq(dependencies: [:simple_name])
|
25
|
+
expect(properties[:name]).to eq(dependencies: [:simple_name], through: nil)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
@@ -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
|