File size: 5,534 Bytes
fea99b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package commonhttp

import (
	"bytes"
	"context"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"mime"
	"net/http"
	"net/url"

	"github.com/openmeterio/openmeter/pkg/framework/transport/httptransport/encoder"
	"github.com/openmeterio/openmeter/pkg/models"
)

// GetMediaType returns the media type of the request.
// If the media type is invalid, it defaults to JSON.
func GetMediaType(r *http.Request) (string, error) {
	var err error

	// Parse media type
	accept := r.Header.Get("Accept")
	if accept == "" {
		accept = "application/json"
	}

	mediatype, _, err := mime.ParseMediaType(accept)
	// Browser can send back media type Go marks as invalid
	// If that happens, default to JSON
	if err != nil {
		err = fmt.Errorf("invalid media type, default to json: %w", err)
		mediatype = "application/json"
	}

	return mediatype, err
}

// JSONResponseEncoder encodes a response as JSON.
func JSONResponseEncoder[Response any](_ context.Context, w http.ResponseWriter, _ *http.Request, response Response) error {
	return jsonResponseEncoder(w, http.StatusOK, response)
}

// JSONResponseEncoder encodes a response as JSON.
func jsonResponseEncoder[Response any](w http.ResponseWriter, statusCode int, response Response) error {
	buf := &bytes.Buffer{}
	enc := json.NewEncoder(buf)
	enc.SetEscapeHTML(true)

	if err := enc.Encode(response); err != nil {
		return err
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(statusCode)

	_, err := w.Write(buf.Bytes())
	if err != nil {
		return err
	}

	return nil
}

func JSONResponseEncoderWithStatus[Response any](statusCode int) encoder.ResponseEncoder[Response] {
	return func(ctx context.Context, w http.ResponseWriter, _ *http.Request, response Response) error {
		return jsonResponseEncoder(w, statusCode, response)
	}
}

// PlainTextResponseEncoder encodes a response as PlainText.
func PlainTextResponseEncoder[Response string](_ context.Context, w http.ResponseWriter, _ *http.Request, response Response) error {
	return plainTextResponseEncoder(w, http.StatusOK, response)
}

// PlainTextResponseEncoder encodes a response as PlainText.
func plainTextResponseEncoder[Response string](w http.ResponseWriter, statusCode int, response Response) error {
	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(statusCode)

	_, err := w.Write([]byte(response))
	if err != nil {
		return err
	}

	return nil
}

// CSVResponse is a response that can be encoded as CSV.
type CSVResponse interface {
	FileName() string
	Records() [][]string
}

// CSVResponseEncoder encodes a response as CSV.
func CSVResponseEncoder[Response CSVResponse](_ context.Context, w http.ResponseWriter, _ *http.Request, response Response) error {
	return csvResponseEncoder(w, http.StatusOK, response)
}

// CSVResponseEncoder encodes a response as CSV.
func csvResponseEncoder[Response CSVResponse](w http.ResponseWriter, statusCode int, response Response) error {
	buf := &bytes.Buffer{}
	writer := csv.NewWriter(buf)
	if err := writer.WriteAll(response.Records()); err != nil {
		return fmt.Errorf("writing record to csv: %w", err)
	}
	if err := writer.Error(); err != nil {
		return fmt.Errorf("writing csv: %w", err)
	}

	w.Header().Set("Content-Type", "text/csv")
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.csv", response.FileName()))
	w.WriteHeader(statusCode)

	if _, err := w.Write(buf.Bytes()); err != nil {
		return err
	}
	return nil
}

func EmptyResponseEncoder[Response any](statusCode int) encoder.ResponseEncoder[Response] {
	return func(_ context.Context, w http.ResponseWriter, _ *http.Request, resp Response) error {
		w.WriteHeader(statusCode)
		return nil
	}
}

// DummyErrorEncoder is a dummy error encoder that always returns a 400 status code with the received error.
func DummyErrorEncoder() encoder.ErrorEncoder {
	return func(ctx context.Context, err error, w http.ResponseWriter, _ *http.Request) bool {
		NewHTTPError(http.StatusBadRequest, err).EncodeError(ctx, w)
		return true
	}
}

// GenericErrorEncoder is an error encoder that encodes the error as a generic error.
func GenericErrorEncoder() encoder.ErrorEncoder {
	return func(ctx context.Context, err error, w http.ResponseWriter, r *http.Request) bool {
		return HandleIssueIfHTTPStatusKnown(ctx, err, w) ||
			HandleErrorIfTypeMatches[*models.GenericConflictError](ctx, http.StatusConflict, err, w) ||
			HandleErrorIfTypeMatches[*models.GenericForbiddenError](ctx, http.StatusForbidden, err, w) ||
			HandleErrorIfTypeMatches[*models.GenericNotImplementedError](ctx, http.StatusNotImplemented, err, w) ||
			HandleErrorIfTypeMatches[*models.GenericValidationError](ctx, http.StatusBadRequest, err, w) ||
			HandleErrorIfTypeMatches[*models.GenericNotFoundError](ctx, http.StatusNotFound, err, w) ||
			HandleErrorIfTypeMatches[*models.GenericUnauthorizedError](ctx, http.StatusUnauthorized, err, w) ||
			HandleErrorIfTypeMatches[*models.GenericPreConditionFailedError](ctx, http.StatusPreconditionFailed, err, w)
	}
}

func RedirectResponseEncoder[Response string](statusCode int) encoder.ResponseEncoder[Response] {
	return func(ctx context.Context, w http.ResponseWriter, r *http.Request, response Response) error {
		redirectURL := string(response)

		_, err := url.Parse(redirectURL)
		if err != nil {
			return fmt.Errorf("invalid redirect url: %w", err)
		}

		if statusCode < 300 || statusCode > 399 {
			return fmt.Errorf("invalid redirect status code: it must be in 3xx range: %w", err)
		}

		http.Redirect(w, r, redirectURL, statusCode)

		return nil
	}
}