//// The composition root's helpers: raw resources (clients, hosts, stores) //// become the capability records the domain modules depend on. Only wiring //// and `main` see both sides. import atproto/constellation import atproto/xrpc.{type Client} import crate/gen/catalog/edit as catalog_edit import crate/gen/catalog/release as catalog_release import crate/gen/client as generated_client import crate_server/catalog/source as catalog_source import crate_server/catalog_deps import crate_server/catalog_index import crate_server/context.{type Context, Atproto, Context, Discogs, Web} import crate_server/follow_index import crate_server/identity_cache.{type Cache, type Identity, Identity} import crate_server/identity_resolver.{Resolver} import crate_server/musicbrainz_client import crate_server/promotion import crate_server/readiness.{type Check} import crate_server/shelf_index import gleam/dynamic/decode import gleam/option.{None} import gleam/result pub fn context( client client: Client, static_directory static_directory: String, base_url base_url: String, resolver resolver: String, constellation_host constellation_host: String, discogs_auth discogs_auth, discogs_creds discogs_creds, known_users known_users, catalog_index catalog_index, follow_index follow_index: follow_index.Store, shelf_index shelf_index: shelf_index.Store, identity_cache identity_cache: Cache, readiness readiness: Check, oauth oauth, ) -> Context { let catalog = catalog_deps(client, constellation_host, resolver) let identity = Resolver(cache: identity_cache, fetch: identity_fetch(client, resolver)) Context( web: Web(static_directory:, base_url:), atproto: Atproto(client:, resolver:, identity:), discogs: Discogs( auth: discogs_auth, creds: discogs_creds, send: text_sender(client), ), catalog:, known_users:, catalog_index:, follow_index:, shelf_index:, variant_source: variant_source(catalog_index), readiness:, oauth:, ) } /// The resolution layer's read port, built directly over the maintained /// `catalog_index`: its rows already carry the chain edges, and its own /// adoption tally is the same public count Constellation would recompute /// live, just already indexed -- so neither field costs a per-request /// network round trip. fn variant_source(catalog_index: catalog_index.Store) -> catalog_source.Source { catalog_source.Source( releases: catalog_index.releases.list, adoption_count: catalog_index.adoptions.count, ) } fn text_sender(client: Client) { fn(r) { case xrpc.send_text(client, r) { Ok(resp) -> Ok(#(resp.status, resp.body)) Error(e) -> Error(xrpc.transport_error_to_string(e)) } } } fn catalog_deps( client: Client, constellation_host: String, resolver: String, ) -> catalog_deps.Deps { catalog_deps.Deps( backlinks: fn(subject, source) { constellation.get_backlinks( client, constellation_host, subject:, source:, limit: 5, cursor: None, ) }, fetch_release: fetch_by_uri( client, resolver, _, catalog_release.catalog_release_decoder(), ), fetch_edit: fetch_by_uri( client, resolver, _, catalog_edit.catalog_edit_decoder(), ), release_mbid: fn(barcodes, discogs_id) { musicbrainz_client.release_mbid(client, barcodes, discogs_id) }, ) } /// Resolve one identifier (did or handle) to an `Identity` via Slingshot's /// `identity.resolveMiniDoc`, injected into `identity_resolver.Resolver` so /// the resolver/cache layer stays free of any xrpc dependency. Exported so /// test support can build a `Resolver` backed by the same fake client a test /// already wires up, instead of duplicating this glue. pub fn identity_fetch( client: Client, resolver: String, ) -> fn(String) -> Result(Identity, Nil) { fn(identifier: String) { generated_client.identity_resolve_mini_doc( client, resolver, generated_client.IdentityResolveMiniDocParams(identifier:), None, ) |> result.replace_error(Nil) |> result.map(fn(doc) { Identity(did: doc.did, handle: doc.handle, pds: doc.pds) }) } } /// Fetch any repo's record by at-uri (Slingshot proxies getRecord cross-repo, /// so no per-PDS resolution is needed) -> #(cid, value). fn fetch_by_uri( client: Client, resolver: String, at_uri: String, value_decoder: decode.Decoder(a), ) -> option.Option(#(String, a)) { use #(did, collection, rkey) <- option.then(promotion.parse_at_uri(at_uri)) let params = generated_client.RepoGetRecordParams( cid: None, collection:, repo: did, rkey:, ) { use output <- result.try( generated_client.repo_get_record(client, resolver, params, None) |> result.replace_error(Nil), ) use cid <- result.try(output.cid |> option.to_result(Nil)) use value <- result.try( decode.run(output.value, value_decoder) |> result.replace_error(Nil), ) Ok(#(cid, value)) } |> option.from_result }