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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67f60de7728f8399358473904162a3db899194defdd92942f59f1c1d4d193df7
4
- data.tar.gz: 467ca5ce7cf34d6594f0836e043a9826b53707e5a69b8c60f26d8342ea3b7c6e
3
+ metadata.gz: bf61a684e515eebb2ffbc60245c9a6749a06ef0f9437227f8dc612b73ed078d0
4
+ data.tar.gz: 9113182a39604aa267b9efcd91f95a543e59e3c8e1d7415da8daef2f64bfedaf
5
5
  SHA512:
6
- metadata.gz: b4f300c0fab00a8613c8dfac7b8e2e872d4ab3b2040eb6919b32e9e4224b1128312aa1b45644277769a7a13c8df209743841832f8187803ee3f9e60fdd2600db
7
- data.tar.gz: 58a7c0089e548d98ee3472ba26f001eb02c761d1d7a2b3f4b27d87440c42153be9cd2a1d18158b8585263b67a0cfc54b97fbde17589f3c37c14be4936cccff94
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
- prop(:context_id).presence || instance_exec(&Compass.config.context_id)
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 instance_exec(&Compass.config.authenticate)
10
+ head(:forbidden) unless current_context.authenticate!
11
11
  end
12
12
 
13
13
  def validate_context
14
- head(:bad_request) unless current_context_id.to_s.eql?(params[:context_id])
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
- instance_exec(&Compass.config.context)
19
- end
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 = Compass.config.client.auth_expires_at&.call(**current_context)
17
- token = Compass.config.client.auth_token&.call(**current_context)
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 { current_context_id }
10
- etag { context_modified_at }
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
@@ -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
- autoload :Middleware, "compass/breadcrumb/middleware"
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(context_id, context: { current_user: }, max: 5)
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 context_id [String] The context identifier.
20
- # @param context [Hash] The context hash.
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: {}, max: 10)
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
- # context_id,
33
- # context: { current_user: },
36
+ # context: current_context,
34
37
  # url: "/section/page",
35
38
  # label: "Page"
36
39
  # )
37
40
  #
38
- # @param context_id [String] The context identifier.
39
- # @param context [Hash] The context hash.
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
- def self.push(context: {}, url:, label:)
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 class for breadcrumb functionality.
6
- # Applications must provide their own implementation.
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 = MyAppBreadcrumbService
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: nil
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 request and context as parameters.
19
+ # It receives the context and the request as parameters.
17
20
  #
18
21
  # I.e.:
19
- # config.breadcrumb.should_track = ->(request, context) { request.path != "/login" }
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: ->(request, context) { true }
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
- # config.authenticate = -> do
11
- # authenticate_or_request_with_http_token do |token, options|
12
- # @current_user = User.find_by(token: token)
13
- # end
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
- # Backend context authentication and context identification
37
- config_accessor :authenticate, default: -> { false }
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
@@ -44,7 +44,7 @@ module Compass
44
44
  end
45
45
 
46
46
  initializer "compass.middleware" do |app|
47
- app.middleware.use Compass::Breadcrumb::Middleware
47
+ app.middleware.use Compass::Middleware
48
48
  end
49
49
  end
50
50
  end
@@ -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
@@ -0,0 +1,7 @@
1
+ module Compass
2
+ module MustImplement
3
+ def must_implement(method)
4
+ raise NotImplementedError, "#{self.class} must implement ##{method}"
5
+ end
6
+ end
7
+ 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
@@ -1,3 +1,3 @@
1
1
  module Compass
2
- VERSION = "0.9.1"
2
+ VERSION = "1.0.0"
3
3
  end
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.9.1
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/middleware.rb
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