Compare commits

...

74 Commits

Author SHA1 Message Date
Emery Hemingway 3a0bd1cd02 Make a proper default.nix, cleanup lockfile 2024-05-10 12:08:51 +02:00
Emery Hemingway cda940cf75 Update syndicate dependency 2024-05-09 18:31:20 +02:00
Emery Hemingway bf0b5d6b86 Add http_client 2024-05-09 18:31:20 +02:00
Emery Hemingway b3a417a072 Replace "var Turn" with "Turn" 2024-04-30 13:56:48 +02:00
Emery Hemingway 48408d2763 Use new dataspace patterns 2024-04-23 10:25:03 +02:00
Emery Hemingway 494418540a Update syndicate library 2024-04-09 10:14:58 +01:00
Emery Hemingway e954fdefec SQL: decompose SQL statements and assert errors 2024-04-09 10:14:38 +01:00
Emery Hemingway de7683b467 Add http driver to README 2024-04-08 14:10:34 +01:00
Emery Hemingway 5e075f3a0c msg: add assertion mode 2024-04-07 18:17:18 +01:00
Emery Hemingway e2b96e39ef Do not specify "src" in Direnv Nix expression 2024-04-07 18:17:18 +01:00
Emery Hemingway d40c29ecad Add http_driver from syndicate-nim 2024-04-07 18:17:18 +01:00
Emery Hemingway 1f099d6bd2 Move actors with shared libraries out of syndesizer 2024-04-07 18:17:15 +01:00
Emery Hemingway 920cd28c89 json_socket_translator: TCP socket support 2024-04-07 18:15:28 +01:00
Emery Hemingway fc9762eb87 Move json_messages.nim to schema 2024-04-07 18:15:28 +01:00
Emery Hemingway 2b80be0fcf Add base64 decoder 2024-04-07 18:15:28 +01:00
Emery Hemingway c9b38dd86e Syndicate API update 2024-04-07 18:15:27 +01:00
Emery Hemingway 242bda24e5 Remove actors broken by CPS migration 2024-04-07 18:02:58 +01:00
Emery Hemingway aa8ff4c364 Update build metadata 2024-02-09 15:53:48 +00:00
Emery Hemingway 4f2e19b0b2 Add XSLT processor 2024-02-09 15:24:45 +00:00
Emery Hemingway 1827c91da0 Preserves: #! is now #: 2024-02-08 15:50:19 +00:00
Emery Hemingway 40ad6a2dbc Preserves: floats and doubles merged 2024-02-08 15:49:34 +00:00
Emery Hemingway 25d1e40990 SQLite logging 2024-02-05 23:00:09 +01:00
Emery Hemingway 89f23f14f5 Use new relays interface 2024-02-05 23:00:01 +01:00
Emery Hemingway f072525dd4 syndesizer: add XML translator 2024-01-20 15:29:43 +02:00
Emery Hemingway 2d3189288f SQL: don't label results, send tuples to a continuation 2024-01-18 12:58:52 +02:00
Emery Hemingway ff1f1ac44b syndesizer: assert SQL results to a seperate cap 2024-01-16 21:08:59 +02:00
Emery Hemingway 028df08d66 Check modules before building sydesizer 2024-01-14 21:52:19 +02:00
Emery Hemingway 48ce4ac7e0 Add pulse actor 2024-01-14 21:43:20 +02:00
Emery Hemingway 119d89ff1c syndesizer/sqlite_actor: move out compile conditional 2024-01-14 12:58:15 +02:00
Emery Hemingway 29b19c711c Add PostgreSQL actor 2024-01-14 12:58:15 +02:00
Emery Hemingway c89a9a333a syndesizer: add file-system-usage 2024-01-14 12:58:12 +02:00
Emery Hemingway 9d0246bb1b syndesizer/websockets: fix connection retraction 2024-01-09 16:28:33 +02:00
Emery Hemingway 2268b75096 syndesizer: absorb sqlite_actor 2024-01-09 14:01:38 +02:00
Emery Hemingway d8965a398a syndesizer: absorb websockets actor 2024-01-09 14:01:38 +02:00
Emery Hemingway eeaa80db88 syndesizer: absorb json_translator 2024-01-09 14:01:38 +02:00
Emery Hemingway 7ed62e0482 syndesizer: absorb cache_actor 2024-01-09 14:01:38 +02:00
Emery Hemingway 3c050e242e syndesizer: absorb json_socket_translator 2024-01-09 14:01:38 +02:00
Emery Hemingway ad3b160e56 Cleanup and document syndesizer/webhooks 2024-01-09 14:01:38 +02:00
Emery Hemingway da8d7ea345 syndesizer: add timer actor 2024-01-09 13:30:01 +02:00
Emery Hemingway bba515cdd8 Refactor for Syndicate API update 2024-01-09 13:30:00 +02:00
Emery Hemingway 0d789361d6 webhooks: parse JSON request bodies 2024-01-08 14:31:42 +02:00
Emery Hemingway 92dbdbe438 Add syndesizer and a basic webhook endpoint 2023-12-14 22:04:19 +02:00
Emery Hemingway f64ffaf188 Update build metadata 2023-11-30 11:14:56 +02:00
Emery Hemingway b9f6f5c24d syndump: process multiple patterns 2023-11-30 10:35:25 +02:00
Emery Hemingway 3c8b485d32 Add mount_actor
Synit utility for mounting file systems.
2023-11-29 23:13:18 +02:00
Emery Hemingway 72f5d04030 Add cache_actor 2023-11-08 15:12:36 +00:00
Emery Hemingway 86be2a67c2 Add rofi_script_actor 2023-10-26 17:20:43 +01:00
Emery Hemingway aea55969bb Syndicate API update 2023-10-21 19:12:05 +01:00
Emery Hemingway d5bdd3baf9 Cleanup imports 2023-10-21 19:08:43 +01:00
Emery Hemingway e700953551 Add inotify_actor 2023-10-10 19:37:30 +01:00
Emery Hemingway 1e0fa6c2a3 Fixup build system 2023-10-10 19:36:29 +01:00
Emery Hemingway aeee1f8dd3 Add lockfile 2023-10-09 16:08:58 +01:00
Emery Hemingway 8cd4f61792 Use new syndicate imports 2023-10-05 18:05:06 +01:00
Emery Hemingway d7dd513c97 json_translator: publish and message to initial dataspace 2023-09-05 14:23:32 +02:00
Emery Hemingway 3e5f791d70 syndump: only text patterns 2023-08-25 20:11:24 +01:00
Emery Hemingway c08ed920b5 Add mintsturdyref
Formally a utility hidden in the syndicate/capabilities module.
2023-08-25 10:14:13 +01:00
Emery Hemingway fe7b0f469d Rename syndicat to syndump 2023-08-25 09:37:11 +01:00
Emery Hemingway 4315c05ef3 Add syndicat utility 2023-08-24 09:20:52 +01:00
Emery Hemingway ef38792970 Update for Nim-2.0.0 2023-08-16 10:57:30 +01:00
Emery Hemingway 58280baabe Add syndex_card 2023-08-01 10:33:49 +01:00
Emery Hemingway 2303339a26 Use different JSON schema 2023-07-26 14:25:58 +01:00
Emery Hemingway c65fcc27ed json_translater: catch better 2023-07-26 10:12:08 +01:00
Emery Hemingway 818c23f025 Discard actor from bootDataspace 2023-07-01 16:06:18 +01:00
Emery Hemingway 83e26352dc Document the net_mapper 2023-07-01 16:06:18 +01:00
Emery Hemingway cc495cf834 Add lost net_mapper code 2023-07-01 16:06:16 +01:00
Emery Hemingway 35779abe5a Better build system 2023-07-01 16:05:46 +01:00
Emery Hemingway df421db071 msg: make the SturdyRef overrideable with $SYNDICATE_STEP 2023-07-01 16:05:46 +01:00
Emery Hemingway e898bc62db Add preserve_process_environment 2023-07-01 16:05:45 +01:00
Emery Hemingway 2a1a3fe200 Adjust to Syndicate-nim changes 2023-05-18 12:41:52 +01:00
Emery Hemingway 8c322d1efd msg: parse command line as text Preserves 2023-05-17 21:06:36 +01:00
Emery Hemingway 3a3fc0db2d Add <connected "…"> assertion to json_socket_translator 2023-05-16 18:44:10 +01:00
Emery Hemingway 2fe54f44bb Remove http_translator
There is better HTTP schema defined elsewhere.
See syndicate-protocols/schemas/http.prs.
2023-05-07 09:22:47 +01:00
Emery Hemingway fe76b714a5 Use runActor proc 2023-05-06 20:08:52 +01:00
Emery Hemingway 9ec580ff79 Use <recv-json …> and <send-json …> schema 2023-05-05 18:15:53 +01:00
55 changed files with 2420 additions and 308 deletions

2
.envrc
View File

@ -1,2 +1,2 @@
source_env ..
use flake syndicate#syndicate_utils
use nix

7
.gitignore vendored
View File

@ -1,5 +1,2 @@
/.direnv
http_translator
json_translator
msg
json_socket_translator
/nim.cfg
*.check

445
README.md
View File

