grape-app 0.8.0 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/Gemfile.lock +29 -29
- data/grape-app.gemspec +1 -1
- data/lib/grape/app/helpers.rb +1 -0
- data/lib/grape/app/helpers/caching.rb +107 -0
- data/spec/grape/app/helpers/caching_spec.rb +57 -0
- data/spec/grape/app/helpers/params_spec.rb +17 -0
- data/spec/spec_helper.rb +57 -0
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77187b9f038601c08edaa41385655236b13907e88a22baa30cdc037641ebb214
|
4
|
+
data.tar.gz: c21cce56ec2393c67ef7885c01c187641205367b44b2624c0944b560f6aeb954
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b29b7f6971e417b1d5d2a0e396eb456c37a5b0cee7ae3b9716faf8adc96a4321df510b9809dd457b5f37e3a70af2ee6e708eb1ffbc140daa5c5455266ad560bb
|
7
|
+
data.tar.gz: dd6e2198260a448270026d13776c2fb0b26f8e236349491fbde0438b5495d959a044361555efc6e7a03bffcf7e4c0f88d0aab0397870ec01e70253ac68fd726a
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
grape-app (0.8.
|
4
|
+
grape-app (0.8.1)
|
5
5
|
activesupport
|
6
6
|
grape (>= 1.2)
|
7
7
|
grape-entity
|
@@ -13,17 +13,17 @@ PATH
|
|
13
13
|
GEM
|
14
14
|
remote: https://rubygems.org/
|
15
15
|
specs:
|
16
|
-
activemodel (6.0.
|
17
|
-
activesupport (= 6.0.
|
18
|
-
activerecord (6.0.
|
19
|
-
activemodel (= 6.0.
|
20
|
-
activesupport (= 6.0.
|
21
|
-
activesupport (6.0.
|
16
|
+
activemodel (6.0.1)
|
17
|
+
activesupport (= 6.0.1)
|
18
|
+
activerecord (6.0.1)
|
19
|
+
activemodel (= 6.0.1)
|
20
|
+
activesupport (= 6.0.1)
|
21
|
+
activesupport (6.0.1)
|
22
22
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
23
23
|
i18n (>= 0.7, < 2)
|
24
24
|
minitest (~> 5.1)
|
25
25
|
tzinfo (~> 1.1)
|
26
|
-
zeitwerk (~> 2.
|
26
|
+
zeitwerk (~> 2.2)
|
27
27
|
ast (2.4.0)
|
28
28
|
axiom-types (0.1.1)
|
29
29
|
descendants_tracker (~> 0.0.4)
|
@@ -47,17 +47,17 @@ GEM
|
|
47
47
|
grape-entity (0.7.1)
|
48
48
|
activesupport (>= 4.0)
|
49
49
|
multi_json (>= 1.3.2)
|
50
|
-
i18n (1.
|
50
|
+
i18n (1.7.0)
|
51
51
|
concurrent-ruby (~> 1.0)
|
52
52
|
ice_nine (0.11.2)
|
53
|
-
jaro_winkler (1.5.
|
54
|
-
minitest (5.
|
55
|
-
multi_json (1.
|
53
|
+
jaro_winkler (1.5.4)
|
54
|
+
minitest (5.13.0)
|
55
|
+
multi_json (1.14.1)
|
56
56
|
mustermann (1.0.3)
|
57
57
|
mustermann-grape (1.0.0)
|
58
58
|
mustermann (~> 1.0.0)
|
59
|
-
parallel (1.
|
60
|
-
parser (2.6.
|
59
|
+
parallel (1.18.0)
|
60
|
+
parser (2.6.5.0)
|
61
61
|
ast (~> 2.4.0)
|
62
62
|
rack (2.0.7)
|
63
63
|
rack-accept (0.4.5)
|
@@ -67,21 +67,21 @@ GEM
|
|
67
67
|
rack-test (1.1.0)
|
68
68
|
rack (>= 1.0, < 3)
|
69
69
|
rainbow (3.0.0)
|
70
|
-
rake (
|
71
|
-
rspec (3.
|
72
|
-
rspec-core (~> 3.
|
73
|
-
rspec-expectations (~> 3.
|
74
|
-
rspec-mocks (~> 3.
|
75
|
-
rspec-core (3.
|
76
|
-
rspec-support (~> 3.
|
77
|
-
rspec-expectations (3.
|
70
|
+
rake (13.0.0)
|
71
|
+
rspec (3.9.0)
|
72
|
+
rspec-core (~> 3.9.0)
|
73
|
+
rspec-expectations (~> 3.9.0)
|
74
|
+
rspec-mocks (~> 3.9.0)
|
75
|
+
rspec-core (3.9.0)
|
76
|
+
rspec-support (~> 3.9.0)
|
77
|
+
rspec-expectations (3.9.0)
|
78
78
|
diff-lcs (>= 1.2.0, < 2.0)
|
79
|
-
rspec-support (~> 3.
|
80
|
-
rspec-mocks (3.
|
79
|
+
rspec-support (~> 3.9.0)
|
80
|
+
rspec-mocks (3.9.0)
|
81
81
|
diff-lcs (>= 1.2.0, < 2.0)
|
82
|
-
rspec-support (~> 3.
|
83
|
-
rspec-support (3.
|
84
|
-
rubocop (0.
|
82
|
+
rspec-support (~> 3.9.0)
|
83
|
+
rspec-support (3.9.0)
|
84
|
+
rubocop (0.76.0)
|
85
85
|
jaro_winkler (~> 1.5.1)
|
86
86
|
parallel (~> 1.10)
|
87
87
|
parser (>= 2.6)
|
@@ -100,7 +100,7 @@ GEM
|
|
100
100
|
coercible (~> 1.0)
|
101
101
|
descendants_tracker (~> 0.0, >= 0.0.3)
|
102
102
|
equalizer (~> 0.0, >= 0.0.9)
|
103
|
-
zeitwerk (2.1
|
103
|
+
zeitwerk (2.2.1)
|
104
104
|
|
105
105
|
PLATFORMS
|
106
106
|
ruby
|
@@ -116,4 +116,4 @@ DEPENDENCIES
|
|
116
116
|
sqlite3
|
117
117
|
|
118
118
|
BUNDLED WITH
|
119
|
-
2.0.
|
119
|
+
2.0.2
|
data/grape-app.gemspec
CHANGED
data/lib/grape/app/helpers.rb
CHANGED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'active_support/digest'
|
3
|
+
require 'active_support/cache'
|
4
|
+
|
5
|
+
# Caching support for Grape.
|
6
|
+
# "Borrowed" from [Ruby on Rails](https://github.com/rails/rails/blob/66cabeda2c46c582d19738e1318be8d59584cc5b/actionpack/lib/action_controller/metal/conditional_get.rb)
|
7
|
+
module Grape::App::Helpers::Caching
|
8
|
+
# Sets the `etag`, or `last_modified`, or both on the response and renders a
|
9
|
+
# "304 Not Modified" response if the request is already fresh.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# get '/articles/:id' do
|
14
|
+
# article = Article.find(params[:id])
|
15
|
+
# fresh_when(article, public: true)
|
16
|
+
#
|
17
|
+
# article
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
def fresh_when(object=nil, etag: nil, last_modified: nil, **cache_control)
|
21
|
+
etag ||= object
|
22
|
+
last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
|
23
|
+
|
24
|
+
etag = ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))
|
25
|
+
header 'ETag', etag
|
26
|
+
header 'Last-Modified', last_modified.httpdate if last_modified
|
27
|
+
cache_control(cache_control)
|
28
|
+
|
29
|
+
if_modified_since = headers['If-Modified-Since']
|
30
|
+
if_modified_since = Time.rfc2822(if_modified_since) rescue nil if if_modified_since # rubocop:disable Style/RescueModifier
|
31
|
+
if_none_match = headers['If-None-Match']
|
32
|
+
return unless if_modified_since || if_none_match
|
33
|
+
|
34
|
+
fresh = true
|
35
|
+
fresh &&= last_modified && if_modified_since >= last_modified if if_modified_since
|
36
|
+
fresh &&= if_none_match == etag if if_none_match
|
37
|
+
error! 'Not Modified', 304 if fresh
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sets the `etag` and/or `last_modified` on the response and checks it against
|
41
|
+
# the client request. If the request doesn't match the options provided, the
|
42
|
+
# request is considered stale and should be generated from scratch.
|
43
|
+
# Otherwise, it's fresh and we don't need to generate anything and reply with `304 Not Modified`.
|
44
|
+
#
|
45
|
+
# @example:
|
46
|
+
#
|
47
|
+
# get '/articles/:id' do
|
48
|
+
# article = Article.find(params[:id])
|
49
|
+
# stats = article.really_expensive_call if stale?(article)
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
def stale?(object=nil, **freshness_opts)
|
53
|
+
fresh_when(object, **freshness_opts)
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
# Sets an HTTP 1.1 Cache-Control header. Defaults to `private`.
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
#
|
61
|
+
# expires_in 20.minutes
|
62
|
+
# expires_in 3.hours, public: true
|
63
|
+
# expires_in 3.hours, public: true, must_revalidate: true
|
64
|
+
#
|
65
|
+
# This method will overwrite an existing Cache-Control header.
|
66
|
+
# See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
|
67
|
+
#
|
68
|
+
# The method will also ensure an HTTP Date header for client compatibility.
|
69
|
+
def expires_in(seconds, public: false, must_revalidate: false, stale_while_revalidate: nil, stale_if_error: nil, extras: {})
|
70
|
+
header 'Date', Time.now.httpdate
|
71
|
+
|
72
|
+
cache_control(
|
73
|
+
max_age: seconds,
|
74
|
+
public: public,
|
75
|
+
must_revalidate: must_revalidate,
|
76
|
+
stale_while_revalidate: stale_while_revalidate,
|
77
|
+
stale_if_error: stale_if_error,
|
78
|
+
extras: extras,
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Sets an HTTP 1.1 Cache-Control header of `no-cache`. This means the
|
83
|
+
# resource will be marked as stale, so clients must always revalidate.
|
84
|
+
# Intermediate/browser caches may still store the asset.
|
85
|
+
def expires_now(public: false)
|
86
|
+
cache_control(no_cache: true, public: public)
|
87
|
+
end
|
88
|
+
|
89
|
+
def cache_control(max_age: nil, no_cache: false, public: false, must_revalidate: false, stale_while_revalidate: nil, stale_if_error: nil, extras: nil)
|
90
|
+
extras = extras.map {|k, v| "#{k}=#{v}" } if extras.is_a?(Hash)
|
91
|
+
opts = []
|
92
|
+
|
93
|
+
if no_cache
|
94
|
+
opts << 'public' if public
|
95
|
+
opts << 'no-cache'
|
96
|
+
else
|
97
|
+
opts << "max-age=#{max_age.to_i}" if max_age
|
98
|
+
opts << (public ? 'public' : 'private')
|
99
|
+
opts << 'must-revalidate' if must_revalidate
|
100
|
+
opts << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
|
101
|
+
opts << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
|
102
|
+
end
|
103
|
+
opts.concat(extras) if extras
|
104
|
+
|
105
|
+
header 'Cache-Control', opts.join(', ')
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Grape::App::Helpers::Caching do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
let(:app) { TestAPI }
|
7
|
+
|
8
|
+
it 'should handle fresh-when' do
|
9
|
+
get '/articles'
|
10
|
+
expect(last_response.status).to eq(200)
|
11
|
+
expect(last_response.headers).to include(
|
12
|
+
'Cache-Control' => 'public',
|
13
|
+
'Content-Type' => 'application/json',
|
14
|
+
'ETag' => '975ca8804565c1a569450d61090b2743',
|
15
|
+
'Last-Modified' => 'Fri, 05 Jan 2018 11:25:20 GMT',
|
16
|
+
)
|
17
|
+
expect(JSON.parse(last_response.body).size).to eq(2)
|
18
|
+
|
19
|
+
get '/articles', {}, 'HTTP_IF_NONE_MATCH' => last_response.headers['ETag']
|
20
|
+
expect(last_response.status).to eq(304)
|
21
|
+
get '/articles', {}, 'HTTP_IF_MODIFIED_SINCE' => 'Fri, 05 Jan 2018 11:25:20 GMT'
|
22
|
+
expect(last_response.status).to eq(304)
|
23
|
+
get '/articles', {}, 'HTTP_IF_NONE_MATCH' => last_response.headers['ETag'], 'HTTP_IF_MODIFIED_SINCE' => 'Fri, 05 Jan 2018 11:25:21 GMT'
|
24
|
+
expect(last_response.status).to eq(304)
|
25
|
+
|
26
|
+
get '/articles', {}, 'HTTP_IF_MODIFIED_SINCE' => 'Fri, 05 Jan 2018 11:25:19 GMT'
|
27
|
+
expect(last_response.status).to eq(200)
|
28
|
+
get '/articles', {}, 'HTTP_IF_MODIFIED_SINCE' => 'Fri, 05 Jan 2018 11:25:19 GMT', 'HTTP_IF_NONE_MATCH' => last_response.headers['ETag']
|
29
|
+
expect(last_response.status).to eq(200)
|
30
|
+
get '/articles', {}, 'HTTP_IF_MODIFIED_SINCE' => 'Fri, 05 Jan 2018 11:25:20 GMT', 'HTTP_IF_NONE_MATCH' => 'other'
|
31
|
+
expect(last_response.status).to eq(200)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should handle stale? (with cache-control)' do
|
35
|
+
get '/articles/1'
|
36
|
+
expect(last_response.status).to eq(200)
|
37
|
+
expect(last_response.headers).to include(
|
38
|
+
'Cache-Control' => 'private, stale-if-error=5, a=1, b=2',
|
39
|
+
'Content-Type' => 'application/json',
|
40
|
+
'ETag' => 'c4ca4238a0b923820dcc509a6f75849b',
|
41
|
+
'Last-Modified' => 'Fri, 05 Jan 2018 11:25:10 GMT',
|
42
|
+
)
|
43
|
+
expect(JSON.parse(last_response.body)).to eq(
|
44
|
+
'id' => 1,
|
45
|
+
'title' => 'Welcome',
|
46
|
+
'updated_at' => '2018-01-05 11:25:10 UTC',
|
47
|
+
)
|
48
|
+
|
49
|
+
get '/articles/1', {}, 'HTTP_IF_NONE_MATCH' => last_response.headers['ETag']
|
50
|
+
expect(last_response.status).to eq(304)
|
51
|
+
expect(last_response.headers).to include(
|
52
|
+
'Cache-Control' => 'private, stale-if-error=5, a=1, b=2',
|
53
|
+
'ETag' => 'c4ca4238a0b923820dcc509a6f75849b',
|
54
|
+
'Last-Modified' => 'Fri, 05 Jan 2018 11:25:10 GMT',
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Grape::App::Helpers::Params do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
let(:app) { TestAPI }
|
7
|
+
|
8
|
+
it 'should limit params' do
|
9
|
+
post '/articles', title: 'Today', fresh: true, id: 1234, updated_at: Time.now
|
10
|
+
expect(last_response.status).to eq(201)
|
11
|
+
expect(JSON.parse(last_response.body)).to eq(
|
12
|
+
'id' => 9,
|
13
|
+
'title' => 'Today',
|
14
|
+
'updated_at' => '2018-01-05 11:25:15 UTC',
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,60 @@
|
|
1
1
|
ENV['RACK_ENV'] ||= 'test'
|
2
2
|
require 'grape-app'
|
3
3
|
require 'rack/test'
|
4
|
+
|
5
|
+
class Article
|
6
|
+
include Virtus.model
|
7
|
+
|
8
|
+
class Scope
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def maximum(*)
|
12
|
+
map(&:updated_at).max
|
13
|
+
end
|
14
|
+
|
15
|
+
def each
|
16
|
+
yield Article.new(id: 1, title: 'Welcome', updated_at: Time.at(1515151510).utc)
|
17
|
+
yield Article.new(id: 2, title: 'Bye', updated_at: Time.at(1515151520).utc)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.all
|
22
|
+
Scope.new
|
23
|
+
end
|
24
|
+
|
25
|
+
attribute :id
|
26
|
+
attribute :title
|
27
|
+
attribute :updated_at
|
28
|
+
|
29
|
+
def to_param
|
30
|
+
id.to_s
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class TestAPI < Grape::API::Instance
|
35
|
+
format :json
|
36
|
+
|
37
|
+
helpers Grape::App::Helpers::Caching
|
38
|
+
helpers Grape::App::Helpers::Params
|
39
|
+
|
40
|
+
get '/articles' do
|
41
|
+
scope = Article.all
|
42
|
+
fresh_when(scope, public: true)
|
43
|
+
scope.map(&:to_hash)
|
44
|
+
end
|
45
|
+
|
46
|
+
get '/articles/:id' do
|
47
|
+
article = Article.all.first
|
48
|
+
article.to_hash if stale?(article, stale_if_error: 5, extras: { a: 1, b: 2 })
|
49
|
+
end
|
50
|
+
|
51
|
+
params do
|
52
|
+
requires :title
|
53
|
+
optional :fresh
|
54
|
+
end
|
55
|
+
post '/articles' do
|
56
|
+
attrs = { id: 9, updated_at: Time.at(1515151515).utc }
|
57
|
+
attrs.update(declared_params)
|
58
|
+
Article.new(attrs).to_hash
|
59
|
+
end
|
60
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: grape-app
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Black Square Media Ltd
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-11-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -230,6 +230,7 @@ files:
|
|
230
230
|
- lib/grape/app/cli.rb
|
231
231
|
- lib/grape/app/configuration.rb
|
232
232
|
- lib/grape/app/helpers.rb
|
233
|
+
- lib/grape/app/helpers/caching.rb
|
233
234
|
- lib/grape/app/helpers/params.rb
|
234
235
|
- lib/grape/app/helpers/respond_with.rb
|
235
236
|
- lib/grape/app/inflector.rb
|
@@ -252,6 +253,8 @@ files:
|
|
252
253
|
- lib/grape/app/templates/db/seeds.rb
|
253
254
|
- lib/grape/app/templates/spec/spec_helper.rb
|
254
255
|
- lib/grape_app.rb
|
256
|
+
- spec/grape/app/helpers/caching_spec.rb
|
257
|
+
- spec/grape/app/helpers/params_spec.rb
|
255
258
|
- spec/grape/app_spec.rb
|
256
259
|
- spec/scenario/Gemfile
|
257
260
|
- spec/scenario/app/api.rb
|
@@ -281,11 +284,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
281
284
|
- !ruby/object:Gem::Version
|
282
285
|
version: '0'
|
283
286
|
requirements: []
|
284
|
-
rubygems_version: 3.0.
|
287
|
+
rubygems_version: 3.0.6
|
285
288
|
signing_key:
|
286
289
|
specification_version: 4
|
287
290
|
summary: Stanalone Grape API apps
|
288
291
|
test_files:
|
292
|
+
- spec/grape/app/helpers/caching_spec.rb
|
293
|
+
- spec/grape/app/helpers/params_spec.rb
|
289
294
|
- spec/grape/app_spec.rb
|
290
295
|
- spec/scenario/Gemfile
|
291
296
|
- spec/scenario/app/api.rb
|