rack-a_day_without 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- YjIxNDM3NmRjZDI3Yjc0ZDc1ZDJjZDAyMWFhMzE5OWYxN2ZiZTFhOA==
4
+ OWQ0MTVhOTE2NzM3YTFhMTExYTM0YWE2Njg5MjI0ODc2MDdkYWU1Nw==
5
5
  data.tar.gz: !binary |-
6
- ZjNjMWM0ZjI1ZDhhNmEyMTc5NDg1YzA1Njc4MjE2OTY1NWE3ZDNjZA==
6
+ NzNhOTA3OWY4MmQ4NDVmMmIzYzNjYTJhOTE0ZWZlMTkyNzk2YTc1NA==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- N2Q4ZDkyZDA4OWJhZmI2OGMxM2RhYzc3MTMyMDVmN2FiNDU5Zjg3NmU2NWQz
10
- ZTM0ZGY2NDQzZGRmY2M4YzZmZTc4MzE3ODBjOTI5YjBhMjMwOTM3ZTM5Yjk2
11
- NTdlYTJiNjJkNDQyOWQ3Y2M1OTgxYzljN2JmMDc4ZmY3ZDcwOWU=
9
+ NzU3OTA3NDgyM2Q3NTc1YzIwNjY4OTY4OWQ4NTI2YWVhNDUyMGRlNDQ0YzYw
10
+ YTZjYzU1Y2Y0ZDNkNDc4OTlmMTRjYmU5YmRhYjFmNzA2M2ZhMWQzNjEzYjI4
11
+ Nzc1ZjVmOTEyYjVmY2ZlNDhjYjc0ZjUwY2RjMGQzNDgyOWQwYTI=
12
12
  data.tar.gz: !binary |-
13
- NTBlYzQzMjJlNmZkYTAzYjZlZjk2MjZlNTkwM2JkOGVlNjJjMDRkNTAwMTBj
14
- ODhkNTUzNmE4MTUwOGZjNTczNzJiZmQ4ZjRiMGY2OGNmNjUxOGFlMGE5Njhk
15
- YWNjMmNhOGViOTE3ODM1MmM0ZmQxNzBkZmM2YTdmMThmMzhhZGI=
13
+ NjU4Y2U4NDJlMDhkMzlkMDBkNWUyZTM1NzljYTZmNWZjZDdmNWRjNDlmZjJl
14
+ MjgwZDU2M2NiZGRjODMyZmRjYTM3NjA3NzMxODQ5MDFjMjgwN2YyOGVhZmVj
15
+ NThhMGU5ZjYzOTQ4MmFkZDIxNjY0ZjU4NTZjNGIzZTk0YTc5ZTE=
@@ -1,3 +1,4 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 2.1.2
3
4
  - 1.9.3
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # Rack::ADayWithout
2
2
 
