cleanup and more docs

This commit is contained in:
Ben Sima 2020-10-23 10:07:02 -04:00
parent 23e45e0e14
commit 77868279ca
5 changed files with 108 additions and 45 deletions

View File

@ -7,3 +7,25 @@ running on your Urbit. You can submit commands (called "pokes") and subscribe to
responses.
See the `test.hs` file for some example usages.
## Design
The Urbit vane `eyre` is responsible for defining the API interface. The path to
the API is `/~/channel/...`, where we send messages to the global log (called
`poke`s) which are then dispatched to the appropriate apps. To receive
responses, we stream messages from a path associated with the app, such as
`/mailbox/~/~zod/mc`. Internally, I believe Urbit calls these `wire`s.
`urbit-airlock` handles most of the path, session, and HTTP request stuff
automatically. See the haddocks for more details.
This library is built on req, conduit, and aeson, all of which are very stable
and usable libraries for working with HTTP requests and web data.
## TODO
- fix test suite on travis (OOM when trying to compile urbit)
- more sophisticated test cases, also use cabal test instead of homegrown thing
- add an exe that wraps the library with a cli
- port to ghcjs
- put some examples in the docs

View File

@ -5,10 +5,44 @@
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- |
-- Module: Urbit.Airlock
-- Copyright: © 2020present Ben Sima
-- License: MIT
--
-- Maintainer: Ben Sima <ben@bsima.me>
-- Stability: experimental
-- Portability: non-portableo
--
-- === About the Urbit API
--
-- The "Urbit Airlock" API is a command-query API that lets you hook into apps
-- running on your Urbit. You can submit commands and subscribe to responses.
--
-- The Urbit vane @eyre@ is responsible for defining the API interface. The HTTP
-- path to the API is @\/~\/channel\/...@, where we send messages to the global
-- log (called @poke@s) which are then dispatched to the appropriate apps. To
-- receive responses, we stream messages from a path associated with the app,
-- such as @\/mailbox\/~\/~zod\/mc@. Internally, I believe Urbit calls these
-- @wire@s.
--
-- === About this library
--
-- This library helps you talk to your Urbit from Haskell, via HTTP. It handles
-- most of the path, session, and HTTP request stuff automatically. You'll need
-- to know what app and mark (data type) to send to, which path/wire listen to,
-- and the shape of the message. The latter can be found in the Hoon source
-- code, called the @vase@ on the poke arm.
--
-- This library is built on req, conduit, and aeson, all of which are very
-- stable and usable libraries for working with HTTP requests and web data.
-- Released under the MIT License, same as Urbit.
module Urbit.Airlock
( Ship (..),
App,
Mark,
( -- * Types
Ship (..),
Session,
-- * Functions
connect,
poke,
ack,
@ -19,7 +53,6 @@ where
import Conduit (ConduitM, runConduitRes, (.|))
import qualified Conduit
import qualified Control.Exception as Exception
import Control.Lens ()
import Data.Aeson ((.=))
import qualified Data.Aeson as Aeson
import Data.ByteString (ByteString)
@ -33,16 +66,16 @@ import qualified Text.URI as URI
-- | Some information about your ship needed to establish connection.
data Ship = Ship
{ -- | A random string for your channel.
{ -- | A random string for your channel
uid :: Text,
-- | The `@p` of your ship.
name :: ShipName,
-- | Track the latest event we saw (needed for poking).
-- | The @\@p@ of your ship
name :: Text,
-- | Track the latest event we saw (needed for poking)
lastEventId :: Int,
-- | Network access point, with port if necessary. Like
-- 'https://sampel-palnet.arvo.network', or 'http://localhost:8080'.
-- | Network access point, with port if necessary, like
-- @https://sampel-palnet.arvo.network@, or @http://localhost:8080@
url :: Text,
-- | Login code, `+code` in the dojo. Don't share this publically.
-- | Login code, @+code@ in the dojo. Don't share this publically
code :: Text
}
deriving (Show)
@ -50,18 +83,10 @@ data Ship = Ship
channelUrl :: Ship -> Text
channelUrl Ship {url, uid} = url <> "/~/channel/" <> uid
type App = Text
type Path = Text
type Mark = Text
-- | The `@p` for the ship (no leading ~).
type ShipName = Text
nextEventId :: Ship -> Int
nextEventId Ship {lastEventId} = lastEventId + 1
-- | A wrapper type for the session cookies.
type Session = HTTP.CookieJar
-- | Connect and login to the ship.
@ -75,22 +100,24 @@ connect ship =
where
body = "password" =: (code ship)
con (url, opts) =
Req.req Req.POST url (Req.ReqBodyUrlEnc body) Req.ignoreResponse $
Req.req Req.POST url (Req.ReqBodyUrlEnc body) Req.bsResponse $
opts
-- | Poke a ship.
poke ::
Aeson.ToJSON a =>
-- | Session cookie from 'connect'
Session ->
-- | Your ship
Ship ->
-- | To what ship will you send the poke?
ShipName ->
-- | Which gall application are you trying to poke?
App ->
-- | What mark should be applied to the data you are sending?
Mark ->
-- | Name of the ship to poke
Text ->
-- | Name of the gall application you want to poke
Text ->
-- | The mark of the message you are sending
Text ->
a ->
IO Req.IgnoreResponse
IO Req.BsResponse
poke sess ship shipName app mark json =
Req.useURI <$> (URI.mkURI $ channelUrl ship) >>= \case
Nothing -> error "could not parse ship url"
@ -103,7 +130,7 @@ poke sess ship shipName app mark json =
Req.POST
url
(Req.ReqBodyJson body)
Req.ignoreResponse
Req.bsResponse
$ opts <> Req.cookieJar sess
body =
[ Aeson.object
@ -117,7 +144,14 @@ poke sess ship shipName app mark json =
]
-- | Acknowledge receipt of a message. (This clears it from the ship's queue.)
ack :: Session -> Ship -> Int -> IO Req.IgnoreResponse
ack ::
-- | Session cookie from 'connect'
Session ->
-- | Your ship
Ship ->
-- | The event number
Int ->
IO Req.BsResponse
ack sess ship eventId =
Req.useURI <$> (URI.mkURI $ channelUrl ship) >>= \case
Nothing -> error "could not parse ship url"
@ -130,7 +164,7 @@ ack sess ship eventId =
Req.POST
url
(Req.ReqBodyJson body)
Req.ignoreResponse
Req.bsResponse
$ opts <> Req.cookieJar sess
body =
[ Aeson.object
@ -144,11 +178,14 @@ instance Req.MonadHttp (ConduitM i o (Conduit.ResourceT IO)) where
-- | Subscribe to ship events on some path.
subscribe ::
-- | Session cookie from 'connect'
Session ->
-- | Your ship
Ship ->
Path ->
-- | The path to subscribe to.
Text ->
-- | A handler conduit to receive the response from the server, e.g.
-- 'Data.Conduit.Binary.sinkFile "my-file.out"'.
-- @Data.Conduit.Binary.sinkFile "my-file.out"@
ConduitM ByteString Conduit.Void (Conduit.ResourceT IO) a ->
IO a
subscribe sess ship path fn =

View File

@ -4,9 +4,12 @@ nixpkgs.mkShell {
name = "urbit-airlock-shell";
buildInputs = [
nixpkgs.ormolu.bin
(nixpkgs.pkgs.haskell.packages.${compiler}.ghcWithPackages (hp: with hp; [
aeson base bytestring conduit conduit-extra http-client lens
modern-uri req req-conduit text uuid wai wai-extra
aeson base bytestring conduit conduit-extra http-client modern-uri
req req-conduit text uuid
]))
];
}

View File

@ -1,7 +1,11 @@
name: urbit-airlock
version: 0.1.0.0
-- synopsis:
-- description:
synopsis: Talk to Urbit from Haskell
description:
@urbit-airlock@ is a Haskell library that helps you connect to the Urbit
API.
.
Built on req, conduit, and aeson for stability and simplicity.
homepage: https://github.com/bsima/urbit-airlock
license: BSD3
license-file: LICENSE
@ -24,14 +28,11 @@ library
conduit,
conduit-extra,
http-client,
lens,
modern-uri,
req,
req-conduit,
text,
uuid,
wai,
wai-extra
uuid
-- executable urlock
-- hs-source-dirs: .

View File

@ -1,15 +1,15 @@
{ mkDerivation, aeson, base, bytestring, conduit, conduit-extra
, http-client, lens, modern-uri, req, req-conduit, stdenv, text
, uuid, wai, wai-extra
, http-client, modern-uri, req, req-conduit, stdenv, text, uuid
}:
mkDerivation {
pname = "urbit-airlock";
version = "0.1.0.0";
src = ./.;
libraryHaskellDepends = [
aeson base bytestring conduit conduit-extra http-client lens
modern-uri req req-conduit text uuid wai wai-extra
aeson base bytestring conduit conduit-extra http-client modern-uri
req req-conduit text uuid
];
homepage = "https://github.com/bsima/urbit-airlock";
description = "Talk to Urbit from Haskell";
license = stdenv.lib.licenses.bsd3;
}