kurosawa 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fff5741b6786fceddee8b3934e4f370848797c46
4
- data.tar.gz: d925216514517c42099bdf1570b030ec88b05554
3
+ metadata.gz: 14e1940ade23b020c48dd0ea923424bf56fafae2
4
+ data.tar.gz: 325c412026e1c1efde785a888b8e70254f7ba59b
5
5
  SHA512:
6
- metadata.gz: 292c41fe1c4c7d513e440b2c48b95f7750e0d3036d618c6d6d7496bad8d3ada4d3b8cecef48f9bdfde71afeebb724f964c6ac9524aa90f7c3e51ec79c8eacae2
7
- data.tar.gz: c6394aa4aaac02b6421bd5099b5b9effb523882ec1924beafd33375e5ffa44fd55efbf1755512dff2d9c5671795417347169be7736fa99f9da8834f546bb29ce
6
+ metadata.gz: 0d4dd6523104e8d90f51c5cdae0998ca417de04e7b81eb6328be73dc4309145f50a17bdff2cedc291afa2dd4e60ae8e5aecef1fb3ff09ffe9af73e797969f527
7
+ data.tar.gz: ac744719d7b169a2f8811712c3d7f72c3c56583fa1590ef9c30e2adb56b55703866c58f99fe4b422b1a5c0ac62cef31f6973ebb19d7cdd097f00ac97e82a1d37
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
- # Kurosawa
1
+ # kurosawa.rb
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/kurosawa`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ ![Kurosawa Ruby](https://github.com/astrobunny/kurosawa.rb/raw/master/docs/images/kurosawa-ruby.jpg)
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ A RESTful JSON-based database for eventually-consistent filesystem backends. Uses the REST path to determine object hierarchy.
6
+
7
+ # THIS DATABASE IS STILL IN ALPHA
6
8
 
7
9
  ## Installation
8
10
 
@@ -22,7 +24,54 @@ Or install it yourself as:
22
24
 
23
25
  ## Usage
24
26
 
25
- TODO: Write usage instructions here
27
+ ### On your local environment
28
+
29
+ Run the server
30
+
31
+ ```
32
+ KUROSAWA_FILESYSTEM=file://fs/db bundle exec kurosawa
33
+ ```
34
+
35
+ ### With a docker container
36
+
37
+ ```
38
+ docker run -ti -p4567:4567 astrobunny/kurosawa.rb
39
+ ```
40
+
41
+ ### Try it out!
42
+
43
+ Send it REST commands! (Here I am using resty)
44
+
45
+ ```
46
+ $ GET /
47
+ null
48
+ $ PUT / '{"a": 7, "b": {"e":[100,200,300]} }'
49
+ {"a":"7","b":{"e":["300","200","100"]}}
50
+ $ GET /
51
+ {"a":"7","b":{"e":["300","200","100"]}}
52
+ $ GET /a/b
53
+ null
54
+ $ GET /a
55
+ "7"
56
+ $ GET /b
57
+ {"e":["300","200","100"]}
58
+ $ GET /b/e
59
+ ["300","200","100"]
60
+ $ PATCH / '{"c": "Idols"}'
61
+ {"a":"7","b":{"e":["300","200","100"]},"c":"Idols"}
62
+ $ GET /c
63
+ "Idols"
64
+ $ DELETE /b/e
65
+ null
66
+ $ GET /
67
+ {"a":"7","b":{},"c":"Idols"}
68
+ $ PUT /c '{}'
69
+ {}
70
+ $ PUT /c '{"5": "a"}'
71
+ {"5":"a"}
72
+
73
+ ```
74
+
26
75
 
27
76
  ## Development
28
77
 
@@ -32,5 +81,5 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
81
 
33
82
  ## Contributing
34
83
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/kurosawa.
84
+ Bug reports and pull requests are welcome on GitHub at https://github.com/astrobunny/kurosawa.rb.
36
85
 
data/exe/kurosawa ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require "kurosawa/database"
3
+
4
+ Kurosawa::Database.run!
data/lib/kurosawa.rb CHANGED
@@ -1,5 +1,3 @@
1
1
  require "kurosawa/version"
2
2
 
3
- module Kurosawa
4
- # Your code goes here...
5
- end
3
+ require "kurosawa/connection"
@@ -0,0 +1,4 @@
1
+ module Kurosawa
2
+ class Connection
3
+ end
4
+ end
@@ -0,0 +1,205 @@
1
+ require "sinatra/base"
2
+ require "sinatra/reloader"
3
+ require "json"
4
+
5
+ # filesystems
6
+ require "kurosawa/filesystems"
7
+
8
+ module Kurosawa
9
+ class Database < ::Sinatra::Base
10
+
11
+ set :bind, '0.0.0.0'
12
+ configure :development do
13
+ register Sinatra::Reloader
14
+ end
15
+
16
+ filesystem = ENV["KUROSAWA_FILESYSTEM"] || "file://~/.kurosawa"
17
+
18
+ set :filesystem, Filesystem.instantiate(filesystem)
19
+
20
+ def sanitize_path(path)
21
+ match = /^\/[a-z0-9\-_\/]*$/i.match(URI.unescape(path))
22
+ if match
23
+ match.to_s.sub(/\/+$/, "")
24
+ else
25
+ p URI.unescape(path)
26
+ halt 400
27
+ end
28
+ end
29
+
30
+ def sanitize_body(body)
31
+ begin
32
+ JSON.parse(body)
33
+ rescue => e
34
+ puts e
35
+ halt 400
36
+ end
37
+ end
38
+
39
+ def random_key
40
+ SecureRandom.hex(4)
41
+ end
42
+
43
+ def get_property(fs, path)
44
+ property_entries = fs.ls("#{path}/$")
45
+ if property_entries.length == 0
46
+ nil
47
+ elsif property_entries.length == 1
48
+ JSON.parse(fs.get(property_entries[0]), symbolize_names: true)
49
+ else
50
+ # conflict
51
+ raise "TODO get_property handle conflict"
52
+ end
53
+ end
54
+
55
+ def set_property(fs, path, type:)
56
+ puts "set property: #{path}"
57
+ property_entries = fs.ls("#{path}/$")
58
+ p property_entries
59
+ if property_entries.length > 0
60
+ property_entries.each do |x|
61
+ fs.del(x)
62
+ end
63
+ end
64
+ fs.put("#{path}/$#{random_key}", {type: type}.to_json)
65
+ end
66
+
67
+
68
+ def read(fs, path, params)
69
+
70
+ puts "read: path=#{path.inspect}"
71
+ prop = get_property(fs, path)
72
+ if prop == nil
73
+ nil
74
+ elsif prop[:type] == "object"
75
+ res = {}
76
+
77
+ inner_entries = fs.ls("#{path}")
78
+ .select{ |x| /^\/\$/.match(x) == nil }
79
+ .map{ |x| x.sub(/\/[^\/]+$/, "") }
80
+ .map{ |x| match = /(?<first>^\/[^\/]+)/.match(x.sub(/^#{path}/, "")); match[:first] if match }
81
+ .select{ |x| x != nil}
82
+ .uniq
83
+
84
+ puts "read: object: inner: #{fs.ls("#{path}").inspect}"
85
+ puts "read: object: inner: #{inner_entries.inspect}"
86
+
87
+ inner_entries.each do |x|
88
+ res[x[1..-1]] = read(fs, path + x, params)
89
+ end
90
+
91
+ res
92
+
93
+ elsif prop[:type] == "array"
94
+ inner_entries = fs.ls("#{path}")
95
+ .select{ |x| /\/\$/.match(x) == nil }
96
+ .map{ |x| match = /(?<first>^\/[^\/]+)/.match(x.sub(/^#{path}/, "")); match[:first] if match }
97
+ .select{ |x| x != nil}
98
+ .uniq
99
+
100
+ puts "read: array: #{inner_entries.inspect}"
101
+
102
+ inner_entries.map do |x|
103
+ read(fs, path + x, params)
104
+ end
105
+
106
+ else
107
+ inner_entries = fs.ls("#{path}")
108
+ .select{ |x| /\/\$/.match(x) == nil }
109
+
110
+ puts "read: string: #{inner_entries.inspect}"
111
+
112
+ if inner_entries.length == 1
113
+ JSON.parse(fs.get(inner_entries[0]), symbolize_names: true)[:value]
114
+ elsif inner_entries.length == 0
115
+ nil
116
+ else
117
+ {"$conflict": inner_entries.map{|x| JSON.parse(fs.get(x), symbolize_names: true) }}
118
+ #raise "read conflict at #{path} #{inner_entries.inspect}"
119
+ end
120
+ end
121
+ end
122
+
123
+ def write(fs, path, body)
124
+
125
+ puts "write: path=#{path.inspect}"
126
+
127
+ if body.is_a? Hash
128
+ set_property(fs, path, type: "object")
129
+ body.each do |k,v|
130
+ write(fs, "#{path}/#{k}", v)
131
+ end
132
+
133
+ elsif body.is_a? Array
134
+ set_property(fs, path, type: "array")
135
+ body.each do |value|
136
+ write(fs, "#{path}/#{random_key}", value)
137
+ end
138
+
139
+ else
140
+ existing_entries = fs.ls("#{path}")
141
+
142
+ if existing_entries.length > 0
143
+ existing_entries.each do |x|
144
+ fs.del(x)
145
+ end
146
+ end
147
+ set_property(fs, path, type: "string")
148
+ fs.put("#{path}/#{random_key}", {value: body.to_s}.to_json)
149
+ end
150
+ end
151
+
152
+ def delete(fs, path)
153
+ inner_entries = fs.ls("#{path}")
154
+
155
+ puts "delete: #{inner_entries.inspect}"
156
+
157
+ inner_entries.each do |x|
158
+ fs.del(x)
159
+ end
160
+ end
161
+
162
+ get "*" do
163
+ path = sanitize_path(request.path)
164
+ x = read(settings.filesystem, path, params)
165
+ status 404 if x == nil
166
+ x.to_json
167
+ end
168
+
169
+ post "*" do
170
+ path = sanitize_path(request.path)
171
+
172
+ end
173
+
174
+ put "*" do
175
+ path = sanitize_path(request.path)
176
+ delete(settings.filesystem, path)
177
+ write(settings.filesystem, path, sanitize_body(request.body.read))
178
+ read(settings.filesystem, path, params).to_json
179
+ end
180
+
181
+ patch "*" do
182
+ path = sanitize_path(request.path)
183
+ write(settings.filesystem, path, sanitize_body(request.body.read))
184
+ read(settings.filesystem, path, params).to_json
185
+ end
186
+
187
+ delete "*" do
188
+ path = sanitize_path(request.path)
189
+ delete(settings.filesystem, path)
190
+ read(settings.filesystem, path, params).to_json
191
+ end
192
+
193
+ head "*" do
194
+ path = sanitize_path(request.path)
195
+
196
+ end
197
+
198
+ options "*" do
199
+ path = sanitize_path(request.path)
200
+
201
+ end
202
+
203
+
204
+ end
205
+ end
@@ -0,0 +1,29 @@
1
+
2
+ require "kurosawa/filesystems/local"
3
+ require "kurosawa/filesystems/s3"
4
+
5
+ require "uri"
6
+
7
+ module Kurosawa
8
+ class Filesystem
9
+ def self.instantiate(fs)
10
+ if fs.start_with?("file://")
11
+ Kurosawa::Filesystems::Local.new(fs.sub("file://", ""))
12
+ elsif fs.start_with?("s3://") or fs.start_with?("s3http://") or fs.start_with?("s3https://")
13
+
14
+ url = URI.parse(fs);
15
+ tokens = url.userinfo.split(":")
16
+
17
+ Kurosawa::Filesystems::S3.new(
18
+ access_key_id:"#{tokens[0]}",
19
+ secret_access_key:"#{tokens[1]}",
20
+ region:"#{tokens[2]}",
21
+ bucket:url.scheme == "s3" ? url.host : "",
22
+ endpoint:url.scheme == "s3" ? nil : "#{url.scheme.sub("s3","")}://#{url.host}:#{url.port}",
23
+ force_path_style: url.scheme != "s3")
24
+ else
25
+ raise "Unknown filesystem"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+
2
+ module Kurosawa::Filesystems
3
+ class Base
4
+ def ls(prefix)
5
+ []
6
+ end
7
+
8
+ def get(key)
9
+ ""
10
+ end
11
+
12
+ def put(key, value)
13
+ end
14
+
15
+ def del(key)
16
+ end
17
+
18
+ def exists(key)
19
+ false
20
+ end
21
+
22
+ def cleanup!
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,59 @@
1
+ require 'kurosawa/filesystems/base'
2
+ require 'fileutils'
3
+
4
+ module Kurosawa::Filesystems
5
+ class Local < Base
6
+
7
+ def initialize(root_path)
8
+ @root_path = File.expand_path(root_path)
9
+ FileUtils.mkdir_p(@root_path)
10
+ end
11
+
12
+ def root_path
13
+ @root_path
14
+ end
15
+
16
+ def path(key)
17
+ File.join(@root_path, key)
18
+ end
19
+
20
+ def ls(prefix)
21
+ puts "fs: ls #{prefix}"
22
+ (Dir["#{@root_path}/#{prefix}*"] + Dir["#{@root_path}/#{prefix}/**/*"]).
23
+ select{|x| File.file?(x)}.
24
+ map{|x| x.sub(/^#{root_path}/, "").
25
+ gsub(/\/+/, "/")}.
26
+ uniq
27
+
28
+ end
29
+
30
+ def get(key)
31
+ puts "fs: get #{path(key)}"
32
+ File.read(path(key))
33
+ end
34
+
35
+ def put(key, value)
36
+ puts "fs: put #{path(key)}"
37
+ FileUtils.mkdir_p(File.dirname(path(key)))
38
+ File.write(path(key), value)
39
+ end
40
+
41
+ def del(key)
42
+ puts "fs: delete #{path(key)}"
43
+ File.delete(path(key)) if File.file?(path(key))
44
+ end
45
+
46
+ def exists(key)
47
+ puts "fs: exists #{path(key)}"
48
+ File.exists?(path(key)) and File.file?(path(key))
49
+ end
50
+
51
+ def cleanup!
52
+ Dir['#{@root_path}**/*'].
53
+ select { |d| File.directory? d }.
54
+ select { |d| (Dir.entries(d) - %w[ . .. ]).empty? }.
55
+ each { |d| Dir.rmdir d }
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,72 @@
1
+ require 'kurosawa/filesystems/base'
2
+ require 'uri'
3
+ require 'aws-sdk'
4
+
5
+ module Kurosawa::Filesystems
6
+ class S3 < Base
7
+
8
+ def initialize(access_key_id:,secret_access_key:,region:,endpoint:,bucket:,force_path_style:false)
9
+
10
+ options = {
11
+ :access_key_id => URI.encode(access_key_id),
12
+ :secret_access_key => URI.decode(secret_access_key),
13
+ :region => region,
14
+ :force_path_style => force_path_style
15
+ }
16
+ options[:endpoint] = endpoint if endpoint
17
+
18
+ @s3 = Aws::S3::Client.new(options)
19
+ @bucket_name = bucket
20
+ @bucket = Aws::S3::Bucket.new(bucket, client: @s3)
21
+ end
22
+
23
+ def ls(prefix)
24
+ prefix.gsub!(/^\/+/, "")
25
+ puts "s3:ls #{prefix}"
26
+ result = []
27
+ next_marker = nil
28
+ loop do
29
+ options = {
30
+ bucket: @bucket_name,
31
+ marker: next_marker
32
+ }
33
+
34
+ options[:prefix] = prefix if prefix.length != 0
35
+
36
+ resp = @s3.list_objects(options);
37
+ result += resp.contents.map { |x| "/#{x.key}" }
38
+ break if resp.next_marker == nil
39
+ next_marker = resp.next_marker
40
+ end
41
+ result
42
+ end
43
+
44
+ def get(key)
45
+ key.gsub!(/^\/+/, "")
46
+ puts "s3:get #{key}"
47
+ resp = @s3.get_object(bucket: @bucket_name, key: key)
48
+ resp.body.read
49
+ end
50
+
51
+ def put(key, value)
52
+ key.gsub!(/^\/+/, "")
53
+ puts "s3:put #{key}"
54
+ @s3.put_object(bucket: @bucket_name, key: key, body: value)
55
+ end
56
+
57
+ def del(key)
58
+ key.gsub!(/^\/+/, "")
59
+ puts "s3:del #{key}"
60
+ @s3.delete_object(bucket: @bucket_name, key: key)
61
+ end
62
+
63
+ def exists(key)
64
+ key.gsub!(/^\/+/, "")
65
+ puts "s3:exists #{key}"
66
+ @bucket.objects[key].exists?
67
+ end
68
+
69
+ def cleanup!
70
+ end
71
+ end
72
+ end
@@ -1,3 +1,3 @@
1
1
  module Kurosawa
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kurosawa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - astrobunny
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-07-17 00:00:00.000000000 Z
11
+ date: 2016-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,25 +52,84 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: fakes3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sinatra
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.4'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sinatra-contrib
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: aws-sdk
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 2.4.4
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 2.4.4
55
111
  description: Tribute to Kurosawa Ruby
56
112
  email:
57
113
  - admin@astrobunny.com
58
- executables: []
114
+ executables:
115
+ - kurosawa
59
116
  extensions: []
60
117
  extra_rdoc_files: []
61
118
  files:
62
- - ".gitignore"
63
- - ".rspec"
64
- - ".travis.yml"
65
119
  - Gemfile
66
120
  - README.md
67
- - Rakefile
68
121
  - bin/console
69
122
  - bin/setup
70
- - kurosawa.gemspec
123
+ - exe/kurosawa
71
124
  - lib/kurosawa.rb
125
+ - lib/kurosawa/connection.rb
126
+ - lib/kurosawa/database.rb
127
+ - lib/kurosawa/filesystems.rb
128
+ - lib/kurosawa/filesystems/base.rb
129
+ - lib/kurosawa/filesystems/local.rb
130
+ - lib/kurosawa/filesystems/s3.rb
72
131
  - lib/kurosawa/version.rb
73
- homepage: https://github.com/astrobunny/kurosawa
132
+ homepage: https://github.com/astrobunny/kurosawa.rb
74
133
  licenses: []
75
134
  metadata:
76
135
  allowed_push_host: https://rubygems.org
@@ -90,7 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
149
  version: '0'
91
150
  requirements: []
92
151
  rubyforge_project:
93
- rubygems_version: 2.5.1
152
+ rubygems_version: 2.4.8
94
153
  signing_key:
95
154
  specification_version: 4
96
155
  summary: kurosawa.rb
data/.gitignore DELETED
@@ -1,9 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --format documentation
2
- --color
data/.travis.yml DELETED
@@ -1,5 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.3.0
5
- before_install: gem install bundler -v 1.12.4
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :spec
data/kurosawa.gemspec DELETED
@@ -1,32 +0,0 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'kurosawa/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "kurosawa"
8
- spec.version = Kurosawa::VERSION
9
- spec.authors = ["astrobunny"]
10
- spec.email = ["admin@astrobunny.com"]
11
-
12
- spec.summary = %q{kurosawa.rb}
13
- spec.description = %q{Tribute to Kurosawa Ruby}
14
- spec.homepage = "https://github.com/astrobunny/kurosawa"
15
-
16
- # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
- # to allow pushing to a single host or delete this section to allow pushing to any host.
18
- if spec.respond_to?(:metadata)
19
- spec.metadata['allowed_push_host'] = "https://rubygems.org"
20
- else
21
- raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
22
- end
23
-
24
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
- spec.bindir = "exe"
26
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
- spec.require_paths = ["lib"]
28
-
29
- spec.add_development_dependency "bundler", "~> 1.12"
30
- spec.add_development_dependency "rake", "~> 10.0"
31
- spec.add_development_dependency "rspec", "~> 3.0"
32
- end