From 3cbe44ff2bfe2f9dd0dee86725522fd8979417e9 Mon Sep 17 00:00:00 2001 From: Kim Burgestrand Date: Fri, 22 Nov 2024 11:48:21 +0100 Subject: [PATCH] Add sweeping new documentation --- Rakefile | 1 + lib/generators/rspec/policy_generator.rb | 1 + lib/generators/test_unit/policy_generator.rb | 1 + lib/pundit.rb | 27 ++++++++++++++++++-- lib/pundit/authorization.rb | 9 +++++++ lib/pundit/cache_store.rb | 22 ++++++++++++++++ lib/pundit/cache_store/legacy_store.rb | 3 +++ lib/pundit/cache_store/null_store.rb | 3 +++ lib/pundit/context.rb | 17 ++++++++++++ lib/pundit/policy_finder.rb | 17 +++++++++++- lib/pundit/rspec.rb | 2 ++ lib/pundit/version.rb | 1 + 12 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 lib/pundit/cache_store.rb diff --git a/Rakefile b/Rakefile index 7d829d9a..510c2129 100644 --- a/Rakefile +++ b/Rakefile @@ -15,6 +15,7 @@ end YARD::Rake::YardocTask.new do |t| t.files = ["lib/**/*.rb"] + t.stats_options = ["--list-undoc"] end task default: :spec diff --git a/lib/generators/rspec/policy_generator.rb b/lib/generators/rspec/policy_generator.rb index 0c85dc89..3f67d688 100644 --- a/lib/generators/rspec/policy_generator.rb +++ b/lib/generators/rspec/policy_generator.rb @@ -4,6 +4,7 @@ module Rspec # @private module Generators + # @private class PolicyGenerator < ::Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) diff --git a/lib/generators/test_unit/policy_generator.rb b/lib/generators/test_unit/policy_generator.rb index cd0175a0..5a105796 100644 --- a/lib/generators/test_unit/policy_generator.rb +++ b/lib/generators/test_unit/policy_generator.rb @@ -4,6 +4,7 @@ module TestUnit # @private module Generators + # @private class PolicyGenerator < ::Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) diff --git a/lib/pundit.rb b/lib/pundit.rb index 19bf86f2..d98cde11 100644 --- a/lib/pundit.rb +++ b/lib/pundit.rb @@ -6,6 +6,7 @@ require "pundit/policy_finder" require "pundit/authorization" require "pundit/context" +require "pundit/cache_store" require "pundit/cache_store/null_store" require "pundit/cache_store/legacy_store" @@ -14,6 +15,8 @@ # keep it here with compact class style definition. class Pundit::Error < StandardError; end # rubocop:disable Style/ClassAndModuleChildren +# Hello? Yes, this is Pundit. +# # @api public module Pundit # @api private @@ -26,8 +29,24 @@ module Generators; end # Error that will be raised when authorization has failed class NotAuthorizedError < Error - attr_reader :query, :record, :policy - + # @see #initialize + attr_reader :query + # @see #initialize + attr_reader :record + # @see #initialize + attr_reader :policy + + # @overload initialize(message) + # Create an error with a simple error message. + # @param [String] message A simple error message string. + # + # @overload initialize(options) + # Create an error with the specified attributes. + # @param [Hash] options The error options. + # @option options [String] :message Optional custom error message. Will default to a generalized message. + # @option options [Symbol] :query The name of the policy method that was checked. + # @option options [Object] :record The object that was being checked with the policy. + # @option options [Class] :policy The class of policy that was used for the check. def initialize(options = {}) if options.is_a? String message = options @@ -103,8 +122,12 @@ def policy!(user, *args, **kwargs, &block) end end + # Rails view helpers, to allow a slightly different view-specific + # implementation of the methods in {Pundit::Authorization}. + # # @api private module Helper + # @see Pundit::Authorization#pundit_policy_scope def policy_scope(scope) pundit_policy_scope(scope) end diff --git a/lib/pundit/authorization.rb b/lib/pundit/authorization.rb index 2e57cff8..07cea26c 100644 --- a/lib/pundit/authorization.rb +++ b/lib/pundit/authorization.rb @@ -196,6 +196,15 @@ def policy_scopes # rubocop:enable Naming/MemoizedInstanceVariableName + # This was added to allow calling `policy_scope!` without flipping the + # `pundit_policy_scoped?` flag. + # + # It's used internally by `policy_scope`, as well as from the views + # when they call `policy_scope`. It works because views get their helper + # from {Pundit::Helper}. + # + # @note This also memoizes the instance with `scope` as the key. + # @see Pundit::Helper#policy_scope # @api private def pundit_policy_scope(scope) policy_scopes[scope] ||= pundit.policy_scope!(scope) diff --git a/lib/pundit/cache_store.rb b/lib/pundit/cache_store.rb new file mode 100644 index 00000000..e5b9ad2a --- /dev/null +++ b/lib/pundit/cache_store.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Pundit + # Namespace for cache store implementations. + # + # Cache stores are used to cache policy lookups, so you get the same policy + # instance for the same record. + module CacheStore + # @!group Cache Store Interface + + # @!method fetch(user:, record:, &block) + # Looks up a stored policy or generate a new one. + # + # @note This is a method template, but the method does not exist in this module. + # @param user [Object] the user that initiated the action + # @param record [Object] the object being accessed + # @param block [Proc] the block to execute if missing + # @return [Object] the policy + + # @!endgroup + end +end diff --git a/lib/pundit/cache_store/legacy_store.rb b/lib/pundit/cache_store/legacy_store.rb index bf41c5c4..b6e3bea4 100644 --- a/lib/pundit/cache_store/legacy_store.rb +++ b/lib/pundit/cache_store/legacy_store.rb @@ -12,6 +12,9 @@ def initialize(hash = {}) @store = hash end + # A cache store that uses only the record as a cache key, and ignores the user. + # + # @note `nil` results are not cached. def fetch(user:, record:) _ = user @store[record] ||= yield diff --git a/lib/pundit/cache_store/null_store.rb b/lib/pundit/cache_store/null_store.rb index a75a23ce..e373b2b1 100644 --- a/lib/pundit/cache_store/null_store.rb +++ b/lib/pundit/cache_store/null_store.rb @@ -16,6 +16,9 @@ class << self attr_reader :instance end + # Always yields, does not cache anything. + # @yield + # @return [any] whatever the block returns. def fetch(*, **) yield end diff --git a/lib/pundit/context.rb b/lib/pundit/context.rb index d906f74b..9030cfed 100644 --- a/lib/pundit/context.rb +++ b/lib/pundit/context.rb @@ -39,6 +39,7 @@ def initialize(user:, policy_cache: CacheStore::NullStore.instance) attr_reader :user # @api private + # @see #initialize attr_reader :policy_cache # @!group Policies @@ -134,6 +135,15 @@ def policy_scope!(scope) # @!group Private Helpers + # Finds a cached policy for the given record, or yields to find one. + # + # @api private + # @param record [Object] the object we're retrieving the policy for + # @yield a policy finder if no policy was cached + # @yieldparam [PolicyFinder] policy_finder + # @yieldreturn [#new(user, model)] + # @return [Policy, nil] an instantiated policy + # @raise [InvalidConstructorError] if policy can't be instantated def cached_find(record) policy_cache.fetch(user: user, record: record) do klass = yield policy_finder(record) @@ -149,10 +159,17 @@ def cached_find(record) end end + # Return a policy finder for the given record. + # + # @api private + # @return [PolicyFinder] def policy_finder(record) PolicyFinder.new(record) end + # Given a possibly namespaced record, return the actual record. + # + # @api private def pundit_model(record) record.is_a?(Array) ? record.last : record end diff --git a/lib/pundit/policy_finder.rb b/lib/pundit/policy_finder.rb index e1884dd5..a5b1dcf8 100644 --- a/lib/pundit/policy_finder.rb +++ b/lib/pundit/policy_finder.rb @@ -13,13 +13,15 @@ module Pundit # finder.scope #=> UserPolicy::Scope # class PolicyFinder + # A constant applied to the end of the class name to find the policy class. + # # @api private SUFFIX = "Policy" + # @see #initialize attr_reader :object # @param object [any] the object to find policy and scope classes for - # def initialize(object) @object = object end @@ -76,6 +78,11 @@ def param_key # rubocop:disable Metrics/AbcSize private + # Given an object, find the policy class name. + # + # Uses recursion to handle namespaces. + # + # @return [String, Class] the policy class, or its name. def find(subject) if subject.is_a?(Array) modules = subject.dup @@ -92,6 +99,14 @@ def find(subject) end end + # Given an object, find its' class name. + # + # - Supports ActiveModel. + # - Supports regular classes. + # - Supports symbols. + # - Supports object instances. + # + # @return [String, Class] the class, or its name. def find_class_name(subject) if subject.respond_to?(:model_name) subject.model_name diff --git a/lib/pundit/rspec.rb b/lib/pundit/rspec.rb index 74c3ed9c..c72f796a 100644 --- a/lib/pundit/rspec.rb +++ b/lib/pundit/rspec.rb @@ -4,7 +4,9 @@ require "active_support/core_ext/array/conversions" module Pundit + # Namespace for Pundit's RSpec integration. module RSpec + # Namespace for Pundit's RSpec matchers. module Matchers extend ::RSpec::Matchers::DSL diff --git a/lib/pundit/version.rb b/lib/pundit/version.rb index 049df533..ccb6ce5d 100644 --- a/lib/pundit/version.rb +++ b/lib/pundit/version.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true module Pundit + # The current version of Pundit. VERSION = "2.4.0" end