Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix to support post a json request with non-object content #20

Merged
merged 1 commit into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions lib/oasis/plug/request_validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,31 @@ defmodule Oasis.Plug.RequestValidator do
end

defp process_body(
%{body_params: body_params, req_headers: req_headers, params: params} = conn,
%{body_params: %{"_json" => _body_params}, params: params} = conn,
body_schema
)
when is_map(body_schema) do

matched_schema =
req_headers
|> find_content_type()
|> schema_may_match_by_request(body_schema)
body_params = parse_and_validate_body_params(conn, body_schema)

body_params =
Oasis.Validator.parse_and_validate!(matched_schema, "body", "body_request", body_params)
if is_map(body_params) do
# Maybe the defined schema specification use the "_json" key as a property of an object.
params = Map.merge(params, body_params)
%{conn | body_params: body_params, params: params}
else
%{conn | body_params: body_params, params: body_params}
end
end

params = params |> Map.merge(body_params)
defp process_body(
conn,
body_schema
)
when is_map(body_schema) do

body_params = parse_and_validate_body_params(conn, body_schema)

params = conn.params |> Map.merge(body_params)

%{conn | body_params: body_params, params: params}
end
Expand All @@ -109,6 +120,13 @@ defmodule Oasis.Plug.RequestValidator do
conn
end

defp parse_and_validate_body_params(%{body_params: body_params, req_headers: req_headers}, body_schema) do
req_headers
|> find_content_type()
|> schema_may_match_by_request(body_schema)
|> Oasis.Validator.parse_and_validate!("body", "body_request", body_params)
end

defp parse_and_validate(schemas, input_params, use_in) when is_map(input_params) do
Enum.reduce(schemas, input_params, fn {param_name, definition}, acc ->
input_value = input_params[param_name]
Expand Down
11 changes: 11 additions & 0 deletions lib/oasis/validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ defmodule Oasis.Validator do
|> process_media_type(media_type, use_in, name, value)
end

defp do_parse_and_validate!(%{schema: %{"type" => type}} = json_schema_root, "body", param_name, %{"_json" => value})
when type == "string" and is_bitstring(value)
when type == "number" and is_number(value)
when type == "integer" and is_integer(value)
when type == "array" and is_list(value)
when type == "boolean" and is_boolean(value) do
# Since `Plug.Parsers.JSON` parses a non-map body content into a "_json" key to allow proper param merging, here
# will unwrap the "_json" key and format the input body params as a matched type to the defined OpenAPI specification.
do_parse_and_validate!(json_schema_root, "body", param_name, value)
end

defp do_parse_and_validate!(%{schema: schema} = json_schema_root, use_in, param_name, value) do
try do
Oasis.Parser.parse(schema, value)
Expand Down
50 changes: 49 additions & 1 deletion test/integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,55 @@ defmodule Oasis.IntegrationTest do
"Find body parameter `body_request` with error: Required property addresses was not present."
end

test "post application/json with non object type", %{url: url} do
start_supervised!({Finch, name: TestFinch})

headers = [{"content-type", "application/json"}]
body = "[{\"id\":1,\"name\":\"hello\"}]"

assert {:ok, response} = Finch.build(:post, "#{url}/test_post_json", headers, body) |> Finch.request(TestFinch)
body = Jason.decode!(response.body)
assert response.status == 200 and
body["body_params"] == [%{"id" => 1, "name" => "hello"}] and
body["body_params"] == body["params"]

headers = [{"content-type", "application/vnd.api-v1+json"}]
body = "1"

assert {:ok, response} = Finch.build(:post, "#{url}/test_post_json", headers, body) |> Finch.request(TestFinch)
body = Jason.decode!(response.body)
assert response.status == 200 and
body["body_params"] == 1 and
body["body_params"] == body["params"]

headers = [{"content-type", "application/vnd.api-v2+json"}]
body = "1.5"

assert {:ok, response} = Finch.build(:post, "#{url}/test_post_json", headers, body) |> Finch.request(TestFinch)
body = Jason.decode!(response.body)
assert response.status == 200 and
body["body_params"] == 1.5 and
body["body_params"] == body["params"]
end

test "post application/json with object type use _json key", %{url: url} do
start_supervised!({Finch, name: TestFinch})

headers = [{"content-type", "application/vnd.api-v3+json"}]
# valid `street_type` is ["Street", "Avenue", "Boulevard"]
body = "{\"_json\":{\"street_name\":\"S1\", \"street_type\":\"Avenue2\"}}"

assert {:ok, response} = Finch.build(:post, "#{url}/test_post_json", headers, body) |> Finch.request(TestFinch)
assert response.body == "Find body parameter `body_request` with error: Value is not allowed in enum."

body = "{\"_json\":{\"street_name\":\"S1\", \"id\":\"1\", \"street_type\":\"Avenue\"}}"
assert {:ok, response} = Finch.build(:post, "#{url}/test_post_json", headers, body) |> Finch.request(TestFinch)
body = Jason.decode!(response.body)
assert response.status == 200 and
body["body_params"]["_json"] == %{"street_name" => "S1", "street_type" => "Avenue", "id" => 1} and
body["body_params"] == body["params"]
end

test "delete request with body schema validation", %{url: url} do
start_supervised!({Finch, name: TestFinch})

Expand Down Expand Up @@ -715,7 +764,6 @@ defmodule Oasis.IntegrationTest do
end

test "verify hmac auth with body", %{url: url} do
IO.puts "url: #{url}"
start_supervised!({Finch, name: TestFinch})
c = Oasis.Test.Support.HMAC.case_with_body()

Expand Down
73 changes: 73 additions & 0 deletions test/support/gen/plug/pre_test_post_json.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
defmodule Oasis.Gen.Plug.PreTestPostJSON do
use Oasis.Controller
use Plug.ErrorHandler

plug(
Plug.Parsers,
parsers: [:json],
json_decoder: Jason,
pass: ["*/*"]
)

plug(
Oasis.Plug.RequestValidator,
body_schema: %{
"required" => true,
"content" => %{
"application/json" => %{
"schema" => %ExJsonSchema.Schema.Root{
schema: %{
"items" => %{
"type" => "object",
"properties" => %{
"id" => %{"type" => "integer"},
"name" => %{"type" => "string"}
}
},
"type" => "array"
}
}
},
"application/vnd.api-v1+json" => %{
"schema" => %ExJsonSchema.Schema.Root{
schema: %{
"type" => "integer"
}
}
},
"application/vnd.api-v2+json" => %{
"schema" => %ExJsonSchema.Schema.Root{
schema: %{
"type" => "number"
}
}
},
"application/vnd.api-v3+json" => %{
"schema" => %ExJsonSchema.Schema.Root{
schema: %{
"properties" => %{
"_json" => %{
"type" => "object",
"properties" => %{
"street_name" => %{"type" => "string"},
"street_type" => %{"enum" => ["Street", "Avenue", "Boulevard"]},
"id" => %{"type" => "integer"}
}
}
},
"type" => "object"
}
}
},

}
}
)

def call(conn, opts) do
conn |> super(opts) |> Oasis.Gen.Plug.TestPost.call(opts) |> halt()
end

defdelegate handle_errors(conn, error), to: Oasis.Gen.Plug.TestPost

end
4 changes: 4 additions & 0 deletions test/support/http_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ defmodule Oasis.HTTPServer.PlugRouter do
to: Oasis.Gen.Plug.PreTestPostMultipart
)

post("/test_post_json",
to: Oasis.Gen.Plug.PreTestPostJSON
)

post("/test_post_non_validate",
to: Oasis.Gen.Plug.TestPostNonValidate
)
Expand Down