lib/step_flow/controllers/step/helpers.ex

defmodule StepFlow.Step.Helpers do
  @moduledoc """
  The Helper Step context.
  """

  @doc """
  Retrieves a value on an Objecta and filtered by the key.
  """

  def get_value_in_parameters(object, key) do
    StepFlow.Map.get_by_key_or_atom(object, :parameters, [])
    |> Enum.filter(fn param ->
      StepFlow.Map.get_by_key_or_atom(param, :id) == key
    end)
    |> Enum.map(fn param ->
      StepFlow.Map.get_by_key_or_atom(
        param,
        :value,
        StepFlow.Map.get_by_key_or_atom(param, :default)
      )
    end)
  end

  @doc """
  Retrieves a value on an Object and filtered by the key and type.
  """
  def get_value_in_parameters_with_type(object, key, type) do
    StepFlow.Map.get_by_key_or_atom(object, :parameters, [])
    |> Enum.filter(fn param ->
      StepFlow.Map.get_by_key_or_atom(param, :id) == key &&
        StepFlow.Map.get_by_key_or_atom(param, :type) == type
    end)
    |> Enum.map(fn param ->
      StepFlow.Map.get_by_key_or_atom(
        param,
        :value,
        StepFlow.Map.get_by_key_or_atom(param, :default)
      )
    end)
  end

  def get_string_or_processed_template_value(
        workflow,
        step,
        dates,
        source_paths,
        key,
        default \\ ""
      ) do
    get_value_in_parameters_with_type(step, key, "string")
    |> List.first()
    |> case do
      nil ->
        get_value_in_parameters_with_type(step, key, "template")
        |> List.first()
        |> case do
          nil ->
            default

          template ->
            template
            |> template_process(workflow, step, dates, source_paths)
        end

      strng_value ->
        strng_value
    end
  end

  def get_jobs_destination_paths(jobs) do
    jobs
    |> Enum.map(fn job ->
      get_job_destination_paths(job)
    end)
    |> List.flatten()
    |> Enum.uniq()
    |> Enum.filter(fn path -> !is_nil(path) end)
  end

  def get_job_destination_paths(job) do
    destination_path = get_value_in_parameters(job, "destination_path")
    destination_paths = get_value_in_parameters(job, "destination_paths")

    destination_path ++ destination_paths
  end

  @doc """
  Filter a list of paths.

  ## Examples

      iex> StepFlow.Step.Helpers.filter_path_list(["path_1.ext1", "path2.ext2"], [%{"ends_with" => ".ext2"}])
      ["path2.ext2"]

      iex> StepFlow.Step.Helpers.filter_path_list(["path_1.ext1", "path2.ext2"], [%{ends_with: ".ext2"}])
      ["path2.ext2"]

  """
  def filter_path_list(source_paths, []), do: source_paths

  def filter_path_list(source_paths, [filter | filters]) do
    new_source_paths =
      case filter do
        %{ends_with: ends_with} ->
          Enum.filter(source_paths, fn path -> String.ends_with?(path, ends_with) end)

        %{"ends_with" => ends_with} ->
          Enum.filter(source_paths, fn path -> String.ends_with?(path, ends_with) end)
      end

    filter_path_list(new_source_paths, filters)
  end

  def filter_empty(source_paths) do
    source_paths
    |> Enum.filter(fn path -> !blank?(path) end)
  end

  def filter_empty_templated(defined_parameters) do
    defined_parameters
    |> Enum.filter(fn {_, value} -> value != "nil" and !blank?(value) end)
  end

  def filter_untemplated(parameters) do
    parameters
    |> Enum.filter(fn value -> !String.contains?(value, ["{", "}"]) end)
  end

  def get_step_requirements(jobs, step) do
    %{paths: get_required_paths(jobs, step)}
  end

  def get_required_paths(jobs, step) do
    # required does not refer to anything for now
    required_ids = StepFlow.Map.get_by_key_or_atom(step, :required, [])

    jobs
    |> Enum.filter(fn job -> job.step_id in required_ids end)
    |> get_jobs_destination_paths
  end

  def add_required_paths(requirements, paths) when is_list(paths) do
    Map.update(requirements, :paths, paths, fn cur_paths ->
      Enum.concat(cur_paths, paths)
      |> Enum.uniq()
    end)
  end

  def add_required_paths(requirements, path) do
    paths =
      Map.get(requirements, :paths, [])
      |> List.insert_at(-1, path)

    add_required_paths(requirements, paths)
  end

  def get_dates do
    now = Timex.now()

    %{
      date_time: Timex.format!(now, "%Y_%m_%d__%H_%M_%S", :strftime),
      date: Timex.format!(now, "%Y_%m_%d", :strftime),
      epoch: Timex.epoch()
    }
  end

  def get_work_directory(step) do
    StepFlow.Map.get_by_key_or_atom(step, :work_dir) ||
      StepFlow.Configuration.get_var_value(StepFlow, :workers_work_directory) ||
      ""
  end

  def get_base_directory(workflow, step) do
    get_work_directory(step) <> "/" <> Integer.to_string(workflow.id) <> "/"
  end

  def templates_process(_templates, _workflow, _step, _dates, result \\ [])
  def templates_process([], _workflow, _step, _dates, result), do: result

  def templates_process([template | templates], workflow, step, dates, result) do
    processed = intern_template_process(template, workflow, step, dates, [])
    result = List.insert_at(result, -1, processed)
    templates_process(templates, workflow, step, dates, result)
  end

  def templates_process(template, workflow, step, dates, _result) do
    intern_template_process(template, workflow, step, dates, [])
  end

  def template_process(template, workflow, step, dates, nil) do
    intern_template_process(template, workflow, step, dates, [])
  end

  def template_process(template, workflow, step, dates, source_path)
      when is_binary(source_path) do
    filename = Path.basename(source_path)
    extension = Path.extname(source_path)
    name = Path.basename(source_path, extension)

    source_keywords =
      Keyword.new()
      |> Keyword.put(:source_path, source_path)
      |> Keyword.put(:filename, filename)
      |> Keyword.put(:extension, extension)
      |> Keyword.put(:name, name)

    intern_template_process(template, workflow, step, dates, source_keywords)
  end

  def template_process(template, workflow, step, dates, source_paths)
      when is_list(source_paths) do
    source_keywords =
      Keyword.new()
      |> Keyword.put(:source_paths, source_paths)

    intern_template_process(template, workflow, step, dates, source_keywords)
  end

  defp intern_template_process(template, workflow, step, dates, source_keywords) do
    step_parameters =
      StepFlow.Map.get_by_key_or_atom(step, :parameters, [])
      |> Enum.filter(fn item ->
        StepFlow.Map.get_by_key_or_atom(item, :type) in [
          "string",
          "integer"
        ]
      end)
      |> Enum.map(fn item ->
        identifier =
          StepFlow.Map.get_by_key_or_atom(item, :id)
          |> String.to_atom()

        value =
          StepFlow.Map.get_by_key_or_atom(
            item,
            :value,
            StepFlow.Map.get_by_key_or_atom(item, :default)
          )

        {identifier, value}
      end)

    defined_parameters =
      workflow.parameters
      |> Enum.filter(fn item ->
        StepFlow.Map.get_by_key_or_atom(item, :type) in [
          "string",
          "array_of_strings",
          "integer",
          "array_of_integers"
        ]
      end)
      |> Enum.map(fn item ->
        identifier =
          StepFlow.Map.get_by_key_or_atom(item, :id)
          |> String.to_atom()

        value =
          StepFlow.Map.get_by_key_or_atom(
            item,
            :value,
            StepFlow.Map.get_by_key_or_atom(item, :default)
          )
          |> convert_to_string()

        {identifier, value}
      end)
      |> Keyword.put(:workflow_id, workflow.id)
      |> Keyword.put(:workflow_reference, workflow.reference)
      |> Keyword.put(:step_name, StepFlow.Map.get_by_key_or_atom(step, :name))
      |> Keyword.put(:work_directory, get_work_directory(step))
      |> Keyword.put(:date_time, dates.date_time)
      |> Keyword.put(:date, dates.date)
      |> Keyword.merge(source_keywords)
      |> Keyword.merge(step_parameters)

    defined_parameters
    |> filter_empty_templated()
    |> Keyword.keys()
    |> replace(template)
    |> EEx.eval_string(defined_parameters)
    |> unstringify_array()
  end

  defp replace([], template), do: template

  defp replace([key | keys], template) do
    template =
      String.replace(
        template,
        "{" <> Atom.to_string(key) <> "}",
        "<%= " <> Atom.to_string(key) <> "%>"
      )

    replace(keys, template)
  end

  defp unstringify_array(template) do
    # "[131e508b-d9a0-4811-be34-1e8c9ec11af6" will crash and probably not much to do
    if String.starts_with?(template, "[") do
      case JSON.decode(template) do
        {:ok, array_template} ->
          if is_list(array_template) do
            array_template
          else
            template
          end

        {_, _} ->
          template
      end
    else
      template
    end
  end

  defp convert_to_string(value) when is_bitstring(value), do: value
  defp convert_to_string(value), do: "#{inspect(value)}"

  # https://stackoverflow.com/questions/52482825/how-do-you-pattern-match-for-nil-or-a-blank-string-in-elixir
  defp blank?(path),
    do: "" == path |> to_string() |> String.trim()
end