3
- `Rack::ADayWithout` is a middleware for Rack-based web applications, originally built to display alternate content for the [Day Without Art](https://en.wikipedia.org/wiki/Day_Without_Art).
3
+ [![Build Status](https://travis-ci.org/jackjennings/rack-a_day_without.svg)](https://travis-ci.org/jackjennings/rack-a_day_without)
4
+ [![Gem Version](https://badge.fury.io/rb/rack-a_day_without.svg)](http://badge.fury.io/rb/rack-a_day_without)
5
+
6
+ `Rack::ADayWithout` is a middleware for Rack-based web applications, originally built to display alternate content for the [Day Without Art](https://en.wikipedia.org/wiki/Day_Without_Art). All requests on a given day will be served blank or alternate content.
4
7
 
5
8
  ## Installation
6
9
 
@@ -18,26 +21,42 @@ Or install it yourself as:
18
21
 
19
22
  ## Usage
20
23
 
21
- Use `Rack::ADayWithout` as a middleware in your Rack (or Rails) application. The `on` option must be set to the date the site should serve the alternate content.
24
+ Use `Rack::ADayWithout` as a middleware in your Rack (or Rails) application. The first parameter defines the `subject` of ADayWithout. The `on` option must be set to the date the middleware should inject the alternate content. `on` should be either a string that can be parsed by `Date.parse` or an instance of `Date`.
22
25
 
23
26
  ```ruby
24
27
  use Rack::ADayWithout, 'Art', on: '1/12/2014'
25
28
  ```
26
29
 
27
- You can also use the alternate syntax which uses child-classes to set the `subject` of ADayWithout. This is equivalent to the above example:
30
+ You can also use the alternate syntax which uses child classes to set the `subject` of ADayWithout. This is equivalent to the above example:
28
31
 
29
32
  ```ruby
30
33
  use Rack::ADayWithout::Art, on: '1/12/2014'
31
34
  ```
32
35
 
36
+ The child class is generated dynamically, and doesn't need to be defined beforehand. Thus the following are all valid:
37
+
38
+ ```ruby
39
+ use Rack::ADayWithout::War, on: '1/1/2100'
40
+ use Rack::ADayWithout::Pizza, on: '1/2/3456'
41
+ use Rack::ADayWithout::Foo, on: '15/7/2020'
42
+ ```
43
+
44
+ ### Setting Timezone
45
+
46
+ By default `ADayWithout` will use GMT dates. You can set the `timezone` option for the middleware to use a different timezone that `tzinfo` knows about.
47
+
48
+ ```ruby
49
+ use Rack::ADayWithout::Art, on: '1/12/2014', timezone: 'America/New_York'
50
+ ```
51
+
33
52
  ### Writing Content
34
53
 
35
54
  By default, the middleware will write an empty content string for all requests on the specified day. If the `content` or `file` options are set, the content string or file contents will be written instead.
36
55
 
37
56
  ```ruby
38
- use Rack::ADayWithout, 'Art', on: '1/12/2014', content: 'A Day Without Art'
57
+ use Rack::ADayWithout::Art, on: '1/12/2014', content: 'A Day Without Art'
39
58
  # or...
40
- use Rack::ADayWithout, 'Art', on: '1/12/2014', file: './public/index.html'
59
+ use Rack::ADayWithout::Art, on: '1/12/2014', file: './public/index.html'
41
60
  ```
42
61
 
43
62
  ### Bypass Routes
@@ -45,7 +64,7 @@ use Rack::ADayWithout, 'Art', on: '1/12/2014', file: './public/index.html'
45
64
  The `bypass` option allows some routes to pass through the middleware without being blocked. This can be useful if you have an admin area that should still be available during the day without. `bypass` can be set to be a `String`, a `Regexp` or an `Array` of either.
46
65
 
47
66
  ```ruby
48
- use Rack::ADayWithout, 'Art', on: '1/12/2014',
67
+ use Rack::ADayWithout::Art, on: '1/12/2014',
49
68
  bypass: [/^\/admin/, '/about']
50
69
  ```
51
70
 
@@ -58,7 +77,7 @@ module YourApp
58
77
  class Application < Rails::Application
59
78
  # ...
60
79
 
61
- config.middleware.use Rack::ADayWithout, 'Art', on: '22/11/2014'
80
+ config.middleware.use Rack::ADayWithout::Art, on: '22/11/2014'
62
81
  end
63
82
  end
64
83
  ```
@@ -1,10 +1,14 @@
1
1
  require "date"
2
+ require "tzinfo"
3
+ require "uri"
2
4
  require "rack"
3
5
  require "rack/a_day_without/version"
4
6
 
5
7
  module Rack
6
8
  class ADayWithout
7
9
 
10
+ attr_accessor :subject
11
+
8
12
  def self.const_missing const_name
9
13
  const_set const_name, self.new_subject_subclass
10
14
  end
@@ -21,34 +25,103 @@ module Rack
21
25
  def initialize app, subject, options = {}
22
26
  @app = app
23
27
  @subject = subject
24
- @content = options[:content]
25
- @file = options[:file]
26
- @date = parse_date options[:on]
27
- @allowed = parse_allowed_routes options[:bypass]
28
+ @options = {
29
+ timezone: 'GMT',
30
+ disabled_on: false
31
+ }.merge options
28
32
  end
29
33
 
30
34
  def call env
31
- allowed = allowed_path? env['PATH_INFO']
32
- if @date == Date.today && !allowed
33
- res = Response.new
34
- res["X-Day-Without"] = @subject
35
- res.write content
36
- res.finish
35
+ request = Request.new env
36
+
37
+ return @app.call env unless request.get? && !request.xhr?
38
+
39
+ if disabling_query? request
40
+ disable! request
41
+ @app.call env
42
+ elsif !disabled?(request) && date == today && !allowed?(request)
43
+ block request
37
44
  else
38
45
  @app.call env
39
46
  end
40
47
  end
41
48
 
42
- private
49
+ def timezone
50
+ @timezone ||= parse_timezone @options[:timezone]
51
+ end
52
+
53
+ def date
54
+ @date ||= parse_date @options[:on]
55
+ end
56
+
57
+ def allowed_paths
58
+ @allowed ||= parse_allowed_routes @options[:bypass]
59
+ end
60
+
61
+ def today
62
+ timezone.now.to_date
63
+ end
43
64
 
44
65
  def content
45
- if @file
46
- ::File.read @file
66
+ if @options[:file]
67
+ ::File.read @options[:file]
68
+ else
69
+ @options[:content].to_s
70
+ end
71
+ end
72
+
73
+ def allowed? request
74
+ allowed_path? request.path_info
75
+ end
76
+
77
+ def allowed_path? path
78
+ allowed_paths.any? do |a|
79
+ a.is_a?(Regexp) ? a.match(path.to_s) : a == path.to_s
80
+ end
81
+ end
82
+
83
+ def disabling_query? request
84
+ key = @options[:disabled_on]
85
+ key != false && request.params.keys.include?(key)
86
+ end
87
+
88
+ def disable! request
89
+ if defined?(ActionDispatch::Request)
90
+ request = ActionDispatch::Request.new(request.env)
91
+ request.cookie_jar[:day_with] = { value: true, path: '/' }
92
+ end
93
+ end
94
+
95
+ def disabled? request
96
+ if defined?(ActionDispatch::Request)
97
+ request = ActionDispatch::Request.new(request.env)
98
+ request.cookie_jar[:day_with] || false
47
99
  else
48
- @content.to_s
100
+ false
49
101
  end
50
102
  end
51
103
 
104
+ private
105
+
106
+ def redirect request
107
+ params = request.params
108
+ params.delete @options[:disabled_on]
109
+ query = URI.encode params.map {|k,v| "#{k}=#{v}"}.join("&")
110
+ location = [request.scheme, '://', request.host, request.path].join
111
+ location << "?#{query}" unless query.empty?
112
+
113
+ Response.new do |r|
114
+ r.redirect location
115
+ end.finish
116
+ end
117
+
118
+ def block request
119
+ Response.new do |r|
120
+ r["X-Day-Without"] = subject
121
+ r.write content
122
+ end.finish
123
+ end
124
+
52
125
  def parse_allowed_routes allowed
53
126
  if allowed.nil?
54
127
  []
@@ -63,10 +136,8 @@ module Rack
63
136
  Date.parse dateish.to_s
64
137
  end
65
138
 
66
- def allowed_path? path
67
- @allowed.any? do |a|
68
- a.is_a?(Regexp) ? a.match(path.to_s) : a == path.to_s
69
- end
139
+ def parse_timezone timezone
140
+ TZInfo::Timezone.get timezone
70
141
  end
71
142
 
72
143
  end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  class ADayWithout
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.3"
4
4
  end
5
5
  end
@@ -18,8 +18,10 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ["lib"]
19
19
 
20
20
  spec.add_dependency "rack", "~> 1.4"
21
+ spec.add_dependency "tzinfo", ">= 0.3.29"
21
22
 
22
23
  spec.add_development_dependency "bundler", "~> 1.3"
23
24
  spec.add_development_dependency "rake", "~> 0"
24
25
  spec.add_development_dependency "minitest", "~> 5.4"
26
+ spec.add_development_dependency "actionpack", "~> 4"
25
27
  end
@@ -3,3 +3,15 @@ require 'rack/a_day_without'
3
3
 
4
4
  require 'minitest'
5
5
  require 'minitest/autorun'
6
+
7
+ def today timezone = 'GMT'
8
+ timezone(timezone).now.to_date
9
+ end
10
+
11
+ def tomorrow timezone = 'GMT'
12
+ timezone(timezone).now.to_date + 1
13
+ end
14
+
15
+ def timezone name
16
+ TZInfo::Timezone.get name
17
+ end
@@ -1,4 +1,23 @@
1
1
  require 'minitest_helper'
2
+ require 'uri'
3
+ require 'action_dispatch'
4
+
5
+ TWENTY_FOUR_HOURS = (60 * 60 * 24)
6
+
7
+ def create_app options = {}
8
+ downstream = Proc.new do
9
+ Rack::Response.new do |r|
10
+ r.write 'Downstream app'
11
+ end.finish
12
+ end
13
+
14
+ Rack::Builder.new do
15
+ use ActionDispatch::Cookies
16
+ use ActionDispatch::Session::CookieStore, key: '_test_session'
17
+ use Rack::ADayWithout, 'Art', options
18
+ run downstream
19
+ end
20
+ end
2
21
 
3
22
  describe Rack::ADayWithout do
4
23
 
@@ -9,28 +28,28 @@ describe Rack::ADayWithout do
9
28
  describe 'when used as middleware' do
10
29
  before do
11
30
  @app = Proc.new do
12
- Rack::Response.new {|r| r.write 'Downstream app'}.finish
31
+ Rack::Response.new do |r|
32
+ r.write 'Downstream app'
33
+ end.finish
13
34
  end
14
35
  end
15
36
 
16
37
  it 'blocks a request' do
17
- endpoint = Rack::ADayWithout.new @app, 'Art', on: Date.today
38
+ endpoint = create_app on: today
18
39
  status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar'))
19
40
  status.must_equal 200
20
41
  body.body.must_equal ['']
21
42
  end
22
43
 
23
44
  it 'passes a request' do
24
- endpoint = Rack::ADayWithout.new @app, 'Art', on: Date.new
45
+ endpoint = create_app on: tomorrow
25
46
  status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar'))
26
47
  status.must_equal 200
27
48
  body.body.must_equal ['Downstream app']
28
49
  end
29
50
 
30
51
  it 'blocks with supplied content' do
31
- endpoint = Rack::ADayWithout.new @app, 'Art',
32
- on: Date.today,
33
- content: 'foobar'
52
+ endpoint = create_app on: today, content: 'foobar'
34
53
  status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar'))
35
54
  status.must_equal 200
36
55
  body.body.must_equal ['foobar']
@@ -39,88 +58,127 @@ describe Rack::ADayWithout do
39
58
  it 'blocks with supplied file' do
40
59
  path = 'test/fixtures/index.html'
41
60
  content = File.read(path)
42
- endpoint = Rack::ADayWithout.new @app, 'Art',
43
- on: Date.today,
44
- file: path
61
+ endpoint = create_app on: today, file: path
45
62
  status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar'))
46
63
  status.must_equal 200
47
64
  body.body.must_equal [content]
48
65
  end
49
66
 
50
67
  it 'passes allowed routes' do
51
- endpoint = Rack::ADayWithout.new @app, 'Art',
52
- on: Date.today,
53
- bypass: '/bar'
68
+ endpoint = create_app on: today, bypass: '/bar'
54
69
  status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar'))
55
70
  status.must_equal 200
56
71
  body.body.must_equal ['Downstream app']
57
72
  end
58
73
 
59
74
  it 'blocks sub-path of allowed routes' do
60
- endpoint = Rack::ADayWithout.new @app, 'Art',
61
- on: Date.today,
62
- bypass: '/bar'
75
+ endpoint = create_app on: today, bypass: '/bar'
63
76
  status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar/bar'))
64
77
  status.must_equal 200
65
78
  body.body.must_equal ['']
66
79
  end
67
80
 
68
81
  it 'passes allowed routes as regexp' do
69
- endpoint = Rack::ADayWithout.new @app, 'Art',
70
- on: Date.today,
71
- bypass: %r{^/bar}
82
+ endpoint = create_app on: today, bypass: %r{^/bar}
72
83
  status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar/baz'))
73
84
  status.must_equal 200
74
85
  body.body.must_equal ['Downstream app']
75
86
  end
76
87
 
77
88
  it 'passes allowed routes as array' do
78
- endpoint = Rack::ADayWithout.new @app, 'Art',
79
- on: Date.today,
80
- bypass: ['/bar', '/baz']
89
+ endpoint = create_app on: today, bypass: ['/bar', '/baz']
81
90
  status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar'))
82
91
  status.must_equal 200
83
92
  body.body.must_equal ['Downstream app']
84
93
  end
85
94
 
86
95
  it 'passes allowed routes as array with regexps' do
87
- endpoint = Rack::ADayWithout.new @app, 'Art',
88
- on: Date.today,
89
- bypass: [%r{^/bar}, '/baz']
96
+ endpoint = create_app on: today, bypass: [%r{^/bar}, '/baz']
90
97
  status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar/baz'))
91
98
  status.must_equal 200
92
99
  body.body.must_equal ['Downstream app']
93
100
  end
94
101
 
95
102
  it 'sets HTTP header when blocked' do
96
- endpoint = Rack::ADayWithout.new @app, 'Art',
97
- on: Date.today
103
+ endpoint = create_app on: today
98
104
  status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar'))
99
105
  headers['X-Day-Without'].must_equal 'Art'
100
106
  end
107
+
108
+ describe 'when sent a diabling request' do
109
+ it 'should show' do
110
+ endpoint = create_app on: today, disabled_on: 'day_with'
111
+ status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar?day_with'))
112
+ body.body.must_equal ['Downstream app']
113
+ end
114
+
115
+ it 'should redirect with other params' do
116
+ endpoint = create_app on: today, disabled_on: 'day_with'
117
+ status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar?day_with&foobar=true'))
118
+ body.body.must_equal ['Downstream app']
119
+ end
120
+
121
+ ## TODO: Not sure how to make this test pass...
122
+ # it 'should display regular content in subsequent requests' do
123
+ # endpoint = create_app on: today, disabled_on: 'day_with'
124
+ # endpoint.call(Rack::MockRequest.env_for('/bar?day_with'))
125
+ # status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar'))
126
+ # body.body.must_equal ['Downstream app']
127
+ # end
128
+
129
+ it 'should disable when set to false' do
130
+ endpoint = create_app on: today, disabled_on: false
131
+ status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar?day_with'))
132
+ status.must_equal 200
133
+ body.body.must_equal ['']
134
+ end
135
+ end
101
136
  end
102
137
 
103
138
  describe 'when initialized' do
104
139
  it 'parses a date' do
105
140
  date = '20/10/2014'
106
141
  endpoint = Rack::ADayWithout.new @app, 'Art', on: date
107
- endpoint.instance_variable_get('@date').must_equal Date.parse(date)
142
+ endpoint.date.must_equal Date.parse(date)
108
143
  end
109
144
 
110
145
  it 'parses a date' do
111
146
  date = '20/10/2014'
112
147
  endpoint = Rack::ADayWithout.new @app, 'Art', on: date
113
- endpoint.instance_variable_get('@date').must_equal Date.parse(date)
148
+ endpoint.date.must_equal Date.parse(date)
114
149
  end
115
150
 
116
151
  it 'stores a subject' do
117
- endpoint = Rack::ADayWithout.new @app, 'Art', on: Date.new
118
- endpoint.instance_variable_get('@subject').must_equal 'Art'
152
+ endpoint = Rack::ADayWithout.new @app, 'Art', on: tomorrow
153
+ endpoint.subject.must_equal 'Art'
154
+ end
155
+
156
+ it 'stores a default timezone' do
157
+ endpoint = Rack::ADayWithout.new @app, 'Art', on: today
158
+ endpoint.timezone.name.must_equal 'GMT'
159
+ end
160
+
161
+ it 'stores a timezone' do
162
+ tz = 'America/New_York'
163
+ endpoint = Rack::ADayWithout.new @app, 'Art', on: today, timezone: tz
164
+ endpoint.timezone.name.must_equal tz
119
165
  end
120
166
 
121
167
  it 'stores allowed routes in array' do
122
- endpoint = Rack::ADayWithout.new @app, 'Art', on: Date.new, bypass: 'foo'
123
- endpoint.instance_variable_get('@allowed').must_equal ['foo']
168
+ endpoint = Rack::ADayWithout.new @app, 'Art', on: tomorrow, bypass: 'foo'
169
+ endpoint.allowed_paths.must_equal ['foo']
170
+ end
171
+
172
+ it 'obeys timezones' do
173
+ now = timezone('America/New_York').now
174
+ midnight = now + TWENTY_FOUR_HOURS - now.to_i % TWENTY_FOUR_HOURS + 10
175
+ frozen_today = midnight.to_date
176
+ Time.stub :now, midnight do
177
+ endpoint = Rack::ADayWithout.new @app, 'Art',
178
+ on: frozen_today,
179
+ timezone: 'America/Los_Angeles'
180
+ endpoint.today.wont_equal endpoint.date
181
+ end
124
182
  end
125
183
  end
126
184
 
@@ -132,8 +190,8 @@ describe Rack::ADayWithout do
132
190
  end
133
191
 
134
192
  it 'store subject by name' do
135
- endpoint = Rack::ADayWithout::Art.new @app, on: Date.today
136
- endpoint.instance_variable_get('@subject').must_equal 'Art'
193
+ endpoint = Rack::ADayWithout::Art.new @app, on: today
194
+ endpoint.subject.must_equal 'Art'
137
195
  end
138
196
  end
139
197
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-a_day_without
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jack Jennings
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-22 00:00:00.000000000 Z
11
+ date: 2014-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ~>
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: tzinfo
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.3.29
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.3.29
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +80,20 @@ dependencies:
66
80
  - - ~>
67
81
  - !ruby/object:Gem::Version
68
82
  version: '5.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: actionpack
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '4'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '4'
69
97
  description:
70
98
  email:
71
99
  - j@ckjennin.gs