gemstack-http 0.2.4 → 0.3.0
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/CHANGELOG.md +2 -22
- data/README.md +6 -18
- data/lib/gemstack-http.rb +5 -0
- metadata +18 -70
- data/lib/gemstack/http/app.rb +0 -30
- data/lib/gemstack/http/config.rb +0 -84
- data/lib/gemstack/http/controller.rb +0 -343
- data/lib/gemstack/http/error_page.rb +0 -111
- data/lib/gemstack/http/error_renderer.rb +0 -63
- data/lib/gemstack/http/json_codec.rb +0 -114
- data/lib/gemstack/http/middleware/body_limit.rb +0 -69
- data/lib/gemstack/http/middleware/compression.rb +0 -127
- data/lib/gemstack/http/middleware/cors.rb +0 -77
- data/lib/gemstack/http/middleware/error_handler.rb +0 -39
- data/lib/gemstack/http/middleware/etags.rb +0 -24
- data/lib/gemstack/http/middleware/health_check.rb +0 -28
- data/lib/gemstack/http/middleware/request_id.rb +0 -31
- data/lib/gemstack/http/middleware/request_logger.rb +0 -39
- data/lib/gemstack/http/middleware/security_headers.rb +0 -31
- data/lib/gemstack/http/middleware_stack.rb +0 -96
- data/lib/gemstack/http/page.rb +0 -36
- data/lib/gemstack/http/params.rb +0 -140
- data/lib/gemstack/http/request.rb +0 -64
- data/lib/gemstack/http/router.rb +0 -315
- data/lib/gemstack/http.rb +0 -41
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76c2d6bc8341e9d2009eeabf8e1da44fc25fa8aece61edc88a18138a16441090
|
|
4
|
+
data.tar.gz: 4b6e6bb5adab8141ed61d05042004684a91293f4d1ee3574ddafc6c5e68f5a08
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a1d7fd696f8113164fd6df369218bb6c870dea2704e4165984281ac23dfba1165a4c08ee6e3fb2fbc96a756352eb460f152be162183fd093ecba36fa83c6a12
|
|
7
|
+
data.tar.gz: e653dbef0def88b2cc877ffd1ff7725bd0ff57ca207798a5803d0e1dfc31360effaf2762196124338c53c458d08d320b5d39f0523fc586332208b648ca74a0d9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,25 +1,5 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.3.0
|
|
4
4
|
|
|
5
|
-
See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
|
|
6
|
-
|
|
7
|
-
## 0.2.3
|
|
8
|
-
|
|
9
|
-
See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
|
|
10
|
-
|
|
11
|
-
## 0.2.2
|
|
12
|
-
|
|
13
|
-
See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
|
|
14
|
-
|
|
15
|
-
## 0.2.1
|
|
16
|
-
|
|
17
|
-
See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
|
|
18
|
-
|
|
19
|
-
## 0.2.0
|
|
20
|
-
|
|
21
|
-
See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
|
|
22
|
-
|
|
23
|
-
## 0.1.0
|
|
24
|
-
|
|
25
|
-
First release. See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
|
|
5
|
+
Merged into the gemstack gem; this version is a transition shim. See the [GemStack changelog](https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md).
|
data/README.md
CHANGED
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
# gemstack-http
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Merged into [`gemstack`](https://rubygems.org/gems/gemstack) in GemStack 0.3.0.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This version is a transition shim: it depends on `gemstack` and loads `gemstack/http`, so
|
|
6
|
+
Gemfiles that still list `gemstack-http` keep working. To finish upgrading, remove `gem "gemstack-http"`
|
|
7
|
+
from your Gemfile (it's loaded by `require "gemstack"`).
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
version.
|
|
10
|
-
|
|
11
|
-
## Installation
|
|
12
|
-
|
|
13
|
-
Installed with the `gemstack` gem; you rarely need to add it yourself.
|
|
14
|
-
|
|
15
|
-
## Documentation
|
|
16
|
-
|
|
17
|
-
- [Guide](https://github.com/gemstack-rb/gemstack/blob/main/docs/controllers.md)
|
|
18
|
-
- [All guides](https://github.com/gemstack-rb/gemstack/tree/main/docs) ·
|
|
19
|
-
[Architecture](https://github.com/gemstack-rb/gemstack/blob/main/ARCHITECTURE.md)
|
|
20
|
-
|
|
21
|
-
Source, issues and pull requests: [gemstack-rb/gemstack](https://github.com/gemstack-rb/gemstack)
|
|
22
|
-
(this gem lives in `gems/gemstack-http`).
|
|
9
|
+
GemStack is a modular Ruby API framework for Next.js applications by
|
|
10
|
+
[Adware Technologies](https://www.adwaretech.com) — [gemstack-rb/gemstack](https://github.com/gemstack-rb/gemstack).
|
|
23
11
|
|
|
24
12
|
## License
|
|
25
13
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gemstack-http
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adware Technologies
|
|
@@ -11,62 +11,28 @@ cert_chain: []
|
|
|
11
11
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: gemstack
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - '='
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.2.4
|
|
20
|
-
type: :runtime
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - '='
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.2.4
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: gemstack-schema
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - '='
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: 0.2.4
|
|
34
|
-
type: :runtime
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - '='
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: 0.2.4
|
|
41
|
-
- !ruby/object:Gem::Dependency
|
|
42
|
-
name: json
|
|
14
|
+
name: gemstack
|
|
43
15
|
requirement: !ruby/object:Gem::Requirement
|
|
44
16
|
requirements:
|
|
45
17
|
- - ">="
|
|
46
18
|
- !ruby/object:Gem::Version
|
|
47
|
-
version:
|
|
19
|
+
version: 0.3.0
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '1.0'
|
|
48
23
|
type: :runtime
|
|
49
24
|
prerelease: false
|
|
50
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
26
|
requirements:
|
|
52
27
|
- - ">="
|
|
53
28
|
- !ruby/object:Gem::Version
|
|
54
|
-
version:
|
|
55
|
-
-
|
|
56
|
-
name: rack
|
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
|
58
|
-
requirements:
|
|
59
|
-
- - "~>"
|
|
60
|
-
- !ruby/object:Gem::Version
|
|
61
|
-
version: '3.1'
|
|
62
|
-
type: :runtime
|
|
63
|
-
prerelease: false
|
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
-
requirements:
|
|
66
|
-
- - "~>"
|
|
29
|
+
version: 0.3.0
|
|
30
|
+
- - "<"
|
|
67
31
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
69
|
-
description:
|
|
32
|
+
version: '1.0'
|
|
33
|
+
description: Since GemStack 0.3.0, gemstack-http is part of the gemstack gem. This
|
|
34
|
+
version only depends on gemstack and loads gemstack/http, so Gemfiles that still
|
|
35
|
+
list it keep working.
|
|
70
36
|
email:
|
|
71
37
|
- gemstack26@gmail.com
|
|
72
38
|
executables: []
|
|
@@ -76,36 +42,18 @@ files:
|
|
|
76
42
|
- CHANGELOG.md
|
|
77
43
|
- LICENSE.txt
|
|
78
44
|
- README.md
|
|
79
|
-
- lib/gemstack
|
|
80
|
-
- lib/gemstack/http/app.rb
|
|
81
|
-
- lib/gemstack/http/config.rb
|
|
82
|
-
- lib/gemstack/http/controller.rb
|
|
83
|
-
- lib/gemstack/http/error_page.rb
|
|
84
|
-
- lib/gemstack/http/error_renderer.rb
|
|
85
|
-
- lib/gemstack/http/json_codec.rb
|
|
86
|
-
- lib/gemstack/http/middleware/body_limit.rb
|
|
87
|
-
- lib/gemstack/http/middleware/compression.rb
|
|
88
|
-
- lib/gemstack/http/middleware/cors.rb
|
|
89
|
-
- lib/gemstack/http/middleware/error_handler.rb
|
|
90
|
-
- lib/gemstack/http/middleware/etags.rb
|
|
91
|
-
- lib/gemstack/http/middleware/health_check.rb
|
|
92
|
-
- lib/gemstack/http/middleware/request_id.rb
|
|
93
|
-
- lib/gemstack/http/middleware/request_logger.rb
|
|
94
|
-
- lib/gemstack/http/middleware/security_headers.rb
|
|
95
|
-
- lib/gemstack/http/middleware_stack.rb
|
|
96
|
-
- lib/gemstack/http/page.rb
|
|
97
|
-
- lib/gemstack/http/params.rb
|
|
98
|
-
- lib/gemstack/http/request.rb
|
|
99
|
-
- lib/gemstack/http/router.rb
|
|
45
|
+
- lib/gemstack-http.rb
|
|
100
46
|
homepage: https://github.com/gemstack-rb/gemstack
|
|
101
47
|
licenses:
|
|
102
48
|
- MIT
|
|
103
49
|
metadata:
|
|
104
50
|
rubygems_mfa_required: 'true'
|
|
105
|
-
source_code_uri: https://github.com/gemstack-rb/gemstack
|
|
106
|
-
changelog_uri: https://github.com/gemstack-rb/gemstack/blob/main/
|
|
51
|
+
source_code_uri: https://github.com/gemstack-rb/gemstack
|
|
52
|
+
changelog_uri: https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md
|
|
107
53
|
bug_tracker_uri: https://github.com/gemstack-rb/gemstack/issues
|
|
108
54
|
documentation_uri: https://github.com/gemstack-rb/gemstack/tree/main/docs
|
|
55
|
+
post_install_message: gemstack-http is now part of the gemstack gem. Remove `gem "gemstack-http"`
|
|
56
|
+
from your Gemfile (it's loaded by `require "gemstack"`).
|
|
109
57
|
rdoc_options: []
|
|
110
58
|
require_paths:
|
|
111
59
|
- lib
|
|
@@ -122,5 +70,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
122
70
|
requirements: []
|
|
123
71
|
rubygems_version: 4.0.20
|
|
124
72
|
specification_version: 4
|
|
125
|
-
summary:
|
|
73
|
+
summary: Merged into the gemstack gem — remove gemstack-http from your Gemfile
|
|
126
74
|
test_files: []
|
data/lib/gemstack/http/app.rb
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module GemStack
|
|
4
|
-
module HTTP
|
|
5
|
-
# The Rack application: the compiled middleware stack in front of the
|
|
6
|
-
# router. Built once; the router can be replaced (for reloading) without
|
|
7
|
-
# rebuilding the stack.
|
|
8
|
-
class App
|
|
9
|
-
attr_reader :config, :codec
|
|
10
|
-
attr_accessor :router
|
|
11
|
-
|
|
12
|
-
def initialize(config:, router:)
|
|
13
|
-
@config = config
|
|
14
|
-
@router = router
|
|
15
|
-
@codec = JSONCodec.resolve(config.json, max_nesting: config.json_max_nesting)
|
|
16
|
-
@stack = config.middleware.build(method(:endpoint))
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def call(env) = @stack.call(env)
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
def endpoint(env)
|
|
24
|
-
env[JSON_CODEC] = @codec
|
|
25
|
-
env[CONFIG] = @config
|
|
26
|
-
@router.call(env)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
data/lib/gemstack/http/config.rb
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module GemStack
|
|
4
|
-
module HTTP
|
|
5
|
-
# `config.http` — every setting has a production-ready default.
|
|
6
|
-
class Config < Settings
|
|
7
|
-
# Where the API lives. Routes are declared relative to it, and the dev
|
|
8
|
-
# gateway uses it to send traffic to Ruby. "" mounts the API at the root.
|
|
9
|
-
setting :api_path, default: "/api"
|
|
10
|
-
|
|
11
|
-
# Liveness endpoint answered by HealthCheck. nil disables it.
|
|
12
|
-
setting :health_path, default: -> { "#{api_path}/health" }
|
|
13
|
-
|
|
14
|
-
# Maximum request body size in bytes (413 beyond it).
|
|
15
|
-
setting :max_body_size, default: 10 * 1024 * 1024
|
|
16
|
-
|
|
17
|
-
# JSON codec: :json (stdlib, default), :oj, or any object responding to
|
|
18
|
-
# dump(object) -> String and load(String) -> object.
|
|
19
|
-
setting :json, default: :json
|
|
20
|
-
|
|
21
|
-
# Maximum nesting depth accepted when parsing JSON request bodies.
|
|
22
|
-
setting :json_max_nesting, default: 64
|
|
23
|
-
|
|
24
|
-
# Include exception class, message and backtrace in 500 responses.
|
|
25
|
-
# Never enable in production.
|
|
26
|
-
setting :show_exceptions, default: -> { GemStack.env.local? }
|
|
27
|
-
|
|
28
|
-
# Accept X-Request-Id from clients/proxies (validated) instead of
|
|
29
|
-
# always generating one.
|
|
30
|
-
setting :trust_request_id, default: true
|
|
31
|
-
|
|
32
|
-
# Headers added to every response unless the response already sets them.
|
|
33
|
-
setting :security_headers, default: {
|
|
34
|
-
"x-content-type-options" => "nosniff",
|
|
35
|
-
"x-frame-options" => "DENY",
|
|
36
|
-
"referrer-policy" => "strict-origin-when-cross-origin",
|
|
37
|
-
"cross-origin-opener-policy" => "same-origin",
|
|
38
|
-
"content-security-policy" => "default-src 'none'; frame-ancestors 'none'"
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
# Strict-Transport-Security for HTTPS requests. nil disables.
|
|
42
|
-
setting :hsts, default: -> { GemStack.env.production? ? "max-age=63072000; includeSubDomains" : nil }
|
|
43
|
-
|
|
44
|
-
# Cross-origin access. Off by default: same-origin apps need none.
|
|
45
|
-
namespace :cors do
|
|
46
|
-
# Allowed origins: exact strings, Regexps, or "*".
|
|
47
|
-
setting :origins, default: []
|
|
48
|
-
setting :methods, default: %w[GET POST PUT PATCH DELETE OPTIONS]
|
|
49
|
-
setting :headers, default: %w[content-type authorization x-request-id]
|
|
50
|
-
setting :expose_headers, default: %w[x-request-id]
|
|
51
|
-
setting :credentials, default: false
|
|
52
|
-
setting :max_age, default: 600
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Response compression (Middleware::Compression).
|
|
56
|
-
namespace :compression do
|
|
57
|
-
setting :enabled, default: true
|
|
58
|
-
# Bodies smaller than this are sent as they are (compression overhead
|
|
59
|
-
# outweighs the savings on tiny responses).
|
|
60
|
-
setting :min_size, default: 1024
|
|
61
|
-
# Server preference; "br" is used only when the brotli gem is installed.
|
|
62
|
-
setting :encodings, default: %w[br gzip]
|
|
63
|
-
# Measured on varied JSON (docs/performance.md): gzip 4 costs half the
|
|
64
|
-
# CPU of gzip 6 for ~2% larger output; Brotli 4 is smaller than gzip 4 and cheaper.
|
|
65
|
-
setting :brotli_quality, default: 4
|
|
66
|
-
setting :gzip_level, default: 4
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# ETags for GET/HEAD responses and 304 Not Modified (Rack::ETag + Rack::ConditionalGet).
|
|
70
|
-
setting :etags, default: true
|
|
71
|
-
|
|
72
|
-
# Defaults for `paginate` in controllers.
|
|
73
|
-
namespace :pagination do
|
|
74
|
-
setting :per_page, default: 25
|
|
75
|
-
setting :max_per_page, default: 100
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# The middleware stack. Edit it with use / insert_before / insert_after /
|
|
79
|
-
# swap / delete. Middleware receive this config and read it when the
|
|
80
|
-
# application is built, so settings may be changed in any order.
|
|
81
|
-
setting :middleware, default: -> { MiddlewareStack.default(self) }
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "digest"
|
|
4
|
-
require "time"
|
|
5
|
-
|
|
6
|
-
module GemStack
|
|
7
|
-
module HTTP
|
|
8
|
-
# Base class for API controllers.
|
|
9
|
-
#
|
|
10
|
-
# class ProductsController < ApplicationController
|
|
11
|
-
# before :load_product, only: %i[show update]
|
|
12
|
-
# rescue_from Payments::Declined, status: 402
|
|
13
|
-
#
|
|
14
|
-
# def index = render(Product.all)
|
|
15
|
-
# def show = render(@product)
|
|
16
|
-
#
|
|
17
|
-
# def create
|
|
18
|
-
# product = Product.create!(params.require(:product).permit(:name, :price))
|
|
19
|
-
# render product, status: :created
|
|
20
|
-
# end
|
|
21
|
-
#
|
|
22
|
-
# private
|
|
23
|
-
#
|
|
24
|
-
# def load_product = @product = Product.find(params[:id])
|
|
25
|
-
# end
|
|
26
|
-
#
|
|
27
|
-
# Actions are the public methods defined in subclasses. An action that
|
|
28
|
-
# doesn't render responds 204 No Content. A `before` callback that
|
|
29
|
-
# renders halts the chain and the action is skipped.
|
|
30
|
-
class Controller
|
|
31
|
-
class DoubleRenderError < Error; end
|
|
32
|
-
class ActionNotFound < Error; end
|
|
33
|
-
|
|
34
|
-
Callback = Struct.new(:callback_method, :block, :only, :except) do
|
|
35
|
-
def applies?(action)
|
|
36
|
-
(only.nil? || only.include?(action)) && (except.nil? || !except.include?(action))
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
RescueHandler = Struct.new(:classes, :handler, :status, :code)
|
|
41
|
-
|
|
42
|
-
class << self
|
|
43
|
-
def before_callbacks = @before_callbacks ||= inherited_copy(:before_callbacks)
|
|
44
|
-
def after_callbacks = @after_callbacks ||= inherited_copy(:after_callbacks)
|
|
45
|
-
def rescue_handlers = @rescue_handlers ||= inherited_copy(:rescue_handlers)
|
|
46
|
-
|
|
47
|
-
# before :authenticate, only: %i[create update]
|
|
48
|
-
# before { render({ error: "..." }, status: 401) unless ok? }
|
|
49
|
-
def before(*methods, only: nil, except: nil, &block)
|
|
50
|
-
add_callbacks(before_callbacks, methods, block, only, except)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def after(*methods, only: nil, except: nil, &block)
|
|
54
|
-
add_callbacks(after_callbacks, methods, block, only, except)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def skip_before(*methods)
|
|
58
|
-
names = methods.map(&:to_sym)
|
|
59
|
-
@before_callbacks = before_callbacks.reject { |cb| names.include?(cb.callback_method) }
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# rescue_from Stripe::CardError, with: :card_declined
|
|
63
|
-
# rescue_from Timeout::Error, status: 504, code: "upstream_timeout"
|
|
64
|
-
# rescue_from(MyError) { |error| render({ message: error.message }, status: 400) }
|
|
65
|
-
# Later declarations take precedence (they are checked first).
|
|
66
|
-
def rescue_from(*classes, with: nil, status: nil, code: nil, &block)
|
|
67
|
-
handler = with || block
|
|
68
|
-
raise ArgumentError, "rescue_from needs with:, a block, or status:" unless handler || status
|
|
69
|
-
|
|
70
|
-
rescue_handlers.unshift(RescueHandler.new(classes, handler, status, code))
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Declares the request schema for actions; `input` returns the
|
|
74
|
-
# validated, coerced data inside those actions (DECISIONS D-021).
|
|
75
|
-
#
|
|
76
|
-
# accepts :create, with: Product.input_schema
|
|
77
|
-
# accepts :update, with: Product.input_schema, partial: true
|
|
78
|
-
# accepts(:search) { required :q, :string }
|
|
79
|
-
def accepts(*actions, with: nil, partial: false, &)
|
|
80
|
-
schema = with || Schema.define(&)
|
|
81
|
-
raise ArgumentError, "accepts needs with: SchemaClass or a block" unless schema
|
|
82
|
-
|
|
83
|
-
schema = schema.partial if partial
|
|
84
|
-
actions.each { |action| input_schemas[action.to_s] = schema }
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def input_schemas = @input_schemas ||= inherited_copy(:input_schemas, {})
|
|
88
|
-
|
|
89
|
-
# Declares an action's response type for the API contract when the
|
|
90
|
-
# convention (<Resource>Serializer, see docs/typescript.md) doesn't apply:
|
|
91
|
-
# returns :search, [ProductSerializer]
|
|
92
|
-
# returns :stats, StatsSerializer
|
|
93
|
-
# returns :ping, nil # no body
|
|
94
|
-
def returns(*actions, type)
|
|
95
|
-
actions.each { |action| response_types[action.to_s] = type }
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def response_types = @response_types ||= inherited_copy(:response_types, {})
|
|
99
|
-
|
|
100
|
-
# Public instance methods added by subclasses of Controller.
|
|
101
|
-
def action_methods
|
|
102
|
-
@action_methods ||= (public_instance_methods(true) - Controller.public_instance_methods(true))
|
|
103
|
-
.to_set(&:to_s).freeze
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def method_added(name)
|
|
107
|
-
super
|
|
108
|
-
@action_methods = nil
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def dispatch(action, env)
|
|
112
|
-
raise ActionNotFound, "#{name}##{action} is not a public action" unless action_methods.include?(action)
|
|
113
|
-
|
|
114
|
-
new(env).process(action)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
private
|
|
118
|
-
|
|
119
|
-
def inherited_copy(name, empty = [])
|
|
120
|
-
superclass.respond_to?(name) ? superclass.public_send(name).dup : empty
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def add_callbacks(list, methods, block, only, except)
|
|
124
|
-
only &&= Array(only).map(&:to_s)
|
|
125
|
-
except &&= Array(except).map(&:to_s)
|
|
126
|
-
methods.each { |m| list << Callback.new(m.to_sym, nil, only, except) }
|
|
127
|
-
list << Callback.new(nil, block, only, except) if block
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
attr_reader :env, :request, :action_name
|
|
132
|
-
|
|
133
|
-
def initialize(env)
|
|
134
|
-
@env = env
|
|
135
|
-
@request = Request.new(env)
|
|
136
|
-
@response = nil
|
|
137
|
-
@response_headers = {}
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def params
|
|
141
|
-
@params ||= Params.new(request.all_params)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# The validated input for the current action, as declared with `accepts`.
|
|
145
|
-
def input
|
|
146
|
-
@input ||= begin
|
|
147
|
-
schema = self.class.input_schemas[action_name] or
|
|
148
|
-
raise Error, "#{self.class.name}##{action_name} has no `accepts` declaration; use params.validate instead"
|
|
149
|
-
schema.call(params)
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Headers to add to the response. Set them before or after rendering.
|
|
154
|
-
def headers = @response_headers
|
|
155
|
-
|
|
156
|
-
def logger = GemStack.logger
|
|
157
|
-
|
|
158
|
-
def rendered? = !@response.nil?
|
|
159
|
-
|
|
160
|
-
# Renders value as JSON.
|
|
161
|
-
# render product
|
|
162
|
-
# render products, status: :ok
|
|
163
|
-
# render({ ok: true }, status: 202, headers: { "cache-control" => "no-store" })
|
|
164
|
-
# render product, serializer: Admin::ProductSerializer
|
|
165
|
-
def render(value = nil, status: 200, headers: {}, serializer: nil)
|
|
166
|
-
raise DoubleRenderError, "render/head called twice in #{self.class.name}##{action_name}" if rendered?
|
|
167
|
-
|
|
168
|
-
@response_headers.merge!(headers)
|
|
169
|
-
@response_headers["content-type"] ||= "application/json; charset=utf-8"
|
|
170
|
-
body = serializer ? apply_serializer(serializer, value) : serialize(value)
|
|
171
|
-
@response = [status_code(status), [codec.dump(body)]]
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Responds without a body: head :no_content, head 404
|
|
175
|
-
def head(status, headers = {})
|
|
176
|
-
raise DoubleRenderError, "render/head called twice in #{self.class.name}##{action_name}" if rendered?
|
|
177
|
-
|
|
178
|
-
@response_headers.merge!(headers)
|
|
179
|
-
@response = [status_code(status), []]
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Turns domain objects into JSON-ready structures. By convention an
|
|
183
|
-
# object of class Product is rendered with ProductSerializer (also for
|
|
184
|
-
# arrays and datasets of them). Values without a serializer go to the
|
|
185
|
-
# JSON codec as they are. Override for custom behaviour.
|
|
186
|
-
def serialize(value)
|
|
187
|
-
return { data: serialize(value.items), meta: value.meta } if value.is_a?(HTTP::Page)
|
|
188
|
-
|
|
189
|
-
Serializer.render(value, serializer_context)
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
# Paginates a dataset (or array) using the `page` and `per_page` query
|
|
193
|
-
# parameters, bounded by config.http.pagination:
|
|
194
|
-
#
|
|
195
|
-
# render paginate(Product.order(:id))
|
|
196
|
-
# render paginate(Product.where(active: true).order(:name), per_page: 50)
|
|
197
|
-
#
|
|
198
|
-
# Invalid values are a 422 with field errors. The dataset should be
|
|
199
|
-
# ordered, or pages may overlap.
|
|
200
|
-
def paginate(scope, per_page: nil)
|
|
201
|
-
settings = pagination_settings
|
|
202
|
-
max = settings.max_per_page
|
|
203
|
-
page, size = PAGE_SCHEMA.call(params).values_at(:page, :per_page)
|
|
204
|
-
size = (size || per_page || settings.per_page).clamp(1, max)
|
|
205
|
-
page ||= 1
|
|
206
|
-
total = scope.count
|
|
207
|
-
offset = (page - 1) * size
|
|
208
|
-
items = scope.is_a?(Array) ? scope[offset, size] || [] : scope.limit(size).offset(offset).all
|
|
209
|
-
HTTP::Page.new(items, page: page, per_page: size, total: total)
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
PAGE_SCHEMA = Schema.define do
|
|
213
|
-
optional :page, :integer, gte: 1
|
|
214
|
-
optional :per_page, :integer, gte: 1
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# HTTP caching. Sets ETag / Last-Modified and answers 304 Not Modified
|
|
218
|
-
# (without rendering) when the client already has this version:
|
|
219
|
-
#
|
|
220
|
-
# def show
|
|
221
|
-
# product = Product.find(params[:id])
|
|
222
|
-
# render product if stale?(etag: product, last_modified: product.updated_at)
|
|
223
|
-
# end
|
|
224
|
-
#
|
|
225
|
-
# etag: any value; records use #cache_key when they have one.
|
|
226
|
-
def stale?(etag: nil, last_modified: nil)
|
|
227
|
-
headers["etag"] = %(W/"#{Digest::SHA256.hexdigest(etag_source(etag))[0, 32]}") unless etag.nil?
|
|
228
|
-
headers["last-modified"] = last_modified.httpdate if last_modified
|
|
229
|
-
return true unless fresh?
|
|
230
|
-
|
|
231
|
-
head :not_modified
|
|
232
|
-
false
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def fresh_when(**) = stale?(**)
|
|
236
|
-
|
|
237
|
-
# cache_control max_age: 60 # private, max-age=60
|
|
238
|
-
# cache_control max_age: 300, public: true, stale_while_revalidate: 30
|
|
239
|
-
# cache_control :no_store
|
|
240
|
-
def cache_control(directive = nil, max_age: nil, public: false, stale_while_revalidate: nil)
|
|
241
|
-
headers["cache-control"] =
|
|
242
|
-
if directive == :no_store then "no-store"
|
|
243
|
-
else
|
|
244
|
-
[public ? "public" : "private", ("max-age=#{Integer(max_age)}" if max_age),
|
|
245
|
-
("stale-while-revalidate=#{Integer(stale_while_revalidate)}" if stale_while_revalidate)].compact.join(", ")
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# Passed to serializers as `context` (e.g. { current_user: current_user }).
|
|
250
|
-
def serializer_context = {}
|
|
251
|
-
|
|
252
|
-
def process(action)
|
|
253
|
-
@action_name = action
|
|
254
|
-
run_callbacks(self.class.before_callbacks)
|
|
255
|
-
public_send(action) unless rendered?
|
|
256
|
-
run_callbacks(self.class.after_callbacks)
|
|
257
|
-
finish
|
|
258
|
-
rescue StandardError => e
|
|
259
|
-
handle_exception(e)
|
|
260
|
-
finish
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
private
|
|
264
|
-
|
|
265
|
-
def finish
|
|
266
|
-
head(:no_content) unless rendered?
|
|
267
|
-
status, body = @response
|
|
268
|
-
[status, @response_headers, body]
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def run_callbacks(callbacks)
|
|
272
|
-
callbacks.each do |callback|
|
|
273
|
-
next unless callback.applies?(@action_name)
|
|
274
|
-
|
|
275
|
-
callback.callback_method ? send(callback.callback_method) : instance_exec(&callback.block)
|
|
276
|
-
break if rendered? && callbacks.equal?(self.class.before_callbacks)
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def handle_exception(error)
|
|
281
|
-
rescuer = self.class.rescue_handlers.find { |h| h.classes.any? { |klass| error.is_a?(klass) } }
|
|
282
|
-
raise error unless rescuer
|
|
283
|
-
|
|
284
|
-
@response = nil
|
|
285
|
-
@response_headers = @response_headers.slice("x-request-id")
|
|
286
|
-
if rescuer.handler
|
|
287
|
-
rescuer.handler.is_a?(Proc) ? instance_exec(error, &rescuer.handler) : call_handler(rescuer.handler, error)
|
|
288
|
-
else
|
|
289
|
-
status = status_code(rescuer.status)
|
|
290
|
-
wrapped = GemStack::Error.new(error.message, status: status,
|
|
291
|
-
code: rescuer.code || ErrorRenderer.code_for(status))
|
|
292
|
-
status, headers, body = ErrorRenderer.render(wrapped, request_id: request.request_id)
|
|
293
|
-
@response_headers.merge!(headers)
|
|
294
|
-
@response = [status, body]
|
|
295
|
-
end
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
def pagination_settings = (env[CONFIG] || Config.new).pagination
|
|
299
|
-
|
|
300
|
-
def etag_source(value)
|
|
301
|
-
case value
|
|
302
|
-
when Array then value.map { |v| etag_source(v) }.join("/")
|
|
303
|
-
else value.respond_to?(:cache_key) ? value.cache_key.to_s : value.to_s
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
def fresh?
|
|
308
|
-
none_match = request.get_header("HTTP_IF_NONE_MATCH")
|
|
309
|
-
etag = headers["etag"]
|
|
310
|
-
if none_match && etag
|
|
311
|
-
tags = none_match.split(",").map { |t| t.strip.delete_prefix("W/") }
|
|
312
|
-
return tags.include?("*") || tags.include?(etag.delete_prefix("W/"))
|
|
313
|
-
end
|
|
314
|
-
modified_since = request.get_header("HTTP_IF_MODIFIED_SINCE")
|
|
315
|
-
last_modified = headers["last-modified"]
|
|
316
|
-
return false unless modified_since && last_modified
|
|
317
|
-
|
|
318
|
-
Time.httpdate(last_modified) <= Time.httpdate(modified_since)
|
|
319
|
-
rescue ArgumentError
|
|
320
|
-
false
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
def apply_serializer(serializer, value)
|
|
324
|
-
return { data: serializer.many(value.items, serializer_context), meta: value.meta } if value.is_a?(HTTP::Page)
|
|
325
|
-
|
|
326
|
-
list = value.is_a?(Array) || (value.respond_to?(:all) && value.respond_to?(:model))
|
|
327
|
-
list ? serializer.many(value, serializer_context) : serializer.serialize(value, serializer_context)
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def call_handler(name, error)
|
|
331
|
-
method(name).arity.zero? ? send(name) : send(name, error)
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
def status_code(status)
|
|
335
|
-
return status if status.is_a?(Integer)
|
|
336
|
-
|
|
337
|
-
Rack::Utils::SYMBOL_TO_STATUS_CODE.fetch(status.to_sym) { raise ArgumentError, "unknown status #{status}" }
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
def codec = env[JSON_CODEC] || JSONCodec.default
|
|
341
|
-
end
|
|
342
|
-
end
|
|
343
|
-
end
|