Class: Graphomaton::InputPolicy

Inherits:
Object
  • Object
show all
Defined in:
lib/graphomaton/input_policy.rb

Overview

Validates untrusted model input before it reaches exporters or graph analysis.

Constant Summary collapse

XML_INVALID_CHARACTERS =
/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F\uFFFE\uFFFF]/
TOP_LEVEL_KEYS =
%i[version states transitions initial initial_state final final_states].freeze
STATE_KEYS =
%i[id name x y label style metadata shape kind initial final accepting].freeze
TRANSITION_KEYS =
%i[from to label style metadata line_style].freeze

Class Method Summary collapse

Class Method Details

.boolean!(value, context:) ⇒ Object

Raises:

  • (ArgumentError)


48
49
50
51
52
# File 'lib/graphomaton/input_policy.rb', line 48

def self.boolean!(value, context:)
  return value if value == true || value == false || value.nil?

  raise ArgumentError, "#{context} must be true or false"
end

.identifier!(value, context:) ⇒ Object

Raises:

  • (ArgumentError)


26
27
28
29
30
31
# File 'lib/graphomaton/input_policy.rb', line 26

def self.identifier!(value, context:)
  raise ArgumentError, "#{context} cannot be nil" if value.nil?

  text!(value, context: context)
  value
end

.known_keys!(hash, allowed, context:, strict:) ⇒ Object

Raises:

  • (ArgumentError)


54
55
56
57
58
59
60
61
# File 'lib/graphomaton/input_policy.rb', line 54

def self.known_keys!(hash, allowed, context:, strict:)
  return unless strict

  unknown = hash.keys.reject { |key| allowed.include?(key.to_s.to_sym) }
  return if unknown.empty?

  raise ArgumentError, "Unknown #{context} keys: #{unknown.join(', ')}"
end

.label!(value, context:, max_bytes: nil) ⇒ Object



33
34
35
36
37
38
39
40
# File 'lib/graphomaton/input_policy.rb', line 33

def self.label!(value, context:, max_bytes: nil)
  if value.is_a?(Hash) || value.is_a?(Array)
    raise ArgumentError, "#{context} must be scalar text or a Graphomaton::Label"
  end

  text!(value.to_s, context: context, max_bytes: max_bytes) unless value.nil?
  value
end

.mapping!(value, context:) ⇒ Object

Raises:

  • (ArgumentError)


42
43
44
45
46
# File 'lib/graphomaton/input_policy.rb', line 42

def self.mapping!(value, context:)
  return value if value.nil? || value.is_a?(Hash)

  raise ArgumentError, "#{context} must be a Hash"
end

.nested_depth!(value, maximum:, context:, max_string_bytes: nil) ⇒ Object

Raises:

  • (ArgumentError)


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/graphomaton/input_policy.rb', line 63

def self.nested_depth!(value, maximum:, context:, max_string_bytes: nil)
  raise ArgumentError, 'max_metadata_depth must be a positive Integer' unless maximum.is_a?(Integer) && maximum.positive?

  stack = [[value, 1]]
  visited = {}
  until stack.empty?
    current, depth = stack.pop
    text!(current, context: context, max_bytes: max_string_bytes) if current.is_a?(String)
    next unless current.is_a?(Hash) || current.is_a?(Array)
    next if visited[current.object_id]

    raise ArgumentError, "#{context} exceeds max_metadata_depth (#{maximum})" if depth > maximum

    visited[current.object_id] = true
    children = current.is_a?(Hash) ? current.flat_map { |key, item| [key, item] } : current
    children.each { |child| stack << [child, depth + 1] }
  end
end

.text!(value, context:, max_bytes: nil) ⇒ Object

Raises:

  • (ArgumentError)


11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/graphomaton/input_policy.rb', line 11

def self.text!(value, context:, max_bytes: nil)
  return value unless value.is_a?(String)

  unless value.encoding == Encoding::UTF_8 ? value.valid_encoding? : value.dup.force_encoding(Encoding::UTF_8).valid_encoding?
    raise ArgumentError, "#{context} must be valid UTF-8"
  end
  utf8 = value.encoding == Encoding::UTF_8 ? value : value.encode(Encoding::UTF_8)
  raise ArgumentError, "#{context} contains characters that are invalid in XML" if utf8.match?(XML_INVALID_CHARACTERS)
  if max_bytes && utf8.bytesize > max_bytes
    raise ArgumentError, "#{context} exceeds max_label_length (#{max_bytes})"
  end

  value
end