Skip to content

Commit

Permalink
Merge pull request #59 from brainn-co/fix#49/handle-plug-upload-struct
Browse files Browse the repository at this point in the history
Fix#49/handle plug upload struct
  • Loading branch information
Danielwsx64 authored Dec 1, 2020
2 parents e385d9e + 7fa0d58 commit b59e4b3
Show file tree
Hide file tree
Showing 16 changed files with 338 additions and 29 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.7.9] - 2020-11-30

### Fixed

- Handle Plug.Upload and generate doc as specified by formats (Swagger and ApiBlueprint)

## [0.7.8] - 2020-11-27

### Fixed
Expand Down Expand Up @@ -113,7 +119,8 @@ Improve CI/CD flow:
- New "tags" parameter to operations object in Swagger format.
- Add changelog and Makefile.

[unreleased]: https://github.com/brainn-co/xcribe/compare/v0.7.8...master
[unreleased]: https://github.com/brainn-co/xcribe/compare/v0.7.9...master
[0.7.9]: https://github.com/brainn-co/xcribe/compare/v0.7.8...v0.7.9
[0.7.8]: https://github.com/brainn-co/xcribe/compare/v0.7.7...v0.7.8
[0.7.7]: https://github.com/brainn-co/xcribe/compare/v0.7.6...v0.7.7
[0.7.6]: https://github.com/brainn-co/xcribe/compare/v0.7.5...v0.7.6
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ mix.exs
```elixir
def deps do
[
{:xcribe, "~> 0.7.8"}
{:xcribe, "~> 0.7.9"}
]
end
```
Expand Down
26 changes: 26 additions & 0 deletions lib/xcribe/api_blueprint/apib.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Xcribe.ApiBlueprint.APIB do
@moduledoc false

alias Xcribe.ApiBlueprint.Multipart
alias Xcribe.JSON

@metadata_template "FORMAT: 1A\nHOST: --host--\n\n# --name--\n--description--\n\n"
Expand All @@ -16,6 +17,8 @@ defmodule Xcribe.ApiBlueprint.APIB do
@schema_template " + Schema\n\n--schema--\n\n"
@body_template " + Body\n\n--body--\n\n"

@multipart_template "\n\n--boundary--\nContent-Disposition: form-data; name=\"--name--\"\nContent-Type: --content_type--\n\n--value--"

@tab_size 4

def encode(%{groups: groups} = struct) do
Expand Down Expand Up @@ -103,6 +106,13 @@ defmodule Xcribe.ApiBlueprint.APIB do
)
end

def body(%Multipart{} = multipart) do
apply_template(
@body_template,
body: build_multipart_body(multipart)
)
end

def body(body) when body == %{}, do: ""

def body(body) do
Expand All @@ -112,6 +122,22 @@ defmodule Xcribe.ApiBlueprint.APIB do
)
end

defp build_multipart_body(multipart),
do: Enum.reduce(multipart.parts, "", &build_multipart_template(&1, &2, multipart.boundary))

defp build_multipart_template(part, acc, boundary) do
part_template =
apply_template(
@multipart_template,
boundary: boundary,
name: part.name,
content_type: part.content_type,
value: part.value
)

acc <> apply_tab(part_template, 3)
end

defp action_uri(uri, query_parameters),
do: Enum.reduce(query_parameters, uri, &add_query_parameter/2)

Expand Down
36 changes: 35 additions & 1 deletion lib/xcribe/api_blueprint/formatter.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Xcribe.ApiBlueprint.Formatter do
@moduledoc false

alias Plug.Upload
alias Xcribe.ApiBlueprint.Multipart
alias Xcribe.{ContentDecoder, JsonSchema, Request}

import Xcribe.Helpers.Formatter
Expand Down Expand Up @@ -56,7 +58,7 @@ defmodule Xcribe.ApiBlueprint.Formatter do
desc => %{
content_type: content_type(headers),
headers: headers(headers),
body: request.request_body,
body: request_body(request),
schema: request_schema(request),
response: response_object(request)
}
Expand Down Expand Up @@ -108,6 +110,14 @@ defmodule Xcribe.ApiBlueprint.Formatter do
|> json_schema_for(body)
end

