Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,40 @@
|
||||
#include "dever_ffi.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
extern long long fixture_triple(long long value);
|
||||
|
||||
static int invoke(void *context, dever_ffi_slice operation, dever_ffi_slice payload, dever_ffi_output *output) {
|
||||
(void)context;
|
||||
if (operation.size == 6 && memcmp(operation.data, "reject", 6) == 0) {
|
||||
const char *error = "{\"error\":\"example.Rejected\",\"payload\":null}";
|
||||
memcpy(output->data, error, strlen(error));
|
||||
output->size = strlen(error);
|
||||
return DEVER_FFI_ERROR;
|
||||
}
|
||||
if (operation.size == 10 && memcmp(operation.data, "bad_length", 10) == 0) {
|
||||
output->size = output->capacity + 1;
|
||||
return DEVER_FFI_OK;
|
||||
}
|
||||
if (operation.size == 5 && memcmp(operation.data, "fault", 5) == 0) return DEVER_FFI_FAULT;
|
||||
if (payload.size >= 64) return DEVER_FFI_FAULT;
|
||||
char input[64];
|
||||
memcpy(input, payload.data, (size_t)payload.size);
|
||||
input[payload.size] = 0;
|
||||
long long value = 0;
|
||||
if (sscanf(input, "{\"value\":%lld}", &value) != 1 || value > 1000000 || value < -1000000) return DEVER_FFI_FAULT;
|
||||
int size = snprintf(output->data, (size_t)output->capacity, "{\"value\":%lld}", fixture_triple(value));
|
||||
if (size < 0 || (uint64_t)size >= output->capacity) return DEVER_FFI_FAULT;
|
||||
output->size = (uint64_t)size;
|
||||
return DEVER_FFI_OK;
|
||||
}
|
||||
|
||||
static void closed(void *context) {
|
||||
(void)context;
|
||||
fputs("closed\n", stderr);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const dever_ffi_handler handler = {DEVER_FFI_ABI_V1, NULL, NULL, invoke, closed};
|
||||
return dever_ffi_serve(&handler);
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
"""Opt-in host SDK contract check. Pass only absolute runtime paths."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELLO = {
|
||||
"kind": "hello", "version": "dever-component-1", "port": "example.Text",
|
||||
"schema": "fixture-schema-v1", "adapter": "example.TextAdapter",
|
||||
"capabilities": [], "operations": ["text.render", "text.fail", "number.echo", "text.stubborn"],
|
||||
"setting": {"prefix": "hello "},
|
||||
}
|
||||
|
||||
|
||||
def frame(message):
|
||||
body = json.dumps(message, separators=(",", ":")).encode()
|
||||
return struct.pack(">I", len(body)) + body
|
||||
|
||||
|
||||
def read_exact(stream, size):
|
||||
result = bytearray()
|
||||
while len(result) < size:
|
||||
part = stream.read(size - len(result))
|
||||
if not part:
|
||||
raise AssertionError("worker closed protocol stream")
|
||||
result.extend(part)
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def receive(process):
|
||||
deadline = threading.Timer(3, process.kill)
|
||||
deadline.start()
|
||||
try:
|
||||
size = struct.unpack(">I", read_exact(process.stdout, 4))[0]
|
||||
assert 0 < size <= 16 * 1024 * 1024
|
||||
return json.loads(read_exact(process.stdout, size))
|
||||
finally:
|
||||
deadline.cancel()
|
||||
|
||||
|
||||
def send(process, message):
|
||||
process.stdin.write(frame(message))
|
||||
process.stdin.flush()
|
||||
|
||||
|
||||
def run_case(command, *, keep_input_open=False):
|
||||
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) as process:
|
||||
try:
|
||||
send(process, HELLO)
|
||||
ready = receive(process)
|
||||
assert ready == {key: value for key, value in HELLO.items() if key != "setting"} | {"kind": "ready"}, ready
|
||||
send(process, {"kind": "health", "id": 0})
|
||||
assert receive(process) == {"kind": "health", "id": 0}
|
||||
send(process, {"kind": "call", "id": 1, "operation": "text.render", "payload": {"text": "world", "delay_ms": 0}})
|
||||
assert receive(process) == {"kind": "result", "id": 1, "payload": {"value": "hello world"}}
|
||||
send(process, {"kind": "call", "id": 2, "operation": "text.fail", "payload": {"text": "x", "delay_ms": 0}})
|
||||
assert receive(process) == {"kind": "error", "id": 2, "error": "example.rejected", "payload": {"reason": "rejected"}}
|
||||
send(process, {"kind": "call", "id": 3, "operation": "text.render", "payload": {"text": "late", "delay_ms": 500}})
|
||||
send(process, {"kind": "cancel", "id": 3})
|
||||
assert receive(process) == {"kind": "error", "id": 3, "error": "dever.cancelled", "payload": None}
|
||||
time.sleep(0.6)
|
||||
send(process, {"kind": "call", "id": 4, "operation": "text.render", "payload": {"text": "again", "delay_ms": 0}})
|
||||
assert receive(process) == {"kind": "result", "id": 4, "payload": {"value": "hello again"}}
|
||||
for request_id, number in enumerate((9007199254740993, 9223372036854775807, -9223372036854775808), 5):
|
||||
send(process, {"kind": "call", "id": request_id, "operation": "number.echo", "payload": {"number": number}})
|
||||
assert receive(process) == {"kind": "result", "id": request_id, "payload": {"number": number}}
|
||||
for request_id in (8, 9):
|
||||
send(process, {"kind": "call", "id": request_id, "operation": "text.render", "payload": {"text": "cancel", "delay_ms": 500}})
|
||||
send(process, {"kind": "cancel", "id": request_id})
|
||||
assert receive(process) == {"kind": "error", "id": request_id, "error": "dever.cancelled", "payload": None}
|
||||
send(process, {"kind": "shutdown"})
|
||||
assert receive(process) == {"kind": "shutdown"}
|
||||
# Python must finalize its reader without relying on the host's EOF timing.
|
||||
if not keep_input_open:
|
||||
process.stdin.close()
|
||||
assert process.wait(timeout=3) == 0, process.stderr.read().decode()
|
||||
except Exception as error:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait(timeout=3)
|
||||
raise AssertionError(f"worker case failed: {error}; stderr={process.stderr.read().decode()}") from error
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait(timeout=3)
|
||||
|
||||
|
||||
def run_queued_shutdown(command):
|
||||
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) as process:
|
||||
try:
|
||||
send(process, HELLO)
|
||||
assert receive(process)["kind"] == "ready"
|
||||
messages = [
|
||||
{"kind": "call", "id": 1, "operation": "text.render", "payload": {"text": "cancel", "delay_ms": 500}},
|
||||
{"kind": "shutdown"},
|
||||
*[{"kind": "health", "id": 0} for _ in range(4)],
|
||||
]
|
||||
process.stdin.write(b"".join(frame(message) for message in messages))
|
||||
process.stdin.flush()
|
||||
assert receive(process) == {"kind": "shutdown"}
|
||||
assert process.wait(timeout=3) == 0, process.stderr.read().decode()
|
||||
assert process.stderr.read() == b"", "shutdown left task/transport diagnostics"
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait(timeout=3)
|
||||
|
||||
|
||||
def run_typed_case(command, *, keep_input_open=False):
|
||||
hello = {
|
||||
"kind": "hello", "version": "dever-component-1", "port": "example.Typed",
|
||||
"schema": "typed-fixture-v1", "adapter": "example.Managed",
|
||||
"capabilities": [], "operations": ["render"], "setting": {"prefix": "hello "},
|
||||
}
|
||||
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) as process:
|
||||
try:
|
||||
send(process, hello)
|
||||
assert receive(process)["kind"] == "ready"
|
||||
message = {"name": "Ada", "number": 9223372036854775807, "labels": ["a", "b"]}
|
||||
send(process, {"kind": "call", "id": 1, "operation": "render", "payload": {"message": message}})
|
||||
assert receive(process) == {"kind": "result", "id": 1, "payload": {"text": "hello Ada9223372036854775807"}}
|
||||
message["name"] = "reject"
|
||||
send(process, {"kind": "call", "id": 2, "operation": "render", "payload": {"message": message}})
|
||||
assert receive(process) == {"kind": "error", "id": 2, "error": "example.Rejected", "payload": {"reason": "rejected"}}
|
||||
send(process, {"kind": "shutdown"})
|
||||
assert receive(process) == {"kind": "shutdown"}
|
||||
if not keep_input_open:
|
||||
process.stdin.close()
|
||||
assert process.wait(timeout=3) == 0, process.stderr.read().decode()
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait(timeout=3)
|
||||
|
||||
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) as process:
|
||||
try:
|
||||
send(process, hello)
|
||||
assert receive(process)["kind"] == "ready"
|
||||
send(process, {"kind": "call", "id": 1, "operation": "render", "payload": {
|
||||
"message": {"name": "Ada", "number": 9223372036854775808, "labels": []},
|
||||
}})
|
||||
process.stdin.close()
|
||||
assert process.wait(timeout=3) != 0
|
||||
assert process.stdout.read() == b""
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait(timeout=3)
|
||||
|
||||
|
||||
def run_rejection(command, body):
|
||||
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process:
|
||||
try:
|
||||
process.stdin.write(struct.pack(">I", len(body)) + body)
|
||||
process.stdin.flush()
|
||||
process.stdin.close()
|
||||
assert process.wait(timeout=3) != 0
|
||||
assert process.stdout.read() == b""
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait(timeout=3)
|
||||
|
||||
|
||||
def run_protocol_rejection(command, body, declared_size=None):
|
||||
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) as process:
|
||||
try:
|
||||
send(process, HELLO)
|
||||
assert receive(process)["kind"] == "ready"
|
||||
process.stdin.write(struct.pack(">I", len(body) if declared_size is None else declared_size) + body)
|
||||
process.stdin.flush()
|
||||
process.stdin.close()
|
||||
assert process.wait(timeout=3) != 0
|
||||
assert process.stdout.read() == b""
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait(timeout=3)
|
||||
|
||||
|
||||
def run_unresponsive_handler(command, final_message):
|
||||
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) as process:
|
||||
try:
|
||||
send(process, HELLO)
|
||||
assert receive(process)["kind"] == "ready"
|
||||
send(process, {"kind": "call", "id": 1, "operation": "text.stubborn", "payload": {"text": "late", "delay_ms": 0}})
|
||||
time.sleep(0.1)
|
||||
send(process, final_message)
|
||||
if final_message["kind"] == "cancel":
|
||||
send(process, {"kind": "call", "id": 2, "operation": "text.render", "payload": {"text": "new", "delay_ms": 0}})
|
||||
send(process, {"kind": "cancel", "id": 2})
|
||||
send(process, {"kind": "shutdown"})
|
||||
assert process.wait(timeout=3) != 0
|
||||
assert process.stdout.read() == b""
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait(timeout=3)
|
||||
|
||||
|
||||
def runtime(path):
|
||||
if path is None:
|
||||
return None
|
||||
executable = Path(path)
|
||||
if not executable.is_absolute() or not executable.is_file():
|
||||
raise SystemExit(f"runtime path must be an existing absolute file: {path}")
|
||||
return str(executable)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--python")
|
||||
parser.add_argument("--node")
|
||||
parser.add_argument("--go")
|
||||
args = parser.parse_args()
|
||||
with tempfile.TemporaryDirectory(prefix="dever-sdk-check-") as directory:
|
||||
selected = {
|
||||
"Python": [runtime(args.python), "-B", str(ROOT / "sdk/python/example.py")] if args.python else None,
|
||||
"JavaScript": [runtime(args.node), str(ROOT / "sdk/javascript/example.mjs")] if args.node else None,
|
||||
}
|
||||
if args.go:
|
||||
go = runtime(args.go)
|
||||
binary = str(Path(directory) / "go-worker")
|
||||
build_environment = {
|
||||
"GOCACHE": str(Path(directory) / "go-cache"),
|
||||
"GOPATH": str(Path(directory) / "go-path"),
|
||||
"GOENV": "off", "GOTOOLCHAIN": "local", "GOPROXY": "off", "GOSUMDB": "off",
|
||||
}
|
||||
subprocess.run([go, "build", "-o", binary, "./example"], cwd=ROOT / "sdk/go", env=build_environment, check=True)
|
||||
selected["Go"] = [binary]
|
||||
else:
|
||||
selected["Go"] = None
|
||||
manifest = str(ROOT / "test/external-sdk/typed_contract.json")
|
||||
typed = {
|
||||
"Python": [runtime(args.python), "-B", str(ROOT / "test/external-sdk/typed_worker.py"), manifest] if args.python else None,
|
||||
"JavaScript": [runtime(args.node), str(ROOT / "test/external-sdk/typed_worker.mjs"), manifest] if args.node else None,
|
||||
}
|
||||
if args.go:
|
||||
typed_binary = str(Path(directory) / "go-typed-worker")
|
||||
subprocess.run([go, "build", "-o", typed_binary, str(ROOT / "test/external-sdk/typed_worker.go")], cwd=ROOT / "sdk/go", env=build_environment, check=True)
|
||||
typed["Go"] = [typed_binary, manifest]
|
||||
else:
|
||||
typed["Go"] = None
|
||||
for language, command in selected.items():
|
||||
if command is None:
|
||||
print(f"{language}: unavailable (no absolute runtime path supplied)")
|
||||
continue
|
||||
run_case(command, keep_input_open=language == "Python")
|
||||
if language == "Python":
|
||||
run_queued_shutdown(command)
|
||||
run_rejection(command, json.dumps({**HELLO, "schema": "wrong"}).encode())
|
||||
run_rejection(command, (json.dumps(HELLO)[:-1] + ',"schema":"duplicate"}').encode())
|
||||
run_rejection(command, (json.dumps(HELLO)[:-1] + ',"extra":true}').encode())
|
||||
run_rejection(command, b"\xff")
|
||||
call = {"kind": "call", "id": 1, "operation": "text.render", "payload": {"text": "x", "delay_ms": 0}}
|
||||
run_protocol_rejection(command, json.dumps({**call, "id": 2}).encode())
|
||||
run_protocol_rejection(command, json.dumps({**call, "operation": "unknown"}).encode())
|
||||
run_protocol_rejection(command, (json.dumps(call)[:-2] + ',"text":"duplicate"}}').encode())
|
||||
run_protocol_rejection(command, (json.dumps(call)[:-1] + ',"extra":true}').encode())
|
||||
run_protocol_rejection(command, b'{"kind":"call","id":1,"operation":"text.render","payload":NaN}')
|
||||
run_protocol_rejection(command, b'{"kind":"call","id":1,"operation":"text.render","payload":1e999}')
|
||||
nested = 0
|
||||
for _ in range(64):
|
||||
nested = [nested]
|
||||
run_protocol_rejection(command, json.dumps({**call, "payload": nested}).encode())
|
||||
run_protocol_rejection(command, json.dumps({**call, "payload": [0] * 65_537}).encode())
|
||||
run_protocol_rejection(command, b"", declared_size=16 * 1024 * 1024 + 1)
|
||||
run_unresponsive_handler(command, {"kind": "cancel", "id": 1})
|
||||
run_unresponsive_handler(command, {"kind": "shutdown"})
|
||||
run_typed_case(typed[language], keep_input_open=language == "Python")
|
||||
print(f"{language}: handshake, health, result, business error, cancel, shutdown and rejection passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,121 @@
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
COMPILER = None
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MANIFEST = {
|
||||
"port": "example.Native", "adapter": "example.C", "schema": "ffi-test-v1",
|
||||
"capabilities": [], "operations": ["triple", "reject", "fault", "bad_length"],
|
||||
}
|
||||
HELLO = MANIFEST | {"kind": "hello", "version": "dever-component-1", "setting": {}}
|
||||
|
||||
|
||||
def frame(message):
|
||||
data = json.dumps(message, separators=(",", ":")).encode()
|
||||
return struct.pack(">I", len(data)) + data
|
||||
|
||||
|
||||
def responses(data):
|
||||
values = []
|
||||
while data:
|
||||
if len(data) < 4:
|
||||
raise AssertionError("truncated frame")
|
||||
size = struct.unpack(">I", data[:4])[0]
|
||||
if not 0 < size <= 16 * 1024 * 1024 or size > len(data) - 4:
|
||||
raise AssertionError("invalid frame size")
|
||||
values.append(json.loads(data[4:4 + size]))
|
||||
data = data[4 + size:]
|
||||
return values
|
||||
|
||||
|
||||
class NativeFfi(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.temporary = tempfile.TemporaryDirectory(prefix="dever-c-ffi-")
|
||||
cls.addClassCleanup(cls.temporary.cleanup)
|
||||
cls.directory = Path(cls.temporary.name)
|
||||
(cls.directory / "lib").mkdir()
|
||||
library = cls.directory / "library.c"
|
||||
library.write_text("long long fixture_triple(long long value) { return value * 3; }\n")
|
||||
subprocess.run([COMPILER, "-shared", "-fPIC", "-Wl,-soname,libfixture.so", str(library), "-o", str(cls.directory / "lib/libfixture.so")], check=True, timeout=30)
|
||||
cls.binary = cls.directory / "worker"
|
||||
subprocess.run([
|
||||
COMPILER, "-std=c11", "-Wall", "-Wextra", "-Werror", "-I", str(ROOT / "sdk/c"),
|
||||
str(ROOT / "sdk/c/dever_ffi.c"), str(ROOT / "test/external-sdk/c_ffi_adapter.c"),
|
||||
"-L", str(cls.directory / "lib"), "-lfixture", "-Wl,-rpath,$ORIGIN/lib", "-o", str(cls.binary),
|
||||
], check=True, timeout=30)
|
||||
(cls.directory / "contract.json").write_text(json.dumps(MANIFEST, indent=2))
|
||||
|
||||
def execute(self, messages, suffix=b""):
|
||||
result = subprocess.run([str(self.binary)], input=b"".join(frame(message) for message in messages) + suffix, cwd=self.directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5, env={})
|
||||
return result, responses(result.stdout)
|
||||
|
||||
def test_c_abi_calls_a_shared_library_and_preserves_protocol_lifecycle(self):
|
||||
result, values = self.execute([
|
||||
HELLO, {"kind": "health", "id": 0},
|
||||
{"kind": "call", "id": 1, "operation": "triple", "payload": {"value": 7}},
|
||||
{"kind": "cancel", "id": 1},
|
||||
{"kind": "call", "id": 2, "operation": "reject", "payload": {}},
|
||||
{"kind": "call", "id": 3, "operation": "triple", "payload": {"value": -9}},
|
||||
{"kind": "shutdown"},
|
||||
])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(values, [
|
||||
MANIFEST | {"kind": "ready", "version": "dever-component-1"},
|
||||
{"kind": "health", "id": 0},
|
||||
{"kind": "result", "id": 1, "payload": {"value": 21}},
|
||||
{"kind": "error", "id": 2, "error": "example.Rejected", "payload": None},
|
||||
{"kind": "result", "id": 3, "payload": {"value": -27}},
|
||||
{"kind": "shutdown"},
|
||||
])
|
||||
self.assertEqual(result.stderr, b"closed\n")
|
||||
|
||||
def test_identity_cannot_be_supplied_by_the_caller(self):
|
||||
for changed in [{"schema": "wrong"}, {"capabilities": ["gpu"]}, {"port": "wrong"}, {"operations": ["other"]}]:
|
||||
with self.subTest(changed=changed):
|
||||
result, values = self.execute([HELLO | changed])
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(values, [])
|
||||
self.assertNotIn(b"closed", result.stderr)
|
||||
|
||||
def test_invalid_calls_and_native_results_fail_closed(self):
|
||||
for call in [
|
||||
{"id": 2, "operation": "triple"},
|
||||
{"id": 1, "operation": "missing"},
|
||||
{"id": 1, "operation": "bad_length"},
|
||||
{"id": 1, "operation": "fault"},
|
||||
{"id": -1, "operation": "triple"},
|
||||
{"id": 9223372036854775808, "operation": "triple"},
|
||||
]:
|
||||
with self.subTest(call=call):
|
||||
result, values = self.execute([HELLO, {"kind": "call", "payload": {"value": 7}} | call])
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(len(values), 1)
|
||||
self.assertIn(b"closed\n", result.stderr)
|
||||
|
||||
def test_truncated_oversized_and_malformed_frames_fail_closed(self):
|
||||
for suffix in [b"\x00", struct.pack(">I", 0), struct.pack(">I", 16777217), struct.pack(">I", 5) + b"{}", frame({"kind": "shutdown", "extra": 1})]:
|
||||
with self.subTest(suffix=suffix):
|
||||
result, values = self.execute([HELLO], suffix)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(len(values), 1)
|
||||
for malformed in [b"{", b'{"kind":"call","id":1e}', b'{"kind":"\\q"}', b"[" * 65 + b"]" * 65, b'{"kind":"shutdown"}\x00']:
|
||||
result, values = self.execute([HELLO], struct.pack(">I", len(malformed)) + malformed)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(len(values), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--cc", required=True)
|
||||
options = parser.parse_args()
|
||||
if not Path(options.cc).is_absolute() or not Path(options.cc).is_file():
|
||||
parser.error("--cc must be an explicit absolute compiler file")
|
||||
COMPILER = options.cc
|
||||
unittest.main(argv=[__file__], verbosity=2)
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"port": "example.Typed",
|
||||
"adapter": "example.Managed",
|
||||
"ecosystem": "exec",
|
||||
"entry": "test/typed",
|
||||
"schema": "typed-fixture-v1",
|
||||
"libs": [],
|
||||
"capabilities": [],
|
||||
"operations": ["render"],
|
||||
"setting": {"type": "record", "fields": {"prefix": {"type": "text"}}},
|
||||
"inputs": {"render": {"type": "record", "fields": {
|
||||
"message": {"type": "record", "fields": {
|
||||
"name": {"type": "text"}, "number": {"type": "int64"},
|
||||
"labels": {"type": "list", "value": {"type": "text"}}
|
||||
}}
|
||||
}}},
|
||||
"outputs": {"render": {"type": "record", "fields": {"text": {"type": "text"}}}},
|
||||
"errors": {"example.Rejected": {"type": "record", "fields": {"reason": {"type": "text"}}}}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
component "dever-component"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bytes, err := os.ReadFile(os.Args[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var manifest component.Manifest
|
||||
if err = json.Unmarshal(bytes, &manifest); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
worker, err := component.NewFromManifest(manifest)
|
||||
if err == nil {
|
||||
err = worker.RegisterHandlers(map[string]func(context.Context, any, any) (any, error){
|
||||
"render": func(_ context.Context, payload any, setting any) (any, error) {
|
||||
message := payload.(map[string]any)["message"].(map[string]any)
|
||||
if message["name"] == "reject" {
|
||||
return nil, component.BusinessError{Identity: "example.Rejected", Payload: map[string]any{"reason": "rejected"}}
|
||||
}
|
||||
prefix := setting.(map[string]any)["prefix"].(string)
|
||||
return map[string]any{"text": prefix + message["name"].(string) + fmt.Sprint(message["number"])}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
if err == nil {
|
||||
err = worker.Serve(os.Stdin, os.Stdout)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "typed worker:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { BusinessError, Worker, main } from '../../sdk/javascript/dever_component.mjs';
|
||||
|
||||
const manifest = JSON.parse(readFileSync(process.argv[2], 'utf8'));
|
||||
const worker = Worker.fromManifest(manifest, {
|
||||
render: async (payload, setting) => {
|
||||
const message = payload.message;
|
||||
if (message.name === 'reject') throw new BusinessError('example.Rejected', { reason: 'rejected' });
|
||||
return { text: setting.prefix + message.name + String(message.number) };
|
||||
},
|
||||
});
|
||||
await main(worker);
|
||||
@@ -0,0 +1,17 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "sdk/python"))
|
||||
from dever_component import BusinessError, Worker, main
|
||||
|
||||
|
||||
async def render(payload, setting):
|
||||
message = payload["message"]
|
||||
if message["name"] == "reject":
|
||||
raise BusinessError("example.Rejected", {"reason": "rejected"})
|
||||
return {"text": setting["prefix"] + message["name"] + str(message["number"])}
|
||||
|
||||
|
||||
manifest = json.loads(Path(sys.argv[1]).read_text())
|
||||
main(Worker.from_manifest(manifest, {"render": render}))
|
||||
Reference in new issue
Block a user