Module: Rjq::AST

Defined in:
lib/rjq/ast.rb

Defined Under Namespace

Classes: ArrayLiteral, Assignment, BinaryOp, Binding, Break, CapturedFilter, Comma, Context, Field, Foreach, Format, FunctionCall, FunctionDefinition, Identity, If, Index, Iterate, Label, Literal, ModuleDirective, Node, ObjectLiteral, Optional, Pipe, Program, Recurse, Reduce, ScopedDefinition, Slice, SourceSpan, StringLiteral, Try, UnaryOp, Variable

Class Method Summary collapse

Class Method Details

.alternative_paths(pattern) ⇒ Object



1306
1307
1308
1309
1310
# File 'lib/rjq/ast.rb', line 1306

def alternative_paths(pattern)
  return [] unless pattern[0] == :alternatives

  pattern[1].map { |candidate| pattern_primary_path(candidate) }
end

.bind_missing_variables(context, names, value) ⇒ Object



1217
1218
1219
1220
1221
# File 'lib/rjq/ast.rb', line 1217

def bind_missing_variables(context, names, value)
  names.reduce(context) do |ctx, name|
    ctx.variables.key?(name) ? ctx : ctx.with_variable(name, value)
  end
end

.bind_pattern(context, pattern, value) ⇒ Object



1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
# File 'lib/rjq/ast.rb', line 1152

def bind_pattern(context, pattern, value)
  case pattern[0]
  when :alternatives
    matched = pattern[1].lazy.map { |candidate| match_bind_pattern(context, candidate, value) }.find(&:itself)
    matched ||= bind_pattern(context, pattern[1].first, value)
    bind_missing_variables(matched, pattern_variable_names(pattern), nil)
  when :both
    bind_pattern(bind_pattern(context, pattern[1], value), pattern[2], value)
  when :var
    context.with_variable(pattern[1], value)
  when :object
    pattern[1].reduce(context) do |ctx, (key, child)|
      actual_key = key.is_a?(Node) ? key.eval(value, ctx).first : key
      unless value.nil? || value.is_a?(Hash)
        raise TypeError, "Cannot index #{Value.type_of(value)} with string #{actual_key.to_s.inspect}"
      end
      bind_pattern(ctx, child, value.is_a?(Hash) ? value[actual_key.to_s] : nil)
    end
  when :array
    unless value.nil? || value.is_a?(Array)
      raise TypeError, "Cannot index #{Value.type_of(value)} with number"
    end
    pattern[1].each_with_index.reduce(context) do |ctx, (child, index)|
      bind_pattern(ctx, child, value.is_a?(Array) ? value[index] : nil)
    end
  end
end

.bind_pattern_candidate_with_path(context, pattern, candidate, value) ⇒ Object



1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
# File 'lib/rjq/ast.rb', line 1236

def bind_pattern_candidate_with_path(context, pattern, candidate, value)
  bound, path, variables = bind_single_pattern_with_path(context, candidate, value)
  selected = value_at_path(value, path)
  pattern_variable_names(pattern).each do |name|
    next if variables.key?(name)

    variables[name] = selected.nil? ? path : (static_variable_path(pattern, name) || path)
  end
  [bind_missing_variables(bound, pattern_variable_names(pattern), nil), path, variables]
end

.bind_pattern_with_path(context, pattern, value) ⇒ Object



1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
# File 'lib/rjq/ast.rb', line 1223

def bind_pattern_with_path(context, pattern, value)
  candidates = pattern[0] == :alternatives ? pattern[1] : [pattern]
  errors = []
  candidates.each do |candidate|
    begin
      return bind_pattern_candidate_with_path(context, pattern, candidate, value)
    rescue Rjq::RuntimeError => e
      errors << e
    end
  end
  raise errors.last
end

.bind_single_pattern_with_path(context, pattern, value) ⇒ Object



1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
# File 'lib/rjq/ast.rb', line 1346

def bind_single_pattern_with_path(context, pattern, value)
  case pattern[0]
  when :var
    [context.with_variable(pattern[1], value), [], { pattern[1] => [] }]
  when :object
    unless value.nil? || value.is_a?(Hash)
      key = pattern[1].first&.first
      key = key.eval(value, context).first if key.is_a?(Node)
      raise TypeError, "Cannot index #{Value.type_of(value)} with string #{key.to_s.inspect}"
    end
    ctx = context
    combined = []
    variables = {}
    pattern[1].each do |key, child|
      actual_key = key.is_a?(Node) ? key.eval(value, ctx).first : key
      child_value = value.is_a?(Hash) ? value[actual_key.to_s] : nil
      ctx, child_path, child_variables = bind_single_pattern_with_path(ctx, child, child_value)
      full = [actual_key.to_s] + child_path
      combined.concat(full)
      variables.merge!(child_variables.transform_values { |path| [actual_key.to_s] + path })
    end
    variables.transform_values! { combined } if variables.length > 1
    [ctx, combined, variables]
  when :array
    unless value.nil? || value.is_a?(Array)
      raise TypeError, "Cannot index #{Value.type_of(value)} with number"
    end
    ctx = context
    entries = pattern[1].each_with_index.map do |child, index|
      child_value = value.is_a?(Array) ? value[index] : nil
      ctx, child_path, child_variables = bind_single_pattern_with_path(ctx, child, child_value)
      [[index] + child_path, child_variables.transform_values { |path| [index] + path }]
    end
    combined = entries.reverse.flat_map(&:first)
    variables = entries.each_with_object({}) { |(_path, vars), all| all.merge!(vars) }
    variables.transform_values! { combined } if variables.length > 1
    [ctx, combined, variables]
  when :both
    first_ctx, first_path, first_variables = bind_single_pattern_with_path(context, pattern[1], value)
    second_ctx, second_path, second_variables = bind_single_pattern_with_path(first_ctx, pattern[2], value)
    combined = first_path + second_path
    variables = first_variables.merge(second_variables)
    variables.transform_values! { combined } if variables.length > 1
    [second_ctx, combined, variables]
  else
    raise TypeError, 'invalid binding pattern'
  end