@ -1,19 +1,448 @@
# Syndicate utils
## http_translator
## Syndesizer
Dispatches HTTP requests to registered handlers.
A Syndicate multitool that includes a number of different actors that become active via configuration.
See [http_translator.config-example.pr](./http_translator.config-example.pr) for an example configuration.
Think of it as a Busybox for Syndicate, if Busybox was created before POSIX.
## json_translator
Whether you use a single instance for many protocols or many specialized instances is up to you.
Wrapper that executes a command, parses its JSON output, and asserts a Preserves conversion in an `<output …>` record.
### Cache
## json_socket_translator
Observes patterns and reässert the values captured for a given lifetime. Takes the argument `<cache { dataspace: #!any lifetime: float }>`. The lifetime of a cache counts down from moment a value is asserted.
Utility to communicate with sockets that send and receive lines of JSON. Compatible with [mpv](https://mpv.io/), see [mpv.config-example.pr](./mpv.config-example.pr).
Example configuration:
```
? <nixspace ?nixspace> [
; Require the nix_actor during observations.
?nixspace> ? <Observe <rec eval _> _> [
$config <require-service <daemon nix_actor>> ]
?nixspace> ? <Observe <rec realise _> _> [
$config <require-service <daemon nix_actor>> ]
; Cache anything captured by observers in the $nixspace for an hour.
; The nix_actor is not required during caching.
$config <require-service <daemon syndesizer>>
$config ? <service-object <daemon syndesizer> ?cap> [
$cap <cache { dataspace: $nixspace lifetime: 3600.0 }> ]
]
```
### File System Usage
Summarize the size of file-system directory. Equivalent to `du -s -b`.
Query the size of a directory in bytes by observing `<file-system-usage "/SOME/PATH" ?size>`.
```
# Configuration example
? <exposed-dataspace ?ds> [
<require-service <daemon syndesizer>>
? <service-object <daemon syndesizer> ?cap> [
$cap <file-system-usage { dataspace: $ds }>
]
]
```
### HTTP driver
Experimental HTTP server that services requests using [some version](https://git.syndicate-lang.org/syndicate-lang/syndicate-protocols/src/commit/9864ce0ec86fb2f916c2aab318a1e6994ab8834c/schemas/http.prs) of the http Syndicate protocol schema.
```
# Configuration example
let ?not-found = dataspace
$not-found ? <request _ ?res> [
$res ! <status 503 "Service unavailable">
$res ! <done "No binding here.">
]
let ?greeting = dataspace
$greeting ? <request _ ?res> [
$res ! <status 200 "ok">
$res ! <chunk "Hello world">
$res ! <done "!">
]
let ?http = dataspace
$http [
<http-bind #f 80 get [ ] $not-found>
<http-bind #f 80 get [|...|] $not-found>
<http-bind #f 80 get ["hello"] $greeting>
]
? <service-object <daemon http-driver> ?cap> [
$cap <http-driver { dataspace: $http }>
]
<daemon http-driver {
argv: [ "/bin/syndesizer" ]
clearEnv: #t
protocol: application/syndicate
}>
<require-service <daemon http-driver>>
```
### JSON Socket Translator
Communicate with sockets that send and receive lines of JSON using `<send …>` and `<recv …>` messages.
Do not send messages into the dataspace configure with `<json-socket-translator …>` until `<connected @socketPath string>` is asserted.
```
# MPV configuration example
<require-service <daemon mpv-server>>
<daemon mpv-server {
argv: [
"/run/current-system/sw/bin/mpv"
"--really-quiet"
"--idle=yes"
"--no-audio-display"
"--input-ipc-server=/run/user/1000/mpv.sock"
"--volume=75"
]
protocol: none
}>
let ?mpvSpace = dataspace
? <service-state <daemon mpv-server> ready> [
<require-service <daemon syndesizer>>
? <service-object <daemon syndesizer> ?cap> [
$cap <json-socket-translator {
dataspace: $mpvSpace
socket: <unix "/run/user/1000/mpv.sock">
}>
]
]
$mpvSpace [
# announce the dataspace when the translator is connected
? <connected $socketPath> [
$config <mpv $mpvSpace>
$config <bind <ref { oid: "mpv" key: #x"" }> $mpvSpace #f>
]
# translate <play-file > to an MPV command
?? <play-file ?file> [
! <send { "command": ["loadfile" $file "append-play"] }>
]
# clear the playlist on idle so it doesn't grow indefinitely
?? <recv {"event": "idle"}> [
! <send { "command": ["playlist-clear"] }>
]
]
```
### JSON Stdio Translator
Executes a command, parses its JSON output, converts to record `<recv @jsonData any>`, and publishes and messages it to a dataspace.
```
# Configuration example
<require-service <daemon syndesizer>>
let ?ds = dataspace
<bind <ref {oid: "syndicate" key: #x""}> $ds #f>
? <service-object <daemon syndesizer> ?cap> [
$cap <json-stdio-translator {
argv: [
"yt-dlp"
"--dump-json"
"https://youtu.be/RR9GkEXDvog"
]
dataspace: $ds
}>
]
```
### Pulse proxy
A proxy actor that passes assertions and messages to a configured capability but only asserts observations on a a periodic pulse.
This can be used to implement polling behavior.
```
# Example config
let ?ds = dataspace
<require-service <daemon syndesizer>>
? <service-object <daemon syndesizer> ?cap> [
$cap <pulse {dataspace: $ds}>
]
$ds ? <pulse 3600.0 ?proxy> [
$proxy ? <assertion-updated-hourly ?value> [
$log ! <log "-" {assertion-updated-hourly: $value}>
]
]
```
### SQLite
Readonly access to SQLite databases. Asserts rows as records in response to SQL query assertions. Dynamic updates are not implemented.
Can be disabled by passing `--define:withSqlite=no` to the Nim compiler.
```
# Configuration example
<require-service <daemon syndesizer>>
let ?sqlspace = dataspace
? <service-object <daemon syndesizer> ?cap> [
$cap <sqlite {
dataspace: $sqlspace
database: "/var/db/example.db"
}>
]
let ?tuplespace = dataspace
$sqlspace <query "SELECT id, name FROM stuff" $tuplespace>
$tuplespace [
? [?id ?name] [
$log ! <log "-" { row: <example-row $id $name> }>
]
? <sqlite-error ?msg ?ctx> [
$log ! <log "-" { msg: $msg ctx: $ctx }>
]
]
```
### XML translator
Translates between Preserves and XML according to the [Conventions for Common Data Types](https://preserves.dev/conventions.html).
Examples:
- `<xml-translation "<foo a=\"1\"> <bar>hello world!</bar></foo>" <foo {"a": 1}<bar "hello world!">>>`
- `<xml-translation "" [#t #f]>`
- `<xml-translation "<<</>>" #f>`
```
# Configuration example
? <sharedspace ?ds> [
$ds ? <Observe <rec xml-translation _> _> $config [
$config <require-service <daemon syndesizer>>
$config ? <service-object <daemon syndesizer> ?cap> [
$cap <xml-translator { dataspace: $ds }>
]
]
]
```
---
## http_client
The inverse of `http-driver`.
### Caveats
- HTTPS is assumed unless the request is to port 80.
- If the request or response sets `Content-Type` to `application/json` or `…/preserves`
the body will be a parsed Preserves value.
- No cache support.
- Internal errors propagate using a `400 Internal client error` response.
Sample Syndicate server script:
```
# A top-level dataspace
let ?ds = dataspace
# A dataspace for handling the HTTP response.
let ?response = dataspace
$response [
?? <done { "code": "EUR" "exchange_middle": ?middle } > [
$ds <exchange EUR RSD $middle>
]
]
$ds [
<request
# Request Euro to Dinar exchange rate.
<http-request 0 "kurs.resenje.org" 443
get ["api" "v1" "currencies" "eur" "rates" "today"]
{Content-Type: "application/json"} {} #f
>
$response
>
# Log all assertions.
? ?any [
$log ! <log "-" { assertion: $any }>
]
]
? <service-object <daemon http-client> ?cap> [
$cap <http-client {
dataspace: $ds
}>
]
<require-service <daemon http-client>>
? <built http-client ?path ?sum> [
<daemon http-client {
argv: [ "/bin/http_client" ]
clearEnv: #t
protocol: application/syndicate
}>
]
```
## mintsturdyref
A utility for minting [Sturdyrefs](https://synit.org/book/operation/builtin/gatekeeper.html#sturdyrefs).
## mount_actor
Actor for mounting filesystems on Linux.
Sample Syndicate server script:
```
# Assert a file-system we want to mount.
<mount "/dev/sda3" "/boot" "vfat">
# Transform mount assertions into mount status observations.
? <mount ?source ?target ?fs> [
? <mount $source $target $fs _> [ ]
]
# Assert mounting succeded.
? <mount _ ?target _ #t> [
<service-state <mountpoint $target> ready>
]
# Assert mount failed.
? <mount _ ?target _ <failure _>> [
<service-state <mountpoint $target> failed>
]
# Assert the details into the machine dataspace.
? <machine-dataspace ?machine> [
$config ? <mount ?source ?target ?fs ?status> [
$machine <mount $source $target $fs $status>
]
]
# Require the mount_actor daemon.
<require-service <daemon mount_actor>>
<daemon mount_actor {
argv: ["/home/emery/src/bin/mount_actor"]
protocol: application/syndicate
}>
# Pass the daemon the config dataspace.
? <service-object <daemon mount_actor> ?cap> [
$cap { dataspace: $config }
]
```
## msg
A utility that sends a message to $SYNDICATE_SOCK in the form `<ARGV…>`.
A utility that parses its command-line arguments as Preserves and send them as messages to `$SYNDICATE_ROUTE`.
When called as `assert` (by a symlink or a rename) it will make assertions instead.
## PostgreSQL
Readonly access to PostgreSQL databases. Asserts rows as records in response to SQL query assertions. Dynamic updates are not implemented.
Can be disabled by passing `--define:withPostgre=no` to the Nim compiler.
```
# Configuration example
<require-service <daemon postgre_actor>>
let ?sqlspace = dataspace
? <service-object <daemon postgre_actor> ?cap> [
$cap <postgre {
dataspace: $sqlspace
connection: [
["host" "example.com"]
["dbname" "foobar"]
["user" "hackme"]
]
}>
]
let ?tuplespace = dataspace
$sqlspace <query "SELECT id, name FROM stuff" $tuplespace>
$tuplespace ? [?id ?name] [
$log ! <log "-" { row: <example-row $id $name> }>
]
```
## preserve_process_environment
This utility serializes it's process environment to Preserves and prints it to stdout.
It can be used to feed the environment variables of a nested child of the Syndicate server back to the server. For example, to retreive the environmental variables that a desktop manager passed on to its children.
## SQLite
Readonly access to SQLite databases. Asserts rows as records in response to SQL query assertions. Dynamic updates are not implemented.
Can be disabled by passing `--define:withSqlite=no` to the Nim compiler.
```
# Configuration example
<require-service <daemon sqlite_actor>>
let ?sqlspace = dataspace
? <service-object <daemon sqlite_actor> ?cap> [
$cap <sqlite {
dataspace: $sqlspace
database: "/var/db/example.db"
}>
]
let ?tuplespace = dataspace
$sqlspace <query "SELECT id, name FROM stuff" $tuplespace>
$tuplespace ? [?id ?name] [
$log ! <log "-" { row: <example-row $id $name> }>
]
```
## syndump
Utility for printing assertions and messages. Parses the command-line arguments as a pattern, connects a dataspace via `$SYNDICATE_ROUTE`, and writes observations to standard-output. Published assertions are prefixed by the `+` character, retractions by `-`, and messages by `!`.
Example
```sh
# Print patterns in use, filter down with AWK to only the published patterns.
$ FS=':' syndump '<Observe ? _>' | awk -F : '/^+/ { print $2 }'
```
## XSLT processor
Perform XML stylesheet transformations. For a given textual XSLT stylesheet and a textual XML document generate an abstract XML document in Preserves form. Inputs may be XML text or paths to XML files.
```
# Configuration example
let ?ds = dataspace
$ds [
? <xslt-transform "/stylesheet.xls" "/doc.xml" ?output> [
? <xml-translation ?text $output> [
$log ! <log "-" { xslt-output: $text }>
]
]
]
<require-service <daemon xslt_actor>>
? <service-object <daemon xslt_actor> ?cap> $cap [
<xml-translator { dataspace: $ds }>
<xslt { dataspace: $ds }>
]
```

3
Tupfile Normal file
View File

@ -0,0 +1,3 @@
include_rules
: |> !nim_lk |> {lockfile}
: {lockfile} |> !nim_cfg |> | ./<lock>

View File

@ -1,2 +1,4 @@
include ../syndicate-nim/depends.tup
NIM = $(DIRENV) $(NIM)
NIM_FLAGS += --path:$(TUP_CWD)/../syndicate-nim/src
NIM_GROUPS += $(TUP_CWD)/<lock>

14
assertions.prs Normal file
View File

@ -0,0 +1,14 @@
version 1.
FileSystemUsage = <file-system-usage @path string @size int>.
# This assertion publishes a dataspace that proxies assertions with
# an exception for <Observe …> which is pulsed every periodSec.
# The pulse resolution is no more than one millisecond.
Pulse = <pulse @periodSec float @proxy #:any>.
XmlTranslation = <xml-translation @xml string @pr any>.
XsltTransform = <xslt-transform @stylesheet string @input string @output any>.
XsltItems = [XsltItem ...].
XsltItem = string.

4
base64.prs Normal file
View File

@ -0,0 +1,4 @@
version 1.
Base64Text = <base64 @txt string @bin bytes> .
Base64File = <base64-file @txt string @path string @size int> .

View File

@ -1,3 +1,76 @@
version 1 .
embeddedType EntityRef.Cap .
JsonSocket = <json-socket @label symbol @path string> .
Base64DecoderArguments = <base64-decoder {
dataspace: #:any
}>.
CacheArguments = <cache {
dataspace: #:any
lifetime: float
}>.
FileSystemUsageArguments = <file-system-usage {
dataspace: #:any
}>.
JsonTranslatorArguments = <json-stdio-translator {
argv: [string ...]
dataspace: #:any
}>.
JsonTranslatorConnected = <connected @address SocketAddress>.
TcpAddress = <tcp @host string @port int>.
UnixAddress = <unix @path string>.
SocketAddress = TcpAddress / UnixAddress .
HttpClientArguments = <http-client {
dataspace: #:any
}>.
HttpDriverArguments = <http-driver {
dataspace: #:any
}>.
JsonSocketTranslatorArguments = <json-socket-translator {
dataspace: #:any
socket: SocketAddress
}>.
PostgreArguments = <postgre {
connection: [PostgreConnectionParameter ...]
dataspace: #:any
}>.
PostgreConnectionParameter = [@key string @val string].
PulseArguments = <pulse {
dataspace: #:any
}>.
SqliteArguments = <sqlite {
database: string
dataspace: #:any
}>.
WebhooksArguments = <webhooks {
endpoints: {[string ...]: #:any ...:...}
listen: Tcp
}>.
WebsocketArguments = <websocket {
dataspace: #:any
url: string
}>.
XmlTranslatorArguments = <xml-translator {
dataspace: #:any
}>.
XsltArguments = <xslt {
dataspace: #:any
}>.
# Reused from syndicate-protocols/transportAddress
Tcp = <tcp @host string @port int>.

34
default.nix Normal file
View File

@ -0,0 +1,34 @@
{
pkgs ? import <nixpkgs> { },
}:
let
inherit (pkgs)
lib
buildNimPackage
fetchFromGitea
libxml2
libxslt
openssl
pkg-config
postgresql
sqlite
;
in
buildNimPackage {
pname = "syndicate_utils";
version = "unstable";
src = if lib.inNixShell then null else lib.cleanSource ./.;
buildInputs = [
postgresql.out
sqlite
libxml2
libxslt
openssl
];
lockFile = ./lock.json;
}

View File

@ -1,22 +0,0 @@
version 1 .
Method = =GET / =HEAD / =POST / =PUT / =DELETE / =CONNECT / =OPTIONS / =TRACE / =PATCH .
Methods = #{Method} .
; A URL path split into elements
Path = [string ...] .
Listener = <listen @port int> .
; Register an entity that will handle requests at path prefix.
; TODO: assert the public base URL of the handler to the entity.
Handler = <handler @methods Methods @path Path @entity #!any> .
Headers = {string: [string ...] ...:...} .
; A request awaiting a response at handle.
; TODO: query parameters
Request = <http @handle int @method Method @headers Headers @path Path @body string> .
; A response to handle.
Response = <http @handle int @code int @headers Headers @body string> .

View File

@ -1,20 +0,0 @@
<require-service <daemon http_translator>>
<daemon http_translator {
argv: "/home/repo/syndicate/syndicate_utils/src/http_translator"
protocol: text/syndicate
}>
let ?other = dataspace
$other [
? <http ?handle GET ?headers ?path ?body> [
<http $handle 200 {} "get handler invoked">
]
]
? <service-object <daemon http_translator> ?cap> $cap [
<listen 8888>
; publish GET requests with prefix "/foo/bar" to other dataspace
handler #{GET} ["foo" "bar" ] $other>
]

3
inotify_actor.prs Normal file
View File

@ -0,0 +1,3 @@
version 1 .
InotifyMessage = <inotify @path string @event symbol @cookie int @name string> .

142
lock.json Normal file
View File

@ -0,0 +1,142 @@
{
"depends": [
{
"method": "fetchzip",
"packages": [
"cps"
],
"path": "/nix/store/8gbhwni0akqskdb3qhn5nfgv6gkdz0vz-source",
"rev": "c90530ac57f98a842b7be969115c6ef08bdcc564",
"sha256": "0h8ghs2fqg68j3jdcg7grnxssmllmgg99kym2w0a3vlwca1zvr62",
"srcDir": "",
"url": "https://github.com/ehmry/cps/archive/c90530ac57f98a842b7be969115c6ef08bdcc564.tar.gz"
},
{
"method": "fetchzip",
"packages": [
"getdns"
],
"path": "/nix/store/x9xmn7w4k6jg8nv5bnx148ibhnsfh362-source",
"rev": "c73cbe288d9f9480586b8fa87f6d794ffb6a6ce6",
"sha256": "1sbgx2x51szr22i72n7c8jglnfmr8m7y7ga0v85d58fwadiv7g6b",
"srcDir": "src",
"url": "https://git.sr.ht/~ehmry/getdns-nim/archive/c73cbe288d9f9480586b8fa87f6d794ffb6a6ce6.tar.gz"
},
{
"method": "fetchzip",
"packages": [
"hashlib"
],
"path": "/nix/store/fav82xdbicvlk34nmcbl89zx99lr3mbs-source",
"rev": "f9455d4be988e14e3dc7933eb7cc7d7c4820b7ac",
"sha256": "1sx6j952lj98629qfgr7ds5aipyw9d6lldcnnqs205wpj4pkcjb3",
"srcDir": "",
"url": "https://github.com/ehmry/hashlib/archive/f9455d4be988e14e3dc7933eb7cc7d7c4820b7ac.tar.gz"
},
{
"method": "fetchzip",
"packages": [
"preserves"
],
"path": "/nix/store/hzb7af7lbd4kgd5y4hbgxv1lswig36yj-source",
"rev": "fd498c6457cb9ad2f3179daa40da69eec00326dd",
"sha256": "182xvw04vjw83mlcrkwkip29b44h0v8dapg2014k9011h90mdsj4",
"srcDir": "src",
"url": "https://git.syndicate-lang.org/ehmry/preserves-nim/archive/fd498c6457cb9ad2f3179daa40da69eec00326dd.tar.gz"
},
{
"method": "fetchzip",
"packages": [
"stew"
],
"path": "/nix/store/mqg8qzsbcc8xqabq2yzvlhvcyqypk72c-source",
"rev": "3c91b8694e15137a81ec7db37c6c58194ec94a6a",
"sha256": "17lfhfxp5nxvld78xa83p258y80ks5jb4n53152cdr57xk86y07w",
"srcDir": "",
"url": "https://github.com/status-im/nim-stew/archive/3c91b8694e15137a81ec7db37c6c58194ec94a6a.tar.gz"
},
{
"method": "fetchzip",
"packages": [
"syndicate"
],
"path": "/nix/store/dw30cq9gxz3353zgaq4a36ajq6chvbwc-source",
"rev": "3a4dc1f13392830b587138199643d30fdbec8541",
"sha256": "1mbd17rjm1fsx7d0ckzyjih2nzdjqs52ck9wscqcg9nvf3ib5mvh",
"srcDir": "src",
"url": "https://git.syndicate-lang.org/ehmry/syndicate-nim/archive/3a4dc1f13392830b587138199643d30fdbec8541.tar.gz"
},
{
"method": "fetchzip",
"packages": [
"sys"
],
"path": "/nix/store/syhxsjlsdqfap0hk4qp3s6kayk8cqknd-source",
"rev": "4ef3b624db86e331ba334e705c1aa235d55b05e1",
"sha256": "1q4qgw4an4mmmcbx48l6xk1jig1vc8p9cq9dbx39kpnb0890j32q",
"srcDir": "src",
"url": "https://github.com/ehmry/nim-sys/archive/4ef3b624db86e331ba334e705c1aa235d55b05e1.tar.gz"
},
{
"method": "fetchzip",
"packages": [
"taps"
],
"path": "/nix/store/6y14ia52kr7jyaa0izx37mlablmq9s65-source",
"rev": "8c8572cd971d1283e6621006b310993c632da247",
"sha256": "1dp166bv9x773jmfqppg5i3v3rilgff013vb11yzwcid9l7s3iy8",
"srcDir": "src",
"url": "https://git.sr.ht/~ehmry/nim_taps/archive/8c8572cd971d1283e6621006b310993c632da247.tar.gz"
},
{
"date": "2024-04-02T15:38:57+01:00",
"deepClone": false,
"fetchLFS": false,
"fetchSubmodules": true,
"hash": "sha256-iZb9aAgYr4FGkqfIg49QWiCqeizIi047kFhugHiP8o0=",
"leaveDotGit": false,
"method": "git",
"packages": [
"solo5_dispatcher"
],
"path": "/nix/store/sf5dgj2ljvahcm6my7d61ibda51vnrii-solo5_dispatcher",
"rev": "a7a894a96a2221284012800e6fd32923d83d20bd",
"sha256": "13gjixw80vjqj0xlx2y85ixal82sa27q7j57j9383bqq11lgv5l9",
"srcDir": "pkg",
"url": "https://git.sr.ht/~ehmry/solo5_dispatcher"
},
{
"method": "fetchzip",
"packages": [
"bigints"
],
"path": "/nix/store/jvrm392g8adfsgf36prgwkbyd7vh5jsw-source",
"rev": "86ea14d31eea9275e1408ca34e6bfe9c99989a96",
"sha256": "15pcpmnk1bnw3k8769rjzcpg00nahyrypwbxs88jnwr4aczp99j4",
"srcDir": "src",
"url": "https://github.com/ehmry/nim-bigints/archive/86ea14d31eea9275e1408ca34e6bfe9c99989a96.tar.gz"
},
{
"method": "fetchzip",
"packages": [
"nimcrypto"
],
"path": "/nix/store/h7lgq3by9mx8in03vzh0y964lnnlkalp-source",
"rev": "ff6afc6a753bd645cad4568472c7733d1715e31e",
"sha256": "0h9vpayp66pg66114bl0nsvlv1nzp7f0x5b35gbsbd7svzlcz5zj",
"srcDir": "",
"url": "https://github.com/cheatfate/nimcrypto/archive/ff6afc6a753bd645cad4568472c7733d1715e31e.tar.gz"
},
{
"method": "fetchzip",
"packages": [
"npeg"
],
"path": "/nix/store/xpn694ibgipj8xak3j4bky6b3k0vp7hh-source",
"rev": "ec0cc6e64ea4c62d2aa382b176a4838474238f8d",
"sha256": "1fi9ls3xl20bmv1ikillxywl96i9al6zmmxrbffx448gbrxs86kg",
"srcDir": "src",
"url": "https://github.com/zevv/npeg/archive/ec0cc6e64ea4c62d2aa382b176a4838474238f8d.tar.gz"
}
]
}

5
mountpoints.prs Normal file
View File

@ -0,0 +1,5 @@
version 1.
Mountpoint = <mount @source string @target string @type string @status Status> .
Status = Failure / @success #t .
Failure = <failure @msg string> .

View File

@ -1,28 +1,56 @@
let ?beepSpace = dataspace
<bind "syndicate" #x"" $beepSpace>
let ?socketPath = "/run/user/1000/mpv.sock"
<require-service <daemon mpv>>
<daemon mpv {
argv: ["/run/current-system/sw/bin/mpv"
"--input-ipc-server=/run/user/1000/mpv.sock"
"--idle=yes"
let ?mpvSpace = dataspace
$mpvSpace [
; announce the dataspace when the translator is connected
? <connected $socketPath> [
$config <mpv $mpvSpace>
]
; translate <play-file …> to an MPV command
?? <play-file ?file> [
$log ! <log "-" { line: <play-file $file> }>
! <send-json { "command": ["loadfile" $file "append-play"] }>
]
; log anything that comes back from MPV
; ?? <recv-json ?js> [ $log ! <log "-" { mpv: $js }> ]
; clear the playlist on idle so it doesn't grow indefinitely
?? <recv-json {"event": "idle"}> [
! <send-json { "command": ["playlist-clear"] }>
]
]
; need the translator and the translator needs the daemon
<require-service <daemon mpv-translator>>
<depends-on <daemon mpv-translator> <service-state <daemon mpv-server> ready>>
? <service-object <daemon mpv-translator> ?cap> [
$cap {
dataspace: $mpvSpace
socket: $socketPath
}
]
; assert and retract the daemon as the daemon is built (this is a testing artifact)
? <built json_socket_translator ?path ?sum> [
<daemon mpv-translator {
argv: [$path]
protocol: application/syndicate
env: {BUILD_SUM: $sum}
}>
]
; start mpv regardless
<daemon mpv-server {
argv: [
"/run/current-system/sw/bin/mpv"
"--really-quiet"
"--idle=yes"
"--no-audio-display"
"--input-ipc-server=/run/user/1000/mpv.sock"
]
protocol: none
}>
<require-service <daemon mpv-translator>>
<daemon mpv-translator {
argv: "/home/repo/syndicate/syndicate_utils/src/json_socket_translator"
protocol: text/syndicate
}>
? <service-object <daemon mpv-translator> ?mpvSpace> [
$mpvSpace <json-socket mpv $socketPath>
$beepSpace ?? <beep ?code> [
$mpvSpace ! <mpv 1 { "command": ["loadfile" "/tmp/beep.ogg"] }>
]
]

2
net_mapper.prs Normal file
View File

@ -0,0 +1,2 @@
version 1.
RoundTripTime = <rtt @address string @minimum float @average float @maximum float>.

5
rofi.prs Normal file
View File

@ -0,0 +1,5 @@
version 1.
Environment = { symbol: string ...:... } .
Select = <rofi-select @option string @environment Environment> .
Options = <rofi-options @options [string ...]> .

8
sql.prs Normal file
View File

@ -0,0 +1,8 @@
version 1 .
# When asserted the actor reponds to @target rows as records
# of the given label and row columns as record fields.
Query = <query @statement [any ...] @target #:any> .
# When a query fails this is asserted instead.
SqlError = <sql-error @msg string @context string>.

View File

@ -1,2 +1,5 @@
include_rules
: foreach *.nim | $(SYNDICATE_PROTOCOL) ./<schema> |> !nim_bin |>
: foreach *.nim | $(SYNDICATE_PROTOCOL) ./<schema> |> !nim_bin |> {bin}
: foreach {bin} |> !assert_built |>
: $(BIN_DIR)/msg |> !symlink |> $(BIN_DIR)/beep
: $(BIN_DIR)/msg |> !symlink |> $(BIN_DIR)/assert

91
src/http_client.nim Normal file
View File

@ -0,0 +1,91 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
# TODO: write a TAPS HTTP client. Figure out how to externalise TLS.
import
std/[httpclient, options, streams, strutils, tables, uri],
pkg/taps,
pkg/preserves,
pkg/syndicate, pkg/syndicate/protocols/http,
./schema/config
proc url(req: HttpRequest): Uri =
result.scheme = if req.port == 80: "http" else: "https"
result.hostname = req.host.present
result.port = $req.port
for i, p in req.path:
if 0 < i: result.path.add '/'
result.path.add p.encodeUrl
for key, vals in req.query:
if result.query.len > 0:
result.query.add '&'
result.query.add key.string.encodeUrl
for i, val in vals:
if i == 0: result.query.add '='
elif i < vals.high: result.query.add ','
result.query.add val.string.encodeUrl
proc bodyString(req: HttpRequest): string =
if req.body.orKind == RequestBodyKind.present:
return cast[string](req.body.present)
proc spawnHttpClient*(turn: Turn; root: Cap): Actor {.discardable.} =
during(turn, root, ?:HttpClientArguments) do (ds: Cap):
spawn("http-client", turn) do (turn: Turn):
during(turn, ds, HttpContext.grabType) do (ctx: HttpContext):
let peer = ctx.res.unembed(Cap).get
var client = newHttpClient()
try:
var
headers = newHttpHeaders()
contentType = ""
for key, val in ctx.req.headers:
if key == Symbol"Content-Type":
contentType = val
client.headers[key.string] = val
let stdRes = client.request(
ctx.req.url,
ctx.req.method.string.toUpper,
ctx.req.bodyString, headers
)
var resp = HttpResponse(orKind: HttpResponseKind.status)
resp.status.code = stdRes.status[0 .. 2].parseInt
resp.status.message = stdRes.status[3 .. ^1]
message(turn, peer, resp)
resp = HttpResponse(orKind: HttpResponseKind.header)
for key, val in stdRes.headers:
if key == "Content-Type":
contentType = val
resp.header.name = key.Symbol
resp.header.value = val
message(turn, peer, resp)
case contentType
of "application/json", "text/preserves":
message(turn, peer,
initRecord("done", stdRes.bodyStream.readAll.parsePreserves))
of "application/preserves":
message(turn, peer,
initRecord("done", stdRes.bodyStream.decodePreserves))
else:
resp = HttpResponse(orKind: HttpResponseKind.done)
resp.done.chunk.string = stdRes.bodyStream.readAll()
message(turn, peer, resp)
except CatchableError as err:
var resp = HttpResponse(orKind: HttpResponseKind.status)
resp.status.code = 400
resp.status.message = "Internal client error"
message(turn, peer, resp)
resp = HttpResponse(orKind: HttpResponseKind.done)
resp.done.chunk.string = err.msg
message(turn, peer, resp)
client.close()
do:
client.close()
when isMainModule:
import syndicate/relays
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
spawnHttpClient(turn, ds)

1
src/http_client.nim.cfg Normal file
View File

@ -0,0 +1 @@
define:ssl

View File

@ -1,92 +0,0 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[asyncdispatch, asynchttpserver, strutils, tables, uri]
import preserves
import syndicate, syndicate/actors
import ./schema/http_protocol
func toHttpCore(methods: Methods): set[HttpMethod] =
# Convert the schema type to the type in the httpcore module.
for m in methods:
result.incl(
case m
of Method.GET: HttpGET
of Method.HEAD: HttpHEAD
of Method.POST: HttpPOST
of Method.PUT: HttpPUT
of Method.DELETE: HttpDELETE
of Method.CONNECT: HttpCONNECT
of Method.OPTIONS: HttpOPTIONS
of Method.TRACE: HttpTRACE
of Method.PATCH: HttpPATCH)
proc splitPath(u: Uri): Path =
u.path.strip(chars = {'/'}).split("/")
proc hitch(a, b: Future[void]) =
a.addCallback do (f: Future[void]):
if f.failed: fail(b, f.error)
else: complete(b)
bootDataspace("main") do (ds: Ref; turn: var Turn):
connectStdio(ds, turn)
var
handlers: Table[seq[string], (Ref, set[HttpMethod])]
pathPrefixMaxLen = 0
requestIdSource = 0
during(turn, ds, ?Handler) do (methods: Methods; path: seq[string]; entity: Ref):
handlers[path] = (entity, methods.toHttpCore)
pathPrefixMaxLen = max(pathPrefixMaxLen, path.len)
do:
handlers.del(path)
pathPrefixMaxLen = 0
for path in handlers.keys:
pathPrefixMaxLen = max(pathPrefixMaxLen, path.len)
var parentFacet = turn.facet
proc handleRequest(req: asynchttpserver.Request): Future[void] =
# TODO: use pattern matching
var
entity: Ref
methods: set[HttpMethod]
path = req.url.splitPath()
block:
var prefix = path[0..min(pathPrefixMaxLen.succ, path.high)]
while entity.isNil:
(entity, methods) = handlers.getOrDefault(prefix)
if prefix.len == 0: break
else: discard prefix.pop()
if entity.isNil:
result = req.respond(Http503, "no handler registered for this path")
else:
var parentFut = newFuture[void]("handleRequest")
result = parentFut
if req.reqMethod notin methods:
result = req.respond(Http405, "method not valid for this handler")
else:
run(parentFacet) do (turn: var Turn):
inc requestIdSource
let
rId = requestIdSource
let rHandle = publish(turn, entity, http_protocol.Request(
handle: rId,
`method`: Method.GET,
headers: req.headers.table[],
path: path,
body: req.body))
onPublish(turn, entity, Response ? { 0: ?rId, 1: ?int, 3: ?string }) do (code: HttpCode, body: string):
req.respond(code, body).addCallback do (fut: Future[void]):
run(parentFacet) do (turn: var Turn): retract(turn, rHandle)
hitch(fut, parentFut)
during(turn, ds, ?Listener) do (port: Port):
var http = newAsyncHttpServer()
asyncCheck serve(http, port, handleRequest)
do:
close(http)
runForever()

View File

@ -1,33 +0,0 @@
# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[asyncdispatch, asyncnet, json]
from std/nativesockets import AF_UNIX, SOCK_STREAM, Protocol
import preserves, preserves/jsonhooks, syndicate, syndicate/patterns
import ./schema/config
bootDataspace("main") do (ds: Ref; turn: var Turn):
connectStdio(ds, turn)
during(turn, ds, ?config.JsonSocket) do (label: Assertion, socketPath: string):
let socket = newAsyncSocket(
domain = AF_UNIX,
sockType = SOCK_STREAM,
protocol = cast[Protocol](0),
buffered = false)
waitFor connectUnix(socket, socketPath)
onMessage(turn, ds, recordPattern(label, ?toPreserve(1), grab())) do (data: Assertion):
var js: JsonNode
if fromPreserve(js, data):
asyncCheck socket.send($js & "\n")
let f = turn.facet
proc processIncoming(fut: Future[string]) {.gcsafe.} =
var data = fut.read.parseJson.toPreserve(Ref)
run(f) do (turn: var Turn):
message(turn, ds, initRecord(label, 0.toPreserve(Ref), data))
socket.recvLine.addCallback(processIncoming)
socket.recvLine.addCallback(processIncoming)
runForever()

View File

@ -1,40 +0,0 @@
# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[asyncdispatch, json, os, osproc, strutils]
import preserves # , preserves/jsonhooks,
import syndicate
from preserves/jsonhooks import toPreserveHook
proc runChild: string =
let params = commandLineParams()
if params.len < 1:
stderr.writeLine "not enough parameters"
quit 1
let
cmd = params[0]
args = params[1..params.high]
try: result = execProcess(command=cmd, args=args, options={poUsePath})
except:
stderr.writeLine "execProcess failed"
quit 1
if result == "":
stderr.writeLine "no ouput"
quit 1
proc translate(output: string): Assertion =
var js: JsonNode
try: js = parseJson output
except:
stderr.writeLine "parseJson failed"
quit 1
js.toPreserveHook(Ref)
bootDataspace("main") do (root: Ref; turn: var Turn):
connectStdio(root, turn)
discard publish(turn, root,
initRecord[Ref]("output", runChild().translate()))
runForever()

44
src/mintsturdyref.nim Normal file
View File

@ -0,0 +1,44 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
from os import commandLineParams
import preserves, syndicate/capabilities, syndicate/protocols/sturdy
const usage = """
mintsturdyref OID < SECRET_KEY
Mint Sturdyrefs using a sixteen-byte secret key read from stdin using OIDs
passed as command-line parameters.
Example:
mintsturdyref '"syndicate"' < /dev/null
See:
https://synit.org/book/operation/builtin/gatekeeper.html#sturdyrefs
https://synit.org/book/glossary.html?highlight=oid#oid
"""
proc main =
var oids: seq[Value]
for p in commandLineParams():
case p
of "-h", "--help", "?":
quit(usage)
else:
add(oids, parsePreserves p)
if oids.len == 0:
stderr.writeLine """using the "syndicate" OID"""
oids.add(toPreserves "syndicate")
var key: array[16, byte]
case readBytes(stdin, key, 0, 16)
of 16: discard
of 0: stderr.writeLine "using null key"
else: quit "expected sixteen bytes of key from stdin"
for oid in oids:
let sturdy = mint(key, oid)
doAssert validate(key, sturdy)
stdout.writeLine(sturdy)
main()

53
src/mount_actor.nim Normal file
View File

@ -0,0 +1,53 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
## An actor for Linux file-system mounting.
when not defined(linux):
{.error: "this component only tested for Linux".}
import std/oserrors
import preserves, preserves/sugar
import syndicate
import ./schema/mountpoints
type BootArgs {.preservesDictionary.} = object
dataspace: Cap
proc mount(source, target, fsType: cstring; flags: culong; data: pointer): cint {.importc, header: "<sys/mount.h>".}
## `mount(2)`
proc umount(target: cstring): cint {.importc, header: "<sys/mount.h>".}
## `umount(2)`
proc spawnMountActor*(turn: Turn; ds: Cap): Actor {.discardable.} =
spawnActor(turn, "mount_actor") do (turn: Turn):
let
targetPat = observePattern(!Mountpoint, { @[%1]: grabLit() })
sourcePat = observePattern(!Mountpoint, {
@[%0]: grabLit(),
@[%2]: grabLit(),
})
during(turn, ds, ?:BootArgs) do (ds: Cap):
during(turn, ds, targetPat) do (target: string):
during(turn, ds, sourcePat) do (source: string, fsType: string):
var mountpoint = Mountpoint(
source: source,
target: target,
`type`: fsType,
)
var rc = mount(source, target, fsType, 0, nil)
if rc == 0:
mountpoint.status = Status(orKind: StatusKind.success)
else:
mountpoint.status = Status(orKind: StatusKind.Failure)
mountpoint.status.failure.msg = osErrorMsg(osLastError())
discard publish(turn, ds, mountpoint)
do:
discard umount(target)
when isMainModule:
import syndicate/relays
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
discard spawnMountActor(turn, ds)

View File

@ -1,19 +1,22 @@
# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[asyncdispatch, os]
import preserves, syndicate, syndicate/capabilities
import std/[sequtils, os, strutils]
import preserves, syndicate, syndicate/relays
proc unixSocketPath: string =
result = getEnv("SYNDICATE_SOCK")
if result == "":
result = getEnv("XDG_RUNTIME_DIR", "/run/user/1000") / "dataspace"
setControlCHook(proc () {.noconv.} = quit())
bootDataspace("main") do (root: Ref; turn: var Turn):
let label = getAppFilename().extractFilename
connectUnix(turn, unixSocketPath(), capabilities.mint()) do (turn: var Turn; ds: Ref):
var a = initRecord[Ref](label, commandLineParams().toPreserve(Ref))
message(turn, ds, a)
for i in 0..7: poll(20)
# A hack to exit
runActor("msg") do (turn: Turn):
let
data = map(commandLineParams(), parsePreserves)
cmd = paramStr(0).extractFilename.normalize
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
case cmd
of "assert":
for e in data:
publish(turn, ds, e)
else: # "msg"
for e in data:
message(turn, ds, e)
sync(turn, ds) do (turn: Turn):
stopActor(turn)

158
src/postgre_actor.nim Normal file
View File

@ -0,0 +1,158 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import preserves, syndicate
import ./schema/[config, sql]
{.passL: "-lpq".}
{.pragma: libpq, header: "libpq-fe.h", importc.}
type
Oid = cuint
PGconn {.libpq.} = ptr object
PGresult {.libpq.} = ptr object
ConnStatusType {.libpq.} = enum
CONNECTION_OK, CONNECTION_BAD, ## Non-blocking mode only below here
##
## The existence of these should never be relied upon - they should only
## be used for user feedback or similar purposes.
##
CONNECTION_STARTED, ## Waiting for connection to be made.
CONNECTION_MADE, ## Connection OK; waiting to send.
CONNECTION_AWAITING_RESPONSE, ## Waiting for a response from the
## postmaster.
CONNECTION_AUTH_OK, ## Received authentication; waiting for
## backend startup.
CONNECTION_SETENV, ## This state is no longer used.
CONNECTION_SSL_STARTUP, ## Negotiating SSL.
CONNECTION_NEEDED, ## Internal state: connect() needed
CONNECTION_CHECK_WRITABLE, ## Checking if session is read-write.
CONNECTION_CONSUME, ## Consuming any extra messages.
CONNECTION_GSS_STARTUP, ## Negotiating GSSAPI.
CONNECTION_CHECK_TARGET, ## Checking target server properties.
CONNECTION_CHECK_STANDBY ## Checking if server is in standby mode.
ExecStatusType = enum
PGRES_EMPTY_QUERY = 0, ## empty query string was executed
PGRES_COMMAND_OK, ## a query command that doesn't return
## anything was executed properly by the
## backend
PGRES_TUPLES_OK, ## a query command that returns tuples was
## executed properly by the backend, PGresult
## contains the result tuples
PGRES_COPY_OUT, ## Copy Out data transfer in progress
PGRES_COPY_IN, ## Copy In data transfer in progress
PGRES_BAD_RESPONSE, ## an unexpected response was recv'd from the
## backend
PGRES_NONFATAL_ERROR, ## notice or warning message
PGRES_FATAL_ERROR, ## query failed
PGRES_COPY_BOTH, ## Copy In/Out data transfer in progress
PGRES_SINGLE_TUPLE, ## single tuple from larger resultset
PGRES_PIPELINE_SYNC, ## pipeline synchronization point
PGRES_PIPELINE_ABORTED ## Command didn't run because of an abort
## earlier in a pipeline
proc PQconnectdbParams(
keywords: cstringArray; values: cstringArray; expand_dbname: cint): PGconn {.libpq.}
proc PQerrorMessage(conn: PGconn): cstring {.libpq.}
proc PQfinish(conn: PGconn) {.libpq.}
proc PQstatus(conn: PGconn): ConnStatusType {.libpq.}
proc PQexec(conn: PGconn; query: cstring): PGresult {.libpq.}
proc PQresultStatus(res: PGresult): ExecStatusType {.libpq.}
proc PQresStatus (status: ExecStatusType): cstring {.libpq.}
proc PQresultErrorMessage(res: PGresult): cstring {.libpq.}
proc PQclear(res: PGresult) {.libpq.}
proc PQntuples(res: PGresult): cint {.libpq.}
proc PQnfields(res: PGresult): cint {.libpq.}
proc PQgetvalue(res: PGresult; tup_num: cint; field_num: cint): cstring {.libpq.}
proc PQftype(res: PGresult; field_num: cint): Oid {.libpq.}
proc PQfsize(res: PGresult; field_num: cint): cint {.libpq.}
# proc PQsocket(conn: PGconn): cint
# proc PQconnectStartParams(
# keywords: cstringArray; values: cstringArray; expand_dbname: cint): PGconn
# TODO: async
proc checkPointer(p: pointer) =
if p.isNil: raise newException(OutOfMemDefect, "Postgres returned nil")
type StringPairs = seq[tuple[key: string, val: string]]
proc splitParams(params: StringPairs): (cstringArray, cstringArray) =
var strings = newSeq[string](params.len)
for i, _ in params: strings[i] = params[i][0]
result[0] = allocCStringArray(strings)
for i, _ in params: strings[i] = params[i][1]
result[1] = allocCStringArray(strings)
proc renderSql(tokens: openarray[Value]): string =
for token in tokens:
if result.len > 0: result.add ' '
case token.kind
of pkSymbol:
result.add token.symbol.string
of pkString:
result.add '\''
result.add token.string
result.add '\''
of pkFloat, pkRegister, pkBigInt:
result.add $token
of pkBoolean:
if token.bool: result.add '1'
else: result.add '0'
else:
return ""
proc spawnPostgreActor*(turn: Turn; root: Cap): Actor {.discardable.} =
spawn("postgre", turn) do (turn: Turn):
during(turn, root, ?:PostgreArguments) do (params: StringPairs, ds: Cap):
var
conn: PGconn
statusHandle: Handle
(keys, vals) = splitParams(params)
conn = PQconnectdbParams(keys, vals, 0)
checkPointer(conn)
let
status = PQstatus(conn)
msg = $PQerrorMessage(conn)
statusHandle = publish(turn, ds,
initRecord("status", toSymbol($status), msg.toPreserves))
if status == CONNECTION_OK:
during(turn, ds, ?:Query) do (statement: seq[Value], target: Cap):
var text = renderSql statement
if text == "":
discard publish(turn, ds, SqlError(msg: "invalid statement", context: $statement))
else:
var
res = PQexec(conn, text)
st = PQresultStatus(res)
if st == PGRES_TUPLES_OK or st == PGRES_SINGLE_TUPLE:
let tuples = PQntuples(res)
let fields = PQnfields(res)
if tuples > 0 and fields > 0:
for r in 0..<tuples:
var tupl = initSequence(fields)
for f in 0..<fields:
tupl[f] = toPreserves($PQgetvalue(res, r, f))
discard publish(turn, target, tupl)
else:
discard publish(turn, ds, SqlError(
msg: $PQresStatus(st),
context: $PQresultErrorMessage(res),
))
PQclear(res)
else:
stderr.writeLine "refusing to do anything when status is ", status
do:
deallocCStringArray(keys)
deallocCStringArray(vals)
PQfinish(conn)
when isMainModule:
import syndicate/relays
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
spawnPostgreActor(turn, ds)

View File

@ -0,0 +1,21 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[tables, os]
import preserves
type ProcessInfo {.preservesDictionary.} = object
program: string
argv: seq[string]
env: Table[string, string]
dir: string
proc main =
var info: ProcessInfo
info.program = getAppFilename()
info.argv = commandLineParams()
for key, val in envPairs(): info.env[key] = val
info.dir = getCurrentDir()
writeLine(stdout, info.toPreserves)
main()

31
src/rofi_script_actor.nim Normal file
View File

@ -0,0 +1,31 @@
# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway
# SPDX-License-Identifier: Unlicense
## See the rofi-script(5) manpage for documentation.
import std/[cmdline, envvars, strutils, tables]
import preserves, syndicate, syndicate/relays
import ./schema/rofi
if getEnv("ROFI_OUTSIDE") == "":
quit("run this program in rofi")
runActor("rofi_script_actor") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
case paramCount()
of 0:
let pat = ?:Options
onPublish(turn, ds, pat) do (options: seq[string]):
stdout.writeLine options.join("\n")
quit()
of 1:
var select = Select(option: commandLineParams()[0])
for (key, val) in envPairs():
if key.startsWith "ROFI_":
select.environment[Symbol key] = val
message(turn, ds, select)
sync(turn, ds, stopActor)
else:
quit("rofi passed an unexpected number of arguments")

31
src/schema/assertions.nim Normal file
View File

@ -0,0 +1,31 @@
import
preserves
type
XsltItems* = seq[XsltItem]
Pulse* {.preservesRecord: "pulse".} = object
`periodSec`*: float
`proxy`* {.preservesEmbedded.}: Value
XsltItem* = string
XmlTranslation* {.preservesRecord: "xml-translation".} = object
`xml`*: string
`pr`*: Value
FileSystemUsage* {.preservesRecord: "file-system-usage".} = object
`path`*: string
`size`*: BiggestInt
XsltTransform* {.preservesRecord: "xslt-transform".} = object
`stylesheet`*: string
`input`*: string
`output`*: Value
proc `$`*(x: XsltItems | Pulse | XsltItem | XmlTranslation | FileSystemUsage |
XsltTransform): string =
`$`(toPreserves(x))
proc encode*(x: XsltItems | Pulse | XsltItem | XmlTranslation | FileSystemUsage |
XsltTransform): seq[byte] =
encode(toPreserves(x))

19
src/schema/base64.nim Normal file
View File

@ -0,0 +1,19 @@
import
preserves
type
Base64File* {.preservesRecord: "base64-file".} = object
`txt`*: string
`path`*: string
`size`*: BiggestInt
Base64Text* {.preservesRecord: "base64".} = object
`txt`*: string
`bin`*: seq[byte]
proc `$`*(x: Base64File | Base64Text): string =
`$`(toPreserves(x))
proc encode*(x: Base64File | Base64Text): seq[byte] =
encode(toPreserves(x))

View File

@ -1,14 +1,165 @@
import
std/typetraits, preserves
preserves, std/tables
type
JsonSocket* {.preservesRecord: "json-socket".} = object
`label`*: Symbol
WebsocketArgumentsField0* {.preservesDictionary.} = object
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
`url`*: string
WebsocketArguments* {.preservesRecord: "websocket".} = object
`field0`*: WebsocketArgumentsField0
HttpClientArgumentsField0* {.preservesDictionary.} = object
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
HttpClientArguments* {.preservesRecord: "http-client".} = object
`field0`*: HttpClientArgumentsField0
JsonTranslatorArgumentsField0* {.preservesDictionary.} = object
`argv`*: seq[string]
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
JsonTranslatorArguments* {.preservesRecord: "json-stdio-translator".} = object
`field0`*: JsonTranslatorArgumentsField0
SocketAddressKind* {.pure.} = enum
`TcpAddress`, `UnixAddress`
`SocketAddress`* {.preservesOr.} = object
case orKind*: SocketAddressKind
of SocketAddressKind.`TcpAddress`:
`tcpaddress`*: TcpAddress
of SocketAddressKind.`UnixAddress`:
`unixaddress`*: UnixAddress
Base64DecoderArgumentsField0* {.preservesDictionary.} = object
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
Base64DecoderArguments* {.preservesRecord: "base64-decoder".} = object
`field0`*: Base64DecoderArgumentsField0
JsonTranslatorConnected* {.preservesRecord: "connected".} = object
`address`*: SocketAddress
JsonSocketTranslatorArgumentsField0* {.preservesDictionary.} = object
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
`socket`*: SocketAddress
JsonSocketTranslatorArguments* {.preservesRecord: "json-socket-translator".} = object
`field0`*: JsonSocketTranslatorArgumentsField0
XsltArgumentsField0* {.preservesDictionary.} = object
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
XsltArguments* {.preservesRecord: "xslt".} = object
`field0`*: XsltArgumentsField0
HttpDriverArgumentsField0* {.preservesDictionary.} = object
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
HttpDriverArguments* {.preservesRecord: "http-driver".} = object
`field0`*: HttpDriverArgumentsField0
WebhooksArgumentsField0* {.preservesDictionary.} = object
`endpoints`*: Table[seq[string], EmbeddedRef]
`listen`*: Tcp
WebhooksArguments* {.preservesRecord: "webhooks".} = object
`field0`*: WebhooksArgumentsField0
FileSystemUsageArgumentsField0* {.preservesDictionary.} = object
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
FileSystemUsageArguments* {.preservesRecord: "file-system-usage".} = object
`field0`*: FileSystemUsageArgumentsField0
SqliteArgumentsField0* {.preservesDictionary.} = object
`database`*: string
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
SqliteArguments* {.preservesRecord: "sqlite".} = object
`field0`*: SqliteArgumentsField0
TcpAddress* {.preservesRecord: "tcp".} = object
`host`*: string
`port`*: BiggestInt
CacheArgumentsField0* {.preservesDictionary.} = object
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
`lifetime`*: float
CacheArguments* {.preservesRecord: "cache".} = object
`field0`*: CacheArgumentsField0
XmlTranslatorArgumentsField0* {.preservesDictionary.} = object
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
XmlTranslatorArguments* {.preservesRecord: "xml-translator".} = object
`field0`*: XmlTranslatorArgumentsField0
PostgreConnectionParameter* {.preservesTuple.} = object
`key`*: string
`val`*: string
PostgreArgumentsField0* {.preservesDictionary.} = object
`connection`*: seq[PostgreConnectionParameter]
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
PostgreArguments* {.preservesRecord: "postgre".} = object
`field0`*: PostgreArgumentsField0
PulseArgumentsField0* {.preservesDictionary.} = object
`dataspace`* {.preservesEmbedded.}: EmbeddedRef
PulseArguments* {.preservesRecord: "pulse".} = object
`field0`*: PulseArgumentsField0
UnixAddress* {.preservesRecord: "unix".} = object
`path`*: string
proc `$`*(x: JsonSocket): string =
`$`(toPreserve(x))
Tcp* {.preservesRecord: "tcp".} = object
`host`*: string
`port`*: BiggestInt
proc encode*(x: JsonSocket): seq[byte] =
encode(toPreserve(x))
proc `$`*(x: WebsocketArguments | HttpClientArguments | JsonTranslatorArguments |
SocketAddress |
Base64DecoderArguments |
JsonTranslatorConnected |
JsonSocketTranslatorArguments |
XsltArguments |
HttpDriverArguments |
WebhooksArguments |
FileSystemUsageArguments |
SqliteArguments |
TcpAddress |
CacheArguments |
XmlTranslatorArguments |
PostgreConnectionParameter |
PostgreArguments |
PulseArguments |
UnixAddress |
Tcp): string =
`$`(toPreserves(x))
proc encode*(x: WebsocketArguments | HttpClientArguments |
JsonTranslatorArguments |
SocketAddress |
Base64DecoderArguments |
JsonTranslatorConnected |
JsonSocketTranslatorArguments |
XsltArguments |
HttpDriverArguments |
WebhooksArguments |
FileSystemUsageArguments |
SqliteArguments |
TcpAddress |
CacheArguments |
XmlTranslatorArguments |
PostgreConnectionParameter |
PostgreArguments |
PulseArguments |
UnixAddress |
Tcp): seq[byte] =
encode(toPreserves(x))

View File

@ -1,38 +0,0 @@
import
std/typetraits, preserves, std/tables
type
Path* = seq[string]
Headers* = Table[string, seq[string]]
Response* {.preservesRecord: "http".} = object
`handle`*: BiggestInt
`code`*: BiggestInt
`headers`*: Headers
`body`*: string
Listener* {.preservesRecord: "listen".} = object
`port`*: BiggestInt
Handler* {.preservesRecord: "handler".} = object
`methods`*: Methods
`path`*: Path
`entity`* {.preservesEmbedded.}: Preserve[void]
`Method`* {.preservesOr, pure.} = enum
`GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`,
`PATCH`
Request* {.preservesRecord: "http".} = object
`handle`*: BiggestInt
`method`*: Method
`headers`*: Headers
`path`*: Path
`body`*: string
Methods* = set[Method]
proc `$`*(x: Path | Headers | Response | Listener | Handler | Request | Methods): string =
`$`(toPreserve(x))
proc encode*(x: Path | Headers | Response | Listener | Handler | Request |
Methods): seq[byte] =
encode(toPreserve(x))

View File

@ -0,0 +1,16 @@
import
preserves
type
InotifyMessage* {.preservesRecord: "inotify".} = object
`path`*: string
`event`*: Symbol
`cookie`*: BiggestInt
`name`*: string
proc `$`*(x: InotifyMessage): string =
`$`(toPreserves(x))
proc encode*(x: InotifyMessage): seq[byte] =
encode(toPreserves(x))

View File

@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/json
import preserves, preserves/jsonhooks
export fromPreservesHook, toPreservesHook
# re-export the hooks so that conversion "just works"
type
SendJson* {.preservesRecord: "send".} = object
data*: JsonNode
RecvJson* {.preservesRecord: "recv".} = object
data*: JsonNode

View File

@ -0,0 +1,30 @@
import
preserves
type
Failure* {.preservesRecord: "failure".} = object
`msg`*: string
Mountpoint* {.preservesRecord: "mount".} = object
`source`*: string
`target`*: string
`type`*: string
`status`*: Status
StatusKind* {.pure.} = enum
`Failure`, `success`
`Status`* {.preservesOr.} = object
case orKind*: StatusKind
of StatusKind.`Failure`:
`failure`*: Failure
of StatusKind.`success`:
`success`* {.preservesLiteral: "#t".}: bool
proc `$`*(x: Failure | Mountpoint | Status): string =
`$`(toPreserves(x))
proc encode*(x: Failure | Mountpoint | Status): seq[byte] =
encode(toPreserves(x))

16
src/schema/net_mapper.nim Normal file
View File

@ -0,0 +1,16 @@
import
preserves
type
RoundTripTime* {.preservesRecord: "rtt".} = object
`address`*: string
`minimum`*: float
`average`*: float
`maximum`*: float
proc `$`*(x: RoundTripTime): string =
`$`(toPreserves(x))
proc encode*(x: RoundTripTime): seq[byte] =
encode(toPreserves(x))

18
src/schema/rofi.nim Normal file
View File

@ -0,0 +1,18 @@
import
preserves, std/tables
type
Environment* = Table[Symbol, string]
Select* {.preservesRecord: "rofi-select".} = object
`option`*: string
`environment`*: Environment
Options* {.preservesRecord: "rofi-options".} = object
`options`*: seq[string]
proc `$`*(x: Environment | Select | Options): string =
`$`(toPreserves(x))
proc encode*(x: Environment | Select | Options): seq[byte] =
encode(toPreserves(x))

18
src/schema/sql.nim Normal file
View File

@ -0,0 +1,18 @@
import
preserves
type
Query* {.preservesRecord: "query".} = object
`statement`*: seq[Value]
`target`* {.preservesEmbedded.}: Value
SqlError* {.preservesRecord: "sql-error".} = object
`msg`*: string
`context`*: string
proc `$`*(x: Query | SqlError): string =
`$`(toPreserves(x))
proc encode*(x: Query | SqlError): seq[byte] =
encode(toPreserves(x))

149
src/sqlite_actor.nim Normal file
View File

@ -0,0 +1,149 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import preserves, syndicate
import ./schema/[config, sql]
# Avoid Sqlite3 from the standard library because it is
# only held together by wishful thinking and dlload.
{.passC: staticExec("pkg-config --cflags sqlite3").}
{.passL: staticExec("pkg-config --libs sqlite3").}
{.pragma: sqlite3h, header: "sqlite3.h".}
var
SQLITE_VERSION_NUMBER {.importc, sqlite3h.}: cint
SQLITE_OK {.importc, sqlite3h.}: cint
SQLITE_ROW {.importc, sqlite3h.}: cint
SQLITE_DONE {.importc, sqlite3h.}: cint
SQLITE_OPEN_READONLY {.importc, sqlite3h.}: cint
const
SQLITE_INTEGER = 1
SQLITE_FLOAT = 2
SQLITE_TEXT = 3
SQLITE_BLOB = 4
# SQLITE_NULL = 5
type
Sqlite3 {.importc: "sqlite3", sqlite3h.} = distinct pointer
Stmt {.importc: "sqlite3_stmt", sqlite3h.} = distinct pointer
{.pragma: importSqlite3, importc: "sqlite3_$1", sqlite3h.}
proc libversion_number: cint {.importSqlite3.}
proc open_v2(filename: cstring; ppDb: ptr Sqlite3; flags: cint; zVfs: cstring): cint {.importSqlite3.}
proc close(ds: Sqlite3): int32 {.discardable, importSqlite3.}
proc errmsg(db: Sqlite3): cstring {.importSqlite3.}
proc prepare_v2(db: Sqlite3; zSql: cstring, nByte: cint; ppStmt: ptr Stmt; pzTail: ptr cstring): cint {.importSqlite3.}
proc step(para1: Stmt): cint {.importSqlite3.}
proc column_count(stmt: Stmt): int32 {.importSqlite3.}
proc column_blob(stmt: Stmt; col: cint): pointer {.importSqlite3.}
proc column_bytes(stmt: Stmt; col: cint): cint {.importSqlite3.}
proc column_double(stmt: Stmt; col: cint): float64 {.importSqlite3.}
proc column_int64(stmt: Stmt; col: cint): int64 {.importSqlite3.}
proc column_text(stmt: Stmt; col: cint): cstring {.importSqlite3.}
proc column_type(stmt: Stmt; col: cint): cint {.importSqlite3.}
proc finalize(stmt: Stmt): cint {.importSqlite3.}
doAssert libversion_number() == SQLITE_VERSION_NUMBER
proc assertError(facet: Facet; cap: Cap; db: Sqlite3; context: string) =
run(facet) do (turn: Turn):
publish(turn, cap, SqlError(
msg: $errmsg(db),
context: context,
))
proc assertError(facet: Facet; cap: Cap; msg, context: string) =
run(facet) do (turn: Turn):
publish(turn, cap, SqlError(
msg: msg,
context: context,
))
proc extractValue(stmt: Stmt; col: cint): Value =
case column_type(stmt, col)
of SQLITE_INTEGER:
result = toPreserves(column_int64(stmt, col))
of SQLITE_FLOAT:
result = toPreserves(column_double(stmt, col))
of SQLITE_TEXT:
result = Value(kind: pkString, string: newString(column_bytes(stmt, col)))
if result.string.len > 0:
copyMem(addr result.string[0], column_text(stmt, col), result.string.len)
of SQLITE_BLOB:
result = Value(kind: pkByteString, bytes: newSeq[byte](column_bytes(stmt, col)))
if result.bytes.len > 0:
copyMem(addr result.bytes[0], column_blob(stmt, col), result.bytes.len)
else:
result = initRecord("null")
proc extractTuple(stmt: Stmt; arity: cint): Value =
result = initSequence(arity)
for col in 0..<arity: result[col] = extractValue(stmt, col)
proc renderSql(tokens: openarray[Value]): string =
for token in tokens:
if result.len > 0: result.add ' '
case token.kind
of pkSymbol:
result.add token.symbol.string
of pkString:
result.add '\''
result.add token.string
result.add '\''
of pkFloat, pkRegister, pkBigInt:
result.add $token
of pkBoolean:
if token.bool: result.add '1'
else: result.add '0'
else:
return ""
proc spawnSqliteActor*(turn: Turn; root: Cap): Actor {.discardable.} =
spawn("sqlite-actor", turn) do (turn: Turn):
during(turn, root, ?:SqliteArguments) do (path: string, ds: Cap):
linkActor(turn, path) do (turn: Turn):
let facet = turn.facet
stderr.writeLine("opening SQLite database ", path)
var db: Sqlite3
if open_v2(path, addr db, SQLITE_OPEN_READONLY, nil) != SQLITE_OK:
assertError(facet, ds, db, path)
else:
turn.onStop do (turn: Turn):
close(db)
stderr.writeLine("closed SQLite database ", path)
during(turn, ds, ?:Query) do (statement: seq[Value], target: Cap):
var
stmt: Stmt
text = renderSql statement
if text == "":
assertError(facet, target, "invalid statement", $statement)
elif prepare_v2(db, text, text.len.cint, addr stmt, nil) != SQLITE_OK:
assertError(facet, target, db, text)
else:
try:
let arity = column_count(stmt)
var res = step(stmt)
while res == SQLITE_ROW:
var rec = extractTuple(stmt, arity)
discard publish(turn, target, rec)
res = step(stmt)
assert res != 100
if res != SQLITE_DONE:
assertError(facet, target, db, text)
finally:
if finalize(stmt) != SQLITE_OK: assertError(facet, target, db, text)
when isMainModule:
import syndicate/relays
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
spawnSqliteActor(turn, ds)

28
src/syndesizer.nim Normal file
View File

@ -0,0 +1,28 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
## Syndicate multitool.
import syndicate, syndicate/relays, syndicate/drivers/timers
import ./syndesizer/[
base64_decoder,
cache_actor,
file_system_usage,
http_driver,
json_socket_translator,
json_translator,
pulses,
xml_translator]
runActor("syndesizer") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
discard spawnTimerDriver(turn, ds)
discard spawnBase64Decoder(turn, ds)
discard spawnCacheActor(turn, ds)
discard spawnFileSystemUsageActor(turn, ds)
discard spawnHttpDriver(turn, ds)
discard spawnJsonSocketTranslator(turn, ds)
discard spawnJsonStdioTranslator(turn, ds)
discard spawnPulseActor(turn, ds)
discard spawnXmlTranslator(turn, ds)

3
src/syndesizer/Tupfile Normal file
View File

@ -0,0 +1,3 @@
include_rules
: foreach *.nim | $(SYNDICATE_PROTOCOL) ../<schema> |> !nim_bin |> {bin}
: foreach {bin} |> !assert_built |>

View File

@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[base64, os]
import pkg/hashlib/misc/blake2
import preserves, preserves/sugar, syndicate
import ../schema/config
import ../schema/base64 as schema
export Base64DecoderArguments
export schema
proc spawnBase64Decoder*(turn: Turn; root: Cap): Actor {.discardable.} =
spawnActor(turn, "base64-decoder") do (turn: Turn):
let tmpDir = getTempDir()
during(turn, root, ?:Base64DecoderArguments) do (ds: Cap):
let decTextPat = observePattern(!Base64Text, { @[%0]: grabLit() })
during(turn, ds, decTextPat) do (txt: string):
discard publish(turn, ds, Base64Text(
txt: txt,
bin: cast[seq[byte]](decode(txt)),
))
let encTextPat = observePattern(!Base64Text, { @[%1]: grabLit() })
during(turn, ds, encTextPat) do (bin: seq[byte]):
discard publish(turn, ds, Base64Text(
txt: encode(bin),
bin: bin,
))
let decFilePat = observePattern( !Base64File, { @[%0]: grabLit() })
during(turn, ds, decFilePat) do (txt: string):
var bin = decode(txt)
var ctx = init[BLAKE2B_512]()
ctx.update(bin)
let
digest = $ctx.final()
path = tmpDir / digest
writeFile(path, bin)
discard publish(turn, ds, Base64File(
txt: txt,
path: path,
size: bin.len,
))
when isMainModule:
import syndicate/relays
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
spawnBase64Decoder(turn, ds)

View File

@ -0,0 +1,58 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/times
import preserves, syndicate,
syndicate/durings,
syndicate/drivers/timers
import ../schema/config
proc afterTimeout(n: float64): LaterThan =
## Get a `LaterThan` record for `n` seconds in the future.
result.seconds = getTime().toUnixFloat() + n
type CacheEntity {.final.} = ref object of Entity
timeouts, target: Cap
# dataspaces for observing timeouts and publishing values
pattern: Pattern
lifetime: float64
method publish(cache: CacheEntity; turn: Turn; ass: AssertionRef; h: Handle) =
## Re-assert pattern captures in a sub-facet.
discard inFacet(turn) do (turn: Turn):
# TODO: a seperate facet for every assertion, too much?
var ass = depattern(cache.pattern, ass.value.sequence)
# Build an assertion with what he have of the pattern and capture.
discard publish(turn, cache.target, ass)
let timeout = afterTimeout(cache.lifetime)
onPublish(turn, cache.timeouts, ?timeout) do:
stop(turn) # end this facet
proc isObserve(pat: Pattern): bool =
pat.orKind == PatternKind.group and
pat.group.type.orKind == GroupTypeKind.rec and
pat.group.type.rec.label.isSymbol"Observe"
proc spawnCacheActor*(turn: Turn; root: Cap): Actor =
spawnActor(turn, "cache_actor") do (turn: Turn):
during(turn, root, ?:CacheArguments) do (ds: Cap, lifetime: float64):
onPublish(turn, ds, ?:Observe) do (pat: Pattern, obs: Cap):
var cache: CacheEntity
if obs.relay != turn.facet and not(pat.isObserve):
# Watch pattern if the observer is not us
# and if the pattern isn't a recursive observe
cache = CacheEntity(
timeouts: root,
target: ds,
pattern: pat,
lifetime: lifetime,
)
discard observe(turn, ds, pat, cache)
when isMainModule:
import syndicate/relays
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
discard spawnTimerDriver(turn, ds)
discard spawnCacheActor(turn, ds)

View File

@ -0,0 +1,28 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[dirs, os, paths]
import preserves, preserves/sugar
import syndicate
import ../schema/[assertions, config]
proc spawnFileSystemUsageActor*(turn: Turn; root: Cap): Actor {.discardable.} =
spawn("file-system-usage", turn) do (turn: Turn):
during(turn, root, ?:FileSystemUsageArguments) do (ds: Cap):
let pat = observePattern(!FileSystemUsage, { @[%0]: grab() })
during(turn, ds, pat) do (lit: Literal[string]):
var ass = FileSystemUsage(path: lit.value)
if fileExists(ass.path): ass.size = getFileSize(ass.path)
else:
for fp in walkDirRec(paths.Path(lit.value), yieldFilter={pcFile}):
var fs = getFileSize(string fp)
inc(ass.size, fs)
discard publish(turn, ds, ass)
# TODO: updates?
when isMainModule:
import syndicate/relays
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
discard spawnFileSystemUsageActor(turn, ds)

View File

@ -0,0 +1,42 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
## Thin wrapper over `syndicate/drivers/http_driver`.
import pkg/preserves, pkg/syndicate
import pkg/syndicate/drivers/http_driver
import pkg/taps
import ../schema/config
proc spawnHttpDriver*(turn: Turn; ds: Cap): Actor {.discardable.}=
http_driver.spawnHttpDriver(turn, ds)
during(turn, ds, ?:HttpDriverArguments) do (ds: Cap):
http_driver.spawnHttpDriver(turn, ds)
when isMainModule:
import syndicate/relays
when defined(solo5):
import solo5
acquireDevices([("eth0", netBasic)], netAcquireHook)
proc envRoute: Route =
var pr = parsePreserves $solo5_start_info.cmdline
if result.fromPreserves pr:
return
elif pr.isSequence:
for e in pr:
if result.fromPreserves e:
return
quit("failed to parse command line for route to Syndicate gatekeeper")
runActor("main") do (turn: Turn):
let ds = newDataspace(turn)
spawnRelays(turn, ds)
resolve(turn, ds, envRoute(), spawnHttpDriver)
else:
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
spawnHttpDriver(turn, ds)

View File

@ -0,0 +1,2 @@
define:ipv6Enabled
include:"std/assertions"

View File

@ -0,0 +1,77 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[json, options]
import pkg/sys/[ioqueue, sockets]
import preserves, preserves/jsonhooks, syndicate
import ../schema/[config, json_messages]
template translateSocketBody {.dirty.} =
# Template workaround for CPS and parameterized types.
var
guard = initGuard(facet)
dec = newBufferedDecoder(0)
buf = new string #TODO: get a pointer into the decoder
alive = true
proc kill(turn: Turn) =
alive = false
proc setup(turn: Turn) =
# Closure, not CPS.
onMessage(turn, ds, ?:SendJson) do (data: JsonNode):
if alive:
discard trampoline:
whelp write(socket[], $data & "\n")
else:
stderr.writeLine "dropped send of ", data
discard publish(turn, ds, initRecord("connected", sa.toPreserves))
onStop(facet, kill)
run(facet, setup)
while alive:
# TODO: parse buffer
buf[].setLen(0x4000)
let n = read(socket[], buf)
if n < 1:
stderr.writeLine "socket read returned ", n
else:
buf[].setLen(n)
dec.feed(buf[])
var data = dec.parse()
if data.isSome:
proc send(turn: Turn) =
# Closure, not CPS.
message(turn, ds, initRecord("recv", data.get))
run(facet, send)
stderr.writeLine "close socket ", sa
close(socket[])
proc translateSocket(facet: Facet; ds: Cap; sa: TcpAddress) {.asyncio.} =
var
socket = new AsyncConn[Protocol.Tcp]
conn = connectTcpAsync(sa.host, Port sa.port)
socket[] = conn
translateSocketBody()
proc translateSocket(facet: Facet; ds: Cap; sa: UnixAddress) {.asyncio.} =
var
socket = new AsyncConn[Protocol.Unix]
conn = connectUnixAsync(sa.path)
socket[] = conn
translateSocketBody()
proc spawnJsonSocketTranslator*(turn: Turn; root: Cap): Actor {.discardable.} =
spawnActor(turn, "json-socket-translator") do (turn: Turn):
during(turn, root, ?:JsonSocketTranslatorArguments) do (ds: Cap, sa: TcpAddress):
linkActor(turn, "json-socket-translator") do (turn: Turn):
discard trampoline:
whelp translateSocket(turn.facet, ds, sa)
during(turn, root, ?:JsonSocketTranslatorArguments) do (ds: Cap, sa: UnixAddress):
linkActor(turn, "json-socket-translator") do (turn: Turn):
discard trampoline:
whelp translateSocket(turn.facet, ds, sa)
when isMainModule:
import syndicate/relays
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
spawnJsonSocketTranslator(turn, ds)

View File

@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[json, osproc]
import preserves
import syndicate
import ../schema/[config, json_messages]
proc runChild(params: seq[string]): string =
if params.len < 1:
stderr.writeLine "not enough parameters"
let
cmd = params[0]
args = params[1..params.high]
try: result = execProcess(command=cmd, args=args, options={poUsePath})
except CatchableError as err:
quit("execProcess failed: " & err.msg)
if result == "":
stderr.writeLine "no ouput"
proc spawnJsonStdioTranslator*(turn: Turn; root: Cap): Actor {.discardable.} =
spawnActor(turn, "json-stdio-translator") do (turn: Turn):
during(turn, root, ?:JsonTranslatorArguments) do (argv: seq[string], ds: Cap):
var js = parseJson(runChild(argv))
message(turn, ds, RecvJson(data: js))
discard publish(turn, ds, RecvJson(data: js))
when isMainModule:
import syndicate/relays
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
spawnJsonStdioTranslator(turn, ds)

105
src/syndesizer/pulses.nim Normal file
View File

@ -0,0 +1,105 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[options, tables, times]
import preserves, preserves/sugar
import syndicate, syndicate/drivers/timers
import ../schema/[assertions, config]
type PulseEntity {.final.} = ref object of Entity
## An entity that asserts and retracts observers on a pulse.
self, timers: Cap
target: Entity
period: float
timerHandle: Handle
observers: Table[Handle, AssertionRef]
observePattern: Pattern
observing: bool
proc schedule(turn: Turn; pulse: PulseEntity) =
## Schedule the next pulse.
## The next pulse will be schedule using the current time as
## reference point and not the moment of the previous pulse.
let then = getTime().toUnixFloat()+pulse.period
pulse.timerHandle = publish(turn, pulse.timers, Observe(
pattern: LaterThan ?: { 0: ?then },
observer: pulse.self,
))
method publish(pulse: PulseEntity; turn: Turn; ass: AssertionRef; h: Handle) =
## Publish observers in reponse to <later-than …> assertions.
pulse.timers.target.retract(turn, pulse.timerHandle)
schedule(turn, pulse)
pulse.observing = true
for h, a in pulse.observers.pairs:
pulse.target.publish(turn, a, h)
pulse.target.sync(turn, pulse.self)
method message(pulse: PulseEntity; turn: Turn; v: AssertionRef) =
## Retract observers in response to a sync message.
pulse.observing = false
for h in pulse.observers.keys:
pulse.target.retract(turn, h)
type ProxyEntity {.final.} = ref object of Entity
## A proxy `Entity` that diverts observers to a `PulseEntity`.
pulse: PulseEntity
method publish(proxy: ProxyEntity; turn: Turn; ass: AssertionRef; h: Handle) =
## Proxy assertions that are not observations.
if proxy.pulse.observePattern.matches ass.value:
if proxy.pulse.observers.len == 0:
schedule(turn, proxy.pulse)
proxy.pulse.observers[h] = ass
else:
proxy.pulse.target.publish(turn, ass, h)
method retract(proxy: ProxyEntity; turn: Turn; h: Handle) =
## Retract proxied assertions.
var obs: AssertionRef
if proxy.pulse.observers.pop(h, obs):
if proxy.pulse.observing:
proxy.pulse.target.retract(turn, h)
if proxy.pulse.observers.len == 0:
proxy.pulse.timers.target.retract(turn, proxy.pulse.timerHandle)
else:
proxy.pulse.target.retract(turn, h)
method message(proxy: ProxyEntity; turn: Turn; v: AssertionRef) =
## Proxy mesages.
proxy.pulse.target.message(turn, v)
method sync(proxy: ProxyEntity; turn: Turn; peer: Cap) =
## Proxy sync.
proxy.pulse.target.sync(turn, peer)
proc newProxyEntity(turn: Turn; timers, ds: Cap; period: float): ProxyEntity =
new result
result.pulse = PulseEntity(
target: ds.target,
timers: timers,
observePattern: ?:Observe,
period: period,
)
result.pulse.self = newCap(turn, result.pulse)
proc spawnPulseActor*(turn: Turn; root: Cap): Actor =
## Spawn an actor that retracts and re-asserts observers on
## a timed pulse. Requires a timer service on the `root` capability.
spawnActor(turn, "pulse") do (turn: Turn):
let grabPeriod = observePattern(!Pulse, { @[%0]: grab() })
during(turn, root, ?:PulseArguments) do (ds: Cap):
during(turn, ds, grabPeriod) do (lit: Literal[float]):
if lit.value < 0.000_1:
stderr.writeLine("pulse period is too small: ", lit.value, "s")
else:
let proxyCap = newCap(turn, newProxyEntity(turn, root, ds, lit.value))
var pulse = Pulse(periodSec: lit.value, proxy: embed proxyCap)
discard publish(turn, ds, pulse)
when isMainModule:
import syndicate/relays
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
discard spawnPulseActor(turn, ds)

View File

@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[options, parsexml, xmlparser, xmltree]
import preserves, preserves/sugar, preserves/xmlhooks
import syndicate
import ../schema/[assertions, config]
proc translateXml(s: string): XmlTranslation =
result.xml = s
try: result.pr = result.xml.parseXml({allowUnquotedAttribs}).toPreservesHook
except XmlError: discard
proc translatePreserves(pr: Value): XmlTranslation {.gcsafe.} =
result.pr = pr
var xn = result.pr.preservesTo(XmlNode)
if xn.isSome: result.xml = $get(xn)
proc spawnXmlTranslator*(turn: Turn; root: Cap): Actor {.discardable.} =
spawnActor(turn, "xml-translator") do (turn: Turn):
during(turn, root, ?:XmlTranslatorArguments) do (ds: Cap):
let xmlPat = observePattern(!XmlTranslation, {@[%0]:grab()})
during(turn, ds, xmlPat) do (xs: Literal[string]):
publish(turn, ds, translateXml(xs.value))
let prPat = observePattern(!XmlTranslation, {@[%1]:grab()})
during(turn, ds, prPat) do (pr: Literal[Value]):
publish(turn, ds, translatePreserves(pr.value))
when isMainModule:
import syndicate/relays
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
spawnXmlTranslator(turn, ds)

69
src/syndump.nim Normal file
View File

@ -0,0 +1,69 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[os, tables]
import preserves, syndicate, syndicate/[durings, relays]
proc parsePattern(pr: Value): Pattern =
let
dropSigil = initRecord("lit", "_".toSymbol)
grabSigil = initRecord("lit", "?".toSymbol)
var pr = grab(pr).toPreserves
apply(pr) do (pr: var Value):
if pr == dropSigil:
pr = initRecord("_")
elif pr == grabSigil:
pr = initRecord("bind", initRecord("_"))
doAssert result.fromPreserves(pr)
proc inputPatterns: seq[Pattern] =
var args = commandLineParams()
result.setLen(args.len)
for i, input in args:
try: result[i] = input.parsePreserves.parsePattern
except ValueError:
quit "failed to parse Preserves argument"
type DumpEntity {.final.} = ref object of Entity
assertions: Table[Handle, seq[Value]]
proc toLine(values: seq[Value]; prefix: char): string =
result = newStringOfCap(1024)
let sep = getEnv("FS", " ")
result.add(prefix)
for v in values:
add(result, sep)
add(result, $v)
add(result, '\n')
method publish(dump: DumpEntity; turn: Turn; ass: AssertionRef; h: Handle) =
var values = ass.value.sequence
stdout.write(values.toLine('+'))
stdout.flushFile()
dump.assertions[h] = values
method retract(dump: DumpEntity; turn: Turn; h: Handle) =
var values: seq[Value]
if dump.assertions.pop(h, values):
stdout.write(values.toLine('-'))
stdout.flushFile()
method message*(dump: DumpEntity; turn: Turn; ass: AssertionRef) =
stdout.write(ass.value.sequence.toLine('!'))
stdout.flushFile()
proc exitProc() {.noconv.} =
stdout.write('\n')
quit()
proc main =
let
patterns = inputPatterns()
entity = DumpEntity()
runActor("syndex_card") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
for pat in patterns:
discard observe(turn, ds, pat, entity)
setControlCHook(exitProc)
main()

211
src/xslt_actor.nim Normal file
View File

@ -0,0 +1,211 @@
# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[os, strutils]
import preserves, preserves/sugar, syndicate
import ./schema/[assertions, config]
{.passC: staticExec("pkg-config --cflags libxslt").}
{.passL: staticExec("pkg-config --libs libxslt").}
{.pragma: libxslt, header: "libxslt/xslt.h", importc.}
type
xmlElementType {.libxslt.} = enum
XML_ELEMENT_NODE = 1,
XML_ATTRIBUTE_NODE = 2,
XML_TEXT_NODE = 3,
XML_CDATA_SECTION_NODE = 4,
XML_ENTITY_REF_NODE = 5,
XML_ENTITY_NODE = 6,
XML_PI_NODE = 7,
XML_COMMENT_NODE = 8,
XML_DOCUMENT_NODE = 9,
XML_DOCUMENT_TYPE_NODE = 10,
XML_DOCUMENT_FRAG_NODE = 11,
XML_NOTATION_NODE = 12,
XML_HTML_DOCUMENT_NODE = 13,
XML_DTD_NODE = 14,
XML_ELEMENT_DECL = 15,
XML_ATTRIBUTE_DECL = 16,
XML_ENTITY_DECL = 17,
XML_NAMESPACE_DECL = 18,
XML_XINCLUDE_START = 19,
XML_XINCLUDE_END = 20
xmlNsPtr = ptr xmlNs
xmlNs {.libxslt.} = object
next: xmlNsPtr
href, prefix: cstring
xmlAttrPtr = ptr xmlAttr
xmlAttr {.libxslt.} = object
name: cstring
next: xmlAttrPtr
children: xmlNodePtr
xmlElementContentPtr = ptr xmlElementContent
xmlElementContent {.libxslt.} = object
encoding: cstring
xmlNodePtr = ptr xmlNode
xmlNode {.libxslt.} = object
`type`: xmlElementType
name: cstring
children, next: xmlNodePtr
content: cstring
properties: xmlAttrPtr
nsDef: xmlNsPtr
xmlDocPtr {.libxslt.} = distinct pointer
xsltStylesheetPtr {.libxslt.} = distinct pointer
proc isNil(x: xmlDocPtr): bool {.borrow.}
proc isNil(x: xsltStylesheetPtr): bool {.borrow.}
proc xmlReadMemory(buf: pointer; len: cint; url, enc: cstring; opts: cint): xmlDocPtr {.libxslt.}
proc xmlReadMemory(buf: string; uri = "noname.xml"): xmlDocPtr =
xmlReadMemory(buf[0].addr, buf.len.cint, uri, "UTF-8", 0)
proc xmlParseFile(filename: cstring): xmlDocPtr {.libxslt.}
proc xmlFreeDoc(p: xmlDocPtr) {.libxslt.}
proc xmlDocGetRootElement(doc: xmlDocPtr): xmlNodePtr {.libxslt.}
proc loadXmlDoc(text: string): xmlDocPtr =
if text.startsWith("/") and fileExists(text):
xmlParseFile(text)
else:
xmlReadMemory(text, "noname.xml")
proc xsltParseStylesheetFile(filename: cstring): xsltStylesheetPtr {.libxslt.}
proc xsltParseStylesheetDoc(doc: xmlDocPtr): xsltStylesheetPtr {.libxslt.}
proc xsltParseStylesheetDoc(text: string; uri = "noname.xml"): xsltStylesheetPtr =
var doc = xmlReadMemory(text, uri)
result = xsltParseStylesheetDoc(doc)
# implicit free of doc
proc loadStylesheet(text: string): xsltStylesheetPtr =
if text.startsWith("/") and fileExists(text):
xsltParseStylesheetFile(text)
else:
xsltParseStylesheetDoc(text, "noname.xsl")
proc xsltApplyStylesheet(
style: xsltStylesheetPtr, doc: xmlDocPtr, params: cstringArray): xmlDocPtr {.libxslt.}
proc xsltFreeStylesheet(style: xsltStylesheetPtr) {.libxslt.}
proc xsltSaveResultToString(txt: ptr pointer; len: ptr cint; res: xmlDocPtr; style: xsltStylesheetPtr): cint {.libxslt.}
proc c_free*(p: pointer) {.importc: "free", header: "<stdlib.h>".}
proc xsltSaveResultToString(res: xmlDocPtr; style: xsltStylesheetPtr): string =
var
txt: pointer
len: cint
if xsltSaveResultToString(addr txt, addr len, res, style) < 0:
raise newException(CatchableError, "xsltSaveResultToString failed")
if len > 0:
result = newString(int len)
copyMem(result[0].addr, txt, len)
c_free(txt)
proc initLibXml =
discard
proc XML_GET_CONTENT(xn: xmlNodePtr): xmlElementContentPtr {.libxslt.}
proc textContent(xn: xmlNodePtr): string =
if xn.content != nil: result = $xn.content
proc content(attr: xmlAttrPtr): string =
var child = attr.children
while not child.isNil:
result.add child.content
child = child.next
proc preserveSiblings(result: var seq[Value]; first: xmlNodePtr) =
var xn = first
while not xn.isNil:
case xn.type
of XML_ELEMENT_NODE:
var child = Value(kind: pkRecord)
if not xn.nsDef.isNil:
child.record.add initDictionary()
var ns = xn.nsDef
while not ns.isNil:
if not ns.href.isNil:
var key = Value(kind: pkString)
if ns.prefix.isNil:
key.string = "xmlns"
else:
key.string = "xmlns:" & $ns.prefix
child.record[0][key] = toPreserves($ns.href)
ns = ns.next
if not xn.properties.isNil:
if child.record.len < 1:
child.record.add initDictionary()
var attr = xn.properties
while not attr.isNil:
var
key = toPreserves($attr.name)
val = toPreserves(attr.content)
child.record[0][key] = val
attr = attr.next
if not xn.children.isNil:
preserveSiblings(child.record, xn.children)
child.record.add tosymbol($xn.name)
result.add child
of XML_TEXT_NODE:
result.add textContent(xn).toPreserves
else:
stderr.writeLine "not an XML_ELEMENT_NODE - ", $xn.type
xn = xn.next
proc toPreservesHook*(xn: xmlNodePtr): Value =
var items = newSeqofCap[Value](1)
preserveSiblings(items, xn)
items[0]
proc spawnXsltActor*(turn: Turn; root: Cap): Actor {.discardable.} =
spawnActor(turn, "xslt") do (turn: Turn):
initLibXml()
during(turn, root, ?:XsltArguments) do (ds: Cap):
let sheetsPat = observePattern(!XsltTransform, {@[%0]: grab(), @[%1]: grab()})
during(turn, ds, sheetsPat) do (stylesheet: Literal[string], input: Literal[string]):
let cur = loadStylesheet(stylesheet.value)
if cur.isNil:
stderr.writeLine "failed to parse stylesheet"
else:
let doc = loadXmlDoc(input.value)
if doc.isNil:
stderr.writeLine "failed to parse input document"
else:
let
params = allocCStringArray([])
res = xsltApplyStylesheet(cur, doc, params)
if res.isNil:
stderr.writeLine "failed to apply stylesheet transformation"
else:
let output = xsltSaveResultToString(res, cur)
deallocCStringArray(params)
publish(turn, ds, XsltTransform(
stylesheet: stylesheet.value,
input: input.value,
output: xmlDocGetRootElement(res).toPreservesHook,
))
xmlFreeDoc(res)
xmlFreeDoc(doc)
xsltFreeStylesheet(cur)
when isMainModule:
import syndicate/relays
runActor("main") do (turn: Turn):
resolveEnvironment(turn) do (turn: Turn; ds: Cap):
spawnXsltActor(turn, ds)

View File

@ -1,13 +1,13 @@
# Package
version = "20230410"
version = "20240509"
author = "Emery Hemingway"
description = "Utilites for Syndicated Actors and Synit"
license = "unlicense"
srcDir = "src"
bin = @["http_translator", "json_socket_translator", "json_translator", "msg"]
bin = @["http_client", "mintsturdyref", "mount_actor", "msg", "postgre_actor", "preserve_process_environment", "rofi_script_actor", "sqlite_actor", "syndesizer", "syndump", "xslt_actor"]
# Dependencies
requires "nim >= 1.6.6", "syndicate >= 0.3.1"
requires "https://git.syndicate-lang.org/ehmry/syndicate-nim.git >= 20240507", "https://github.com/ehmry/nim-sys.git#4ef3b624db86e331ba334e705c1aa235d55b05e1", "https://git.sr.ht/~ehmry/nim_taps >= 20240405"