Class: Servus::Guard

Inherits:
Object
  • Object
show all
Defined in:
lib/servus/guard.rb

Overview

Base class for guards that encapsulate validation logic with rich error responses.

Guard classes define reusable validation rules with declarative metadata and localized error messages. They provide a clean, performant alternative to scattering validation logic throughout services.

Examples:

Basic guard

class SufficientBalanceGuard < Servus::Guard
  http_status 422
  error_code 'insufficient_balance'

  message "Insufficient balance: need %{required}, have %{available}" do
    {
      required: amount,
      available: .balance
    }
  end

  def test(account:, amount:)
    .balance >= amount
  end
end

Using a guard in a service

class TransferService < Servus::Base
  def call
    enforce_sufficient_balance!(account: , amount: amount)
    # ... perform transfer ...
    success(result)
  end
end

See Also:

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**kwargs) ⇒ Guard

Initializes a new guard instance with the provided arguments.

Parameters:

  • kwargs (Hash)

    keyword arguments for the guard



215
216
217
# File 'lib/servus/guard.rb', line 215

def initialize(**kwargs)
  @kwargs = kwargs
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Provides convenience access to kwargs as methods.

This allows the message data block to access parameters directly (e.g., amount instead of kwargs[:amount]).

Parameters:

  • method_name (Symbol)

    the method name

  • args (Array)

    method arguments

  • block (Proc)

    method block

Returns:

  • (Object)

    the value from kwargs

Raises:

  • (NoMethodError)

    if the method is not found



275
276
277
# File 'lib/servus/guard.rb', line 275

def method_missing(method_name, *args, &)
  kwargs[method_name] || super
end

Class Attribute Details

.error_code_valueString? (readonly)

Returns the error code.

Returns:

  • (String, nil)

    the error code or nil if not set



109
110
111
# File 'lib/servus/guard.rb', line 109

def error_code_value
  @error_code_value
end

.http_status_codeInteger? (readonly)

Returns the HTTP status code.

Returns:

  • (Integer, nil)

    the HTTP status code or nil if not set



91
92
93
# File 'lib/servus/guard.rb', line 91

def http_status_code
  @http_status_code
end

.message_blockProc? (readonly)

Returns the message data block.

Returns:

  • (Proc, nil)

    the message data block



145
146
147
# File 'lib/servus/guard.rb', line 145

def message_block
  @message_block
end

.message_templateString, ... (readonly)

Returns the message template.

Returns:

  • (String, Symbol, Proc, Hash, nil)

    the message template



140
141
142
# File 'lib/servus/guard.rb', line 140

def message_template
  @message_template
end

Instance Attribute Details

#kwargsObject (readonly)

Returns the value of attribute kwargs.



210
211
212
# File 'lib/servus/guard.rb', line 210

def kwargs
  @kwargs
end

Class Method Details

.derive_method_name(guard_class) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Converts a guard class name to a method name.

Strips the 'Guard' suffix and converts to snake_case. The resulting name is used with 'enforce_' and 'check_' prefixes.

Examples:

derive_method_name(SufficientBalanceGuard) # => "sufficient_balance"
derive_method_name(PresenceGuard) # => "presence"

Parameters:

  • guard_class (Class)

    the guard class

Returns:

  • (String)

    the base method name (without enforce_/check_ prefix or ! or ?)



201
202
203
204
205
206
207
# File 'lib/servus/guard.rb', line 201

def derive_method_name(guard_class)
  class_name = guard_class.name.split('::').last
  class_name.gsub(/Guard$/, '')
            .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
            .gsub(/([a-z\d])([A-Z])/, '\1_\2')
            .downcase
end

.error_code(code) ⇒ void

This method returns an undefined value.

Declares the error code for API responses.

Examples:

class MyGuard < Servus::Guard
  error_code 'insufficient_balance'
end

Parameters:

  • code (String)

    the error code



102
103
104
# File 'lib/servus/guard.rb', line 102

def error_code(code)
  @error_code_value = code
end

.execute!(guard_class) ⇒ void

This method returns an undefined value.

Executes a guard and throws :guard_failure with the guard's error if validation fails.

This is the bang (!) execution method that halts execution on failure. The caller is responsible for catching the thrown error and handling it.

Examples:

Servus::Guard.execute!(EnsurePositive, amount: 100)  # passes, returns nil
Servus::Guard.execute!(EnsurePositive, amount: -10) # throws :guard_failure

Parameters:

  • guard_class (Class)

    the guard class to execute

  • kwargs (Hash)

    keyword arguments for the guard



53
54
55
56
57
58
# File 'lib/servus/guard.rb', line 53

def execute!(guard_class, **)
  guard = guard_class.new(**)
  return if guard.test(**)

  throw(:guard_failure, guard.error)
end

.execute?(guard_class) ⇒ Boolean

Executes a guard and returns boolean result without throwing.

