diff --git a/Makefile b/Makefile index ab6c09f..fd7b9a8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .DEFAULT_GOAL := help SHELL := bash -GLEAM_DIRS := codegen/src codegen/test atproto/src atproto/test shared/src shared/test server/src server/test web/src web/test +GLEAM_DIRS := shared/src shared/test server/src server/test web/src web/test .PHONY: help gen build dev run test format check clean docker-build up down logs @@ -10,7 +10,7 @@ help: ## list available targets | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}' gen: ## generate lexicon codecs into shared/src/at_record/gen/ - cd codegen && gleam run -- ../lexicons ../shared/src/at_record/gen at_record/gen dev.mokkenstorm.crate.,com.atproto. + cd shared && gleam run -m atproto_codegen -- ../lexicons src/at_record/gen at_record/gen dev.mokkenstorm.crate.,com.atproto. cd shared && gleam format src/at_record/gen build: gen ## generate codecs + stage the web frontend into the server @@ -27,9 +27,7 @@ run: build ## build then run the server on http://localhost:8080 dev: ## native watch loop: rebuilds web/server on change, Postgres in docker (browse http://127.0.0.1:8080) ./scripts/dev.sh -test: gen ## run tests for codegen, atproto, shared, server, and web - cd codegen && gleam test - cd atproto && gleam test +test: gen ## run tests for shared, server, and web cd shared && gleam test cd server && gleam test cd web && gleam test @@ -41,7 +39,7 @@ check: ## verify all gleam source is formatted gleam format --check $(GLEAM_DIRS) clean: ## remove build artifacts and generated codecs - rm -rf codegen/build atproto/build shared/build server/build web/build web/dist + rm -rf shared/build server/build web/build web/dist rm -rf shared/src/at_record/gen rm -rf server/priv/static diff --git a/ROADMAP.md b/ROADMAP.md index 3dd99dd..c24fba4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -122,26 +122,20 @@ Remaining: `rating`, `acquiredAt`, `release`, `snapshot.thumbUrl` are not settable. Widen `AddForm` + `do_add` when needed. -### Publishing: extract `atproto` + `codegen` to their own repo, publish to Hex - -Both packages are structured publish-ready (no app references to untangle) and -are useful to the wider Gleam atproto ecosystem on their own: `atproto` is -~80% of a Gleam atproto SDK (XRPC, identity, OAuth discovery, repo CRUD, dual -target), and the lexicon codegen has no Gleam equivalent. - -- **Move both to one new repo** (two packages, one repo keeps codegen tests - against `atproto` types cheap); at-record then consumes them as Hex deps - instead of path deps. `shared` and the app stay here. -- **When: unblocked.** The gating blob work has landed (`atproto/blob` + - `repo.upload_blob` + the BitArray transport + the codegen `blob` case, all - shipped with the covers-as-blobs change), and `shared` already consumes - `atproto` as a dependency, exactly as it would post-extraction. Extract at - the next stable point. -- **Before first publish:** pick Hex names (check availability; `atproto` may - be taken), start at 0.x semver, add package docs + licence metadata, CI - (`gleam test` both targets), and a changelog. Path-dep users break silently - otherwise, so cut the at-record switch to Hex deps in the same PR as the - extraction. +### Publishing: extracted and on Hex (done 2026-07) + +The in-tree `atproto` + `codegen` packages now live in +[gleam-atproto](https://tangled.org/@mokkenstorm.dev/gleam-atproto) and publish +to Hex as [`atproto_client`](https://hex.pm/packages/atproto_client) (the plain +`atproto` name is taken by an Elixir package; modules keep the `atproto/` +namespace) and [`atproto_codegen`](https://hex.pm/packages/atproto_codegen) +(CLI-parameterized: lexicons dir, out dir/module, NSID prefixes). at-record +consumes both from Hex; `make gen` runs the codegen via +`gleam run -m atproto_codegen` from `shared`. Releases are tag-driven through +spindle (`-v`), semver per package, changesets-style +changelogs by hand. The atproto OAuth client machinery (PKCE, PAR, DPoP, +tokens) was ported into `atproto_client`; migrating the server's `oauth/` +modules onto it is the natural follow-up. ### Scan-to-import flow (designed, not built) diff --git a/atproto/LICENSE b/atproto/LICENSE deleted file mode 100644 index 4b94985..0000000 --- a/atproto/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Niels Mokkenstorm - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/atproto/README.md b/atproto/README.md deleted file mode 100644 index 63b2f62..0000000 --- a/atproto/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# atproto - -A small, transport-agnostic [atproto](https://atproto.com/) client for Gleam. No -app or lexicon knowledge: just XRPC, identity, auth, and generic repo CRUD. - -Extracted from [at-record](../README.md) and structured so it can be published -on its own. Not on Hex yet. - -## Transport injection - -The client carries a `send` function instead of a hardcoded HTTP backend, so it -compiles on both erlang and javascript and you choose the backend: - -```gleam -import atproto/xrpc -import gleam/httpc -import gleam/result -import gleam/string - -let client = - xrpc.Client(send: fn(req) { - httpc.send(req) |> result.map_error(string.inspect) - }) -``` - -## Modules - -| Module | What it does | -| ------------------ | --------------------------------------------------------------------------- | -| `atproto/xrpc` | `Client`, `XrpcError`, `get`/`post_json`, `parse`. | -| `atproto/identity` | `resolve_pds` (handle/DID -> PDS) via Slingshot `resolveMiniDoc`. | -| `atproto/auth` | `create_session` (`com.atproto.server.createSession`). | -| `atproto/repo` | `list_records`/`create_record`/`delete_record`, generic over a row decoder. | -| `atproto/uri` | at-uri helpers (`rkey`). | - -## Development - -```sh -gleam test -``` diff --git a/atproto/gleam.toml b/atproto/gleam.toml deleted file mode 100644 index f41c144..0000000 --- a/atproto/gleam.toml +++ /dev/null @@ -1,12 +0,0 @@ -name = "atproto" -version = "0.1.0" -description = "A small, transport-agnostic atproto client for Gleam: XRPC, identity, auth, and repo CRUD. No app or lexicon knowledge." -licences = ["MIT"] - -[dependencies] -gleam_stdlib = ">= 1.0.0 and < 2.0.0" -gleam_http = ">= 4.3.0 and < 5.0.0" -gleam_json = ">= 3.1.0 and < 4.0.0" - -[dev_dependencies] -gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/atproto/manifest.toml b/atproto/manifest.toml deleted file mode 100644 index 78432eb..0000000 --- a/atproto/manifest.toml +++ /dev/null @@ -1,20 +0,0 @@ -# Do not manually edit this file, it is managed by Gleam. -# -# This file locks the dependency versions used, to make your build -# deterministic and to prevent unexpected versions from being included -# in your application. -# -# You should check this file into your source control repository. - -packages = [ - { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, - { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, - { name = "gleam_stdlib", version = "1.0.3", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1F543AFBA5D33DA493E6087F4E4C4F20D899411343512686C98A8ABB2963CF22" }, - { name = "gleeunit", version = "1.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "EC31ABA74256AEA531EDF8169931D775BBB384FED0A8A1BDC4DD9354E3E21826" }, -] - -[requirements] -gleam_http = { version = ">= 4.3.0 and < 5.0.0" } -gleam_json = { version = ">= 3.1.0 and < 4.0.0" } -gleam_stdlib = { version = ">= 1.0.0 and < 2.0.0" } -gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/atproto/src/atproto/auth.gleam b/atproto/src/atproto/auth.gleam deleted file mode 100644 index deab91b..0000000 --- a/atproto/src/atproto/auth.gleam +++ /dev/null @@ -1,62 +0,0 @@ -//// atproto session auth: exchange an identifier + password for session tokens -//// via `com.atproto.server.createSession`, and refresh them via -//// `com.atproto.server.refreshSession`. - -import atproto/xrpc.{type Client, type XrpcError} -import gleam/dynamic/decode -import gleam/json -import gleam/option.{None, Some} -import gleam/result - -pub type SessionTokens { - SessionTokens( - did: String, - handle: String, - access_jwt: String, - refresh_jwt: String, - ) -} - -pub fn create_session( - client: Client, - pds: String, - identifier: String, - password: String, -) -> Result(SessionTokens, XrpcError) { - let body = - json.object([ - #("identifier", json.string(identifier)), - #("password", json.string(password)), - ]) - use resp <- result.try(xrpc.post_json( - client, - pds <> "/xrpc/com.atproto.server.createSession", - None, - body, - )) - xrpc.parse(resp.body, tokens_decoder()) -} - -/// Exchange a refresh JWT for a fresh session (the refresh token is sent as the -/// bearer credential, per `com.atproto.server.refreshSession`). -pub fn refresh_session( - client: Client, - pds: String, - refresh_jwt: String, -) -> Result(SessionTokens, XrpcError) { - use resp <- result.try(xrpc.post_json( - client, - pds <> "/xrpc/com.atproto.server.refreshSession", - Some(refresh_jwt), - json.object([]), - )) - xrpc.parse(resp.body, tokens_decoder()) -} - -fn tokens_decoder() -> decode.Decoder(SessionTokens) { - use did <- decode.field("did", decode.string) - use handle <- decode.field("handle", decode.string) - use access_jwt <- decode.field("accessJwt", decode.string) - use refresh_jwt <- decode.field("refreshJwt", decode.string) - decode.success(SessionTokens(did:, handle:, access_jwt:, refresh_jwt:)) -} diff --git a/atproto/src/atproto/blob.gleam b/atproto/src/atproto/blob.gleam deleted file mode 100644 index 78bc39b..0000000 --- a/atproto/src/atproto/blob.gleam +++ /dev/null @@ -1,34 +0,0 @@ -//// The atproto blob reference, as embedded in records and returned by -//// `com.atproto.repo.uploadBlob`: -//// `{"$type":"blob","ref":{"$link":},"mimeType":...,"size":...}`. - -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/uri - -pub type Blob { - Blob(cid: String, mime_type: String, size: Int) -} - -pub fn encode_blob(value: Blob) -> Json { - json.object([ - #("$type", json.string("blob")), - #("ref", json.object([#("$link", json.string(value.cid))])), - #("mimeType", json.string(value.mime_type)), - #("size", json.int(value.size)), - ]) -} - -pub fn blob_decoder() -> decode.Decoder(Blob) { - use cid <- decode.field("ref", decode.at(["$link"], decode.string)) - use mime_type <- decode.field("mimeType", decode.string) - use size <- decode.field("size", decode.int) - decode.success(Blob(cid:, mime_type:, size:)) -} - -/// The public URL a PDS serves this blob's bytes from. -pub fn public_url(pds: String, did: String, blob: Blob) -> String { - pds - <> "/xrpc/com.atproto.sync.getBlob?" - <> uri.query_to_string([#("did", did), #("cid", blob.cid)]) -} diff --git a/atproto/src/atproto/identity.gleam b/atproto/src/atproto/identity.gleam deleted file mode 100644 index 24a82d5..0000000 --- a/atproto/src/atproto/identity.gleam +++ /dev/null @@ -1,24 +0,0 @@ -//// Resolve a handle or DID to its PDS endpoint via an injected -//// Slingshot-compatible `resolveMiniDoc` resolver. This is not native atproto -//// identity resolution (DID doc / PLC / did:web / DNS); consumers depend on the -//// resolver service being available. - -import atproto/xrpc.{type Client, type XrpcError} -import gleam/dynamic/decode -import gleam/option.{None} -import gleam/result -import gleam/uri - -pub const default_resolver = "https://slingshot.microcosm.blue" - -pub fn resolve_pds( - client: Client, - resolver: String, - identifier: String, -) -> Result(String, XrpcError) { - let query = uri.query_to_string([#("identifier", identifier)]) - let url = - resolver <> "/xrpc/com.bad-example.identity.resolveMiniDoc?" <> query - use resp <- result.try(xrpc.get(client, url, None)) - xrpc.parse(resp.body, decode.at(["pds"], decode.string)) -} diff --git a/atproto/src/atproto/oauth/metadata.gleam b/atproto/src/atproto/oauth/metadata.gleam deleted file mode 100644 index 07bde92..0000000 --- a/atproto/src/atproto/oauth/metadata.gleam +++ /dev/null @@ -1,80 +0,0 @@ -//// atproto OAuth discovery: from a user's PDS, find the authorization server -//// and its endpoints. Two hops of well-known metadata: -//// PDS /.well-known/oauth-protected-resource -> authorization_servers[] -//// issuer /.well-known/oauth-authorization-server -> endpoints - -import atproto/xrpc.{type Client, type XrpcError} -import gleam/dynamic/decode -import gleam/list -import gleam/option.{type Option, None} -import gleam/result - -pub type AuthServerMetadata { - AuthServerMetadata( - issuer: String, - authorization_endpoint: String, - token_endpoint: String, - pushed_authorization_request_endpoint: String, - revocation_endpoint: Option(String), - ) -} - -fn protected_resource_decoder() -> decode.Decoder(List(String)) { - decode.at(["authorization_servers"], decode.list(decode.string)) -} - -fn auth_server_decoder() -> decode.Decoder(AuthServerMetadata) { - use issuer <- decode.field("issuer", decode.string) - use authorization_endpoint <- decode.field( - "authorization_endpoint", - decode.string, - ) - use token_endpoint <- decode.field("token_endpoint", decode.string) - use par <- decode.field( - "pushed_authorization_request_endpoint", - decode.string, - ) - use revocation_endpoint <- decode.optional_field( - "revocation_endpoint", - None, - decode.optional(decode.string), - ) - decode.success(AuthServerMetadata( - issuer:, - authorization_endpoint:, - token_endpoint:, - pushed_authorization_request_endpoint: par, - revocation_endpoint:, - )) -} - -pub fn fetch_protected_resource( - client: Client, - pds: String, -) -> Result(List(String), XrpcError) { - let url = pds <> "/.well-known/oauth-protected-resource" - use resp <- result.try(xrpc.get(client, url, None)) - xrpc.parse(resp.body, protected_resource_decoder()) -} - -pub fn fetch_authorization_server( - client: Client, - issuer: String, -) -> Result(AuthServerMetadata, XrpcError) { - let url = issuer <> "/.well-known/oauth-authorization-server" - use resp <- result.try(xrpc.get(client, url, None)) - xrpc.parse(resp.body, auth_server_decoder()) -} - -/// Resolve a PDS to its authorization server's endpoints in one call. -pub fn discover( - client: Client, - pds: String, -) -> Result(AuthServerMetadata, XrpcError) { - use servers <- result.try(fetch_protected_resource(client, pds)) - use issuer <- result.try(result.replace_error( - list.first(servers), - xrpc.DecodeFailed("no authorization_servers in protected-resource metadata"), - )) - fetch_authorization_server(client, issuer) -} diff --git a/atproto/src/atproto/repo.gleam b/atproto/src/atproto/repo.gleam deleted file mode 100644 index 68d7948..0000000 --- a/atproto/src/atproto/repo.gleam +++ /dev/null @@ -1,208 +0,0 @@ -//// Generic `com.atproto.repo.*` record CRUD. Callers supply the collection NSID -//// and a decoder for each record row, so this module stays free of any lexicon. - -import atproto/blob.{type Blob} -import atproto/xrpc.{type Client, type XrpcError} -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import gleam/uri - -pub type CreatedRecord { - CreatedRecord(uri: String, cid: String) -} - -/// List every record in a collection, following the cursor across pages (the -/// XRPC endpoint caps each page at 100). Returns the full set. -pub fn list_records( - client: Client, - pds: String, - token: String, - did: String, - collection: String, - row_decoder: decode.Decoder(a), -) -> Result(List(a), XrpcError) { - list_page(client, pds, token, did, collection, row_decoder, None, []) -} - -fn list_page( - client: Client, - pds: String, - token: String, - did: String, - collection: String, - row_decoder: decode.Decoder(a), - cursor: Option(String), - acc: List(a), -) -> Result(List(a), XrpcError) { - let base = [#("repo", did), #("collection", collection), #("limit", "100")] - let params = case cursor { - Some(c) -> [#("cursor", c), ..base] - None -> base - } - let url = - pds <> "/xrpc/com.atproto.repo.listRecords?" <> uri.query_to_string(params) - use resp <- result.try(xrpc.get(client, url, Some(token))) - let page = { - use rows <- decode.field("records", decode.list(row_decoder)) - use next <- decode.optional_field( - "cursor", - None, - decode.optional(decode.string), - ) - decode.success(#(rows, next)) - } - use #(rows, next) <- result.try(xrpc.parse(resp.body, page)) - let all = list.append(acc, rows) - case next { - Some("") | None -> Ok(all) - Some(c) -> - list_page(client, pds, token, did, collection, row_decoder, Some(c), all) - } -} - -pub fn create_record( - client: Client, - pds: String, - token: String, - did: String, - collection: String, - record: Json, -) -> Result(CreatedRecord, XrpcError) { - let body = - json.object([ - #("repo", json.string(did)), - #("collection", json.string(collection)), - #("record", record), - ]) - use resp <- result.try(xrpc.post_json( - client, - pds <> "/xrpc/com.atproto.repo.createRecord", - Some(token), - body, - )) - let decoder = { - use uri <- decode.field("uri", decode.string) - use cid <- decode.field("cid", decode.string) - decode.success(CreatedRecord(uri:, cid:)) - } - xrpc.parse(resp.body, decoder) -} - -/// Fetch one record, decoding the full envelope (uri/cid/value) with the -/// supplied decoder, e.g. to build a strongRef to it. -pub fn get_record_envelope( - client: Client, - pds: String, - token: String, - did: String, - collection: String, - rkey: String, - decoder: decode.Decoder(a), -) -> Result(a, XrpcError) { - let query = - uri.query_to_string([ - #("repo", did), - #("collection", collection), - #("rkey", rkey), - ]) - let url = pds <> "/xrpc/com.atproto.repo.getRecord?" <> query - use resp <- result.try(xrpc.get(client, url, Some(token))) - xrpc.parse(resp.body, decoder) -} - -/// Fetch one record and decode its `value` with the supplied decoder. -pub fn get_record( - client: Client, - pds: String, - token: String, - did: String, - collection: String, - rkey: String, - value_decoder: decode.Decoder(a), -) -> Result(a, XrpcError) { - let query = - uri.query_to_string([ - #("repo", did), - #("collection", collection), - #("rkey", rkey), - ]) - let url = pds <> "/xrpc/com.atproto.repo.getRecord?" <> query - use resp <- result.try(xrpc.get(client, url, Some(token))) - xrpc.parse(resp.body, decode.at(["value"], value_decoder)) -} - -/// Write a record at a known rkey (create or replace), keeping a stable URI. -pub fn put_record( - client: Client, - pds: String, - token: String, - did: String, - collection: String, - rkey: String, - record: Json, -) -> Result(CreatedRecord, XrpcError) { - let body = - json.object([ - #("repo", json.string(did)), - #("collection", json.string(collection)), - #("rkey", json.string(rkey)), - #("record", record), - ]) - use resp <- result.try(xrpc.post_json( - client, - pds <> "/xrpc/com.atproto.repo.putRecord", - Some(token), - body, - )) - let decoder = { - use uri <- decode.field("uri", decode.string) - use cid <- decode.field("cid", decode.string) - decode.success(CreatedRecord(uri:, cid:)) - } - xrpc.parse(resp.body, decoder) -} - -/// Upload bytes to the authed repo. The returned ref must be embedded in a -/// record before the PDS garbage-collects it. -pub fn upload_blob( - client: Client, - pds: String, - token: String, - bytes: BitArray, - mime_type: String, -) -> Result(Blob, XrpcError) { - use resp <- result.try(xrpc.post_bits( - client, - pds <> "/xrpc/com.atproto.repo.uploadBlob", - Some(token), - bytes, - mime_type, - )) - xrpc.parse(resp.body, decode.at(["blob"], blob.blob_decoder())) -} - -pub fn delete_record( - client: Client, - pds: String, - token: String, - did: String, - collection: String, - rkey: String, -) -> Result(Nil, XrpcError) { - let body = - json.object([ - #("repo", json.string(did)), - #("collection", json.string(collection)), - #("rkey", json.string(rkey)), - ]) - use _ <- result.try(xrpc.post_json( - client, - pds <> "/xrpc/com.atproto.repo.deleteRecord", - Some(token), - body, - )) - Ok(Nil) -} diff --git a/atproto/src/atproto/uri.gleam b/atproto/src/atproto/uri.gleam deleted file mode 100644 index ae36c40..0000000 --- a/atproto/src/atproto/uri.gleam +++ /dev/null @@ -1,13 +0,0 @@ -//// Helpers for at-uris (`at:////`). - -import gleam/list -import gleam/result -import gleam/string - -/// The record key is the last `/`-separated segment of an at-uri. -pub fn rkey(at_uri: String) -> String { - at_uri - |> string.split("/") - |> list.last - |> result.unwrap("") -} diff --git a/atproto/src/atproto/xrpc.gleam b/atproto/src/atproto/xrpc.gleam deleted file mode 100644 index 8ce045c..0000000 --- a/atproto/src/atproto/xrpc.gleam +++ /dev/null @@ -1,173 +0,0 @@ -//// Transport-agnostic XRPC plumbing. The `Client` wraps a `send` function so -//// the caller chooses the HTTP backend (erlang `httpc`, JS `fetch`, a test -//// stub), keeping this module free of any target-specific dependency. Bodies -//// are BitArrays so blobs ride the same client; the text helpers below keep -//// JSON call sites string-shaped. - -import gleam/bit_array -import gleam/dynamic/decode -import gleam/http -import gleam/http/request.{type Request} -import gleam/http/response.{type Response} -import gleam/json.{type Json} -import gleam/option.{type Option, None, Some} -import gleam/result -import gleam/string - -pub type Client { - Client(send: fn(Request(BitArray)) -> Result(Response(BitArray), String)) -} - -pub type XrpcError { - RequestFailed(String) - /// A non-2xx response. atproto error bodies are JSON `{error, message}`; both - /// are parsed out (when present) so callers can branch on `error` (e.g. - /// `ExpiredToken`) instead of string-matching the raw body. - BadStatus( - status: Int, - error: Option(String), - message: Option(String), - body: String, - ) - DecodeFailed(String) -} - -/// Send a text request and decode the response body as text. The seam between -/// the string-shaped JSON world and the BitArray transport. -pub fn send_text( - client: Client, - req: Request(String), -) -> Result(Response(String), String) { - use resp <- result.try(client.send(request.map(req, bit_array.from_string))) - case bit_array.to_string(resp.body) { - Ok(body) -> Ok(response.set_body(resp, body)) - Error(_) -> Error("response body is not valid utf-8") - } -} - -pub fn get( - client: Client, - url: String, - token: Option(String), -) -> Result(Response(String), XrpcError) { - use base <- result.try( - request.to(url) |> result.replace_error(RequestFailed("bad url: " <> url)), - ) - base - |> with_auth(token) - |> send_text(client, _) - |> result.map_error(RequestFailed) - |> result.try(check_ok) -} - -/// GET returning the raw bytes (e.g. blob or image downloads). The error body -/// on a bad status is decoded leniently for the message. -pub fn get_bits( - client: Client, - url: String, - token: Option(String), -) -> Result(Response(BitArray), XrpcError) { - use base <- result.try( - request.to(url) |> result.replace_error(RequestFailed("bad url: " <> url)), - ) - base - |> with_auth(token) - |> request.map(bit_array.from_string) - |> client.send - |> result.map_error(RequestFailed) - |> result.try(fn(resp) { - case resp.status >= 200 && resp.status < 300 { - True -> Ok(resp) - False -> { - let body = - bit_array.to_string(resp.body) |> result.unwrap("") - let #(error, message) = parse_error(body) - Error(BadStatus(resp.status, error, message, body)) - } - } - }) -} - -pub fn post_json( - client: Client, - url: String, - token: Option(String), - body: Json, -) -> Result(Response(String), XrpcError) { - use base <- result.try( - request.to(url) |> result.replace_error(RequestFailed("bad url: " <> url)), - ) - base - |> request.set_method(http.Post) - |> request.set_header("content-type", "application/json") - |> with_auth(token) - |> request.set_body(json.to_string(body)) - |> send_text(client, _) - |> result.map_error(RequestFailed) - |> result.try(check_ok) -} - -/// POST raw bytes (e.g. `uploadBlob`); the response is decoded as text (JSON). -pub fn post_bits( - client: Client, - url: String, - token: Option(String), - body: BitArray, - content_type: String, -) -> Result(Response(String), XrpcError) { - use base <- result.try( - request.to(url) |> result.replace_error(RequestFailed("bad url: " <> url)), - ) - let sent = - base - |> request.set_method(http.Post) - |> request.set_header("content-type", content_type) - |> with_auth(token) - |> request.map(bit_array.from_string) - |> request.set_body(body) - |> client.send - use resp <- result.try(sent |> result.map_error(RequestFailed)) - case bit_array.to_string(resp.body) { - Ok(text) -> check_ok(response.set_body(resp, text)) - Error(_) -> Error(RequestFailed("response body is not valid utf-8")) - } -} - -pub fn parse(body: String, decoder: decode.Decoder(a)) -> Result(a, XrpcError) { - json.parse(body, decoder) - |> result.map_error(fn(e) { DecodeFailed(string.inspect(e)) }) -} - -fn with_auth(req: Request(String), token: Option(String)) -> Request(String) { - case token { - Some(t) -> request.set_header(req, "authorization", "Bearer " <> t) - None -> req - } -} - -fn check_ok(resp: Response(String)) -> Result(Response(String), XrpcError) { - case resp.status >= 200 && resp.status < 300 { - True -> Ok(resp) - False -> { - let #(error, message) = parse_error(resp.body) - Error(BadStatus(resp.status, error, message, resp.body)) - } - } -} - -fn parse_error(body: String) -> #(Option(String), Option(String)) { - let decoder = { - use error <- decode.optional_field( - "error", - None, - decode.optional(decode.string), - ) - use message <- decode.optional_field( - "message", - None, - decode.optional(decode.string), - ) - decode.success(#(error, message)) - } - json.parse(body, decoder) |> result.unwrap(#(None, None)) -} diff --git a/atproto/test/atproto_test.gleam b/atproto/test/atproto_test.gleam deleted file mode 100644 index cfb013e..0000000 --- a/atproto/test/atproto_test.gleam +++ /dev/null @@ -1,15 +0,0 @@ -import atproto/uri -import gleeunit - -pub fn main() -> Nil { - gleeunit.main() -} - -pub fn rkey_is_last_segment_test() { - let at_uri = "at://did:plc:abc/dev.mokkenstorm.crate.shelf.item/3kxyz" - assert uri.rkey(at_uri) == "3kxyz" -} - -pub fn rkey_of_empty_is_empty_test() { - assert uri.rkey("") == "" -} diff --git a/atproto/test/blob_test.gleam b/atproto/test/blob_test.gleam deleted file mode 100644 index 3becae3..0000000 --- a/atproto/test/blob_test.gleam +++ /dev/null @@ -1,43 +0,0 @@ -import atproto/blob.{Blob} -import atproto/repo -import atproto/xrpc -import gleam/bit_array -import gleam/http -import gleam/http/response.{Response} -import gleam/json - -pub fn blob_round_trips_test() { - let value = Blob(cid: "bafyabc", mime_type: "image/jpeg", size: 1234) - let encoded = blob.encode_blob(value) |> json.to_string - assert encoded - == "{\"$type\":\"blob\",\"ref\":{\"$link\":\"bafyabc\"},\"mimeType\":\"image/jpeg\",\"size\":1234}" - let assert Ok(decoded) = json.parse(encoded, blob.blob_decoder()) - assert decoded == value -} - -pub fn public_url_encodes_query_test() { - let value = Blob(cid: "bafyabc", mime_type: "image/jpeg", size: 1) - assert blob.public_url("https://pds.example", "did:plc:abc", value) - == "https://pds.example/xrpc/com.atproto.sync.getBlob?did=did%3Aplc%3Aabc&cid=bafyabc" -} - -pub fn upload_blob_posts_bytes_and_parses_ref_test() { - let client = - xrpc.Client(send: fn(req) { - assert req.method == http.Post - assert req.path == "/xrpc/com.atproto.repo.uploadBlob" - assert req.body == <<1, 2, 3>> - let body = - "{\"blob\":{\"$type\":\"blob\",\"ref\":{\"$link\":\"bafyxyz\"},\"mimeType\":\"image/png\",\"size\":3}}" - Ok(Response(200, [], bit_array.from_string(body))) - }) - let assert Ok(uploaded) = - repo.upload_blob( - client, - "https://pds.example", - "token", - <<1, 2, 3>>, - "image/png", - ) - assert uploaded == Blob(cid: "bafyxyz", mime_type: "image/png", size: 3) -} diff --git a/atproto/test/client_test.gleam b/atproto/test/client_test.gleam deleted file mode 100644 index a761011..0000000 --- a/atproto/test/client_test.gleam +++ /dev/null @@ -1,57 +0,0 @@ -import atproto/repo -import atproto/xrpc -import gleam/bit_array -import gleam/dynamic/decode -import gleam/http/response.{Response} -import gleam/option.{None, Some} -import gleam/string - -fn uri_decoder() -> decode.Decoder(String) { - use uri <- decode.field("uri", decode.string) - decode.success(uri) -} - -const page_one = "{\"records\":[{\"uri\":\"at://1\"},{\"uri\":\"at://2\"}],\"cursor\":\"c1\"}" - -const page_two = "{\"records\":[{\"uri\":\"at://3\"}]}" - -fn paged_client() -> xrpc.Client { - xrpc.Client(send: fn(req) { - let body = case string.contains(option.unwrap(req.query, ""), "cursor=") { - True -> page_two - False -> page_one - } - Ok(Response(200, [], bit_array.from_string(body))) - }) -} - -pub fn list_records_follows_cursor_across_pages_test() { - let assert Ok(rows) = - repo.list_records( - paged_client(), - "https://pds.example", - "token", - "did:plc:abc", - "app.example.thing", - uri_decoder(), - ) - assert rows == ["at://1", "at://2", "at://3"] -} - -pub fn bad_status_parses_atproto_error_body_test() { - let client = - xrpc.Client(send: fn(_req) { - Ok(Response( - 400, - [], - bit_array.from_string( - "{\"error\":\"InvalidRequest\",\"message\":\"nope\"}", - ), - )) - }) - let assert Error(xrpc.BadStatus(status:, error:, message:, ..)) = - xrpc.get(client, "https://pds.example/xrpc/x", None) - assert status == 400 - assert error == Some("InvalidRequest") - assert message == Some("nope") -} diff --git a/atproto/test/oauth_metadata_test.gleam b/atproto/test/oauth_metadata_test.gleam deleted file mode 100644 index 867266c..0000000 --- a/atproto/test/oauth_metadata_test.gleam +++ /dev/null @@ -1,40 +0,0 @@ -import atproto/oauth/metadata -import atproto/xrpc -import gleam/bit_array -import gleam/http/response.{Response} -import gleam/string - -const protected_resource = "{\"authorization_servers\":[\"https://auth.example\"]}" - -const auth_server = "{\"issuer\":\"https://auth.example\",\"authorization_endpoint\":\"https://auth.example/authorize\",\"token_endpoint\":\"https://auth.example/token\",\"pushed_authorization_request_endpoint\":\"https://auth.example/par\"}" - -fn stub_client() -> xrpc.Client { - xrpc.Client(send: fn(req) { - let body = case string.contains(req.path, "oauth-protected-resource") { - True -> protected_resource - False -> auth_server - } - Ok(Response(status: 200, headers: [], body: bit_array.from_string(body))) - }) -} - -pub fn discover_resolves_par_and_token_endpoints_test() { - let assert Ok(meta) = metadata.discover(stub_client(), "https://pds.example") - assert meta.issuer == "https://auth.example" - assert meta.pushed_authorization_request_endpoint - == "https://auth.example/par" - assert meta.token_endpoint == "https://auth.example/token" - assert meta.authorization_endpoint == "https://auth.example/authorize" -} - -pub fn discover_errors_when_no_authorization_servers_test() { - let client = - xrpc.Client(send: fn(_req) { - Ok(Response( - status: 200, - headers: [], - body: bit_array.from_string("{\"authorization_servers\":[]}"), - )) - }) - let assert Error(_) = metadata.discover(client, "https://pds.example") -} diff --git a/codegen/.gitignore b/codegen/.gitignore deleted file mode 100644 index 599be4e..0000000 --- a/codegen/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.beam -*.ez -/build -erl_crash.dump diff --git a/codegen/README.md b/codegen/README.md deleted file mode 100644 index 6d0bda3..0000000 --- a/codegen/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# codegen - -Internal build tool for [at-record](../README.md). Not published to Hex. - -Reads the lexicon JSON under `lexicons/` and generates Gleam codecs into `shared/src/at_record/gen/` (gitignored). Invoked by `make gen`; not part of the running app. - -See the [root README](../README.md) for build, run, and test instructions. diff --git a/codegen/gleam.toml b/codegen/gleam.toml deleted file mode 100644 index fbf7bb4..0000000 --- a/codegen/gleam.toml +++ /dev/null @@ -1,15 +0,0 @@ -name = "codegen" -version = "1.0.0" -description = "Generates Gleam lexicon codecs from lexicons/ into the shared package." -licences = ["MIT"] - -[dependencies] -gleam_stdlib = ">= 1.0.0 and < 2.0.0" -gleam_json = ">= 3.1.0 and < 4.0.0" -simplifile = ">= 2.4.0 and < 3.0.0" -justin = ">= 1.1.0 and < 2.0.0" -argv = ">= 1.1.0 and < 2.0.0" -gleam_erlang = ">= 1.3.0 and < 2.0.0" - -[dev_dependencies] -gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/codegen/manifest.toml b/codegen/manifest.toml deleted file mode 100644 index 4400fcd..0000000 --- a/codegen/manifest.toml +++ /dev/null @@ -1,27 +0,0 @@ -# Do not manually edit this file, it is managed by Gleam. -# -# This file locks the dependency versions used, to make your build -# deterministic and to prevent unexpected versions from being included -# in your application. -# -# You should check this file into your source control repository. - -packages = [ - { name = "argv", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "3277D100448BDB4A29B6D58C0F36F631CBC349E8BDD09766C6309DF202831140" }, - { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, - { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, - { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, - { name = "gleam_stdlib", version = "1.0.3", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1F543AFBA5D33DA493E6087F4E4C4F20D899411343512686C98A8ABB2963CF22" }, - { name = "gleeunit", version = "1.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "EC31ABA74256AEA531EDF8169931D775BBB384FED0A8A1BDC4DD9354E3E21826" }, - { name = "justin", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "8B1C62269E8607D0A0ED698B7903984CE0BF7B6B3C0DEA3C6C8302B39402837B" }, - { name = "simplifile", version = "2.4.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "7C18AFA4FED0B4CE1FA5B0B4BAC1FA1744427054EA993565F6F3F82E5453170D" }, -] - -[requirements] -argv = { version = ">= 1.1.0 and < 2.0.0" } -gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } -gleam_json = { version = ">= 3.1.0 and < 4.0.0" } -gleam_stdlib = { version = ">= 1.0.0 and < 2.0.0" } -gleeunit = { version = ">= 1.0.0 and < 2.0.0" } -justin = { version = ">= 1.1.0 and < 2.0.0" } -simplifile = { version = ">= 2.4.0 and < 3.0.0" } diff --git a/codegen/priv/internal.gleam b/codegen/priv/internal.gleam deleted file mode 100644 index 35bccc0..0000000 --- a/codegen/priv/internal.gleam +++ /dev/null @@ -1,63 +0,0 @@ -import gleam/bit_array -import gleam/dict -import gleam/dynamic.{type Dynamic} -import gleam/dynamic/decode -import gleam/json -import gleam/option.{type Option, None, Some} - -/// One JSON field, or nothing when the value is absent. Keeps optional -/// properties out of the encoded object entirely rather than emitting null. -pub fn opt( - name: String, - value: Option(a), - to_json: fn(a) -> json.Json, -) -> List(#(String, json.Json)) { - case value { - Some(v) -> [#(name, to_json(v))] - None -> [] - } -} - -/// A `cid-link` is rendered in JSON as `{ "$link": }`. -pub fn encode_cid_link(cid: String) -> json.Json { - json.object([#("$link", json.string(cid))]) -} - -pub fn cid_link_decoder() -> decode.Decoder(String) { - decode.at(["$link"], decode.string) -} - -/// `bytes` are rendered in JSON as `{ "$bytes": }`. -pub fn encode_bytes(bytes: BitArray) -> json.Json { - json.object([#("$bytes", json.string(bit_array.base64_encode(bytes, False)))]) -} - -pub fn bytes_decoder() -> decode.Decoder(BitArray) { - use encoded <- decode.then(decode.at(["$bytes"], decode.string)) - case bit_array.base64_decode(encoded) { - Ok(bytes) -> decode.success(bytes) - Error(_) -> decode.failure(<<>>, "bytes") - } -} - -/// Re-encode an already-decoded `unknown` value back to JSON so records that -/// carry one round-trip. Reconstructs the JSON value from the decoded `Dynamic`. -pub fn dynamic_to_json(value: Dynamic) -> json.Json { - case decode.run(value, json_value_decoder()) { - Ok(rendered) -> rendered - Error(_) -> json.null() - } -} - -fn json_value_decoder() -> decode.Decoder(json.Json) { - use <- decode.recursive - decode.one_of(decode.bool |> decode.map(json.bool), [ - decode.int |> decode.map(json.int), - decode.float |> decode.map(json.float), - decode.string |> decode.map(json.string), - decode.list(json_value_decoder()) |> decode.map(json.preprocessed_array), - decode.dict(decode.string, json_value_decoder()) - |> decode.map(fn(entries) { json.object(dict.to_list(entries)) }), - decode.success(json.null()), - ]) -} diff --git a/codegen/src/codegen.gleam b/codegen/src/codegen.gleam deleted file mode 100644 index da7da06..0000000 --- a/codegen/src/codegen.gleam +++ /dev/null @@ -1,163 +0,0 @@ -//// Lexicon -> Gleam codegen. Reads `/**/*.json` and writes typed -//// modules into ``, importable as `/...`. See -//// `codegen/lexicon` (parsing), `codegen/naming` (names/refs), `codegen/emit` -//// (source emission). - -import argv -import codegen/config.{type Config, Config} -import codegen/emit -import codegen/lexicon.{ - type Def, type FieldType, type Lexicon, Object, Record, TArray, TRef, - Unsupported, -} -import codegen/naming -import gleam/erlang/application -import gleam/io -import gleam/json -import gleam/list -import gleam/option.{None, Some} -import gleam/result -import gleam/string -import simplifile - -const generated_banner = "//// Generated by codegen from priv/internal.gleam. Do not edit by hand.\n\n" - -const usage = "usage: gleam run -- [,...]" - -pub fn main() -> Nil { - case argv.load().arguments { - [lexicons_dir, out_dir, out_module, prefixes] -> - run(Config( - lexicons_dir:, - out_dir:, - out_module:, - nsid_prefixes: string.split(prefixes, ","), - )) - _ -> io.println(usage) - } -} - -fn run(cfg: Config) -> Nil { - let assert Ok(files) = simplifile.get_files(cfg.lexicons_dir) - let lexicons = - files - |> list.filter(string.ends_with(_, ".json")) - |> list.filter_map(fn(path) { - case load_lexicon(path) { - Ok(lex) -> Ok(lex) - Error(reason) -> { - io.println(" WARN skipping " <> path <> ": " <> reason) - Error(Nil) - } - } - }) - - report_unresolved_refs(lexicons) - - let assert Ok(_) = simplifile.create_directory_all(cfg.out_dir) - let assert Ok(internal) = simplifile.read(internal_template()) - let assert Ok(_) = - simplifile.write( - to: cfg.out_dir <> "/internal.gleam", - contents: generated_banner <> internal, - ) - - list.each(lexicons, fn(lex) { - list.each(lex.defs, fn(d) { - case d { - Unsupported(nsid, name, reason) -> - io.println(" skip " <> nsid <> "#" <> name <> " (" <> reason <> ")") - _ -> Nil - } - }) - case emit.emit_lexicon(cfg, lex) { - Some(source) -> { - let path = - cfg.out_dir - <> "/" - <> naming.module_subpath(cfg.nsid_prefixes, lex.nsid) - <> ".gleam" - let assert Ok(_) = simplifile.create_directory_all(parent_dir(path)) - let assert Ok(_) = simplifile.write(to: path, contents: source) - io.println(" gen " <> path) - } - None -> Nil - } - }) - io.println("codegen: done") -} - -// Resolved via the app priv dir so the tool also works installed as a dep. -fn internal_template() -> String { - let priv = - application.priv_directory("codegen") - |> result.unwrap("priv") - priv <> "/internal.gleam" -} - -fn load_lexicon(path: String) -> Result(Lexicon, String) { - use contents <- result.try( - simplifile.read(path) - |> result.map_error(fn(e) { "read failed: " <> string.inspect(e) }), - ) - json.parse(contents, lexicon.lexicon_decoder()) - |> result.map_error(fn(e) { "parse failed: " <> string.inspect(e) }) -} - -/// Warn about any `ref` that points at a def we never generate (a different -/// lexicon's def, an external NSID, or an unsupported def). Such a ref would -/// otherwise emit Gleam that doesn't compile, with no signal from codegen. -fn report_unresolved_refs(lexicons: List(Lexicon)) -> Nil { - let known = - list.flat_map(lexicons, fn(lex) { - list.filter_map(lex.defs, fn(d) { - case d { - Record(nsid, _) -> Ok(nsid <> "#main") - Object(nsid, name, _) -> Ok(nsid <> "#" <> name) - _ -> Error(Nil) - } - }) - }) - let refs = - list.flat_map(lexicons, fn(lex) { - list.flat_map(lex.defs, fn(d) { - list.flat_map(def_properties(d), fn(p) { - field_ref_keys(lex.nsid, p.field_type) - }) - }) - }) - refs - |> list.unique - |> list.filter(fn(key) { !list.contains(known, key) }) - |> list.each(fn(key) { - io.println( - " WARN unresolved ref " <> key <> " (referencing def won't compile)", - ) - }) -} - -fn def_properties(def: Def) -> List(lexicon.Property) { - case def { - Record(_, props) -> props - Object(_, _, props) -> props - _ -> [] - } -} - -fn field_ref_keys(current: String, ft: FieldType) -> List(String) { - case ft { - TRef(ref) -> { - let r = naming.parse_ref(current, ref) - [r.nsid <> "#" <> r.def_name] - } - TArray(inner) -> field_ref_keys(current, inner) - _ -> [] - } -} - -fn parent_dir(path: String) -> String { - case string.split(path, "/") |> list.reverse { - [_, ..rest] -> rest |> list.reverse |> string.join("/") - [] -> "." - } -} diff --git a/codegen/src/codegen/config.gleam b/codegen/src/codegen/config.gleam deleted file mode 100644 index b45f9fa..0000000 --- a/codegen/src/codegen/config.gleam +++ /dev/null @@ -1,15 +0,0 @@ -//// Generation settings threaded through naming + emission, so the tool is -//// not coupled to any one project's paths or namespace. - -pub type Config { - Config( - /// Where the lexicon JSON files live. - lexicons_dir: String, - /// Directory the generated modules are written into. - out_dir: String, - /// Module prefix the generated code imports itself under (matches out_dir). - out_module: String, - /// NSID prefixes stripped from module paths (own namespace + vendored). - nsid_prefixes: List(String), - ) -} diff --git a/codegen/src/codegen/emit.gleam b/codegen/src/codegen/emit.gleam deleted file mode 100644 index b451448..0000000 --- a/codegen/src/codegen/emit.gleam +++ /dev/null @@ -1,345 +0,0 @@ -//// Emit Gleam source (types + `gleam/json` encoders + `gleam/dynamic/decode` -//// decoders) from lexicon defs, plus the fixed `internal` helper module. - -import codegen/config.{type Config} -import codegen/lexicon.{ - type Def, type FieldType, type Lexicon, type Property, Object, Record, TArray, - TBlob, TBool, TBytes, TCidLink, TInt, TNumber, TRef, TString, TUnknown, - TUnsupported, -} -import codegen/naming.{ - def_type_name, field_name, module_alias, module_subpath, parse_ref, qualifier, - ref_type_name, -} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/string -import justin - -const banner = "//// Generated by codegen from lexicons/. Do not edit by hand.\n\n" - -fn escape(value: String) -> String { - value - |> string.replace("\\", "\\\\") - |> string.replace("\"", "\\\"") -} - -fn type_uses_helper(ft: FieldType) -> Bool { - case ft { - TCidLink | TBytes | TUnknown -> True - TArray(inner) -> type_uses_helper(inner) - _ -> False - } -} - -fn type_uses_blob(ft: FieldType) -> Bool { - case ft { - TBlob -> True - TArray(inner) -> type_uses_blob(inner) - _ -> False - } -} - -fn type_uses_unknown(ft: FieldType) -> Bool { - case ft { - TUnknown -> True - TArray(inner) -> type_uses_unknown(inner) - _ -> False - } -} - -fn gleam_type(cfg: Config, current: String, ft: FieldType) -> String { - case ft { - TString -> "String" - TInt -> "Int" - TBool -> "Bool" - TNumber -> "Float" - TRef(ref) -> { - let r = parse_ref(current, ref) - qualifier(cfg.nsid_prefixes, current, r.nsid) - <> ref_type_name(cfg.nsid_prefixes, r) - } - TArray(inner) -> "List(" <> gleam_type(cfg, current, inner) <> ")" - TCidLink -> "String" - TBytes -> "BitArray" - TBlob -> "blob.Blob" - TUnknown -> "dynamic.Dynamic" - TUnsupported(kind) -> "Dynamic_" <> kind - } -} - -fn encoder_expr(cfg: Config, current: String, ft: FieldType) -> String { - case ft { - TString -> "json.string" - TInt -> "json.int" - TBool -> "json.bool" - TNumber -> "json.float" - TRef(ref) -> { - let r = parse_ref(current, ref) - qualifier(cfg.nsid_prefixes, current, r.nsid) - <> "encode_" - <> justin.snake_case(ref_type_name(cfg.nsid_prefixes, r)) - } - TArray(inner) -> - "fn(items) { json.array(items, " - <> encoder_expr(cfg, current, inner) - <> ") }" - TCidLink -> "internal.encode_cid_link" - TBytes -> "internal.encode_bytes" - TBlob -> "blob.encode_blob" - TUnknown -> "internal.dynamic_to_json" - TUnsupported(_) -> "json.string" - } -} - -fn decoder_expr(cfg: Config, current: String, ft: FieldType) -> String { - case ft { - TString -> "decode.string" - TInt -> "decode.int" - TBool -> "decode.bool" - TNumber -> "decode.float" - TRef(ref) -> { - let r = parse_ref(current, ref) - qualifier(cfg.nsid_prefixes, current, r.nsid) - <> justin.snake_case(ref_type_name(cfg.nsid_prefixes, r)) - <> "_decoder()" - } - TArray(inner) -> "decode.list(" <> decoder_expr(cfg, current, inner) <> ")" - TCidLink -> "internal.cid_link_decoder()" - TBytes -> "internal.bytes_decoder()" - TBlob -> "blob.blob_decoder()" - TUnknown -> "decode.dynamic" - TUnsupported(_) -> "decode.string" - } -} - -pub fn emit_lexicon(cfg: Config, lex: Lexicon) -> Option(String) { - let emittable = - list.filter(lex.defs, fn(d) { - case d { - Record(..) | Object(..) -> True - _ -> False - } - }) - case emittable { - [] -> None - defs -> { - let bodies = list.map(defs, emit_def(cfg, lex.nsid, _)) - let header = emit_header(cfg, lex.nsid, defs) - Some(banner <> header <> "\n" <> string.join(bodies, "\n\n") <> "\n") - } - } -} - -fn emit_header(cfg: Config, nsid: String, defs: List(Def)) -> String { - let props = list.flat_map(defs, def_properties) - let needs_option = list.any(props, fn(p) { !p.required }) - let uses_helper = list.any(props, fn(p) { type_uses_helper(p.field_type) }) - let uses_unknown = list.any(props, fn(p) { type_uses_unknown(p.field_type) }) - let uses_blob = list.any(props, fn(p) { type_uses_blob(p.field_type) }) - let externals = - props - |> list.flat_map(fn(p) { ref_nsids(p.field_type) }) - |> list.filter(fn(n) { n != nsid }) - |> list.unique - |> list.sort(string.compare) - - let base = [ - "import gleam/dynamic/decode", "import gleam/json", "import gleam/list", - ] - let blob = case uses_blob { - True -> ["import atproto/blob"] - False -> [] - } - let dyn = case uses_unknown { - True -> ["import gleam/dynamic"] - False -> [] - } - let opt = case needs_option { - True -> ["import gleam/option.{type Option}"] - False -> [] - } - let internal = case needs_option || uses_helper { - True -> ["import " <> cfg.out_module <> "/internal"] - False -> [] - } - let ext = - list.map(externals, fn(n) { - "import " - <> cfg.out_module - <> "/" - <> module_subpath(cfg.nsid_prefixes, n) - <> " as " - <> module_alias(cfg.nsid_prefixes, n) - }) - string.join(list.flatten([blob, base, dyn, opt, internal, ext]), "\n") <> "\n" -} - -fn def_properties(def: Def) -> List(Property) { - case def { - Record(_, props) -> props - Object(_, _, props) -> props - _ -> [] - } -} - -fn ref_nsids(ft: FieldType) -> List(String) { - case ft { - TRef(ref) -> { - let r = parse_ref("", ref) - case r.nsid { - "" -> [] - nsid -> [nsid] - } - } - TArray(inner) -> ref_nsids(inner) - _ -> [] - } -} - -fn emit_def(cfg: Config, nsid: String, def: Def) -> String { - let name = def_type_name(cfg.nsid_prefixes, def) - let props = def_properties(def) - let is_record = case def { - Record(..) -> True - _ -> False - } - string.join( - list.flatten([ - case is_record { - True -> ["pub const collection = \"" <> escape(nsid) <> "\""] - False -> [] - }, - [emit_type(cfg, nsid, name, props)], - [emit_encoder(cfg, nsid, name, props, is_record)], - [emit_decoder(cfg, nsid, name, props)], - ]), - "\n\n", - ) -} - -fn emit_type( - cfg: Config, - nsid: String, - name: String, - props: List(Property), -) -> String { - case props { - [] -> "pub type " <> name <> " {\n " <> name <> "\n}" - _ -> { - let fields = - list.map(props, fn(p) { - let t = gleam_type(cfg, nsid, p.field_type) - let t = case p.required { - True -> t - False -> "Option(" <> t <> ")" - } - " " <> field_name(p.json_name) <> ": " <> t <> "," - }) - "pub type " - <> name - <> " {\n " - <> name - <> "(\n" - <> string.join(fields, "\n") - <> "\n )\n}" - } - } -} - -fn emit_encoder( - cfg: Config, - nsid: String, - name: String, - props: List(Property), - is_record: Bool, -) -> String { - let #(required, optional) = list.partition(props, fn(p) { p.required }) - let required_entries = - list.map(required, fn(p) { - "#(\"" - <> escape(p.json_name) - <> "\", " - <> encoder_expr(cfg, nsid, p.field_type) - <> "(value." - <> field_name(p.json_name) - <> "))" - }) - let type_entry = case is_record { - True -> ["#(\"$type\", json.string(\"" <> escape(nsid) <> "\"))"] - False -> [] - } - let inline = - "[" - <> string.join(list.flatten([type_entry, required_entries]), ", ") - <> "]" - let optional_groups = - list.map(optional, fn(p) { - "internal.opt(\"" - <> escape(p.json_name) - <> "\", value." - <> field_name(p.json_name) - <> ", " - <> encoder_expr(cfg, nsid, p.field_type) - <> ")" - }) - let groups = string.join([inline, ..optional_groups], ",\n ") - "pub fn encode_" - <> justin.snake_case(name) - <> "(value: " - <> name - <> ") -> json.Json {\n json.object(list.flatten([\n " - <> groups - <> ",\n ]))\n}" -} - -fn emit_decoder( - cfg: Config, - nsid: String, - name: String, - props: List(Property), -) -> String { - let uses = - list.map(props, fn(p) { - let var = field_name(p.json_name) - case p.required { - True -> - " use " - <> var - <> " <- decode.field(\"" - <> escape(p.json_name) - <> "\", " - <> decoder_expr(cfg, nsid, p.field_type) - <> ")" - False -> - " use " - <> var - <> " <- decode.optional_field(\"" - <> escape(p.json_name) - <> "\", option.None, decode.optional(" - <> decoder_expr(cfg, nsid, p.field_type) - <> "))" - } - }) - let constructor = case props { - [] -> " decode.success(" <> name <> ")" - _ -> - " decode.success(" - <> name - <> "(" - <> string.join( - list.map(props, fn(p) { field_name(p.json_name) <> ":" }), - ", ", - ) - <> "))" - } - "pub fn " - <> justin.snake_case(name) - <> "_decoder() -> decode.Decoder(" - <> name - <> ") {\n" - <> string.join(uses, "\n") - <> "\n" - <> constructor - <> "\n}" -} diff --git a/codegen/src/codegen/lexicon.gleam b/codegen/src/codegen/lexicon.gleam deleted file mode 100644 index 2dec3e3..0000000 --- a/codegen/src/codegen/lexicon.gleam +++ /dev/null @@ -1,159 +0,0 @@ -//// Lexicon model + JSON decoding: parse lexicon files into typed defs. -//// -//// Supports record/object defs with string/integer/number/boolean/array/ref/ -//// cid-link/bytes/blob/unknown fields. A def using anything else (e.g. -//// `union`, `token`) becomes `Unsupported` and is reported, not emitted wrong. - -import gleam/dict.{type Dict} -import gleam/dynamic/decode -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/string - -pub type FieldType { - TString - TInt - TBool - TNumber - TRef(String) - TArray(FieldType) - TCidLink - TBytes - TBlob - TUnknown - TUnsupported(String) -} - -pub type Property { - Property(json_name: String, field_type: FieldType, required: Bool) -} - -pub type Def { - Record(nsid: String, properties: List(Property)) - Object(nsid: String, name: String, properties: List(Property)) - Unsupported(nsid: String, name: String, reason: String) - Ignored -} - -pub type Lexicon { - Lexicon(nsid: String, defs: List(Def)) -} - -fn field_type_decoder() -> decode.Decoder(FieldType) { - use kind <- decode.field("type", decode.string) - case kind { - "string" -> decode.success(TString) - "integer" -> decode.success(TInt) - "boolean" -> decode.success(TBool) - "number" -> decode.success(TNumber) - "ref" -> { - use ref <- decode.field("ref", decode.string) - decode.success(TRef(ref)) - } - "array" -> { - use items <- decode.field("items", field_type_decoder()) - decode.success(TArray(items)) - } - "cid-link" -> decode.success(TCidLink) - "bytes" -> decode.success(TBytes) - "blob" -> decode.success(TBlob) - "unknown" -> decode.success(TUnknown) - other -> decode.success(TUnsupported(other)) - } -} - -type Schema { - Schema(properties: Dict(String, FieldType), required: List(String)) -} - -fn schema_decoder() -> decode.Decoder(Schema) { - use properties <- decode.optional_field( - "properties", - dict.new(), - decode.dict(decode.string, field_type_decoder()), - ) - use required <- decode.optional_field( - "required", - [], - decode.list(decode.string), - ) - decode.success(Schema(properties:, required:)) -} - -type RawDef { - RawDef(kind: String, schema: Schema) -} - -fn raw_def_decoder() -> decode.Decoder(RawDef) { - use kind <- decode.field("type", decode.string) - case kind { - "object" -> { - use schema <- decode.then(schema_decoder()) - decode.success(RawDef("object", schema)) - } - "record" -> { - use schema <- decode.field("record", schema_decoder()) - decode.success(RawDef("record", schema)) - } - other -> decode.success(RawDef(other, Schema(dict.new(), []))) - } -} - -pub fn lexicon_decoder() -> decode.Decoder(Lexicon) { - use nsid <- decode.field("id", decode.string) - use raw <- decode.field("defs", decode.dict(decode.string, raw_def_decoder())) - let defs = - raw - |> dict.to_list - |> list.sort(fn(a, b) { string.compare(a.0, b.0) }) - |> list.map(fn(pair) { build_def(nsid, pair.0, pair.1) }) - decode.success(Lexicon(nsid:, defs:)) -} - -fn build_def(nsid: String, name: String, raw: RawDef) -> Def { - let props = - raw.schema.properties - |> dict.to_list - |> list.sort(fn(a, b) { string.compare(a.0, b.0) }) - |> list.map(fn(pair) { - Property( - json_name: pair.0, - field_type: pair.1, - required: list.contains(raw.schema.required, pair.0), - ) - }) - case unsupported_reason(props) { - Some(reason) -> - case raw.kind { - "object" | "record" -> Unsupported(nsid, name, reason) - _ -> Ignored - } - None -> - case raw.kind { - "object" -> Object(nsid, name, props) - "record" -> Record(nsid, props) - _ -> Ignored - } - } -} - -fn unsupported_reason(props: List(Property)) -> Option(String) { - props - |> list.filter_map(fn(p) { - case unsupported_type(p.field_type) { - Some(kind) -> - Ok("field `" <> p.json_name <> "` has type `" <> kind <> "`") - None -> Error(Nil) - } - }) - |> list.first - |> option.from_result -} - -fn unsupported_type(ft: FieldType) -> Option(String) { - case ft { - TUnsupported(kind) -> Some(kind) - TArray(inner) -> unsupported_type(inner) - _ -> None - } -} diff --git a/codegen/src/codegen/naming.gleam b/codegen/src/codegen/naming.gleam deleted file mode 100644 index 0e996bb..0000000 --- a/codegen/src/codegen/naming.gleam +++ /dev/null @@ -1,83 +0,0 @@ -//// Naming + at-uri ref resolution: NSIDs <-> Gleam module/type/field names. - -import codegen/lexicon.{type Def, Object, Record} -import gleam/list -import gleam/string -import justin - -fn strip_prefix(prefixes: List(String), nsid: String) -> String { - case list.find(prefixes, string.starts_with(nsid, _)) { - Ok(prefix) -> string.drop_start(nsid, string.length(prefix)) - Error(Nil) -> nsid - } -} - -// Segments are snake_cased so camelCase NSIDs (strongRef) emit valid modules. -fn segments(prefixes: List(String), nsid: String) -> List(String) { - strip_prefix(prefixes, nsid) - |> string.split(".") - |> list.map(justin.snake_case) -} - -pub fn module_subpath(prefixes: List(String), nsid: String) -> String { - segments(prefixes, nsid) |> string.join("/") -} - -pub fn module_alias(prefixes: List(String), nsid: String) -> String { - segments(prefixes, nsid) |> string.join("_") -} - -pub type Ref { - Ref(nsid: String, def_name: String) -} - -pub fn parse_ref(current: String, ref: String) -> Ref { - case string.split(ref, "#") { - ["", def_name] -> Ref(current, def_name) - [nsid, def_name] -> Ref(nsid, def_name) - [nsid] -> Ref(nsid, "main") - _ -> Ref(current, ref) - } -} - -pub fn ref_type_name(prefixes: List(String), ref: Ref) -> String { - case ref.def_name { - "main" -> justin.pascal_case(module_alias(prefixes, ref.nsid)) - name -> justin.pascal_case(name) - } -} - -// A `main` object def takes the module's name, matching how refs to it are -// emitted (`ref_type_name` resolves bare NSIDs to #main). -pub fn def_type_name(prefixes: List(String), def: Def) -> String { - case def { - Record(nsid, _) -> justin.pascal_case(module_alias(prefixes, nsid)) - Object(nsid, "main", _) -> justin.pascal_case(module_alias(prefixes, nsid)) - Object(_, name, _) -> justin.pascal_case(name) - _ -> "" - } -} - -const keywords = [ - "as", "assert", "case", "const", "echo", "fn", "if", "import", "let", "opaque", - "panic", "pub", "todo", "type", "use", -] - -pub fn field_name(json_name: String) -> String { - let snake = justin.snake_case(json_name) - case list.contains(keywords, snake) { - True -> snake <> "_" - False -> snake - } -} - -pub fn qualifier( - prefixes: List(String), - current: String, - nsid: String, -) -> String { - case nsid == current { - True -> "" - False -> module_alias(prefixes, nsid) <> "." - } -} diff --git a/codegen/test/codegen_test.gleam b/codegen/test/codegen_test.gleam deleted file mode 100644 index 2fc7dcf..0000000 --- a/codegen/test/codegen_test.gleam +++ /dev/null @@ -1,80 +0,0 @@ -import codegen/config.{type Config, Config} -import codegen/emit -import codegen/lexicon.{Lexicon, Object, Property, Record, TBlob, TInt, TString} -import codegen/naming -import gleam/json -import gleam/option.{Some} -import gleam/string -import gleeunit - -pub fn main() -> Nil { - gleeunit.main() -} - -pub fn parse_ref_local_test() { - let r = naming.parse_ref("a.b.c", "#thing") - assert r.nsid == "a.b.c" - assert r.def_name == "thing" -} - -pub fn parse_ref_external_test() { - let r = naming.parse_ref("a.b.c", "x.y.z#thing") - assert r.nsid == "x.y.z" - assert r.def_name == "thing" -} - -pub fn parse_ref_main_test() { - let r = naming.parse_ref("a.b.c", "x.y.z") - assert r.nsid == "x.y.z" - assert r.def_name == "main" -} - -pub fn field_name_escapes_keywords_test() { - assert naming.field_name("type") == "type_" -} - -pub fn field_name_snake_cases_test() { - assert naming.field_name("artistDisplay") == "artist_display" -} - -const sample = "{\"lexicon\":1,\"id\":\"a.b.c\",\"defs\":{\"main\":{\"type\":\"record\",\"key\":\"tid\",\"record\":{\"type\":\"object\",\"required\":[\"title\"],\"properties\":{\"title\":{\"type\":\"string\"},\"count\":{\"type\":\"integer\"}}}}}}" - -pub fn lexicon_decodes_record_with_sorted_props_test() { - let assert Ok(lex) = json.parse(sample, lexicon.lexicon_decoder()) - assert lex.nsid == "a.b.c" - // Properties come out sorted by name (count before title), regardless of the - // JSON object order. - assert lex.defs - == [ - Record("a.b.c", [ - Property("count", TInt, False), - Property("title", TString, True), - ]), - ] -} - -const blob_sample = "{\"lexicon\":1,\"id\":\"a.b.c\",\"defs\":{\"pic\":{\"type\":\"object\",\"properties\":{\"cover\":{\"type\":\"blob\",\"accept\":[\"image/*\"]}}}}}" - -pub fn lexicon_decodes_blob_field_test() { - let assert Ok(lex) = json.parse(blob_sample, lexicon.lexicon_decoder()) - assert lex.defs == [Object("a.b.c", "pic", [Property("cover", TBlob, False)])] -} - -fn test_cfg() -> Config { - Config( - lexicons_dir: "unused", - out_dir: "unused", - out_module: "my_app/gen", - nsid_prefixes: ["a.b.", "com.atproto."], - ) -} - -pub fn emit_blob_field_uses_atproto_blob_test() { - let lex = - Lexicon("a.b.c", [Object("a.b.c", "pic", [Property("cover", TBlob, True)])]) - let assert Some(source) = emit.emit_lexicon(test_cfg(), lex) - assert string.contains(source, "import atproto/blob") - assert string.contains(source, "cover: blob.Blob") - assert string.contains(source, "blob.encode_blob(value.cover)") - assert string.contains(source, "decode.field(\"cover\", blob.blob_decoder())") -} diff --git a/server/gleam.toml b/server/gleam.toml index c69b407..5a0d773 100644 --- a/server/gleam.toml +++ b/server/gleam.toml @@ -11,7 +11,7 @@ gleam_httpc = ">= 5.0.0 and < 6.0.0" gleam_json = ">= 3.1.0 and < 4.0.0" gleam_erlang = ">= 1.3.0 and < 2.0.0" at_record_shared = { path = "../shared" } -atproto = { path = "../atproto" } +atproto_client = ">= 0.1.0 and < 1.0.0" gleam_time = ">= 1.8.0 and < 2.0.0" gleam_http = ">= 4.3.0 and < 5.0.0" envoy = ">= 1.0.0 and < 2.0.0" diff --git a/server/manifest.toml b/server/manifest.toml index e4b77e5..63816ec 100644 --- a/server/manifest.toml +++ b/server/manifest.toml @@ -7,8 +7,8 @@ # You should check this file into your source control repository. packages = [ - { name = "at_record_shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], source = "local", path = "../shared" }, - { name = "atproto", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_json", "gleam_stdlib"], source = "local", path = "../atproto" }, + { name = "at_record_shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["atproto_client", "gleam_json", "gleam_stdlib"], source = "local", path = "../shared" }, + { name = "atproto_client", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_http", "gleam_json", "gleam_stdlib", "gleam_time", "gose", "kryptos"], otp_app = "atproto_client", source = "hex", outer_checksum = "728717DD1D24A45B1966665B9F62B6CC08CAE188CBEB3B4905D0CAFE0ED42989" }, { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, { name = "bigi", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "bigi", source = "hex", outer_checksum = "44FEDF1DF5C1F426A80B67C32B976D0828D66524E11BDC2DD22B78D37C883BA7" }, { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, @@ -44,7 +44,7 @@ packages = [ [requirements] at_record_shared = { path = "../shared" } -atproto = { path = "../atproto" } +atproto_client = { version = ">= 0.1.0 and < 1.0.0" } envoy = { version = ">= 1.0.0 and < 2.0.0" } gleam_crypto = { version = ">= 1.6.0 and < 2.0.0" } gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } diff --git a/shared/gleam.toml b/shared/gleam.toml index 7310488..adbf4f1 100644 --- a/shared/gleam.toml +++ b/shared/gleam.toml @@ -6,7 +6,8 @@ licences = ["MIT"] [dependencies] gleam_stdlib = ">= 1.0.0 and < 2.0.0" gleam_json = ">= 3.1.0 and < 4.0.0" -atproto = { path = "../atproto" } +atproto_client = ">= 0.1.0 and < 1.0.0" [dev_dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" +atproto_codegen = ">= 0.1.0 and < 1.0.0" diff --git a/shared/manifest.toml b/shared/manifest.toml index 91ebc91..614f9e4 100644 --- a/shared/manifest.toml +++ b/shared/manifest.toml @@ -7,15 +7,27 @@ # You should check this file into your source control repository. packages = [ - { name = "atproto", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_json", "gleam_stdlib"], source = "local", path = "../atproto" }, + { name = "argv", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "3277D100448BDB4A29B6D58C0F36F631CBC349E8BDD09766C6309DF202831140" }, + { name = "atproto_client", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_http", "gleam_json", "gleam_stdlib", "gleam_time", "gose", "kryptos"], otp_app = "atproto_client", source = "hex", outer_checksum = "728717DD1D24A45B1966665B9F62B6CC08CAE188CBEB3B4905D0CAFE0ED42989" }, + { name = "atproto_codegen", version = "0.1.0", build_tools = ["gleam"], requirements = ["argv", "gleam_erlang", "gleam_json", "gleam_stdlib", "justin", "simplifile"], otp_app = "atproto_codegen", source = "hex", outer_checksum = "13892EF820D8957EDB5A3ADD9B132558E12CDBE17CB0968934D2CE98ACA6A9BA" }, + { name = "bigi", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "bigi", source = "hex", outer_checksum = "44FEDF1DF5C1F426A80B67C32B976D0828D66524E11BDC2DD22B78D37C883BA7" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "gleam_crypto", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "2DE9E4EF53CF6FEE049D4F765731F7178F7A11AEFAE00EEE63BF7536B354AD3F" }, + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, { name = "gleam_stdlib", version = "1.0.3", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1F543AFBA5D33DA493E6087F4E4C4F20D899411343512686C98A8ABB2963CF22" }, + { name = "gleam_time", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "533D8723774D61AD4998324F5DD1DABDCDBFABAFB9E87CB5D03C6955448FC97D" }, { name = "gleeunit", version = "1.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "EC31ABA74256AEA531EDF8169931D775BBB384FED0A8A1BDC4DD9354E3E21826" }, + { name = "gose", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "gleam_time", "kryptos"], otp_app = "gose", source = "hex", outer_checksum = "F1F172D428A07BC6F3229F8363B85E6E8C79A5E9E28F27B07C2EFBF1F6B08114" }, + { name = "justin", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "8B1C62269E8607D0A0ED698B7903984CE0BF7B6B3C0DEA3C6C8302B39402837B" }, + { name = "kryptos", version = "1.4.0", build_tools = ["gleam"], requirements = ["bigi", "gleam_stdlib", "gleam_time"], otp_app = "kryptos", source = "hex", outer_checksum = "734DAB837962A4A45788F8E6350EF4A6DF20A1F54734736CD1FF3A03FFCE4C97" }, + { name = "simplifile", version = "2.5.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "6C72DCCDF25C38A5931740B30E823969F33106831FD1637719B5EDBCA30027A4" }, ] [requirements] -atproto = { path = "../atproto" } +atproto_client = { version = ">= 0.1.0 and < 1.0.0" } +atproto_codegen = { version = ">= 0.1.0 and < 1.0.0" } gleam_json = { version = ">= 3.1.0 and < 4.0.0" } gleam_stdlib = { version = ">= 1.0.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/web/manifest.toml b/web/manifest.toml index c9847b5..337712e 100644 --- a/web/manifest.toml +++ b/web/manifest.toml @@ -8,13 +8,18 @@ packages = [ { name = "argv", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "3277D100448BDB4A29B6D58C0F36F631CBC349E8BDD09766C6309DF202831140" }, - { name = "at_record_shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["atproto", "gleam_json", "gleam_stdlib"], source = "local", path = "../shared" }, - { name = "atproto", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_json", "gleam_stdlib"], source = "local", path = "../atproto" }, + { name = "at_record_shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["atproto_client", "gleam_json", "gleam_stdlib"], source = "local", path = "../shared" }, + { name = "atproto", version = "0.1.0", build_tools = ["mix"], requirements = ["ecto", "xrpc"], otp_app = "atproto", source = "hex", outer_checksum = "F8FA8FC387DD47FD6E9722EC9B716ABC39595744A14A77588C66CD55DD371E41" }, + { name = "atproto_client", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_http", "gleam_json", "gleam_stdlib", "gleam_time", "gose", "kryptos"], otp_app = "atproto_client", source = "hex", outer_checksum = "728717DD1D24A45B1966665B9F62B6CC08CAE188CBEB3B4905D0CAFE0ED42989" }, + { name = "bigi", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "bigi", source = "hex", outer_checksum = "44FEDF1DF5C1F426A80B67C32B976D0828D66524E11BDC2DD22B78D37C883BA7" }, { name = "booklet", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "booklet", source = "hex", outer_checksum = "08E0FDB78DC4D8A5D3C80295B021505C7D2A2E7B6C6D5EAB7286C36F4A53C851" }, + { name = "decimal", version = "3.1.1", build_tools = ["mix"], requirements = [], otp_app = "decimal", source = "hex", outer_checksum = "C5F25F2CED74A0587D03E6023F595DB8E924C9D3922C8C8FFD9EDFC4498CF1F6" }, { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, + { name = "ecto", version = "3.14.0", build_tools = ["mix"], requirements = ["decimal", "jason", "telemetry"], otp_app = "ecto", source = "hex", outer_checksum = "130D69FFB4285F9CE4792B65DFBB994FD13EA4CBC3CBEA2524B199AA3DE84AF3" }, { name = "envoy", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "9C6FBB6BFA02A52798BEEC5977A738CAD6E4A057F4B67FD0C8061AD2502C191A" }, { name = "exception", version = "2.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "6BDEA95248093599391C3B5DF1835C5C6A86C353C2F99CE539B450E3432FE117" }, { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "finch", version = "0.23.0", build_tools = ["mix"], requirements = ["mime", "mint", "nimble_options", "nimble_pool", "telemetry"], otp_app = "finch", source = "hex", outer_checksum = "80E58D3F936F57E3FDF404F83A3642897AE6D9FB642934E46DA4D8FE761B99D5" }, { name = "gleam_community_ansi", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "B5AA433AF84313E23FDF90CCFF752B9380FE9FFCE02B2949D49B7AACCC77B16D" }, { name = "gleam_community_colour", version = "2.0.4", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "6DB4665555D7D2B27F0EA32EF47E8BEBC4303821765F9C73D483F38EE24894F0" }, { name = "gleam_crypto", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "2DE9E4EF53CF6FEE049D4F765731F7178F7A11AEFAE00EEE63BF7536B354AD3F" }, @@ -31,24 +36,35 @@ packages = [ { name = "gleeunit", version = "1.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "EC31ABA74256AEA531EDF8169931D775BBB384FED0A8A1BDC4DD9354E3E21826" }, { name = "glint", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "26CCA9BC3ACB56CD9D754ACF598F49B4C281C4FDD81AE61FA21C8E92E9C8218E" }, { name = "glisten", version = "9.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging"], otp_app = "glisten", source = "hex", outer_checksum = "7795AA50830656F3A0316A6B26595F893C83272DA901B3405E31339CAA31A10B" }, + { name = "gose", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "gleam_time", "kryptos"], otp_app = "gose", source = "hex", outer_checksum = "F1F172D428A07BC6F3229F8363B85E6E8C79A5E9E28F27B07C2EFBF1F6B08114" }, { name = "gramps", version = "6.0.1", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "D55636072DEE173F6586A5679D3C02EC7A0DE3F8646B78C351B72908FF223DF7" }, { name = "group_registry", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "group_registry", source = "hex", outer_checksum = "BC798A53D6F2406DB94E27CB45C57052CB56B32ACF7CC16EA20F6BAEC7E36B90" }, { name = "houdini", version = "1.2.1", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "6F8AC2F12974567FB744BEA66AC93CEB76AAEA19AD28564623F76CDA9BC26A85" }, { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "hpax", version = "1.0.4", build_tools = ["mix"], requirements = [], otp_app = "hpax", source = "hex", outer_checksum = "AFC7CB142EBCC2D01CE7816190B98CE5DD49E799111B24249F3443D730F377CA" }, + { name = "jason", version = "1.4.5", build_tools = ["mix"], requirements = ["decimal"], otp_app = "jason", source = "hex", outer_checksum = "B0C823996102BCD0239B3C2444EB00409B72F6A140C1950BC8B457D836B30684" }, { name = "justin", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "8B1C62269E8607D0A0ED698B7903984CE0BF7B6B3C0DEA3C6C8302B39402837B" }, + { name = "kryptos", version = "1.4.0", build_tools = ["gleam"], requirements = ["bigi", "gleam_stdlib", "gleam_time"], otp_app = "kryptos", source = "hex", outer_checksum = "734DAB837962A4A45788F8E6350EF4A6DF20A1F54734736CD1FF3A03FFCE4C97" }, { name = "logging", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "BC5F18CE5DD9686100229FE5409BDC3DD5C46D5A7DF2F804AD2D8F0DD6C5060E" }, { name = "lustre", version = "5.7.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "38C6FCBD7B7BACE994D5BF643202BB69B02576D44250B4C5DCE738387C0E8F87" }, { name = "lustre_dev_tools", version = "2.3.6", build_tools = ["gleam"], requirements = ["argv", "booklet", "filepath", "gleam_community_ansi", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_regexp", "gleam_stdlib", "glint", "group_registry", "justin", "lustre", "mist", "polly", "simplifile", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "7A650A107C7E767D902A8CB608460B927DE0CDD47D5BA04DED71F9F622FE6AD9" }, { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, + { name = "mime", version = "2.0.7", build_tools = ["mix"], requirements = [], otp_app = "mime", source = "hex", outer_checksum = "6171188E399EE16023FFC5B76CE445EB6D9672E2E241D2DF6050F3C771E80CCD" }, + { name = "mint", version = "1.9.1", build_tools = ["mix"], requirements = ["castore", "hpax"], otp_app = "mint", source = "hex", outer_checksum = "831101BD560B086316FAB5F7ADB21A4F3455717D8E4BC8368B052E09AA9163E0" }, { name = "mist", version = "6.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "1B07F321D5FA0CB162D81496F2DE96AEB6EF8980F4F38230A4CC3F849497E020" }, { name = "modem", version = "2.1.3", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "F03560273C4E857025E7628BCE1EBA8B7E9A1A81A73CA998FC29FD1CED188968" }, + { name = "nimble_options", version = "1.1.1", build_tools = ["mix"], requirements = [], otp_app = "nimble_options", source = "hex", outer_checksum = "821B2470CA9442C4B6984882FE9BB0389371B8DDEC4D45A9504F00A66F650B44" }, + { name = "nimble_pool", version = "1.1.0", build_tools = ["mix"], requirements = [], otp_app = "nimble_pool", source = "hex", outer_checksum = "AF2E4E6B34197DB81F7AAD230C1118EAC993ACC0DAE6BC83BAC0126D4AE0813A" }, { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, { name = "polly", version = "3.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_erlang", "gleam_otp", "gleam_stdlib", "simplifile"], otp_app = "polly", source = "hex", outer_checksum = "51FB565D81FF6212FDF3306D44419601F2A7C4EDD1F00FC9DA5C376A00AED4FE" }, + { name = "req", version = "0.3.12", build_tools = ["mix"], requirements = ["brotli", "ezstd", "finch", "jason", "mime", "nimble_csv", "plug"], otp_app = "req", source = "hex", outer_checksum = "C91103D4D1C8EDEBA90C84E0BA223A59865B673EAAB217BFD17DA3AA54AB136C" }, { name = "rsvp", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "A38F2DB9657E84A584278DB50FD903693FF370C22C191753B773546FEB2465F0" }, { name = "simplifile", version = "2.4.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "7C18AFA4FED0B4CE1FA5B0B4BAC1FA1744427054EA993565F6F3F82E5453170D" }, { name = "snag", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "274F41D6C3ECF99F7686FDCE54183333E41D2C1CA5A3A673F9A8B2C7A4401077" }, + { name = "telemetry", version = "1.4.2", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "928F6495066506077862C0D1646609EED891A4326BEE3126BA54B60AF61FEBB1" }, { name = "tom", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "DCF04CB7AB35D58CFC598C66EA2E1816D160759802C89B2BA6238780D59BC256" }, { name = "wisp", version = "2.2.2", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "5FF5F1E288C3437252ABB93D8F9CF42FF652CE7AD54480CFE736038DC09C4F22" }, + { name = "xrpc", version = "0.1.0", build_tools = ["mix"], requirements = ["req"], otp_app = "xrpc", source = "hex", outer_checksum = "3E71C6AB1390F14FDD8BC015C261C2F9F9BF6E0CEE0FFA850DCFF8444658C6AB" }, ] [requirements]