mirror of
https://github.com/bsima/haskell-urbit-api.git
synced 2024-10-27 02:30:46 +03:00
cleanup and more docs
This commit is contained in:
parent
23e45e0e14
commit
77868279ca
22
README.md
22
README.md
@ -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
|
||||
|
103
Urbit/Airlock.hs
103
Urbit/Airlock.hs
@ -5,10 +5,44 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
-- |
|
||||
-- Module: Urbit.Airlock
|
||||
-- Copyright: © 2020–present 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 =
|
||||
|
@ -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
|
||||
|
||||
]))
|
||||
];
|
||||
}
|
||||
|
@ -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: .
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user