yaks 0.3.1 → 0.4.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +11 -0
  4. data/Gemfile +0 -2
  5. data/LICENSE +7 -0
  6. data/README.md +160 -35
  7. data/Rakefile +2 -1
  8. data/lib/yaks/collection_mapper.rb +25 -18
  9. data/lib/yaks/collection_resource.rb +11 -17
  10. data/lib/yaks/config.rb +96 -0
  11. data/lib/yaks/default_policy.rb +34 -4
  12. data/lib/yaks/fp.rb +18 -0
  13. data/lib/yaks/mapper/association.rb +19 -27
  14. data/lib/yaks/mapper/class_methods.rb +4 -2
  15. data/lib/yaks/mapper/config.rb +24 -39
  16. data/lib/yaks/mapper/has_many.rb +7 -6
  17. data/lib/yaks/mapper/has_one.rb +4 -3
  18. data/lib/yaks/mapper/link.rb +52 -55
  19. data/lib/yaks/mapper.rb +38 -26
  20. data/lib/yaks/null_resource.rb +3 -3
  21. data/lib/yaks/primitivize.rb +29 -27
  22. data/lib/yaks/resource/link.rb +4 -0
  23. data/lib/yaks/resource.rb +18 -7
  24. data/lib/yaks/serializer/collection_json.rb +38 -0
  25. data/lib/yaks/serializer/hal.rb +55 -0
  26. data/lib/yaks/serializer/json_api.rb +61 -0
  27. data/lib/yaks/serializer.rb +25 -4
  28. data/lib/yaks/util.rb +2 -42
  29. data/lib/yaks/version.rb +1 -1
  30. data/lib/yaks.rb +10 -32
  31. data/notes.org +72 -0
  32. data/shaved_yak.gif +0 -0
  33. data/spec/acceptance/acceptance_spec.rb +46 -0
  34. data/spec/acceptance/models.rb +28 -0
  35. data/spec/integration/map_to_resource_spec.rb +11 -15
  36. data/spec/json/confucius.hal.json +23 -0
  37. data/spec/json/confucius.json_api.json +22 -0
  38. data/spec/json/john.hal.json +29 -0
  39. data/spec/json/youtypeitwepostit.collection.json +45 -0
  40. data/spec/spec_helper.rb +12 -1
  41. data/spec/support/shared_contexts.rb +7 -10
  42. data/spec/support/youtypeit_models_mappers.rb +20 -0
  43. data/spec/unit/yaks/collection_mapper_spec.rb +84 -0
  44. data/spec/unit/yaks/collection_resource_spec.rb +72 -0
  45. data/spec/unit/yaks/config_spec.rb +129 -0
  46. data/spec/unit/yaks/fp_spec.rb +31 -0
  47. data/spec/unit/yaks/mapper/association_spec.rb +80 -0
  48. data/spec/{yaks → unit/yaks}/mapper/class_methods_spec.rb +4 -4
  49. data/spec/unit/yaks/mapper/config_spec.rb +191 -0
  50. data/spec/unit/yaks/mapper/has_many_spec.rb +46 -0
  51. data/spec/unit/yaks/mapper/has_one_spec.rb +34 -0
  52. data/spec/unit/yaks/mapper/link_spec.rb +152 -0
  53. data/spec/unit/yaks/mapper_spec.rb +177 -0
  54. data/spec/unit/yaks/resource_spec.rb +40 -0
  55. data/spec/{yaks/hal_serializer_spec.rb → unit/yaks/serializer/hal_spec.rb} +2 -2
  56. data/spec/unit/yaks/serializer_spec.rb +12 -0
  57. data/spec/unit/yaks/util_spec.rb +43 -0
  58. data/spec/yaml/confucius.yaml +10 -0
  59. data/spec/yaml/youtypeitwepostit.yaml +9 -0
  60. data/yaks.gemspec +7 -8
  61. metadata +92 -53
  62. data/Gemfile.lock +0 -111
  63. data/lib/yaks/hal_serializer.rb +0 -59
  64. data/lib/yaks/json_api_serializer.rb +0 -59
  65. data/lib/yaks/link_lookup.rb +0 -23
  66. data/lib/yaks/mapper/lookup.rb +0 -19
  67. data/lib/yaks/mapper/map_links.rb +0 -17
  68. data/lib/yaks/profile_registry.rb +0 -60
  69. data/lib/yaks/rel_registry.rb +0 -20
  70. data/lib/yaks/shared_options.rb +0 -15
  71. data/spec/support/shorthands.rb +0 -22
  72. data/spec/yaks/collection_resource_spec.rb +0 -9
  73. data/spec/yaks/mapper/association_spec.rb +0 -21
  74. data/spec/yaks/mapper/config_spec.rb +0 -77
  75. data/spec/yaks/mapper/has_one_spec.rb +0 -16
  76. data/spec/yaks/mapper/link_spec.rb +0 -38
  77. data/spec/yaks/mapper/map_links_spec.rb +0 -46
  78. data/spec/yaks/mapper_spec.rb +0 -65
  79. data/spec/yaks/resource_spec.rb +0 -23
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Yaks::Mapper::HasOne do
4
+ AuthorMapper = Class.new(Yaks::Mapper) { attributes :name }
5
+
6
+ let(:name) { 'William S. Burroughs' }
7
+ let(:mapper) { AuthorMapper }
8
+ let(:has_one) { described_class.new(:author, mapper, 'http://rel', Yaks::Undefined) }
9
+ let(:author) { double(:name => name) }
10
+ let(:policy) {
11
+ double(
12
+ Yaks::DefaultPolicy,
13
+ derive_type_from_mapper_class: 'author',
14
+ derive_mapper_from_association: AuthorMapper
15
+ )
16
+ }
17
+ let(:context) {{policy: policy, env: {}}}
18
+
19
+ it 'should map to a single Resource' do
20
+ expect(has_one.map_resource(author, context)).to eq Yaks::Resource.new(type: 'author', attributes: {name: name})
21
+ end
22
+
23
+ context 'with no mapper specified' do
24
+ let(:mapper) { Yaks::Undefined }
25
+
26
+ it 'should derive one based on policy' do
27
+ expect(has_one.create_subresource(nil, {author: author}, context)).to eql [
28
+ 'http://rel',
29
+ Yaks::Resource.new(type: 'author', attributes: {name: name})
30
+ ]
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,152 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Yaks::Mapper::Link do
4
+ subject(:link) { described_class.new(rel, template, options) }
5
+
6
+ let(:rel) { :next }
7
+ let(:template) { '/foo/bar/{x}/{y}' }
8
+ let(:options) { {} }
9
+
10
+ its(:template_variables) { should eq [:x, :y] }
11
+ its(:uri_template) { should eq URITemplate.new(template) }
12
+ its(:expand?) { should be true }
13
+
14
+ describe '#rel?' do
15
+ it 'should return true if the relation matches' do
16
+ expect(link.rel?(:next)).to be true
17
+ end
18
+
19
+ it 'should return false if the relation does not match' do
20
+ expect(link.rel?(:previous)).to be false
21
+ end
22
+
23
+ context 'with URI rels' do
24
+ let(:rel) { 'http://foo/bar/rel' }
25
+
26
+ it 'should return true if the relation matches' do
27
+ expect(link.rel?('http://foo/bar/rel')).to be true
28
+ end
29
+
30
+ it 'should return false if the relation does not match' do
31
+ expect(link.rel?('http://foo/bar/other')).to be false
32
+ end
33
+ end
34
+ end
35
+
36
+ describe '#expand_with' do
37
+ it 'should look up expansion values through the provided callable' do
38
+ expect(link.expand_with(->(var){ var.upcase })).to eq '/foo/bar/X/Y'
39
+ end
40
+
41
+ context 'with expansion turned off' do
42
+ let(:options) { {expand: false} }
43
+
44
+ it 'should keep the template in the response' do
45
+ expect(link.expand_with(->{ })).to eq '/foo/bar/{x}/{y}'
46
+ end
47
+
48
+ its(:expand?) { should be false }
49
+ end
50
+
51
+ context 'with a URI without expansion variables' do
52
+ let(:template) { '/orders' }
53
+
54
+ it 'should return the link as is' do
55
+ expect(link.expand_with(->{ })).to eq '/orders'
56
+ end
57
+ end
58
+
59
+ context 'with partial expansion' do
60
+ let(:options) { { expand: [:y] } }
61
+
62
+ it 'should only expand the given variables' do
63
+ expect(link.expand_with({:y => 7}.method(:[]))).to eql '/foo/bar/{x}/7'
64
+ end
65
+ end
66
+
67
+ context 'with a symbol for a template' do
68
+ let(:template) { :a_symbol }
69
+
70
+ it 'should use the lookup mechanism for finding the link' do
71
+ expect(link.expand_with({:a_symbol => '/foo/foo'}.method(:[]))).to eq '/foo/foo'
72
+ end
73
+ end
74
+ end
75
+
76
+ describe '#map_to_resource_link' do
77
+ subject(:resource_link) { link.map_to_resource_link(mapper) }
78
+
79
+ its(:rel) { should eq :next }
80
+
81
+ let(:object) { Struct.new(:x,:y).new(3,4) }
82
+
83
+ let(:mapper) do
84
+ Yaks::Mapper.new(object, nil)
85
+ end
86
+
87
+ context 'with attributes' do
88
+ it 'should not have a title' do
89
+ expect(resource_link.options.key?(:title)).to be false
90
+ end
91
+
92
+ it 'should not be templated' do
93
+ expect(resource_link.options[:templated]).to be_falsey
94
+ end
95
+
96
+ context 'with extra options' do
97
+ let(:options) { {title: 'foo', expand: [:x], foo: :bar} }
98
+
99
+ it 'should pass on unknown options' do
100
+ expect(resource_link.options[:foo]).to eql :bar
101
+ end
102
+ end
103
+
104
+ it 'should create an instance of Yaks::Resource::Link' do
105
+ expect(resource_link).to be_a(Yaks::Resource::Link)
106
+ end
107
+
108
+ it 'should expand the URI template' do
109
+ expect(resource_link.uri).to eq '/foo/bar/3/4'
110
+ end
111
+ end
112
+
113
+ context 'with expansion turned off' do
114
+ let(:options) { {expand: false} }
115
+
116
+ it 'should be templated' do
117
+ expect(resource_link.options[:templated]).to be true
118
+ end
119
+
120
+ it 'should not propagate :expand' do
121
+ expect(resource_link.options.key?(:expand)).to be false
122
+ end
123
+ end
124
+
125
+ context 'with partial expansion' do
126
+ let(:options) { {expand: [:x]} }
127
+
128
+ it 'should be templated' do
129
+ expect(resource_link.options[:templated]).to be true
130
+ end
131
+ end
132
+
133
+ context 'with a title set' do
134
+ let(:options) { { title: 'link title' } }
135
+
136
+ it 'should set the title on the resource link' do
137
+ expect(resource_link.title).to eq 'link title'
138
+ end
139
+ end
140
+
141
+ context 'with a title lambda' do
142
+ let(:options) { { title: -> { "say #{mapper_method}" } } }
143
+
144
+ it 'should evaluate the lambda in the context of the mapper' do
145
+ expect(mapper).to receive(:mapper_method).and_return('hello')
146
+ expect(resource_link.title).to eq 'say hello'
147
+ end
148
+ end
149
+
150
+ end
151
+
152
+ end
@@ -0,0 +1,177 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Yaks::Mapper do
4
+ subject(:mapper) { mapper_class.new(instance, context) }
5
+
6
+ let(:mapper_class) { Class.new(Yaks::Mapper) { type 'foo' } }
7
+ let(:instance) { double(foo: 'hello', bar: 'world') }
8
+ let(:policy) { nil }
9
+ let(:options) { {} }
10
+ let(:context) {{policy: policy, env: {}}}
11
+
12
+ describe '#map_attributes' do
13
+ before do
14
+ mapper_class.attributes :foo, :bar
15
+ end
16
+
17
+ it 'should make the configured attributes available on the instance' do
18
+ expect(mapper.attributes).to eq [:foo, :bar]
19
+ end
20
+
21
+ it 'should load them from the model' do
22
+ expect(mapper.map_attributes).to eq(foo: 'hello', bar: 'world')
23
+ end
24
+
25
+ context 'with attribute filtering' do
26
+ before do
27
+ mapper_class.class_eval do
28
+ def filter(attrs)
29
+ attrs.to_a - [:foo]
30
+ end
31
+ end
32
+ end
33
+
34
+ it 'should only map the non-filtered attributes' do
35
+ expect(mapper.map_attributes).to eq(:bar => 'world')
36
+ end
37
+ end
38
+ end
39
+
40
+ describe '#map_links' do
41
+ before do
42
+ mapper_class.link :profile, 'http://foo/bar'
43
+ end
44
+
45
+ it 'should map the link' do
46
+ expect(mapper.map_links).to eq [
47
+ Yaks::Resource::Link.new(:profile, 'http://foo/bar', {})
48
+ ]
49
+ end
50
+
51
+ it 'should use the link in the resource' do
52
+ expect(mapper.to_resource.links).to include Yaks::Resource::Link.new(:profile, 'http://foo/bar', {})
53
+ end
54
+
55
+ context 'with the same link rel defined multiple times' do
56
+ before do
57
+ mapper_class.class_eval do
58
+ link(:self, 'http://foo/bam')
59
+ link(:self, 'http://foo/baz')
60
+ link(:self, 'http://foo/baq')
61
+ end
62
+ end
63
+
64
+ it 'should map all the links' do
65
+ expect(mapper.map_links).to eq [
66
+ Yaks::Resource::Link.new(:profile, 'http://foo/bar', {}),
67
+ Yaks::Resource::Link.new(:self, 'http://foo/bam', {}),
68
+ Yaks::Resource::Link.new(:self, 'http://foo/baz', {}),
69
+ Yaks::Resource::Link.new(:self, 'http://foo/baq', {})
70
+ ]
71
+ end
72
+ end
73
+ end
74
+
75
+ describe '#map_subresources' do
76
+ let(:instance) { double(widget: widget) }
77
+ let(:widget) { double(type: 'super_widget') }
78
+ let(:widget_mapper) { Class.new(Yaks::Mapper) { type 'widget' } }
79
+ let(:policy) { double('Policy') }
80
+
81
+ describe 'has_one' do
82
+ let(:has_one_opts) do
83
+ { mapper: widget_mapper,
84
+ rel: 'http://foo.bar/rels/widgets' }
85
+ end
86
+
87
+ before do
88
+ widget_mapper.attributes :type
89
+ mapper_class.has_one(:widget, has_one_opts)
90
+ end
91
+
92
+
93
+ it 'should have the subresource in the resource' do
94
+ expect(mapper.to_resource.subresources).to eq("http://foo.bar/rels/widgets" => Yaks::Resource.new(type: 'widget', attributes: {:type => "super_widget"}))
95
+ end
96
+
97
+ context 'with explicit mapper and rel' do
98
+ it 'should delegate to the given mapper' do
99
+ expect(mapper.map_subresources).to eq(
100
+ "http://foo.bar/rels/widgets" => Yaks::Resource.new(type: 'widget', attributes: {:type => "super_widget"})
101
+ )
102
+ end
103
+ end
104
+
105
+ context 'with unspecified mapper' do
106
+ let(:has_one_opts) do
107
+ { rel: 'http://foo.bar/rels/widgets' }
108
+ end
109
+
110
+ it 'should derive the mapper based on policy' do
111
+ expect(policy).to receive(:derive_mapper_from_association) {|assoc|
112
+ expect(assoc).to be_a Yaks::Mapper::HasOne
113
+ widget_mapper
114
+ }
115
+ expect(mapper.map_subresources).to eq(
116
+ "http://foo.bar/rels/widgets" => Yaks::Resource.new(type: 'widget', attributes: {:type => "super_widget"})
117
+ )
118
+ end
119
+ end
120
+
121
+ context 'with unspecified rel' do
122
+ let(:has_one_opts) do
123
+ { mapper: widget_mapper }
124
+ end
125
+
126
+ it 'should derive the rel based on policy' do
127
+ expect(policy).to receive(:derive_rel_from_association) {|parent_mapper, assoc|
128
+ expect(parent_mapper).to equal mapper
129
+ expect(assoc).to be_a Yaks::Mapper::HasOne
130
+ 'http://rel/rel'
131
+ }
132
+ expect(mapper.map_subresources).to eq(
133
+ "http://rel/rel" => Yaks::Resource.new(type: 'widget', attributes: {:type => "super_widget"})
134
+ )
135
+ end
136
+ end
137
+
138
+ context 'with the association filtered out' do
139
+ before do
140
+ mapper_class.class_eval do
141
+ def filter(attrs) [] end
142
+ end
143
+ end
144
+
145
+ it 'should not map the resource' do
146
+ expect(mapper.map_subresources).to eq({})
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ describe '#load_attributes' do
153
+ context 'when the mapper implements a method with the attribute name' do
154
+ before do
155
+ mapper_class.class_eval do
156
+ attributes :fooattr, :bar
157
+
158
+ def fooattr
159
+ "#{object.foo} my friend"
160
+ end
161
+ end
162
+ end
163
+
164
+ it 'should get the attribute from the mapper' do
165
+ expect(mapper.map_attributes).to eq(fooattr: 'hello my friend', bar: 'world')
166
+ end
167
+ end
168
+ end
169
+
170
+ describe '#to_resource' do
171
+ let(:instance) { nil }
172
+
173
+ it 'should return a NullResource when the subject is nil' do
174
+ expect(mapper.to_resource).to be_a Yaks::NullResource
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Yaks::Resource do
4
+ subject(:resource) { described_class.new(init_opts) }
5
+ let(:init_opts) { {} }
6
+
7
+ its(:type) { should be_nil }
8
+ its(:attributes) { should eql({}) }
9
+ its(:links) { should eql [] }
10
+ its(:subresources) { should eql({}) }
11
+
12
+ context 'with a type' do
13
+ let(:init_opts) { { type: 'post' } }
14
+ its(:type) { should eql 'post' }
15
+ end
16
+
17
+ context 'with attributes' do
18
+ let(:init_opts) { { attributes: {name: 'Arne', age: 31} } }
19
+
20
+ it 'should delegate [] to attribute access' do
21
+ expect(resource[:name]).to eql 'Arne'
22
+ end
23
+ end
24
+
25
+ context 'with links' do
26
+ let(:init_opts) { { links: [Yaks::Resource::Link.new(:self, '/foo/bar', {})] } }
27
+ its(:links) { should eql [Yaks::Resource::Link.new(:self, '/foo/bar', {})] }
28
+ end
29
+
30
+ context 'with subresources' do
31
+ let(:init_opts) { { subresources: { 'comments' => [Yaks::Resource.new(type: 'comment')] } } }
32
+ its(:subresources) { should eql 'comments' => [Yaks::Resource.new(type: 'comment')] }
33
+ end
34
+
35
+ its(:collection?) { should equal false }
36
+
37
+ it 'should act as a collection of one' do
38
+ expect(resource.each.to_a).to eql [resource]
39
+ end
40
+ end
@@ -1,9 +1,9 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Yaks::HalSerializer do
3
+ RSpec.describe Yaks::Serializer::Hal do
4
4
  include_context 'plant collection resource'
