Skip to main content

lib/json_path/eval.ex

defmodule JSONPath.Eval do
  @moduledoc false

  alias JSONPath.Eval.Slice

  @operators [:eq, :neq, :gt, :gte, :lt, :lte]
  @node_false []

  @type path :: [String.t() | non_neg_integer()]
  @type jsonpath_node :: {any(), path()}

  def evaluate(root, ast), do: evaluate(root, root, ast, [])

  @spec evaluate(any(), any(), any(), path()) :: jsonpath_node()
  defp evaluate(root, current_node, conditions, path) do
    do_eval(root, current_node, conditions, path) |> discard_nothing()
  end

  defp do_eval(_root, current_node, {:selectors, :current_node, :full}, path) do
    [{current_node, path}]
  end

  defp do_eval(root, _current_node, {:selectors, :root, :full}, _) do
    [{root, []}]
  end

  defp do_eval(root, current_node, {:selectors, :current_node, conditions}, path) do
    conditions
    |> Enum.flat_map(&evaluate_selector(root, current_node, &1, path))
    |> discard_nothing()
  end

  defp do_eval(root, _current_node, {:selectors, :root, conditions}, path) do
    conditions
    |> Enum.flat_map(&evaluate_selector(root, root, &1, path))
    |> discard_nothing()
  end

  defp do_eval(root, current_node, {:selectors, to_select, conditions}, path) do
    evaluate(root, current_node, to_select, path)
    |> Enum.flat_map(fn {node, path} -> evaluate_selectors(root, node, conditions, path) end)
    |> discard_nothing()
  end

  # For descendant segments:
  # - Nodes in an array are visited in order
  # - Nodes are visited before their descendants
  defp do_eval(root, current_node, {:descendant_segment, selector, conditions}, path) do
    evaluate(root, current_node, selector, path)
    |> Enum.flat_map(fn {node, path} ->
      node_matching = Enum.flat_map(conditions, &evaluate_selector(root, node, &1, path))

      child_conditions = {:descendant_segment, {:selectors, :current_node, :full}, conditions}

      children_matching =
        node
        |> iter()
        |> Enum.flat_map(fn {value, key_or_idx} ->
          evaluate(root, value, child_conditions, [key_or_idx | path])
        end)

      node_matching ++ children_matching
    end)
    |> discard_nothing()
  end

  # Filter expressions
  defp do_eval(_root, _current_node, {:literal, value}, path), do: [{value, path}]

  defp do_eval(root, current_node, {op, left, right}, path) when op in @operators do
    left_res = evaluate(root, current_node, left, path) |> value()
    right_res = evaluate(root, current_node, right, path) |> value()

    case op do
      :eq -> left_res == right_res
      :neq -> left_res != right_res
      :gt -> type_strict_op(left_res, right_res, &Kernel.>/2)
      :lt -> type_strict_op(left_res, right_res, &Kernel.</2)
      :gte -> left_res == right_res or type_strict_op(left_res, right_res, &Kernel.>/2)
      :lte -> left_res == right_res or type_strict_op(left_res, right_res, &Kernel.</2)
    end
    |> to_node_boolean(path)
  end

  defp do_eval(root, current_node, {:not, expr}, path) do
    case evaluate(root, current_node, expr, path) do
      @node_false -> [{true, path}]
      _ -> @node_false
    end
  end

  defp do_eval(root, current_node, {:and, expr1, expr2}, path) do
    case evaluate(root, current_node, expr1, path) do
      @node_false -> @node_false
      _ -> evaluate(root, current_node, expr2, path) |> discard_nothing()
    end
  end

  defp do_eval(root, current_node, {:or, expr1, expr2}, path) do
    case evaluate(root, current_node, expr1, path) do
      @node_false -> evaluate(root, current_node, expr2, path)
      other -> other
    end
  end

  # Functions
  defp do_eval(root, current_node, {:function, :length, [expr]}, path) do
    case evaluate(root, current_node, expr, path) |> value() do
      [value] when is_list(value) -> [{length(value), path}]
      [value] when is_map(value) -> [{map_size(value), path}]
      [value] when is_binary(value) -> [{String.length(value), path}]
      _ -> [{:nothing, path}]
    end
  end

  defp do_eval(root, current_node, {:function, :value, [expr]}, path) do
    case evaluate(root, current_node, expr, path) do
      [{value, _}] -> [{value, path}]
      _ -> [{:nothing, path}]
    end
  end

  defp do_eval(root, current_node, {:function, :count, [expr]}, path) do
    [{length(evaluate(root, current_node, expr, path)), path}]
  end

  defp do_eval(root, current_node, {:function, :search, [expr1, expr2]}, path) do
    with [string] when is_binary(string) <- evaluate(root, current_node, expr1, path) |> value(),
         [regex] <- evaluate(root, current_node, expr2, path) |> value(),
         {:ok, pattern} <- compile_pattern(regex) do
      Regex.match?(pattern, string) |> to_node_boolean(path)
    else
      _ -> @node_false
    end
  end

  defp do_eval(root, current_node, {:function, :match, [expr1, expr2]}, path) do
    with [string] when is_binary(string) <- evaluate(root, current_node, expr1, path) |> value(),
         [regex] <- evaluate(root, current_node, expr2, path) |> value(),
         {:ok, pattern} <- compile_pattern(regex) do
      matches = pattern |> Regex.scan(string, capture: :first) |> Enum.map(fn [val] -> val end)
      to_node_boolean(string in matches, path)
    else
      _ -> @node_false
    end
  end

  defp evaluate_selectors(root, current_node, conditions, path) when is_list(conditions) do
    Enum.flat_map(conditions, &evaluate_selector(root, current_node, &1, path))
  end

  defp evaluate_selector(_root, node, :wildcard, path) do
    cond do
      is_list(node) ->
        node
        |> Enum.with_index()
        |> Enum.map(fn {val, idx} -> {val, [idx | path]} end)

      is_map(node) ->
        Enum.map(node, fn {k, v} -> {v, [k | path]} end)

      true ->
        [{:nothing, path}]
    end
  end

  defp evaluate_selector(_root, node, {:property, key}, path)
       when is_map(node) and is_map_key(node, key) do
    [{node[key], [key | path]}]
  end

  defp evaluate_selector(_root, _node, {:property, _}, path), do: [{:nothing, path}]

  defp evaluate_selector(_root, node, {:index, idx}, path) when is_list(node) do
    case Enum.at(node, idx) do
      nil -> [{:nothing, path}]
      value -> [{value, [to_positive(idx, length(node)) | path]}]
    end
  end

  defp evaluate_selector(_root, _node, {:index, _}, path), do: [{:nothing, path}]

  defp evaluate_selector(_root, v, {:slice, _, _, step}, path) when step == 0 or not is_list(v),
    do: [{:nothing, path}]

  defp evaluate_selector(_root, [], {:slice, _, _, _}, path), do: [{:nothing, path}]

  defp evaluate_selector(_, node, {:slice, start, stop, step}, path) when is_list(node) do
    Enum.map(Slice.apply(node, start, stop, step), fn {value, index} ->
      {value, [index | path]}
    end)
  end

  defp evaluate_selector(root, node, {:filter, expr}, path) when is_map(node) or is_list(node) do
    node
    |> iter()
    |> Enum.filter(fn {node, key_or_idx} ->
      true_expr?(root, node, expr, [key_or_idx | path])
    end)
    |> Enum.map(fn {node, key_or_idx} -> {node, [key_or_idx | path]} end)
  end

  defp evaluate_selector(_root, _node, {:filter, _}, path), do: [{:nothing, path}]

  defp iter(node) when is_list(node), do: Enum.with_index(node)
  defp iter(node) when is_map(node), do: Enum.map(node, fn {k, v} -> {v, k} end)
  defp iter(_node), do: []

  defp true_expr?(root, node, expr, path), do: not Enum.empty?(evaluate(root, node, expr, path))

  # Since JSON Path uses arrays to express true/false, we must convert some boolean
  # results (from ==, >, etc.) to an array with equivalent behavior
  defp to_node_boolean(true, path), do: [{true, path}]
  defp to_node_boolean(false, _path), do: []
  defp to_node_boolean([true], path), do: [{true, path}]
  defp to_node_boolean([], _path), do: []

  defp type_strict_op([l], [r], func) when is_binary(l) and is_binary(r), do: func.(l, r)
  defp type_strict_op([l], [r], func) when is_number(l) and is_number(r), do: func.(l, r)
  defp type_strict_op(_, _, _), do: false

  @spec discard_nothing(list(node)) :: list(node)
  defp discard_nothing(results), do: Enum.reject(results, &(elem(&1, 0) == :nothing))

  defp compile_pattern(%Regex{} = pattern), do: {:ok, pattern}
  defp compile_pattern(pattern) when is_binary(pattern), do: Regex.compile(pattern, "u")
  defp compile_pattern(_), do: [:nothing]

  defp value([]), do: []
  defp value([{val, _path}]), do: [val]
  defp value(vals) when is_list(vals), do: Enum.map(vals, &value/1)

  defp to_positive(index, _len) when index >= 0, do: index
  defp to_positive(index, len), do: len + index
end