Skip to content

Commit b59e4b3

Browse files
authored
Merge pull request #59 from brainn-co/fix#49/handle-plug-upload-struct
Fix#49/handle plug upload struct
2 parents e385d9e + 7fa0d58 commit b59e4b3

16 files changed

Lines changed: 338 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.7.9] - 2020-11-30
11+
12+
### Fixed
13+
14+
- Handle Plug.Upload and generate doc as specified by formats (Swagger and ApiBlueprint)
15+
1016
## [0.7.8] - 2020-11-27
1117

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

116-
[unreleased]: https://github.com/brainn-co/xcribe/compare/v0.7.8...master
122+
[unreleased]: https://github.com/brainn-co/xcribe/compare/v0.7.9...master
123+
[0.7.9]: https://github.com/brainn-co/xcribe/compare/v0.7.8...v0.7.9
117124
[0.7.8]: https://github.com/brainn-co/xcribe/compare/v0.7.7...v0.7.8
118125
[0.7.7]: https://github.com/brainn-co/xcribe/compare/v0.7.6...v0.7.7
119126
[0.7.6]: https://github.com/brainn-co/xcribe/compare/v0.7.5...v0.7.6

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ mix.exs
2121
```elixir
2222
def deps do
2323
[
24-
{:xcribe, "~> 0.7.8"}
24+
{:xcribe, "~> 0.7.9"}
2525
]
2626
end
2727
```

lib/xcribe/api_blueprint/apib.ex

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule Xcribe.ApiBlueprint.APIB do
22
@moduledoc false
33

4+
alias Xcribe.ApiBlueprint.Multipart
45
alias Xcribe.JSON
56

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

20+
@multipart_template "\n\n--boundary--\nContent-Disposition: form-data; name=\"--name--\"\nContent-Type: --content_type--\n\n--value--"
21+
1922
@tab_size 4
2023

2124
def encode(%{groups: groups} = struct) do
@@ -103,6 +106,13 @@ defmodule Xcribe.ApiBlueprint.APIB do
103106
)
104107
end
105108

109+
def body(%Multipart{} = multipart) do
110+
apply_template(
111+
@body_template,
112+
body: build_multipart_body(multipart)
113+
)
114+
end
115+
106116
def body(body) when body == %{}, do: ""
107117

108118
def body(body) do
@@ -112,6 +122,22 @@ defmodule Xcribe.ApiBlueprint.APIB do
112122
)
113123
end
114124

125+
defp build_multipart_body(multipart),
126+
do: Enum.reduce(multipart.parts, "", &build_multipart_template(&1, &2, multipart.boundary))
127+
128+
defp build_multipart_template(part, acc, boundary) do
129+
part_template =
130+
apply_template(
131+
@multipart_template,
132+
boundary: boundary,
133+
name: part.name,
134+
content_type: part.content_type,
135+
value: part.value
136+
)
137+
138+
acc <> apply_tab(part_template, 3)
139+
end
140+
115141
defp action_uri(uri, query_parameters),
116142
do: Enum.reduce(query_parameters, uri, &add_query_parameter/2)
117143

lib/xcribe/api_blueprint/formatter.ex

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
defmodule Xcribe.ApiBlueprint.Formatter do
22
@moduledoc false
33

4+
alias Plug.Upload
5+
alias Xcribe.ApiBlueprint.Multipart
46
alias Xcribe.{ContentDecoder, JsonSchema, Request}
57

68
import Xcribe.Helpers.Formatter
@@ -56,7 +58,7 @@ defmodule Xcribe.ApiBlueprint.Formatter do
5658
desc => %{
5759
content_type: content_type(headers),
5860
headers: headers(headers),
59-
body: request.request_body,
61+
body: request_body(request),
6062
schema: request_schema(request),
6163
response: response_object(request)
6264
}
@@ -108,6 +110,14 @@ defmodule Xcribe.ApiBlueprint.Formatter do
108110
|> json_schema_for(body)
109111
end
110112

113+
def request_body(%Request{request_body: body}) when body == %{}, do: %{}
114+
115+
def request_body(%Request{request_body: body, header_params: headers}) do
116+
headers
117+
|> content_type()
118+
|> body_data_for(headers, body)
119+
end
120+
111121
def action_name(%Request{action: action, resource: resource}),
112122
do: "#{capitalize_all_words(resource)}#{action}"
113123

@@ -192,6 +202,30 @@ defmodule Xcribe.ApiBlueprint.Formatter do
192202

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

