marianoguerra/slack/api does not have a README file

    Transport

    pub(open) trait Transport {
    async fn send(Self, HttpRequest) -> HttpResponse
    fn describe(Self) -> String
    }

    A thing that can send a Slack request and return what came back.

    Deliberately dumb: no retries, no rate limiting, no JSON. Those are decided above, where they can be tested without a socket. A transport's whole job is bytes out, bytes in -- and reporting the status rather than interpreting it, because a Slack error arrives as HTTP 200.

    SlackError

    pub(all) suberror SlackError {
    RequestError(String)
    HttpError(status~ : Int, headers~ : Map[String, String], body~ : String)
    PlatformError(result~ : ApiResponse)
    RateLimitedError(retry_after~ : Int)
    FileUploadInvalidArgumentsError(String)
    FileUploadReadFileDataError(String)
    } derive(
    Debug
    )

    impl Show for SlackError

    SlackError::code

    fn SlackError::code(self : SlackError) -> String

    node-slack-sdk's ErrorCode, verbatim.

    SlackError::describe_error

    fn SlackError::describe_error(self : SlackError) -> String

    node-slack-sdk's message text, verbatim.

    SlackError::describe_error_java

    fn SlackError::describe_error_java(self : SlackError) -> String

    java-slack-sdk's SlackApiException phrasing.

    Offered alongside the node one because the two ecosystems' operators recognise different shapes, and because this is the only form that shows needed and provided -- which, on a missing_scope, is the entire content of the error.

    ApiResponse

    pub(all) struct ApiResponse {
    ok : Bool
    error : String?
    warning : String?
    needed : String?
    provided : String?
    response_metadata : ResponseMetadata
    raw : Json
    } derive(Eq,
    Debug
    )

    One decoded Slack response.

    ApiResponse::get

    fn ApiResponse::get(self : ApiResponse, key : String) -> Json?

    Read a field out of the raw body.

    ApiResponse::get_str

    fn ApiResponse::get_str(self : ApiResponse, key : String) -> String?

    Read a string field out of the raw body, None if absent or not a string.

    ApiResponse::of_http

    fn ApiResponse::of_http(resp : HttpResponse) -> ApiResponse

    Decode a response body, merging in the headers Slack answers with.

    Never raises. A body that is not JSON becomes {ok: false, error: <the rawtext>} -- node-slack-sdk's behaviour, and the right one: a proxy's HTML error page or a captive portal's login screen is far more useful surfaced as the error than swallowed into "malformed response".

    ApiResponse::of_json

    fn ApiResponse::of_json(body : Json) -> ApiResponse

    Decode an already-parsed body. Split out from of_http because the corpus tests and the paginator both work from JSON with no HTTP around it.

    BoolStyle

    pub(all) enum BoolStyle {
    TrueFalse
    OneZero
    } derive(Eq,
    Debug
    )

    How booleans reach the wire.

    The reference SDKs disagree: node-slack-sdk sends true/false, java-slack-sdk sends 1/0. Slack accepts both, so this is a knob rather than a fork -- and having it is what lets both SDKs' golden strings be tested literally.

    HttpRequest

    pub(all) struct HttpRequest {
    url : String
    http_method : String
    headers : Map[String, String]
    body : Bytes
    } derive(Eq,
    Debug
    )

    One outbound call, already reduced to bytes.

    A struct rather than a (method, params) pair, because the token has been lifted into a header by this point and the body has been serialised exactly once. A transport that could still see the params would be free to re-serialise them differently from whatever signed or measured them.

    HttpRequest::body_text

    fn HttpRequest::body_text(self : HttpRequest) -> String

    The body as text. For assertions and for debug logging -- never for computing a length.

    HttpRequest::content_length

    fn HttpRequest::content_length(self : HttpRequest) -> Int

    The UTF-8 byte count, which is what Content-Length must carry.

    HttpResponse

    pub(all) struct HttpResponse {
    status : Int
    headers : Map[String, String]
    body : String
    } derive(Eq,
    Debug
    )

    HttpResponse::header

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

    ParamValue

    pub(all) enum ParamValue {
    Str(String)
    Int(Int)
    I64(Int64)
    Num(Double)
    Bool(Bool)
    Csv(Array[String])
    JsonVal(Json)
    } derive(Eq,
    Debug
    )

    One argument value.

    A closed set rather than Json, because Slack's form encoding treats these cases differently and a caller who hands over a bare Json has already lost the distinction: Str("42") and Int(42) are the same bytes on the wire, but Csv(["a","b"]) and JsonVal(["a","b"]) are a,b and ["a","b"], and which one a field wants is a per-field fact from Slack's docs.

    ParamValue::to_wire

    fn ParamValue::to_wire(self : ParamValue, bool_style~ : BoolStyle) -> String

    The value as it appears between the = and the &, before percent-encoding.

    Params

    pub(all) struct Params {
    entries : Array[(String, ParamValue)]
    } derive(Eq,
    Debug
    )

    An ordered list of (name, value) pairs.

    Ordered, and not a Map: every reference SDK's golden request body is position-sensitive, MoonBit's Map is a hash map, and a body whose field order changes between runs is miserable to diff in a proxy log.

    Params::length

    fn Params::length(self : Params) -> Int

    Params::new

    fn Params::new() -> Params

    Params::of

    fn Params::of(entries : Array[(String, ParamValue)]) -> Params

    Params::put

    fn Params::put(self : Params, key : String, value : ParamValue) -> Unit

    Append a value unconditionally.

    Params::put_bool

    fn Params::put_bool(self : Params, key : String, value : Bool?) -> Unit

    Params::put_csv

    fn Params::put_csv(self : Params, key : String, value : Array[String]?) -> Unit

    Params::put_i64

    fn Params::put_i64(self : Params, key : String, value : Int64?) -> Unit

    Params::put_int

    fn Params::put_int(self : Params, key : String, value : Int?) -> Unit

    Params::put_json

    fn Params::put_json(self : Params, key : String, value : Json?) -> Unit

    Params::put_num

    fn Params::put_num(self : Params, key : String, value : Double?) -> Unit

    Params::put_opt

    fn Params::put_opt(self : Params, key : String, value : ParamValue?) -> Unit

    Append a value, or nothing at all when it is None.

    The whole put_* family exists for this one behaviour. An absent argument must vanish from the body; it must never become the four characters null, which is what a naive to_string on an option produces and is the most common form-encoding bug in hand-rolled Slack clients. Slack would read it as the literal string "null" and reject or, worse, accept it.

    Params::put_str

    fn Params::put_str(self : Params, key : String, value : String?) -> Unit

    ResponseMetadata

    pub(all) struct ResponseMetadata {
    next_cursor : String?
    warnings : Array[String]
    messages : Array[String]
    scopes : Array[String]
    accepted_scopes : Array[String]
    retry_after : Int?
    extra : Map[String, Json]
    } derive(Eq,
    Debug
    )

    The response_metadata object, plus the three things Slack puts in headers rather than in the body.

    Merging the headers in here follows node-slack-sdk: a caller who has to reach back into the raw response to find out which scopes their token was missing will simply not bother, and the whole point of needed/provided and scopes is to make that diagnosable.

    ResponseMetadata::diagnostics

    fn ResponseMetadata::diagnostics(self : ResponseMetadata) -> Array[(Severity, String)]

    Split messages into (severity, text) pairs.

    Returned rather than logged. This library has no logger -- node-slack-sdk's equivalent writes to one and a caller who wants to fail the request on an [ERROR] has to intercept logging to find out. An entry with no recognised prefix is a Warn, which is the safer default for something Slack chose to tell you about.

    ResponseMetadata::empty

    Severity

    pub(all) enum Severity {
    Err
    Warn
    } derive(Eq,
    Debug
    )

    How seriously Slack meant a response_metadata.messages entry.
    impl Show for Severity

    build_request

    fn build_request(base_url : String, api_method : String, params : Params, token? : String, extra_headers? : Map[String, String], bool_style? : BoolStyle) -> HttpRequest

    Assemble one Web API request.

    The token becomes Authorization: Bearer and is never written into the body. That is not a style choice: a form body ends up in proxy access logs, crash dumps and error trackers, whereas an Authorization header is redacted by convention nearly everywhere. node-slack-sdk has a test asserting the token is absent from the body for exactly this reason.

    decode_form

    fn decode_form(body : String) -> Array[(String, String)]

    Decode an application/x-www-form-urlencoded body into ordered pairs.

    The inverse of encode_form, and it lives here rather than beside its first caller because it has a second one that has nothing to do with testing: Slack delivers slash commands and interactivity payloads as form bodies, and @signature -- which verifies exactly those requests -- is already in this module. Anyone handling one today writes this by hand.

    Ordered, and not a Map, for the same reason Params is: the order is information, a repeated key is legal, and a caller comparing what went out with what came back wants both preserved.

    An entry with no = yields an empty value, which is how a&b=1 is read everywhere else. An EMPTY body yields no pairs -- not one pair of two empty strings, which is what a naive split gives and is the bug that makes chat.postMessage with no arguments look like a request for a channel named "".

    default_api_url

    let default_api_url : String

    encode_form

    fn encode_form(params : Params, bool_style? : BoolStyle) -> String

    Serialise to an application/x-www-form-urlencoded body.

    normalize_api_url

    fn normalize_api_url(url : String) -> String

    Ensure the base URL ends in /, so that base + method is a URL.

    node-slack-sdk's constructor does exactly this, and node's own tests cover it: without the fixup, a caller who passes https://example.com/slack/api silently posts to https://example.com/slack/apichat.postMessage and gets a 404 that names a method they never called.

    parse_retry_after

    fn parse_retry_after(header : String) -> Int?

    Parse a Retry-After header holding a delay in seconds.

    Prefix-greedy and lenient, matching node-slack-sdk's Number.parseInt: "120" and "120s" are both 120, and anything that does not start with a digit is None rather than zero. Zero would mean "retry immediately", which is the worst possible reading of a header you failed to understand.

    Slack always sends seconds here. The HTTP-date form the RFC also allows is deliberately not supported: parsing it would need a clock, and this package does not have one.

    percent_decode_component

    fn percent_decode_component(s : String) -> String

    Decode one percent-encoded form field name or value.

    Over the UTF-8 bytes, like the encoder, and for the same reason: MoonBit strings are UTF-16, so decoding per code unit would reassemble %C3%A3 as two characters instead of one and mangle every accented value that encoded correctly.

    An invalid escape -- %zz, a trailing %, %4 at the end -- is left as the literal characters rather than raising. WHATWG's urlencoded parser does the same, and a hand-built body with one stray % in it is far more useful decoded than rejected.

    + becomes a space. percent_encode_component never emits + for a space (it emits %20, which is what node:querystring and OkHttp's FormBody both send) and it escapes a literal + as %2B -- so reading + as a space costs this pairing nothing, and it is what a body from any other client means.

    percent_encode_component

    fn percent_encode_component(s : String) -> String

    Percent-encode one form field name or value.

    Over the UTF-8 bytes, not the characters: MoonBit strings are UTF-16, so encoding per code unit would emit surrogate halves for anything outside the BMP and mis-encode every accented character. A body that disagrees with its own Content-Length is the failure mode.

    Space becomes %20, not +. Both are legal and Slack reads either, but %20 is what node:querystring and OkHttp's FormBody both emit, and a signature is computed over the bytes actually sent -- so "whatever the reference SDKs send" is the only defensible choice.

    split_scopes

    fn split_scopes(header : String) -> Array[String]

    Split an x-oauth-scopes-style header on commas, ignoring surrounding space.

    Exposed because the rule (/\s*,\s*/ after a trim, per node-slack-sdk) is exactly the kind of thing a caller re-derives slightly wrong and then compares against a scope name with a stray space in it.

    to_slack_error

    fn to_slack_error(e : Error) -> SlackError

    Narrow an arbitrary Error to a SlackError.

    Variant patterns are the only way to match an error value -- the suberror type name is not a pattern -- and they are only in scope inside this package. Hence the helper, rather than every caller re-listing six variants. The same shape as marianoguerra/mcp's to_transport_error.

    user_agent

    let user_agent : String

    The library's own User-Agent contribution.

    java-slack-sdk sends Java-Slack-SDK; slack-api-client/<ver>; ... and node-slack-sdk sends @slack:web-api/<ver> node/<ver> darwin/<ver>; both name the SDK first and then the runtime. There is no runtime to name here that is true on all four backends, so this is the SDK part alone and a caller who wants to add myapp/1.2.3 passes it as an extra header.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io