A character sheet creator for TTRPGs sheetr.app/
gleam dnd dnd5e atproto

alicia/identity: Basic identity parsing

Signed-off-by: Naomi Roberts <mia@naomieow.xyz>

lesbian.skin 076fac07 9d05c755

verified
+1 -1
alicia/README.md
··· 1 1 # alicia 2 2 3 - An ATProtocol SDK in Gleam. 3 + An ATProtocol SDK in Gleam, inspired by [indigo](https://github.com/bluesky-social/indigo/). 4 4 5 5 ## License 6 6
+7
alicia/identity/birdie_snapshots/parse_empty_handle.accepted
··· 1 + --- 2 + version: 1.5.3 3 + title: Parse empty handle 4 + file: ./test/alicia_identity_test.gleam 5 + test_name: empty_handle_parse_test 6 + --- 7 + Error(HandleEmpty)
+7
alicia/identity/birdie_snapshots/parse_invalid_handle.accepted
··· 1 + --- 2 + version: 1.5.3 3 + title: Parse invalid handle 4 + file: ./test/alicia_identity_test.gleam 5 + test_name: invalid_handle_parse_test 6 + --- 7 + Error(HandleRegexFailure("lesbian#skin"))
+7
alicia/identity/birdie_snapshots/parse_valid_handle.accepted
··· 1 + --- 2 + version: 1.5.3 3 + title: Parse valid handle 4 + file: ./test/alicia_identity_test.gleam 5 + test_name: handle_parse_test 6 + --- 7 + Ok("lesbian.skin")
+3
alicia/identity/gleam.toml
··· 14 14 15 15 [dependencies] 16 16 gleam_stdlib = ">= 0.44.0 and < 2.0.0" 17 + gleam_regexp = ">= 1.1.1 and < 2.0.0" 17 18 18 19 [dev-dependencies] 19 20 gleeunit = ">= 1.0.0 and < 2.0.0" 21 + birdie = ">= 1.5.3 and < 2.0.0" 22 + pprint = ">= 1.0.6 and < 2.0.0"
+25
alicia/identity/manifest.toml
··· 2 2 # You typically do not need to edit this file 3 3 4 4 packages = [ 5 + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 + { name = "birdie", version = "1.5.3", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "envoy", "filepath", "glance", "gleam_community_ansi", "gleam_stdlib", "global_value", "justin", "rank", "simplifile", "term_size", "tom", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "64E7D9CC9E84272DA07061628E1B8F31F34FCD2008BCED47AB8FD58457CA63E2" }, 7 + { name = "edit_distance", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "7DC465C34695F9E57D79FC65670C53C992CE342BF29E0AA41FF44F61AF62FC56" }, 8 + { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, 9 + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 10 + { name = "glam", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "237C2CE218A2A0A5D46D625F8EF5B78F964BC91018B78D692B17E1AB84295229" }, 11 + { name = "glance", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "49E0ED4793BB3F56C3E5ED00528D70CAE21D263F70A735604124B95C5F62E2DB" }, 12 + { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, 13 + { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, 14 + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 15 + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, 5 16 { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" }, 17 + { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, 6 18 { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 19 + { name = "glexer", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "splitter"], otp_app = "glexer", source = "hex", outer_checksum = "40A1FB0919FA080AD6C5809B4C7DBA545841CAAC8168FACDFA0B0667C22475CC" }, 20 + { name = "global_value", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "global_value", source = "hex", outer_checksum = "23F74C91A7B819C43ABCCBF49DAD5BB8799D81F2A3736BA9A534BD47F309FF4F" }, 21 + { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 22 + { name = "pprint", version = "1.0.6", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "4E9B34AE03B2E81D60F230B9BAF1792BE1AC37AFB5564B8DEBEE56BAEC866B7D" }, 23 + { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, 24 + { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" }, 25 + { name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" }, 26 + { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, 27 + { name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" }, 28 + { name = "trie_again", version = "1.1.4", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "E3BD66B4E126EF567EA8C4944EAB216413392ADF6C16C36047AF79EE5EF13466" }, 7 29 ] 8 30 9 31 [requirements] 32 + birdie = { version = ">= 1.5.3 and < 2.0.0" } 33 + gleam_regexp = { version = ">= 1.1.1 and < 2.0.0" } 10 34 gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 11 35 gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 36 + pprint = { version = ">= 1.0.6 and < 2.0.0" }
+203
alicia/identity/src/alicia/identity.gleam
··· 1 + import alicia/identity/did 2 + import gleam/bool 3 + import gleam/dict 4 + import gleam/list 5 + import gleam/regexp 6 + import gleam/result 7 + import gleam/string 8 + import gleam/uri 9 + 10 + pub const invalid_handle = "handle.invalid" 11 + 12 + pub type IdentityError { 13 + NoServicesFound 14 + ServiceNotFound(String) 15 + UrlParseFailure 16 + RegexCompileFailure 17 + HandleEmpty 18 + HandleTooLong 19 + HandleRegexFailure(String) 20 + HandleNotDeclared 21 + } 22 + 23 + pub type Handle = 24 + String 25 + 26 + pub type Identity { 27 + Identity( 28 + did: did.Did, 29 + handle: Handle, 30 + also_known_as: List(String), 31 + services: dict.Dict(String, ServiceEndpoint), 32 + keys: dict.Dict(String, VerificationMethod), 33 + ) 34 + } 35 + 36 + pub type ServiceEndpoint { 37 + ServiceEndpoint(type_: String, url: String) 38 + } 39 + 40 + pub type VerificationMethod { 41 + VerificationMethod(type_: String, public_key_multibase: String) 42 + } 43 + 44 + pub fn parse_identity(doc doc: did.Document) -> Identity { 45 + let keys = 46 + list.fold(doc.verification_method, dict.new(), fn(keys, vm) { 47 + { 48 + use #(_, right) <- result.try(string.split_once(vm.id, "#")) 49 + // ignore keys not controlled by this DID 50 + use <- bool.guard(when: vm.controller != doc.did, return: Error(Nil)) 51 + // don't overwrite existing keys with this ID fragment 52 + use <- bool.guard(when: dict.has_key(keys, right), return: Error(Nil)) 53 + Ok(dict.insert( 54 + keys, 55 + right, 56 + VerificationMethod( 57 + type_: vm.type_, 58 + public_key_multibase: vm.public_key_multibase, 59 + ), 60 + )) 61 + } 62 + |> result.unwrap(keys) 63 + }) 64 + 65 + let services = 66 + list.fold(doc.service, dict.new(), fn(services, svc) { 67 + { 68 + use #(_, right) <- result.try(string.split_once(svc.id, "#")) 69 + // don't overwrite existing services with this ID fragment 70 + use <- bool.guard( 71 + when: dict.has_key(services, right), 72 + return: Error(Nil), 73 + ) 74 + Ok(dict.insert( 75 + services, 76 + right, 77 + ServiceEndpoint(type_: svc.type_, url: svc.service_endpoint), 78 + )) 79 + } 80 + |> result.unwrap(services) 81 + }) 82 + Identity( 83 + did: doc.did, 84 + handle: invalid_handle, 85 + also_known_as: doc.also_known_as, 86 + services:, 87 + keys:, 88 + ) 89 + } 90 + 91 + pub fn service_endpoint( 92 + identity identity: Identity, 93 + id id: String, 94 + ) -> Result(String, IdentityError) { 95 + use <- bool.guard( 96 + when: dict.is_empty(identity.services), 97 + return: Error(NoServicesFound), 98 + ) 99 + use endpoint <- result.try( 100 + dict.get(identity.services, id) 101 + |> result.replace_error(ServiceNotFound(id)), 102 + ) 103 + use _ <- result.try( 104 + uri.parse(endpoint.url) 105 + |> result.replace_error(UrlParseFailure), 106 + ) 107 + // TODO: Possibly return Uri here instead of just using it to validate the 108 + // url? - Depends on usecase in future. 109 + Ok(endpoint.url) 110 + } 111 + 112 + pub fn pds_endpoint( 113 + identity identity: Identity, 114 + ) -> Result(String, IdentityError) { 115 + service_endpoint(identity:, id: "atproto_pds") 116 + } 117 + 118 + pub fn handle_regex() -> Result(regexp.Regexp, IdentityError) { 119 + regexp.from_string( 120 + "^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$", 121 + ) 122 + |> result.replace_error(RegexCompileFailure) 123 + } 124 + 125 + pub fn parse_handle(handle handle: String) -> Result(Handle, IdentityError) { 126 + use <- bool.guard(when: string.is_empty(handle), return: Error(HandleEmpty)) 127 + use <- bool.guard( 128 + when: string.length(handle) > 253, 129 + return: Error(HandleTooLong), 130 + ) 131 + use regex <- result.try(handle_regex()) 132 + use <- bool.guard( 133 + when: !regexp.check(regex, handle), 134 + return: Error(HandleRegexFailure(handle)), 135 + ) 136 + Ok(handle) 137 + } 138 + 139 + pub fn normalise_handle(handle handle: Handle) -> Handle { 140 + string.lowercase(handle) 141 + } 142 + 143 + pub fn declared_handle( 144 + identity identity: Identity, 145 + ) -> Result(Handle, IdentityError) { 146 + let valid_akas = 147 + list.map(identity.also_known_as, fn(handle) { 148 + case handle { 149 + "at://" <> rest -> Ok(rest) 150 + _ -> Error(HandleNotDeclared) 151 + } 152 + }) 153 + use handle <- result.try( 154 + valid_akas 155 + |> result.values 156 + |> list.first 157 + |> result.replace_error(HandleNotDeclared), 158 + ) 159 + use handle <- result.try(parse_handle(handle)) 160 + Ok(normalise_handle(handle)) 161 + } 162 + 163 + pub fn public_key(identity identity: Identity, id id: String) -> String { 164 + todo as "unimplemented" 165 + } 166 + 167 + pub fn repo_public_key(identity identity: Identity) -> String { 168 + public_key(identity:, id: "atproto") 169 + } 170 + 171 + pub fn did_document(identity identity: Identity) -> did.Document { 172 + let verification_method = 173 + dict.fold(identity.keys, [], fn(acc, handle, vm) { 174 + [ 175 + did.DocumentVerificationMethod( 176 + id: identity.did <> "#" <> handle, 177 + type_: vm.type_, 178 + controller: identity.did, 179 + public_key_multibase: vm.public_key_multibase, 180 + ), 181 + ..acc 182 + ] 183 + }) 184 + 185 + let service = 186 + dict.fold(identity.services, [], fn(acc, handle, svc) { 187 + [ 188 + did.DocumentService( 189 + id: "#" <> handle, 190 + type_: svc.type_, 191 + service_endpoint: svc.url, 192 + ), 193 + ..acc 194 + ] 195 + }) 196 + 197 + did.Document( 198 + did: identity.did, 199 + also_known_as: identity.also_known_as, 200 + verification_method:, 201 + service:, 202 + ) 203 + }
+62
alicia/identity/src/alicia/identity/did.gleam
··· 1 + import gleam/dynamic/decode 2 + 3 + pub type Did = 4 + String 5 + 6 + pub type Document { 7 + Document( 8 + did: Did, 9 + also_known_as: List(String), 10 + verification_method: List(DocumentVerificationMethod), 11 + service: List(DocumentService), 12 + ) 13 + } 14 + 15 + pub fn document_decoder() -> decode.Decoder(Document) { 16 + use did <- decode.field("did", decode.string) 17 + use also_known_as <- decode.field("alsoKnownAs", decode.list(decode.string)) 18 + use verification_method <- decode.field( 19 + "verificationMethod", 20 + decode.list(document_verification_method_decoder()), 21 + ) 22 + use service <- decode.field( 23 + "service", 24 + decode.list(document_service_decoder()), 25 + ) 26 + decode.success(Document(did:, also_known_as:, verification_method:, service:)) 27 + } 28 + 29 + pub type DocumentVerificationMethod { 30 + DocumentVerificationMethod( 31 + id: String, 32 + type_: String, 33 + controller: String, 34 + public_key_multibase: String, 35 + ) 36 + } 37 + 38 + pub fn document_verification_method_decoder() -> decode.Decoder( 39 + DocumentVerificationMethod, 40 + ) { 41 + use id <- decode.field("id", decode.string) 42 + use type_ <- decode.field("type", decode.string) 43 + use controller <- decode.field("controller", decode.string) 44 + use public_key_multibase <- decode.field("publicKeyMultibase", decode.string) 45 + decode.success(DocumentVerificationMethod( 46 + id:, 47 + type_:, 48 + controller:, 49 + public_key_multibase:, 50 + )) 51 + } 52 + 53 + pub type DocumentService { 54 + DocumentService(id: String, type_: String, service_endpoint: String) 55 + } 56 + 57 + pub fn document_service_decoder() -> decode.Decoder(DocumentService) { 58 + use id <- decode.field("id", decode.string) 59 + use type_ <- decode.field("type", decode.string) 60 + use service_endpoint <- decode.field("serviceEndpoint", decode.string) 61 + decode.success(DocumentService(id:, type_:, service_endpoint:)) 62 + }
+18 -5
alicia/identity/test/alicia_identity_test.gleam
··· 1 + import alicia/identity 2 + import birdie 1 3 import gleeunit 4 + import pprint 2 5 3 6 pub fn main() -> Nil { 4 7 gleeunit.main() 5 8 } 6 9 7 - // gleeunit test functions end in `_test` 8 - pub fn hello_world_test() { 9 - let name = "Joe" 10 - let greeting = "Hello, " <> name <> "!" 10 + pub fn handle_parse_test() { 11 + identity.parse_handle("lesbian.skin") 12 + |> pprint.format 13 + |> birdie.snap(title: "Parse valid handle") 14 + } 15 + 16 + pub fn empty_handle_parse_test() { 17 + identity.parse_handle("") 18 + |> pprint.format 19 + |> birdie.snap(title: "Parse empty handle") 20 + } 11 21 12 - assert greeting == "Hello, Joe!" 22 + pub fn invalid_handle_parse_test() { 23 + identity.parse_handle("lesbian#skin") 24 + |> pprint.format 25 + |> birdie.snap(title: "Parse invalid handle") 13 26 }