Class: Pressroom::Blocks::Schema

Inherits:
Object
  • Object
show all
Defined in:
lib/pressroom/blocks/schema.rb

Overview

Declarative description of one block's data payload.

The schema exists for security first and validation second: it records which fields carry HTML from Editor.js inline tools, so the renderer knows exactly what to run through the sanitizer and what to escape.

Defined Under Namespace

Classes: Field

Constant Summary collapse

SCALAR_TYPES =
%i[string html integer boolean enum].freeze
MAX_TREE_DEPTH =

Guards against a hand-crafted document nesting a list deep enough to exhaust the stack during coercion or rendering.

8

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeSchema

Returns a new instance of Schema.



28
29
30
# File 'lib/pressroom/blocks/schema.rb', line 28

def initialize
  @fields = {}
end

Instance Attribute Details

#fieldsObject (readonly)

Returns the value of attribute fields.



26
27
28
# File 'lib/pressroom/blocks/schema.rb', line 26

def fields
  @fields
end

Class Method Details

.build(&block) ⇒ Object



20
21
22
23
24
# File 'lib/pressroom/blocks/schema.rb', line 20

def self.build(&block)
  schema = new
  schema.instance_eval(&block) if block
  schema
end

Instance Method Details

#array(name, of:, required: false, default: nil) ⇒ Object



55
56
57
# File 'lib/pressroom/blocks/schema.rb', line 55

def array(name, of:, required: false, default: nil)
  add(name, :array, required, default || [], { of: of.to_sym })
end

#boolean(name, required: false, default: false) ⇒ Object



44
45
46
# File 'lib/pressroom/blocks/schema.rb', line 44

def boolean(name, required: false, default: false)
  add(name, :boolean, required, default)
end

#coerce(data) ⇒ Object

Returns [normalized_data, errors]. Never raises: a malformed payload degrades to defaults so that one bad article cannot take a page down.



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/pressroom/blocks/schema.rb', line 113

def coerce(data)
  data = data.is_a?(Hash) ? stringify(data) : {}
  errors = []
  result = {}

  fields.each do |name, field|
    if !data.key?(name) || data[name].nil?
      errors << "#{name} is required" if field.required
      result[name] = dup_default(field)
      next
    end

    value, error = cast(field, data[name])
    errors << "#{name}: #{error}" if error
    result[name] = error ? dup_default(field) : value
  end

  [result, errors]
end

#enum(name, required: false, default: nil, **options) ⇒ Object

in: is a Ruby keyword and cannot be a parameter name, so it is collected through the options hash.



50
51
52
53
# File 'lib/pressroom/blocks/schema.rb', line 50

def enum(name, required: false, default: nil, **options)
  values = Array(options.fetch(:in)).map(&:to_s)
  add(name, :enum, required, default, { values: values })
end

#html(name, required: false, default: nil) ⇒ Object



36
37
38
# File 'lib/pressroom/blocks/schema.rb', line 36

def html(name, required: false, default: nil)
  add(name, :html, required, default)
end

#html_fieldsObject

Names of fields whose values must be sanitized before rendering.



73
74
75
76
77
78
79
80
81
# File 'lib/pressroom/blocks/schema.rb', line 73

def html_fields
  fields.each_value.filter_map do |field|
    next field.name if field.type == :html
    next field.name if field.type == :array && field.options[:of] == :html
    next field.name if field.type == :tree && field.options[:content] == :html

    nil
  end
end

#integer(name, required: false, default: nil) ⇒ Object



40
41
42
# File 'lib/pressroom/blocks/schema.rb', line 40

def integer(name, required: false, default: nil)
  add(name, :integer, required, default)
end

#nested(name, required: false, &block) ⇒ Object



59
60
61
# File 'lib/pressroom/blocks/schema.rb', line 59

def nested(name, required: false, &block)
  add(name, :nested, required, nil, {}, Schema.build(&block))
end

#sanitize(data, sanitizer) ⇒ Object

Applies the sanitizer to every field that can carry markup.

This lives on the schema rather than the renderer because only the schema knows the shape of each field: a flat string, an array, a nested object, or a recursive tree.



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/pressroom/blocks/schema.rb', line 88

def sanitize(data, sanitizer)
  result = data.dup

  fields.each do |name, field|
    case field.type
    when :html
      result[name] = sanitizer.sanitize(result[name])
    when :array
      next unless field.options[:of] == :html

      result[name] = Array(result[name]).map { |item| sanitizer.sanitize(item) }
    when :tree
      next unless field.options[:content] == :html

      result[name] = sanitize_tree(Array(result[name]), sanitizer)
    when :nested
      result[name] = field.schema.sanitize(result[name] || {}, sanitizer)
    end
  end

  result
end

#string(name, required: false, default: nil) ⇒ Object



32
33
34
# File 'lib/pressroom/blocks/schema.rb', line 32

def string(name, required: false, default: nil)
  add(name, :string, required, default)
end

#tree(name, content: :html, required: false) ⇒ Object

A recursive list, as produced by @editorjs/list 2.x:

[{ "content" => "a", "items" => [{ "content" => "a1", "items" => [] }] }]

Plain strings are accepted too, so documents written by the 1.x tool keep rendering after an upgrade.



68
69
70
# File 'lib/pressroom/blocks/schema.rb', line 68

def tree(name, content: :html, required: false)
  add(name, :tree, required, [], { content: content.to_sym })
end