Akka HTTP의 검증 라이브러리를 만들었습니다.

10154 단어 Akka-HTTPAkkaScala

도전


  • Akka HTTP를 일반 API 서버로 사용할 때 요청의 JSON이 적절한지 여부를 확인하고 싶습니다.
  • 값의 검증은 도메인 로직으로 해도 괜찮습니다만, 원래 마이너스 값이 들어가지 않게 하고 싶다든가, 입력 필수 항목이 채워져 있지 않은가를 돌려주고 싶다.

  • 라는 것이있었습니다. Play라고 Form에 밸리데이션의 API가 있습니다만, Akka HTTP로는 없을 것 같아&조사해도 3rd party 라이브러리로서도 별로 없을 것 같다.

    그래서 시험에 스스로 만들어 보았습니다.

    만든 것



    유효성 검사를 위반하는 요청을 보내면 이러한 응답이 반환됩니다.



    중첩된 객체나 Array에도 대응하고 있습니다.

    응답의 details에게는, 보내진 json의 「어떤 Key가 어떤 이유로 안되는지」를 각각 돌려줍니다.

    만들 때 생각한 것



    API 사양



    Play의 Validation이면 반환 값이
    {
      "obj1.obj2.column1[0]": "error.required"
    }
    

    와 같은 형태로 돌아오므로, 에러 내용을 화면에서 취급하려면 Key의 문자열을 퍼스 하지 않으면 안 되고 사용하기 어려웠으므로, 이번은 이런 형태로 돌려주고 싶었다.
    {
      "obj1": {
        "obj2": {
           "column1": {
             "0": "error.required"
           }
        }
      }
    }
    

    코드 해설



    크기로서는 100행도 없는 작은 라이브러리입니다.

    Validation 정의



    JSON은 Object , Array 가 중첩되어 마지막으로 리터럴 값이 나옵니다.
    그들을 다루기 위해,
    trait ValidatorBase[T] {
      def validate(model: T): Option[JsValue]
    }
    trait ValidatorSeq[T] extends ValidatorBase[Seq[T]]
    trait Validator[T] extends ValidatorBase[T]
    

    각각을 정의합니다.
    그리고, 각각에서 재귀적으로 validate 메소드를 호출하는 것으로 밸리데이션 결과가 조립되어 최종적으로는 하나의 Option[JsValue] 형태가 생성되어 그것을 응답에 건네주는 형태가 됩니다.

    실제로 사용자 코드에서 유효성 검사를 정의할 때,
    - 어떤 키의 유효성 검사
    - 어떤 밸리데이션을 걸까
    - 실패한 경우 어떤 메시지를 발행할지
    같은 내용을 전달하여 정의합니다.
    case class Tag(
      id: Int,
      text: String
    )
    
    object TagValidator extends Validator[Tag] {
      private def idRule: Validation =
        genValidation(
          "id",
          tag => tag.id <= 0,
          "id must be positive"
        )
    
      private def textRule: Validation =
        genValidation(
          "text",
          tag => tag.text.isEmpty,
          "text must not be empty"
        )
    
      val validations = Seq(idRule, textRule)
    }
    

    또한 중첩 된 객체에 대한 유효성 검사를 원하면 중첩 된 객체에 대해 이미 만든 Validator를 전달하여 대응합니다.
    private def tagRule: Validation =
        genValidationInternal(
          "tag",
          nested => nested.tag,
          TagValidator
        )
    

    Validation 호출



    Akka HTTP의 JSON Support와 함께하는 일은 동일하며,
    - 자작 클래스의 변환(밸리데이션)을 정의해, 호출측에서 extends 한다
    - 변환(밸리데이션)의 API를 호출할 때, implicit parameter로서 상기에서 만든 정의를 건네준다.

    라는 형태입니다.
    htps // c c. 어? 이오/도 cs/아카-h tp/콰렌 t/코몬/j そーすっぽ rt. HTML
    trait CustomValidationDirectives extends ValidationDirectiveBase {
      implicit val tagV = TagValidator
      implicit val taglistV = TagListValidator
      implicit val nestedTagV = NestedTagValidator
    }
    
    
    object Main extends CustomValidationDirectives with CustomJsonFormat {
    
      validate(as[Tag]) { tag => // JSON to validated Tag
       ...
      }
    
    ValidationDirectiveBase 를 extends한 자바 커스텀 지시어를 만듭니다.
    호출측에서는, 그것을 상속하는 것으로, validate 지시어와, implicit에 Validator를 건네주는 as[T] 를 사용할 수 있게 됩니다.

    여기서 as[T]는 akka-http-spray-json의 as[T]가 아닙니다.
    // akka-http-spray-json
    def as[T](implicit um: FromRequestUnmarshaller[T]) = um
    
    // akka-http-json-validation
    def as[T](implicit um: FromRequestUnmarshaller[T], validator: ValidatorBase[T]): (FromRequestUnmarshaller[T], ValidatorBase[T]) = (um, validator)
    
    validate 지시문에서는, 이 「JSON 로부터 Entity 로의 변환 정의」, 「Entity 의 밸리데이션 정의」를 사용하는 것으로, validate 끝난 데이터를 돌려줍니다.

    요약



    구현을 통해 Akka HTTP에서 사용자 지정 지시문을 사용하는 방법과 implicit에 주입되는 것의 동작을 확인했습니다.

    짧은 코드이므로, 꼭 읽어 주셔서 코멘트하실 수 있으면 기쁩니다.

    좋은 웹페이지 즐겨찾기