Transport
The client talks to the cluster over its REST API using
ureq — a synchronous, pure-Rust HTTP client backed by
rustls. There is no async runtime and no OpenSSL, which keeps the
dependency tree vendorable and the build reproducible. Elasticsearch
and OpenSearch expose the same _search, _bulk,
_doc, _cat, and _cluster
endpoints, so a single client covers both without version-locked SDK
types.
Connection cache
A ureq::Agent owns an HTTP keep-alive connection pool. One
Agent is cached per (base_url, auth) tuple in a
OnceCell<Mutex<HashMap>> for the life of the
stryke process, so repeated Search::* calls reuse pooled
sockets instead of reconnecting. Auth precedence is api_key
(sent as ApiKey <key>) over username +
password (HTTP Basic), resolved once when the key is built.
FFI contract
Each #[no_mangle] extern "C" fn search__* is a
JSON-string-in / JSON-string-out wrapper. stryke's FFI bridge resolves
the symbols at first use Search, passes a JSON-encoded args
dict on each call, and copies the returned JSON into a stryke string;
stryke_free_cstring frees the allocation. Handlers run
inside catch_unwind, so a panic becomes an
{ "error": … } result rather than unwinding across the FFI
boundary.
Request building
Every network wrapper builds its request hash through one helper,
Search::_req. It copies the connection keys
(url, host, port,
username, password, api_key,
tls, params) from the caller's
%opts, filtering out undefined values so the cdylib
applies its own defaults (127.0.0.1:9200, scheme from
tls). When neither url nor host
is supplied it falls back to the $SEARCH_URL environment
variable. Per-operation fields (index, id, body, query, ops, …) are
merged in by each wrapper before the hash is JSON-encoded and handed to
the FFI symbol.
Error dispatch
Every wrapper routes its FFI return through
Search::_decode, which parses the JSON and, when the
result is a hash carrying an error key, raises
die "Search::<fn>: <reason>". Callers see a
stryke exception instead of a silent error hash. Boolean-style
wrappers (ping, index_exists,
doc_exists, index_template_exists) unwrap the
value field; list wrappers (index_list,
cat) dereference the returned array.
Pure builders
The query-DSL builders (match, term,
range, bool, multi_match,
geo_bounding_box, function_score,
more_like_this, …), the aggregation builders
(agg_terms, agg_date_histogram,
agg_top_hits, agg_global, …), the body and
field composers (query_body, sort,
highlight, bulk_ndjson, escape),
and the URL helpers (build_url, parse_url,
redact_url, valid_index_name) take no
connection. They return bare clauses that nest directly inside other
builders. They are unit-tested in the crate and pinned by the surface
test, so they validate in CI with no cluster present.
Test + build matrix
The crate is cdylib-only, so the integration test asserts
only that the crate type-checks and links. The real coverage is in
src/lib.rs's #[cfg(test)] module (NDJSON
build, Lucene escaping, URL parse / redact, basic-auth header,
percent-encoding), the t/test_stryke_search_surface.stk
surface pin (every Search::* wrapper resolves and every
builder produces the right shape, no cluster), and
t/test_search.stk (end-to-end CRUD + search against a live
cluster, short-circuited when none answers). CI runs
cargo check, fmt, clippy -D
warnings, cargo test on Ubuntu and macOS,
cargo doc -D warnings, release builds for the three host
triples, and the docs / README gate suite.