end

.binding_path_components_valid?(input, path) ⇒ Boolean

Returns:

  • (Boolean)


1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
# File 'lib/rjq/ast.rb', line 1293

def binding_path_components_valid?(input, path)
  current = input
  path.each do |component|
    return true if current.nil?
    return false unless current.is_a?(Array) || current.is_a?(Hash)
    return false if current.is_a?(Array) && !component.is_a?(Numeric)
    return false if current.is_a?(Hash) && !component.is_a?(String)

    current = current.is_a?(Array) ? current[component.to_i] : current[component]
  end
  true
end

.binding_path_matches?(input, path, result) ⇒ Boolean

Returns:

  • (Boolean)


1289
1290
1291
# File 'lib/rjq/ast.rb', line 1289

def binding_path_matches?(input, path, result)
  path && binding_path_components_valid?(input, path) && Value.equal?(value_at_path(input, path), result)
end

.match_bind_pattern(context, pattern, value) ⇒ Object



1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
# File 'lib/rjq/ast.rb', line 1180

def match_bind_pattern(context, pattern, value)
  case pattern[0]
  when :var
    bind_pattern(context, pattern, value)
  when :object
    return nil unless value.nil? || value.is_a?(Hash)

    bind_pattern(context, pattern, value)
  when :array
    return nil unless value.nil? || value.is_a?(Array)

    bind_pattern(context, pattern, value)
  when :alternatives
    pattern[1].lazy.map { |candidate| match_bind_pattern(context, candidate, value) }.find(&:itself)
  when :both
    first = match_bind_pattern(context, pattern[1], value)
    first && match_bind_pattern(first, pattern[2], value)
  end
end

.nan_number?(value) ⇒ Boolean

Returns:

  • (Boolean)


1471
1472
1473
# File 'lib/rjq/ast.rb', line 1471

def nan_number?(value)
  value.respond_to?(:nan?) && value.nan?
end

.normalize_boundary(index, length, rounding) ⇒ Object

Raises:



1462
1463
1464
1465
1466
1467
1468
1469
# File 'lib/rjq/ast.rb', line 1462

def normalize_boundary(index, length, rounding)
  raise TypeError, 'slice index must be a number' unless index.is_a?(Numeric)

  rounded = rounding == :ceil ? index.ceil : index.floor

  normalized = index.negative? ? length + rounded : rounded
  [[normalized, 0].max, length].min
end

.numeric(value) ⇒ Object

Raises:



1445
1446
1447
1448
1449
# File 'lib/rjq/ast.rb', line 1445

def numeric(value)
  raise TypeError, "#{Value.type_of(value)} is not a number" unless value.is_a?(Numeric)

  value
end

.pattern_primary_path(pattern) ⇒ Object



1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
# File 'lib/rjq/ast.rb', line 1312

def pattern_primary_path(pattern)
  case pattern[0]
  when :var then []
  when :object
    key, child = pattern[1].first
    [key.is_a?(Node) ? nil : key.to_s] + pattern_primary_path(child)
  when :array then [0] + pattern_primary_path(pattern[1].first || [:var, ''])
  when :both then pattern_primary_path(pattern[1]) + pattern_primary_path(pattern[2])
  else []
  end
end

.pattern_variable_names(pattern) ⇒ Object



1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
# File 'lib/rjq/ast.rb', line 1200

def pattern_variable_names(pattern)
  case pattern[0]
  when :var
    [pattern[1]]
  when :object
    pattern[1].flat_map { |_key, child| pattern_variable_names(child) }
  when :array
    pattern[1].flat_map { |child| pattern_variable_names(child) }
  when :both
    pattern_variable_names(pattern[1]) + pattern_variable_names(pattern[2])
  when :alternatives
    pattern[1].flat_map { |child| pattern_variable_names(child) }.uniq
  else
    []
  end
end

.range_for(length, start, finish) ⇒ Object



