Got basic subscriptions working

This commit is contained in:
iko 2023-06-11 10:50:24 +03:00
parent 89f0999d9e
commit 6964397757
Signed by untrusted user: iko
GPG Key ID: 82C257048D1026F2
16 changed files with 22421 additions and 12 deletions

7
dev/elm-live.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/sh
set -e
cd "$(dirname "$0")/../example"
nix run nixpkgs#elmPackages.elm-live -- src/Main.elm --start-page=index.html -- --output=elm.js --debug "$@"

View File

@ -10,9 +10,16 @@
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {
"TSFoster/elm-bytes-extra": "1.3.0 <= v < 1.4.0",
"chelovek0v/bbase64": "1.0.1 <= v < 2.0.0",
"elm/browser": "1.0.2 <= v < 2.0.0",
"elm/bytes": "1.0.8 <= v < 2.0.0",
"elm/core": "1.0.0 <= v < 1.0.5",
"elm-community/list-extra": "8.7.0 <= v < 9.0.0"
"elm/html": "1.0.0 <= v < 2.0.0",
"elm/http": "2.0.0 <= v < 3.0.0",
"elm/json": "1.1.3 <= v < 2.0.0",
"elm/url": "1.0.0 <= v < 2.0.0",
"elm-community/list-extra": "8.7.0 <= v < 9.0.0",
"elm-community/maybe-extra": "5.3.0 <= v < 6.0.0"
},
"test-dependencies": {
"elm-explorations/test": "2.1.1 <= v < 3.0.0"

21571
example/elm.js Normal file

File diff suppressed because it is too large Load Diff

44
example/elm.json Normal file
View File

@ -0,0 +1,44 @@
{
"type": "application",
"source-directories": [
"src",
"../src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"TSFoster/elm-bytes-extra": "1.3.0",
"chelovek0v/bbase64": "1.0.1",
"cmditch/elm-bigint": "2.0.1",
"elm/browser": "1.0.2",
"elm/bytes": "1.0.8",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/parser": "1.1.0",
"elm/url": "1.0.0",
"elm-community/list-extra": "8.7.0",
"elm-community/maybe-extra": "5.3.0",
"figbus/elm-urbit-api": "4.0.1",
"jxxcarlson/hex": "4.0.0",
"ktonon/elm-word": "2.1.2",
"mdgriffith/elm-ui": "1.1.8",
"robinheghan/murmur3": "1.0.0"
},
"indirect": {
"AdrianRibao/elm-derberos-date": "1.2.3",
"bitsoflogic/elm-radixint": "2.0.0",
"elm/file": "1.0.5",
"elm/random": "1.0.0",
"elm/regex": "1.0.0",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.3",
"rtfeldman/elm-hex": "1.0.0"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@ -0,0 +1,2 @@
var m=class extends Error{};async function A(r,e,t){let n=r.getReader(),o={done:!1,value:new Uint8Array};for(;o&&!o.done;)o=await Promise.race([n.read(),new Promise((g,i)=>{setTimeout(()=>i(new Error("getBytes timed out")),t)})]),e(o.value)}function R(r){let e,t,n,o=!1;return function(i){e===void 0?(e=i,t=0,n=-1):e=L(e,i);let s=e.length,a=0;for(;t<s;){o&&(e[t]===10&&(a=++t),o=!1);let c=-1;for(;t<s&&c===-1;++t)switch(e[t]){case 58:n===-1&&(n=t-a);break;case 13:o=!0;case 10:c=t;break}if(c===-1)break;r(e.subarray(a,c),n),a=t,n=-1}a===s?e=void 0:a!==0&&(e=e.subarray(a),t-=a)}}function P(r,e,t){let n=k(),o=new TextDecoder;return function(i,s){if(i.length===0)r?.(n),n=k();else if(s>0){let a=o.decode(i.subarray(0,s)),c=s+(i[s+1]===32?2:1),p=o.decode(i.subarray(c));switch(a){case"data":n.data=n.data?n.data+`
`+p:p;break;case"event":n.event=p;break;case"id":e?.(n.id=p);break;case"retry":let l=parseInt(p,10);isNaN(l)||t?.(n.retry=l);break}}}}function L(r,e){let t=new Uint8Array(r.length+e.length);return t.set(r),t.set(e,r.length),t}function k(){return{data:"",event:"",id:"",retry:void 0}}var b="text/event-stream",N=1e3,T="last-event-id";function q(r,{signal:e,headers:t,onopen:n,onmessage:o,onclose:g,onerror:i,openWhenHidden:s,fetch:a,responseTimeout:c,...p}){return new Promise((l,M)=>{let y={...t};y.accept||(y.accept=b);let f;function w(){f.abort(),document.hidden||E()}typeof document<"u"&&!s&&document.addEventListener("visibilitychange",w);let S=N,h;function x(){typeof document<"u"&&!s&&document.removeEventListener("visibilitychange",w),clearTimeout(h),f.abort()}e?.addEventListener("abort",()=>{x(),l()});let I=a??fetch,U=n??O,v=!1;async function E(){f=new AbortController;try{let u=await Promise.race([I(r,{...p,headers:y,signal:f.signal}),new Promise((d,C)=>{setTimeout(()=>C(new Error("fetch timed out")),c)})]);if(u.status===404)throw new m("Channel reaped");if(u.status<200||u.status>=300)throw new Error(`Invalid server response: ${u.status}`);await U(u,v),v&&(v=!1),await A(u.body,R(P(o,d=>{d?y[T]=d:delete y[T]},d=>{S=d})),c),g?.(),x(),l()}catch(u){if(!f.signal.aborted)try{v=!0;let d=i?.(u)??S;clearTimeout(h),f.abort(),h=setTimeout(E,d)}catch(d){x(),M(d)}}}E()})}function O(r){let e=r.headers.get("content-type");if(!e?.startsWith(b))throw new Error(`Expected content-type to be ${b}, Actual: ${e}`)}export{b as EventStreamContentType,q as fetchEventSource};

13
example/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="elm"></div>
<script src="/elm.js"></script>
<script type="module" src="/script.js"></script>
</body>
</html>

27
example/script.js Normal file
View File

@ -0,0 +1,27 @@
import { fetchEventSource } from "/fetch-event-source.js"
const uid = `${Math.floor(Date.now() / 1000)}-${Math.random()}`;
let app = Elm.Main.init({ node: document.getElementById("elm"), flags: { uid } });
app.ports.createEventSource.subscribe((url) => {
fetchEventSource(url, {
headers: {
Accept: 'application/x-urb-jam',
"x-channel-format": 'application/x-urb-jam',
"content-type": 'application/x-urb-jam'
},
credentials: 'include',
responseTimeout: 25000,
openWhenHidden: true,
onmessage(ev) {
console.log(ev)
app.ports.onEventSourceMessage.send({ message: ev.data });
},
onerror(err) {
console.log(err)
app.ports.onEventSourceMessage.send({ error: err });
}
});
});

80
example/src/Main.elm Normal file
View File

@ -0,0 +1,80 @@
port module Main exposing (main)
import Browser exposing (Document)
import Element exposing (..)
import Html exposing (Html)
import Json.Decode as JD
import Ur.Deconstructor as D
import Ur
import Ur.Run
import Ur.Sub
main =
Ur.Run.application
{ init =
\_ _ ->
( { error = ""
}
, Ur.logIn "http://localhost:8080" "lidlut-tabwed-pillex-ridrup"
|> Cmd.map (result (Debug.toString >> Error) (always Noop))
)
, update = update
, view = view
, subscriptions = always Sub.none
, createEventSource = createEventSource
, urbitSubscriptions = \_ -> Ur.Sub.subscribe
{
ship = "~zod", app = "journal",
path = ["updates"]
, deconstructor = D.tar |> D.map (always Noop)
}
, onEventSourceMsg = onEventSourceMessage
, onUrlChange = \_ -> Noop
, onUrlRequest = \_ -> Noop
, urbitUrl = \_ -> "http://localhost:8080"
}
type alias Model =
{ error : String
}
type Msg
= Noop
| Error String
update : Msg -> Model -> ( Model, Cmd msg )
update msg model =
case msg of
Noop ->
( model, Cmd.none )
Error err ->
( { model | error = err }, Cmd.none )
view : Model -> Document Msg
view model =
{ body =
[ layout [] (column [ centerX, centerY ] [ el [ alignTop ] (text model.error), text "Hello" ]) ]
, title = "Airlock"
}
result : (a -> c) -> (b -> c) -> Result a b -> c
result f g res =
case res of
Ok b ->
g b
Err a ->
f a
port createEventSource : String -> Cmd msg
port onEventSourceMessage : (JD.Value -> msg) -> Sub msg

View File

@ -1,7 +1,8 @@
module Urbit exposing
module Ur exposing
( Noun(..)
, cue
, jam
, logIn
, mat
, rub
)
@ -12,9 +13,26 @@ import Bitwise
import Bytes exposing (Bytes)
import Bytes.Extra as Bytes
import Dict exposing (Dict)
import Http
import List.Extra as List
logIn : String -> String -> Cmd (Result Http.Error ())
logIn root password =
Http.riskyRequest
{ url = root ++ "/~/login"
, method = "POST"
, headers = []
, timeout = Nothing
, tracker = Nothing
, body =
Http.stringBody
"application/x-www-form-urlencoded; charset=utf-8"
("password=" ++ password)
, expect = Http.expectWhatever identity
}
type Noun
= Cell ( Noun, Noun )
| Atom Bytes

View File

@ -1,4 +1,4 @@
module Urbit.Constructor exposing
module Ur.Constructor exposing
( Constructor
, cell
, cord
@ -14,7 +14,7 @@ module Urbit.Constructor exposing
import Bitwise
import Bytes exposing (Bytes, Endianness(..))
import Bytes.Encode as BE
import Urbit exposing (..)
import Ur exposing (..)
type alias Constructor a =

View File

@ -1,4 +1,4 @@
module Urbit.Deconstructor exposing
module Ur.Deconstructor exposing
( Deconstructor
, alt
, bytes
@ -25,7 +25,7 @@ import Bytes exposing (Bytes, Endianness(..))
import Bytes.Decode as BD
import Bytes.Encode as BE
import Bytes.Extra
import Urbit exposing (..)
import Ur exposing (..)
type Deconstructor a b
@ -220,9 +220,9 @@ oneOf l =
alt x (oneOf xs)
tar : Deconstructor a a
tar : Deconstructor (Noun -> a) a
tar =
Deconstructor (\_ a -> Just a)
Deconstructor (\noun f -> Just (f noun))
llec : Deconstructor a b -> Deconstructor b c -> Deconstructor a c

28
src/Ur/Phonemic.elm Normal file
View File

@ -0,0 +1,28 @@
module Ur.Phonemic exposing (..)
import BigInt exposing (toHexString)
import Bytes exposing (Bytes)
import Hex.Convert as Hex
import Urbit.Encoding.Atom exposing (toBigInt)
import Urbit.Encoding.Phonemic exposing (..)
p : String -> Maybe Bytes
p s =
case fromPatp s of
Ok atom ->
let
hexString =
toBigInt atom |> toHexString
paddedHexString =
if modBy 2 (String.length hexString) == 0 then
hexString
else
"0" ++ hexString
in
Hex.toBytes paddedHexString
Err _ ->
Nothing

377
src/Ur/Run.elm Normal file
View File

@ -0,0 +1,377 @@
module Ur.Run exposing (Model, application)
import Browser exposing (Document, UrlRequest)
import Browser.Navigation as Nav
import Dict exposing (Dict)
import Html
import Http
import Json.Decode as JD
import Maybe.Extra
import Task
import Ur exposing (Noun)
import Ur.Constructor as C
import Ur.Deconstructor as D
import Ur.Phonemic
import Ur.Sub
import Ur.Uw
import Url exposing (Url)
type alias SubDict msg =
Dict
-- (ship, app, path)
( String, String, List String )
{ deconstructor : D.Deconstructor (msg -> msg) msg
, number : Int
}
type alias Flags =
{ uid : String }
type alias Model app msg =
{ subscriptions : SubDict msg
, subscriptionIntMapping : Dict Int ( String, String, List String )
, app : app
, connected : Bool
, messageCounter : Int
, flags : Flags
, requestsToRetry : List Noun
}
type Msg msg
= AppMsg msg
| EventSourceMsg JD.Value
-- | ActivateSubscription ( String, String, List String ) Int
| FailedSubscribe Noun
| FailedUnsubscribe Noun
| Noop
| OpenConnection
| NeedsActivation
type UrbitAction
= Subscribe ( String, String, List String )
| Unsubscribe Int
| Poke { ship : String, agent : String, mark : String, noun : Noun }
renderUrbitActions : Int -> List ( UrbitAction, a ) -> ( Int, Maybe Noun, List ( Int, a ) )
renderUrbitActions idCounter acts =
let
( newIdCounter, x ) =
renderUrbitActions_ idCounter acts
in
( newIdCounter
, if List.isEmpty x then
Nothing
else
x |> List.map Tuple.first |> Maybe.Extra.values |> C.listOf identity |> Just
, x |> List.map Tuple.second
)
renderUrbitActions_ : Int -> List ( UrbitAction, a ) -> ( Int, List ( Maybe Noun, ( Int, a ) ) )
renderUrbitActions_ idCounter acts =
case acts of
[] ->
( idCounter, [] )
( act, a ) :: rest ->
renderUrbitActions_ (idCounter + 1) rest
|> Tuple.mapSecond
(\xs ->
( case act of
Subscribe ( ship, app, path ) ->
Ur.Phonemic.p ship
|> Maybe.map
(\shipAtom ->
C.cell (C.cord "subscribe") <|
C.cell (C.int idCounter) <|
C.cell (Ur.Atom shipAtom) <|
C.cell (C.cord app) (C.listOf C.cord path)
)
Unsubscribe subId ->
Just <|
C.cell (C.cord "usubscribe") <|
C.cell (C.int idCounter) (C.int subId)
Poke { ship, agent, mark, noun } ->
Ur.Phonemic.p ship
|> Maybe.map
(\shipAtom ->
C.cell (C.cord "poke") <|
C.cell (C.int idCounter) <|
C.cell (Ur.Atom shipAtom) <|
C.cell (C.cord agent) <|
C.cell (C.cord mark) <|
noun
)
, ( idCounter, a )
)
:: xs
)
application :
{ init : Url -> Nav.Key -> ( model, Cmd msg )
, view : model -> Document msg
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
, urbitSubscriptions : model -> Ur.Sub.Sub msg
, onUrlRequest : UrlRequest -> msg
, onUrlChange : Url -> msg
, createEventSource : String -> Cmd (Msg msg)
, onEventSourceMsg : (JD.Value -> Msg msg) -> Sub (Msg msg)
, urbitUrl : model -> String
}
-> Program Flags (Model model msg) (Msg msg)
application inp =
let
{ init, view, subscriptions, onUrlRequest, onUrlChange, urbitSubscriptions, createEventSource, onEventSourceMsg, urbitUrl } =
inp
in
Browser.application
{ init =
\flags u key ->
let
( app, appCmds ) =
init u key
-- urbitSubs_ =
-- urbitSubscriptions app |> (\(Ur.Sub.Sub x) -> x)
-- urbitSubs =
-- urbitSubs_
-- |> Dict.map (\_ deconstructor -> { active = Nothing, deconstructor = deconstructor })
-- ( messageCounter, actions ) =
-- subscriptionActions Dict.empty urbitSubs |> renderUrbitActions 0
in
( { subscriptions = Dict.empty
, subscriptionIntMapping = Dict.empty
, app = app
, connected = False
, messageCounter = 0
, flags = flags
, requestsToRetry = []
}
, [ Cmd.map AppMsg appCmds, pureCmd NeedsActivation ] |> Cmd.batch
)
, view =
\model ->
view model.app
|> (\{ body, title } -> { title = title, body = body |> List.map (Html.map AppMsg) })
, update = update inp
, subscriptions =
\model ->
Sub.batch
[ subscriptions model.app |> Sub.map AppMsg
, onEventSourceMsg EventSourceMsg
]
, onUrlRequest = \req -> onUrlRequest req |> AppMsg
, onUrlChange = \url -> onUrlChange url |> AppMsg
}
result : (a -> c) -> (b -> c) -> Result a b -> c
result f g res =
case res of
Ok b ->
g b
Err a ->
f a
update :
{ r
| update : msg -> app -> ( app, Cmd msg )
, createEventSource : String -> Cmd (Msg msg)
, urbitUrl : app -> String
, urbitSubscriptions : app -> Ur.Sub.Sub msg
}
-> Msg msg
-> Model app msg
-> ( Model app msg, Cmd (Msg msg) )
update inp msg model =
let
url =
inp.urbitUrl model.app ++ "/~/channel/" ++ model.flags.uid
in
case msg of
AppMsg msg_ ->
let
( appModel, appCmds ) =
inp.update msg_ model.app
urbitSubs_ =
inp.urbitSubscriptions model.app |> (\(Ur.Sub.Sub x) -> x)
urbitSubs =
urbitSubs_
|> Dict.map (\_ deconstructor -> { deconstructor = deconstructor })
( messageCounter, newSubscriptionActions, intMapping ) =
Dict.diff urbitSubs model.subscriptions
|> Dict.toList
|> List.map (\( address, _ ) -> ( Subscribe address, address ))
|> renderUrbitActions model.messageCounter
removedSubscriptions =
Dict.diff model.subscriptions urbitSubs
( messageCounter_, removedSubscriptionActions, _ ) =
removedSubscriptions
|> Dict.toList
|> List.map (\( _, { number } ) -> ( Unsubscribe number, () ))
|> renderUrbitActions messageCounter
foo =
intMapping |> List.map (\( a, b ) -> ( b, a )) |> Dict.fromList
newSubscriptions =
Dict.merge
(\_ _ x -> x)
(\key number { deconstructor } ->
Dict.insert key
{ deconstructor = deconstructor
, number = number
}
)
(\_ _ x -> x)
foo
urbitSubs
Dict.empty
in
( { model
| app = appModel
, messageCounter = messageCounter_
, subscriptions =
Dict.diff model.subscriptions removedSubscriptions
|> Dict.union newSubscriptions
, subscriptionIntMapping =
model.subscriptionIntMapping
|> Dict.union
(newSubscriptions
|> Dict.toList
|> List.map (\( key, { number } ) -> ( number, key ))
|> Dict.fromList
)
}
, Cmd.batch
[ appCmds |> Cmd.map AppMsg
, removedSubscriptionActions
|> Maybe.map
(\noun ->
sendUr
{ noun = noun
, url = url
, success = Noop
, error = FailedUnsubscribe noun
}
)
|> Maybe.withDefault Cmd.none
, newSubscriptionActions
|> Maybe.map
(\noun ->
sendUr
{ noun = noun
, url = url
, success = Noop
, error = FailedSubscribe noun
}
)
|> Maybe.withDefault Cmd.none
]
)
EventSourceMsg value ->
let
model_ =
model
in
case JD.decodeValue (JD.field "message" JD.string) value of
Ok string ->
case D.runBytes (D.cell D.int (D.cell D.cord D.tar) |> D.map (\a b c -> ( a, b, c ) |> Debug.log "event")) (Ur.Uw.decode string) of
Just ( _, "watch-ack", _ ) ->
-- Not sure what to do. Assume things are fine.
( model_, Cmd.none )
Just ( _, "poke-ack", _ ) ->
-- Not sure what to do.
( model_, Cmd.none )
Just ( subscriptionNumber, "fact", rest ) ->
case
Dict.get subscriptionNumber model.subscriptionIntMapping |> Maybe.andThen (\key -> Dict.get key model.subscriptions)
of
Just { deconstructor } ->
case D.run (D.cell D.tar deconstructor |> D.map (\_ subMsg -> subMsg)) rest of
Just subMsg ->
( model_, pureCmd (AppMsg subMsg) )
-- Got gargbage
Nothing ->
( model_, Cmd.none )
-- Got a fact for a subscription we do not hold
Nothing ->
( model_, Cmd.none )
-- got something we don't expect
_ ->
( model_, Cmd.none )
Err _ ->
case JD.decodeValue (JD.field "error" JD.value) value of
Ok _ ->
( { model_ | connected = False }, Cmd.none )
Err _ ->
-- we got garbage
( model_, Cmd.none )
NeedsActivation ->
let
( newMessageCounter, nouns, _ ) =
[ ( Poke { ship = "~zod", agent = "hood", mark = "helm-hi", noun = C.cord "Opening airlock!" }, () ) ]
|> renderUrbitActions model.messageCounter
in
( { model | messageCounter = newMessageCounter }
, nouns
|> Maybe.map (\noun -> sendUr { url = url, noun = noun, success = OpenConnection, error = NeedsActivation })
|> Maybe.withDefault Cmd.none
)
Noop ->
( model, Cmd.none )
FailedSubscribe noun ->
( { model | requestsToRetry = noun :: model.requestsToRetry }, Cmd.none )
FailedUnsubscribe noun ->
( { model | requestsToRetry = noun :: model.requestsToRetry }, Cmd.none )
OpenConnection ->
( { model | connected = True }, inp.createEventSource url )
pureCmd : msg -> Cmd msg
pureCmd msg =
Task.succeed msg |> Task.perform identity
sendUr : { url : String, error : c, success : c, noun : Noun } -> Cmd c
sendUr { url, error, success, noun } =
Http.riskyRequest
{ method = "PUT"
, headers = []
, url = url
, body = Ur.jam noun |> Ur.Uw.encode |> Http.stringBody "application/x-urb-jam"
, expect = Http.expectWhatever (result (\_ -> error) (always success))
, timeout = Nothing
, tracker = Nothing
}

28
src/Ur/Sub.elm Normal file
View File

@ -0,0 +1,28 @@
module Ur.Sub exposing (Sub(..), batch, none, subscribe)
import Dict exposing (Dict)
import Ur.Deconstructor as D
type Sub msg
= Sub
(Dict
-- key is (ship, app, path)
( String, String, List String )
(D.Deconstructor (msg -> msg) msg)
)
subscribe : { ship : String, app : String, path : List String, deconstructor : D.Deconstructor (msg -> msg) msg } -> Sub msg
subscribe { ship, app, path, deconstructor } =
Dict.singleton ( ship, app, path ) deconstructor |> Sub
none : Sub msg
none =
Sub Dict.empty
batch : List (Sub msg) -> Sub msg
batch subs =
subs |> List.map (\(Sub dict) -> dict) |> List.foldl Dict.union Dict.empty |> Sub

196
src/Ur/Uw.elm Normal file
View File

@ -0,0 +1,196 @@
module Ur.Uw exposing (decode, encode)
import BitParser as BP
import BitWriter as BW
import Bitwise
import Bytes exposing (Bytes)
import Bytes.Extra
import Dict
import List.Extra
decode : String -> Bytes
decode string =
if string == "0w0" then
Bytes.Extra.empty
else
let
chars =
string
|> String.toList
-- 0w
|> List.drop 2
go : List Char -> BW.BitWriter -> BW.BitWriter
go cs writer =
case cs of
[] ->
writer
'.' :: rest ->
go rest writer
c :: rest ->
case Dict.get c charToBits of
Just bits ->
go rest writer |> BW.bits bits
Nothing ->
go rest writer
in
BW.run (go chars BW.empty)
encode : Bytes -> String
encode bytes =
let
w =
Bytes.width bytes * 8
go () =
take w 6
|> BP.andThen
(\bits ->
if List.isEmpty bits then
BP.succeed []
else
case Dict.get (BP.bitsToInt bits) intToChar of
Nothing ->
BP.fail
Just char ->
go () |> BP.map (\chars -> char :: chars)
)
encoded =
BP.run (go ()) bytes
|> Maybe.withDefault []
|> List.Extra.dropWhileRight (\x -> x == '0')
|> dot
|> List.reverse
in
if List.isEmpty encoded then
"0w0"
else
String.fromList ('0' :: 'w' :: encoded)
dot : List Char -> List Char
dot chars =
if List.isEmpty chars then
[]
else if List.length chars <= 5 then
chars
else
List.take 5 chars ++ ('.' :: dot (List.drop 5 chars))
take : Int -> Int -> BP.BitParser (List Int)
take width n =
if n <= 0 then
BP.succeed []
else
BP.getOffset
|> BP.andThen
(\offset ->
if offset >= width then
BP.succeed []
else
BP.bit
|> BP.andThen
(\bit -> take width (n - 1) |> BP.map (\bits -> bit :: bits))
)
intToChar : Dict.Dict Int Char
intToChar =
Dict.fromList mapping
charToBits : Dict.Dict Char (List Int)
charToBits =
let
intToBits bitsLeft n =
if bitsLeft > 0 then
Bitwise.and 1 n :: intToBits (bitsLeft - 1) (Bitwise.shiftRightBy 1 n)
else
[]
in
mapping |> List.map (\( x, y ) -> ( y, intToBits 6 x )) |> Dict.fromList
mapping : List ( number, Char )
mapping =
[ ( 0, '0' )
, ( 1, '1' )
, ( 2, '2' )
, ( 3, '3' )
, ( 4, '4' )
, ( 5, '5' )
, ( 6, '6' )
, ( 7, '7' )
, ( 8, '8' )
, ( 9, '9' )
, ( 10, 'a' )
, ( 11, 'b' )
, ( 12, 'c' )
, ( 13, 'd' )
, ( 14, 'e' )
, ( 15, 'f' )
, ( 16, 'g' )
, ( 17, 'h' )
, ( 18, 'i' )
, ( 19, 'j' )
, ( 20, 'k' )
, ( 21, 'l' )
, ( 22, 'm' )
, ( 23, 'n' )
, ( 24, 'o' )
, ( 25, 'p' )
, ( 26, 'q' )
, ( 27, 'r' )
, ( 28, 's' )
, ( 29, 't' )
, ( 30, 'u' )
, ( 31, 'v' )
, ( 32, 'w' )
, ( 33, 'x' )
, ( 34, 'y' )
, ( 35, 'z' )
, ( 36, 'A' )
, ( 37, 'B' )
, ( 38, 'C' )
, ( 39, 'D' )
, ( 40, 'E' )
, ( 41, 'F' )
, ( 42, 'G' )
, ( 43, 'H' )
, ( 44, 'I' )
, ( 45, 'J' )
, ( 46, 'K' )
, ( 47, 'L' )
, ( 48, 'M' )
, ( 49, 'N' )
, ( 50, 'O' )
, ( 51, 'P' )
, ( 52, 'Q' )
, ( 53, 'R' )
, ( 54, 'S' )
, ( 55, 'T' )
, ( 56, 'U' )
, ( 57, 'V' )
, ( 58, 'W' )
, ( 59, 'X' )
, ( 60, 'Y' )
, ( 61, 'Z' )
, ( 62, '-' )
, ( 63, '~' )
]

View File

@ -9,9 +9,10 @@ import Fuzz exposing (Fuzzer)
import List.Extra as List
import Test exposing (..)
import Test.Utils exposing (..)
import Urbit exposing (..)
import Urbit.Constructor as C
import Urbit.Deconstructor as D
import Ur exposing (..)
import Ur.Constructor as C
import Ur.Deconstructor as D
import Ur.Uw
tests : Test
@ -204,6 +205,11 @@ tests =
)
)
]
, Test.describe "Ur.Uw"
[ Test.fuzz atom
"encode <-> decode"
(\bs -> bytesEq bs (bs |> Ur.Uw.encode |> Ur.Uw.decode |> stripTrailingZeros))
]
]
@ -234,7 +240,12 @@ atom : Fuzzer Bytes
atom =
bytes
|> Fuzz.map
(\a -> a |> Bytes.toByteValues |> List.dropWhileRight (\x -> x == 0) |> Bytes.fromByteValues)
stripTrailingZeros
stripTrailingZeros : Bytes -> Bytes
stripTrailingZeros =
Bytes.toByteValues >> List.dropWhileRight (\x -> x == 0) >> Bytes.fromByteValues
noun : () -> Fuzzer Noun