tomo/data/data.go

130 lines
3.7 KiB
Go

// Package data provides operations to deal with arbitrary data and MIME types.
package data
import "io"
import "bytes"
import "strings"
// Data represents arbitrary polymorphic data that can be used for data transfer
// between applications.
type Data interface {
// Convert converts the data to the specified MIME type and returns it.
// If the type is not supported, this behavior will return nil, and
// false for ok. Note that Convert may be called multiple times for the
// same MIME type, so it must not return the same reader.
Convert (Mime) (reader io.ReadSeekCloser, ok bool)
// Supported returns a slice of MIME types that Convert can accept.
Supported () []Mime
}
// Mime represents a MIME type.
type Mime struct {
// Type is the first half of the MIME type, and Subtype is the second
// half. The separating slash is not included in either. For example,
// text/html becomes:
// Mime { Type: "text", Subtype: "html" }
// The subtype is stored here including the tree and the suffix.
Type, Subtype string
// Charset is an optional field applicable to text types that specifies
// the character encoding of the data. If empty, UTF-8 is conventionally
// assumed because it's the only text encoding worth using.
Charset string
}
// M is shorthand for creating a MIME type.
func M (ty, subtype string) Mime {
return Mime {
Type: strings.ToLower(ty),
Subtype: strings.ToLower(subtype),
}
}
// ParseMime parses a MIME type from text.
func ParseMime (text string) Mime {
ty, subty, _ := strings.Cut(text, "/")
subty, parameters, _ := strings.Cut(subty, ";")
mime := M(ty, subty)
for _, parameter := range strings.Split(parameters, " ") {
if parameter == "" { continue }
key, val, _ := strings.Cut(parameter, "=")
// TODO handle things like quoted values
val = strings.TrimSpace(val)
switch strings.TrimSpace(strings.ToLower(key)) {
case "charset":
mime.Charset = val
}
}
return mime
}
// MimePlain returns the MIME type of plain text.
func MimePlain () Mime { return Mime { Type: "text", Subtype: "plain" } }
// MimeFile returns the MIME type of a file path/URI.
func MimeFile () Mime { return Mime { Type: "text", Subtype: "uri-list" } }
// String returns the string representation of the MIME type.
func (mime Mime) String () string {
out := mime.Type + "/" + mime.Subtype
if mime.Charset != "" {
out += "; charset=" + mime.Charset
}
return out
}
// FromText returns plain text Data given a string.
func FromText (text string) Data {
return FromBytes(MimePlain(), []byte(text))
}
type byteReadCloser struct { *bytes.Reader }
func (byteReadCloser) Close () error { return nil }
type bytesData struct {
mime Mime
buffer []byte
}
// FromBytes constructs a Data given a buffer and a mime type.
func FromBytes (mime Mime, buffer []byte) Data {
return bytesData {
mime: mime,
buffer: buffer,
}
}
func (bytesDat bytesData) Convert (mime Mime) (io.ReadSeekCloser, bool) {
if mime != bytesDat.mime { return nil, false }
return byteReadCloser { bytes.NewReader(bytesDat.buffer) }, true
}
func (bytesDat bytesData) Supported () []Mime {
return []Mime { bytesDat.mime }
}
type mergedData []Data
func (merged mergedData) Convert (mime Mime) (io.ReadSeekCloser, bool) {
for _, individual := range merged {
if reader, ok := individual.Convert(mime); ok {
return reader, ok
}
}
return nil, false
}
func (merged mergedData) Supported () (supported []Mime) {
for _, individual := range merged {
supported = append(individual.Supported(), supported...)
}
return supported
}
// Merge combines several Datas together. If multiple Datas provide a reader for
// the same mime type, the ones first in the list will take precedence.
func Merge (individual ...Data) (combined Data) {
return mergedData(individual)
}