5
5
 
6
- subject { described_class.new(resource).serialize }
6
+ subject { Yaks::Primitivize.create.call(described_class.new(resource).serialize) }
7
7
 
8
8
  it { should eq(load_json_fixture('hal_plant_collection')) }
9
9
  end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe Yaks::Serializer do
4
+ describe '.by_name' do
5
+ specify { expect(Yaks::Serializer.by_name(:hal)).to eql Yaks::Serializer::Hal }
6
+ specify { expect(Yaks::Serializer.by_name(:json_api)).to eql Yaks::Serializer::JsonApi }
7
+ end
8
+
9
+ describe '.by_mime_type' do
10
+ specify { expect(Yaks::Serializer.by_mime_type('application/hal+json')).to eql Yaks::Serializer::Hal }
11
+ end
12
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Yaks::Util do
4
+ include Yaks::Util
5
+
6
+ describe '#Resolve' do
7
+ it 'should return non-proc-values' do
8
+ expect(Resolve('foo')).to eql 'foo'
9
+ end
10
+
11
+ it 'should resolve a proc' do
12
+ expect(Resolve(->{ 123 })).to eql 123
13
+ end
14
+
15
+ it 'should resolve the proc in the given context' do
16
+ expect(Resolve(->{ upcase }, 'foo')).to eql 'FOO'
17
+ end
18
+
19
+ it 'should resolve a proc without context in the context it was lexically defined' do
20
+ expect(Resolve(->{ self })).to be_a RSpec::Core::ExampleGroup
21
+ end
22
+
23
+ it 'should receive the context as an argument when it has an arity > 0' do
24
+ expect(Resolve(->(s){ s.upcase }, 'foo')).to eql 'FOO'
25
+ end
26
+
27
+ it 'should work with method objects' do
28
+ expect(Resolve('foo'.method(:upcase))).to eql 'FOO'
29
+ end
30
+ end
31
+
32
+ describe '#camelize' do
33
+ it 'should camelize' do
34
+ expect(camelize('foo_bar/baz')).to eql 'FooBar::Baz'
35
+ end
36
+ end
37
+
38
+ describe '#underscore' do
39
+ it 'should underscorize' do
40
+ expect(underscore('FooBar::Baz-Quz::Quux')).to eql 'foo_bar/baz__quz/quux'
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,10 @@
1
+ --- !ruby/object:Scholar
2
+ id: 9
3
+ name: "孔子"
4
+ pinyin: "Kongzi"
5
+ latinized: "Confucius"
6
+ works:
7
+ - !ruby/object:Work
8
+ id: 11
9
+ chinese_name: "論語"
10
+ english_name: "Analects"
@@ -0,0 +1,9 @@
1
+ ---
2
+ - !ruby/object:Youtypeitwepostit::Message
3
+ id: 12091295723803341
4
+ text: "massage"
5
+ date_posted: "2014-05-29T07:56:58.035Z"
6
+ - !ruby/object:Youtypeitwepostit::Message
7
+ id: 613856331910938
8
+ text: "Squid!"
9
+ date_posted: "2013-03-28T21:51:08.406Z"
data/yaks.gemspec CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |gem|
7
7
  gem.version = Yaks::VERSION
