Class: StimulusPlumbers::Password::Requirements

Inherits:
Object
  • Object
show all
Defined in:
lib/stimulus_plumbers/password/requirements.rb

Overview

Declarative password rule set: DSL + evaluation + serialization. Consumed by the form renderer, PasswordStrengthValidator, and the JS controller (via to_stimulus).

Constant Summary collapse

LEVEL_KEYS =
%i[weak fine strong].freeze
BUILTIN_KEYS =
%i[length uppercase lowercase digit symbol].freeze
DEFAULT_THRESHOLDS =
{ low: 34, high: 100, optimum: 100 }.freeze
BUILTIN_PATTERNS =
{
  uppercase: "[A-Z]",
  lowercase: "[a-z]",
  digit:     "\\d",
  symbol:    "[^A-Za-z0-9]"
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeRequirements

Returns a new instance of Requirements.



25
26
27
28
29
# File 'lib/stimulus_plumbers/password/requirements.rb', line 25

def initialize
  @enforced_options = nil
  @custom = {}
  @overrides = {}
end

Class Method Details

.buildObject



20
21
22
# File 'lib/stimulus_plumbers/password/requirements.rb', line 20

def build
  new.tap { |req| yield req if block_given? }
end

Instance Method Details

#active?Boolean

Any rule present — built-ins (only when enforced?) or a custom rule. Drives whether the form renders the strength UI; enforced? alone misses custom-only rule sets.

Returns:

  • (Boolean)


58
59
60
# File 'lib/stimulus_plumbers/password/requirements.rb', line 58

def active?
  descriptors.any?
end

#enforce(**options) ⇒ Object



31
32
33
34
# File 'lib/stimulus_plumbers/password/requirements.rb', line 31

def enforce(**options)
  @enforced_options = options
  nil
end

#enforced?Boolean

Returns:

  • (Boolean)


52
53
54
# File 'lib/stimulus_plumbers/password/requirements.rb', line 52

def enforced?
  !@enforced_options.nil?
end

#evaluate(password) ⇒ Object



70
71
72
73
74
75
# File 'lib/stimulus_plumbers/password/requirements.rb', line 70

def evaluate(password)
  password = password.to_s
  results = descriptors.to_h { |descriptor| [descriptor[:key], satisfies?(descriptor, password)] }
  satisfied = results.values.count(true)
  { rules: results, value: score(satisfied, results.size), level: level_for(satisfied, results.size) }
end

#rule(key, label = nil, pattern: nil, min: nil, max: nil, negate: false) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/stimulus_plumbers/password/requirements.rb', line 36

def rule(key, label = nil, pattern: nil, min: nil, max: nil, negate: false)
  key = key.to_sym
  if pattern
    if negate
      min = 0
      max = 0
    end
    @custom[key] = { pattern: source_of(pattern), min: min.nil? ? 1 : min, max: max, label: label }
  else
    raise ArgumentError, unknown_rule_message(key) unless BUILTIN_KEYS.include?(key)

    @overrides[key] = label
  end
  nil
end

#rulesObject



62
63
64
# File 'lib/stimulus_plumbers/password/requirements.rb', line 62

def rules
  descriptors.to_h { |descriptor| [descriptor[:key], descriptor[:label]] }
end

#thresholdsObject



66
67
68
# File 'lib/stimulus_plumbers/password/requirements.rb', line 66

def thresholds
  DEFAULT_THRESHOLDS.merge((@enforced_options || {}).slice(*DEFAULT_THRESHOLDS.keys))
end

#to_stimulusObject



82
83
84
# File 'lib/stimulus_plumbers/password/requirements.rb', line 82

def to_stimulus
  { rules: descriptors.map { |descriptor| serialize(descriptor) }, options: stimulus_options, labels: level_labels }
end

#valid?(password) ⇒ Boolean

Returns:

  • (Boolean)


77
78
79
80
# File 'lib/stimulus_plumbers/password/requirements.rb', line 77

def valid?(password)
  result = evaluate(password)
  result[:rules].any? && result[:rules].values.all?
end