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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbe3e3723dc43a07adf70cb9803a3f679930d6de5e3ddeeb9737e4ce4547898a
4
- data.tar.gz: 1d5b409da0440ae34f48967f5a3fb3845ff542f20c160d561911f63b7f6a968c
3
+ metadata.gz: 77187b9f038601c08edaa41385655236b13907e88a22baa30cdc037641ebb214
4
+ data.tar.gz: c21cce56ec2393c67ef7885c01c187641205367b44b2624c0944b560f6aeb954
5
5
  SHA512:
6
- metadata.gz: ed20c0d9a5ed53765e44111ffda80b362c1c2911b4a7f769422e2c67a78741628a91b800003e46b3cff76346282d019343cfa51d898d11d944530b80dcba77a3
7
- data.tar.gz: 2a30feaecf44c728cd1f28d6612aa239fbefd5bb2fad2a6d6ee3ef622af82c9b14ed97751c54e4dbb6697e4c3e71a50dce3d07670883fc4063ec93978145ffc0
6
+ metadata.gz: b29b7f6971e417b1d5d2a0e396eb456c37a5b0cee7ae3b9716faf8adc96a4321df510b9809dd457b5f37e3a70af2ee6e708eb1ffbc140daa5c5455266ad560bb
7
+ data.tar.gz: dd6e2198260a448270026d13776c2fb0b26f8e236349491fbde0438b5495d959a044361555efc6e7a03bffcf7e4c0f88d0aab0397870ec01e70253ac68fd726a
@@ -3,3 +3,6 @@ inherit_from:
3
3
 
4
4
  AllCops:
5
5
  TargetRubyVersion: "2.5"
6
+ Metrics/ParameterLists:
7
+ Exclude:
8
+ - lib/grape/app/helpers/caching.rb
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- grape-app (0.8.0)
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.0)
17
- activesupport (= 6.0.0)
18
- activerecord (6.0.0)
19
- activemodel (= 6.0.0)
20
- activesupport (= 6.0.0)
21
- activesupport (6.0.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.1, >= 2.1.8)
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.6.0)
50
+ i18n (1.7.0)
51
51
  concurrent-ruby (~> 1.0)
52
52
  ice_nine (0.11.2)
53
- jaro_winkler (1.5.3)
54
- minitest (5.11.3)
55
- multi_json (1.13.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.17.0)
60
- parser (2.6.3.0)
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 (12.3.3)
71
- rspec (3.8.0)
72
- rspec-core (~> 3.8.0)
73
- rspec-expectations (~> 3.8.0)
74
- rspec-mocks (~> 3.8.0)
75
- rspec-core (3.8.2)
76
- rspec-support (~> 3.8.0)
77
- rspec-expectations (3.8.4)
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.8.0)
80
- rspec-mocks (3.8.1)
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.8.0)
83
- rspec-support (3.8.2)
84
- rubocop (0.74.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.9)
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.1
119
+ 2.0.2
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'grape-app'
3
- s.version = '0.8.0'
3
+ s.version = '0.8.1'
4
4
  s.authors = ['Black Square Media Ltd']
5
5
  s.email = ['info@blacksquaremedia.com']
6
6
  s.summary = %(Stanalone Grape API apps)
@@ -1,6 +1,7 @@
1
1
  module Grape::App::Helpers
2
2
  extend ActiveSupport::Autoload
3
3
 
4
+ autoload :Caching
4
5
  autoload :Params
5
6
  autoload :RespondWith
6
7
  end
@@ -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
@@ -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.0
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-08-27 00:00:00.000000000 Z
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.3
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