///|
/// A GitHub API client backed by the transport-independent SDK runtime.
///
/// REST request and response shapes come from the generated `gen` package,
/// which is produced from the vendored first-party OpenAPI document. This facade
/// does not restate them: it only sends a generated request, follows `Link`
/// pages, and reads GitHub's error bodies.
///
/// GraphQL has no such generated vocabulary — a reply's shape belongs to the
/// caller's query rather than to the schema — so it is served by the `graphql`
/// passthrough over `Json`.
pub struct GitHub {
priv client : @runtime.Client
priv graphql_url : String
}
///|
/// Redacted debug representation of a GitHub client.
pub extend GitHub with @debug.Debug::{to_repr}
///|
/// Formats a client without exposing authentication or other runtime internals.
pub impl @debug.Debug for GitHub with fn to_repr(_self) {
@debug.Repr::ctor("GitHub", [
(None, @debug.Repr::literal("")),
])
}
///|
/// Creates a GitHub client.
///
/// `token` is optional: GitHub serves public resources unauthenticated, so an
/// absent token means `NoAuth` rather than a configuration error. A present
/// token is sent as `Authorization: Bearer`, which covers personal access
/// tokens, installation tokens, and `GITHUB_TOKEN`.
///
/// `base_url` is the API root. GitHub Enterprise Server mounts the REST API
/// under `/api/v3`, so pass `https://ghe.example.com/api/v3` there.
///
/// The default headers are `accept: application/vnd.github+json`,
/// `x-github-api-version`, and `user-agent`, which GitHub requires. A generated
/// operation that needs a different representation — `application/vnd.github.diff`,
/// `.patch`, `.sarif` — sets `accept` on its own request, and the runtime keeps
/// request headers ahead of client defaults.
///
/// `retry` defaults to `RetryPolicy::conservative().with_max_retries(3)` and
/// is wrapped in `GitHubRetry`, which also waits out GitHub's primary and
/// secondary rate limits and retries a transient GraphQL failure of a query.
///
/// Unless `limiter` says otherwise, responses feed a `WindowLimiter` reading
/// `x-ratelimit-remaining` and `x-ratelimit-reset`, keyed by
/// `x-ratelimit-resource` so that a write shares the `core` window. It spaces
/// writes and GraphQL requests at least 1 s apart, writes that send
/// notifications 3 s and searches 2 s, as octokit does. `gate` makes the
/// requests of each of those groups serial, as docs.github.com asks ("make
/// requests serially instead of concurrently"): pass
/// `gate=() => @http_async.SemaphoreGate::new()` from `gaato/http-async`.
/// Without it concurrent requests are only spaced, not serialised. `gate` is
/// ignored when `limiter` is given. Pass `limiter=@runtime.NoLimiter::new()`
/// to opt out of both.
///
/// `timeouts` limits each attempt and each call (`@runtime.Timeouts`); without
/// it only the transport's own timeouts apply.
///
/// `graphql_url` is the GraphQL endpoint, which is not under the REST root:
/// GitHub Enterprise Server serves REST from `/api/v3` and GraphQL from
/// `/api/graphql`. It is derived from `base_url` and only needs passing for a
/// deployment that puts the two somewhere else.
pub fn GitHub::new(
transport : &@http.Transport,
clock : &@clock.Clock,
token? : String,
base_url~ : String = "https://api.github.com",
api_version~ : String = "2022-11-28",
user_agent~ : String = "gaato-mbt-sdk/0.1.0",
retry? : @runtime.RetryPolicy,
limiter? : &@runtime.RateLimiter,
graphql_url? : String,
gate? : () -> &@sync.Gate,
timeouts? : @runtime.Timeouts,
) -> GitHub {
let auth = match token {
Some(token) => @runtime.Bearer(token)
None => @runtime.NoAuth
}
let limiter : &@runtime.RateLimiter = match limiter {
Some(limiter) => limiter
None => default_limiter(clock, gate?)
}
let graphql_url = match graphql_url {
Some(url) => url
None => default_graphql_url(base_url)
}
let retry = GitHubRetry::{
policy: retry.unwrap_or_else(() => {
@runtime.RetryPolicy::conservative().with_max_retries(3)
}),
}
{
client: @runtime.Client::new(
transport,
clock,
base_url~,
default_headers=default_headers(api_version~, user_agent~),
auth~,
retry~,
limiter~,
timeouts?,
),
graphql_url,
}
}
///|
/// Wraps a preconfigured runtime client.
///
/// This is the escape hatch for deployments this facade does not model — a
/// proxy that authenticates differently, a GitHub Enterprise Server instance
/// with extra headers — and for tests that build the client themselves.
///
/// A runtime client keeps its `base_url` to itself, so the GraphQL endpoint
/// cannot be derived from it here and defaults to GitHub's own. Pass
/// `graphql_url` for anything else.
pub fn GitHub::from_client(
client : @runtime.Client,
graphql_url~ : String = "https://api.github.com/graphql",
) -> GitHub {
{ client, graphql_url, }
}
///|
/// The headers every request carries unless the request sets them itself.
fn default_headers(
api_version~ : String,
user_agent~ : String,
) -> @http.Headers {
@http.Headers::from_array([
("accept", "application/vnd.github+json"),
("x-github-api-version", api_version),
("user-agent", user_agent),
])
}
///|
/// The GraphQL endpoint that goes with a REST root.
///
/// GraphQL is not mounted under the REST root. GitHub Enterprise Server serves
/// REST from `http(s)://HOSTNAME/api/v3` and GraphQL from
/// `http(s)://HOSTNAME/api/graphql`, so appending `/graphql` to a `/api/v3`
/// root would address an endpoint that does not exist. On GitHub.com the two
/// share a host and appending is right.
fn default_graphql_url(base_url : String) -> String {
let root = if base_url.has_suffix("/") {
base_url[:base_url.length() - 1].to_owned()
} else {
base_url
}
if root.has_suffix("/api/v3") {
// Keep everything up to and including "/api/", then name the other API.
root[:root.length() - 2].to_owned() + "graphql"
} else {
root + "/graphql"
}
}
///|
/// The rate limit window GitHub advertises on every response, recorded under
/// the resource the response names, with GitHub's pacing and the caller's
/// gate.
fn default_limiter(
clock : &@clock.Clock,
gate? : () -> &@sync.Gate,
) -> @runtime.WindowLimiter {
@runtime.WindowLimiter::new(
clock,
@runtime.rate_limit_headers(
remaining="x-ratelimit-remaining",
reset_unix_seconds="x-ratelimit-reset",
),
gate?,
pace=github_pace,
server_key=(_, headers) => headers.get("x-ratelimit-resource"),
)
}