This is the predicate (?) execution method for conditional checks.

Examples:

Servus::Guard.execute?(EnsurePositive, amount: 100) # => true
Servus::Guard.execute?(EnsurePositive, amount: -10) # => false

Parameters:

  • guard_class (Class)

    the guard class to execute

  • kwargs (Hash)

    keyword arguments for the guard

Returns:

  • (Boolean)

    true if guard passes, false otherwise



71
72
73
# File 'lib/servus/guard.rb', line 71

def execute?(guard_class, **)
  guard_class.new(**).test(**)
end

.http_status(status) ⇒ void

This method returns an undefined value.

Declares the HTTP status code for API responses.

Examples:

class MyGuard < Servus::Guard
  http_status 422
end

Parameters:

  • status (Integer)

    the HTTP status code



84
85
86
# File 'lib/servus/guard.rb', line 84

def http_status(status)
  @http_status_code = status
end

.inherited(subclass) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Hook called when a class inherits from Guard.

Automatically defines guard methods on the Servus::Guards module.

Parameters:

  • subclass (Class)

    the inheriting class



154
155
156
157
# File 'lib/servus/guard.rb', line 154

def inherited(subclass)
  super
  register_guard_methods(subclass)
end

.message(template) { ... } ⇒ void

This method returns an undefined value.

Declares the message template and data block.

The template can be a String (static or with %{} interpolation), a Symbol (I18n key), a Proc (dynamic), or a Hash (inline translations).

The block provides data for message interpolation and is evaluated in the guard instance's context.

Examples:

With string template

message "Balance must be at least %{minimum}" do
  { minimum: 100 }
end

With I18n key

message :insufficient_balance do
  { required: amount, available: .balance }
end

Parameters:

  • template (String, Symbol, Proc, Hash)

    the message template

Yields:

  • block that returns a Hash of interpolation data



132
133
134
135
# File 'lib/servus/guard.rb', line 132

def message(template, &block)
  @message_template = template
  @message_block    = block if block_given?
end

.register_guard_methods(guard_class) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Defines bang and predicate methods on Servus::Guards for the guard class.

Creates two methods:

  • enforce_! (throws :guard_failure on validation failure)
  • check_? (returns boolean)

Examples:

# For SufficientBalanceGuard, creates:
#   enforce_sufficient_balance!(account:, amount:)
#   check_sufficient_balance?(account:, amount:)

Parameters:

  • guard_class (Class)

    the guard class to register



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/servus/guard.rb', line 173

def register_guard_methods(guard_class)
  return unless guard_class.name

  base_name = derive_method_name(guard_class)

  # Define bang method (throws on failure)
  Servus::Guards.define_method("enforce_#{base_name}!") do |**kwargs|
    Servus::Guard.execute!(guard_class, **kwargs)
  end

  # Define predicate method (returns boolean)
  Servus::Guards.define_method("check_#{base_name}?") do |**kwargs|
    Servus::Guard.execute?(guard_class, **kwargs)
  end
end

Instance Method Details

#errorServus::Support::Errors::GuardError

Returns a GuardError instance configured with this guard's metadata.

Called when a guard fails to create the error that gets thrown. The caller decides how to handle the error (e.g., wrap in a failure response).

Returns:



256
257
258
259
260
261
262
# File 'lib/servus/guard.rb', line 256

def error
  Servus::Support::Errors::GuardError.new(
    message,
    code: self.class.error_code_value || 'validation_failed',
    http_status: self.class.http_status_code || 422
  )
end

#messageString

Returns the formatted error message.

Uses Support::MessageResolver to resolve the template and interpolate data from the message block.

Returns:

  • (String)

    the formatted error message

See Also:



242
243
244
245
246
247
248
# File 'lib/servus/guard.rb', line 242

def message
  Servus::Support::MessageResolver.new(
    template: self.class.message_template,
    data: self.class.message_block ? instance_exec(&self.class.message_block) : {},
    i18n_scope: 'guards'
  ).resolve(context: self)
end

#respond_to_missing?(method_name, include_private = false) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Checks if the guard responds to a method.

Parameters:

  • method_name (Symbol)

    the method name

  • include_private (Boolean) (defaults to: false)

    whether to include private methods

Returns:

  • (Boolean)

    true if the method exists or is in kwargs



285
286
287
# File 'lib/servus/guard.rb', line 285

def respond_to_missing?(method_name, include_private = false)
  kwargs.key?(method_name) || super
end

#testBoolean

Tests whether the guard passes.

Subclasses must implement this method with explicit keyword arguments that define the guard's contract.

Examples:

def test(account:, amount:)
  .balance >= amount
end

Returns:

  • (Boolean)

    true if the guard passes, false otherwise

Raises:

  • (NotImplementedError)

    if not implemented by subclass



231
232
233
# File 'lib/servus/guard.rb', line 231

def test
  raise NotImplementedError, "#{self.class} must implement #test"
end