MIME for MoonBit: media types, Content-Disposition and multipart bodies.
moon add moonbitstack/moonmimeimport {
"moonbitstack/moonmime",
}let mt = @moonmime.media_type("Text/HTML; charset=UTF-8")
println(mt.essence) // text/html
println(mt.type_()) // text
println(mt.subtype()) // html
println(mt.param("charset").unwrap_or("")) // UTF-8
println(mt.to_string()) // text/html; charset=UTF-8let mt = @moonmime.MediaType::parse("Text/HTML; charset=\"utf-8\"").unwrap()
println(mt.essence) // text/html
println(mt.param("charset").unwrap_or("")) // utf-8
let listed = @moonmime.MediaType::parse("text/html, text/plain")
println(listed is None) // trueprintln(@moonmime.content_disposition("menu.pdf"))
// attachment; filename="menu.pdf"
println(@moonmime.content_disposition("receta \"de la abuela\".pdf"))
// attachment; filename*=utf-8''receta%20%22de%20la%20abuela%22.pdflet d = @moonmime.Disposition::parse(
"attachment; filename=\"fallback.txt\"; filename*=utf-8''r%C3%A9sum%C3%A9.txt",
)
println(d.filename().unwrap_or("")) // résumé.txtprintln(@moonmime.guess_type("photo.jpg")) // image/jpeg
println(@moonmime.guess_type("archive.tar.gz")) // application/gzip
println(@moonmime.guess_extension("image/jpeg").unwrap_or("")) // .jpg// `body` is the raw bytes; `boundary` is the delimiter without its leading dashes.
let boundary = "----edge"
let body = b"------edge\r\nContent-Disposition: form-data; name=\"recipe\"\r\n\r\nchiles en nogada\r\n------edge--\r\n"
let parts = @moonmime.multipart(body[:], boundary)
assert_eq(parts.length(), 1)
assert_eq(parts[0].header("content-disposition").unwrap_or(""), "form-data; name=\"recipe\"")
let form = @moonmime.formdata(body[:], boundary)
assert_eq(form.field("recipe"), Some("chiles en nogada"))
assert_eq(form.uploads().length(), 0)let subject = @moonmime.encode_words("Grüße aus München")
assert_eq(@moonmime.decode_words(subject), "Grüße aus München")
let qp_bytes = @qp.encode(b"caf\xc3\xa9 = coffee")
assert_eq(@qp.decode(qp_bytes), b"caf\xc3\xa9 = coffee")assert_eq(@moonmime.encode_charset(name="iso-8859-1", text="é"), b"\xe9")
assert_eq(@moonmime.decode_charset(name="iso-8859-1", data=b"\xe9"), "é")| Item | What it does |
|---|---|
| media_type(s) | Parse a Content-Type header into a MediaType, keeping what it can. |
| MediaType::parse(s) | Read a value strictly, as the MIME Sniffing standard does; None for anything else. |
| MediaType | type_, subtype, param(name), to_string. |
| guess_type(filename) | The media type a filename's extension implies. |
| guess_extension(type) | The extension people use for a media type. |
| fallback | application/octet-stream. |
| content_disposition(name, inline?) | Build a Content-Disposition header. |
| Disposition | parse, param(name), filename(). |
| multipart(body, boundary, limits?, strict?) | Split a multipart body into Parts. |
| Part | header(name), content_type(), disposition(). |
| formdata(body, boundary, limits?, strict?) | Read multipart/form-data into a Form. |
| Form | field, values, file, entries, uploads, is_empty, new. |
| Upload | size(), header(name). |
| Limits / limits | Bounds on part count and part size. |
| encode_word(text, charset?, encoding?, charsets?) | One RFC 2047 encoded word. |
| encode_words(text, ...) | A header value as encoded words, split to fit; ASCII stays as it is. |
| decode_words(value, charsets?) | Read encoded words back, dropping the folding between them. |
| WordEncoding | Base64 or QuotedPrintable, the B and Q of RFC 2047. |
| Charset | The trait a further charset implements: decode, encode. |
| decode_charset(name~, data~, charsets?) / encode_charset(...) | Bytes to text in a named charset, and back. |
| Refused | Why a body was not read: Parts, Part, Malformed, NotAForm, Charset. |
| @qp.encode(bytes) / @qp.decode(bytes, strict?) | Quoted-printable, from moonbitstack/moonmime/qp. |
let small = @moonmime.Limits::new(parts=8, part_size=64 * 1024)
assert_eq(@moonmime.formdata(body[:], boundary, limits=small).field("recipe"), Some("chiles en nogada"))moon run src/examples/lookup
moon run src/examples/form
moon run src/examples/tour| Data file | Cases | Passing |
|---|---|---|
| mimesniff/mime-types/resources/mime-types.json | 74 | 74 |
| mimesniff/mime-types/resources/generated-mime-types.json | 881 | 881 |
| Extension | moonmime | Python mimetypes | Go mime | Why |
|---|---|---|---|---|
| .3gp | video/3gpp | audio/3gpp | — | IANA registers video/3gpp for the 3GPP container; Python's audio/3gpp keeps only the audio track. |
| .heic | image/heic | image/heic | — | IANA registers image/heic for .heic, as Python does; the table's old image/heif borrowed the .heif sibling's subtype, so it is corrected to the IANA and Python side. |
| .srt | application/x-subrip | text/plain | — | neither side is IANA-registered; keeps the specific x- name over Python's generic text/plain. |
| .wav | audio/wav | audio/x-wav | — | IANA registered audio/wav in 2018, retiring audio/x-wav; takes the IANA side over Python's x- form. |
| .xml | application/xml | text/xml | text/xml | IANA registers application/xml (RFC 7303); the text/xml alias is retired, which Go still ships. |
| .sh | text/x-shellscript | application/x-sh | — | neither side is IANA-registered; keeps the semantic x- name over Python's application/x-sh. |
| .ts | video/mp2t | — | — | IANA registers video/mp2t for the MPEG-2 transport stream, the common meaning of .ts; neither reference ships it. |
| .js | text/javascript | text/javascript | text/javascript | all three agree and IANA registers text/javascript; Go's charset=utf-8 is a default parameter, not a different essence. |
type Formtest {
let mt = @moonmime.MediaType::parse("Text/HTML; charset=\"utf-8\"").unwrap()
inspect(mt.essence, content="text/html")
inspect(mt.param("charset").unwrap_or(""), content="utf-8")
inspect(
@moonmime.MediaType::parse("text/html, text/plain") is None,
content="true",
)
}fn content_disposition(filename : StringView, inline? : Bool) -> Stringtest {
let text = @moonmime.decode_charset(name="iso-8859-1", data=b"\xe9") catch {
_ => ""
}
inspect(text, content="é")
}test {
inspect(@moonmime.decode_words("=?utf-8?B?Q2Fmw6k=?="), content="Café")
}test {
let bytes = @moonmime.encode_charset(name="iso-8859-1", text="é") catch {
_ => b""
}
inspect(bytes == b"\xe9", content="true")
}fn encode_word(text : StringView, charset? : String, encoding? : WordEncoding, charsets? : Map[String, &Charset]) -> String raise Refusedtest {
let word = @moonmime.encode_word("Café") catch { _ => "" }
inspect(@moonmime.decode_words(word[:]), content="Café")
}fn encode_words(text : StringView, charset? : String, encoding? : WordEncoding, column? : Int, max? : Int, charsets? : Map[String, &Charset]) -> String raise Refusedfn guess_extension(value : StringView) -> String?fn guess_type(filename : StringView) -> StringInstall
Download zipMIME for MoonBit: media types, Content-Disposition and multipart bodies.