205+
defp body_data_for("multipart/form-data", headers, body) when is_map(body) do
206+
%Multipart{
207+
parts: Enum.reduce(body, [], &data_schema/2),
208+
boundary: content_type_boundary(headers)
209+
}
210+
end
211+
212+
defp body_data_for(_content_type, _headers, body), do: body
213+
214+
defp data_schema({key, %Upload{} = upload}, acc) do
215+
[
216+
%{
217+
content_type: upload.content_type,
218+
name: key,
219+
value: "image-binary",
220+
filename: upload.filename
221+
}
222+
| acc
223+
]
224+
end
225+
226+
defp data_schema({key, value}, acc),
227+
do: [%{content_type: "text/plain", name: key, value: value} | acc]
228+
195229
defp reduce_path_params({param, value}, parameters) do
196230
Map.put(
197231
parameters,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
defmodule Xcribe.ApiBlueprint.Multipart do
2+
defstruct [:parts, :boundary]
3+
end

lib/xcribe/helpers/formatter.ex

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,36 @@
11
defmodule Xcribe.Helpers.Formatter do
22
@moduledoc false
33

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

1618
@doc """
17-
return the authorization header from a list of headers
19+
return the content type boundary
1820
"""
19-
def authorization(headers) when is_list(headers) do
20-
Enum.reduce_while(headers, nil, &find_authorization/2)
21+
22+
def content_type_boundary(headers) when is_list(headers) do
23+
headers
24+
|> Enum.find_value(&find_content_type_boundary/1)
25+
|> handle_regex_match()
2126
end
2227

28+
@doc """
29+
return the authorization header from a list of headers
30+
"""
31+
def authorization(headers) when is_list(headers),
32+
do: Enum.reduce_while(headers, nil, &find_authorization/2)
33+
2334
@doc """
2435
Format the path params.
2536
@@ -29,18 +40,19 @@ defmodule Xcribe.Helpers.Formatter do
2940
def format_path_parameter(name),
3041
do: " #{name}" |> Macro.camelize() |> String.replace_prefix(" ", "")
3142

32-
defp find_content_type({"content-type", value}, _default) do
33-
@content_type_regex
34-
|> Regex.run(value, capture: :all_but_first)
35-
|> handle_content_type_regex()
36-
end
43+
defp find_content_type({"content-type", value}),
44+
do: Regex.run(@content_type_regex, value, capture: :all_but_first)
45+
46+
defp find_content_type(_tuple), do: nil
47+
48+
defp find_content_type_boundary({"content-type", value}),
49+
do: Regex.run(@content_type_boundary_regex, value, capture: :all_but_first)
3750

38-
defp find_content_type(_header, default), do: {:cont, default}
51+
defp find_content_type_boundary(_tuple), do: nil
3952

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

43-
defp handle_content_type_regex(nil), do: {:halt, nil}
44-
defp handle_content_type_regex([content_type]), do: {:halt, content_type}
45-
defp handle_content_type_regex([content_type | _vnd_spec]), do: {:halt, content_type}
56+
defp handle_regex_match([value | _vnd_spec]), do: value
57+
defp handle_regex_match(value), do: value
4658
end

lib/xcribe/json_schema.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
defmodule Xcribe.JsonSchema do
22
@moduledoc false
33

4+
alias Plug.Upload
5+
46
@doc """
57
Return the type of given data
68
"""
@@ -35,6 +37,14 @@ defmodule Xcribe.JsonSchema do
3537
@opt_no_title {:title, false}
3638
@opt_example {:example, true}
3739

40+
defp schema_object_for({title, %Upload{}}, opts) do
41+
schema_add_title(
42+
%{type: "string", format: "binary"},
43+
title,
44+
@opt_no_title in opts
45+
)
46+
end
47+
3848
defp schema_object_for({title, value}, opts) when is_map(value) do
3949
%{type: "object"}
4050
|> schema_add_title(title, @opt_no_title in opts)

lib/xcribe/request/validator.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
defmodule Xcribe.Request.Validator do
2+
alias Plug.Upload
23
alias Xcribe.Request
34
alias Xcribe.Request.Error
45

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

24+
defp find_struct(%Upload{}), do: :ok
25+
2326
defp find_struct(%{__struct__: module}) do
2427
%Error{
2528
type: :validation,

lib/xcribe/swagger/formatter.ex

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule Xcribe.Swagger.Formatter do
22
@moduledoc false
33

4+
alias Plug.Upload
45
alias Xcribe.{ContentDecoder, JsonSchema, Request}
56

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

191-
%{
192-
description: "",
193-
content: %{
194-
media_type => %{schema: build_schema_for_media(content, media_type)}
195-
}
196-
}
192+
media_type_schema =
193+
%{}
194+
|> Map.put(:schema, build_schema_for_media(content, media_type))
195+
|> add_enconding_if_needed(content)
196+
197+
%{description: "", content: %{media_type => media_type_schema}}
198+
end
199+
200+
defp add_enconding_if_needed(schema, content) when is_map(content) do
201+
content
202+
|> Map.keys()
203+
|> Enum.reduce(schema, &enconding_for(content[&1], &2, &1))
204+
end
205+
206+
defp add_enconding_if_needed(schema, _content), do: schema
207+
208+
defp enconding_for(%Upload{} = upload, schema, property) do
209+
encoding = %{property => %{contentType: upload.content_type}}
210+
211+
Map.update(schema, :encoding, encoding, &Map.merge(&1, encoding))
197212
end
198213

214+
defp enconding_for(_value, schema, _property), do: schema
215+
199216
defp build_schema_for_media(content, content_type) when is_binary(content) do
200217
content
201218
|> ContentDecoder.decode!(content_type)

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Xcribe.MixProject do
22
use Mix.Project
33

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

0 commit comments

Comments
 (0)