Crystal JSON은 기초를 뛰어넘었습니다.


소개하다.
업무 영역을 모델링할 때, 언어 원어의 맨 위에 사용자 정의 데이터 형식을 정의하는 것을 자주 발견할 수 있습니다.만약 네가 함수식 프로그래밍을 접한 적이 있다면, 너는 특별히 sum types을 쟁취하려고 노력할 것이다.
Crystal에서는 및 유형을 추상적인 유형에서 상속된 복합 유형으로 표현할 수 있습니다.덧붙인 장점으로 이러한 모델은 사용자 정의 유형 인코딩(과 디코딩)을 JSON(과 JSON에서 디코딩)으로 직접 할 수 있다. 예를 들어 official documentation과 같다.
본고에서 우리는 json 모듈과 강력한 매크로를 사용하여 크리스탈에서 JSON과 유형을 인코딩하고 디코딩하는 방법을 이해할 것이다.
다음과 같은 내용을 설명합니다.
  • JSON::Serializable 자동 인코딩 사용
  • (감별기 포함) 유형 해상도
  • 복합 데이터 형식의 인코딩
  • 이 방법의 확장성에 대한 고려 사항

  • 사례 연구 – P2P 클라이언트
    가령 우리가 대등한 응용 프로그램과 관련된 이벤트를 모델링하려고 한다면.우리는 두 가지 분야의 사건에 주목할 것이다.
  • Connected 이벤트: 피어와의 연결
  • Started 이벤트: 파일 세그먼트 다운로드를 시작합니다.
  • 이 장면에서 흔히 볼 수 있는 모델은 각종 유형의 사건을 기본 Event 유형에서 계승된 유형이나 구조로 표시하는 것이다.이벤트는 본질적으로 변할 수 없기 때문에 그것들을 Getter를 가진 구조로 모델링하는 것은 의미가 있다.
    alias Peer = String
    
    abstract struct Event
    end
    
    struct Connected < Event
      getter peer
      def initialize(@peer : Peer); end
    end
    
    struct Started < Event
      getter peer, piece
      def initialize(@peer : Peer, @piece : UInt32); end
    end
    
    위 부분에서
  • 은 첫 번째 줄에 있고 Peer은 하나의 문자열로 그의 IP 주소를 표시한다.
  • 은 세 번째 줄에서 추상적인 구조인 Event을 정의했는데 이것은 모든 구체적인 사건 유형의 기본 유형으로 정의했다.abstract 표지부호는 Event 대상을 실례화할 수 없습니다. 이것은 Event.new이 컴파일하지 않을 것을 의미합니다.

  • JSON 인코딩
    우리가 정성스럽게 설계한 이벤트 차원 구조를 고려할 때 감사 목적을 위해 응용 프로그램이 처리하는 모든 P2P 이벤트를 영구화해야 한다는 요구가 있다.팀과의 격렬한 토론을 거쳐 우리는 JSON 형식을 채택하기로 결정했다.Event개의 인스턴스를 JSON으로 변환하기 위해 코드를 업데이트하겠습니다.
    require "json"
    
    abstract struct Event
      include JSON::Serializable
    end
    
    여기서 JSON 패키지(1행)를 가져온 다음 JSON::Serializable 모듈을 Event(4행)에 간단하게 혼합합니다.
    그런가요?그래, 생각해 볼게...
    e0 = Connected.new("0.0.0.0") #=> Connected(@peer="0.0.0.0")
    e1 = Started.new("0.0.0.0", 2) #=> Started(@peer="0.0.0.0", @piece=2)
    
    e0.to_json #=> {"peer":"0.0.0.0"}
    e1.to_json #=> {"peer":"0.0.0.0","piece":2}
    
    지금, 이것은 매우 인상적이다!간단하게 JSON::Serializable 모듈을 기본 유형에 포함하면 그 하위 유형에 효과적인 #to_json 방법을 배치할 수 있다.다음 작업도 효과적입니다.
    Connected.from_json(e0.to_json) #=> Connected(@peer="0.0.0.0")
    Started.from_json(e1.to_json) #=> Started(@peer="0.0.0.0", @piece=2)
    
    이것은 아주 좋습니다. 예를 들어 테스트 목적에 사용되지만, 컴파일할 때, 이벤트의 정확한 유형을 알 수 없기 때문에, 우리가 실제로 실행하고자 하는 것은
    Event.from_json(e0.to_json)
    # raises "Error: can't instantiate abstract struct Event"
    
    불행하게도, 이것은 오류를 일으킬 수 있습니다. 기본적으로 JSON::Serializable 모듈에 정의된 반서열화 프로그램은 Event 대상을 실례화하려고 시도합니다.위에서 말한 바와 같이 유형의 추상적 성질 때문에 이것은 불가능하다.그럼 우리 이제 어떡하지?

    구원자
    JSON 부하를 올바른 실행 시 형식으로 반서열화하기 위해 이벤트 형식에 대한 추가 메타데이터를 JSON 자체에 추가합니다.우리는 이 필드를 감별기라고 부른다.
    다행히도 json 모듈은 적당한 명칭의 use_json_discriminator 매크로를 가지고 있다.이것은 우리가 찾고 있는 반서열화 기능을 제공하지만, 서열화할 때 감별기 필드를 정확하게 채워야 합니다.
    감별기에 대한 지원을 추가하기 위해 코드를 업데이트합시다.
    abstract struct Event
      include JSON::Serializable
    
      use_json_discriminator "type", {
        connected: Connected,
        started: Started
      }
    end
    
    struct Connected < Event
      getter peer
      getter type = "connected"
      def initialize(@peer : Peer); end
    end
    
    struct Started < Event
      getter peer, piece
      getter type = "started"
      def initialize(@peer : Peer, @piece : UInt32); end
    end
    
    됐어, 이게 어떻게 된 거야?
  • 은 4행에서 감별기 필드 값과 유형 사이의 맵을 제공하여 use_json_discriminator을 호출합니다.이 경우, 반서열화 프로그램은 감별기가 'type' 필드에 나타나기를 원합니다.
  • 12행과 18행은 이벤트 유형 이름에 따라 type 필드를 채워야 합니다.
  • type 필드의 값과 감별기 맵 사이의 대응 관계를 알 수 있습니다.
    서열화 프로그램에 미치는 영향을 확인해 봅시다.
    e0.to_json #=> {"type":"connected","peer":"0.0.0.0"}
    e1.to_json #=> {"type":"started","peer":"0.0.0.0","piece":2}
    
    유형 메타데이터는 현재 생성된 JSON의 일부입니다.이것은 반대로 다음과 같은 일을 전개할 수 있게 한다.
    Event.from_json(e0.to_json) #=> Connected(@type="connected", @peer="0.0.0.0")
    Event.from_json(e1.to_json) #=> Started(@type="started", @peer="0.0.0.0", @piece=2)
    
    너무 좋아요!

    조합 유형
    상술한 방법은 필드를 기본 유형으로 하는 복합 유형에 적용되지만 만약에 우리가 다른 복합 유형을 바탕으로 복합 유형을 정의한다면?😱Peer의 정의를 확장해 봅시다.
    struct Peer
      getter address : String
      getter port : Int32
    
      def initialize(@address, @port)
      end
    end
    
    e0 = Connected.new(Peer.new("0.0.0.0", 8020))
    e1 = Started.new(Peer.new("0.0.0.0", 8020), 2)
    
    현재 아래의 방법은 실패했다
    e0.to_json
    # raises "Error: no overload matches 'Peer#to_json' with type JSON::Builder"
    
    이곳의 컴파일러는 매우 명확하다. Peer의 대상을 어떻게 JSON으로 변환하는지 모른다.
    🤔 나 이거 알아!JSON::SerializablePeer에 포함시켜 보겠습니다.
    struct Peer
      include JSON::Serializable
    
      getter address : String
      getter port : Int32
    
      def initialize(@address, @port)
      end
    end
    
    지금 해볼게요.
    e0.to_json #=> {"type":"connected","peer":{"address":"0.0.0.0","port":8020}}
    e1.to_json #=> {"type":"started","peer":{"address":"0.0.0.0","port":8020},"piece":2}
    
    성공!반서열화는요?
    Event.from_json(s0) #=> Connected(@type="connected", @peer=Peer(@address="0.0.0.0", @port=8020))
    Event.from_json(s1) #=> Started(@type="started", @peer=Peer(@address="0.0.0.0", @port=8020), @piece=2)
    
    걸출하다그게 다야.흥미로운 기교와 이 방법의 확장성에 대한 더 많은 고려를 정리해 봅시다.

    이벤트 하위 유형 추가
    현재 기본 이벤트 유형과 그 실현은 모두 JSON을 지원한다. 이것은 이 두 가지 유형의 코드가 모두 JSON 지원과 관련된 위치를 포함한다는 것을 의미한다.
    그 자체는 문제가 되지 않지만 JSON 논리가 세부 사항을 Event 서브 유형 정의에 누설하고 있는 것 같다.Event 실현자가 유형 필드를 알 필요가 없도록 할 수 있습니까?결국, type Getter는 프로그래밍을 통해 계산할 수 있는 값을 되돌려줍니다. 이 예에서 형식 이름의downcase 버전입니다.
    사실은 우리가 할 수 있는 일이 다음과 같다는 것을 증명한다.
    abstract struct Event
      include JSON::Serializable
    
      use_json_discriminator "type", {connected: Connected, started: Started}
    
      macro inherited
        getter type : String = {{@type.stringify.downcase}}
      end
    end
    
    잠깐만, 6호선의 macro inherited은 무엇에 관한 것입니까?그것은 주체의 코드를 Event에서 계승하는 모든 유형에 주입하는 특수한 매크로 갈고리이다.이것이 바로 우리가 필요로 하는 것이다. 왜냐하면 type Getter를 Event의 모든 실현에 주입하고,stringized와downcased 형식 이름으로 설정할 수 있기 때문이다.7행에서는 매크로에 나타나는 @type이 인스턴스화된 유형의 이름으로 해석됩니다.
    다음 코드는 다음과 같습니다.
    struct Connected < Event
      getter peer
      def initialize(@peer : Peer); end
    end
    
    struct Started < Event
      getter peer, piece
      def initialize(@peer : Peer, @piece : UInt32); end
    end
    
    JSON 논리의 흔적이 없어요.🎉
    새로운 Event 유형을 소개하여 이 점을 보여 드리겠습니다.
    # An event indicating the completion of a file piece.
    struct Completed < Event
      getter peer, piece
      def initialize(@peer : Peer, @piece : UInt32); end
    end
    
    예상한 바와 같이 실현자에게는 별도의 논리가 없지만, 우리는 여전히 감별기 맵을 업데이트해야 한다.
    abstract struct Event
      use_json_discriminator "type", {
        connected: Connected,
        started: Started,
        completed: Completed
      }
      # ...
    end
    
    도전하다.수동으로 맵을 업데이트하는 것을 피할 수 있습니까?
    팁: 다음 매크로는 우리가 찾는 NamedTupleLiteral을 생성하는 데 적합합니다.
    abstract struct Event
      macro subclasses
        {
          {% for name in @type.subclasses %}
          {{ name.stringify.downcase.id }}: {{ name.id }},
          {% end %}
        }
      end
    end
    
    Event.subclasses # => {connected: Connected, started: Started, completed: Completed}
    
    불행히도 다음과 같은 방법은 효과가 없다.
    use_json_discriminator "type", Event.subclasses
    #=> raises Error: mapping argument must be a HashLiteral or a NamedTupleLiteral, not Call
    
    이는 매크로 use_json_discriminator이 펼쳐졌을 때 Event.subclasses이 아직 펴지지 않았기 때문이다.나는 매크로를 사용할 때 자주 이런 문제가 발생하는 것을 본 적이 있다. 매크로는 대량의 코드를 작성하는 것을 피할 수 있지만, 매크로를 작성하는 것은 사람을 낙담하게 하고 복잡하게 할 수도 있다.
    다음은 나의 건의이다.

    when working with macros, keep it simple. If something feels too complicated, it probably is.


    어쨌든 해커 솔루션을 공유하고 싶다면 아래 부분에 댓글을 남겨주세요.

    한층 더 읽다
  • 본문의 영감은 제가 크리스탈에서 BitTorrent client을 쓴 경험에서 왔습니다.
  • 대수 데이터 유형에 대한 정보를 더 알고 싶으면 제임스 신클라어의 this article을 추천합니다.
  • 공식 json 모듈 설명서 here
  • 크리스탈 매크로 연결에 대한 자세한 내용은 공식 크리스탈 reference을 참조하십시오.
  • 나는 네가 이 JSON을 주제로 한 문장을 좋아하고 크리스탈에 대해 좀 알기를 바란다.만약 JSON 서열화 공간에 대해 궁금한 점이나 이해가 있다면, 저는 아래의 평론 부분에서 기꺼이 읽겠습니다.
    만약 당신이 연락을 유지하고 싶다면, 당신은 구독하거나 나를 주목할 수 있습니다.너는 때때로 Twitch에서 나의 실시간 인코딩을 찾을 수 있다📺

    좋은 웹페이지 즐겨찾기