File size: 2,052 Bytes
8da2481
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
defmodule Plausible.Stats.JSONSchema do
  @moduledoc """
  Module for validating query parameters against JSON schema.

  Note that `internal` queries expose some metrics, filter types and other features not
  available on the public API.
  """
  use Plausible
  alias Plausible.Stats.JSONSchema.Utils
  alias Plausible.Stats.QueryError

  @json_schema_filepath "priv/json-schemas/query-api-schema.json"
  @external_resource @json_schema_filepath

  @raw_schema Application.app_dir(:plausible, @json_schema_filepath)
              |> File.read!()
              |> Jason.decode!()
              |> Utils.traverse(fn
                %{"$comment" => "only :ee"} = value ->
                  if(ee?(), do: Map.delete(value, "$comment"), else: :remove)

                value ->
                  value
              end)

  def raw_schema(), do: @raw_schema

  @query_schema ExJsonSchema.Schema.resolve(@raw_schema)

  def validate(params) do
    case ExJsonSchema.Validator.validate(@query_schema, params) do
      :ok ->
        :ok

      {:error, errors} ->
        {:error,
         %QueryError{code: :failed_schema_validation, message: format_errors(errors, params)}}
    end
  end

  defp format_errors(errors, params) do
    errors
    |> Enum.map_join("\n", fn {error, path} ->
      value = JSONPointer.get!(params, path)

      "#{path}: #{reword(path, error, value)}"
    end)
  end

  @no_matches "Expected exactly one of the schemata to match, but none of them did."

  defp reword("#/dimensions/" <> _, @no_matches, value), do: "Invalid dimension #{i(value)}"
  defp reword("#/metrics/" <> _, @no_matches, value), do: "Invalid metric #{i(value)}"
  defp reword("#/filters/" <> _, @no_matches, value), do: "Invalid filter #{i(value)}"
  defp reword("#/date_range", @no_matches, value), do: "Invalid date range #{i(value)}"

  defp reword("#/order_by/" <> _, @no_matches, value) do
    "Invalid value in order_by #{i(value)}"
  end

  defp reword(_path, error, _value), do: error

  defp i(value), do: inspect(value, charlists: :as_lists)
end