Packages
ash_json_api
1.7.1
1.7.1
1.7.0
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0-rc.2
1.6.0-rc.1
1.6.0-rc.0
1.5.1
1.5.0
1.4.45
1.4.44
1.4.43
1.4.42
1.4.41
1.4.40
1.4.39
1.4.38
1.4.37
1.4.36
1.4.35
1.4.34
1.4.33
1.4.32
1.4.31
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.13
1.4.12
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.8
1.3.7
1.3.6
1.3.5
1.3.4
retired
1.3.3
1.3.2
1.3.1
1.3.0
1.2.2
1.2.1
1.2.0
1.1.2
1.1.1
1.1.0
1.0.0
1.0.0-rc.6
1.0.0-rc.5
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.34.2
0.34.1
0.34.0
0.33.1
0.33.0
0.32.1
0.32.0
0.31.3
0.31.2
retired
0.31.1
0.31.0
0.30.1
0.30.0-rc.4
0.30.0-rc.3
0.30.0-rc.2
0.30.0-rc.1
0.30.0-rc.0
0.29.1
0.29.0
0.28.6
0.28.5
0.28.4
0.28.3
0.28.2
0.28.1
0.28.0
0.27.6
0.27.5
0.27.1
0.27.0
0.25.0
0.24.4
0.24.2
0.24.1
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.1
0.10.0
0.9.0
0.8.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.4
0.2.3
0.2.1
0.2.0
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
The JSON:API extension for the Ash Framework.
Current section
Files
Jump to
Current section
Files
lib/ash_json_api/plug/parser.ex
# SPDX-FileCopyrightText: 2019 ash_json_api contributors <https://github.com/ash-project/ash_json_api/graphs/contributors>
#
# SPDX-License-Identifier: MIT
defmodule AshJsonApi.Plug.Parser do
@moduledoc """
Extracts ash multipart request body.
For use with `Plug.Parsers`, as in the example below.
## Examples
Should be used with `Plug.Parsers`:
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json, #{inspect(__MODULE__)}],
pass: ["*/*"],
json_decoder: Jason
## Protocol
To use files in your request, send a
[multipart](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#multipart)
with the content type `multipart/x.ash+form-data`. The request MUST
contain a JSON object with the key `data` and the value of the object you want
to send.
The request MAY contain other keys with the value of the file you want to
send. The parser will walk through all of the `data` JSON and replace each
string equal to a part name with the content of the part. This means that if
you have a part named `users_csv` and a key in the `data` JSON object with the
value `users_csv`, the parser will replace the string with the content of the
part.
> #### Conflicting Part names {:.warning}
>
> Ensure that each part name is unique and does not naturally occur inside as
> a string in the `data` JSON object. If a part name is found in the `data`
> JSON object, the parser will replace it with the content of the part.
>
> It is recommended to use a unique value like a UUID as the part name.
## Example HTTP Message
```
POST /action
Host: example.com
Content-Length: 2740
Content-Type: multipart/x.ash+form-data; boundary=abcde12345
--abcde12345
Content-Disposition: form-data; name="data"
Content-Type: application/vnd.api+json
{"users": "users_csv", "meatdata": "metadata_json"}
--abcde12345
Content-Disposition: form-data; name="users_csv"; filename="users.csv"
Content-Type: text/csv
[file content goes here]
--abcde12345
Content-Disposition: form-data; name="metadata_json"; filename="metadata.json"
Content-Type: application/json
[file content goes there]
--abcde12345--
```
"""
alias Plug.Parsers.JSON
alias Plug.Parsers.MULTIPART
@behaviour Plug.Parsers
@typep json_node() ::
integer()
| binary()
| float()
| boolean()
| nil
| [json_node()]
| %{optional(String.t()) => json_node()}
@typep part_acc() :: {:ok, json_node(), Plug.Conn.Query.decoder(), Plug.Conn.t()}
@doc false
@impl Plug.Parsers
def init(opts) do
json_opts =
opts
|> Keyword.put_new(:body_reader, {__MODULE__, :read_part, []})
|> JSON.init()
opts
|> Keyword.put_new(:multipart_to_params, {__MODULE__, :multipart_to_params, [json_opts]})
|> MULTIPART.init()
end
@doc false
@impl Plug.Parsers
def parse(conn, "multipart", "x.ash+form-data", headers, opts),
do:
MULTIPART.parse(
conn,
"multipart",
"mixed",
headers,
opts
)
def parse(conn, _type, _subtype, _headers, _opts), do: {:next, conn}
@doc false
def read_part(%Plug.Conn{private: %{__MODULE__ => part}} = conn, _opts),
do: {:ok, part, %Plug.Conn{conn | private: Map.delete(conn.private, __MODULE__)}}
@doc false
def multipart_to_params(parts, conn, json_opts) do
parts
|> Enum.reverse()
|> Enum.reduce_while(
{:ok, %{}, Plug.Conn.Query.decode_init(), conn},
&reduce_part(&1, &2, json_opts)
)
|> case do
{:ok, data, acc, conn} ->
{:ok, %{"data" => integrate_uploads(data, Plug.Conn.Query.decode_done(acc))}, conn}
{:error, reason} ->
{:error, reason}
end
end
@spec reduce_part(
part :: {name :: String.t(), headers :: Plug.Conn.headers(), body :: Plug.Upload.t()},
part_acc :: part_acc(),
json_opts :: term(),
force_generic :: boolean()
) :: {:cont, part_acc()} | {:halt, {:error, term()}}
defp reduce_part(part, acc, json_opts, force_generic \\ false)
# sobelow_skip ["Traversal.FileModule"]
defp reduce_part(
{"data", part_headers, %Plug.Upload{path: path} = body},
{:ok, data, acc, conn},
json_opts,
false
)
when data == %{} do
%Plug.Conn{} = conn
with {:ok, type, subtype, _params} <- extract_part_type(part_headers),
{:ok, content} <- File.read(path),
{:ok, data, conn} <-
JSON.parse(
%{conn | private: Map.put(conn.private, __MODULE__, content)},
type,
subtype,
part_headers,
json_opts
) do
{:cont, {:ok, data, acc, conn}}
else
{:error, :invalid_mime_type} ->
reduce_part({"data", part_headers, body}, {:ok, data, acc, conn}, json_opts, true)
{:error, :no_content_type} ->
reduce_part({"data", part_headers, body}, {:ok, data, acc, conn}, json_opts, true)
{:next, conn} ->
reduce_part({"data", part_headers, body}, {:ok, data, acc, conn}, json_opts, true)
{:error, reason} ->
{:halt, {:error, reason}}
end
end
defp reduce_part({name, _headers, body}, {:ok, data, acc, conn}, _json_opts, _force_generic),
do: {:cont, {:ok, data, Plug.Conn.Query.decode_each({name, body}, acc), conn}}
@spec extract_part_type(part_headers :: Plug.Conn.headers()) ::
{:ok, type :: binary(), subtype :: binary(), params :: Plug.Conn.Utils.params()}
| {:error, :invalid_mime_type | :no_content_type}
defp extract_part_type(part_headers) do
case List.keyfind(part_headers, "content-type", 0) do
{"content-type", content_type} ->
case Plug.Conn.Utils.content_type(content_type) do
{:ok, type, subtype, params} -> {:ok, type, subtype, params}
:error -> {:error, :invalid_mime_type}
end
_ ->
{:error, :no_content_type}
end
end
@spec integrate_uploads(
node :: json_node(),
uploads :: %{optional(String.t()) => Plug.Upload.t()}
) :: json_node()
defp integrate_uploads(node, uploads) when is_map(node),
do: Map.new(node, fn {k, v} -> {k, integrate_uploads(v, uploads)} end)
defp integrate_uploads(node, uploads) when is_list(node),
do: Enum.map(node, &integrate_uploads(&1, uploads))
defp integrate_uploads(node, uploads) when is_binary(node), do: Map.get(uploads, node, node)
defp integrate_uploads(node, _uploads), do: node
end