# Copyright (C) 2020 by the Georgia Tech Research Institute (GTRI) # This software may be modified and distributed under the terms of # the BSD 3-Clause license. See the LICENSE file for details. defmodule Helpers do @moduledoc """ Collection of generic helper functions. """ @doc """ get_slug/1: extracts the slug from the provided URI argument and returns the path # Example iex(1)> {:ok, slug} = Helpers.get_slug("https://github.com/kitplummer/xmpprails") {:ok, "kitplummer/xmpprails"} iex(2)> slug "kitplummer/xmpprails" """ @spec get_slug(String.t) :: {:ok, String.t} | {:error, String.t} def get_slug(url) do uri = URI.parse(url) case uri.path do nil -> {:error, "invalid source URL"} path -> path = String.slice(path, 1..-1) {:ok, path} end end @doc """ split_slug/1: splits apart the username and repo from a git slug returning discrete stings. ## Examples iex(6)> {:ok, org, repo} = Helpers.split_slug("kitplummer/xmpprails") {:ok, "kitplummer", "xmpprails"} iex(7)> org "kitplummer" iex(8)> repo "xmpprails" """ @spec split_slug(String.t) :: {:ok, String.t, String.t} | {:error, String.t} def split_slug(slug) do if String.contains?(slug, "/") do v = String.split(slug, "/") {:ok, Enum.at(v, 0), Enum.at(v, 1)} else {:error, "bad_slug"} end end @spec count_forward_slashes(String.t) :: non_neg_integer def count_forward_slashes(url) do url |> String.graphemes() |> Enum.count(&(&1 == "/")) end @doc """ validate_url/1: validates field is a valid url. ## Examples iex> "https:://www.url.com" ...> |> Helpers.validate_url() {:error, "invalid URI path"} iex> "https://github.com/" ...> |> Helpers.validate_url() :ok iex> '"https://"https://www.google.com"' ...> |> Helpers.validate_url() {:error, "invalid URI"} iex> "zipbooks.com" ...> |> Helpers.validate_url() {:error, "invalid URI"} iex> "https://zipbooks..com" ...> |> Helpers.validate_url() {:error, "invalid URI host"} """ @spec validate_url(String.t) :: :ok | {:error, String.t} def validate_url(url) do try do with :ok <- validate_scheme(url), :ok <- validate_host(url), do: :ok rescue FunctionClauseError -> {:error, "invalid URI"} end end @doc """ validate_urls/1: validates a list of urls ## Examples iex> ["http://www.google.com","http://www.test.com"] ...> |> Helpers.validate_urls() :ok iex> ["https://zipbooks..com", "http://www.test.com"] ...> |> Helpers.validate_urls() {:error, "invalid URI"} iex> ["https//github.com/kitplummer/xmpp4rails","https://www.zipbooks.com", "http://www.test.com"] ...> |> Helpers.validate_urls() {:error, "invalid URI"} iex> "https://zipbooks.com" ...> |> Helpers.validate_urls() {:error, "invalid URI"} """ @spec validate_urls([String.t]) :: :ok | {:error, String.t} def validate_urls(urls) do try do if !is_list(urls), do: throw(:break) Enum.each(urls, fn url -> if :ok == validate_url(url), do: :ok, else: throw(:break) end) catch :break -> {:error, "invalid URI"} end end # Found a rare case. https://https://www.google.com is a valid # URI, which makes 'https' the host, which apparently resolves. # Killed way too many cells chasing this edge case: # iex(1)> :inet.gethostbyname(Kernel.to_charlist "https") # {:ok, # {:hostent, 'https.cust.blueprintrf.com', [], :inet, 4, # [{23, 202, 231, 167}, {23, 217, 138, 108}]}} # oh well i guess, will handle the issue downstream i guess. @spec validate_host(String.t) :: :ok | {:error, String.t} defp validate_host(url) do case URI.parse(url) do %URI{host: host, path: path} -> cond do host == nil -> case File.dir?(path) do true -> :ok false -> {:error, "invalid URI path"} end host == "" -> case File.dir?(path) do true -> :ok false -> {:error, "invalid URI path"} end host != "" -> case :inet.gethostbyname(Kernel.to_charlist(host)) do {:ok, _} -> :ok {:error, _} -> {:error, "invalid URI host"} end end end end @spec validate_scheme(String.t) :: :ok | {:error, String.t} defp validate_scheme(url) do case URI.parse(url) do %URI{scheme: nil} -> {:error, "invalid URI"} %URI{scheme: scheme} -> case scheme do "https" -> :ok "http" -> :ok "file" -> :ok _ -> {:error, "invalid URI scheme"} end end end @doc """ convert_config_to_list/1: takes in Application.get_all_env(:app) and returns a list of maps, to be encoded as JSON. Since JSON doesn't have an equivalent tuple type the libs all bonk on encoding config values. """ @spec convert_config_to_list([any]) :: map def convert_config_to_list(config) do Enum.into(config, %{}) |> Map.delete(:jobs_per_core_max) end @doc """ remove_git_prefix/1: removes the git+ prefix found in some public Git URLs """ @spec remove_git_prefix(String.t) :: String.t def remove_git_prefix(url) do String.trim_leading(url, "git+") end end