README

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