The graph face
What resolution buys you ended with the catalog as one Turtle graph, and Why an endpoint describes itself claimed that “what can I do with these things?” is a query over it. This chapter runs the query. It is the shortest chapter in the part, because the RDF audience already knows what happens next and everybody else only needs to see that it does.
Two endpoints, no new code
Nothing is written here. urn:kernel:catalog already answers as Turtle. The published
ikigai-sparql crate binds urn:sparql:select (and ask, construct, describe), and
this part’s host mounts its space beside Part I’s — the Fallback in
building_endpoints::space(). The SPARQL endpoint holds no store of its own: its graph
argument names a resource, and it resolves that resource through the kernel and
queries whatever came back.
So the graph you query is the catalog, fetched the ordinary way, by name.
One SELECT over your own endpoints
“Which endpoints take a string?” — asked of the graph:
/// The `ik:id` of every endpoint in `kernel`'s catalog that takes an argument of RDF
/// class `class` — "what can I do with a string?", asked of the graph.
///
/// `graph=urn:kernel:catalog` is the whole trick: the SPARQL endpoint does not hold a
/// store, it *resolves* the graph you name. Any resource that answers as Turtle would do.
///
/// The property path `ik:input | ik:action/ik:input` matters. A single-verb endpoint's
/// inputs hang off the endpoint node; an endpoint with per-verb `ActionSpec`s declares
/// each verb's inputs on that verb's *action* node. Ask only the first and every
/// multi-verb endpoint is invisible to the question.
pub async fn endpoints_taking(kernel: &Kernel, class: &str) -> Result<Vec<String>> {
let query = format!(
"PREFIX ik: <https://ikigai-rs.dev/ns#>
SELECT DISTINCT ?id WHERE {{
?endpoint a ik:Endpoint ; ik:id ?id ; (ik:input | ik:action/ik:input) ?input .
?input ik:class <{class}> .
}} ORDER BY ?id"
);
let request = Request::new(
Verb::Source,
Iri::parse("urn:sparql:select").expect("a constant IRI"),
)
.with_arg("graph", ArgRef::Inline(b"urn:kernel:catalog".to_vec()))
.with_arg("query", ArgRef::Inline(query.into_bytes()))
.with_arg("as", ArgRef::Inline(b"text/csv".to_vec()));
let repr = kernel.issue(request, &Capability::root()).await?;
let csv = String::from_utf8_lossy(&repr.bytes);
// One column, so each line after the header is an id.
Ok(csv
.lines()
.skip(1)
.map(|line| line.trim().to_string())
.collect())
}
And the test that asks it of this host:
/// One SELECT over the reader's own endpoints.
#[test]
fn the_endpoints_that_take_a_string_are_the_ones_that_declared_it() {
let kernel = kernel_in(None);
let ids = block_on(endpoints_taking(
&kernel,
"http://www.w3.org/2001/XMLSchema#string",
))
.expect("the catalog is a graph and the graph answers");
// Declared `.class(xsd:string)` on an input, and found by that declaration.
assert!(ids.contains(&"camel-case".to_string()), "{ids:?}");
assert!(ids.contains(&"title".to_string()), "{ids:?}");
// Declared a different class, and correctly absent.
assert!(!ids.contains(&"counter".to_string()), "{ids:?}");
// Declared no class at all: an input with no `class` is invisible to this
// question, which is the argument for declaring one.
assert!(!ids.contains(&"markdown".to_string()), "{ids:?}");
}
cargo run -p building-endpoints -- --strings
camel-case
title
Three things that test pins down are the chapter.
Declaring class is what made the answer possible. camel-case and title declared
.class(xsd:string) on an input, so they are in the result. counter declared
xsd:nonNegativeInteger, so it is correctly absent. markdown declared no class at all —
and it is invisible to the question, not because it takes something else but because it
never said what it takes. That is Why an endpoint describes
itself, as a query result.
The property path is not decoration. (ik:input | ik:action/ik:input) walks two
shapes. A single-verb endpoint’s inputs hang off the endpoint node; an endpoint with
per-verb ActionSpecs — title, counter — declares each verb’s inputs on that verb’s
action node. Ask only ik:input and every multi-verb endpoint drops out of the answer
silently. (The first draft of this chapter did exactly that, and the test caught it.)
The answer is a representation. as=text/csv chose it; application/sparql-results+json
is the default, and CONSTRUCT would have come back as Turtle. A query result is a resolution
like any other, with a type you asked for.
From a shell
The CLI links the same crate, so the same question works against its much larger catalog:
ikigai --plain -c 'source urn:sparql:select graph=urn:kernel:catalog query="PREFIX ik: <https://ikigai-rs.dev/ns#> SELECT DISTINCT ?id WHERE { ?e a ik:Endpoint ; ik:id ?id ; (ik:input | ik:action/ik:input) ?i . ?i ik:class <http://www.w3.org/2001/XMLSchema#string> } ORDER BY ?id" as=text/csv'
⚠ The ikigai vocabulary is always loaded beside whatever
graphnames — the query above could have joinedik:Endpointto itsrdfs:subClassOfrelatives — and a transreptor shows up as bothik:Endpointandik:Transreptor, typed explicitly so a consumer that does not reason over the subclass axiom still finds it. If a query over the catalog returns nothing where you expected something, the first things to check are the property path above and whether the endpoint declared aclassat all.
Try it
#![allow(unused)]
fn main() {
extern crate building_endpoints;
extern crate ikigai_core;
extern crate futures;
use futures::executor::block_on;
use ikigai_core::{ArgRef, Capability, Iri, Request, Verb};
let kernel = building_endpoints::kernel_in(None);
// ASK: does anything in this host convert to Markdown?
let request = Request::new(Verb::Source, Iri::parse("urn:sparql:ask").unwrap())
.with_arg("graph", ArgRef::Inline(b"urn:kernel:catalog".to_vec()))
.with_arg("query", ArgRef::Inline(
b"PREFIX ik: <https://ikigai-rs.dev/ns#> ASK { ?t a ik:Transreptor ; ik:transreptsTo \"text/markdown\" }".to_vec(),
))
.with_arg("as", ArgRef::Inline(b"text/csv".to_vec()));
let repr = block_on(kernel.issue(request, &Capability::root())).unwrap();
assert_eq!(String::from_utf8_lossy(&repr.bytes).trim(), "true");
}