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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +22 -0
  4. data/bin/praxis +6 -0
  5. data/lib/praxis/api_definition.rb +8 -4
  6. data/lib/praxis/collection.rb +11 -0
  7. data/lib/praxis/docs/open_api/response_object.rb +21 -6
  8. data/lib/praxis/extensions/attribute_filtering.rb +14 -1
  9. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +154 -63
  10. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
  11. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +46 -43
  12. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
  13. data/lib/praxis/mapper/resource.rb +2 -2
  14. data/lib/praxis/media_type_identifier.rb +11 -1
  15. data/lib/praxis/response_definition.rb +46 -66
  16. data/lib/praxis/responses/http.rb +3 -1
  17. data/lib/praxis/tasks/routes.rb +6 -6
  18. data/lib/praxis/version.rb +1 -1
  19. data/spec/praxis/action_definition_spec.rb +3 -1
  20. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +259 -172
  21. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
  22. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +117 -19
  23. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
  24. data/spec/praxis/mapper/resource_spec.rb +3 -3
  25. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  26. data/spec/praxis/response_definition_spec.rb +37 -129
  27. data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
  28. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +4 -0
  29. data/tasks/thor/templates/generator/example_app/config/environment.rb +1 -1
  30. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +2 -2
  31. 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', 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)
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=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
31
80
  end
32
81
  end
33
82
 
34
- context 'with an associated MediaType' do
35
- let(:params_for_post_media_type) do
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 = 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
@@ -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
- 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