encoder.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. package exporter
  2. import (
  3. "bytes"
  4. "compress/gzip"
  5. "encoding"
  6. "fmt"
  7. "io"
  8. "github.com/opencost/opencost/core/pkg/util/json"
  9. "google.golang.org/protobuf/encoding/protojson"
  10. "google.golang.org/protobuf/proto"
  11. )
  12. // Encoder[T] is a generic interface for encoding an instance of a T type into a byte slice.
  13. type Encoder[T any] interface {
  14. Encode(*T) ([]byte, error)
  15. // EncodeTo performs a streaming write to an io.Writer instead of writing and returning the full
  16. // binary encoding.
  17. EncodeTo(io.Writer, *T) error
  18. // FileExt returns the file extension for the encoded data. This can be used by a pathing strategy
  19. // to append the file extension when exporting the data. Returning an empty string will typically
  20. // omit the file extension completely.
  21. FileExt() string
  22. }
  23. // BinaryMarshalerPtr[T] is a generic constraint to ensure types passed to the encoder implement
  24. // encoding.BinaryMarshaler and are pointers to T.
  25. type BinaryMarshalerPtr[T any] interface {
  26. encoding.BinaryMarshaler
  27. MarshalBinaryTo(io.Writer) error
  28. *T
  29. }
  30. // BingenEncoder[T, U] is a generic encoder that uses the BinaryMarshaler interface to encode data.
  31. // It supports any type T that implements the encoding.BinaryMarshaler interface.
  32. type BingenEncoder[T any, U BinaryMarshalerPtr[T]] struct {
  33. fileExt string
  34. }
  35. // NewBingenEncoder creates an `Encoder[T]` implementation which supports binary encoding for the `T`
  36. // type, and doesn't have a file extension.
  37. func NewBingenEncoder[T any, U BinaryMarshalerPtr[T]]() Encoder[T] {
  38. return &BingenEncoder[T, U]{
  39. fileExt: "",
  40. }
  41. }
  42. // NewBingenFileEncoder creates a new `Encoder[T]` implementation which supports binary encoding for the
  43. // 'T' type with the ".bingen" file extension.
  44. func NewBingenFileEncoder[T any, U BinaryMarshalerPtr[T]]() Encoder[T] {
  45. return &BingenEncoder[T, U]{
  46. fileExt: "bingen",
  47. }
  48. }
  49. // Encode encodes the provided data of type T into a byte slice using the BinaryMarshaler interface.
  50. func (b *BingenEncoder[T, U]) Encode(data *T) ([]byte, error) {
  51. var bingenData U = data
  52. return bingenData.MarshalBinary()
  53. }
  54. // EncodeTo performs a streaming write to an io.Writer instead of writing and returning the full
  55. // binary encoding.
  56. func (b *BingenEncoder[T, U]) EncodeTo(writer io.Writer, data *T) error {
  57. var bingenData U = data
  58. return bingenData.MarshalBinaryTo(writer)
  59. }
  60. // FileExt returns the configured file extension for the encoded data. This may be an empty
  61. // string when no file extension is configured, or a non-empty value such as "bingen".
  62. func (b *BingenEncoder[T, U]) FileExt() string {
  63. return b.fileExt
  64. }
  65. // JSONEncoder[T] is a generic encoder that uses the JSON encoding format to encode data.
  66. type JSONEncoder[T any] struct{}
  67. // NewJSONEncoder creates an `Encoder[T]` implementation which supports JSON encoding for the `T`
  68. // type.
  69. func NewJSONEncoder[T any]() Encoder[T] {
  70. return new(JSONEncoder[T])
  71. }
  72. // Encode encodes the provided data of type T into a byte slice using JSON encoding.
  73. func (j *JSONEncoder[T]) Encode(data *T) ([]byte, error) {
  74. return json.Marshal(data)
  75. }
  76. // EncodeTo performs a streaming write to an io.Writer instead of writing and returning the full
  77. // binary encoding.
  78. func (j *JSONEncoder[T]) EncodeTo(writer io.Writer, data *T) error {
  79. jsonWriter := json.NewEncoder(writer)
  80. return jsonWriter.Encode(data)
  81. }
  82. // FileExt returns the file extension for the encoded data. In this case, it returns "json" to indicate
  83. // that the data is in JSON format.
  84. func (j *JSONEncoder[T]) FileExt() string {
  85. return "json"
  86. }
  87. type GZipEncoder[T any] struct {
  88. encoder Encoder[T]
  89. level int
  90. }
  91. // NewGZipEncoder creates a new GZip encoder which wraps the provided encoder.
  92. // The encoder is used to encode the data before compressing it with GZip.
  93. func NewGZipEncoder[T any](encoder Encoder[T]) Encoder[T] {
  94. return NewGZipEncoderWithLevel(encoder, gzip.DefaultCompression)
  95. }
  96. // NewGZipEncoderWithLevel creates a new GZip encoder which wraps the provided encoder,
  97. // and uses the specified encoding level when gzipping.
  98. func NewGZipEncoderWithLevel[T any](encoder Encoder[T], level int) Encoder[T] {
  99. return &GZipEncoder[T]{
  100. encoder: encoder,
  101. level: level,
  102. }
  103. }
  104. // Encode encodes the provided data of type T into a byte slice using JSON encoding.
  105. func (gz *GZipEncoder[T]) Encode(data *T) ([]byte, error) {
  106. encoded, err := gz.encoder.Encode(data)
  107. if err != nil {
  108. return nil, fmt.Errorf("GZipEncoder: nested encode failure: %w", err)
  109. }
  110. compressed, err := gZipEncode(encoded, gz.level)
  111. if err != nil {
  112. return nil, fmt.Errorf("GZipEncoder: failed to compress encoded data: %w", err)
  113. }
  114. return compressed, nil
  115. }
  116. // EncodeTo performs a streaming write to an io.Writer instead of writing and returning the full
  117. // binary encoding.
  118. func (gz *GZipEncoder[T]) EncodeTo(writer io.Writer, data *T) error {
  119. gzWriter, err := gzip.NewWriterLevel(writer, gz.level)
  120. if err != nil {
  121. return fmt.Errorf("failed to create gzip writer: %w", err)
  122. }
  123. if err := gz.encoder.EncodeTo(gzWriter, data); err != nil {
  124. _ = gzWriter.Close()
  125. return fmt.Errorf("failed to encode to gzip writer: %w", err)
  126. }
  127. return gzWriter.Close()
  128. }
  129. func gZipEncode(data []byte, level int) ([]byte, error) {
  130. var buf bytes.Buffer
  131. gzWriter, err := gzip.NewWriterLevel(&buf, level)
  132. if err != nil {
  133. return nil, err
  134. }
  135. if _, err := gzWriter.Write(data); err != nil {
  136. _ = gzWriter.Close()
  137. return nil, err
  138. }
  139. if err := gzWriter.Close(); err != nil {
  140. return nil, err
  141. }
  142. return buf.Bytes(), nil
  143. }
  144. // FileExt returns the file extension for the encoded data. In this case, it returns the wrapped encoder's
  145. // file extension with ".gz" appended to indicate that the data is compressed with GZip.
  146. func (gz *GZipEncoder[T]) FileExt() string {
  147. prev := gz.encoder.FileExt()
  148. if prev == "" {
  149. return "gz"
  150. }
  151. return prev + ".gz"
  152. }
  153. // ProtoMessagePtr [T] is a generic constraint to ensure types passed to the encoder implement
  154. // proto.Message and are pointers to T.
  155. type ProtoMessagePtr[T any] interface {
  156. proto.Message
  157. *T
  158. }
  159. // ProtobufEncoder [T, U] is a generic encoder that uses the proto.Message interface to encode data.
  160. // It supports any type T that implements the proto.Message interface.
  161. type ProtobufEncoder[T any, U ProtoMessagePtr[T]] struct{}
  162. // NewProtobufEncoder creates an `Encoder[T]` implementation which supports binary encoding for the `T`
  163. // type.
  164. func NewProtobufEncoder[T any, U ProtoMessagePtr[T]]() Encoder[T] {
  165. return new(ProtobufEncoder[T, U])
  166. }
  167. // Encode encodes the provided data of type T into a byte slice using the proto.Message interface.
  168. func (p *ProtobufEncoder[T, U]) Encode(data *T) ([]byte, error) {
  169. var message U = data
  170. raw, err := proto.Marshal(message)
  171. if err != nil {
  172. return nil, fmt.Errorf("failed to encode protobuf message: %w", err)
  173. }
  174. return raw, nil
  175. }
  176. // EncodeTo performs a streaming write to an io.Writer instead of writing and returning the full
  177. // binary encoding.
  178. func (p *ProtobufEncoder[T, U]) EncodeTo(writer io.Writer, data *T) error {
  179. var message U = data
  180. bytes, err := proto.Marshal(message)
  181. if err != nil {
  182. return fmt.Errorf("failed to encode protobuf message: %w", err)
  183. }
  184. if _, err = writer.Write(bytes); err != nil {
  185. return fmt.Errorf("failed to write encoded message to writer: %w", err)
  186. }
  187. return nil
  188. }
  189. // FileExt returns the file extension for the encoded data. In this case, it returns an empty string
  190. // to indicate that there is no specific file extension for the binary encoded data.
  191. func (p *ProtobufEncoder[T, U]) FileExt() string {
  192. return "binpb"
  193. }
  194. // ProtoJsonEncoder [T, U] is a generic encoder that uses the proto.Message interface to encode data in json format.
  195. // It supports any type T that implements the proto.Message interface.
  196. type ProtoJsonEncoder[T any, U ProtoMessagePtr[T]] struct{}
  197. // NewProtoJsonEncoder creates an `Encoder[T]` implementation which supports binary encoding for the `T`
  198. // type.
  199. func NewProtoJsonEncoder[T any, U ProtoMessagePtr[T]]() Encoder[T] {
  200. return new(ProtoJsonEncoder[T, U])
  201. }
  202. // Encode encodes the provided data of type T into a byte slice using the proto.Message interface.
  203. func (p *ProtoJsonEncoder[T, U]) Encode(data *T) ([]byte, error) {
  204. var message U = data
  205. raw, err := protojson.Marshal(message)
  206. if err != nil {
  207. return nil, fmt.Errorf("failed to encode protobuf message to json: %w", err)
  208. }
  209. return raw, nil
  210. }
  211. // EncodeTo performs a streaming write to an io.Writer instead of writing and returning the full
  212. // binary encoding.
  213. func (p *ProtoJsonEncoder[T, U]) EncodeTo(writer io.Writer, data *T) error {
  214. var message U = data
  215. // protojson doesn't have a way to marshal directly to an io.Writer, so we'll encode as normal,
  216. // and write the resulting data out to the writer
  217. bytes, err := protojson.Marshal(message)
  218. if err != nil {
  219. return fmt.Errorf("failed to marshal protojson: %w", err)
  220. }
  221. _, err = writer.Write(bytes)
  222. if err != nil {
  223. return fmt.Errorf("failed to write encoded protojson to writer: %w", err)
  224. }
  225. return nil
  226. }
  227. // FileExt returns the file extension for the encoded data. In this case, it returns an empty string
  228. // to indicate that there is no specific file extension for the binary encoded data.
  229. func (p *ProtoJsonEncoder[T, U]) FileExt() string {
  230. return "json"
  231. }