power-compass 0.9.1 → 1.0.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/app/components/compass/layout/breadcrumbs.rb +21 -0
- data/app/components/concerns/compass/config_props.rb +2 -1
- data/app/controllers/compass/application_controller.rb +4 -7
- data/app/controllers/compass/info_controller.rb +2 -2
- data/app/controllers/concerns/compass/conditional_cache.rb +2 -8
- data/lib/compass/breadcrumb/strategies/cookie.rb +106 -0
- data/lib/compass/breadcrumb/strategies/http.rb +147 -0
- data/lib/compass/breadcrumb.rb +15 -11
- data/lib/compass/configuration/breadcrumb.rb +10 -7
- data/lib/compass/configuration/client.rb +0 -13
- data/lib/compass/configuration.rb +7 -12
- data/lib/compass/context.rb +93 -0
- data/lib/compass/engine.rb +1 -1
- data/lib/compass/middleware.rb +44 -0
- data/lib/compass/must_implement.rb +7 -0
- data/lib/compass/notification/provider.rb +1 -4
- data/lib/compass/search/provider.rb +1 -6
- data/lib/compass/version.rb +1 -1
- data/lib/compass.rb +3 -0
- metadata +6 -2
- data/lib/compass/breadcrumb/middleware.rb +0 -44
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf61a684e515eebb2ffbc60245c9a6749a06ef0f9437227f8dc612b73ed078d0
|
|
4
|
+
data.tar.gz: 9113182a39604aa267b9efcd91f95a543e59e3c8e1d7415da8daef2f64bfedaf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b306a91383a0af5789beddc4dbe311b2ea83f9fc09aa3dd86ffb8bd792175d1897569c04a0bb12b0122d9e2d67621a8a4e1b7c22864ae1ce348173b94608e08a
|
|
7
|
+
data.tar.gz: 23f9a16ec5815ca38e9bc3373a66650aabc6150ef49beab94a0730a6dec4dc9bd8d0788bb40264be4d9b2986d520379b120b2bdadf529ca1e71db0b4641b1abb
|
|
@@ -4,6 +4,27 @@ module Compass
|
|
|
4
4
|
class Layout
|
|
5
5
|
class Breadcrumbs < ApplicationComponent
|
|
6
6
|
prop :breadcrumbs
|
|
7
|
+
prop :context
|
|
8
|
+
prop :max, default: 10
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# Resolves the breadcrumb trail. An explicit +breadcrumbs:+ prop wins;
|
|
13
|
+
# otherwise the configured strategy is queried via +Compass::Breadcrumb.for+.
|
|
14
|
+
# Memoized so the template's repeated reads only resolve once per render.
|
|
15
|
+
def breadcrumbs
|
|
16
|
+
@resolved_breadcrumbs ||=
|
|
17
|
+
prop(:breadcrumbs).presence ||
|
|
18
|
+
Compass::Breadcrumb.for(context: resolved_context, max: prop(:max)) ||
|
|
19
|
+
[]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# An explicit +context:+ prop wins; otherwise build the request-scoped
|
|
23
|
+
# context the same way the controllers do.
|
|
24
|
+
def resolved_context
|
|
25
|
+
# bearer:disable ruby_rails_sql_injection
|
|
26
|
+
prop(:context) || Compass::Context.from(request.env)
|
|
27
|
+
end
|
|
7
28
|
end
|
|
8
29
|
end
|
|
9
30
|
end
|
|
@@ -15,7 +15,8 @@ module Compass
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
17
|
def context_id
|
|
18
|
-
|
|
18
|
+
# bearer:disable ruby_rails_sql_injection
|
|
19
|
+
prop(:context_id).presence || Compass::Context.from(request.env).context_id
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def backends
|
|
@@ -7,19 +7,16 @@ module Compass
|
|
|
7
7
|
before_action :validate_context
|
|
8
8
|
|
|
9
9
|
def authenticate
|
|
10
|
-
head(:forbidden) unless
|
|
10
|
+
head(:forbidden) unless current_context.authenticate!
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def validate_context
|
|
14
|
-
head(:bad_request) unless
|
|
14
|
+
head(:bad_request) unless current_context.context_id.to_s.eql?(params[:context_id])
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def current_context
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def current_context_id
|
|
22
|
-
instance_exec(&Compass.config.context_id)
|
|
18
|
+
# bearer:disable ruby_rails_sql_injection
|
|
19
|
+
Compass::Context.from(request.env)
|
|
23
20
|
end
|
|
24
21
|
end
|
|
25
22
|
end
|
|
@@ -13,8 +13,8 @@ module Compass
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def auth
|
|
16
|
-
expiration =
|
|
17
|
-
token =
|
|
16
|
+
expiration = current_context.auth_expires_at
|
|
17
|
+
token = current_context.auth_token
|
|
18
18
|
|
|
19
19
|
if expiration&.future?
|
|
20
20
|
expires_in(expiration - Time.now)
|
|
@@ -6,14 +6,8 @@ module Compass
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
8
|
included do
|
|
9
|
-
etag {
|
|
10
|
-
etag {
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
|
|
14
|
-
def context_modified_at
|
|
15
|
-
instance_exec(&Compass.config.modified_at)
|
|
16
|
-
end
|
|
9
|
+
etag { current_context.context_id }
|
|
10
|
+
etag { current_context.modified_at }
|
|
17
11
|
end
|
|
18
12
|
|
|
19
13
|
def with_cache(items, config, etag:)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Compass
|
|
4
|
+
module Breadcrumb
|
|
5
|
+
module Strategies
|
|
6
|
+
# Stores the breadcrumb trail in a Rails signed cookie.
|
|
7
|
+
#
|
|
8
|
+
# The trail is JSON-encoded inside a *signed* cookie so it cannot be
|
|
9
|
+
# tampered with client-side — the stored URLs are rendered as link hrefs
|
|
10
|
+
# and must be trusted.
|
|
11
|
+
#
|
|
12
|
+
# This is the default strategy. It is stateless and thread-safe: it holds
|
|
13
|
+
# only its constructor options; all per-request data lives on
|
|
14
|
+
# +context.request+.
|
|
15
|
+
#
|
|
16
|
+
# I.e.:
|
|
17
|
+
#
|
|
18
|
+
# config.breadcrumb.strategy = Compass::Breadcrumb::Strategies::Cookie.new(
|
|
19
|
+
# domain: ".example.com"
|
|
20
|
+
# )
|
|
21
|
+
#
|
|
22
|
+
class Cookie
|
|
23
|
+
DEFAULT_NAME = "compass_breadcrumbs".freeze
|
|
24
|
+
DEFAULT_MAX_ENTRIES = 10
|
|
25
|
+
|
|
26
|
+
# @param domain [String, nil] cookie domain. When set, the breadcrumb
|
|
27
|
+
# cookie is shared across that host scope; when omitted, Rails cookie
|
|
28
|
+
# jar defaults apply.
|
|
29
|
+
# @param name [String] cookie name.
|
|
30
|
+
# @param max_entries [Integer] maximum crumbs persisted in the cookie.
|
|
31
|
+
def initialize(domain: nil, name: DEFAULT_NAME, max_entries: DEFAULT_MAX_ENTRIES)
|
|
32
|
+
@domain = domain
|
|
33
|
+
@name = name
|
|
34
|
+
@max_entries = max_entries
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Reads the stored trail.
|
|
38
|
+
#
|
|
39
|
+
# @param context [Compass::Context, nil] the request-scoped context.
|
|
40
|
+
# @param max [Integer] maximum number of crumbs to return.
|
|
41
|
+
# @return [Array<Hash>] up to +max+ crumbs, newest first; +[]+ when there
|
|
42
|
+
# is no request or the cookie is missing/invalid.
|
|
43
|
+
def for(context:, max: DEFAULT_MAX_ENTRIES)
|
|
44
|
+
request = context&.request
|
|
45
|
+
return [] unless request
|
|
46
|
+
|
|
47
|
+
read(request).first(max)
|
|
48
|
+
rescue => e
|
|
49
|
+
Compass.logger&.error "Breadcrumb cookie read error: #{e.message}"
|
|
50
|
+
[]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Prepends a crumb, deduping by label and capping to +max_entries+.
|
|
54
|
+
#
|
|
55
|
+
# No-ops when there is no request or the label is blank.
|
|
56
|
+
#
|
|
57
|
+
# @param context [Compass::Context, nil] the request-scoped context.
|
|
58
|
+
# @param url [String] the crumb URL.
|
|
59
|
+
# @param label [String] the crumb label.
|
|
60
|
+
# @return [nil]
|
|
61
|
+
def push(context:, url:, label:)
|
|
62
|
+
request = context&.request
|
|
63
|
+
return if request.nil? || label.blank?
|
|
64
|
+
|
|
65
|
+
trail = read(request).reject { |crumb| crumb[:label] == label }
|
|
66
|
+
trail.unshift(url: url, label: label)
|
|
67
|
+
|
|
68
|
+
write(request, trail.first(@max_entries))
|
|
69
|
+
nil
|
|
70
|
+
rescue => e
|
|
71
|
+
Compass.logger&.error "Breadcrumb cookie write error: #{e.message}"
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def read(request)
|
|
78
|
+
raw = request.cookie_jar.signed[@name]
|
|
79
|
+
parsed = raw.is_a?(String) ? JSON.parse(raw) : raw
|
|
80
|
+
|
|
81
|
+
Array(parsed).filter_map do |entry|
|
|
82
|
+
next unless entry.respond_to?(:to_h)
|
|
83
|
+
|
|
84
|
+
crumb = entry.to_h.symbolize_keys
|
|
85
|
+
{ url: crumb[:url], label: crumb[:label] } if crumb[:url] || crumb[:label]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def write(request, trail)
|
|
90
|
+
request.cookie_jar.signed[@name] = { value: JSON.dump(trail) }.merge(write_options(request))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def write_options(request)
|
|
94
|
+
options = {
|
|
95
|
+
path: "/",
|
|
96
|
+
httponly: true,
|
|
97
|
+
same_site: :lax,
|
|
98
|
+
secure: request.ssl?
|
|
99
|
+
}
|
|
100
|
+
options[:domain] = @domain if @domain
|
|
101
|
+
options
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "net/http"
|
|
4
|
+
|
|
5
|
+
module Compass
|
|
6
|
+
module Breadcrumb
|
|
7
|
+
module Strategies
|
|
8
|
+
# Stores and retrieves the breadcrumb trail over HTTP from a remote
|
|
9
|
+
# backend, using only Ruby's stdlib +Net::HTTP+.
|
|
10
|
+
#
|
|
11
|
+
# Backend contract (implemented by the remote app, not by this gem). The
|
|
12
|
+
# +backend+ URL is used verbatim as the breadcrumbs endpoint:
|
|
13
|
+
#
|
|
14
|
+
# GET {backend}?max=N -> 200 with JSON [{ "url":, "label": }]
|
|
15
|
+
# POST {backend} -> any 2xx is success; body ignored
|
|
16
|
+
#
|
|
17
|
+
# Auth is taken from the context's +auth_token+ and sent as a bearer token.
|
|
18
|
+
#
|
|
19
|
+
# The strategy is stateless and thread-safe: it holds only configuration
|
|
20
|
+
# (backend URL, timeouts, optional transport). The per-request +#for+ cache
|
|
21
|
+
# lives on +context.request.env+, never on the instance.
|
|
22
|
+
#
|
|
23
|
+
# I.e.:
|
|
24
|
+
#
|
|
25
|
+
# config.breadcrumb.strategy = Compass::Breadcrumb::Strategies::Http.new(
|
|
26
|
+
# backend: "https://my-app.example.com/compass"
|
|
27
|
+
# )
|
|
28
|
+
#
|
|
29
|
+
class Http
|
|
30
|
+
DEFAULT_OPEN_TIMEOUT = 1
|
|
31
|
+
DEFAULT_READ_TIMEOUT = 2
|
|
32
|
+
MEMO_KEY = "compass.breadcrumb.http.for".freeze
|
|
33
|
+
|
|
34
|
+
# @param backend [String] the breadcrumbs endpoint URL, used verbatim.
|
|
35
|
+
# @param open_timeout [Numeric] connection open timeout, in seconds.
|
|
36
|
+
# @param read_timeout [Numeric] response read timeout, in seconds.
|
|
37
|
+
# @param transport [#call, nil] injectable executor for specs; receives
|
|
38
|
+
# +(uri, net_http_request)+ and returns a response responding to
|
|
39
|
+
# +#code+ and +#body+. Defaults to a Net::HTTP-backed transport.
|
|
40
|
+
def initialize(backend:, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT, transport: nil)
|
|
41
|
+
@backend = backend.to_s
|
|
42
|
+
@open_timeout = open_timeout
|
|
43
|
+
@read_timeout = read_timeout
|
|
44
|
+
@transport = transport || method(:net_http_transport)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Fetches the trail, memoized per request so repeated renders on the same
|
|
48
|
+
# request only hit the backend once.
|
|
49
|
+
#
|
|
50
|
+
# @param context [Compass::Context, nil] the request-scoped context.
|
|
51
|
+
# @param max [Integer] maximum number of crumbs to request.
|
|
52
|
+
# @return [Array<Hash>] the crumbs, or +[]+ on any failure.
|
|
53
|
+
def for(context:, max: 10)
|
|
54
|
+
env = context&.request&.env
|
|
55
|
+
return fetch(context, max) unless env
|
|
56
|
+
|
|
57
|
+
return env[MEMO_KEY] if env.key?(MEMO_KEY)
|
|
58
|
+
|
|
59
|
+
env[MEMO_KEY] = fetch(context, max)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Posts a crumb to the backend. The response body is ignored.
|
|
63
|
+
#
|
|
64
|
+
# @param context [Compass::Context, nil] the request-scoped context.
|
|
65
|
+
# @param url [String] the crumb URL.
|
|
66
|
+
# @param label [String] the crumb label.
|
|
67
|
+
# @return [nil]
|
|
68
|
+
def push(context:, url:, label:)
|
|
69
|
+
uri = build_uri
|
|
70
|
+
response = @transport.call(uri, post_request(uri, context, url: url, label: label))
|
|
71
|
+
log_failure(response) unless success?(response)
|
|
72
|
+
nil
|
|
73
|
+
rescue => e
|
|
74
|
+
log(e)
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def fetch(context, max)
|
|
81
|
+
uri = build_uri("max" => max)
|
|
82
|
+
response = @transport.call(uri, get_request(uri, context))
|
|
83
|
+
return [] unless success?(response)
|
|
84
|
+
|
|
85
|
+
parse_list(response.body)
|
|
86
|
+
rescue => e
|
|
87
|
+
log(e)
|
|
88
|
+
[]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_uri(query = nil)
|
|
92
|
+
uri = URI.parse(@backend)
|
|
93
|
+
uri.query = URI.encode_www_form(query) if query
|
|
94
|
+
uri
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def get_request(uri, context)
|
|
98
|
+
apply_headers(Net::HTTP::Get.new(uri), context)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def post_request(uri, context, url:, label:)
|
|
102
|
+
request = apply_headers(Net::HTTP::Post.new(uri), context)
|
|
103
|
+
request.body = JSON.dump(url: url, label: label)
|
|
104
|
+
request
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def apply_headers(request, context)
|
|
108
|
+
request["Content-Type"] = "application/json"
|
|
109
|
+
request["Accept"] = "application/json"
|
|
110
|
+
|
|
111
|
+
token = context&.auth_token
|
|
112
|
+
request["Authorization"] = "Bearer #{token}" if token.present?
|
|
113
|
+
request
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def parse_list(body)
|
|
117
|
+
Array(JSON.parse(body)).filter_map do |entry|
|
|
118
|
+
next unless entry.respond_to?(:to_h)
|
|
119
|
+
|
|
120
|
+
crumb = entry.to_h.symbolize_keys
|
|
121
|
+
{ url: crumb[:url], label: crumb[:label] } if crumb[:url] || crumb[:label]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def success?(response)
|
|
126
|
+
(200..299).cover?(response.code.to_i)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def net_http_transport(uri, request)
|
|
130
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
131
|
+
http.use_ssl = uri.scheme == "https"
|
|
132
|
+
http.open_timeout = @open_timeout
|
|
133
|
+
http.read_timeout = @read_timeout
|
|
134
|
+
http.request(request)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def log(error)
|
|
138
|
+
Compass.logger&.error "Breadcrumb HTTP error: #{error.message}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def log_failure(response)
|
|
142
|
+
Compass.logger&.error "Breadcrumb HTTP non-success response: #{response.code}"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
data/lib/compass/breadcrumb.rb
CHANGED
|
@@ -3,24 +3,28 @@ module Compass
|
|
|
3
3
|
# It leverages the configured breadcrumb strategy to fetch and push breadcrumb items.
|
|
4
4
|
#
|
|
5
5
|
module Breadcrumb
|
|
6
|
-
|
|
6
|
+
# Bundled breadcrumb strategies.
|
|
7
|
+
module Strategies
|
|
8
|
+
autoload :Cookie, "compass/breadcrumb/strategies/cookie"
|
|
9
|
+
autoload :Http, "compass/breadcrumb/strategies/http"
|
|
10
|
+
end
|
|
7
11
|
|
|
8
12
|
# Fetch breadcrumb for the given context, using the configured strategy.
|
|
9
13
|
#
|
|
10
14
|
# I.e.:
|
|
11
15
|
#
|
|
12
|
-
# Compass::Breadcrumb.for(
|
|
16
|
+
# Compass::Breadcrumb.for(context: current_context, max: 5)
|
|
13
17
|
# => [
|
|
14
18
|
# { url: "/home", label: "Home" },
|
|
15
19
|
# { url: "/section", label: "Section" },
|
|
16
20
|
# { url: "/section/page", label: "Page" }
|
|
17
21
|
# ]
|
|
18
22
|
#
|
|
19
|
-
# @param
|
|
20
|
-
#
|
|
23
|
+
# @param context [Compass::Context, nil] The request-scoped context. Strategies
|
|
24
|
+
# that need the request read it from +context.request+.
|
|
21
25
|
# @param max [Integer] The maximum number of items to return.
|
|
22
|
-
# @return [Array<Hash>,nil] The breadcrumb items.
|
|
23
|
-
def self.for(context
|
|
26
|
+
# @return [Array<Hash>,nil] The breadcrumb items, or nil when no strategy is configured.
|
|
27
|
+
def self.for(context:, max: 10)
|
|
24
28
|
Compass.config.breadcrumb.strategy&.for(context: context, max: max)
|
|
25
29
|
end
|
|
26
30
|
|
|
@@ -29,17 +33,17 @@ module Compass
|
|
|
29
33
|
# I.e.:
|
|
30
34
|
#
|
|
31
35
|
# Compass::Breadcrumb.push(
|
|
32
|
-
#
|
|
33
|
-
# context: { current_user: },
|
|
36
|
+
# context: current_context,
|
|
34
37
|
# url: "/section/page",
|
|
35
38
|
# label: "Page"
|
|
36
39
|
# )
|
|
37
40
|
#
|
|
38
|
-
# @param
|
|
39
|
-
#
|
|
41
|
+
# @param context [Compass::Context, nil] The request-scoped context. Strategies
|
|
42
|
+
# that need the request read it from +context.request+.
|
|
40
43
|
# @param url [String] The URL of the breadcrumb item.
|
|
41
44
|
# @param label [String] The label of the breadcrumb item.
|
|
42
|
-
|
|
45
|
+
# @return [nil]
|
|
46
|
+
def self.push(context:, url:, label:)
|
|
43
47
|
Compass.config.breadcrumb.strategy&.push(context: context, url: url, label: label)
|
|
44
48
|
end
|
|
45
49
|
end
|
|
@@ -2,23 +2,26 @@ module Compass
|
|
|
2
2
|
module Configuration::Breadcrumb
|
|
3
3
|
include ActiveSupport::Configurable
|
|
4
4
|
|
|
5
|
-
# Strategy
|
|
6
|
-
#
|
|
5
|
+
# Strategy instance for breadcrumb functionality.
|
|
6
|
+
#
|
|
7
|
+
# Defaults to a signed-cookie strategy. Assign another strategy instance to
|
|
8
|
+
# override it, or assign +nil+ to disable breadcrumbs entirely.
|
|
7
9
|
#
|
|
8
10
|
# I.e.:
|
|
9
|
-
# config.breadcrumb.strategy =
|
|
11
|
+
# config.breadcrumb.strategy = Compass::Breadcrumb::Strategies::Http.new(backend: "...")
|
|
12
|
+
# config.breadcrumb.strategy = nil # disable
|
|
10
13
|
#
|
|
11
14
|
# @see Compass::Breadcrumb for method signatures.
|
|
12
15
|
#
|
|
13
|
-
config_accessor :strategy, default:
|
|
16
|
+
config_accessor :strategy, default: Compass::Breadcrumb::Strategies::Cookie.new
|
|
14
17
|
|
|
15
18
|
# Proc to determine if a request should be tracked for breadcrumbs.
|
|
16
|
-
# It receives the
|
|
19
|
+
# It receives the context and the request as parameters.
|
|
17
20
|
#
|
|
18
21
|
# I.e.:
|
|
19
|
-
# config.breadcrumb.should_track = ->(
|
|
22
|
+
# config.breadcrumb.should_track = ->(context, request) { request.path != "/login" }
|
|
20
23
|
#
|
|
21
24
|
# @return [Proc] The tracking determination proc.
|
|
22
|
-
config_accessor :should_track, default: ->(
|
|
25
|
+
config_accessor :should_track, default: ->(context, request) { true }
|
|
23
26
|
end
|
|
24
27
|
end
|
|
@@ -8,18 +8,5 @@ module Compass
|
|
|
8
8
|
# config.client.backends = [ "http://localhost:3000/compass" ]
|
|
9
9
|
#
|
|
10
10
|
config_accessor :backends, default: Set.new
|
|
11
|
-
|
|
12
|
-
# Authentication configuration
|
|
13
|
-
#
|
|
14
|
-
# I.e.:
|
|
15
|
-
# config.client.auth_token = -> { "1234567890" }
|
|
16
|
-
# config.client.auth_expires_at = -> { 1.hour.from_now }
|
|
17
|
-
#
|
|
18
|
-
# @return [Proc] The authentication token proc.
|
|
19
|
-
config_accessor :auth_token, default: ->(**) { }
|
|
20
|
-
|
|
21
|
-
# Proc to determine the authentication expiration time.
|
|
22
|
-
# @return [Proc] The authentication expiration time proc.
|
|
23
|
-
config_accessor :auth_expires_at, default: ->(**) { }
|
|
24
11
|
end
|
|
25
12
|
end
|
|
@@ -7,13 +7,10 @@ module Compass
|
|
|
7
7
|
# To configure, use the `configure` method. For example:
|
|
8
8
|
#
|
|
9
9
|
# Compass.configure do |config|
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# end
|
|
15
|
-
# config.context = -> { { current_user: } }
|
|
16
|
-
# config.etag = -> { current_user.id }
|
|
10
|
+
# # A Compass::Context subclass, instantiated per request. It is responsible
|
|
11
|
+
# # for authentication, context identification, caching timestamps and client
|
|
12
|
+
# # auth. See Compass::Context.
|
|
13
|
+
# config.context = CompassContext
|
|
17
14
|
#
|
|
18
15
|
# # Configure menu
|
|
19
16
|
# config.menu.items = %w[ TestMenu ]
|
|
@@ -33,11 +30,9 @@ module Compass
|
|
|
33
30
|
included do
|
|
34
31
|
include ActiveSupport::Configurable
|
|
35
32
|
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
config_accessor :context, default:
|
|
39
|
-
config_accessor :context_id, default: -> { }
|
|
40
|
-
config_accessor :modified_at, default: -> { }
|
|
33
|
+
# The request-scoped context class. It must be a Compass::Context subclass
|
|
34
|
+
# and is instantiated with the current request on every Compass request.
|
|
35
|
+
config_accessor :context, default: Compass::Context
|
|
41
36
|
|
|
42
37
|
# Client configuration
|
|
43
38
|
config_accessor :client, default: Compass::Configuration::Client
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
# The request-scoped Compass context.
|
|
3
|
+
#
|
|
4
|
+
# An application subclasses this and assigns the subclass to
|
|
5
|
+
# `Compass.config.context`. `Compass::Middleware` instantiates it once per
|
|
6
|
+
# request with the Rack env and stores it in `env[Compass::Context::ENV_KEY]`,
|
|
7
|
+
# so the same instance is reused for authentication, identification, caching,
|
|
8
|
+
# and client authentication across the whole request. It also acts as the
|
|
9
|
+
# context passed to Menu, Search, and Notification providers.
|
|
10
|
+
#
|
|
11
|
+
# Use `Compass::Context.from(env)` to retrieve the context for a request.
|
|
12
|
+
#
|
|
13
|
+
# I.e.:
|
|
14
|
+
#
|
|
15
|
+
# class CompassContext < Compass::Context
|
|
16
|
+
# def authenticate!
|
|
17
|
+
# current_user.present?
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# def context_id = current_user&.id
|
|
21
|
+
# def modified_at = current_user&.updated_at
|
|
22
|
+
# def auth_token = current_user&.api_token
|
|
23
|
+
# def auth_expires_at = current_user&.token_expires_at
|
|
24
|
+
#
|
|
25
|
+
# # Exposed to providers and view locals.
|
|
26
|
+
# def to_hash = { current_user: current_user }
|
|
27
|
+
#
|
|
28
|
+
# def current_user
|
|
29
|
+
# @current_user ||= User.find_by(token: request.authorization.to_s.remove("Bearer "))
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# Compass.configure do |config|
|
|
34
|
+
# config.context = CompassContext
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
class Context
|
|
38
|
+
include Compass::MustImplement
|
|
39
|
+
|
|
40
|
+
# Rack env key under which the request context is memoized.
|
|
41
|
+
ENV_KEY = "compass.context".freeze
|
|
42
|
+
|
|
43
|
+
# The context for the given Rack env. Builds the configured context and
|
|
44
|
+
# memoizes it in the env when absent, guaranteeing a single context per
|
|
45
|
+
# request.
|
|
46
|
+
#
|
|
47
|
+
# @param env [Hash] the Rack environment
|
|
48
|
+
# @return [Compass::Context]
|
|
49
|
+
def self.from(env)
|
|
50
|
+
env[ENV_KEY] ||= Compass.config.context.new(env)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The Rack environment the context was built from.
|
|
54
|
+
# @return [Hash]
|
|
55
|
+
attr_reader :env
|
|
56
|
+
|
|
57
|
+
# The request object wrapping the request env
|
|
58
|
+
attr_reader :request
|
|
59
|
+
|
|
60
|
+
# @param env [Hash] the Rack environment
|
|
61
|
+
def initialize(env, request = ActionDispatch::Request.new(env))
|
|
62
|
+
@env = env
|
|
63
|
+
@request = request
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Authenticates the current request.
|
|
67
|
+
# @return [Boolean] whether the request is authenticated.
|
|
68
|
+
def authenticate! = false
|
|
69
|
+
|
|
70
|
+
# The context identifier. Used to namespace the Compass API paths and as an
|
|
71
|
+
# ETag source.
|
|
72
|
+
# @return [String, Integer, nil]
|
|
73
|
+
def context_id = must_implement(:context_id)
|
|
74
|
+
|
|
75
|
+
# Timestamp used for HTTP ETag caching.
|
|
76
|
+
# @return [Time, nil]
|
|
77
|
+
def modified_at = must_implement(:modified_at)
|
|
78
|
+
|
|
79
|
+
# Bearer token exposed through the client auth endpoint.
|
|
80
|
+
# @return [String, nil]
|
|
81
|
+
def auth_token = nil
|
|
82
|
+
|
|
83
|
+
# Expiration time for the auth token.
|
|
84
|
+
# @return [Time, nil]
|
|
85
|
+
def auth_expires_at = nil
|
|
86
|
+
|
|
87
|
+
# Hash representation of the context. It is splatted into Menu, Search, and
|
|
88
|
+
# Notification providers as well as into Search view locals, so applications
|
|
89
|
+
# should override it to expose the data providers depend on.
|
|
90
|
+
# @return [Hash]
|
|
91
|
+
def to_hash = {}
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/compass/engine.rb
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Compass
|
|
2
|
+
# Rack middleware that establishes the request-scoped Compass context.
|
|
3
|
+
#
|
|
4
|
+
# It builds the configured `Compass::Context` once per request and memoizes
|
|
5
|
+
# it in the Rack env under `Compass::Context::ENV_KEY`, so controllers and
|
|
6
|
+
# components reuse the same instance via `Compass::Context.from(env)`.
|
|
7
|
+
#
|
|
8
|
+
# It also runs per-request Compass features with that context — currently
|
|
9
|
+
# breadcrumb tracking, which uses the configured breadcrumb strategy to save
|
|
10
|
+
# breadcrumb items for requests that should be tracked.
|
|
11
|
+
#
|
|
12
|
+
class Middleware
|
|
13
|
+
def initialize(app)
|
|
14
|
+
@app = app
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(env)
|
|
18
|
+
request = ActionDispatch::Request.new(env)
|
|
19
|
+
# bearer:disable ruby_rails_sql_injection
|
|
20
|
+
context = Compass::Context.from(env)
|
|
21
|
+
|
|
22
|
+
save_breadcrumb!(context, request) if should_track?(context, request)
|
|
23
|
+
rescue => e
|
|
24
|
+
# Silently handle errors - don't break the request
|
|
25
|
+
Compass.logger&.error "Compass middleware error: #{e.message}"
|
|
26
|
+
ensure
|
|
27
|
+
return @app.call(env)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def save_breadcrumb!(context, request)
|
|
33
|
+
::Compass::Breadcrumb.push(
|
|
34
|
+
context: context,
|
|
35
|
+
label: request.params[:mt],
|
|
36
|
+
url: request.url
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def should_track?(context, request)
|
|
41
|
+
Compass.config.breadcrumb.should_track.call(context, request)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -2,6 +2,7 @@ module Compass
|
|
|
2
2
|
module Notification
|
|
3
3
|
class Provider
|
|
4
4
|
include Compass::JsonFormatter
|
|
5
|
+
include Compass::MustImplement
|
|
5
6
|
|
|
6
7
|
format :icon, :color, id: :required, title: :required, body: :required, url: :required, date: :required
|
|
7
8
|
|
|
@@ -36,10 +37,6 @@ module Compass
|
|
|
36
37
|
|
|
37
38
|
private
|
|
38
39
|
|
|
39
|
-
def must_implement(method)
|
|
40
|
-
raise NotImplementedError, "#{self.class} must implement ##{method}"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
40
|
def log_error(error)
|
|
44
41
|
Compass.logger&.error(
|
|
45
42
|
"[ Compass::Notification::Provider ] #{self.class}: #{error.class}: #{error.message}"
|
|
@@ -3,6 +3,7 @@ module Compass
|
|
|
3
3
|
class Provider
|
|
4
4
|
include Compass::Search::Rendering
|
|
5
5
|
include Compass::JsonFormatter
|
|
6
|
+
include Compass::MustImplement
|
|
6
7
|
|
|
7
8
|
format :url, :label, :category, :tag, :html, :confirm, :extra
|
|
8
9
|
|
|
@@ -29,12 +30,6 @@ module Compass
|
|
|
29
30
|
search_options: search_options
|
|
30
31
|
}
|
|
31
32
|
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def must_implement(method)
|
|
36
|
-
raise NotImplementedError, "#{self.class} must implement ##{method}"
|
|
37
|
-
end
|
|
38
33
|
end
|
|
39
34
|
end
|
|
40
35
|
end
|
data/lib/compass/version.rb
CHANGED
data/lib/compass.rb
CHANGED
|
@@ -9,10 +9,13 @@ module Compass
|
|
|
9
9
|
attr_accessor :logger
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
autoload :Context, "compass/context"
|
|
13
|
+
autoload :Middleware, "compass/middleware"
|
|
12
14
|
autoload :Menu, "compass/menu"
|
|
13
15
|
autoload :Search, "compass/search"
|
|
14
16
|
autoload :Notification, "compass/notification"
|
|
15
17
|
autoload :Breadcrumb, "compass/breadcrumb"
|
|
18
|
+
autoload :MustImplement, "compass/must_implement"
|
|
16
19
|
autoload :LazyAttribute, "compass/lazy_attribute"
|
|
17
20
|
autoload :JsonFormatter, "compass/json_formatter"
|
|
18
21
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: power-compass
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carlos Palhares
|
|
@@ -83,7 +83,8 @@ files:
|
|
|
83
83
|
- config/routes.rb
|
|
84
84
|
- lib/compass.rb
|
|
85
85
|
- lib/compass/breadcrumb.rb
|
|
86
|
-
- lib/compass/breadcrumb/
|
|
86
|
+
- lib/compass/breadcrumb/strategies/cookie.rb
|
|
87
|
+
- lib/compass/breadcrumb/strategies/http.rb
|
|
87
88
|
- lib/compass/configuration.rb
|
|
88
89
|
- lib/compass/configuration/breadcrumb.rb
|
|
89
90
|
- lib/compass/configuration/client.rb
|
|
@@ -91,12 +92,15 @@ files:
|
|
|
91
92
|
- lib/compass/configuration/menu.rb
|
|
92
93
|
- lib/compass/configuration/notification.rb
|
|
93
94
|
- lib/compass/configuration/search.rb
|
|
95
|
+
- lib/compass/context.rb
|
|
94
96
|
- lib/compass/engine.rb
|
|
95
97
|
- lib/compass/json_formatter.rb
|
|
96
98
|
- lib/compass/lazy_attribute.rb
|
|
97
99
|
- lib/compass/menu.rb
|
|
98
100
|
- lib/compass/menu/item.rb
|
|
99
101
|
- lib/compass/menu/item_list.rb
|
|
102
|
+
- lib/compass/middleware.rb
|
|
103
|
+
- lib/compass/must_implement.rb
|
|
100
104
|
- lib/compass/notification.rb
|
|
101
105
|
- lib/compass/notification/provider.rb
|
|
102
106
|
- lib/compass/rspec.rb
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
module Compass
|
|
2
|
-
# Compass breadcrumb handling module.
|
|
3
|
-
# It leverages the configured breadcrumb strategy to fetch and push breadcrumb items.
|
|
4
|
-
#
|
|
5
|
-
module Breadcrumb
|
|
6
|
-
# Middleware to track breadcrumb items based on requests.
|
|
7
|
-
# It uses the configured breadcrumb strategy to save breadcrumb items
|
|
8
|
-
# for requests that should be tracked.
|
|
9
|
-
#
|
|
10
|
-
class Middleware
|
|
11
|
-
def initialize(app)
|
|
12
|
-
@app = app
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def call(env)
|
|
16
|
-
@request = ActionDispatch::Request.new(env)
|
|
17
|
-
context = instance_exec(&Compass.config.context)
|
|
18
|
-
|
|
19
|
-
save_breadcrumb!(context) if should_track?(context)
|
|
20
|
-
rescue => e
|
|
21
|
-
# Silently handle breadcrumb errors - don't break the request
|
|
22
|
-
Compass.logger&.error "Breadcrumb middleware error: #{e.message}"
|
|
23
|
-
ensure
|
|
24
|
-
return @app.call(env)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
attr_reader :request
|
|
30
|
-
|
|
31
|
-
def save_breadcrumb!(context)
|
|
32
|
-
::Compass::Breadcrumb.push(
|
|
33
|
-
context: context,
|
|
34
|
-
label: @request.params[:mt],
|
|
35
|
-
url: @request.url
|
|
36
|
-
)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def should_track?(context)
|
|
40
|
-
Compass.config.breadcrumb.should_track.call(@request, context)
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|