| 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 |
|
|