use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use async_trait::async_trait;
use base64::Engine;
use futures::future::{select, Either};
use ikigai_core::{
ArgRef, Capability, Error, Iri, Kernel, ReprType, Representation, Request, Result, Verb,
};
use oxrdf::{Literal, Term, Triple};
use crate::rdf::{self, node, object_iri, subject_iri};
pub const NS: &str = "urn:layers:gateway:";
pub const SHAPES: &str = include_str!("../gateway/layers/gateway.shapes.ttl");
pub const CONTEXT: &str = include_str!("../gateway/layers/gateway.context.jsonld");
pub const EVENT_SHAPES: &str = include_str!("../gateway/layers/gateway-events.shapes.ttl");
pub const EVENT_CONTEXT: &str = include_str!("../gateway/layers/gateway-events.context.jsonld");
pub const ERROR_KINDS: [&str; 10] = [
"unresolved",
"missing-argument",
"invalid-argument",
"endpoint",
"denied",
"not-found",
"conflict",
"timeout",
"unavailable",
"depth-exceeded",
];
pub const MAX_MESSAGE: usize = 4000;
pub const MAX_ARGUMENT_NAME: usize = 64;
pub const MAX_MEDIA_TYPE: usize = 255;
pub fn term(local: &str) -> String {
format!("{NS}{local}")
}
fn ik(local: &str) -> String {
format!("{}{local}", rdf::IK)
}
const XSD_BASE64: &str = "http://www.w3.org/2001/XMLSchema#base64Binary";
pub fn verb_word(verb: Verb) -> &'static str {
match verb {
Verb::Source => "Source",
Verb::Sink => "Sink",
Verb::Exists => "Exists",
Verb::Delete => "Delete",
Verb::Meta => "Meta",
}
}
pub fn verb_of(word: &str) -> Option<Verb> {
[
Verb::Source,
Verb::Sink,
Verb::Exists,
Verb::Delete,
Verb::Meta,
]
.into_iter()
.find(|v| verb_word(*v) == word)
}
pub fn request_subject(layer: &str, correlation: &str) -> String {
format!("urn:layer:{layer}:request:{correlation}")
}
pub fn reply_subject(layer: &str, correlation: &str) -> String {
format!("urn:layer:{layer}:reply:{correlation}")
}
fn tuples(layer: &str) -> String {
format!("urn:layer:{layer}:tuples")
}
fn layer_request(verb: Verb, iri: &str, args: &[(&str, &[u8])]) -> Request {
let mut request = Request::new(
verb,
Iri::parse(iri).unwrap_or_else(|e| panic!("`{iri}`: {e:?}")),
);
for (name, value) in args {
request = request.with_arg(*name, ArgRef::Inline(value.to_vec()));
}
request
}
pub fn is_correlation(correlation: &str) -> bool {
let mut bytes = correlation.bytes();
matches!(bytes.next(), Some(b) if b.is_ascii_alphanumeric())
&& correlation.len() <= 64
&& bytes.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Ask {
pub verb: Verb,
pub resource: Iri,
pub arguments: Vec<(String, String)>,
pub resolver: Option<String>,
}
impl Ask {
pub fn new(verb: Verb, resource: Iri) -> Ask {
Ask {
verb,
resource,
arguments: Vec::new(),
resolver: None,
}
}
pub fn arg(mut self, name: impl Into<String>, value: impl Into<String>) -> Ask {
self.arguments.push((name.into(), value.into()));
self
}
pub fn to(mut self, resolver: impl Into<String>) -> Ask {
self.resolver = Some(resolver.into());
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Asked {
pub subject: String,
pub correlation: String,
pub asker: String,
pub ask: Ask,
}
pub fn request_turtle(layer: &str, correlation: &str, ask: &Ask) -> Result<String> {
if !is_correlation(correlation) {
return Err(Error::InvalidArgument {
name: "correlation".into(),
detail: format!(
"`{correlation}` is not a correlation id: 1 to 64 of [0-9A-Za-z._-], starting \
with a letter or digit"
),
});
}
let subject = node(&request_subject(layer, correlation));
let mut triples = vec![
Triple::new(subject.clone(), node(rdf::RDF_TYPE), node(&term("Request"))),
Triple::new(
subject.clone(),
node(&term("correlation")),
Literal::new_simple_literal(correlation),
),
Triple::new(
subject.clone(),
node(&term("resource")),
node(ask.resource.as_str()),
),
Triple::new(
subject.clone(),
node(&ik("verb")),
Literal::new_simple_literal(verb_word(ask.verb)),
),
];
for (name, value) in &ask.arguments {
if !is_argument_name(name) {
return Err(Error::InvalidArgument {
name: name.clone(),
detail: "not an argument name a gateway carries: [A-Za-z_][A-Za-z0-9_.-]*, at \
most 64"
.into(),
});
}
triples.push(Triple::new(
subject.clone(),
node(&term("argument")),
Literal::new_simple_literal(format!("{name}={value}")),
));
}
if let Some(resolver) = &ask.resolver {
let to = oxrdf::NamedNode::new(resolver.as_str()).map_err(|e| Error::InvalidArgument {
name: "resolver".into(),
detail: format!("`{resolver}` is not an IRI: {e}"),
})?;
triples.push(Triple::new(subject, node(&rdf::ik_addressed_to()), to));
}
Ok(rdf::to_turtle_with(&triples, &[("gw", NS)]))
}
fn is_argument_name(name: &str) -> bool {
let mut bytes = name.bytes();
matches!(bytes.next(), Some(b) if b.is_ascii_alphabetic() || b == b'_')
&& name.len() <= MAX_ARGUMENT_NAME
&& bytes.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b'-'))
}
fn about(text: &str, subject: &str) -> std::result::Result<BTreeMap<String, Vec<Term>>, String> {
let mut found: BTreeMap<String, Vec<Term>> = BTreeMap::new();
for t in rdf::parse_turtle(text)? {
if subject_iri(&t.subject) == Some(subject) {
found
.entry(t.predicate.as_str().to_string())
.or_default()
.push(t.object);
}
}
if found.is_empty() {
return Err(format!("nothing about <{subject}>"));
}
Ok(found)
}
fn one_literal(found: &BTreeMap<String, Vec<Term>>, p: &str) -> Option<String> {
match found.get(p).map(Vec::as_slice) {
Some([Term::Literal(l)]) => Some(l.value().to_string()),
_ => None,
}
}
fn one_iri(found: &BTreeMap<String, Vec<Term>>, p: &str) -> Option<String> {
match found.get(p).map(Vec::as_slice) {
Some([object]) => object_iri(object).map(str::to_string),
_ => None,
}
}
fn types(found: &BTreeMap<String, Vec<Term>>) -> Vec<String> {
found
.get(rdf::RDF_TYPE)
.into_iter()
.flatten()
.filter_map(object_iri)
.map(str::to_string)
.collect()
}
pub fn parse_request(layer: &str, text: &str) -> std::result::Result<Asked, String> {
let triples = rdf::parse_turtle(text)?;
let prefix = format!("urn:layer:{layer}:request:");
let subject = triples
.iter()
.filter_map(|t| subject_iri(&t.subject))
.find(|s| s.starts_with(&prefix))
.ok_or_else(|| format!("no request of layer `{layer}`"))?
.to_string();
let found = about(text, &subject)?;
if !types(&found).contains(&term("Request")) {
return Err(format!("<{subject}> is not a gw:Request"));
}
let correlation =
one_literal(&found, &term("correlation")).ok_or("no single gw:correlation")?;
if subject != request_subject(layer, &correlation) {
return Err(format!(
"<{subject}> carries gw:correlation `{correlation}`, which names another request"
));
}
let resource = one_iri(&found, &term("resource")).ok_or("no single gw:resource")?;
let resource = Iri::parse(resource).map_err(|e| format!("gw:resource: {e:?}"))?;
let verb = one_literal(&found, &ik("verb"))
.and_then(|w| verb_of(&w))
.ok_or("no single ik:verb of the five")?;
let asker = one_iri(&found, rdf::PROV_ATTRIBUTED).ok_or("no prov:wasAttributedTo")?;
let mut arguments = Vec::new();
for value in found.get(&term("argument")).into_iter().flatten() {
let Term::Literal(l) = value else {
return Err("a gw:argument that is not a literal".into());
};
let (name, value) = l
.value()
.split_once('=')
.ok_or("a gw:argument that is not name=value")?;
arguments.push((name.to_string(), value.to_string()));
}
arguments.sort();
Ok(Asked {
subject,
correlation,
asker,
ask: Ask {
verb,
resource,
arguments,
resolver: one_iri(&found, &rdf::ik_addressed_to()),
},
})
}
pub fn reply_turtle(layer: &str, asked: &Asked, answer: &Result<Representation>) -> String {
let subject = node(&reply_subject(layer, &asked.correlation));
let mut triples = vec![
Triple::new(
subject.clone(),
node(&rdf::ik_offer()),
node(&asked.subject),
),
Triple::new(
subject.clone(),
node(&rdf::ik_addressed_to()),
node(&asked.asker),
),
Triple::new(
subject.clone(),
node(&term("correlation")),
Literal::new_simple_literal(&asked.correlation),
),
];
let literal =
|p: &str, v: String| Triple::new(subject.clone(), node(p), Literal::new_simple_literal(v));
match answer {
Ok(repr) => {
triples.push(Triple::new(
subject.clone(),
node(rdf::RDF_TYPE),
node(&term("Reply")),
));
triples.push(literal(&ik("mediaType"), repr.repr_type.canonical()));
triples.push(Triple::new(
subject.clone(),
node(&term("bytes")),
Literal::new_typed_literal(
base64::engine::general_purpose::STANDARD.encode(&repr.bytes),
node(XSD_BASE64),
),
));
}
Err(error) => {
triples.push(Triple::new(
subject.clone(),
node(rdf::RDF_TYPE),
node(&term("Failure")),
));
triples.push(literal(&term("errorKind"), error.kind().to_string()));
let (message, argument, target, depth) = parts(error);
if let Some(message) = message {
triples.push(literal(&term("message"), cut(&message, MAX_MESSAGE)));
}
if let Some(argument) = argument {
triples.push(literal(
&term("argumentName"),
cut(&argument, MAX_ARGUMENT_NAME),
));
}
if let Some(target) = target {
triples.push(Triple::new(
subject.clone(),
node(&term("target")),
node(target.as_str()),
));
}
if let Some(depth) = depth {
triples.push(Triple::new(
subject.clone(),
node(&term("depth")),
Literal::new_typed_literal(depth.to_string(), node(rdf::XSD_INTEGER)),
));
}
}
}
rdf::to_turtle_with(&triples, &[("gw", NS)])
}
type Parts<'a> = (Option<String>, Option<String>, Option<&'a Iri>, Option<u32>);
fn parts(error: &Error) -> Parts<'_> {
match error {
Error::Unresolved(iri) => (None, None, Some(iri), None),
Error::MissingArgument(name) => (None, Some(name.clone()), None, None),
Error::InvalidArgument { name, detail } => {
(Some(detail.clone()), Some(name.clone()), None, None)
}
Error::Endpoint(m)
| Error::Denied(m)
| Error::NotFound(m)
| Error::Conflict(m)
| Error::Timeout(m)
| Error::Unavailable(m) => (Some(m.clone()), None, None, None),
Error::DepthExceeded { depth, target } => (None, None, Some(target), Some(*depth)),
other => (Some(other.to_string()), None, None, None),
}
}
fn cut(text: &str, max: usize) -> String {
if text.chars().count() <= max {
return text.to_string();
}
text.chars().take(max).collect()
}
pub fn parse_reply(
layer: &str,
correlation: &str,
text: &str,
) -> std::result::Result<Result<Representation>, String> {
let subject = reply_subject(layer, correlation);
let found = about(text, &subject)?;
if one_literal(&found, &term("correlation")).as_deref() != Some(correlation) {
return Err(format!(
"<{subject}> does not carry gw:correlation `{correlation}`"
));
}
let kinds = types(&found);
if kinds.contains(&term("Reply")) {
let media = one_literal(&found, &ik("mediaType")).ok_or("no single ik:mediaType")?;
let bytes = one_literal(&found, &term("bytes")).ok_or("no single gw:bytes")?;
let bytes = base64::engine::general_purpose::STANDARD
.decode(bytes.as_bytes())
.map_err(|e| format!("gw:bytes is not base64: {e}"))?;
return Ok(Ok(Representation::new(repr_type(&media), bytes)));
}
if !kinds.contains(&term("Failure")) {
return Err(format!(
"<{subject}> is neither a gw:Reply nor a gw:Failure"
));
}
let kind = one_literal(&found, &term("errorKind")).ok_or("no single gw:errorKind")?;
let message = one_literal(&found, &term("message")).unwrap_or_default();
let argument = one_literal(&found, &term("argumentName"));
let target = one_iri(&found, &term("target"))
.map(|t| Iri::parse(t).map_err(|e| format!("gw:target: {e:?}")))
.transpose()?;
let need_target = || {
target
.clone()
.ok_or_else(|| format!("a `{kind}` failure with no gw:target"))
};
Ok(Err(match kind.as_str() {
"unresolved" => Error::Unresolved(need_target()?),
"missing-argument" => Error::MissingArgument(argument.unwrap_or(message)),
"invalid-argument" => Error::InvalidArgument {
name: argument.unwrap_or_default(),
detail: message,
},
"endpoint" => Error::Endpoint(message),
"denied" => Error::Denied(message),
"not-found" => Error::NotFound(message),
"conflict" => Error::Conflict(message),
"timeout" => Error::Timeout(message),
"unavailable" => Error::Unavailable(message),
"depth-exceeded" => Error::DepthExceeded {
depth: one_literal(&found, &term("depth"))
.and_then(|d| d.parse().ok())
.ok_or("a depth-exceeded failure with no gw:depth")?,
target: need_target()?,
},
other => return Err(format!("gw:errorKind `{other}` is not one of the ten")),
}))
}
fn repr_type(canonical: &str) -> ReprType {
let mut pieces = canonical.split(';');
let mut repr = ReprType::new(pieces.next().unwrap_or_default().trim());
for param in pieces {
if let Some((k, v)) = param.split_once('=') {
repr = repr.with_param(k.trim(), v.trim());
}
}
repr
}
pub fn invalidation_turtle(resource: &Iri, reason: &str) -> String {
let subject = node(crate::channel::NEW_SUBJECT);
let triples = [
Triple::new(
subject.clone(),
node(rdf::RDF_TYPE),
node(&term("Invalidation")),
),
Triple::new(
subject.clone(),
node(&term("resource")),
node(resource.as_str()),
),
Triple::new(
subject,
node(&term("reason")),
Literal::new_simple_literal(reason),
),
];
rdf::to_turtle_with(&triples, &[("gw", NS)])
}
#[async_trait]
pub trait Door: Send + Sync {
async fn call(&self, request: Request) -> Result<Representation>;
}
pub struct KernelDoor<'k> {
kernel: &'k Kernel,
capability: Capability,
}
impl<'k> KernelDoor<'k> {
pub fn new(kernel: &'k Kernel, capability: Capability) -> KernelDoor<'k> {
KernelDoor { kernel, capability }
}
}
#[async_trait]
impl Door for KernelDoor<'_> {
async fn call(&self, request: Request) -> Result<Representation> {
self.kernel.issue(request, &self.capability).await
}
}
fn utf8(repr: &Representation) -> Result<String> {
String::from_utf8(repr.bytes.clone())
.map_err(|_| Error::Endpoint("the layer answered a non-UTF-8 representation".into()))
}
async fn wait_feed<D: Door + ?Sized>(
door: &D,
layer: &str,
after: u64,
wait: Duration,
) -> Result<u64> {
let secs = wait.as_secs() + u64::from(wait.subsec_nanos() > 0);
let secs = secs.min(crate::feed::MAX_WAIT_SECS).to_string();
let after = after.to_string();
let repr = door
.call(layer_request(
Verb::Source,
&format!("urn:layer:{layer}:feed"),
&[
("after", after.as_bytes()),
("wait", secs.as_bytes()),
("as", b"application/json"),
],
))
.await?;
let json: serde_json::Value = serde_json::from_slice(&repr.bytes)
.map_err(|e| Error::Endpoint(format!("layer `{layer}`'s feed is not JSON: {e}")))?;
json.get("last")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| Error::Endpoint(format!("layer `{layer}`'s feed names no `last`")))
}
async fn wait_feed_until<D: Door + ?Sized>(
door: &D,
layer: &str,
after: u64,
deadline: Instant,
) -> Result<Option<u64>> {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Ok(None);
}
let feed = Box::pin(wait_feed(door, layer, after, remaining));
let timer = futures_timer::Delay::new(remaining);
match select(feed, timer).await {
Either::Left((last, _)) => last.map(Some),
Either::Right(((), _)) => Ok(None),
}
}
pub struct Asker<D: Door> {
door: D,
layer: String,
cursor: Mutex<u64>,
minted: AtomicU64,
}
impl<D: Door> Asker<D> {
pub fn new(door: D, layer: &str) -> Asker<D> {
Asker {
door,
layer: layer.to_string(),
cursor: Mutex::new(0),
minted: AtomicU64::new(0),
}
}
fn mint(&self) -> String {
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or_default();
let count = self.minted.fetch_add(1, Ordering::SeqCst);
crate::id::mint(
millis,
(u64::from(std::process::id()) << 40) | (count & 0xff_ffff_ffff),
)
}
pub async fn ask(&self, ask: &Ask, deadline: Duration) -> Result<Representation> {
let mut tries = 0;
loop {
match self.ask_as(&self.mint(), ask, deadline).await {
Err(Error::Conflict(m)) if m.contains("already names") && tries < 3 => tries += 1,
other => return other,
}
}
}
pub async fn ask_as(
&self,
correlation: &str,
ask: &Ask,
deadline: Duration,
) -> Result<Representation> {
let started = Instant::now();
let deadline_at = started + deadline;
let request = request_turtle(&self.layer, correlation, ask)?;
let subject = request_subject(&self.layer, correlation);
let reply = reply_subject(&self.layer, correlation);
self.door
.call(layer_request(
Verb::Sink,
&tuples(&self.layer),
&[("content", request.as_bytes())],
))
.await?;
loop {
if let Some(answer) = self.take_reply(correlation, &reply).await? {
return answer;
}
let after = *self.cursor.lock().expect("cursor lock");
match wait_feed_until(&self.door, &self.layer, after, deadline_at).await {
Ok(Some(last)) => *self.cursor.lock().expect("cursor lock") = last,
Ok(None) => break,
Err(Error::Conflict(_)) => *self.cursor.lock().expect("cursor lock") = 0,
Err(e) => return Err(e),
}
}
let withdrawn = self
.door
.call(layer_request(
Verb::Delete,
&tuples(&self.layer),
&[("tuple", subject.as_bytes())],
))
.await;
let waited = format!("{:.3}s", started.elapsed().as_secs_f64());
match withdrawn {
Ok(_) => Err(Error::Timeout(format!(
"no resolver took <{subject}> within {waited}; it is withdrawn"
))),
Err(Error::Conflict(taken)) => {
if let Some(answer) = self.take_reply(correlation, &reply).await? {
return answer;
}
Err(Error::Timeout(format!(
"no answer to <{subject}> within {waited} ({taken}); a late answer stays \
addressed to this asker"
)))
}
Err(Error::Denied(why)) => Err(Error::Timeout(format!(
"no answer to <{subject}> within {waited}, and it cannot be withdrawn ({why}): \
it stays pending for the resolver it is addressed to"
))),
Err(e) => Err(e),
}
}
async fn take_reply(
&self,
correlation: &str,
reply: &str,
) -> Result<Option<Result<Representation>>> {
let taken = self
.door
.call(layer_request(
Verb::Delete,
&tuples(&self.layer),
&[("tuple", reply.as_bytes())],
))
.await;
let text = match taken {
Ok(repr) => utf8(&repr)?,
Err(Error::NotFound(_)) => return Ok(None),
Err(Error::Conflict(_)) => utf8(
&self
.door
.call(layer_request(
Verb::Source,
&tuples(&self.layer),
&[("state", b"taken"), ("tuple", reply.as_bytes())],
))
.await?,
)?,
Err(e) => return Err(e),
};
parse_reply(&self.layer, correlation, &text)
.map(Some)
.map_err(|e| Error::Endpoint(format!("the answer <{reply}>: {e}")))
}
}
pub struct Resolver<D: Door> {
door: D,
verbs: Vec<Verb>,
prefixes: Vec<String>,
}
impl<D: Door> Resolver<D> {
pub fn new(door: D) -> Resolver<D> {
Resolver {
door,
verbs: vec![Verb::Source, Verb::Exists, Verb::Meta],
prefixes: Vec::new(),
}
}
pub fn verbs(mut self, verbs: impl IntoIterator<Item = Verb>) -> Resolver<D> {
self.verbs = verbs.into_iter().collect();
self
}
pub fn prefixes<S: Into<String>>(
mut self,
prefixes: impl IntoIterator<Item = S>,
) -> Resolver<D> {
self.prefixes = prefixes.into_iter().map(Into::into).collect();
self
}
pub async fn resolve(&self, asked: &Asked) -> Result<Representation> {
let ask = &asked.ask;
if !self.verbs.contains(&ask.verb) {
return Err(Error::Denied(format!(
"this gateway's resolver does not answer {}: it answers {}",
verb_word(ask.verb),
self.verbs
.iter()
.map(|v| verb_word(*v))
.collect::<Vec<_>>()
.join(", ")
)));
}
let name = ask.resource.as_str();
if !self.prefixes.is_empty() && !self.prefixes.iter().any(|p| name.starts_with(p)) {
return Err(Error::Denied(format!(
"this gateway's resolver does not answer <{name}>: it answers names under {}",
self.prefixes.join(", ")
)));
}
let mut request = Request::new(ask.verb, ask.resource.clone());
for (arg, value) in &ask.arguments {
request = request.with_arg(arg.as_str(), ArgRef::Inline(value.as_bytes().to_vec()));
}
self.door.call(request).await
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Served {
Answered(String),
Lost(String),
NotOurs(String),
}
pub async fn serve<L: Door + ?Sized, R: Door>(
layer_door: &L,
layer: &str,
resolver: &Resolver<R>,
subject: &str,
) -> Result<Served> {
let taken = layer_door
.call(layer_request(
Verb::Delete,
&tuples(layer),
&[("tuple", subject.as_bytes())],
))
.await;
let text = match taken {
Ok(repr) => utf8(&repr)?,
Err(Error::Conflict(m)) => return Ok(Served::Lost(m)),
Err(Error::Denied(m)) => return Ok(Served::NotOurs(m)),
Err(e) => return Err(e),
};
let asked = parse_request(layer, &text)
.map_err(|e| Error::Endpoint(format!("took <{subject}>, which is not a request: {e}")))?;
let answer = resolver.resolve(&asked).await;
let drop_answer = |turtle: String| async move {
layer_door
.call(layer_request(
Verb::Sink,
&tuples(layer),
&[("content", turtle.as_bytes())],
))
.await
};
match drop_answer(reply_turtle(layer, &asked, &answer)).await {
Ok(repr) => Ok(Served::Answered(utf8(&repr)?)),
Err(Error::InvalidArgument { detail, .. }) if answer.is_ok() => {
let failure = Err(Error::Endpoint(format!(
"the answer could not cross layer `{layer}`: {detail}"
)));
let repr = drop_answer(reply_turtle(layer, &asked, &failure)).await?;
Ok(Served::Answered(utf8(&repr)?))
}
Err(e) => Err(e),
}
}
pub async fn serve_pending<L: Door + ?Sized, R: Door>(
layer_door: &L,
layer: &str,
resolver: &Resolver<R>,
) -> Result<Vec<Served>> {
let prefix = format!("urn:layer:{layer}:request:");
let mut served = Vec::new();
let mut after = 0u64;
loop {
let after_text = after.to_string();
let listed = utf8(
&layer_door
.call(layer_request(
Verb::Source,
&tuples(layer),
&[("after", after_text.as_bytes())],
))
.await?,
)?;
let lines: Vec<(u64, String)> = listed
.lines()
.filter_map(|line| {
let mut words = line.split(' ');
let seq = words.next()?.parse().ok()?;
let subject = words.nth(1)?.to_string();
Some((seq, subject))
})
.collect();
let Some((last, _)) = lines.last() else {
return Ok(served);
};
after = *last;
for (_, subject) in lines.into_iter().filter(|(_, s)| s.starts_with(&prefix)) {
served.push(serve(layer_door, layer, resolver, &subject).await?);
}
}
}
pub async fn follow<L: Door + ?Sized, R: Door>(
layer_door: &L,
layer: &str,
resolver: &Resolver<R>,
after: u64,
wait: Duration,
) -> Result<u64> {
let last = match wait_feed(layer_door, layer, after, wait).await {
Ok(last) => last,
Err(Error::Conflict(_)) => wait_feed(layer_door, layer, 0, Duration::ZERO).await?,
Err(e) => return Err(e),
};
serve_pending(layer_door, layer, resolver).await?;
Ok(last)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_error_kind_round_trips() {
let asked = Asked {
subject: request_subject("gw", "c"),
correlation: "c".into(),
asker: "urn:collab:participant:a".into(),
ask: Ask::new(Verb::Source, Iri::parse("urn:x").unwrap()),
};
let x = || Iri::parse("urn:x:y").unwrap();
let errors = [
Error::Unresolved(x()),
Error::MissingArgument("in".into()),
Error::InvalidArgument {
name: "in".into(),
detail: "bad".into(),
},
Error::Endpoint("boom".into()),
Error::Denied("no".into()),
Error::NotFound("gone".into()),
Error::Conflict("taken".into()),
Error::Timeout("slow".into()),
Error::Unavailable("down".into()),
Error::DepthExceeded {
depth: 65,
target: x(),
},
];
assert_eq!(errors.len(), ERROR_KINDS.len());
for (error, kind) in errors.into_iter().zip(ERROR_KINDS) {
assert_eq!(error.kind(), kind);
let turtle = reply_turtle("gw", &asked, &Err(error.clone()));
let back = parse_reply("gw", "c", &turtle).unwrap().unwrap_err();
assert_eq!(back.to_string(), error.to_string(), "{kind}");
assert_eq!(back.kind(), kind);
}
}
#[test]
fn a_representation_round_trips_with_its_parameters() {
let asked = Asked {
subject: request_subject("gw", "c"),
correlation: "c".into(),
asker: "urn:collab:participant:a".into(),
ask: Ask::new(Verb::Source, Iri::parse("urn:x").unwrap()),
};
let bytes: Vec<u8> = (0..=255).collect();
let repr = Representation::new(
ReprType::new("application/octet-stream").with_param("charset", "binary"),
bytes.clone(),
);
let turtle = reply_turtle("gw", &asked, &Ok(repr.clone()));
let back = parse_reply("gw", "c", &turtle).unwrap().unwrap();
assert_eq!(back, repr);
assert_eq!(back.bytes, bytes);
}
#[test]
fn a_request_round_trips() {
let ask = Ask::new(Verb::Exists, Iri::parse("urn:data:a").unwrap())
.arg("b", "x=y")
.arg("a", "")
.to("urn:collab:participant:r");
let turtle = request_turtle("gw", "c.1", &ask).unwrap();
let stamped = format!(
"{turtle}\n<{}> <{}> <urn:collab:participant:a> .\n",
request_subject("gw", "c.1"),
rdf::PROV_ATTRIBUTED
);
let asked = parse_request("gw", &stamped).unwrap();
assert_eq!(asked.asker, "urn:collab:participant:a");
assert_eq!(asked.correlation, "c.1");
let mut expected = ask.clone();
expected.arguments.sort();
assert_eq!(asked.ask, expected);
}
#[test]
fn what_the_shapes_would_refuse_is_refused_before_sending() {
let ask = Ask::new(Verb::Source, Iri::parse("urn:data:a").unwrap());
for bad in ["", "-a", "a b", "a/b", &"a".repeat(65)] {
assert!(request_turtle("gw", bad, &ask).is_err(), "{bad:?}");
}
for name in ["", "1a", "a b", "a=b"] {
assert!(
request_turtle("gw", "c", &ask.clone().arg(name, "v")).is_err(),
"{name:?}"
);
}
}
#[test]
fn a_long_message_is_cut_to_the_shape() {
let asked = Asked {
subject: request_subject("gw", "c"),
correlation: "c".into(),
asker: "urn:collab:participant:a".into(),
ask: Ask::new(Verb::Source, Iri::parse("urn:x").unwrap()),
};
let long = "é".repeat(MAX_MESSAGE + 10);
let turtle = reply_turtle("gw", &asked, &Err(Error::Endpoint(long)));
match parse_reply("gw", "c", &turtle).unwrap() {
Err(Error::Endpoint(m)) => assert_eq!(m.chars().count(), MAX_MESSAGE),
other => panic!("{other:?}"),
}
}
}