encoder.go 9.1 KB

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