Cowboy REST API에 대한 JSON 유효성 검사 - Elixir에서 Joi 축하 모방
갑자기 Javascript에서 Elixir로 REST API를 다시 작성해야 하는데 기존 라이브러리 중 일부가 단순성과 가능한 적은 코드에 대한 기본적인 요구 사항을 충족하지 못한다는 것을 알게 되었습니다.
Node-Express 배경이 있는 우리 모두는 Celebrate 및 Joi를 사용하여 키의 존재, 데이터 유형 및 저장된 값을 확인했습니다.
이 기사에서는 Celebrate Joi의 (최소한의) 기능을 모방하기 위해 Elixir에서 스키마 유효성 검사기를 구현하는 방법을 보여줍니다.
목차
1) 플러그 및 미들웨어
There is a common architecture between Node-Express and Elixir-Cowboy, Express have middlewares and Cowboy have Plugs.
In both cases these are used for routing, body parsing, user authentication verification, cross-origin resource sharing, data logging, data validation, metrics request, etc..
예: 고전적인 Node-Express 미들웨어 체인
//file: app_router.js
// import boiler plate ...
app.use(cors(cors_configuration)); //CORS
app.use(express.json());//Json body parsing
app.use(isAuth);//user jwt authentication verification
app.use(function (err, req, res, next) { // error handling
if (err.name === "UnauthorizedError") {
res.status(401).send("invalid token...");
} else {
next(err);
}
});
app.use(attachUserId);// add extra information about the current user
app.use((req, res, next) => { // some debug
console.log("Method",req.method);
console.log("Url", req.originalUrl);
console.log("Body:",req.body);
return next();
});
다음과 같이 플러그가 차례로 연결된 Elixir-Cowboy에서도 마찬가지입니다.
# file: app_router.ex
# use/import boiler plate ...
plug Corsica, cors_configuration # CORS
plug :match
plug Plug.Telemetry, telemetry_configuration # metrics request
# body parsing
plug Plug.Parsers, parsers: [:json], pass: ["application/json"], json_decoder: Jason
# Json validation
plug Plug.RouterValidatorschema, on_error: &__MODULE__.on_error_fn/2
# user jwt authentication verification
plug Plug.Authenticator, on_error: &__MODULE__.on_error_fn/2
# add extra information about the current user
plug Plug.AttachUserId, on_error: &__MODULE__.on_error_fn/2
# some debug
plug Plug.PrintUrlMethod
plug :dispatch
Celebrate는 미들웨어이며, Elixir와 동등한 것은 플러그입니다. 플러그 작성을 시작하겠습니다.
다음 링크는 플러그를 작성하는 방법을 이해하고 배우는 출발점입니다. 이 링크를 읽고 여기로 오시기 바랍니다: The Plug Specification
그것을 정의하는 두 가지 방법이 있습니다: 기능 플러그와 모듈 플러그, 저는 모듈 플러그 방식을 선택하겠습니다.
2) 경로 스키마 검증
Has programers we want to write the minimal possible lines of code. Bassically reduce all to a schema definition and then apply in our route.
Schema:
photo_book_schema = [
qty: {:number, 1, 200},
hardcover: {:boolean},
colors: {:options, ["bw", "color", "sepia"]},
client_name: {:string, 6, 20},
client_phone: {:string, 8, 25, ~r/^[\d\-()\s]+$/}
]
Route:
plug(:match)
plug(Plug.RouterValidatorschema, on_error: &__MODULE__.on_error_fn/2)
plug(:dispatch)
post "/create/", private: %{validate: photo_book_schema} do
status = Orders.create(conn.body_params)
{:ok, str} = Jason.encode(%{status: status})
send_resp(conn |> put_resp_content_type("application/json"), str)
end
def on_error_fn(conn, errors, status \\ 422) do
IO.inspect(errors, label: "errors")
{:ok, str} = Jason.encode(%{status: "fail", errors: errors})
send_resp(conn |> put_resp_content_type("application/json"), str) |> halt()
end
Anxious for the code ?
Before to go deeper in the code, we learn a bite about Function Clause Matching.
2) 함수 절 매칭
One of the powerfull tools of Elixir is pattern matching. It is applied in many places, thanks to that, we write less lines of code compared with another languages.
In this case is applied in function clauses, Elixir let us to write multiple definitions for the same function, every definitions is called a clause.
See the next example:
defmodule Discount do
def calculate(type, price) do
case type do
:normal -> price * 1
:high -> price * 0.8
:low -> price * 0.9
end
end
end
Instead we can write:
defmodule Discount do
def calculate(:normal, price), do: price
def calculate(:high, price), do: price * 0.8
def calculate(:low, price), do: price * 0.9
end
Where the "case" statement is moved outside of the function and the virtual machine take the decision of which must to apply.All definition refer to the same function "calculate" with arity 2.
2.1) 팁.
함수 절 일치를 사용할 때 몇 가지 팁입니다.
그룹:
항상 동일한 기능의 절을 함께 그룹화합니다.
다른 사람과 섞이지 마십시오:
defmodule FooBar do
def foo(:a), do: 'a'
def foo(:b), do: 'b'
def bar(arg), do: IO.puts arg
def foo(:c), do: 'c'
def foo(arg), do: IO.puts arg
end
컴파일러 경고를 받게 됩니다.
warning: clauses with the same name and arity (number of arguments) should be grouped together, "def foo/1" was previously defined.
올바른 방법:
defmodule FooBar do
def foo(:a), do: 'a'
def foo(:b), do: 'b'
def foo(:c), do: 'c'
def foo(arg), do: IO.puts arg
def bar(arg), do: IO.puts arg
end
주문하다:
선언 순서는 정말 중요합니다.
하지 말아야 할 일:
defmodule FooBar do
def foo(arg), do: arg
def foo(:b), do: 'b'
def foo(:c), do: 'c'
end
컴파일러 경고를 받게 됩니다.
warning: this clause for foo/1 cannot match because a previous clause always matches
올바른 방법, 선택적인 무게에 대한 정렬:
defmodule FooBar do
def foo(:b), do: 'b'
def foo(:c), do: 'c'
def foo(arg), do: arg
end
모두 일치:
항상 final match all 절을 작성하십시오.
때로는 예상치 못한 인수 값을 받은 다음 오류가 발생합니다.
(FunctionClauseError) no function clause matching
하지 말아야 할 일:
defmodule FooBar do
def foo(:b), do: 'b'
def foo(:c), do: 'c'
end
올바른 방법:
defmodule FooBar do
def foo(:b), do: 'b'
def foo(:c), do: 'c'
def foo(_), do: "Something happen"
end
4) 스키마 검증
The Plug.Router provide a way to passing data between routes and plugsPlug.conn 구조체
Plug.conn
와 플러그 체인을 더 잘 이해하기 위한 비유는 마차가 있는 기차 기관차에서 생각하는 것입니다. 이때 모든 플러그는 데이터를 로드하거나 언로드하는 스테이션이고 마차는 구조체의 필드입니다.Plug.conn
구조체에는 private
라는 필드가 있으며 키와 값으로 설정됩니다.
post "/create/" , private: %{validate: photo_book_schema} do
.....
end
플러그가
validate
필드 내에서 private
키를 찾으면 유효성 검사 프로세스가 시작되어 함수validate_schema()
를 호출합니다.
defmodule Plug.RouterValidatorSchema do
def init(opts), do: opts
def call(conn, opts) do
case conn.private[:validate] do
nil -> conn
schema -> validate_schema(conn,schema, opts[:on_error])
end
end
validate_schema()
는 스키마 목록을 살펴보고 내부 키의 유효성을 검사합니다conn.body_params
.모든 유효성 검사가 성공하면 빈 맵을 반환합니다.
그렇지 않으면 유효성 검사에 실패하면 수집된 오류 목록과 함께 지정된
on_error
콜백을 호출합니다.오류 목록의 모양은 다음과 같습니다
[ %{"key1"=> "error"}, %{"key2"=> "error"}]
.
defp validate_schema(conn, schema, on_error) do
errors = Enum.reduce(schema, [], validate_key(conn.body_params))
if Enum.empty?(errors) do
conn
else
on_error.(conn, %{errors: errors})
end
end
defp validate_key(data) do
fn {key, test}, errors ->
skey = Atom.to_string(key)
cond do
not Map.has_key?(data, skey) -> [%{skey => "missing key"} | errors]
is_nil(data[skey]) -> [%{skey => "value is nil"} | errors] # or just return errors if value can accept nil
true ->
case is_t(test, data[skey]) do
:ok -> errors
{:invalid, reason} -> [%{skey => reason} | errors]
end
end
end
end
validate_key(data)
는 모든 테스트를 주요 관련 데이터에 적용하는 매개변수화된 함수를 반환합니다.is_t(test,value)
는 테스트를 수행하며 여기에서 함수 절 일치, 부울, 숫자, 문자열, 문자열 패턴, 옵션에 대한 함수 테스트를 활용하고 원하는 데이터 유형에 대한 새 테스트를 매우 쉽게 추가할 수 있습니다.
defp is_t({:boolean}, value) do
if (not is_boolean(value)) do
{:invalid, "not is boolean"}
else
:ok
end
end
defp is_t({:number}, value) do
if (not is_number(value)) do
{:invalid, "not is number"}
else
:ok
end
end
defp is_t({:number, vmin, vmax}, value) do
cond do
not is_number(value) -> {:invalid, "not is number"}
value <= vmin -> {:invalid, "min value"}
value > vmax -> {:invalid, "max value"}
true ->:ok
end
end
defp is_t({:string}, value) do
if is_bitstring(value) do
:ok
else
{:invalid, "not is string"}
end
end
defp is_t({:string, lmin, lmax}, value) do
cond do
not is_bitstring(value) -> {:invalid, "not is string"}
String.length(value) <= lmin -> {:invalid, "min length"}
String.length(value) > lmax -> {:invalid, "max length"}
true ->:ok
end
end
defp is_t({:string, lmin, lmax, reg}, value) do
cond do
not is_bitstring(value) -> {:invalid, "not is string"}
String.length(value) <= lmin -> {:invalid, "min length"}
String.length(value) > lmax -> {:invalid, "max length"}
not Regex.match?(reg,value) -> {:invalid, "regex not match"}
true ->:ok
end
end
defp is_t({:options, alloweds}, value) do
cond do
value not in alloweds -> {:invalid, "not allowed"}
true ->:ok
end
end
# ---------------------------
# Final match :
# ---------------------------
defp is_t(_value,_test) do
{:invalid, "test match fail"}
end
Reference
이 문제에 관하여(Cowboy REST API에 대한 JSON 유효성 검사 - Elixir에서 Joi 축하 모방), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/lionelmarco/json-validation-for-cowboy-rest-api-mimic-celebrate-joi-in-elixir-4l27텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)