multipart

    A streaming multipart reader/writer library integrated with `moonbitlang/async`.

    multipart
    form-data
    Download zip
    Author
    Version
    0.1.2
    License
    Apache-2.0
    Last updated
    2 hours ago
    Downloads
    8

    Dependencies

    #multipart

    A streaming multipart reader/writer library integrated with moonbitlang/async.

    Reader/Writer handle MIME multipart, Form/FormWriter handle multipart/form-data. Part bodies are streamed as raw bytes.

    #Examples

    Upload a field and a file through an HTTP client:

    ///|
    async fn upload(client : @http.Client, file : &@io.Reader) -> @http.Response {
    let form = @multipart.FormWriter(client)
    client.request(Post, "/upload", extra_headers={
    "Content-Type": form.content_type(),
    })
    form.write_field("description", "Release archive")
    form.write_file("archive", filename="release.zip", file)
    form.finish()
    client.end_request()
    }

    Read an upload using the request's Content-Type and copy its archive part:

    ///|
    async fn receive_archive(
    source : &@io.Reader,
    target : &@io.Writer,
    content_type : String,
    ) -> Unit {
    let (media_type, parameters) = @multipart.parse_header_value(content_type)
    guard @http.CaseInsensitiveString(media_type) == "multipart/form-data" else {
    raise Failure("expected multipart/form-data")
    }
    guard parameters.get("boundary") is Some(boundary) else {
    raise Failure("missing multipart boundary")
    }
    let form = @multipart.Form(source, boundary~)
    while form.next_part() is Some(part) {
    if part.name() == "archive" {
    target.write_reader(part)
    }
    }
    }

    next_part() discards unread content from the previous part. Process parts sequentially; finish() leaves the underlying writer open.

    For other multipart subtypes, use Reader/Writer with your own headers:

    ///|
    async fn copy_parts(source : &@io.Reader, target : &@io.Writer) -> Unit {
    let reader = @multipart.Reader(source, boundary="incoming")
    let writer = @multipart.Writer(target, boundary="outgoing")
    while reader.next_part() is Some((headers, body)) {
    writer.write_reader(headers~, body)
    }
    writer.finish()
    }

    Choose an output boundary that does not occur in the content. FormWriter creates a random boundary automatically.

    #Tests

    See fixtures for specification sources and snapshot tests.

    Callback

    type Callback = async (&
    Writer
    ) -> Unit

    MultipartError

    pub suberror MultipartError {
    MultipartError(String)
    } derive(
    Debug
    )

    Form

    type Form

    Streams the parts of a multipart/form-data body in their original order.

    Form::Form

    fn Form::Form(source : &
    Reader
    , boundary~ : String) -> Form raise MultipartError

    Form::next_part

    async fn Form::next_part(self : Form) -> FormPart?

    Advancing discards any unread bytes from the previous part. Bodies remain bytes: charset and Content-Transfer-Encoding are not decoded. Nested multipart bodies are exposed as parts and can be traversed with Reader.

    FormPart

    type FormPart

    impl Reader for FormPart

    FormPart::content_type

    fn FormPart::content_type(self : FormPart) -> String

    Returns the full Content-Type value, including any charset parameter. RFC 7578 §4.4 specifies text/plain when the header is absent.

    FormPart::filename

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

    Returns the supplied filename. When saving a file, the caller must discard directory components and choose a suitable local name (RFC 7578 §4.2). Percent escapes remain literal because multipart/form-data does not identify whether a filename uses the optional percent-encoding convention.

    FormPart::name

    fn FormPart::name(self : FormPart) -> String

    FormWriter

    type FormWriter

    FormWriter::FormWriter

    Create a form writer with a boundary containing 192 bits of platform entropy.

    FormWriter::boundary

    fn FormWriter::boundary(self : FormWriter) -> String

    The generated boundary, for constructing a matching reader or MIME header.

    FormWriter::content_type

    fn FormWriter::content_type(self : FormWriter) -> String

    The HTTP Content-Type for the generated form body.

    FormWriter::finish

    async fn FormWriter::finish(self : FormWriter) -> Unit

    FormWriter::write_field

    async fn FormWriter::write_field(self : FormWriter, name : String, value : String) -> Unit

    Write a UTF-8 text field, preserving the value's line endings.

    FormWriter::write_file

    async fn FormWriter::write_file(self : FormWriter, name : String, filename? : String, content_type? : String, file : &
    Reader
    ) -> Unit

    Stream a file without closing its reader. An unspecified content type uses application/octet-stream; filename is optional (RFC 7578 sections 4.2–4.4).

    FormWriter::write_json

    async fn FormWriter::write_json(self : FormWriter, name : String, value : Json) -> Unit

    Write a JSON field with Content-Type application/json.

    FormWriter::write_part

    async fn FormWriter::write_part(self : FormWriter, headers~ : Map[
    CaseInsensitiveString
    , String], callback : async (&
    Writer
    ) -> Unit) -> Unit

    Write a part with caller-supplied MIME headers.

    Reader

    type Reader

    A sequential, streaming MIME multipart reader (RFC 2046 section 5.1.1). The source must be limited to the enclosing message body.

    Reader::Reader

    fn Reader::Reader(source : &
    Reader
    , boundary~ : String) -> Reader raise MultipartError

    boundary is the decoded Content-Type parameter, without quotes or --.

    Reader::next_part

    Read the next part's headers and a reader limited to its body. Advancing drains the previous part; retained readers then return EOF. Preamble and epilogue are ignored. Bodies are returned without transfer decoding. Header folding is unfolded; repeated names are comma-joined as in @http.Headers. Calls on this reader and its current part must be made sequentially.

    Writer

    type Writer

    Writer::Writer

    fn Writer::Writer(target : &
    Writer
    , boundary~ : String) -> Writer raise MultipartError

    Create a multipart writer. The caller must choose a boundary that does not occur as a delimiter in any part (RFC 2046 section 5.1.1).

    Writer::finish

    async fn Writer::finish(self : Writer) -> Unit

    Write the closing delimiter without closing the underlying writer. Repeated calls have no effect.

    Writer::write_part

    async fn Writer::write_part(self : Writer, headers~ : Map[
    CaseInsensitiveString
    , String], callback : async (&
    Writer
    ) -> Unit) -> Unit

    Write a part without buffering its body. Call parts sequentially, and use the callback's writer only until the callback returns.

    Writer::write_reader

    Stream a reader's remaining bytes as one part, leaving both streams open.

    parse_header_value

    fn parse_header_value(value : String) -> (String, Map[
    CaseInsensitiveString
    , String]) raise MultipartError

    Parses an unfolded MIME field value, such as Content-Type or Content-Disposition. The main value is a token or type/subtype; parameter names are case-insensitive. Main values and parameter values retain their case. Comments are ignored and quoted pairs are unescaped; percent escapes and extended parameters stay raw. Grammar: RFC 2045 §5.1 and RFC 2183 §2.