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.
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