///|
/// The Neo4j HTTP transactional endpoint (`/db/{database}/tx`), an alternative
/// to Bolt for running Cypher over plain HTTP.
///
/// A request `POST`s a JSON body of the shape
/// `{"statements": [{"statement": "...", "parameters": {...}}]}` and the server
/// replies with `{"results": [{"columns": [...], "data": [{"row": [...], ...}]}],
/// "errors": [...]}`. This module builds the request and parses the response,
/// reusing [`PackStreamValue`] as the shared value model across both transports.
///
/// The actual HTTP `POST` is delegated to an [`HttpClient`]; MoonBit's core
/// library does not ship a socket/HTTP client as of this release, so a concrete
/// HTTP client is left to the target platform (需查官方文档 for the backend's
/// networking API). Everything here is testable against an in-memory client.
///|
/// A single statement in a transactional request.
pub struct Statement {
statement : String
parameters : Array[(String, PackStreamValue)]
} derive(Eq, @debug.Debug)
///|
pub fn Statement::new(
statement : String,
parameters : Array[(String, PackStreamValue)],
) -> Statement {
{ statement, parameters, }
}
///|
/// One result set returned by the server.
pub struct ResultSet {
columns : Array[String]
rows : Array[Array[PackStreamValue]]
} derive(Eq, @debug.Debug)
///|
/// An error reported by the server.
pub struct TxError {
code : String
message : String
} derive(Eq, @debug.Debug)
///|
/// The parsed response of a transactional request.
pub struct TxResponse {
results : Array[ResultSet]
errors : Array[TxError]
} derive(Eq, @debug.Debug)
///|
/// A minimal HTTP client used by the transactional endpoint.
pub trait HttpClient {
///| POST a JSON body to `url` and return the parsed JSON response, or `None`
///| on a transport failure.
fn post_json(Self, String, Json) -> Json?
}
///|
/// An in-memory [`HttpClient`] for tests. The response is fixed up front and
/// the request body is captured for inspection.
pub struct MockHttpClient {
response : Json?
mut last_body : Json?
}
///|
pub fn MockHttpClient::new(response : Json?) -> MockHttpClient {
{ response, last_body: None, }
}
///|
/// The body of the most recent `POST`.
pub fn MockHttpClient::last_body(self : MockHttpClient) -> Json? {
self.last_body
}
///|
pub impl HttpClient for MockHttpClient with fn post_json(self, _url, body) {
self.last_body = Some(body)
self.response
}
///|
/// Commit a batch of statements and parse the response. Returns `None` on a
/// transport failure or a malformed response.
pub fn[T : HttpClient] tx_commit(
client : T,
url : String,
statements : Array[Statement],
) -> TxResponse? {
let request = build_tx_request(statements)
match client.post_json(url, request) {
None => None
Some(response) => parse_tx_response(response)
}
}
///|
/// Build the JSON request body for a list of statements.
pub fn build_tx_request(statements : Array[Statement]) -> Json {
let stmts = []
for s in statements {
stmts.push(statement_to_json(s))
}
let root = @builtin.Map([])
root["statements"] = Json::array(stmts)
Json::object(root)
}
///|
/// Parse a transactional response body into a [`TxResponse`], or `None` when
/// the JSON does not have the expected shape.
pub fn parse_tx_response(json : Json) -> TxResponse? {
match json {
Object(obj) => {
let results = match obj.get("results") {
Some(Array(rs)) =>
match parse_result_sets(rs) {
Some(sets) => sets
None => return None
}
_ => return None
}
let errors = match obj.get("errors") {
Some(Array(errs)) => parse_errors(errs)
None => []
_ => return None
}
Some({ results, errors, })
}
_ => None
}
}
///|
/// Convert a PackStream value to its JSON representation. Structs and bytes
/// have no JSON-native form and map to `null`; use Bolt for those.
pub fn value_to_json(value : PackStreamValue) -> Json {
match value {
Null => Json::null()
Bool(b) => Json::boolean(b)
Int(i) => Json::number(i.to_double(), repr=i.to_string())
Float(f) => Json::number(f)
Str(s) => Json::string(s)
List(items) => {
let arr = []
for item in items {
arr.push(value_to_json(item))
}
Json::array(arr)
}
Map(entries) => {
let obj = @builtin.Map([])
for (k, v) in entries {
obj[k] = value_to_json(v)
}
Json::object(obj)
}
Struct(_, _) => Json::null()
Bytes(_) => Json::null()
}
}
///|
/// Convert a JSON value to its PackStream representation. JSON objects map to
/// PackStream maps (nodes/relationships are not distinguished here).
pub fn value_from_json(json : Json) -> PackStreamValue {
match json {
Null => PackStreamValue::null()
True => PackStreamValue::bool(true)
False => PackStreamValue::bool(false)
Number(n, ..) => number_value(n)
String(s) => PackStreamValue::str(s)
Array(items) => {
let out = []
for item in items {
out.push(value_from_json(item))
}
PackStreamValue::list(out)
}
Object(obj) => {
let out = []
for (k, v) in obj.to_array() {
out.push((k, value_from_json(v)))
}
PackStreamValue::map(out)
}
}
}
///|
/// Map a JSON number to a PackStream integer when it is a whole number within
/// the 53-bit safe-integer range, else a float.
fn number_value(d : Double) -> PackStreamValue {
if d == d.floor() && d >= -9007199254740992.0 && d <= 9007199254740992.0 {
PackStreamValue::int(d.to_int64())
} else {
PackStreamValue::float(d)
}
}
///|
fn statement_to_json(s : Statement) -> Json {
let obj = @builtin.Map([])
obj["statement"] = Json::string(s.statement)
obj["parameters"] = parameters_to_json(s.parameters)
Json::object(obj)
}
///|
fn parameters_to_json(params : Array[(String, PackStreamValue)]) -> Json {
let obj = @builtin.Map([])
for (k, v) in params {
obj[k] = value_to_json(v)
}
Json::object(obj)
}
///|
fn parse_result_sets(rs : Array[Json]) -> Array[ResultSet]? {
let out = []
for r in rs {
match parse_result_set(r) {
Some(set) => out.push(set)
None => return None
}
}
Some(out)
}
///|
fn parse_result_set(json : Json) -> ResultSet? {
match json {
Object(obj) => {
let columns = match obj.get("columns") {
Some(Array(cols)) =>
match parse_strings(cols) {
Some(names) => names
None => return None
}
_ => return None
}
let rows = match obj.get("data") {
Some(Array(data)) =>
match parse_rows(data) {
Some(rows) => rows
None => return None
}
_ => return None
}
Some({ columns, rows, })
}
_ => None
}
}
///|
fn parse_strings(arr : Array[Json]) -> Array[String]? {
let out = []
for s in arr {
match s {
String(str) => out.push(str)
_ => return None
}
}
Some(out)
}
///|
fn parse_rows(data : Array[Json]) -> Array[Array[PackStreamValue]]? {
let out = []
for row_obj in data {
match row_obj {
Object(o) =>
match o.get("row") {
Some(Array(cells)) => {
let row = []
for cell in cells {
row.push(value_from_json(cell))
}
out.push(row)
}
_ => return None
}
_ => return None
}
}
Some(out)
}
///|
fn parse_errors(errs : Array[Json]) -> Array[TxError] {
let out = []
for e in errs {
match e {
Object(o) => {
let code = match o.get("code") {
Some(String(s)) => s
_ => ""
}
let message = match o.get("message") {
Some(String(s)) => s
_ => ""
}
out.push({ code, message, })
}
_ => ()
}
}
out
}