defmodule Wiki.Ores do
@moduledoc """
This module provides an adapter for the [ORES](https://www.mediawiki.org/wiki/ORES) scoring service.
## Examples
```elixir
"enwiki"
|> Wiki.Ores.new()
|> Wiki.Ores.request(
models: ["damaging", "wp10"],
revids: 456789
)
|> Jason.encode!(pretty: true)
|> IO.puts()
```
"""
alias Wiki.Util
# TODO:
# * Wrap models?
# * Chunk at 50 revisions per request.
# * Offer parallelism up to 4.
@doc """
Create a new ORES client.
## Arguments
- `project` - Short code for the wiki where your articles appear. For example, "enwiki" for English Wikipedia.
## Return value
Returns an opaque client object, which should be passed to `request/2`.
"""
@spec new(String.t()) :: Tesla.Client.t()
def new(project) do
url = endpoint() <> project <> "/"
client([
{Tesla.Middleware.BaseUrl, url}
])
end
defp endpoint do
Application.get_env(:wiki_elixir, :ores_endpoint, "https://ores.wikimedia.org/v3/scores/")
end
@doc """
Make an ORES request.
Don't request scores for more than 50 revisions per request.
## Arguments
- `client` - Client object as returned by `new/1`.
- `params` - Keyword list of query parameters,
- `:models` - Learning models to query. These vary per wiki, see the [support matrix](https://tools.wmflabs.org/ores-support-checklist/)
for availability and to read about what each model is scoring. Multiple models can be passed as a list, for example,
`[:damaging, :wp10]`, or as a single atom, `:damaging`.
- `:revids` - Revision IDs to score, as a single integer or as a list.
"""
@spec request(Tesla.Client.t(), keyword | map) :: map
def request(client, params) do
response = Tesla.get!(client, "/", query: normalize(params))
assert_success(response)
response.body
end
@spec normalize(keyword) :: keyword
defp normalize(params) do
params
|> pipe_lists()
end
defp pipe_lists(params) do
params
|> Enum.map(fn
{k, v} when is_list(v) -> {k, Enum.join(v, "|")}
entry -> entry
end)
end
defp assert_success(result) do
cond do
result.body in [nil, "", %{}] ->
raise "Empty response"
not is_map(result.body) ->
raise "Malformed response, HTTP status #{result.status}"
error = result.body["error"] ->
raise summarize_error(error)
result.status < 200 or result.status >= 300 ->
raise "Error received with HTTP status #{result.status}"
true ->
nil
end
end
defp summarize_error(error) do
error["message"] ||
error["code"] ||
"unknown"
end
@spec client(list) :: Tesla.Client.t()
defp client(extra) do
middleware =
extra ++
[
{Tesla.Middleware.Compression, format: "gzip"},
{Tesla.Middleware.Headers,
[
{"user-agent", Util.user_agent()}
]},
Tesla.Middleware.JSON
# Debugging only:
# Tesla.Middleware.Logger
]
Tesla.client(middleware, Util.default_adapter())
end
end