Class: Graphomaton::InputPolicy
- Inherits:
-
Object
- Object
- Graphomaton::InputPolicy
- 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
- .boolean!(value, context:) ⇒ Object
- .identifier!(value, context:) ⇒ Object
- .known_keys!(hash, allowed, context:, strict:) ⇒ Object
- .label!(value, context:, max_bytes: nil) ⇒ Object
- .mapping!(value, context:) ⇒ Object
- .nested_depth!(value, maximum:, context:, max_string_bytes: nil) ⇒ Object
- .text!(value, context:, max_bytes: nil) ⇒ Object
Class Method Details
.boolean!(value, context:) ⇒ Object
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
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
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
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
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
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 |