1// Package json provides a JSON formatter implementation.
2package json
3
4import (
5	"bytes"
6	gojson "encoding/json"
7	"io"
8
9	"github.com/golang/protobuf/jsonpb" //nolint:staticcheck
10	"github.com/golang/protobuf/proto"  //nolint:staticcheck
11	"github.com/golang/protobuf/ptypes"
12	"github.com/ktr0731/evans/format"
13	"github.com/ktr0731/evans/present"
14	"github.com/ktr0731/evans/present/json"
15	"github.com/pkg/errors"
16	_ "google.golang.org/genproto/googleapis/rpc/errdetails" // For calling RegisterType.
17	"google.golang.org/grpc/metadata"
18	"google.golang.org/grpc/status"
19)
20
21// responseFormatter is a formatter that formats *usecase.GRPCResponse into a JSON object.
22type responseFormatter struct {
23	w io.Writer
24	s struct {
25		Status struct {
26			Code    string        `json:"code"`
27			Number  uint32        `json:"number"`
28			Message string        `json:"message"`
29			Details []interface{} `json:"details,omitempty"`
30		} `json:"status,omitempty"`
31		Header   *metadata.MD             `json:"header,omitempty"`
32		Messages []map[string]interface{} `json:"messages,omitempty"`
33		Trailer  *metadata.MD             `json:"trailer,omitempty"`
34	}
35	p           present.Presenter
36	pbMarshaler *jsonpb.Marshaler
37}
38
39func NewResponseFormatter(w io.Writer) format.ResponseFormatterInterface {
40	return &responseFormatter{w: w, p: json.NewPresenter("  "), pbMarshaler: &jsonpb.Marshaler{}}
41}
42
43func (p *responseFormatter) FormatHeader(header metadata.MD) {
44	p.s.Header = &header
45}
46
47func (p *responseFormatter) FormatMessage(v interface{}) error {
48	m, err := p.convertProtoMessageToMap(v.(proto.Message))
49	if err != nil {
50		return err
51	}
52	p.s.Messages = append(p.s.Messages, m)
53	return nil
54}
55
56func (p *responseFormatter) FormatTrailer(trailer metadata.MD) {
57	p.s.Trailer = &trailer
58}
59
60func (p *responseFormatter) FormatStatus(s *status.Status) error {
61	var details []interface{}
62	if len(s.Details()) != 0 {
63		details = make([]interface{}, 0, len(s.Details()))
64		for _, d := range s.Details() {
65			d, ok := d.(proto.Message)
66			if !ok {
67				continue
68			}
69			// Convert to Any to insert @type field.
70			m, err := p.convertProtoMessageAsAnyToMap(d)
71			if err != nil {
72				return err
73			}
74			details = append(details, m)
75		}
76	}
77
78	p.s.Status = struct {
79		Code    string        `json:"code"`
80		Number  uint32        `json:"number"`
81		Message string        `json:"message"`
82		Details []interface{} `json:"details,omitempty"`
83	}{
84		Code:    s.Code().String(),
85		Number:  uint32(s.Code()),
86		Message: s.Message(),
87		Details: details,
88	}
89	return nil
90}
91
92func (p *responseFormatter) Done() error {
93	s, err := p.p.Format(p.s)
94	if err != nil {
95		return err
96	}
97	_, err = io.WriteString(p.w, s+"\n")
98	return err
99}
100
101func (p *responseFormatter) convertProtoMessageToMap(m proto.Message) (map[string]interface{}, error) {
102	var buf bytes.Buffer
103	err := p.pbMarshaler.Marshal(&buf, m)
104	if err != nil {
105		return nil, err
106	}
107	var res map[string]interface{}
108	if err := gojson.Unmarshal(buf.Bytes(), &res); err != nil {
109		return nil, err
110	}
111	return res, nil
112}
113
114func (p *responseFormatter) convertProtoMessageAsAnyToMap(m proto.Message) (map[string]interface{}, error) {
115	any, err := ptypes.MarshalAny(m)
116	if err != nil {
117		return nil, errors.Wrap(err, "failed to convert a message to *any.Any")
118	}
119	return p.convertProtoMessageToMap(any)
120}
121