power-compass 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 62d24dd9988012b6ce11695c503b785d1f0fc662c08708fefad59eaab4d8a96b
4
+ data.tar.gz: f04fbc2588e8013529171751bf5df84026d5743ef605dc74f7d3cffd974c7d50
5
+ SHA512:
6
+ metadata.gz: 10be0899560a5fd97c5f14ffcad8208e535cd1b597ef058804806f4b4cc4b0b050126e6e1d108bc304708aaae536d4f19d4c2661a9b65e9a17d960232d1dfa56
7
+ data.tar.gz: 61b19fa23127e9e6a6c9efff137692ce56ddb95476928bd8136b0b034730a4808a2c9dc8c6683a8a70d79093d152733898ed157cbf39b4207c980aa290112c13
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Carlos Palhares
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Compass
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "compass"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install compass
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "bundler/gem_tasks"
5
+ load "rails/tasks/statistics.rake"
6
+
7
+ require "rspec/core/rake_task"
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ require "rubocop/rake_task"
11
+ RuboCop::RakeTask.new(:rubocop)
12
+
13
+ task default: %i[rubocop spec]
@@ -0,0 +1,37 @@
1
+ module Compass
2
+ # Base compass controller.
3
+ #
4
+ # @private
5
+ class ApplicationController < ActionController::API
6
+ etag { current_context_id }
7
+ etag { context_modified_at }
8
+
9
+ before_action unless: :authenticate do
10
+ head(:forbidden)
11
+ end
12
+
13
+ before_action unless: :validate_context_id do
14
+ head(:bad_request)
15
+ end
16
+
17
+ def authenticate
18
+ instance_exec(&Compass.config.authenticate)
19
+ end
20
+
21
+ def current_context
22
+ instance_exec(&Compass.config.context)
23
+ end
24
+
25
+ def current_context_id
26
+ instance_exec(&Compass.config.context_id)
27
+ end
28
+
29
+ def context_modified_at
30
+ instance_exec(&Compass.config.modified_at)
31
+ end
32
+
33
+ def validate_context_id
34
+ current_context_id.to_s.eql?(params[:context_id])
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ module Compass
2
+ # Base compass controller.
3
+ #
4
+ # @private
5
+ class MenuController < ApplicationController
6
+ etag { filter.values.compact }
7
+ etag { menu_items.maximum(:updated_at) }
8
+
9
+ def index
10
+ expires_in(*Array(Compass.config.menu.cache))
11
+
12
+ return unless stale? menu_items, etag: "compass-menu"
13
+
14
+ render json: menu_items, filter: filter
15
+ end
16
+
17
+ private
18
+
19
+ def menu_items
20
+ @menu_items ||= Compass::Menu.build(**current_context)
21
+ end
22
+
23
+ def filter
24
+ {
25
+ authorized: true,
26
+ tagged: params[:tagged],
27
+ sort: params.fetch(:sort, %i[ gravity label ])
28
+ }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ require "compass/notification/provider"
2
+
3
+ module Compass
4
+ class NotificationController < ApplicationController
5
+ def index
6
+ expires_in(*Array(Compass.config.notification.cache), public: true)
7
+
8
+ providers = Compass::Notification::Provider.global_notifications(current_context)
9
+
10
+ return unless stale?(providers, etag: "compass-notification")
11
+
12
+ render json: providers
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,62 @@
1
+ require "compass/search/provider"
2
+
3
+ module Compass
4
+ class SearchController < ApplicationController
5
+ def index
6
+ if params[:q]
7
+ results = Compass::Search::Provider.global_search(params.require(:q), current_context)
8
+ render json: { results: results }
9
+ else
10
+ providers
11
+ end
12
+ end
13
+
14
+ def show
15
+ result = Compass::Search::Provider.search(params[:q], params[:provider_name], current_context)
16
+ render json: {
17
+ provider: params[:provider_name],
18
+ context_id: params[:context_id],
19
+ **result
20
+ }
21
+ rescue Compass::Search::UnknownProvider => error
22
+ render json: {
23
+ error: error.message,
24
+ available_providers: error.available_providers
25
+ }, status: :not_found
26
+ end
27
+
28
+ def providers
29
+ render json: {
30
+ context_id: params[:context_id],
31
+ providers: available_provider_names.map do |name|
32
+ provider_class = find_provider_by_name(name)
33
+
34
+ base_info = {
35
+ name: name,
36
+ class_name: provider_class&.name
37
+ }
38
+ begin
39
+ label = provider_class&.new(**current_context)&.label || name.humanize
40
+ base_info.merge(label: label)
41
+ rescue => e
42
+ Rails.logger.warn "Error getting provider info for #{name}: #{e.message}"
43
+ base_info.merge(
44
+ label: name.humanize,
45
+ error: e.message
46
+ )
47
+ end
48
+ end
49
+ }
50
+ end
51
+
52
+ private
53
+
54
+ def find_provider_by_name(name)
55
+ Compass::Search::Provider.find_by_name(name)
56
+ end
57
+
58
+ def available_provider_names
59
+ Compass::Search::Provider.available_provider_names
60
+ end
61
+ end
62
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,12 @@
1
+ Compass::Engine.routes.draw do
2
+ get ":context_id/menu", to: "menu#index", as: :menu
3
+
4
+ get ":context_id/search", to: "search#index", as: :search
5
+ get ":context_id/search/providers", to: "search#providers", as: :search_providers
6
+ get ":context_id/search/:provider_name",
7
+ to: "search#show",
8
+ as: :search_provider,
9
+ constraints: { provider_name: /[a-zA-Z_]+/ }
10
+
11
+ get ":context_id/notifications", to: "notification#index", as: :notifications
12
+ end
@@ -0,0 +1,44 @@
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
@@ -0,0 +1,46 @@
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
+ autoload :Middleware, "compass/breadcrumb/middleware"
7
+
8
+ # Fetch breadcrumb for the given context, using the configured strategy.
9
+ #
10
+ # I.e.:
11
+ #
12
+ # Compass::Breadcrumb.for(context_id, context: { current_user: }, max: 5)
13
+ # => [
14
+ # { url: "/home", label: "Home" },
15
+ # { url: "/section", label: "Section" },
16
+ # { url: "/section/page", label: "Page" }
17
+ # ]
18
+ #
19
+ # @param context_id [String] The context identifier.
20
+ # @param context [Hash] The context hash.
21
+ # @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)
24
+ Compass.config.breadcrumb.strategy&.for(context: context, max: max)
25
+ end
26
+
27
+ # Push a new breadcrumb item for the given context, using the configured strategy.
28
+ #
29
+ # I.e.:
30
+ #
31
+ # Compass::Breadcrumb.push(
32
+ # context_id,
33
+ # context: { current_user: },
34
+ # url: "/section/page",
35
+ # label: "Page"
36
+ # )
37
+ #
38
+ # @param context_id [String] The context identifier.
39
+ # @param context [Hash] The context hash.
40
+ # @param url [String] The URL of the breadcrumb item.
41
+ # @param label [String] The label of the breadcrumb item.
42
+ def self.push(context: {}, url:, label:)
43
+ Compass.config.breadcrumb.strategy&.push(context: context, url: url, label: label)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,24 @@
1
+ module Compass
2
+ module Configuration::Breadcrumb
3
+ include ActiveSupport::Configurable
4
+
5
+ # Strategy class for breadcrumb functionality.
6
+ # Applications must provide their own implementation.
7
+ #
8
+ # I.e.:
9
+ # config.breadcrumb.strategy = MyAppBreadcrumbService
10
+ #
11
+ # @see Compass::Breadcrumb for method signatures.
12
+ #
13
+ config_accessor :strategy, default: nil
14
+
15
+ # Proc to determine if a request should be tracked for breadcrumbs.
16
+ # It receives the request and context as parameters.
17
+ #
18
+ # I.e.:
19
+ # config.breadcrumb.should_track = ->(request, context) { request.path != "/login" }
20
+ #
21
+ # @return [Proc] The tracking determination proc.
22
+ config_accessor :should_track, default: ->(request, context) { true }
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module Compass
2
+ # Compass configuration for menu items.
3
+ module Configuration::Menu
4
+ include ActiveSupport::Configurable
5
+
6
+ # List of menu classes to be autoloaded.
7
+ #
8
+ # I.e.: Compass.config.menu.items = %w[ ProjectMenu UserMenu ]
9
+ config_accessor :items, default: []
10
+
11
+ # Cache-Control configuration for menu items.
12
+ # This configuration is used to set the `Cache-Control` header in the response.
13
+ # You can set the cache max age in seconds, and other Cache-Control attributes.
14
+ # The first argument is always the cache max age, either in seconds or in
15
+ # ActiveSupport::Duration.
16
+ #
17
+ # I.e.: Compass.config.menu.cache = 3600
18
+ # I.e.: Compass.config.menu.cache = [1.hour, public: false]
19
+ #
20
+ config_accessor :cache, default: 3600
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ module Compass
2
+ module Configuration
3
+ # Compass configuration for notifications.
4
+ class Notification
5
+ include ActiveSupport::Configurable
6
+
7
+ # List of notification provider classes to be used.
8
+ #
9
+ # Example:
10
+ # Compass.config.notification.providers = [UserNotificationProvider, ProjectNotificationProvider]
11
+ config_accessor :providers, default: []
12
+
13
+ # Polling interval for notifications (in seconds).
14
+ # Can be set to an integer or a lambda returning an integer.
15
+ #
16
+ # Example:
17
+ # Compass.config.notification.polling_interval = 30
18
+ config_accessor :polling_interval, default: -> { 30 }
19
+
20
+ # Maximum number of notifications to return.
21
+ #
22
+ # Example:
23
+ # Compass.config.notification.max_notifications = 100
24
+ config_accessor :max_notifications, default: 100
25
+
26
+ # Cache-Control configuration for notifications.
27
+ # Used to set the `Cache-Control` header in the response.
28
+ # The first argument is the cache max age (in seconds or ActiveSupport::Duration).
29
+ #
30
+ # Example:
31
+ # Compass.config.notification.cache = 3600
32
+ # Compass.config.notification.cache = [1.hour, public: true]
33
+ config_accessor :cache, default: 3600
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,18 @@
1
+ module Compass
2
+ module Configuration::Search
3
+ # Compass configuration for search providers and view paths.
4
+ include ActiveSupport::Configurable
5
+
6
+ # List of search provider classes to be used.
7
+ #
8
+ # Example:
9
+ # Compass.config.search.providers = [CarsSearchProvider, UserSearchProvider]
10
+ config_accessor :providers, default: []
11
+
12
+ # List of view paths for search rendering.
13
+ #
14
+ # Example:
15
+ # Compass.config.search.view_paths = ["app/views/search"]
16
+ config_accessor :view_paths, default: []
17
+ end
18
+ end
@@ -0,0 +1,45 @@
1
+ require "active_support/configurable"
2
+
3
+ # Compass configuration
4
+ module Compass
5
+ # Configuration options for Compass.
6
+ #
7
+ # To configure, use the `configure` method. For example:
8
+ #
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 }
17
+ #
18
+ # # Configure menu
19
+ # config.menu.items = %w[ TestMenu ]
20
+ # config.menu.cache = 3600
21
+ # end
22
+ #
23
+ module Configuration
24
+ extend ::ActiveSupport::Concern
25
+
26
+ autoload :Menu, "compass/configuration/menu"
27
+ autoload :Search, "compass/configuration/search"
28
+ autoload :Notification, "compass/configuration/notification"
29
+ autoload :Breadcrumb, "compass/configuration/breadcrumb"
30
+
31
+ included do
32
+ include ActiveSupport::Configurable
33
+
34
+ config_accessor :authenticate, default: -> { false }
35
+ config_accessor :context, default: -> { {} }
36
+ config_accessor :context_id, default: -> { }
37
+ config_accessor :modified_at, default: -> { }
38
+
39
+ config_accessor :menu, default: Compass::Configuration::Menu
40
+ config_accessor :search, default: Compass::Configuration::Search
41
+ config_accessor :notification, default: Compass::Configuration::Notification
42
+ config_accessor :breadcrumb, default: Compass::Configuration::Breadcrumb
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ require "compass"
2
+
3
+ module Compass
4
+ # Rails plugin to autoload compass features such as Menu.
5
+ # Mounting this engine exposes the routes necessary to allow other apps
6
+ # to integrate with this Rails application. See more in `config/routes.rb`.
7
+ #
8
+ class Engine < ::Rails::Engine
9
+ isolate_namespace Compass
10
+ config.generators.api_only = true
11
+ config.compass = Compass.config
12
+
13
+ initializer "compass.logger" do
14
+ Compass.logger ||= Rails.logger.respond_to?(:tagged) ? Rails.logger.tagged("Compass") : Rails.logger
15
+ end
16
+
17
+ initializer "compass.search.view_paths", after: "action_controller.set_configs" do |app|
18
+ ActiveSupport.on_load(:action_controller) do
19
+ Compass.config.search.view_paths = ActionController::Base.view_paths
20
+ end
21
+ end
22
+
23
+ initializer "compass.middleware" do |app|
24
+ app.middleware.use Compass::Breadcrumb::Middleware
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,55 @@
1
+ module Compass
2
+ # Allows a Ruby class to define an attribute, that relies on another
3
+ # attribute to evaluate a value.
4
+ # @private
5
+ module LazyAttribute
6
+ # A lazy evaluated value, evaluated in the given context when required.
7
+ #
8
+ # @private
9
+ class LazyValue
10
+ def initialize(ctx, value)
11
+ @ctx = ctx
12
+ @value = value
13
+ end
14
+
15
+ def get
16
+ case @value
17
+ when Symbol then @ctx.send(@value)
18
+ when Proc then @ctx.instance_exec(&@value)
19
+ else @value
20
+ end
21
+ end
22
+ end
23
+
24
+ extend ActiveSupport::Concern
25
+
26
+ class_methods do
27
+ # Defines a lazy_attribute, which can be set to a block or a symbol to be
28
+ # lazily evaluated within the context of the given attribute.
29
+ #
30
+ # I.e.:
31
+ #
32
+ # class Car < Struct.new(:engine)
33
+ # lazy_attribute :power, :engine
34
+ # end
35
+ # StrongEngine = Struct.new(:cylinder)
36
+ #
37
+ # car = Car.new(StrongEngine.new(2000))
38
+ # car.power(:cylinder)
39
+ # car.power => 2000
40
+ #
41
+ def lazy_attribute(name, ctxmth, &getter)
42
+ define_method(name) do |value = nil, &block|
43
+ context = send(ctxmth)
44
+ if value.present? || block.present?
45
+ instance_variable_set("@#{name}", LazyValue.new(context, value || block))
46
+ else
47
+ lazy_value = instance_variable_get("@#{name}")
48
+ value = lazy_value ? lazy_value.get : context.respond_to?(name) ? context.send(name) : nil
49
+ instance_exec(value, &(getter || :itself))
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,91 @@
1
+ module Compass
2
+ # It is an item builder class. It leverages Compass::LazyAttribute to be able
3
+ # to build its attributes based on a menu context, allowing shared methods to
4
+ # live in the Menu class.
5
+ #
6
+ class Menu::Item
7
+ attr_reader :items, :menu
8
+
9
+ include LazyAttribute
10
+
11
+ def initialize(menu, &block)
12
+ @menu = menu
13
+ @items = Menu::ItemList.new
14
+
15
+ instance_exec(menu, &block)
16
+ end
17
+
18
+ # Item gravity. This is used to sort items in the menu.
19
+ # The default gravity is 1, so if an item should always float to the top,
20
+ # the gravity should be set to 0.#
21
+ lazy_attribute(:gravity, :menu) do |gravity|
22
+ gravity.presence || 1
23
+ end
24
+ # Shallowness of the item. If the item is shallow, it will not be rendered,
25
+ # meaning that its children will be rendered in its place.
26
+ lazy_attribute :shallow, :menu
27
+ # Item label. This is the text that will be displayed in the menu.
28
+ lazy_attribute :label, :menu
29
+ # Item URL. This is the URL that the item will link to.
30
+ lazy_attribute :url, :menu
31
+ # Item icon. This is the icon that will be displayed in the menu.
32
+ lazy_attribute :icon, :menu
33
+ # Item meta data is used for various things, usually tied to the UI behavior.
34
+ # For example, to tell the UI that this item should be rendered in full screen
35
+ # meta could be set to `meta { full_screen true }`.
36
+ lazy_attribute :meta, :menu
37
+ # Badge count. This is the number that will be displayed in the badge when above 0.
38
+ lazy_attribute :badge_count, :menu do |count|
39
+ count.to_i + items.sum(&:badge_count)
40
+ end
41
+ # Authorizes the current context to render the item.
42
+ # I.e.: authorized { current_user.admin? }
43
+ lazy_attribute :authorized, :menu do |authorized|
44
+ authorized || authorized.nil?
45
+ end
46
+ # The last time anything about this item in this context was changed, including the authorization.
47
+ lazy_attribute :updated_at, :menu do |updated_at|
48
+ [ self.items.maximum(:updated_at), updated_at ].compact.max
49
+ end
50
+ # Tags that are associated with the item. This is used to filter items.
51
+ lazy_attribute :tags, :menu do |tags|
52
+ [ *self.items.flat_map(&:tags), *tags ]
53
+ end
54
+
55
+ # Nest items under the current item.
56
+ #
57
+ # I.e.:
58
+ # item do
59
+ # label "Users"
60
+ # item do
61
+ # label "Profile"
62
+ # url :profile_url
63
+ # end
64
+ # end
65
+ #
66
+ def item(&block)
67
+ Menu::Item.new(@menu, &block).tap do |item|
68
+ self.items.push(item)
69
+ end
70
+ end
71
+
72
+ # @private
73
+ def each(&block)
74
+ return yield self unless shallow
75
+
76
+ items.each(&block)
77
+ end
78
+
79
+ def as_json(options = {})
80
+ {
81
+ label: label,
82
+ icon: icon,
83
+ url: url,
84
+ items: items,
85
+ badge: badge_count.nonzero?,
86
+ meta: meta,
87
+ gravity: gravity
88
+ }.as_json(options).compact_blank
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,57 @@
1
+ module Compass
2
+ # It is a list of Compass::Menu::Item, which can be filtered and
3
+ # represented as_json recursivelly throuhout the entire tree.
4
+ #
5
+ # @private
6
+ class Menu::ItemList
7
+ include Enumerable
8
+
9
+ def initialize(items = [])
10
+ @items = items
11
+ end
12
+
13
+ def push(...)
14
+ @items.push(...)
15
+ end
16
+
17
+ def each(&block)
18
+ @items.each do |item|
19
+ item.each(&block)
20
+ end
21
+ end
22
+
23
+ def as_json(options = {})
24
+ filter(**options.fetch(:filter, {})).to_a.as_json(options)
25
+ end
26
+
27
+ def filter(**filters)
28
+ filters.reduce(self) do |items, (filter, arg)|
29
+ arg ? items.public_send(filter, arg) : items
30
+ end
31
+ end
32
+
33
+ def maximum(attr)
34
+ filter_map(&attr).max
35
+ end
36
+
37
+ def select(&block)
38
+ self.class.new(super(&block))
39
+ end
40
+
41
+ def authorized(*)
42
+ select(&:authorized)
43
+ end
44
+
45
+ def tagged(tag)
46
+ select { |item| item.tags&.include?(tag) }
47
+ end
48
+
49
+ def sort(attrs)
50
+ attrs = Array(attrs)
51
+ sorted = sort_by do |item|
52
+ attrs.map { |attr| item.public_send(attr) }
53
+ end
54
+ self.class.new(sorted)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,117 @@
1
+ module Compass
2
+ # Compass::Menu is the base class for exposing menus from a Compass enabled application.
3
+ # This class acts as a builder for the menu items. Each root menu should extend this
4
+ # class to build a menu tree.
5
+ #
6
+ # I.e.:
7
+ #
8
+ # class UserMenu < Compass::Menu
9
+ # include Rails.application.routes.mounted_helpers
10
+ #
11
+ # item do
12
+ # label "Users"
13
+ #
14
+ # item do
15
+ # label "Profile"
16
+ # url :profile_url
17
+ # end
18
+ #
19
+ # item do
20
+ # label "Settings"
21
+ # url :user_settings_url
22
+ # end
23
+ # end
24
+ # end
25
+ #
26
+ # In most cases an application will expose multiple menus, and so an Application level
27
+ # base class should be created:
28
+ #
29
+ # I.e.:
30
+ #
31
+ # class ApplicationMenu < Compass::Menu
32
+ # include Rails.application.routes.mounted_helpers
33
+ # end
34
+ #
35
+ # class UserMenu < ApplicationMenu
36
+ # item do
37
+ # label "Users"
38
+ #
39
+ # item do
40
+ # label "Profile"
41
+ # url :profile_url
42
+ # end
43
+ #
44
+ # item do
45
+ # label "Settings"
46
+ # url :user_settings_url
47
+ # end
48
+ # end
49
+ # end
50
+ #
51
+ # @see Compass::Menu::Item
52
+ class Menu
53
+ autoload :Item, "compass/menu/item"
54
+ autoload :ItemList, "compass/menu/item_list"
55
+
56
+ # Allow building a menu from a list of items.
57
+ # This is useful for building a menu from a list of items
58
+ # with the given context.
59
+ #
60
+ # I.e.:
61
+ # Compass::Menu.build(["UserMenu", "AdminMenu"], current_user: user)
62
+ #
63
+ # By default, it will build items from `Compass.menus`.
64
+ #
65
+ # I.e.:
66
+ # Compass.config.menu.items = ["UserMenu", "AdminMenu"]
67
+ # Compass::Menu.build(current_user: user)
68
+ #
69
+ # @param items [Array<String>] the list of menu items to build
70
+ # @param context [Hash] the context to pass to the menu items
71
+ # @return [ItemList] the list of menu items
72
+ def self.build(items = Compass.config.menu.items, **context)
73
+ Array(items).map(&:constantize)
74
+ .map { _1.new(**context).build }
75
+ .then(&ItemList.method(:new))
76
+ end
77
+
78
+ # Define the root item of the menu.
79
+ # This method should be called in the class definition to define the root item
80
+ # of the menu.
81
+ #
82
+ # I.e.:
83
+ # class UserMenu < Compass::Menu
84
+ # item do
85
+ # label "Users"
86
+ #
87
+ # item do
88
+ # label "Profile"
89
+ # url :profile_url
90
+ # end
91
+ # end
92
+ # end
93
+ #
94
+ # @yield the block to define the root item
95
+ def self.item(&block)
96
+ define_method(:build) do
97
+ @item ||= Item.new(self, &block)
98
+ end
99
+ end
100
+
101
+ # Initialize the menu with the given context.
102
+ # Each context key will be available as a method on the instance.
103
+ # Because items are built from the context, they will have access
104
+ # to these context methods
105
+ #
106
+ # I.e.:
107
+ # menu = UserMenu.new(current_user: user)
108
+ # menu.current_user # => user
109
+ #
110
+ # @param context [Hash] the context to pass to the menu items
111
+ def initialize(**context)
112
+ context.each do |key, value|
113
+ define_singleton_method(key) { value }
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,73 @@
1
+ module Compass
2
+ module Notification
3
+ class UnknownProvider < StandardError
4
+ attr_reader :provider_name, :available_providers
5
+
6
+ def initialize(provider_name, available_providers)
7
+ @provider_name = provider_name
8
+ @available_providers = available_providers
9
+ super("Notification provider '#{provider_name}' not found")
10
+ end
11
+ end
12
+
13
+ class Provider
14
+ class << self
15
+ def global_notifications(context = {})
16
+ Compass.config.notification.providers.map do |provider|
17
+ begin
18
+ provider_class = provider.try(:constantize) || provider
19
+ provider_class.new(**context)
20
+ rescue => e
21
+ Compass.logger&.error("[ Compass::Notification::Provider ] #{provider}: #{e.class}: #{e.message}")
22
+ nil
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # Instance methods
29
+ def initialize(**context)
30
+ @context = context
31
+ context.each { |key, value| define_singleton_method(key) { value } }
32
+ end
33
+
34
+ def fetch_notifications
35
+ retrieve_notifications
36
+ rescue => e
37
+ log_error(e)
38
+ []
39
+ end
40
+
41
+ def retrieve_notifications
42
+ raise NotImplementedError, "#{self.class} must implement #retrieve_notifications"
43
+ end
44
+
45
+ def format_notification(notification)
46
+ raise NotImplementedError, "#{self.class} must implement #format_notification"
47
+ end
48
+
49
+ def label
50
+ raise NotImplementedError, "#{self.class} must implement #label"
51
+ end
52
+
53
+ def updated_at
54
+ raise NotImplementedError, "#{self.class} must implement #updated_at"
55
+ end
56
+
57
+ def as_json(options = {})
58
+ {
59
+ label: label,
60
+ notifications: Array(fetch_notifications).map { |notification| format_notification(notification) }.compact
61
+ }
62
+ end
63
+
64
+ private
65
+
66
+ def log_error(error)
67
+ Compass.logger&.error(
68
+ "[ Compass::Notification::Provider ] #{self.class}: #{error.class}: #{error.message}"
69
+ )
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,5 @@
1
+ module Compass
2
+ module Notification
3
+ autoload :Provider, "compass/notification/provider"
4
+ end
5
+ end
@@ -0,0 +1,108 @@
1
+ module Compass
2
+ # Compass::Menu test helper for RSpec
3
+ module Rspec::Menu
4
+ # Creates a matcher for menu items, allowing you to assert that a menu item
5
+ # with a given label exists.
6
+ #
7
+ # I.e.:
8
+ # menu = ExampleMenu.new
9
+ #
10
+ # expect(menu).to have_menu_item("Dashboard")
11
+ # expect(menu).to have_menu_item("Dashboard").with_url("/dashboard")
12
+ # expect(menu).to have_menu_item("Dashboard").with_icon("fa fa-dashboard")
13
+ # expect(menu).to have_menu_item("Dashboard").with_tags("tag1", "tag2")
14
+ # expect(menu).to have_menu_item("Dashboard").with_badge(1)
15
+ # expect(menu).to(
16
+ # have_menu_item("Dashboard")
17
+ # .with_badge(1)
18
+ # .with_icon("dashboard")
19
+ # )
20
+ #
21
+ #
22
+ # The matcher will test whether the given menu/item has a
23
+ # nested authorized item with the given attributes.
24
+ # To match unauthorized items, you can also pass `authorized: false`
25
+ # to `have_menu_item`.
26
+ #
27
+ # I.e.:
28
+ #
29
+ # menu = ExampleMenu.new
30
+ # expect(menu).to have_menu_item("Unauthorized Item", authorized: false)
31
+ #
32
+ # @param label [String] the label of the menu item
33
+ # @param url [String] the URL of the menu item
34
+ # @param badge [String] the badge of the menu item
35
+ # @param icon [String] the icon of the menu item
36
+ # @param tags [Array<String>] the tags of the menu item
37
+ # @param authorized [Boolean] whether the menu item is authorized
38
+ # @return [HaveMenuItem] the matcher
39
+ def have_menu_item(...) # rubocop:disable Naming/PredicateName
40
+ HaveMenuItem.new(...)
41
+ end
42
+
43
+ class HaveMenuItem
44
+ def initialize(label, url: nil, badge: nil, icon: nil, tags: nil, authorized: true)
45
+ @label = label
46
+ @attributes = {
47
+ url: url,
48
+ authorized: authorized,
49
+ badge: badge,
50
+ icon: icon,
51
+ tags: tags
52
+ }
53
+ end
54
+
55
+ def matches?(menu_item)
56
+ @menu_item = menu_item
57
+ matching_label = menu_item.items.find do |item|
58
+ item.label == @label
59
+ end
60
+ if matching_label
61
+ @matcher = RSpec::Matchers::BuiltIn::HaveAttributes.new(@attributes.compact)
62
+ def @matcher.actual_formatted = @actual.label.inspect
63
+ @matcher.matches?(matching_label)
64
+ else
65
+ false
66
+ end
67
+ end
68
+
69
+ # Matches the item URL
70
+ def with_url(url)
71
+ @attributes[:url] = url
72
+ self
73
+ end
74
+
75
+ # Matches the item icon
76
+ def with_icon(icon)
77
+ @attributes[:icon] = icon
78
+ self
79
+ end
80
+
81
+ # Matches the item tags
82
+ def with_tags(*tags)
83
+ @attributes[:tags] = tags.flatten
84
+ self
85
+ end
86
+
87
+ # Matches the item badge
88
+ def with_badge(badge)
89
+ @attributes[:badge] = badge
90
+ self
91
+ end
92
+
93
+ # @private
94
+ def failure_message
95
+ return @matcher.failure_message if @matcher
96
+
97
+ "expected #{@menu_item.label} to have nested item #{@label}"
98
+ end
99
+
100
+ # @private
101
+ def failure_message_when_negated
102
+ return @matcher.failure_message_when_negated if @matcher
103
+
104
+ "expected #{@menu_item.label} to not have nested item #{@label}"
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,5 @@
1
+ module Compass
2
+ module Rspec
3
+ autoload :Menu, "compass/rspec/menu"
4
+ end
5
+ end
@@ -0,0 +1,118 @@
1
+ module Compass
2
+ module Search
3
+ class UnknownProvider < StandardError
4
+ attr_reader :provider_name, :available_providers
5
+
6
+ def initialize(provider_name, available_providers)
7
+ @provider_name = provider_name
8
+ @available_providers = available_providers
9
+ super("Provider '#{provider_name}' not found")
10
+ end
11
+ end
12
+
13
+ class Provider
14
+ include Compass::Search::Rendering
15
+
16
+ class << self
17
+ attr_reader :providers
18
+
19
+ # Handles both class objects and string class names
20
+ def constantize_provider(provider)
21
+ provider.is_a?(String) ? provider.constantize : provider
22
+ end
23
+
24
+ # Aggregates search results from all registered providers
25
+ def global_search(query, context = {})
26
+ Compass.config.search.providers.flat_map do |provider|
27
+ begin
28
+ provider_class = constantize_provider(provider)
29
+ provider_class.new(**context).search(query)
30
+ rescue => e
31
+ Compass.logger&.error("[ Compass::Search::Provider ] #{provider_class}: #{e.class}: #{e.message}")
32
+ []
33
+ end
34
+ end
35
+ end
36
+
37
+ # Add this new class method for individual provider search
38
+ def search(query, provider_name, context = {})
39
+ provider_class = find_by_name(provider_name)
40
+ if provider_class
41
+ provider_class.new(**context).search(query)
42
+ else
43
+ raise UnknownProvider.new(provider_name, available_provider_names)
44
+ end
45
+ end
46
+
47
+ def normalize_name(provider_class)
48
+ provider_class.name.demodulize.underscore.sub(/_?provider$/, "")
49
+ end
50
+
51
+ def find_by_name(name)
52
+ Compass.config.search.providers.find do |provider|
53
+ provider_class = constantize_provider(provider)
54
+ normalize_name(provider_class) == name.to_s
55
+ end&.then { |p| constantize_provider(p) }
56
+ end
57
+
58
+ def available_provider_names
59
+ Compass.config.search.providers.map do |provider|
60
+ provider_class = constantize_provider(provider)
61
+ normalize_name(provider_class)
62
+ end
63
+ end
64
+ end
65
+
66
+ def initialize(**context)
67
+ @context = context
68
+ context.each { |key, value| define_singleton_method(key) { value } }
69
+ end
70
+
71
+ def search(query)
72
+ records = fetch_records(query)
73
+ {
74
+ label: label,
75
+ search_options: search_options,
76
+ results: Array(records).map { |record| format_result(record) }.compact
77
+ }
78
+ rescue => e
79
+ log_error(e, query)
80
+ {
81
+ label: label,
82
+ search_options: search_options,
83
+ results: []
84
+ }
85
+ end
86
+
87
+ def label
88
+ self.class.name.demodulize.sub(/Provider$/, "").titleize
89
+ end
90
+
91
+ def search_options
92
+ {
93
+ keys: [ { name: "label", weight: 1 } ],
94
+ threshold: 0.2
95
+ }
96
+ end
97
+
98
+ def fetch_records(query)
99
+ raise NotImplementedError, "#{self.class} must implement #fetch_records"
100
+ end
101
+
102
+ def format_result(record)
103
+ {
104
+ id: record.try(:id),
105
+ label: record.try(:label) || record.try(:name) || record.try(:title) || record.try(:to_s),
106
+ category: record.try(:category) || record.class.name.demodulize,
107
+ tag: record.try(:tag) || record.try(:role)
108
+ }.compact
109
+ end
110
+
111
+ def log_error(error, query)
112
+ Compass.logger&.error(
113
+ "[ Compass::Search::Provider ] #{self.class}: #{error.class}: #{error.message} | Query: #{query.inspect}"
114
+ )
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,27 @@
1
+ module Compass
2
+ module Search
3
+ module Rendering
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :helpers, default: []
8
+ end
9
+
10
+ class_methods do
11
+ def helper(*mods)
12
+ self.helpers += mods
13
+ end
14
+ end
15
+
16
+ def view_context
17
+ @view_context ||= ViewContext.new.tap do |ctx|
18
+ self.class.helpers.each { |mod| ctx.extend(mod) }
19
+ end
20
+ end
21
+
22
+ def render(partial, locals = {})
23
+ view_context.render(partial: partial, locals: { **@context, **locals })
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ module Compass
2
+ module Search
3
+ class ViewContext < ActionView::Base
4
+ def initialize(view_paths = [])
5
+ all_view_paths = Compass.config.search.view_paths + Array(view_paths)
6
+ super ActionView::LookupContext.new(all_view_paths), {}, nil
7
+ end
8
+
9
+ def view_paths
10
+ Compass.config.search.view_paths
11
+ end
12
+
13
+ def compiled_method_container
14
+ self.class.compiled_method_container
15
+ end
16
+
17
+ def self.compiled_method_container
18
+ self
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ module Compass
2
+ module Search
3
+ autoload :Rendering, "compass/search/rendering"
4
+ autoload :Provider, "compass/search/provider"
5
+ autoload :ViewContext, "compass/search/view_context"
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Compass
2
+ VERSION = "0.1.0"
3
+ end
data/lib/compass.rb ADDED
@@ -0,0 +1,19 @@
1
+ require "compass/configuration"
2
+ require "compass/version"
3
+
4
+ # Compass implements the Compass communication protocol, allowing apps
5
+ # to integrate to a Compass ecossystem, exposing Menu, Search, and other
6
+ # features.
7
+ module Compass
8
+ class << self
9
+ attr_accessor :logger
10
+ end
11
+
12
+ autoload :Menu, "compass/menu"
13
+ autoload :Search, "compass/search"
14
+ autoload :Notification, "compass/notification"
15
+ autoload :Breadcrumb, "compass/breadcrumb"
16
+ autoload :LazyAttribute, "compass/lazy_attribute"
17
+
18
+ include Compass::Configuration
19
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :compass do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: power-compass
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Carlos Palhares
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-10-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '7.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
33
+ description: Compass provides backend services for Compass:UI
34
+ email:
35
+ - chjunior@gmail.com
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - MIT-LICENSE
41
+ - README.md
42
+ - Rakefile
43
+ - app/controllers/compass/application_controller.rb
44
+ - app/controllers/compass/menu_controller.rb
45
+ - app/controllers/compass/notification_controller.rb
46
+ - app/controllers/compass/search_controller.rb
47
+ - config/routes.rb
48
+ - lib/compass.rb
49
+ - lib/compass/breadcrumb.rb
50
+ - lib/compass/breadcrumb/middleware.rb
51
+ - lib/compass/configuration.rb
52
+ - lib/compass/configuration/breadcrumb.rb
53
+ - lib/compass/configuration/menu.rb
54
+ - lib/compass/configuration/notification.rb
55
+ - lib/compass/configuration/search.rb
56
+ - lib/compass/engine.rb
57
+ - lib/compass/lazy_attribute.rb
58
+ - lib/compass/menu.rb
59
+ - lib/compass/menu/item.rb
60
+ - lib/compass/menu/item_list.rb
61
+ - lib/compass/notification.rb
62
+ - lib/compass/notification/provider.rb
63
+ - lib/compass/rspec.rb
64
+ - lib/compass/rspec/menu.rb
65
+ - lib/compass/search.rb
66
+ - lib/compass/search/provider.rb
67
+ - lib/compass/search/rendering.rb
68
+ - lib/compass/search/view_context.rb
69
+ - lib/compass/version.rb
70
+ - lib/tasks/compass_tasks.rake
71
+ homepage: https://powerhrg.com
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ homepage_uri: https://powerhrg.com
76
+ source_code_uri: https://powerhrg.com
77
+ changelog_uri: https://powerhrg.com
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubygems_version: 3.5.22
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: Compass provides backend services for Compass:UI
97
+ test_files: []