Protocols

XML-RPC

XML-RPC calls and answers with every value type and faults, a client with system.multicall, and the server side for a route that answers XML-RPC itself.

Version 1.0.0 · 8 functions · 4 crates

#api#xml#integration#b2b

Functions

What use xml_rpc.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use xmlrpc_request, xmlrpc_response from xml_rpc (Quillo 0.6).

xmlrpc_request(method: str, params: list) → stror an error

A call as XML-RPC text, to send yourself: params is a list of values. Ints past 32 bits go as <i8> (the Apache extension most servers know), null as <nil/>; { "$base64": "..." } is a base64 value and { "$datetime": "2026-10-30T12:00:00" } a dateTime.iso8601 (xmlrpc_base64 and xmlrpc_datetime make them).

xmlrpc_response(value: any) → stror an error

An answer to a call, as XML-RPC text: for a route serving XML-RPC (respond(200, xmlrpc_response(v)?, { "content-type": "text/xml" })).

xmlrpc_fault(code: int, message: str) → str

A fault answer, as XML-RPC text: code and message (faultCode and faultString).

xmlrpc_base64(b64: str) → any

A base64 value for a call (b64 is base64 text): a file's bytes, an image.

xmlrpc_datetime(iso: str) → anyor an error

A dateTime.iso8601 value for a call, from 2026-10-30T12:00:00.

xmlrpc_parse(xml: str) → anyor an error

XML-RPC text read: a call is { kind: "call", method, params } (what a route serving XML-RPC gets in request.body), an answer { kind: "response", value }, a fault { kind: "fault", code, message }. A dateTime comes back as 1998-07-17T14:08:55, base64 as its text, a value with no type as a string.

xmlrpc_call(url: str, method: str, params: list, options: any) → anyor an error

Calls method with params at url (http:// or https://) and gives the answer's value; a fault is an error value, fault 4: Too many parameters. options: { timeout_ms (10000), user, password (basic auth), headers }.

xmlrpc_multicall(url: str, calls: list, options: any) → anyor an error

Several calls in one request with system.multicall: calls is [{ method, params }]; gives one item per call, { value } or { fault: { code, message } }, in order.

Get it

$ quillo install xml_rpc               # xml_rpc.qwer into this folder$ quillo install xml_rpc --example     # and its example, xml_rpc.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/xml_rpc.qwer$ curl -O https://quillolang.org/plugins/files/xml_rpc.qwe

Then, in your program, all of it or only what you call:

main.qwe
use xml_rpc.qweruse xmlrpc_request, xmlrpc_response from xml_rpc      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }roxmltree = "0.20"base64 = "0.22"ureq = "2"

Example

xml_rpc.qwe
// XML-RPC, still behind WordPress, Odoo, Bugzilla, Confluence's old API and many routers: calls// to a server (faults and system.multicall too), and this program serving XML-RPC itself.////   XMLRPC_URL=https://example.com/xmlrpc.php quillo run xml_rpc.qwe//   then open http://localhost:8080/call, /echo, /multicall and /serve; POST /RPC2 serves callsuse "xml_rpc.qwer"let URL = env("XMLRPC_URL", "http://127.0.0.1:8000/RPC2")route GET /call = {    state: xmlrpc_call(URL, "examples.getStateName", [41], {})?,    both: xmlrpc_call(URL, "sample.sumAndDifference", [10, 3], {})?,    fault: xmlrpc_call(URL, "examples.getStateName", [99], {}).error,    unknown: xmlrpc_call(URL, "examples.nothing", [], {}).error,}// Every type there and back: the server's echo gives its params back.route GET /echo {    let sent = [42, 9007199254740993, 3.25, true, null, "a < b & c", xmlrpc_datetime("2026-10-30T12:00:00")?, xmlrpc_base64("aGVsbG8="), { sku: "A-1", sizes: [38, 39] }]    { back: xmlrpc_call(URL, "echo", sent, { timeout_ms: 5000 })? }}route GET /multicall = xmlrpc_multicall(URL, [    { method: "examples.getStateName", params: [5] },    { method: "examples.getStateName", params: [0] },    { method: "system.listMethods" },], {})?// Serving XML-RPC: the methods this program answers.fn answer(call) = match call {    { method: "orders.status", params: [str as id] } => xmlrpc_response({ id, status: id == "A-1042" ? "shipped" : "unknown", updated: xmlrpc_datetime("2026-10-29T16:30:00")? })    { method: "orders.status" } => xmlrpc_fault(-32602, "orders.status takes one string, the order id")    { method: "math.add", params: [num as a, num as b] } => xmlrpc_response(a + b)    { method } => xmlrpc_fault(-32601, `no method ${method}`)}fn serve(body) {    let call = xmlrpc_parse(body)    if is_err(call) || call.kind != "call" { return xmlrpc_fault(-32700, `not an XML-RPC call: ${call.error ?? call.kind}`) }    answer(call)?}route POST /RPC2 = respond(200, serve(request.body), { "content-type": "text/xml" })// What a client sending to POST /RPC2 sees: its calls made here, and the answers read back.route GET /serve = {    status: xmlrpc_parse(serve(xmlrpc_request("orders.status", ["A-1042"])?))?,    sum: xmlrpc_parse(serve(xmlrpc_request("math.add", [2, 0.5])?))?.value,    wrong: xmlrpc_parse(serve(xmlrpc_request("orders.status", [7])?))?,    request: xmlrpc_request("orders.status", ["A-1042"])?,}
$ quillo run xml_rpc.qwe$ curl 'localhost:8080/call'$ curl 'localhost:8080/echo'$ curl 'localhost:8080/multicall'$ curl 'localhost:8080/serve'

