grape-app 0.8.0 → 0.8.1
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 +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
|