github.com/safing/portbase@v0.19.5/formats/dsd/http.go (about)

     1  package dsd
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"strings"
    10  )
    11  
    12  // HTTP Related Errors.
    13  var (
    14  	ErrMissingBody        = errors.New("dsd: missing http body")
    15  	ErrMissingContentType = errors.New("dsd: missing http content type")
    16  )
    17  
    18  const (
    19  	httpHeaderContentType = "Content-Type"
    20  )
    21  
    22  // LoadFromHTTPRequest loads the data from the body into the given interface.
    23  func LoadFromHTTPRequest(r *http.Request, t interface{}) (format uint8, err error) {
    24  	return loadFromHTTP(r.Body, r.Header.Get(httpHeaderContentType), t)
    25  }
    26  
    27  // LoadFromHTTPResponse loads the data from the body into the given interface.
    28  // Closing the body is left to the caller.
    29  func LoadFromHTTPResponse(resp *http.Response, t interface{}) (format uint8, err error) {
    30  	return loadFromHTTP(resp.Body, resp.Header.Get(httpHeaderContentType), t)
    31  }
    32  
    33  func loadFromHTTP(body io.Reader, mimeType string, t interface{}) (format uint8, err error) {
    34  	// Read full body.
    35  	data, err := io.ReadAll(body)
    36  	if err != nil {
    37  		return 0, fmt.Errorf("dsd: failed to read http body: %w", err)
    38  	}
    39  
    40  	// Load depending on mime type.
    41  	return MimeLoad(data, mimeType, t)
    42  }
    43  
    44  // RequestHTTPResponseFormat sets the Accept header to the given format.
    45  func RequestHTTPResponseFormat(r *http.Request, format uint8) (mimeType string, err error) {
    46  	// Get mime type.
    47  	mimeType, ok := FormatToMimeType[format]
    48  	if !ok {
    49  		return "", ErrIncompatibleFormat
    50  	}
    51  
    52  	// Request response format.
    53  	r.Header.Set("Accept", mimeType)
    54  
    55  	return mimeType, nil
    56  }
    57  
    58  // DumpToHTTPRequest dumps the given data to the HTTP request using the given
    59  // format. It also sets the Accept header to the same format.
    60  func DumpToHTTPRequest(r *http.Request, t interface{}, format uint8) error {
    61  	// Get mime type and set request format.
    62  	mimeType, err := RequestHTTPResponseFormat(r, format)
    63  	if err != nil {
    64  		return err
    65  	}
    66  
    67  	// Serialize data.
    68  	data, err := dumpWithoutIdentifier(t, format, "")
    69  	if err != nil {
    70  		return fmt.Errorf("dsd: failed to serialize: %w", err)
    71  	}
    72  
    73  	// Add data to request.
    74  	r.Header.Set("Content-Type", mimeType)
    75  	r.Body = io.NopCloser(bytes.NewReader(data))
    76  
    77  	return nil
    78  }
    79  
    80  // DumpToHTTPResponse dumpts the given data to the HTTP response, using the
    81  // format defined in the request's Accept header.
    82  func DumpToHTTPResponse(w http.ResponseWriter, r *http.Request, t interface{}) error {
    83  	// Serialize data based on accept header.
    84  	data, mimeType, _, err := MimeDump(t, r.Header.Get("Accept"))
    85  	if err != nil {
    86  		return fmt.Errorf("dsd: failed to serialize: %w", err)
    87  	}
    88  
    89  	// Write data to response
    90  	w.Header().Set("Content-Type", mimeType)
    91  	_, err = w.Write(data)
    92  	if err != nil {
    93  		return fmt.Errorf("dsd: failed to write response: %w", err)
    94  	}
    95  	return nil
    96  }
    97  
    98  // MimeLoad loads the given data into the interface based on the given mime type accept header.
    99  func MimeLoad(data []byte, accept string, t interface{}) (format uint8, err error) {
   100  	// Find format.
   101  	format = FormatFromAccept(accept)
   102  	if format == 0 {
   103  		return 0, ErrIncompatibleFormat
   104  	}
   105  
   106  	// Load data.
   107  	err = LoadAsFormat(data, format, t)
   108  	return format, err
   109  }
   110  
   111  // MimeDump dumps the given interface based on the given mime type accept header.
   112  func MimeDump(t any, accept string) (data []byte, mimeType string, format uint8, err error) {
   113  	// Find format.
   114  	format = FormatFromAccept(accept)
   115  	if format == AUTO {
   116  		return nil, "", 0, ErrIncompatibleFormat
   117  	}
   118  
   119  	// Serialize and return.
   120  	data, err = dumpWithoutIdentifier(t, format, "")
   121  	return data, mimeType, format, err
   122  }
   123  
   124  // FormatFromAccept returns the format for the given accept definition.
   125  // The accept parameter matches the format of the HTTP Accept header.
   126  // Special cases, in this order:
   127  // - If accept is an empty string: returns default serialization format.
   128  // - If accept contains no supported format, but a wildcard: returns default serialization format.
   129  // - If accept contains no supported format, and no wildcard: returns AUTO format.
   130  func FormatFromAccept(accept string) (format uint8) {
   131  	if accept == "" {
   132  		return DefaultSerializationFormat
   133  	}
   134  
   135  	var foundWildcard bool
   136  	for _, mimeType := range strings.Split(accept, ",") {
   137  		// Clean mime type.
   138  		mimeType = strings.TrimSpace(mimeType)
   139  		mimeType, _, _ = strings.Cut(mimeType, ";")
   140  		if strings.Contains(mimeType, "/") {
   141  			_, mimeType, _ = strings.Cut(mimeType, "/")
   142  		}
   143  		mimeType = strings.ToLower(mimeType)
   144  
   145  		// Check if mime type is supported.
   146  		format, ok := MimeTypeToFormat[mimeType]
   147  		if ok {
   148  			return format
   149  		}
   150  
   151  		// Return default mime type as fallback if any mimetype is okay.
   152  		if mimeType == "*" {
   153  			foundWildcard = true
   154  		}
   155  	}
   156  
   157  	if foundWildcard {
   158  		return DefaultSerializationFormat
   159  	}
   160  	return AUTO
   161  }
   162  
   163  // Format and MimeType mappings.
   164  var (
   165  	FormatToMimeType = map[uint8]string{
   166  		CBOR:    "application/cbor",
   167  		JSON:    "application/json",
   168  		MsgPack: "application/msgpack",
   169  		YAML:    "application/yaml",
   170  	}
   171  	MimeTypeToFormat = map[string]uint8{
   172  		"cbor":    CBOR,
   173  		"json":    JSON,
   174  		"msgpack": MsgPack,
   175  		"yaml":    YAML,
   176  		"yml":     YAML,
   177  	}
   178  )