diff --git a/src/at/car.gleam b/src/at/car.gleam new file mode 100644 index 0000000..b881814 --- /dev/null +++ b/src/at/car.gleam @@ -0,0 +1,191 @@ +// IMPORTS --------------------------------------------------------------------- + +import at/cbor +import at/cid.{type Cid} +import gleam/bit_array +import gleam/dynamic/decode.{type Decoder} +import gleam/int +import gleam/list + +// TYPES ----------------------------------------------------------------------- + +/// +/// +pub opaque type Car { + Car(roots: List(Cid), blocks: List(Block)) +} + +type CarHeader { + CarHeader(version: Int, roots: List(Cid)) +} + +/// +/// +pub type Block { + Block(cid: Cid, data: BitArray) +} + +/// Errors that can occur while decoding a [`Car`](#Car) from a `BitArray`. +/// +pub type DecodeError { + MalformedInput + UnexpectedEndOfInput + UnsupportedVersion(Int) + InvalidHeader(cbor.DecodeError) + InvalidCid +} + +// CONSTRUCTORS ---------------------------------------------------------------- + +/// +/// +pub fn from_bit_array(bits: BitArray) -> Result(Car, DecodeError) { + case parse_varint(bits) { + Ok(#(0, _)) -> Error(MalformedInput) + + Ok(#(length, remaining)) -> + case remaining { + <> -> + case cbor.parse(header, header_decoder()) { + Ok(CarHeader(version: 1, roots:)) -> + case parse_blocks(remaining, []) { + Ok(blocks) -> Ok(Car(roots:, blocks:)) + Error(reason) -> Error(reason) + } + + Ok(CarHeader(version:, ..)) -> Error(UnsupportedVersion(version)) + + Error(reason) -> Error(InvalidHeader(reason)) + } + + _ -> Error(UnexpectedEndOfInput) + } + + Error(reason) -> Error(reason) + } +} + +fn header_decoder() -> Decoder(CarHeader) { + use version <- decode.field("version", decode.int) + use roots <- decode.field("roots", decode.list(cid.decoder())) + + decode.success(CarHeader(version:, roots:)) +} + +fn parse_blocks( + bits: BitArray, + blocks: List(Block), +) -> Result(List(Block), DecodeError) { + case bits { + <<>> -> Ok(list.reverse(blocks)) + + _ -> + case parse_block(bits) { + Ok(#(block, remaining)) -> parse_blocks(remaining, [block, ..blocks]) + Error(reason) -> Error(reason) + } + } +} + +fn parse_block(bits: BitArray) -> Result(#(Block, BitArray), DecodeError) { + case parse_varint(bits) { + Ok(#(length, _)) if length < 36 -> Error(MalformedInput) + + Ok(#(length, remaining)) -> { + let data_size = length - 36 + + case remaining { + <> -> + case cid.from_bit_array(cid_bits) { + Ok(cid) -> Ok(#(Block(cid:, data:), remaining)) + Error(_) -> Error(InvalidCid) + } + + _ -> Error(UnexpectedEndOfInput) + } + } + + Error(reason) -> Error(reason) + } +} + +fn parse_varint(bits: BitArray) -> Result(#(Int, BitArray), DecodeError) { + do_parse_varint(bits, 1, 0, 0) +} + +fn do_parse_varint( + bits: BitArray, + multiplier: Int, + acc: Int, + bytes_read: Int, +) -> Result(#(Int, BitArray), DecodeError) { + case bits { + _ if bytes_read > 8 -> Error(MalformedInput) + + <<0:1, value:7, remaining:bits>> -> + Ok(#(acc + value * multiplier, remaining)) + + <<1:1, value:7, remaining:bits>> -> + do_parse_varint( + remaining, + multiplier * 128, + acc + value * multiplier, + bytes_read + 1, + ) + + _ -> Error(UnexpectedEndOfInput) + } +} + +// QUERIES --------------------------------------------------------------------- + +/// +/// +pub fn roots(car: Car) -> List(Cid) { + car.roots +} + +/// +/// +pub fn blocks(car: Car) -> List(Block) { + car.blocks +} + +/// +/// +pub fn block(car: Car, id: Cid) -> Result(Block, Nil) { + list.find(car.blocks, fn(block) { block.cid == id }) +} + +// CONVERSIONS ----------------------------------------------------------------- + +pub fn to_bit_array(roots: List(Cid), blocks: List(Block)) -> BitArray { + let header = + cbor.to_bit_array( + cbor.object([ + #("version", cbor.int(1)), + #("roots", cbor.array(roots, cbor.cid)), + ]), + ) + + let bits = <> + + use bits, block <- list.fold(blocks, bits) + let block_bytes = cid.to_bit_array(block.cid) + let block_length = + bit_array.byte_size(block_bytes) + bit_array.byte_size(block.data) + + <> +} + +fn varint(value: Int) -> BitArray { + case value < 128 { + True -> <<0:1, value:7>> + False -> { + let low = int.bitwise_and(value, 127) + let rest = int.bitwise_shift_right(value, 7) + + <<1:1, low:7, varint(rest):bits>> + } + } +} diff --git a/test/at/cbor_test.gleam b/test/at/cbor_test.gleam index c7a4d69..b8ee3a5 100644 --- a/test/at/cbor_test.gleam +++ b/test/at/cbor_test.gleam @@ -1,40 +1,18 @@ // IMPORTS --------------------------------------------------------------------- import at/cbor -import at/cid.{type Cid} +import at/cid import gleam/bit_array -import gleam/dynamic.{type Dynamic} import gleam/dynamic/decode -import gleam/option.{type Option, None} +import gleam/option.{None} import gleam/result // ATPROTO INTEROP ------------------------------------------------------------- // https://github.com/bluesky-social/atproto-interop-tests/blob/main/data-model/data-model-fixtures.json -type AtprotoInteropNestedFixture { - AtprotoInteropNestedFixture( - string: String, - unicode: String, - integer: Int, - bool: Bool, - null: Option(Dynamic), - array: List(String), - object: AtprotoInteropNestedFixtureInner, - ) -} - -type AtprotoInteropNestedFixtureInner { - AtprotoInteropNestedFixtureInner( - string: String, - number: Int, - bool: Bool, - arr: List(String), - ) -} - -pub fn atproto_interop_nested_cbor_test() { +pub fn atproto_interop_core_types_cbor_test() { let assert Ok(bits) = - // json: { + // { // "string": "abc", // "unicode": "a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧", // "integer": 123, @@ -66,76 +44,46 @@ pub fn atproto_interop_nested_cbor_test() { use bool <- decode.field("bool", decode.bool) use arr <- decode.field("arr", decode.list(decode.string)) - decode.success(AtprotoInteropNestedFixtureInner( - string:, - number:, - bool:, - arr:, - )) + decode.success(#(string, number, bool, arr)) }) - decode.success(AtprotoInteropNestedFixture( - string:, - unicode:, - integer:, - bool:, - null:, - array:, - object:, - )) + decode.success(#(string, unicode, integer, bool, null, array, object)) }) assert fixture - == AtprotoInteropNestedFixture( - string: "abc", - unicode: "a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧", - integer: 123, - bool: True, - null: None, - array: ["abc", "def", "ghi"], - object: AtprotoInteropNestedFixtureInner( - string: "abc", - number: 123, - bool: True, - arr: ["abc", "def", "ghi"], - ), + == #( + "abc", + "a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧", + 123, + True, + None, + ["abc", "def", "ghi"], + #("abc", 123, True, ["abc", "def", "ghi"]), ) assert bits == cbor.to_bit_array( cbor.object([ - #("string", cbor.string(fixture.string)), - #("unicode", cbor.string(fixture.unicode)), - #("integer", cbor.int(fixture.integer)), - #("bool", cbor.bool(fixture.bool)), - #("null", cbor.option(fixture.null, fn(_) { cbor.int(1234) })), - #("array", cbor.array(fixture.array, cbor.string)), + #("string", cbor.string(fixture.0)), + #("unicode", cbor.string(fixture.1)), + #("integer", cbor.int(fixture.2)), + #("bool", cbor.bool(fixture.3)), + #("null", cbor.option(fixture.4, fn(_) { cbor.int(1234) })), + #("array", cbor.array(fixture.5, cbor.string)), #("object", { cbor.object([ - #("string", cbor.string(fixture.object.string)), - #("number", cbor.int(fixture.object.number)), - #("bool", cbor.bool(fixture.object.bool)), - #("arr", cbor.array(fixture.object.arr, cbor.string)), + #("string", cbor.string(fixture.6.0)), + #("number", cbor.int(fixture.6.1)), + #("bool", cbor.bool(fixture.6.2)), + #("arr", cbor.array(fixture.6.3, cbor.string)), ]) }), ]), ) } -type AtprotoInteropCidFixture { - AtprotoInteropCidFixture( - a: Cid, - b: BitArray, - c: AtprotoInteropCidFixtureInner, - ) -} - -type AtprotoInteropCidFixtureInner { - AtprotoInteropCidFixtureInner(ref: Cid, mime_type: String, size: Int) -} - pub fn atproto_interop_cid_cbor_test() { - // json: { + // { // "a": { // "$link": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" // }, @@ -167,52 +115,127 @@ pub fn atproto_interop_cid_cbor_test() { use size <- decode.field("size", decode.int) case t { - "blob" -> - decode.success(AtprotoInteropCidFixtureInner( - ref:, - mime_type:, - size:, - )) - - _ -> - decode.failure( - AtprotoInteropCidFixtureInner(ref:, mime_type:, size:), - "$type: \"blob\"", - ) + "blob" -> decode.success(#(ref, mime_type, size)) + _ -> decode.failure(#(ref, mime_type, size), "$type: \"blob\"") } }) - decode.success(AtprotoInteropCidFixture(a:, b:, c:)) + decode.success(#(a, b, c)) }) assert fixture - == AtprotoInteropCidFixture( - a: "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" + == #( + "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" |> cid.from_string |> result.unwrap(cid.zero), - b: "nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0" + "nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0" |> bit_array.base64_decode |> result.unwrap(<<>>), - c: AtprotoInteropCidFixtureInner( - ref: "bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity" + #( + "bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity" |> cid.from_string |> result.unwrap(cid.zero), - mime_type: "image/jpeg", - size: 10_000, + "image/jpeg", + 10_000, ), ) assert bits == cbor.to_bit_array( cbor.object([ - #("a", cbor.cid(fixture.a)), - #("b", cbor.bytes(fixture.b)), + #("a", cbor.cid(fixture.0)), + #("b", cbor.bytes(fixture.1)), #("c", { cbor.object([ #("$type", cbor.string("blob")), - #("ref", cbor.cid(fixture.c.ref)), - #("mimeType", cbor.string(fixture.c.mime_type)), - #("size", cbor.int(fixture.c.size)), + #("ref", cbor.cid(fixture.2.0)), + #("mimeType", cbor.string(fixture.2.1)), + #("size", cbor.int(fixture.2.2)), + ]) + }), + ]), + ) +} + +pub fn atproto_interop_nested_structures_cbor_test() { + // { + // "a": { + // "b": [ + // { + // "d": [ + // { + // "$link": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" + // }, + // { + // "$link": "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" + // } + // ], + // "e": [ + // { "$bytes": "nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0" }, + // { "$bytes": "iE+sPoHobU9tSIqGI+309LLCcWQIRmEXwxcoDt19tas" } + // ] + // } + // ] + // } + // } + let assert Ok(bits) = + bit_array.base64_decode( + "oWFhoWFigaJhZILYKlglAAFxEiBlBipaWgD8Ftc8aUQjfMvBWxxKcjRIkzaJHQkXQaI50NgqWCUAAXESIGUGKlpaAPwW1zxpRCN8y8FbHEpyNEiTNokdCRdBojnQYWWCWCCcURGO8suLD2qbjkmuof1BPPILYu7Vdvid7r6wGsLMjVggiE+sPoHobU9tSIqGI+309LLCcWQIRmEXwxcoDt19tas", + ) + + let assert Ok(fixture) = + cbor.parse(bits, { + use a <- decode.field("a", { + use b <- decode.field("b", { + decode.list({ + use d <- decode.field("d", decode.list(cid.decoder())) + use e <- decode.field("e", decode.list(decode.bit_array)) + + decode.success(#(d, e)) + }) + }) + + decode.success(b) + }) + + decode.success(a) + }) + + assert fixture + == [ + #( + [ + "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" + |> cid.from_string + |> result.unwrap(cid.zero), + "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a" + |> cid.from_string + |> result.unwrap(cid.zero), + ], + [ + "nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0" + |> bit_array.base64_decode + |> result.unwrap(<<>>), + "iE+sPoHobU9tSIqGI+309LLCcWQIRmEXwxcoDt19tas" + |> bit_array.base64_decode + |> result.unwrap(<<>>), + ], + ), + ] + + assert bits + == cbor.to_bit_array( + cbor.object([ + #("a", { + cbor.object([ + #("b", { + cbor.array(fixture, fn(element) { + cbor.object([ + #("d", cbor.array(element.0, cbor.cid)), + #("e", cbor.array(element.1, cbor.bytes)), + ]) + }) + }), ]) }), ]), diff --git a/test/at/did_test.gleam b/test/at/did_test.gleam index 09152aa..c6c6673 100644 --- a/test/at/did_test.gleam +++ b/test/at/did_test.gleam @@ -1,84 +1,72 @@ +// IMPORTS --------------------------------------------------------------------- + import at/did -import gleam/json import gleam/list import gleam/uri -pub fn valid_dids_test() { - let examples = [ - "did:plc:ewvi7nxzyoun6zhxrhs64oiz", - "did:plc:7iza6de2dwap2sbkpav7c6c6", - "did:web:user.example.com", - "did:web:john-john.test", - "did:web:8.cn", - "did:web:localhost%3A1234", - ] - - list.each(examples, fn(example) { - let assert Ok(parsed) = did.from_string(example) as example - assert did.to_string(parsed) == example - }) +// FIXTURES -------------------------------------------------------------------- - let assert Ok(localhost) = did.from_string("did:web:localhost%3a3000") - assert did.to_string(localhost) == "did:web:localhost%3A3000" -} +const valid = [ + "did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "did:plc:7iza6de2dwap2sbkpav7c6c6", + "did:web:user.example.com", + "did:web:john-john.test", + "did:web:8.cn", + "did:web:localhost%3A1234", +] -pub fn invalid_dids_test() { - let examples = [ - // Only the two blessed atproto methods are supported. - "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N", - // did:plc is exactly 24 lowercase base32 characters - "did:plc:ewvi7nxzyoun6zhxrhs64oi", - "did:plc:EWVI7NXZYOUN6ZHXHRS64OIZ", - "did:plc:ewvi7nxzyoun6zhxrhs64oi0", - // did:web uses lowercase, hostname-level identifiers - "did:web:EXAMPLE.COM", - "did:web:john", - "did:web:john..test", - "did:web:-john.test", - "did:web:john-.test", - "did:web:john.0", - "did:web:bücher.test", - "did:web:example.com:user", - // Reserved TLDs fail atproto resolution policy - "did:web:blah.arpa", - // Ports are only available on localhost in development - "did:web:example.com%3A3000", - "did:web:localhost%3A65536", - ] +const invalid = [ + // Only the two blessed atproto methods are supported. + "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N", + // did:plc is exactly 24 lowercase base32 characters + "did:plc:ewvi7nxzyoun6zhxrhs64oi", + "did:plc:EWVI7NXZYOUN6ZHXHRS64OIZ", + "did:plc:ewvi7nxzyoun6zhxrhs64oi0", + // did:web uses lowercase, hostname-level identifiers + "did:web:EXAMPLE.COM", + "did:web:john", + "did:web:john..test", + "did:web:-john.test", + "did:web:john-.test", + "did:web:john.0", + "did:web:bücher.test", + "did:web:example.com:user", + // Reserved TLDs fail atproto resolution policy + "did:web:blah.arpa", + // Ports are only available on localhost in development + "did:web:example.com%3A3000", + "did:web:localhost%3A65536", +] - list.each(examples, fn(example) { - assert did.from_string(example) == Error(Nil) as example - }) -} +const uris = [ + #( + "did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "https://plc.directory/did:plc:ewvi7nxzyoun6zhxrhs64oiz", + ), + #("did:web:example.com", "https://example.com/.well-known/did.json"), + #("did:web:localhost%3A3000", "https://localhost:3000/.well-known/did.json"), +] -pub fn document_uri_examples_test() { - let examples = [ - #( - "did:plc:ewvi7nxzyoun6zhxrhs64oiz", - "https://plc.directory/did:plc:ewvi7nxzyoun6zhxrhs64oiz", - ), - #("did:web:example.com", "https://example.com/.well-known/did.json"), - #("did:web:localhost%3A3000", "https://localhost:3000/.well-known/did.json"), - ] +// - use example <- list.each(examples) - let #(input, expected) = example - let assert Ok(parsed) = did.from_string(input) +pub fn atproto_interop_valid_did_test() { + use input <- list.each(valid) + let assert Ok(did) = did.from_string(input) as input - assert parsed |> did.document_uri |> uri.to_string == expected + assert did.to_string(did) == input } -pub fn json_conversion_and_decoder_test() { - let input = "did:web:example.com" - let assert Ok(parsed) = did.from_string(input) +pub fn atproto_interop_invalid_did_test() { + use input <- list.each(invalid) + + assert did.from_string(input) == Error(Nil) as input +} - assert parsed - |> did.to_json - |> json.to_string - == "\"" <> input <> "\"" +// - let assert Ok(decoded) = json.parse("\"" <> input <> "\"", did.decoder()) - assert did.to_string(decoded) == input +pub fn document_uri_examples_test() { + use #(input, expected) <- list.each(uris) + let assert Ok(parsed) = did.from_string(input) as input - let assert Error(_) = json.parse("\"did:web:invalid\"", did.decoder()) + assert parsed |> did.document_uri |> uri.to_string == expected } diff --git a/test/at/handle_test.gleam b/test/at/handle_test.gleam new file mode 100644 index 0000000..86aa511 --- /dev/null +++ b/test/at/handle_test.gleam @@ -0,0 +1,147 @@ +// IMPORTS --------------------------------------------------------------------- + +import at/handle +import gleam/list +import gleam/string + +// FIXTURES -------------------------------------------------------------------- + +const valid = [ + "A.ISI.EDU", + "XX.LCS.MIT.EDU", + "SRI-NIC.ARPA", + "john.test", + "jan.test", + "a234567890123456789.test", + "john2.test", + "john-john.test", + "john.bsky.app", + "jo.hn", + "a.co", + "a.org", + "joh.n", + "j0.h0", + "jaymome-johnber123456.test", + "jay.mome-johnber123456.test", + "john.test.bsky.app", + "shoooort.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.test", + "short.ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.test", + "john.t", + "laptop.local", + "laptop.arpa", + "xn--ls8h.test", + "xn--bcher-kva.tld", + "xn--3jk.com", + "xn--w3d.com", + "xn--vqb.com", + "xn--ppd.com", + "xn--cs9a.com", + "xn--8r9a.com", + "xn--cfd.com", + "xn--5jk.com", + "xn--2lb.com", + "expyuzz4wqqyqhjn.onion", + "friend.expyuzz4wqqyqhjn.onion", + "g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", + "friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", + "friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", + "2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", + "friend.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", + "12345.test", + "8.cn", + "4chan.org", + "4chan.o-g", + "blah.4chan.org", + "thing.a01", + "120.0.0.1.com", + "0john.test", + "9sta--ck.com", + "99stack.com", + "0ohn.test", + "john.t--t", + "thing.0aa.thing", + "stack.com", + "sta-ck.com", + "sta---ck.com", + "sta--ck9.com", + "stack99.com", + "sta99ck.com", + "google.com.uk", + "google.co.in", + "google.com", + "maselkowski.pl", + "m.maselkowski.pl", + "xn--masekowski-d0b.pl", + "xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s", + "xn--stackoverflow.com", + "stackoverflow.xn--com", + "stackoverflow.co.uk", + "xn--masekowski-d0b.pl", + "xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s", +] + +const invalid = [ + "did:thing.test", + "did:thing", + "john-.test", + "john.0", + "john.-", + "xn--bcher-.tld", + "john..test", + "jo_hn.test", + "-john.test", + ".john.test", + "jo!hn.test", + "jo%hn.test", + "jo&hn.test", + "jo@hn.test", + "jo*hn.test", + "jo|hn.test", + "jo:hn.test", + "jo/hn.test", + "john💩.test", + "bücher.test", + "john .test", + "john.test.", + "john", + "john.", + ".john", + "john.test.", + ".john.test", + " john.test", + "john.test ", + "joh-.test", + "john.-est", + "john.tes-", + "shoooort.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.test", + "short.oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.test", + "org", + "ai", + "gg", + "io", + "cn.8", + "thing.0aa", + "thing.0aa", + "127.0.0.1", + "192.168.0.142", + "fe80::7325:8a97:c100:94b", + "2600:3c03::f03c:9100:feb0:af1f", + "-notvalid.at-all", + "-thing.com", + "www.masełkowski.pl.com", +] + +// INTEROP TESTS --------------------------------------------------------------- + +pub fn atproto_interop_valid_handle_test() { + use input <- list.each(valid) + let assert Ok(handle) = handle.from_string(input) as input + + assert handle.to_string(handle) == string.lowercase(input) +} + +pub fn atproto_interop_invalid_handle_test() { + use input <- list.each(invalid) + + assert handle.from_string(input) == Error(Nil) as input +} diff --git a/test/at/rkey_test.gleam b/test/at/rkey_test.gleam new file mode 100644 index 0000000..3a95e42 --- /dev/null +++ b/test/at/rkey_test.gleam @@ -0,0 +1,55 @@ +// IMPORTS --------------------------------------------------------------------- + +import at/rkey +import gleam/list + +// FIXTURES -------------------------------------------------------------------- + +const valid = [ + "self", + "example.com", + "~1.2-3_", + "dHJ1ZQ", + "_", + "literal:self", + "pre:fix", + ":", + "-", + "_", + "~", + "...", + "self.", + "lang:", + ":lang", + "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo", +] + +const invalid = [ + "alpha/beta", + ".", + "..", + "#extra", + "@handle", + "any space", + "any+space", + "number[3]", + "number(3)", + "\"quote\"", + "dHJ1ZQ==", + "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo", +] + +// INTEROP TESTS --------------------------------------------------------------- + +pub fn atproto_interop_valid_rkey_test() { + use input <- list.each(valid) + let assert Ok(rkey) = rkey.from_string(input) as input + + assert rkey.to_string(rkey) == input +} + +pub fn atproto_interop_invalid_rkey_test() { + use input <- list.each(invalid) + + assert rkey.from_string(input) == Error(Nil) as input +} diff --git a/test/at/uri_test.gleam b/test/at/uri_test.gleam new file mode 100644 index 0000000..d5fe1d8 --- /dev/null +++ b/test/at/uri_test.gleam @@ -0,0 +1,127 @@ +// IMPORTS --------------------------------------------------------------------- + +import at/uri +import gleam/list + +// FIXTURES -------------------------------------------------------------------- + +const valid = [ + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/a", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/asdf-123", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/asdf123", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/asdf123", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/record", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/record", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.postV2", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/io.nsid.someFunc/_", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/io.nsid.someFunc/-", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/io.nsid.someFunc/:", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/io.nsid.someFunc/...", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/io.nsid.someFunc/~", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/io.nsid.someFunc/lang:", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/io.nsid.someFunc/record-key", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/io.nsid.someFunc/self.", + + "at://did:web:user.example.com", + "at://did:web:john-john.test", + "at://did:web:8.cn", + "at://did:web:localhost%3A1234", + + "at://user.bsky.social", + "at://user.bsky.social", +] + +const invalid = [ + "a://did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at//did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at:/a/did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at:/did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "AT://did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "http://did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "://did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at:did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at:/did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at:///did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at://:/did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at:/ /did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz ", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/ ", + " at://did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post ", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post# ", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post#/ ", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post#/frag ", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post#fr ag", + "//did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at://name", + "at://name.0", + "at://diD:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.p@st", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.p$st", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.p%st", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.p&st", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.p()t", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed_post", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/-com.atproto.feed.post", + "at://did:plc:asdf@123/com.atproto.feed.post", + "at://DID:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at://user.bsky.123", + "at://bsky", + "at://did:plc:", + "at://did:plc:", + "at://frag", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo", + "at://user.bsky.social//", + "at://user.bsky.social//com.atproto.feed.post", + "at://user.bsky.social/com.atproto.feed.post//", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/asdf123/more/more',", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/short/stuff", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/12345", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/", + "at://user.bsky.social/", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/record/", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/record/#/frag", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/asdf123/asdf", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz#", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz##", + "#at://did:plc:ewvi7nxzyoun6zhxrhs64oiz", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz#/asdf#/asdf", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/%23", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/$@!*)(:,;~.sdf123", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/~'sdf123\")", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/$", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/@", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/!", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/*", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/(", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/,", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/;", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/abc%30123", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/%30", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/%3", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/%", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/%zz", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/%%%", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/.", + "at://did:plc:ewvi7nxzyoun6zhxrhs64oiz/com.atproto.feed.post/..", +] + +// INTEROP TESTS --------------------------------------------------------------- + +pub fn atproto_interop_valid_uri_test() { + use input <- list.each(valid) + let assert Ok(uri) = uri.from_string(input) as input + + assert uri.to_string(uri) == input +} + +pub fn atproto_interop_invalid_uri_test() { + use input <- list.each(invalid) + + assert uri.from_string(input) == Error(Nil) as input +} diff --git a/test/at_test.gleam b/test/at_test.gleam index 902c4da..bac6835 100644 --- a/test/at_test.gleam +++ b/test/at_test.gleam @@ -1,5 +1,9 @@ +// IMPORTS --------------------------------------------------------------------- + import gleeunit +// MAIN ------------------------------------------------------------------------ + pub fn main() -> Nil { gleeunit.main() }