Class: Email::ContentRules

Inherits:
Object
  • Object
show all
Defined in:
app/services/email/content_rules.rb

Overview

Server-enforced design rules for Redactor 4 email-template body_v4, the email
analogue of Blog::ContentRules. Same idea: house conventions stated in a prompt
are advisory and the model drifts (e.g. bare unstyled , off-brand link colors,
hand-rolled column grids). These rules are DERIVED from the real corpus of
human-authored/converted R4 templates (see config/initializers/email_content_rules.rb
for the empirical basis) and enforced at save time so Sunny's output stays
consistent with what the design team actually ships.

Rules are advisory-by-default (severity :warn) — they're returned to the model so
it can self-correct, but do not hard-block a save unless registered as :error.
Styling drift is a quality issue, not a data-integrity one, so we surface it and
let the model fix it rather than reject outright (a half-styled draft a human can
still finish beats a hard failure).

Usage:
Email::ContentRules.register(
Email::ContentRules::Rule.new(
name: :headings_must_be_styled, applies_to: :body_v4, severity: :warn,
validate: ->(html) { ... nil or message ... }
)
)
violations = Email::ContentRules.validate(scope: :body_v4, html: body_v4)

=> [{ rule:, message:, severity: }, ...]

Defined Under Namespace

Modules: Helpers Classes: Rule

Class Method Summary collapse

Class Method Details

.allObject



64
# File 'app/services/email/content_rules.rb', line 64

def all = @mutex.synchronize { @rules.dup }

.any_errors?(violations) ⇒ Boolean

True if any violation is severity :error (would hard-block a save).

Returns:

  • (Boolean)


75
# File 'app/services/email/content_rules.rb', line 75

def any_errors?(violations) = violations.any? { |v| v[:severity] == :error }

.format_advisory(violations) ⇒ Object

Format violations into a single advisory string for tool responses. Returns
nil when there are no violations.



68
69
70
71
72
# File 'app/services/email/content_rules.rb', line 68

def format_advisory(violations)
  return nil if violations.empty?

  violations.map { |v| "• [#{v[:rule]}] #{v[:message]}" }.join("\n")
end

.register(rule) ⇒ Object

Raises:

  • (ArgumentError)


44
45
46
47
48
49
# File 'app/services/email/content_rules.rb', line 44

def register(rule)
  raise ArgumentError, 'rule must be an Email::ContentRules::Rule' unless rule.is_a?(Rule)

  @mutex.synchronize { @rules << rule }
  rule
end

.reset!Object



51
52
53
# File 'app/services/email/content_rules.rb', line 51

def reset!
  @mutex.synchronize { @rules = [] }
end

.rules_for(scope) ⇒ Object



63
# File 'app/services/email/content_rules.rb', line 63

def rules_for(scope) = @mutex.synchronize { @rules.select { |r| r.applies_to == scope } }

.validate(scope:, html:) ⇒ Array<Hash>

Returns { rule:, message:, severity: } for every violation in scope.

Returns:

  • (Array<Hash>)

    { rule:, message:, severity: } for every violation in scope.



56
57
58
59
60
61
# File 'app/services/email/content_rules.rb', line 56

def validate(scope:, html:)
  return [] if html.nil? || html.to_s.strip.empty?

  scoped = @mutex.synchronize { @rules.select { |r| r.applies_to == scope } }
  scoped.flat_map { |r| r.call(html) }
end