def request_body(%Request{request_body: body}) when body == %{}, do: %{}

def request_body(%Request{request_body: body, header_params: headers}) do
headers
|> content_type()
|> body_data_for(headers, body)
end

def action_name(%Request{action: action, resource: resource}),
do: "#{capitalize_all_words(resource)}#{action}"

Expand Down Expand Up @@ -192,6 +202,30 @@ defmodule Xcribe.ApiBlueprint.Formatter do

defp json_schema_for(_content_type, _body), do: %{}

defp body_data_for("multipart/form-data", headers, body) when is_map(body) do
%Multipart{
parts: Enum.reduce(body, [], &data_schema/2),
boundary: content_type_boundary(headers)
}
end

defp body_data_for(_content_type, _headers, body), do: body

defp data_schema({key, %Upload{} = upload}, acc) do
[
%{
content_type: upload.content_type,
name: key,
value: "image-binary",
filename: upload.filename
}
| acc
]
end

defp data_schema({key, value}, acc),
do: [%{content_type: "text/plain", name: key, value: value} | acc]

defp reduce_path_params({param, value}, parameters) do
Map.put(
parameters,
Expand Down
3 changes: 3 additions & 0 deletions lib/xcribe/api_blueprint/multipart.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule Xcribe.ApiBlueprint.Multipart do
defstruct [:parts, :boundary]
end
42 changes: 27 additions & 15 deletions lib/xcribe/helpers/formatter.ex
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
defmodule Xcribe.Helpers.Formatter do
@moduledoc false

@content_type_regex ~r{^(\w*\/\w*(\.\w*\+\w*)?);?.*}

@content_type_regex ~r/^(\w*\/[\w-]*(\.\w*\+\w*)?);?.*/
@content_type_boundary_regex ~r/boundary=(.*);?/
@doc """
return the content type by a list of header params
### Options:
* `default`: a value to be returned when not found content-type header.
"""
def content_type(headers, opts \\ []) when is_list(headers) do
Enum.reduce_while(headers, Keyword.get(opts, :default), &find_content_type/2)
headers
|> Enum.find_value(Keyword.get(opts, :default), &find_content_type/1)
|> handle_regex_match()
end

@doc """
return the authorization header from a list of headers
return the content type boundary
"""
def authorization(headers) when is_list(headers) do
Enum.reduce_while(headers, nil, &find_authorization/2)

def content_type_boundary(headers) when is_list(headers) do
headers
|> Enum.find_value(&find_content_type_boundary/1)
|> handle_regex_match()
end

@doc """
return the authorization header from a list of headers
"""
def authorization(headers) when is_list(headers),
do: Enum.reduce_while(headers, nil, &find_authorization/2)

@doc """
Format the path params.
Expand All @@ -29,18 +40,19 @@ defmodule Xcribe.Helpers.Formatter do
def format_path_parameter(name),
do: " #{name}" |> Macro.camelize() |> String.replace_prefix(" ", "")

defp find_content_type({"content-type", value}, _default) do
@content_type_regex
|> Regex.run(value, capture: :all_but_first)
|> handle_content_type_regex()
end
defp find_content_type({"content-type", value}),
do: Regex.run(@content_type_regex, value, capture: :all_but_first)

defp find_content_type(_tuple), do: nil

defp find_content_type_boundary({"content-type", value}),
do: Regex.run(@content_type_boundary_regex, value, capture: :all_but_first)

defp find_content_type(_header, default), do: {:cont, default}
defp find_content_type_boundary(_tuple), do: nil

defp find_authorization({"authorization", value}, _acc), do: {:halt, value}
defp find_authorization(_header, _acc), do: {:cont, nil}

defp handle_content_type_regex(nil), do: {:halt, nil}
defp handle_content_type_regex([content_type]), do: {:halt, content_type}
defp handle_content_type_regex([content_type | _vnd_spec]), do: {:halt, content_type}
defp handle_regex_match([value | _vnd_spec]), do: value
defp handle_regex_match(value), do: value
end
10 changes: 10 additions & 0 deletions lib/xcribe/json_schema.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Xcribe.JsonSchema do
@moduledoc false

alias Plug.Upload

@doc """
Return the type of given data
"""
Expand Down Expand Up @@ -35,6 +37,14 @@ defmodule Xcribe.JsonSchema do
@opt_no_title {:title, false}
@opt_example {:example, true}

defp schema_object_for({title, %Upload{}}, opts) do
schema_add_title(
%{type: "string", format: "binary"},
title,
@opt_no_title in opts
)
end

defp schema_object_for({title, value}, opts) when is_map(value) do
%{type: "object"}
|> schema_add_title(title, @opt_no_title in opts)
Expand Down
3 changes: 3 additions & 0 deletions lib/xcribe/request/validator.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule Xcribe.Request.Validator do
alias Plug.Upload
alias Xcribe.Request
alias Xcribe.Request.Error

Expand All @@ -20,6 +21,8 @@ defmodule Xcribe.Request.Validator do
|> handle_validate_params(request)
end

defp find_struct(%Upload{}), do: :ok

defp find_struct(%{__struct__: module}) do
%Error{
type: :validation,
Expand Down
29 changes: 23 additions & 6 deletions lib/xcribe/swagger/formatter.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Xcribe.Swagger.Formatter do
@moduledoc false

alias Plug.Upload
alias Xcribe.{ContentDecoder, JsonSchema, Request}

import Xcribe.Helpers.Formatter, only: [content_type: 1, authorization: 1]
Expand Down Expand Up @@ -188,14 +189,30 @@ defmodule Xcribe.Swagger.Formatter do
defp media_type_object(headers, content) do
media_type = content_type(headers)

%{
description: "",
content: %{
media_type => %{schema: build_schema_for_media(content, media_type)}
}
}
media_type_schema =
%{}
|> Map.put(:schema, build_schema_for_media(content, media_type))
|> add_enconding_if_needed(content)

%{description: "", content: %{media_type => media_type_schema}}
end

defp add_enconding_if_needed(schema, content) when is_map(content) do
content
|> Map.keys()
|> Enum.reduce(schema, &enconding_for(content[&1], &2, &1))
end

defp add_enconding_if_needed(schema, _content), do: schema

defp enconding_for(%Upload{} = upload, schema, property) do
encoding = %{property => %{contentType: upload.content_type}}

Map.update(schema, :encoding, encoding, &Map.merge(&1, encoding))
end

defp enconding_for(_value, schema, _property), do: schema

defp build_schema_for_media(content, content_type) when is_binary(content) do
content
|> ContentDecoder.decode!(content_type)
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Xcribe.MixProject do
use Mix.Project

@version "0.7.8"
@version "0.7.9"
@description "A lib to generate API documentation from test specs"
@links %{"GitHub" => "https://github.com/brainn-co/xcribe"}

Expand Down
38 changes: 37 additions & 1 deletion test/xcribe/api_blueprint/apib_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Xcribe.ApiBlueprint.APIBTest do
use ExUnit.Case, async: false

alias Xcribe.ApiBlueprint
alias Xcribe.ApiBlueprint.{APIB, Formatter}
alias Xcribe.ApiBlueprint.{APIB, Formatter, Multipart}
alias Xcribe.Support.RequestsGenerator

setup do
Expand Down Expand Up @@ -243,6 +243,42 @@ defmodule Xcribe.ApiBlueprint.APIBTest do
test "empty body" do
assert APIB.body(%{}) == ""
end

test "multipart body" do
body = %Multipart{
boundary: "---boundary",
parts: [
%{content_type: "text/plain", name: "user_id", value: "123"},
%{
content_type: "image/png",
filename: "screenshot.png",
name: "file",
value: "image-binary"
}
]
}

expected = """
+ Body
---boundary
Content-Disposition: form-data; name="user_id"
Content-Type: text/plain
123
---boundary
Content-Disposition: form-data; name="file"
Content-Type: image/png
image-binary
"""

assert APIB.body(body) == expected
end
end

describe "full_response/1" do
Expand Down
Loading

0 comments on commit b59e4b3

Please sign in to comment.