Skip to content

Commit

Permalink
Fix to support post a json request with non-object content (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
xinz authored Feb 7, 2023
1 parent 292bf63 commit ea76ff0
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 9 deletions.
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

0 comments on commit ea76ff0

Please sign in to comment.