1456
1457
1458
1459
1460
# File 'lib/rjq/ast.rb', line 1456

def range_for(length, start, finish)
  from = start.nil? || nan_number?(start) ? 0 : normalize_boundary(start, length, :floor)
  to = finish.nil? || nan_number?(finish) ? length : normalize_boundary(finish, length, :ceil)
  from...to
end

.short_dump(value) ⇒ Object



1451
1452
1453
1454
# File 'lib/rjq/ast.rb', line 1451

def short_dump(value)
  dumped = JSON::Dumper.dump(value, indent: nil)
  dumped.length > 14 ? "#{dumped[0, 11]}..." : dumped
end

.static_variable_path(pattern, target, prefix = []) ⇒ Object



1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
# File 'lib/rjq/ast.rb', line 1395

def static_variable_path(pattern, target, prefix = [])
  case pattern[0]
  when :var
    return prefix if pattern[1] == target
  when :object
    pattern[1].each do |key, child|
      next if key.is_a?(Node)

      found = static_variable_path(child, target, prefix + [key.to_s])
      return found if found
    end
  when :array
    pattern[1].each_with_index do |child, index|
      found = static_variable_path(child, target, prefix + [index])
      return found if found
    end
  when :both
    return static_variable_path(pattern[1], target, prefix) || static_variable_path(pattern[2], target, prefix)
  when :alternatives
    pattern[1].each do |candidate|
      found = static_variable_path(candidate, target, prefix)
      return found if found
    end
  end
  nil
end

.valid_path_parent?(input, path) ⇒ Boolean

Returns:

  • (Boolean)


1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
# File 'lib/rjq/ast.rb', line 1432

def valid_path_parent?(input, path)
  return true if input.nil? || path.empty?

  parent = path[0...-1].reduce(input) do |current, component|
    return false unless current.is_a?(Array) || current.is_a?(Hash)
    return false if current.is_a?(Array) && !component.is_a?(Numeric)
    return false if current.is_a?(Hash) && !component.is_a?(String)

    current.is_a?(Array) ? current[component.to_i] : current[component]
  end
  parent.is_a?(Array) || parent.is_a?(Hash)
end

.validate_binding_results(input, results, paths, alternative_base: nil, variable_paths: [], alternative_paths: []) ⇒ Object



1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
# File 'lib/rjq/ast.rb', line 1247

def validate_binding_results(input, results, paths, alternative_base: nil, variable_paths: [],
                             alternative_paths: [])
  return [] if results.empty?

  validated = []
  switched_path = nil
  results.zip(paths).each do |result, path|
    if alternative_paths.include?([]) && path == alternative_base &&
       !binding_path_matches?(input, path, result)
      path = []
    end
    variable_path = variable_paths.include?(path)
    if switched_path && variable_path
      if binding_path_matches?(input, path, result)
        validated << switched_path
        next
      end
      raise InvalidPathError.new("Invalid path expression with result #{JSON::Dumper.dump(result, indent: nil)}",
                                 result, outputs: validated)
    end

    switching = !alternative_paths.empty? && variable_path && path != alternative_base
    if switching && validated.empty?
      validate_pattern_path(input, path) unless alternative_paths.include?([])
      validated << path
      switched_path = path unless binding_path_matches?(input, path, result)
      next
    end

    unless binding_path_matches?(input, path, result)
      raise InvalidPathError.new("Invalid path expression with result #{JSON::Dumper.dump(result, indent: nil)}",
                                 result, outputs: validated)
    end
    validated << path
    if switching && binding_path_matches?(input, path, result)
      switched_path = path
      validated << path
    end
  end
  validated
end

.validate_pattern_path(input, path) ⇒ Object



1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
# File 'lib/rjq/ast.rb', line 1324

def validate_pattern_path(input, path)
  return true if input.nil? || path.empty?

  current = input
  previous_container = input
  path.each do |component|
    unless current.is_a?(Array) || current.is_a?(Hash)
      raise InvalidPathError.new("Invalid path expression near attempt to access element #{component.inspect} of " \
                                 "#{JSON::Dumper.dump(previous_container, indent: nil)}", current)
    end
    if current.is_a?(Array) && !component.is_a?(Numeric)
      raise TypeError, "Cannot index array with string #{component.to_s.inspect}"
    end
    if current.is_a?(Hash) && !component.is_a?(String)
      raise TypeError, 'Cannot index object with number'
    end
    previous_container = current
    current = current.is_a?(Array) ? current[component.to_i] : current[component]
  end
  true
end

.value_at_path(value, path) ⇒ Object



1422
1423
1424
1425
1426
1427
1428
1429
1430
# File 'lib/rjq/ast.rb', line 1422

def value_at_path(value, path)
  path.reduce(value) do |current, component|
    return nil unless current.is_a?(Array) || current.is_a?(Hash)
    return nil if current.is_a?(Array) && !component.is_a?(Numeric)
    return nil if current.is_a?(Hash) && !component.is_a?(String)

    current.is_a?(Array) ? current[component.to_i] : current[component]
  end
end