Cowboy REST API에 대한 JSON 유효성 검사 - Elixir에서 Joi 축하 모방

39030 단어 elixircowboy
REST API에서 작업할 때 가장 먼저 해야 할 일 중 하나는 들어오는 데이터의 유효성을 검사하는 것입니다.

갑자기 Javascript에서 Elixir로 REST API를 다시 작성해야 하는데 기존 라이브러리 중 일부가 단순성과 가능한 적은 코드에 대한 기본적인 요구 사항을 충족하지 못한다는 것을 알게 되었습니다.
Node-Express 배경이 있는 우리 모두는 Celebrate 및 Joi를 사용하여 키의 존재, 데이터 유형 및 저장된 값을 확인했습니다.

이 기사에서는 Celebrate Joi의 (최소한의) 기능을 모방하기 위해 Elixir에서 스키마 유효성 검사기를 구현하는 방법을 보여줍니다.

목차


  • 1-Plugs and Midlewares
  • 2-Route-Schema Validation
  • 3-Function Clause Matching
  • 4-Json Schema Validation

  • 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..

    How the documentation say :"An Express application is essentially a series of middleware function calls." Using middlewars

    예: 고전적인 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 plugs

    Plug.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
    
    
    

    좋은 웹페이지 즐겨찾기