8
8
  gem.authors = [ 'Arne Brasseur' ]
9
9
  gem.email = [ 'arne@arnebrasseur.net' ]
10
- gem.description = 'Serialize to JSON-API and similar'
10
+ gem.description = 'Serialize to hypermedia. HAL, JSON-API, etc.'
11
11
  gem.summary = gem.description
12
12
  gem.homepage = 'https://github.com/plexus/yaks'
13
13
  gem.license = 'MIT'
@@ -17,15 +17,14 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = `git ls-files -- spec`.split($/)
18
18
  gem.extra_rdoc_files = %w[README.md]
19
19
 
20
- gem.add_runtime_dependency 'hamster'
21
- gem.add_runtime_dependency 'inflection' , '~> 1.0.0' # 12k , flog: 226.8
22
- gem.add_runtime_dependency 'concord' , '~> 0.1.4' # 8k , flog: 62.3
23
- gem.add_runtime_dependency 'uri_template' , '~> 0.6.0' # 104k , flog: 1521.4
24
-
25
- # For comparison, ActiveSupport has a flog score of 11134.7
20
+ gem.add_runtime_dependency 'inflection' , '~> 1.0'
21
+ gem.add_runtime_dependency 'concord' , '~> 0.1.4'
22
+ gem.add_runtime_dependency 'uri_template' , '~> 0.6.0'
23
+ gem.add_runtime_dependency 'rack-accept' , '~> 0.4.5'
26
24
 
27
25
  gem.add_development_dependency 'virtus'
28
- gem.add_development_dependency 'rspec', '~> 2.14'
26
+ gem.add_development_dependency 'rspec', '~> 3.0'
29
27
  gem.add_development_dependency 'rake'
30
28
  gem.add_development_dependency 'mutant-rspec'
29
+ gem.add_development_dependency 'rspec-its'
31
30
  end