Sign in

    moonmime

    moonmime — the MIME family for MoonBit: media types and their parameters, the extension-to-type registry, Content-Disposition, multipart bodies and multipart/form-data, each read and written from bytes. No sockets, no files.

    mime
    media-type
    mimetypes
    multipart
    form-data
    content-disposition
    moonbit
    Download zip
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    5 hours ago
    Downloads
    75

    #moonmime

    The MIME family for MoonBit: media types and their parameters, the extension-to-type registry, Content-Disposition, multipart bodies, and multipart/form-data. Bytes in, data out — no sockets, no files, no global state.

    #What is here

    • media_type / MediaType — parse a Content-Type into its essence and parameters (RFC 2045 §5.1, RFC 2046 §4.2), and write it back.
    • guess_type / guess_extension — the extension-to-type table Python's mimetypes ships: which type a filename implies, which extension a type usually takes.
    • content_disposition / Disposition — build and parse Content-Disposition (RFC 6266), with filename and filename* (RFC 5987).
    • multipart — split any multipart body into parts (RFC 2046 §5.1); the shape a form upload and an email share.
    • formdata / Form — read multipart/form-data (RFC 7578) into text fields and file uploads, bounded by Limits. Form::new builds one from fields and files, so a decoder of the other form encoding hands back the same type.

    #Bounds

    multipart and formdata take a Limits — a thousand parts of a megabyte each by default, what Starlette and python-multipart default to. A body over its bound is refused whole, not truncated. A body that stops making sense returns what it read so far; strict turns that into an error.

    #Not here

    • Sockets, files, streams: parts are held whole; spilling a part to disk is the caller's loop around this package.
    • application/x-www-form-urlencoded: that is the WHATWG URL Standard's, and lives in moonurl as Query. Picking one of the two form decoders by Content-Type is moonhttp's form.
    • Transfer encodings: a part's Content-Transfer-Encoding header is surfaced on the part; decoding it is the caller's call.

    #Example

    examples/tour/main.mbt runs the calls end to end — moon run examples/tour: guessing tacos-al-pastor.png, naming receta "de la abuela".pdf the RFC 5987 way, and pulling chiles en nogada out of a form.

    #Licence

    Apache-2.0. See LICENSE.

    Refused

    pub(all) suberror Refused {
    Parts(limit~ : Int)
    Part(limit~ : Int, got~ : Int)
    Malformed(at~ : Int)
    NotAForm(content_type~ : String)
    } derive(Eq,
    Debug
    )

    Why a body was not read as a form.

    A body over its bounds is refused whole rather than truncated: a handler given the first thousand parts of a larger body would be answering a request nobody sent.

    Refused::equal

    fn Refused::equal(Refused, Refused) -> Bool

    Refused::not_equal

    fn Refused::not_equal(x : Refused, y : Refused) -> Bool

    Refused::to_repr

    Disposition

    pub(all) struct Disposition {
    kind : String
    params : Array[(String, String)]
    } derive(Eq,
    Debug
    )

    A parsed Content-Disposition header (RFC 6266).

    kind is the lowercased disposition type — inline, attachment, form-data — and params the rest, names lowercased and quoted values unquoted.

    Disposition::equal

    fn Disposition::equal(Disposition, Disposition) -> Bool

    Disposition::filename

    fn Disposition::filename(self : Disposition) -> String?

    The filename the disposition names, filename* first (RFC 6266 §4.3).

    A filename* that is not UTF-8 falls back to the plain filename.

    Disposition::not_equal

    fn Disposition::not_equal(x : Disposition, y : Disposition) -> Bool

    Disposition::param

    fn Disposition::param(self : Disposition, name : StringView) -> String?

    A parameter by lowercased name.

    Disposition::parse

    fn Disposition::parse(s : StringView) -> Disposition

    Parse a header value. Lenient, like [media_type].

    Field

    pub(all) struct Field {
    name : String
    value : String
    } derive(Eq,
    Debug
    )

    A named text value from a form.

    Field::equal

    fn Field::equal(Field, Field) -> Bool

    Field::not_equal

    fn Field::not_equal(x : Field, y : Field) -> Bool

    Field::to_repr

    Form

    type Form

    A parsed form: its text fields and its files.

    Form::entries

    fn Form::entries(self : Form) -> Array[Field]

    Every field, in the order they were sent.

    Form::field

    fn Form::field(self : Form, name : StringView) -> String?

    The first value under this name.

    Form::file

    fn Form::file(self : Form, name : StringView) -> Upload?

    The first file under this name.

    Form::is_empty

    fn Form::is_empty(self : Form) -> Bool

    Whether anything was parsed at all.

    Form::new

    fn Form::new(fields? : Array[Field], files? : Array[Upload]) -> Form

    A form holding these fields and files, for a decoder of another encoding (application/x-www-form-urlencoded lives in moonurl) to hand back the same type this package reads multipart into.

    Form::uploads

    fn Form::uploads(self : Form) -> Array[Upload]

    Every file, in the order they were sent.

    Form::values

    fn Form::values(self : Form, name : StringView) -> Array[String]

    Every value under this name — a set of checkboxes sends the same name many times, and taking only the first silently drops the rest.

    Limits

    pub(all) struct Limits {
    parts : Int
    part_size : Int
    } derive(Eq,
    Debug
    )

    What a body is allowed to be.

    A body arrives from whoever sent it, so both bounds are needed: a million empty parts and one enormous part are different ways of asking a server to allocate more than it has.

    Limits::equal

    fn Limits::equal(Limits, Limits) -> Bool

    Limits::new

    fn Limits::new(parts? : Int, part_size? : Int) -> Limits

    Build a bound by naming the parts that differ from the default.

    Limits::not_equal

    fn Limits::not_equal(x : Limits, y : Limits) -> Bool

    Limits::to_repr

    MediaType

    pub(all) struct MediaType {
    essence : String
    params : Array[(String, String)]
    } derive(Eq,
    Debug
    )

    A media type: text/plain; charset=utf-8.

    essence is the lowercased type/subtype; params keeps the parameters in order, names lowercased and quoted values unquoted. A malformed parameter is dropped rather than guessed at.
    impl Show for MediaType

    MediaType::equal

    fn MediaType::equal(MediaType, MediaType) -> Bool

    MediaType::not_equal

    fn MediaType::not_equal(x : MediaType, y : MediaType) -> Bool

    MediaType::output

    fn MediaType::output(self : MediaType, logger : &Logger) -> Unit

    MediaType::param

    fn MediaType::param(self : MediaType, name : StringView) -> String?

    A parameter by lowercased name, quoted or bare.

    MediaType::subtype

    fn MediaType::subtype(self : MediaType) -> String

    The subtype half: plain in text/plain, empty when there is no slash.

    MediaType::to_string

    fn MediaType::to_string(self : MediaType) -> String

    MediaType::type_

    fn MediaType::type_(self : MediaType) -> String

    The type half: text in text/plain.

    Part

    pub(all) struct Part {
    headers : Array[(String, String)]
    body : Bytes
    } derive(Eq,
    Debug
    )

    One part of a multipart body: its header fields and its body octets.

    Part::content_type

    fn Part::content_type(self : Part) -> MediaType?

    The part's Content-Type, parsed; None when it has none.

    Part::disposition

    fn Part::disposition(self : Part) -> Disposition?

    The part's Content-Disposition, parsed; None when it has none.

    Part::equal

    fn Part::equal(Part, Part) -> Bool

    Part::header

    fn Part::header(self : Part, name : StringView) -> String?

    One of the part's own headers, by lowercased name.

    Part::not_equal

    fn Part::not_equal(x : Part, y : Part) -> Bool

    Part::to_repr

    Upload

    pub(all) struct Upload {
    name : String
    filename : String
    content_type : String
    content : Bytes
    headers : Array[(String, String)]
    } derive(Eq,
    Debug
    )

    A file from a multipart/form-data body.

    The content is held whole. Streaming a part to disk instead of into memory is what limits exists to make unnecessary until it is built.

    Upload::equal

    fn Upload::equal(Upload, Upload) -> Bool

    Upload::header

    fn Upload::header(self : Upload, name : StringView) -> String?

    One of the part's own headers, by lowercased name.

    Upload::not_equal

    fn Upload::not_equal(x : Upload, y : Upload) -> Bool

    Upload::size

    fn Upload::size(self : Upload) -> Int

    The size of the uploaded content in bytes.

    Upload::to_repr

    content_disposition

    fn content_disposition(filename : StringView, inline? : Bool) -> String

    The Content-Disposition value naming filename (RFC 6266).

    A name that is plain ASCII goes out quoted; anything else — a space is not enough, but an accent, a quote, a newline, a byte outside ASCII — goes out as filename*, the only form that carries them and the only one a name cannot break out of.

    inline asks the browser to show the content rather than save it.

    fallback

    let fallback : String

    What a response says it is when nothing says otherwise.

    formdata

    fn formdata(body : BytesView, boundary : StringView, limits? : Limits, strict? : Bool) -> Form raise Refused

    Parse a multipart/form-data body (RFC 7578) given its boundary.

    A part goes into the files when its Content-Disposition carries a filename and into the fields when it does not, which is the only thing distinguishing them on the wire.

    guess_extension

    fn guess_extension(value : StringView) -> String?

    The extension most people use for a media type, with its dot; None when the type is not in the table. Parameters are ignored, so the value may be given with its charset.

    guess_type

    fn guess_type(filename : StringView) -> String

    The media type a filename's extension implies; fallback for anything else.

    The last dot wins, so archive.tar.gz resolves on gz. The table is the common part of the IANA registry, as Python's mimetypes ships it; a caller that knows its type names it rather than hoping the extension does.

    limits

    let limits : Limits

    A thousand parts of a megabyte each — what Starlette's max_files and max_fields default to, with python-multipart's part size. A caller that knows its own shape should say so.

    media_type

    fn media_type(s : StringView) -> MediaType

    Parse a header value into a media type. Lenient: anything that is not a parameter is left out, so an empty or foreign value yields an empty essence rather than an error.

    multipart

    fn multipart(body : BytesView, boundary : StringView, limits? : Limits, strict? : Bool) -> Array[Part] raise Refused

    Split a multipart body into its parts (RFC 2046 §5.1) given its boundary.

    Preamble and epilogue are dropped; the two CRLF before each delimiter belong to the delimiter, not to the part. A body that stops making sense returns what it read so far, unless strict says to refuse it.