Class: Moderate::Result

Inherits:
Data
  • Object
show all
Defined in:
lib/moderate/result.rb

Overview

The single return type of every filter adapter: ‘adapter.classify(value) → Moderate::Result`.

This is the gem’s content-filtering value object — immutable, frozen at construction. It answers the two questions the rest of the gem asks (“is this allowed?” and “if not, why?”) and carries the per-label detail for the moderation queue, the DSA statement of reasons, and the transparency counters.

The public surface follows the README’s “Content filtering” section verbatim:

result.allowed?    # => false
result.flagged?    # => true   (the inverse — convenience for the validator)
result.categories  # => [:hate, :"hate/threatening"]   (canonical slugs)
result.scores      # => { "hate" => 0.97, "hate/threatening" => 0.81 }
result.labels      # => [#<Moderate::Label ...>, ...]
result.source      # => "wordlist" / "openai" / your adapter name
result.raw         # => the untouched provider response (for debugging/audit)

Built on ‘Data.define` (Ruby 3.2+) for a frozen value object. We expose a keyword `.new` whose contract is forgiving: an adapter can hand us either a rich `labels:` array OR the flatter `categories:`/`scores:` shape (the simpler shape a deterministic adapter naturally returns), and we reconcile both into a coherent Result.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(allowed: nil, labels: nil, categories: nil, scores: nil, source: nil, raw: nil) ⇒ Result

Returns a new instance of Result.

Parameters:

  • allowed (Boolean, nil) (defaults to: nil)

    explicit allow/deny. If nil, we infer it from whether any label is flagged (no labels ⇒ allowed).

  • labels (Array<Moderate::Label, Hash>) (defaults to: nil)

    rich per-label verdicts. Hashes are coerced to ‘Moderate::Label`. Optional.

  • categories (Array<String, Symbol>) (defaults to: nil)

    flat canonical slugs, the simpler shape adapters may return instead of ‘labels:`. Each becomes a Label (parsing “hate/threatening” → category :hate, subcategory :threatening).

  • scores (Hash) (defaults to: nil)

    slug => 0..1 score, merged onto the labels built from ‘categories:` (and onto label slugs generally).

  • source (String, Symbol) (defaults to: nil)

    the adapter name that produced this — the value recorded as ‘Moderate::Flag#source` so the queue shows which backend flagged each item. Defaults to “unknown”.

  • raw (Object) (defaults to: nil)

    the untouched provider payload, kept for audit and debugging. Never relied on by the gem’s own logic.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/moderate/result.rb', line 42

def initialize(allowed: nil, labels: nil, categories: nil, scores: nil, source: nil, raw: nil)
  scores_hash = normalize_scores(scores)
  built_labels = build_labels(labels, categories, scores_hash)

  # Infer `allowed` when the adapter didn't say: any flagged label ⇒ denied.
  # This lets a deterministic adapter return just `categories: [...]` and have
  # the verdict fall out correctly, without having to compute `allowed` itself.
  resolved_allowed =
    if allowed.nil?
      built_labels.none?(&:flagged)
    else
      allowed ? true : false
    end

  super(
    allowed: resolved_allowed,
    labels: built_labels.freeze,
    source: (source || "unknown").to_s,
    raw: raw
  )
end

Instance Attribute Details

#allowedObject (readonly)

Returns the value of attribute allowed

Returns:

  • (Object)

    the current value of allowed



27
28
29
# File 'lib/moderate/result.rb', line 27

def allowed
  @allowed
end

#labelsObject (readonly)

Returns the value of attribute labels

Returns:

  • (Object)

    the current value of labels



27
28
29
# File 'lib/moderate/result.rb', line 27

def labels
  @labels
end

#rawObject (readonly)

Returns the value of attribute raw

Returns:

  • (Object)

    the current value of raw



27
28
29
# File 'lib/moderate/result.rb', line 27

def raw
  @raw
end

#sourceObject (readonly)

Returns the value of attribute source

Returns:

  • (Object)

    the current value of source



27
28
29
# File 'lib/moderate/result.rb', line 27

def source
  @source
end

Class Method Details

.allowed(source: nil, raw: nil) ⇒ Object

Convenience builder for the most common deterministic case: nothing matched.



65
66
67
# File 'lib/moderate/result.rb', line 65

def self.allowed(source: nil, raw: nil)
  new(allowed: true, labels: [], source: source, raw: raw)
end

Instance Method Details

#allowed?Boolean

Returns:

  • (Boolean)


69
# File 'lib/moderate/result.rb', line 69

def allowed? = allowed

#categoriesObject

Canonical category slugs as symbols, e.g. [:hate, :“hate/threatening”]. Only flagged labels count — an adapter may return a full score map including non-tripping categories, and those shouldn’t show up as “the categories this tripped”. De-duplicated, order-preserving.



80
81
82
# File 'lib/moderate/result.rb', line 80

def categories
  flagged_labels.map { |label| label.slug.to_sym }.uniq
end

#flagged?Boolean

The inverse of ‘allowed?`. The validator and the `moderates` concern read this; it’s spelled out (rather than ‘!allowed`) because “flagged?” is the word everyone reaches for.

Returns:

  • (Boolean)


74
# File 'lib/moderate/result.rb', line 74

def flagged? = !allowed

#scoresObject

slug => score, e.g. { “hate” => 0.97, “hate/threatening” => 0.81 }. String keys to match OpenAI’s wire format and what we persist on the Flag. Skips labels with a nil score (a deterministic adapter may not provide one).



87
88
89
90
91
# File 'lib/moderate/result.rb', line 87

def scores
  flagged_labels.each_with_object({}) do |label, acc|
    acc[label.slug] = label.score unless label.score.nil?
  end
end