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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +1 -3
  4. data/CHANGELOG.md +26 -0
  5. data/bin/praxis +65 -2
  6. data/lib/praxis/api_definition.rb +8 -4
  7. data/lib/praxis/bootloader_stages/environment.rb +1 -0
  8. data/lib/praxis/collection.rb +11 -0
  9. data/lib/praxis/docs/open_api/response_object.rb +21 -6
  10. data/lib/praxis/docs/open_api_generator.rb +1 -1
  11. data/lib/praxis/extensions/attribute_filtering.rb +14 -1
  12. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +206 -66
  13. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
  14. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +45 -41
  15. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
  16. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
  17. data/lib/praxis/extensions/pagination.rb +5 -32
  18. data/lib/praxis/mapper/active_model_compat.rb +4 -0
  19. data/lib/praxis/mapper/resource.rb +18 -2
  20. data/lib/praxis/mapper/selector_generator.rb +1 -0
  21. data/lib/praxis/mapper/sequel_compat.rb +7 -0
  22. data/lib/praxis/media_type_identifier.rb +11 -1
  23. data/lib/praxis/plugins/mapper_plugin.rb +22 -13
  24. data/lib/praxis/plugins/pagination_plugin.rb +34 -4
  25. data/lib/praxis/response_definition.rb +46 -66
  26. data/lib/praxis/responses/http.rb +3 -1
  27. data/lib/praxis/tasks/api_docs.rb +4 -1
  28. data/lib/praxis/tasks/routes.rb +6 -6
  29. data/lib/praxis/version.rb +1 -1
  30. data/spec/praxis/action_definition_spec.rb +3 -1
  31. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +267 -167
  32. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
  33. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +100 -17
  34. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
  35. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
  36. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
  37. data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
  38. data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
  39. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  40. data/spec/praxis/response_definition_spec.rb +37 -129
  41. data/tasks/thor/example.rb +12 -6
  42. data/tasks/thor/model.rb +40 -0
  43. data/tasks/thor/scaffold.rb +117 -0
  44. data/tasks/thor/templates/generator/empty_app/config/environment.rb +1 -0
  45. data/tasks/thor/templates/generator/example_app/Rakefile +9 -2
  46. data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
  47. data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
  48. data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +2 -2
  49. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +15 -0
  50. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +7 -28
  51. data/tasks/thor/templates/generator/example_app/config.ru +1 -2
  52. data/tasks/thor/templates/generator/example_app/config/environment.rb +3 -2
  53. data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +3 -2
  54. data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
  55. data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +4 -4
  56. data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +1 -6
  57. data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +4 -2
  58. data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +2 -2
  59. data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +2 -2
  60. data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
  61. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
  62. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
  63. data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
  64. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
  65. data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
  66. data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
  67. 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', 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,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::AVAILABLE_OPERATORS.each do |op|
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=1&two!=2&three>=3&four<=4&five<5&six>6&seven!&eight!!'}
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: '1'},
23
- { name: :two, op: '!=', value: '2'},
24
- { name: :three, op: '>=', value: '3'},
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 'with an associated MediaType' do
35
- let(:params_for_post_media_type) do
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 = params_for_post_media_type.load(str).parsed_array
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 = params_for_post_media_type.load(str).parsed_array
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 = params_for_post_media_type.load(str).parsed_array
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
- eq(described_class.new('application/vnd.icecream+json; cherry=true; nuts=false'))
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
- headers({ "X-Header" => "value", "Content-Type" => "application/some-type" })
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 "#validate_location!" do
276
- let(:block) { proc { status 200 } }
277
-
278
- context 'checking location mismatches' do
279
- before { response_definition.location(location) }
280
-
281
- context 'for Regexp' do
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
- end
302
-
303
- describe "#validate_headers!" do
304
- before { response_definition.headers(headers) }
305
- context 'checking headers are set' do
306
- context 'when there are missing headers' do
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
- context "when headers specs are name strings" do
316
- context "and is missing" do
317
- let (:headers) { [ "X-Just-Key" ] }
318
- it 'should raise error' do
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
- context "when header specs are hashes" do
336
- context "and is missing" do
337
- let (:headers) {
338
- [ { "X-Just-Key" => "notfoodbar" } ]
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
- context "when header specs are of mixed type " do
360
- context "and is missing" do
361
- let (:headers) {
362
- [ { "X-Header" => "value" }, "not-gonna-find-me" ]
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
- response.headers(headers) if headers
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(1).keys
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