It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.

Source

xml_rpc.qwer
//! XML-RPC both ways: calls and answers built and read with every value type (int, i8, boolean, string, double, dateTime, base64, struct, array, nil) and faults, a client over HTTP with system.multicall, and the server side for routes (a call read, an answer or a fault written).//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }//! roxmltree = "0.20"//! base64 = "0.22"//! ureq = "2"use serde_json::{json, Value};fn esc(s: &str) -> String {    let mut o = String::with_capacity(s.len());    for c in s.chars() {        match c {            '&' => o.push_str("&amp;"),            '<' => o.push_str("&lt;"),            '>' => o.push_str("&gt;"),            '\r' => o.push_str("&#13;"),            c if (c as u32) < 0x20 && c != '\n' && c != '\t' => {}            c => o.push(c),        }    }    o}/// `2026-10-30T12:00:00` (or `...Z`, or the XML-RPC form) as XML-RPC writes it: `20261030T12:00:00`.fn to_iso8601_basic(s: &str) -> Result<String, String> {    let t = s.trim().trim_end_matches('Z');    let t = if t.len() > 19 && t.as_bytes()[10] == b'T' { &t[..19] } else { t };    let compact = t.replacen('-', "", 2);    let ok = compact.len() == 17 && compact.as_bytes()[8] == b'T' && compact[..8].bytes().all(|b| b.is_ascii_digit());    if !ok {        return Err(format!("{:?} is not a date and time (2026-10-30T12:00:00)", s));    }    Ok(compact)}fn value_xml(v: &Value, out: &mut String, depth: usize) -> Result<(), String> {    if depth > 64 {        return Err("values nested more than 64 deep".into());    }    out.push_str("<value>");    match v {        Value::Null => out.push_str("<nil/>"),        Value::Bool(b) => out.push_str(&format!("<boolean>{}</boolean>", if *b { 1 } else { 0 })),        Value::Number(n) => {            if let Some(i) = n.as_i64() {                if i >= i32::MIN as i64 && i <= i32::MAX as i64 {                    out.push_str(&format!("<int>{}</int>", i));                } else {                    out.push_str(&format!("<i8>{}</i8>", i));                }            } else {                let f = n.as_f64().unwrap_or(0.0);                let mut s = format!("{}", f);                if !s.contains('.') && !s.contains('e') {                    s.push_str(".0");                }                out.push_str(&format!("<double>{}</double>", s));            }        }        Value::String(s) => out.push_str(&format!("<string>{}</string>", esc(s))),        Value::Array(a) => {            out.push_str("<array><data>");            for x in a {                value_xml(x, out, depth + 1)?;            }            out.push_str("</data></array>");        }        Value::Object(m) => {            if m.len() == 1 {                if let Some(Value::String(b)) = m.get("$base64") {                    out.push_str(&format!("<base64>{}</base64>", esc(b.trim())));                    out.push_str("</value>");                    return Ok(());                }                if let Some(Value::String(d)) = m.get("$datetime") {                    out.push_str(&format!("<dateTime.iso8601>{}</dateTime.iso8601>", to_iso8601_basic(d)?));                    out.push_str("</value>");                    return Ok(());                }            }            out.push_str("<struct>");            for (k, x) in m {                out.push_str(&format!("<member><name>{}</name>", esc(k)));                value_xml(x, out, depth + 1)?;                out.push_str("</member>");            }            out.push_str("</struct>");        }    }    out.push_str("</value>");    Ok(())}fn params_xml(params: &[Value], out: &mut String) -> Result<(), String> {    out.push_str("<params>");    for p in params {        out.push_str("<param>");        value_xml(p, out, 0)?;        out.push_str("</param>");    }    out.push_str("</params>");    Ok(())}fn call_xml(method: &str, params: &[Value]) -> Result<String, String> {    if method.is_empty() || !method.chars().all(|c| c.is_ascii_alphanumeric() || "_.:/".contains(c)) {        return Err(format!("{:?} is not a method name (letters, digits, _ . : /)", method));    }    let mut out = format!("<?xml version=\"1.0\"?>\n<methodCall><methodName>{}</methodName>", method);    params_xml(params, &mut out)?;    out.push_str("</methodCall>\n");    Ok(out)}/// A call as XML-RPC text, to send yourself: `params` is a list of values. Ints past 32 bits go as `<i8>` (the Apache extension most servers know), null as `<nil/>`; `{ "$base64": "..." }` is a base64 value and `{ "$datetime": "2026-10-30T12:00:00" }` a dateTime.iso8601 (`xmlrpc_base64` and `xmlrpc_datetime` make them).pub fn xmlrpc_request(method: &str, params: Vec<serde_json::Value>) -> Result<String, String> {    call_xml(method, &params)}/// An answer to a call, as XML-RPC text: for a route serving XML-RPC (`respond(200, xmlrpc_response(v)?, { "content-type": "text/xml" })`).pub fn xmlrpc_response(value: serde_json::Value) -> Result<String, String> {    let mut out = String::from("<?xml version=\"1.0\"?>\n<methodResponse>");    params_xml(&[value], &mut out)?;    out.push_str("</methodResponse>\n");    Ok(out)}/// A fault answer, as XML-RPC text: `code` and `message` (faultCode and faultString).pub fn xmlrpc_fault(code: i64, message: &str) -> String {    let mut out = String::from("<?xml version=\"1.0\"?>\n<methodResponse><fault>");    let _ = value_xml(&json!({ "faultCode": code, "faultString": message }), &mut out, 0);    out.push_str("</fault></methodResponse>\n");    out}/// A base64 value for a call (`b64` is base64 text): a file's bytes, an image.pub fn xmlrpc_base64(b64: &str) -> Value {    json!({ "$base64": b64 })}/// A dateTime.iso8601 value for a call, from `2026-10-30T12:00:00`.pub fn xmlrpc_datetime(iso: &str) -> Result<Value, String> {    to_iso8601_basic(iso)?;    Ok(json!({ "$datetime": iso }))}// ---------------------------------------------------------------- readingfn elems<'a, 'i>(n: roxmltree::Node<'a, 'i>) -> impl Iterator<Item = roxmltree::Node<'a, 'i>> {    n.children().filter(|c| c.is_element())}fn child<'a, 'i>(n: roxmltree::Node<'a, 'i>, name: &str) -> Option<roxmltree::Node<'a, 'i>> {    elems(n).find(|c| c.tag_name().name() == name)}fn text_of(n: roxmltree::Node) -> String {    n.descendants().filter(|d| d.is_text()).map(|d| d.text().unwrap_or("")).collect()}fn read_value(v: roxmltree::Node, depth: usize) -> Result<Value, String> {    if depth > 64 {        return Err("values nested more than 64 deep".into());    }    let typed = elems(v).next();    let t = match typed {        None => return Ok(json!(text_of(v))), // no type: a string        Some(t) => t,    };    let name = t.tag_name().name();    let text = text_of(t);    let s = text.trim();    Ok(match name {        "i4" | "int" | "i1" | "i2" | "i8" | "biginteger" => json!(s.parse::<i64>().map_err(|_| format!("<{}>{}</{}> is not an integer", name, s, name))?),        "boolean" => match s { "1" | "true" => json!(true), "0" | "false" => json!(false), _ => return Err(format!("<boolean>{}</boolean> is neither 0 nor 1", s)) },        "string" => json!(text),        "double" | "float" | "bigdecimal" => {            let f: f64 = s.parse().map_err(|_| format!("<double>{}</double> is not a number", s))?;            serde_json::Number::from_f64(f).map(Value::Number).unwrap_or(Value::Null)        }        "dateTime.iso8601" => {            // 19980717T14:08:55 (sometimes with dashes or a zone): given back as 1998-07-17T14:08:55.            let c = s.replace('-', "");            if c.len() >= 17 && c.as_bytes()[8] == b'T' {                json!(format!("{}-{}-{}{}", &c[..4], &c[4..6], &c[6..8], &c[8..]))            } else {                json!(s)            }        }        "base64" => json!(s.split_whitespace().collect::<String>()),        "nil" => Value::Null,        "array" => {            let data = child(t, "data").ok_or("an <array> has a <data>")?;            let mut out = vec![];            for x in elems(data).filter(|e| e.tag_name().name() == "value") {                out.push(read_value(x, depth + 1)?);            }            Value::Array(out)        }        "struct" => {            let mut m = serde_json::Map::new();            for mem in elems(t).filter(|e| e.tag_name().name() == "member") {                let k = child(mem, "name").map(text_of).ok_or("a <member> has a <name>")?;                let val = child(mem, "value").ok_or("a <member> has a <value>")?;                m.insert(k, read_value(val, depth + 1)?);            }            Value::Object(m)        }        other => return Err(format!("unknown XML-RPC type <{}>", other)),    })}fn read_params(n: Option<roxmltree::Node>) -> Result<Vec<Value>, String> {    let mut out = vec![];    if let Some(ps) = n {        for p in elems(ps).filter(|e| e.tag_name().name() == "param") {            let v = child(p, "value").ok_or("a <param> has a <value>")?;            out.push(read_value(v, 0)?);        }    }    Ok(out)}fn parse(xml: &str) -> Result<Value, String> {    let opts = roxmltree::ParsingOptions { allow_dtd: false, ..Default::default() };    let doc = roxmltree::Document::parse_with_options(xml.trim_start_matches('\u{feff}'), opts).map_err(|e| format!("not XML: {}", e))?;    let root = doc.root_element();    match root.tag_name().name() {        "methodCall" => {            let method = child(root, "methodName").map(|n| text_of(n).trim().to_string()).ok_or("a <methodCall> has a <methodName>")?;            Ok(json!({ "kind": "call", "method": method, "params": read_params(child(root, "params"))? }))        }        "methodResponse" => {            if let Some(f) = child(root, "fault") {                let v = child(f, "value").ok_or("a <fault> has a <value>")?;                let s = read_value(v, 0)?;                return Ok(json!({ "kind": "fault", "code": s["faultCode"], "message": s["faultString"] }));            }            let ps = read_params(child(root, "params"))?;            Ok(json!({ "kind": "response", "value": ps.into_iter().next().unwrap_or(Value::Null) }))        }        other => Err(format!("an XML-RPC message is a <methodCall> or a <methodResponse>, not <{}>", other)),    }}/// XML-RPC text read: a call is `{ kind: "call", method, params }` (what a route serving XML-RPC gets in `request.body`), an answer `{ kind: "response", value }`, a fault `{ kind: "fault", code, message }`. A dateTime comes back as `1998-07-17T14:08:55`, base64 as its text, a value with no type as a string.pub fn xmlrpc_parse(xml: &str) -> Result<Value, String> {    parse(xml)}// ---------------------------------------------------------------- clientfn post(url: &str, body: &str, opts: &Value) -> Result<String, String> {    let ms = opts["timeout_ms"].as_i64().unwrap_or(10_000).clamp(100, 300_000) as u64;    let agent = ureq::AgentBuilder::new().timeout(std::time::Duration::from_millis(ms)).build();    let mut req = agent.post(url).set("Content-Type", "text/xml").set("User-Agent", "quillo-xml-rpc/1.0");    if let (Some(u), Some(p)) = (opts["user"].as_str(), opts["password"].as_str()) {        use base64::Engine;        req = req.set("Authorization", &format!("Basic {}", base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", u, p))));    }    if let Some(h) = opts["headers"].as_object() {        for (k, v) in h {            if let Some(v) = v.as_str() {                req = req.set(k, v);            }        }    }    match req.send_string(body) {        Ok(r) => r.into_string().map_err(|e| e.to_string()),        Err(ureq::Error::Status(code, r)) => {            let t = r.into_string().unwrap_or_default();            // Some servers send a fault with a 500.            if t.contains("<methodResponse") {                return Ok(t);            }            Err(format!("the XML-RPC server answered HTTP {}{}", code, if code == 401 { " (give user and password)" } else { "" }))        }        Err(e) => Err(format!("cannot reach {}: {}", url, e)),    }}/// Calls `method` with `params` at `url` (http:// or https://) and gives the answer's value; a fault is an error value, `fault 4: Too many parameters`. `options`: `{ timeout_ms (10000), user, password (basic auth), headers }`.pub fn xmlrpc_call(url: &str, method: &str, params: Vec<serde_json::Value>, options: serde_json::Value) -> Result<Value, String> {    let r = parse(&post(url, &call_xml(method, &params)?, &options)?)?;    match r["kind"].as_str() {        Some("response") => Ok(r["value"].clone()),        Some("fault") => Err(format!("fault {}: {}", r["code"], r["message"].as_str().unwrap_or(""))),        _ => Err("the server answered with a call, not a response".into()),    }}/// Several calls in one request with `system.multicall`: `calls` is `[{ method, params }]`; gives one item per call, `{ value }` or `{ fault: { code, message } }`, in order.pub fn xmlrpc_multicall(url: &str, calls: Vec<serde_json::Value>, options: serde_json::Value) -> Result<Value, String> {    let list: Vec<Value> = calls        .iter()        .map(|c| json!({ "methodName": c["method"], "params": if c["params"].is_array() { c["params"].clone() } else { json!([]) } }))        .collect();    let r = parse(&post(url, &call_xml("system.multicall", &[Value::Array(list)])?, &options)?)?;    if r["kind"] == "fault" {        return Err(format!("fault {}: {}", r["code"], r["message"].as_str().unwrap_or("")));    }    let items = r["value"].as_array().ok_or("system.multicall answered with something other than a list")?;    Ok(Value::Array(        items            .iter()            .map(|i| match i {                // A result is a list of one; a fault a struct.                Value::Array(a) => json!({ "value": a.first().cloned().unwrap_or(Value::Null) }),                Value::Object(f) => json!({ "fault": { "code": f.get("faultCode"), "message": f.get("faultString") } }),                other => json!({ "value": other }),            })            .collect(),    ))}
SOAP soap.qwer · 1.0.0SOAP 1.1 and 1.2 calls: WSDL files read for their operations and endpoints, envelopes built with WS-Security, operations called by name, answers and faults read into maps, and a route answering SOAP.apixmlb2bintegration6 views · 9 downloadsgRPC grpc.qwer · 1.0.0gRPC calls with no generated code: a .proto file read when the program runs, or the server's reflection, with JSON in and out, streams both ways, metadata, deadlines, status codes and TLS.apiprotobufhttpintegration7 views · 7 downloadsJSON-RPC json_rpc.qwer · 1.0.0JSON-RPC 2.0 calls, notifications and batches over HTTP or a WebSocket kept open, with the server's own notifications buffered; and a route serving JSON-RPC from a table of methods, errors and batches as the specification says.apijsonwebsocketsintegration7 views · 10 downloadsEDIFACT edifact.qwer · 1.0.0UN/EDIFACT for European retail and logistics: interchanges parsed and checked, ORDERS, INVOIC and DESADV read into parties, lines and totals, interchanges built with escapes and counts, and CONTRL acknowledgments.b2bintegrationinvoicesshipping8 views · 9 downloadsProtobuf wire format protobuf_wire.qwer · 1.0.0Protocol Buffers without generated code: any message decoded field by field with no schema, decoded and encoded by name with the field numbers given, and length-delimited files split.binaryapiimportintegration7 views · 8 downloadsX12 EDI x12.qwer · 1.0.0ANSI X12 EDI for trading with retailers and carriers: interchanges parsed and checked, 850 orders, 810 invoices and 856 ship notices read into plain fields, interchanges built with their envelopes, and 997 acknowledgments.b2bintegrationinvoicesshipping8 views · 6 downloads
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

On XML-RPC: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.