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 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