Class: RailsVitals::Playground::SafeChainBuilder
- Inherits:
-
Object
- Object
- RailsVitals::Playground::SafeChainBuilder
- Defined in:
- lib/rails_vitals/playground/safe_chain_builder.rb
Constant Summary collapse
- ALLOWED_METHODS =
%w[ all where select limit offset order group includes preload eager_load joins left_joins find find_by first last count sum average pluck distinct having references unscoped not ].freeze
- DISALLOWED_CLASS_METHODS =
%w[ connection execute exec system eval send public_send __send__ instance_eval class_eval module_eval define_method method_missing delete destroy delete_all destroy_all update_all ].freeze
- ParseError =
Class.new(StandardError)
Class Method Summary collapse
- .build(chain_str, model) ⇒ Object
- .keyword_hash_start?(scanner) ⇒ Boolean
- .parse_args(scanner) ⇒ Object
- .parse_chain(str) ⇒ Object
- .parse_hash_key(scanner) ⇒ Object
- .scan_array(scanner) ⇒ Object
- .scan_double_quoted_string(scanner) ⇒ Object
- .scan_hash_literal(scanner) ⇒ Object
- .scan_keyword_hash(scanner) ⇒ Object
- .scan_number(scanner) ⇒ Object
- .scan_single_quoted_string(scanner) ⇒ Object
- .scan_string(scanner) ⇒ Object
- .scan_symbol(scanner) ⇒ Object
- .scan_value(scanner) ⇒ Object
Class Method Details
.build(chain_str, model) ⇒ Object
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 21 def self.build(chain_str, model) relation = model.all parse_chain(chain_str).each do |method_name, args| if DISALLOWED_CLASS_METHODS.include?(method_name) raise ParseError, "Method '#{method_name}' is not allowed for security reasons" end unless ALLOWED_METHODS.include?(method_name) raise ParseError, "Method '#{method_name}' is not allowed. Allowed: #{ALLOWED_METHODS.join(', ')}" end relation = relation.public_send(method_name, *args) end raise ParseError, "Expression must return an ActiveRecord::Relation" unless relation.is_a?(ActiveRecord::Relation) relation end |
.keyword_hash_start?(scanner) ⇒ Boolean
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 86 def self.keyword_hash_start?(scanner) pos = scanner.pos ident = scanner.scan(/[a-zA-Z_][a-zA-Z0-9_]*/) return false unless ident scanner.skip(/\s*/) if scanner.scan(/:/) && !scanner.scan(/:/) scanner.pos = pos return true end scanner.pos = pos false end |
.parse_args(scanner) ⇒ Object
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 64 def self.parse_args(scanner) args = [] scanner.skip(/\s+/) return args if scanner.eos? || scanner.peek(1) == ")" loop do scanner.skip(/\s+/) break if scanner.eos? || scanner.peek(1) == ")" if keyword_hash_start?(scanner) args << scan_keyword_hash(scanner) else args << scan_value(scanner) end scanner.skip(/\s+/) break unless scanner.scan(/,/) end args end |
.parse_chain(str) ⇒ Object
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 39 def self.parse_chain(str) scanner = StringScanner.new(str) calls = [] scanner.skip(/\s+/) until scanner.eos? scanner.skip(/\.\s*/) name = scanner.scan(/[a-z_][a-zA-Z0-9_!?]*/) raise ParseError, "Expected method name at position #{scanner.pos}" unless name scanner.skip(/\s+/) if scanner.scan(/\(/) args = parse_args(scanner) scanner.skip(/\s*\)/) calls << [ name, args ] else calls << [ name, [] ] end scanner.skip(/\s+/) end calls end |
.parse_hash_key(scanner) ⇒ Object
272 273 274 275 276 277 278 279 280 281 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 272 def self.parse_hash_key(scanner) case scanner.peek(1) when '"', "'" then scan_string(scanner) when ":" then scan_symbol(scanner) else ident = scanner.scan(/[a-zA-Z_][a-zA-Z0-9_]*/) raise ParseError, "Expected hash key" unless ident ident end end |
.scan_array(scanner) ⇒ Object
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 225 def self.scan_array(scanner) scanner.pos += 1 arr = [] scanner.skip(/\s+/) unless scanner.peek(1) == "]" loop do arr << scan_value(scanner) scanner.skip(/\s+/) break unless scanner.scan(/,/) scanner.skip(/\s+/) end end scanner.skip(/\s*\]/) raise ParseError, "Unterminated array" unless scanner.matched arr end |
.scan_double_quoted_string(scanner) ⇒ Object
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 177 def self.scan_double_quoted_string(scanner) scanner.pos += 1 result = +"" until scanner.eos? case scanner.peek(1) when '"' scanner.pos += 1 return result when "\\" scanner.pos += 1 escaped = scanner.getch case escaped when '"' then result << '"' when "\\" then result << "\\" when "n" then result << "\n" when "t" then result << "\t" when "r" then result << "\r" when "#" then result << "#" else result << "\\#{escaped}" end else result << scanner.getch end end raise ParseError, "Unterminated double-quoted string" end |
.scan_hash_literal(scanner) ⇒ Object
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 242 def self.scan_hash_literal(scanner) scanner.pos += 1 hash = {} scanner.skip(/\s+/) unless scanner.eos? || scanner.peek(1) == "}" loop do scanner.skip(/\s+/) break if scanner.eos? || scanner.peek(1) == "}" key = parse_hash_key(scanner) scanner.skip(/\s+/) if scanner.scan(/=>/) scanner.skip(/\s+/) hash[key] = scan_value(scanner) elsif scanner.scan(/:\s*/) hash[key.to_sym] = scan_value(scanner) else raise ParseError, "Expected '=>' or ':' after hash key" end scanner.skip(/\s+/) break unless scanner.scan(/,/) end end scanner.skip(/\s*}/) raise ParseError, "Unterminated hash" unless scanner.matched hash end |
.scan_keyword_hash(scanner) ⇒ Object
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 102 def self.scan_keyword_hash(scanner) hash = {} loop do scanner.skip(/\s+/) break if scanner.eos? || scanner.peek(1) == ")" || scanner.peek(1) == "}" key = scanner.scan(/[a-zA-Z_][a-zA-Z0-9_]*/) raise ParseError, "Expected hash key" unless key scanner.skip(/\s*:\s*/) value = scan_value(scanner) hash[key.to_sym] = value scanner.skip(/\s+/) break unless scanner.scan(/,/) end hash end |
.scan_number(scanner) ⇒ Object
215 216 217 218 219 220 221 222 223 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 215 def self.scan_number(scanner) num_str = scanner.scan(/-?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?/) raise ParseError, "Invalid number at position #{scanner.pos}" unless num_str if num_str.include?(".") || num_str.match?(/[eE]/) num_str.to_f else num_str.to_i end end |
.scan_single_quoted_string(scanner) ⇒ Object
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 154 def self.scan_single_quoted_string(scanner) scanner.pos += 1 result = +"" until scanner.eos? case scanner.peek(1) when "'" scanner.pos += 1 return result when "\\" scanner.pos += 1 escaped = scanner.getch case escaped when "'" then result << "'" when "\\" then result << "\\" else result << "\\#{escaped}" end else result << scanner.getch end end raise ParseError, "Unterminated single-quoted string" end |
.scan_string(scanner) ⇒ Object
283 284 285 286 287 288 289 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 283 def self.scan_string(scanner) case scanner.peek(1) when "'" then scan_single_quoted_string(scanner) when '"' then scan_double_quoted_string(scanner) else raise ParseError, "Expected string" end end |
.scan_symbol(scanner) ⇒ Object
204 205 206 207 208 209 210 211 212 213 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 204 def self.scan_symbol(scanner) scanner.pos += 1 if scanner.scan(/[a-zA-Z_][a-zA-Z0-9_]*/) scanner.matched.to_sym elsif (str = scanner.scan(/"[^"]*"/) || scanner.scan(/'[^']*'/)) str[1..-2].to_sym else raise ParseError, "Invalid symbol at position #{scanner.pos}" end end |
.scan_value(scanner) ⇒ Object
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/rails_vitals/playground/safe_chain_builder.rb', line 119 def self.scan_value(scanner) scanner.skip(/\s+/) ch = scanner.peek(1) raise ParseError, "Unexpected end of expression" unless ch case ch when "'" then scan_single_quoted_string(scanner) when '"' then scan_double_quoted_string(scanner) when ":" then scan_symbol(scanner) when "t" if scanner.scan(/true\b/) true else raise ParseError, "Unexpected token at position #{scanner.pos}" end when "f" if scanner.scan(/false\b/) false else raise ParseError, "Unexpected token at position #{scanner.pos}" end when "n" if scanner.scan(/nil\b/) nil else raise ParseError, "Unexpected token at position #{scanner.pos}" end when "[" then scan_array(scanner) when "{" then scan_hash_literal(scanner) when "-", "+", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" then scan_number(scanner) else raise ParseError, "Unexpected token '#{ch}' at position #{scanner.pos}" end end |