Adapt to new trustfall model

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2025-02-09 19:14:54 +01:00
parent e600807376
commit 11c3a8de94
14 changed files with 1632 additions and 685 deletions

941
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -15,6 +15,7 @@ jiff = "0.1.28"
kdl.workspace = true
miette = { version = "7.4.0", features = ["fancy", "syntect-highlighter"] }
owo-colors = "4.1.0"
paperless-rs = "0.1.5"
tokio = { version = "1.43.0", features = ["full"] }
tokio-stream = { version = "0.1.17", features = ["full"] }
tracing = "0.1.41"

View file

@ -0,0 +1,185 @@
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::sync::Arc;
use std::sync::OnceLock;
use paperless_rs::PaperlessClient;
use trustfall::provider::resolve_coercion_using_schema;
use trustfall::provider::resolve_property_with;
use trustfall::provider::AsVertex;
use trustfall::provider::ContextIterator;
use trustfall::provider::ContextOutcomeIterator;
use trustfall::provider::EdgeParameters;
use trustfall::provider::ResolveEdgeInfo;
use trustfall::provider::ResolveInfo;
use trustfall::provider::Typename;
use trustfall::provider::VertexIterator;
use trustfall::FieldValue;
use trustfall::Schema;
use super::vertex::Vertex;
use crate::parsing::DefinitionKind;
use crate::parsing::Record;
static SCHEMA: OnceLock<Schema> = OnceLock::new();
#[non_exhaustive]
pub struct Adapter {
schema: Arc<Schema>,
records: Vec<Record>,
definitions: Arc<BTreeMap<String, BTreeMap<String, DefinitionKind>>>,
paperless_client: Option<PaperlessClient>,
runtime_handle: tokio::runtime::Handle,
}
impl std::fmt::Debug for Adapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Adapter").finish_non_exhaustive()
}
}
impl Adapter {
pub fn new(
schema: Schema,
records: Vec<Record>,
definitions: BTreeMap<String, BTreeMap<String, DefinitionKind>>,
paperless_client: Option<PaperlessClient>,
runtime: tokio::runtime::Handle,
) -> Self {
Self {
schema: Arc::new(schema),
records,
definitions: Arc::new(definitions),
paperless_client,
runtime_handle: runtime,
}
}
pub const SCHEMA_TEXT: &'static str = include_str!("./schema.graphql");
pub fn schema() -> &'static Schema {
SCHEMA.get_or_init(|| Schema::parse(Self::SCHEMA_TEXT).expect("not a valid schema"))
}
}
impl<'a> trustfall::provider::Adapter<'a> for Adapter {
type Vertex = Vertex;
fn resolve_starting_vertices(
&self,
edge_name: &Arc<str>,
_parameters: &EdgeParameters,
resolve_info: &ResolveInfo,
) -> VertexIterator<'a, Self::Vertex> {
match edge_name.as_ref() {
"Records" => super::entrypoints::records(resolve_info, &self.records),
_ => {
unreachable!(
"attempted to resolve starting vertices for unexpected edge name: {edge_name}"
)
}
}
}
fn resolve_property<V: AsVertex<Self::Vertex> + 'a>(
&self,
contexts: ContextIterator<'a, V>,
type_name: &Arc<str>,
property_name: &Arc<str>,
resolve_info: &ResolveInfo,
) -> ContextOutcomeIterator<'a, V, FieldValue> {
if property_name.as_ref() == "__typename" {
return resolve_property_with(contexts, |vertex| vertex.typename().into());
}
match type_name.as_ref() {
"PaperlessDocument" => super::properties::resolve_paperless_document_property(
contexts,
property_name.as_ref(),
resolve_info,
),
"Path" => super::properties::resolve_path_property(
contexts,
property_name.as_ref(),
resolve_info,
),
"File" => super::properties::resolve_file_property(
contexts,
property_name.as_ref(),
resolve_info,
),
"Directory" => super::properties::resolve_directory_property(
contexts,
property_name.as_ref(),
resolve_info,
),
"Record" => {
super::properties::resolve_record_property(contexts, property_name, resolve_info)
}
kind if kind.starts_with("p_") => {
super::properties::resolve_record_property(contexts, property_name, resolve_info)
}
_ => {
unreachable!(
"attempted to read property '{property_name}' on unexpected type: {type_name}"
)
}
}
}
fn resolve_neighbors<V: AsVertex<Self::Vertex> + 'a>(
&self,
contexts: ContextIterator<'a, V>,
type_name: &Arc<str>,
edge_name: &Arc<str>,
parameters: &EdgeParameters,
resolve_info: &ResolveEdgeInfo,
) -> ContextOutcomeIterator<'a, V, VertexIterator<'a, Self::Vertex>> {
match type_name.as_ref() {
"Directory" => super::edges::resolve_directory_edge(
contexts,
edge_name.as_ref(),
parameters,
resolve_info,
),
kind if kind.starts_with("p_") => super::edges::resolve_record_edge(
contexts,
edge_name,
parameters,
resolve_info,
&self.definitions,
),
_ => {
unreachable!(
"attempted to resolve edge '{edge_name}' on unexpected type: {type_name}"
)
}
}
}
fn resolve_coercion<V: AsVertex<Self::Vertex> + 'a>(
&self,
contexts: ContextIterator<'a, V>,
_type_name: &Arc<str>,
coerce_to_type: &Arc<str>,
_resolve_info: &ResolveInfo,
) -> ContextOutcomeIterator<'a, V, bool> {
let schema = self.schema.clone();
let coerce_to_type = coerce_to_type.clone();
Box::new(contexts.map(move |ctx| {
let subtypes: BTreeSet<_> = schema
.subtypes(coerce_to_type.as_ref())
.unwrap_or_else(|| panic!("type {coerce_to_type} is not part of this schema"))
.collect();
match ctx.active_vertex::<Vertex>() {
None => (ctx, false),
Some(vertex) => {
let typename = vertex.typename();
let can_coerce = subtypes.contains(typename);
(ctx, can_coerce)
}
}
}))
}
}

View file

@ -0,0 +1,85 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use trustfall::provider::resolve_neighbors_with;
use trustfall::provider::AsVertex;
use trustfall::provider::ContextIterator;
use trustfall::provider::ContextOutcomeIterator;
use trustfall::provider::EdgeParameters;
use trustfall::provider::ResolveEdgeInfo;
use trustfall::provider::VertexIterator;
use super::Vertex;
use crate::parsing::DefinitionKind;
pub(super) fn resolve_directory_edge<'a, V: AsVertex<Vertex> + 'a>(
contexts: ContextIterator<'a, V>,
edge_name: &str,
_parameters: &EdgeParameters,
resolve_info: &ResolveEdgeInfo,
) -> ContextOutcomeIterator<'a, V, VertexIterator<'a, Vertex>> {
match edge_name {
"Children" => directory::children(contexts, resolve_info),
_ => unreachable!("attempted to resolve unexpected edge '{edge_name}' on type 'Directory'"),
}
}
mod directory {
use camino::Utf8Path;
use trustfall::provider::resolve_neighbors_with;
use trustfall::provider::AsVertex;
use trustfall::provider::ContextIterator;
use trustfall::provider::ContextOutcomeIterator;
use trustfall::provider::ResolveEdgeInfo;
use trustfall::provider::VertexIterator;
use crate::adapter::Vertex;
pub(super) fn children<'a, V: AsVertex<Vertex> + 'a>(
contexts: ContextIterator<'a, V>,
_resolve_info: &ResolveEdgeInfo,
) -> ContextOutcomeIterator<'a, V, VertexIterator<'a, Vertex>> {
resolve_neighbors_with(contexts, move |vertex| {
let vertex = vertex
.as_directory()
.expect("conversion failed, vertex was not a Directory");
fn read_children(path: &Utf8Path) -> Option<impl Iterator<Item = Vertex>> {
Some(
path.read_dir_utf8()
.ok()?
.flat_map(|item| Some(Vertex::Path(item.ok()?.path().to_path_buf()))),
)
}
read_children(vertex)
.map(|i| {
let it: Box<dyn Iterator<Item = Vertex>> = Box::new(i);
it
})
.unwrap_or_else(|| Box::new(std::iter::empty()))
})
}
}
pub(super) fn resolve_record_edge<'a, V: AsVertex<Vertex> + 'a>(
contexts: ContextIterator<'a, V>,
edge_name: &Arc<str>,
_parameters: &EdgeParameters,
_resolve_info: &ResolveEdgeInfo,
definitions: &Arc<BTreeMap<String, BTreeMap<String, DefinitionKind>>>,
) -> ContextOutcomeIterator<'a, V, VertexIterator<'a, Vertex>> {
let edge_name = edge_name.clone();
let definitions = definitions.clone();
resolve_neighbors_with(contexts, move |v| {
let rec = v.as_record().expect("Expected a record");
let def = &definitions[&rec.kind][edge_name.as_ref()];
match def {
DefinitionKind::Path => Box::new(std::iter::once(Vertex::Path(
rec.fields[edge_name.as_ref()].as_string().unwrap().into(),
))),
_ => unreachable!("Only `Path` can appear as edge for now"),
}
})
}

View file

@ -0,0 +1,16 @@
use trustfall::provider::ResolveInfo;
use trustfall::provider::VertexIterator;
use super::vertex::Vertex;
use crate::parsing::Record;
pub(super) fn records<'a>(
_resolve_info: &ResolveInfo,
records: &'_ [Record],
) -> VertexIterator<'a, Vertex> {
#[expect(
clippy::unnecessary_to_owned,
reason = "We have to go through a vec to satisfy the lifetimes"
)]
Box::new(records.to_vec().into_iter().map(Vertex::Record))
}

View file

@ -0,0 +1,55 @@
mod adapter_impl;
mod edges;
mod entrypoints;
mod properties;
mod vertex;
#[cfg(test)]
mod tests;
pub use adapter_impl::Adapter;
use tracing::trace;
use trustfall::Schema;
pub use vertex::Vertex;
pub struct CustomVertex {
pub name: String,
pub definition: String,
}
impl crate::parsing::Definition {
fn to_custom_vertices(&self) -> Vec<CustomVertex> {
let name = format!("p_{}", self.name);
let fields = self
.fields
.iter()
.map(|(fname, ftype)| {
let kind = ftype.trustfall_kind(&format!("{name}{fname}"));
format!("{fname}: {kind}")
})
.chain([String::from("_at: String!"), String::from("_kind: String!")])
.collect::<Vec<_>>();
let definition = format!("type {name} implements Record {{ {} }}", fields.join(","));
[CustomVertex { name, definition }].into_iter().collect()
}
}
pub(crate) fn to_schema(
definitions: &std::collections::BTreeMap<String, Vec<crate::parsing::Definition>>,
) -> trustfall::Schema {
let base_text = Adapter::SCHEMA_TEXT;
let generated = definitions
.values()
.flat_map(|defs| defs.last().unwrap().to_custom_vertices())
.map(|v| v.definition)
.collect::<Vec<_>>()
.join("\n");
let input = format!("{base_text}{generated}");
trace!(%input, "Using schema");
Schema::parse(input).unwrap()
}

View file

@ -0,0 +1,159 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use kdl::KdlValue;
use paperless_rs::PaperlessClient;
use trustfall::provider::field_property;
use trustfall::provider::resolve_property_with;
use trustfall::provider::AsVertex;
use trustfall::provider::ContextIterator;
use trustfall::provider::ContextOutcomeIterator;
use trustfall::provider::ResolveInfo;
use trustfall::FieldValue;
use super::vertex::Vertex;
pub(super) fn resolve_path_property<'a, V: AsVertex<Vertex> + 'a>(
contexts: ContextIterator<'a, V>,
property_name: &str,
_resolve_info: &ResolveInfo,
) -> ContextOutcomeIterator<'a, V, FieldValue> {
match property_name {
"exists" => resolve_property_with(contexts, move |v: &Vertex| {
let path = v.as_path().expect("vertex was not a Path");
path.exists().into()
}),
"basename" => resolve_property_with(contexts, move |v: &Vertex| {
let path = v.as_path().expect("vertex was not a Path");
path.file_name().into()
}),
"path" => resolve_property_with(contexts, move |v: &Vertex| {
let path = v.as_path().expect("vertex was not a Path");
path.to_string().into()
}),
_ => {
unreachable!("attempted to read unexpected property '{property_name}' on type 'Path'")
}
}
}
pub(super) fn resolve_directory_property<'a, V: AsVertex<Vertex> + 'a>(
contexts: ContextIterator<'a, V>,
property_name: &str,
_resolve_info: &ResolveInfo,
) -> ContextOutcomeIterator<'a, V, FieldValue> {
match property_name {
"exists" => resolve_property_with(contexts, move |v: &Vertex| {
let directory = v.as_directory().expect("vertex was not a Directory");
directory.exists().into()
}),
"basename" => resolve_property_with(contexts, move |v: &Vertex| {
let directory = v.as_directory().expect("vertex was not a Directory");
directory.file_name().into()
}),
"path" => resolve_property_with(contexts, move |v: &Vertex| {
let directory = v.as_directory().expect("vertex was not a Directory");
directory.to_string().into()
}),
_ => {
unreachable!("attempted to read unexpected property '{property_name}' on type 'File'")
}
}
}
pub(super) fn resolve_file_property<'a, V: AsVertex<Vertex> + 'a>(
contexts: ContextIterator<'a, V>,
property_name: &str,
_resolve_info: &ResolveInfo,
) -> ContextOutcomeIterator<'a, V, FieldValue> {
match property_name {
"exists" => resolve_property_with(contexts, move |v: &Vertex| {
let file = v.as_file().expect("vertex was not a File");
file.exists().into()
}),
"basename" => resolve_property_with(contexts, move |v: &Vertex| {
let file = v.as_file().expect("vertex was not a File");
file.file_name().into()
}),
"path" => resolve_property_with(contexts, move |v: &Vertex| {
let file = v.as_file().expect("vertex was not a File");
file.to_string().into()
}),
"extension" => resolve_property_with(contexts, move |v: &Vertex| {
let file = v.as_file().expect("vertex was not a File");
file.extension().into()
}),
_ => {
unreachable!("attempted to read unexpected property '{property_name}' on type 'File'")
}
}
}
pub(super) fn resolve_paperless_document_property<'a, V: AsVertex<Vertex> + 'a>(
contexts: ContextIterator<'a, V>,
property_name: &str,
_resolve_info: &ResolveInfo,
) -> ContextOutcomeIterator<'a, V, FieldValue> {
match property_name {
"added" => resolve_property_with(contexts, field_property!(as_paperless_document, added)),
"archive_serial_number" => resolve_property_with(
contexts,
field_property!(as_paperless_document, archive_serial_number),
),
"content" => {
resolve_property_with(contexts, field_property!(as_paperless_document, content))
}
"created" => {
resolve_property_with(contexts, field_property!(as_paperless_document, created))
}
"id" => resolve_property_with(contexts, field_property!(as_paperless_document, id)),
"title" => resolve_property_with(contexts, field_property!(as_paperless_document, title)),
_ => {
unreachable!(
"attempted to read unexpected property '{property_name}' on type 'PaperlessDocument'"
)
}
}
}
pub(super) fn resolve_record_property<'a, V: AsVertex<Vertex> + 'a>(
contexts: ContextIterator<'a, V>,
property_name: &Arc<str>,
_resolve_info: &ResolveInfo,
) -> ContextOutcomeIterator<'a, V, FieldValue> {
let property_name = property_name.clone();
match property_name.as_ref() {
"_at" => resolve_property_with(
contexts,
field_property!(as_record, at, { at.to_string().into() }),
),
"_kind" => resolve_property_with(contexts, field_property!(as_record, kind)),
_ => resolve_property_with(contexts, move |v: &Vertex| {
let rec = v
.as_record()
.expect("Called record property without it being a record");
kdl_to_trustfall_value(rec.fields[property_name.as_ref()].clone())
}),
}
}
fn kdl_to_trustfall_value(val: KdlValue) -> FieldValue {
match val {
KdlValue::Bool(b) => FieldValue::Boolean(b),
KdlValue::Float(f) => FieldValue::Float64(f),
KdlValue::Null => FieldValue::Null,
KdlValue::Integer(i) => FieldValue::Int64(i.try_into().unwrap()),
KdlValue::String(s) => FieldValue::String(s.into()),
}
}

View file

@ -0,0 +1,88 @@
schema {
query: RootSchemaQuery
}
directive @filter(
"""
Name of the filter operation to perform.
"""
op: String!
"""
List of string operands for the operator.
"""
value: [String!]
) repeatable on FIELD | INLINE_FRAGMENT
directive @tag(
"""
Name to apply to the given property field.
"""
name: String
) on FIELD
directive @output(
"""
What to designate the output field generated from this property field.
"""
name: String
) on FIELD
directive @optional on FIELD
directive @recurse(
"""
Recurse up to this many times on this edge. A depth of 1 produces the current
vertex and its immediate neighbors along the given edge.
"""
depth: Int!
) on FIELD
directive @fold on FIELD
directive @transform(
"""
Name of the transformation operation to perform.
"""
op: String!
) on FIELD
"""
All the possible data types to begin querying
"""
type RootSchemaQuery {
"""
All records in your plaixt instance
"""
Records: [Record!]!
}
interface Record {
_kind: String!
_at: String!
}
interface Path {
path: String!
exists: Boolean!
basename: String!
}
interface File implements Path {
path: String!
exists: Boolean!
basename: String!
extension: String!
}
type Directory implements Path {
path: String!
exists: Boolean!
basename: String!
Children: [Path!]!
}
type PaperlessDocument {
id: Int!
title: String!
content: String!
archive_serial_number: Int
created: String!
added: String!
}

View file

@ -0,0 +1,16 @@
use trustfall::provider::check_adapter_invariants;
use super::Adapter;
#[tokio::test]
async fn adapter_satisfies_trustfall_invariants() {
let schema = Adapter::schema();
let adapter = Adapter::new(
schema.clone(),
vec![],
[].into(),
None,
tokio::runtime::Handle::current(),
);
check_adapter_invariants(schema, adapter);
}

View file

@ -0,0 +1,15 @@
use camino::Utf8PathBuf;
use paperless_rs::endpoint::documents::Document as PaperlessDocument;
use crate::parsing::Record;
#[non_exhaustive]
#[derive(Debug, Clone, trustfall::provider::TrustfallEnumVertex)]
pub enum Vertex {
Path(Utf8PathBuf),
File(Utf8PathBuf),
Directory(Utf8PathBuf),
PaperlessDocument(PaperlessDocument),
Record(Record),
}

View file

@ -8,7 +8,6 @@ use camino::Utf8PathBuf;
use clap::Parser;
use clap::Subcommand;
use clap::ValueHint;
use filesystem_trustfall_adapter::FileSystemAdapter;
use human_panic::Metadata;
use miette::IntoDiagnostic;
use parsing::Definition;
@ -18,9 +17,9 @@ use tracing_subscriber::EnvFilter;
use trustfall::execute_query;
use trustfall::FieldValue;
mod adapter;
mod config;
mod parsing;
mod trustfall_plaixt;
#[derive(Debug, Parser)]
struct Args {
@ -97,14 +96,19 @@ async fn main() -> miette::Result<()> {
fn get_schema_and_adapter(
definitions: &BTreeMap<String, Vec<Definition>>,
records: Vec<Record>,
) -> (trustfall::Schema, trustfall_plaixt::TrustfallMultiAdapter) {
let schema = trustfall_plaixt::to_schema(definitions);
let adapter = trustfall_plaixt::TrustfallMultiAdapter {
plaixt: trustfall_plaixt::PlaixtAdapter {
records: records.clone(),
},
filesystem: FileSystemAdapter::new(),
};
) -> (trustfall::Schema, adapter::Adapter) {
let schema = adapter::to_schema(definitions);
let definitions = definitions
.iter()
.map(|(name, def)| (name.clone(), def.last().cloned().unwrap().fields))
.collect();
let adapter = adapter::Adapter::new(
schema.clone(),
records,
definitions,
None,
tokio::runtime::Handle::current(),
);
(schema, adapter)
}

View file

@ -1,5 +1,4 @@
use std::collections::BTreeMap;
use std::collections::HashMap;
use camino::Utf8Path;
use camino::Utf8PathBuf;
@ -15,8 +14,6 @@ use miette::NamedSource;
use owo_colors::OwoColorize;
use tokio_stream::wrappers::ReadDirStream;
use crate::trustfall_plaixt::ADAPTER_SEP;
#[derive(Debug, Clone)]
pub struct Record {
pub(crate) kind: String,
@ -155,7 +152,7 @@ pub(crate) async fn load_records(
Ok(defs)
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum DefinitionKind {
String,
Path,
@ -163,11 +160,11 @@ pub enum DefinitionKind {
}
impl DefinitionKind {
pub(crate) fn trustfall_kind(&self) -> String {
pub(crate) fn trustfall_kind(&self, _namespace: &str) -> String {
match self {
DefinitionKind::String => String::from("String"),
DefinitionKind::Path => format!("fs{ADAPTER_SEP}Path"),
DefinitionKind::OneOf(_vecs) => String::from("String"),
DefinitionKind::String => String::from("String!"),
DefinitionKind::Path => String::from("Path!"),
DefinitionKind::OneOf(_vecs) => String::from("String!"),
}
}
@ -188,6 +185,23 @@ impl DefinitionKind {
.ok_or_else(|| format!("Expected one of: {}", options.join(", "))),
}
}
pub(crate) fn extra_trustfall_kinds(
&self,
namespace: &str,
) -> Vec<crate::adapter::CustomVertex> {
match self {
DefinitionKind::OneOf(defs) => {
let name = format!("{namespace}Def");
let vec = vec![crate::adapter::CustomVertex {
definition: format!("enum {name} {{ {} }}", defs.join(",")),
name,
}];
vec
}
_ => vec![],
}
}
}
impl TryFrom<&str> for DefinitionKind {
@ -201,11 +215,11 @@ impl TryFrom<&str> for DefinitionKind {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Definition {
pub(crate) name: String,
pub(crate) since: Timestamp,
pub(crate) fields: HashMap<String, DefinitionKind>,
pub(crate) fields: BTreeMap<String, DefinitionKind>,
}
pub(crate) fn parse_definition(

View file

@ -1,639 +0,0 @@
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fmt::Write;
use std::ops::Not;
use std::sync::Arc;
use kdl::KdlValue;
use tracing::debug;
use tracing::trace;
use trustfall::provider::field_property;
use trustfall::provider::resolve_coercion_with;
use trustfall::provider::resolve_neighbors_with;
use trustfall::provider::resolve_property_with;
use trustfall::provider::Adapter;
use trustfall::provider::AsVertex;
use trustfall::FieldValue;
use trustfall::Schema;
use crate::parsing::Definition;
use crate::parsing::Record;
pub(crate) const ADAPTER_SEP: &str = "__";
#[derive(Debug, Default)]
pub struct StartingVertex {
adapter_name: String,
start_vertex_name: String,
vertex_type: String,
}
impl StartingVertex {
pub fn new(adapter_name: String, start_vertex_name: String, start_vertex_type: String) -> Self {
Self {
adapter_name,
start_vertex_name,
vertex_type: start_vertex_type,
}
}
pub fn schema_name(&self) -> String {
format!(
"{}{ADAPTER_SEP}{}",
self.adapter_name, self.start_vertex_name
)
}
pub fn vertex_type(&self) -> &str {
&self.vertex_type
}
}
#[derive(Debug, Default)]
pub struct VertexType {
adapter_name: String,
vertex_name: String,
vertex_fields: HashMap<String, String>,
implements: Vec<String>,
}
impl VertexType {
pub fn new(
adapter_name: String,
vertex_name: String,
vertex_fields: HashMap<String, String>,
implements: Vec<String>,
) -> Self {
Self {
adapter_name,
vertex_name,
vertex_fields,
implements,
}
}
pub fn schema_name(&self) -> String {
format!("{}{ADAPTER_SEP}{}", self.adapter_name, self.vertex_name)
}
pub fn schema_type(&self) -> String {
format!(
r#"type {name} {impls} {{ {fields} }}"#,
name = self.schema_name(),
impls = self
.implements
.is_empty()
.not()
.then(|| format!("implements {}", self.implements.join(" & ")))
.unwrap_or_else(String::new),
fields = self.vertex_fields.iter().fold(String::new(), |mut out, f| {
write!(out, "{}: {}, ", f.0, f.1).unwrap();
out
}),
)
}
}
#[derive(Debug, Default)]
pub struct DynamicSchema {
roots: Vec<StartingVertex>,
types: Vec<VertexType>,
}
impl DynamicSchema {
pub fn new() -> Self {
Self::default()
}
pub fn add_root(&mut self, root: StartingVertex) {
self.roots.push(root);
}
pub fn add_type(&mut self, kind: VertexType) {
self.types.push(kind);
}
}
pub(crate) fn to_schema(definitions: &BTreeMap<String, Vec<Definition>>) -> Schema {
let mut schema = DynamicSchema::new();
schema.add_root(StartingVertex::new(
"Plaixt".to_string(),
"RecordsAll".to_string(),
"[Record!]!".to_string(),
));
for definition in definitions.values().flat_map(|d| d.first()) {
let fields = VertexType::new(
"Plaixt".to_string(),
format!("{}Fields", definition.name),
definition
.fields
.iter()
.map(|(name, val)| (name.clone(), format!("{}!", val.trustfall_kind())))
.collect(),
vec![],
);
schema.add_type(VertexType::new(
"Plaixt".to_string(),
definition.name.clone(),
[
(String::from("at"), String::from("String!")),
(String::from("kind"), String::from("String!")),
(String::from("fields"), format!("{}!", fields.schema_name())),
]
.into(),
vec![String::from("Record")],
));
schema.add_type(fields);
}
let schema = format!(
r#"schema {{
query: RootSchemaQuery
}}
{}
type RootSchemaQuery {{
{roots}
fs{ADAPTER_SEP}Path(path: String!): fs{ADAPTER_SEP}Path!,
}}
interface Record {{
at: String!,
kind: String!,
}}
interface fs{ADAPTER_SEP}Path {{
path: String!
}}
type fs{ADAPTER_SEP}Folder implements fs{ADAPTER_SEP}Path {{
path: String!
children: [fs{ADAPTER_SEP}Path!]
}}
type fs{ADAPTER_SEP}File implements fs{ADAPTER_SEP}Path {{
path: String!
size: Int!
extension: String!
Hash: String!
}}
{types}
"#,
Schema::ALL_DIRECTIVE_DEFINITIONS,
roots = schema.roots.iter().fold(String::new(), |mut out, r| {
write!(out, "{}: {}, ", r.schema_name(), r.vertex_type()).unwrap();
out
}),
types = schema.types.iter().fold(String::new(), |mut out, t| {
writeln!(out, "{}", t.schema_type()).unwrap();
out
}),
);
trace!(%schema, "Using schema");
Schema::parse(schema).unwrap()
}
pub struct TrustfallMultiAdapter {
pub plaixt: PlaixtAdapter,
pub filesystem: filesystem_trustfall_adapter::FileSystemAdapter,
}
#[derive(Debug, Clone)]
pub enum TrustfallMultiVertex {
Plaixt(PlaixtVertex),
Filesystem(filesystem_trustfall_adapter::vertex::Vertex),
}
impl AsVertex<filesystem_trustfall_adapter::vertex::Vertex> for TrustfallMultiVertex {
fn as_vertex(&self) -> Option<&filesystem_trustfall_adapter::vertex::Vertex> {
self.as_filesystem()
}
fn into_vertex(self) -> Option<filesystem_trustfall_adapter::vertex::Vertex> {
self.as_filesystem().cloned()
}
}
impl AsVertex<PlaixtVertex> for TrustfallMultiVertex {
fn as_vertex(&self) -> Option<&PlaixtVertex> {
self.as_plaixt()
}
fn into_vertex(self) -> Option<PlaixtVertex> {
self.as_plaixt().cloned()
}
}
impl TrustfallMultiVertex {
pub fn as_plaixt(&self) -> Option<&PlaixtVertex> {
if let Self::Plaixt(v) = self {
Some(v)
} else {
None
}
}
pub fn as_filesystem(
&self,
) -> Option<
&<filesystem_trustfall_adapter::FileSystemAdapter as trustfall::provider::Adapter<
'static,
>>::Vertex,
> {
if let Self::Filesystem(v) = self {
Some(v)
} else {
None
}
}
}
impl<'v> Adapter<'v> for TrustfallMultiAdapter {
type Vertex = TrustfallMultiVertex;
fn resolve_starting_vertices(
&self,
edge_name: &Arc<str>,
parameters: &trustfall::provider::EdgeParameters,
resolve_info: &trustfall::provider::ResolveInfo,
) -> trustfall::provider::VertexIterator<'v, Self::Vertex> {
let (adapter_name, edge_name) = edge_name.split_once(ADAPTER_SEP).unwrap();
trace!(
?adapter_name,
?edge_name,
"resolving top-level starting vertex"
);
match adapter_name {
"Plaixt" => {
let iter = self.plaixt.resolve_starting_vertices(
&Arc::from(edge_name),
parameters,
resolve_info,
);
Box::new(iter.map(TrustfallMultiVertex::Plaixt))
}
"fs" => {
let iter = self.filesystem.resolve_starting_vertices(
&Arc::from(edge_name),
parameters,
resolve_info,
);
Box::new(
iter.inspect(|v| trace!(?v, "Got vertex"))
.map(TrustfallMultiVertex::Filesystem),
)
}
_ => unreachable!(),
}
}
fn resolve_property<V>(
&self,
contexts: trustfall::provider::ContextIterator<'v, V>,
type_name: &Arc<str>,
property_name: &Arc<str>,
resolve_info: &trustfall::provider::ResolveInfo,
) -> trustfall::provider::ContextOutcomeIterator<'v, V, FieldValue>
where
V: trustfall::provider::AsVertex<Self::Vertex> + 'v,
{
trace!(?type_name, ?property_name, "resolving top-level property");
let (adapter_name, type_name) = type_name
.split_once(ADAPTER_SEP)
.unwrap_or(("Plaixt", type_name));
match adapter_name {
"Plaixt" => {
let contexts = contexts.collect::<Vec<_>>();
let properties = self.plaixt.resolve_property(
Box::new(
contexts
.clone()
.into_iter()
.map(|v| v.flat_map(&mut |v: V| v.into_vertex())),
),
&Arc::from(type_name),
property_name,
resolve_info,
);
Box::new(
properties
.into_iter()
.zip(contexts)
.map(|((_ctx, name), og_ctx)| (og_ctx, name)),
)
}
"fs" => {
let contexts = contexts.collect::<Vec<_>>();
let properties = self.filesystem.resolve_property(
Box::new(
contexts
.clone()
.into_iter()
.map(|v| v.flat_map(&mut |v: V| v.into_vertex()))
.inspect(|v| trace!(?v, "Got vertex")),
),
&Arc::from(type_name),
property_name,
resolve_info,
);
Box::new(
properties
.into_iter()
.zip(contexts)
.map(|((_ctx, name), og_ctx)| (og_ctx, name)),
)
}
_ => unreachable!(),
}
}
fn resolve_neighbors<V: trustfall::provider::AsVertex<Self::Vertex> + 'v>(
&self,
contexts: trustfall::provider::ContextIterator<'v, V>,
type_name: &Arc<str>,
edge_name: &Arc<str>,
parameters: &trustfall::provider::EdgeParameters,
resolve_info: &trustfall::provider::ResolveEdgeInfo,
) -> trustfall::provider::ContextOutcomeIterator<
'v,
V,
trustfall::provider::VertexIterator<'v, Self::Vertex>,
> {
trace!(?type_name, ?edge_name, "Resolving top-level neighbor");
let (adapter_name, type_name) = type_name.split_once(ADAPTER_SEP).unwrap();
match adapter_name {
"Plaixt" => {
let contexts = contexts.collect::<Vec<_>>();
let properties = self.plaixt.resolve_neighbors(
Box::new(
contexts
.clone()
.into_iter()
.map(|v| v.flat_map(&mut |v: V| v.into_vertex())),
),
&Arc::from(type_name),
edge_name,
parameters,
resolve_info,
);
Box::new(
properties
.into_iter()
.zip(contexts)
.map(|((_ctx, vals), og_ctx)| {
(
og_ctx,
Box::new(vals.map(TrustfallMultiVertex::Plaixt)) as Box<_>,
)
}),
)
}
"fs" => {
let contexts = contexts.collect::<Vec<_>>();
let properties = self.filesystem.resolve_neighbors(
Box::new(
contexts
.clone()
.into_iter()
.map(|v| v.flat_map(&mut |v: V| v.into_vertex())),
),
&Arc::from(type_name),
edge_name,
parameters,
resolve_info,
);
Box::new(
properties
.into_iter()
.zip(contexts)
.map(|((_ctx, vals), og_ctx)| {
(
og_ctx,
Box::new(
vals.inspect(|v| trace!(?v, "Got vertex"))
.map(TrustfallMultiVertex::Filesystem),
) as Box<_>,
)
}),
)
}
_ => unreachable!(),
}
}
fn resolve_coercion<V: trustfall::provider::AsVertex<Self::Vertex> + 'v>(
&self,
contexts: trustfall::provider::ContextIterator<'v, V>,
type_name: &Arc<str>,
coerce_to_type: &Arc<str>,
resolve_info: &trustfall::provider::ResolveInfo,
) -> trustfall::provider::ContextOutcomeIterator<'v, V, bool> {
trace!(?type_name, ?coerce_to_type, "Top-level coerce");
let (adapter_name, coerce_to_type) = coerce_to_type.split_once(ADAPTER_SEP).unwrap();
match adapter_name {
"Plaixt" => {
let contexts = contexts.collect::<Vec<_>>();
let properties = self.plaixt.resolve_coercion(
Box::new(
contexts
.clone()
.into_iter()
.map(|v| v.flat_map(&mut |v: V| v.into_vertex())),
),
type_name,
&Arc::from(coerce_to_type),
resolve_info,
);
Box::new(
properties
.into_iter()
.zip(contexts)
.map(|((_ctx, val), og_ctx)| (og_ctx, val)),
)
}
"fs" => {
let contexts = contexts.collect::<Vec<_>>();
let properties = self.filesystem.resolve_coercion(
Box::new(
contexts
.clone()
.into_iter()
.map(|v| v.flat_map(&mut |v: V| v.into_vertex())),
),
type_name,
&Arc::from(coerce_to_type),
resolve_info,
);
let type_name = type_name.clone();
let coerce_to_type = coerce_to_type.to_string();
Box::new(properties.into_iter().zip(contexts).map(
move |((_ctx, allowed), og_ctx)| {
trace!(?allowed, ?type_name, ?coerce_to_type, "Allowed coercion?");
(og_ctx, allowed)
},
))
}
_ => unreachable!(),
}
}
}
pub(crate) struct PlaixtAdapter {
pub(crate) records: Vec<Record>,
}
#[derive(Clone, Debug)]
pub(crate) enum PlaixtVertex {
Record(Record),
Fields {
name: String,
values: BTreeMap<String, KdlValue>,
},
}
impl PlaixtVertex {
pub(crate) fn as_fields(&self) -> Option<&BTreeMap<String, KdlValue>> {
if let Self::Fields { values, .. } = self {
Some(values)
} else {
None
}
}
pub(crate) fn as_record(&self) -> Option<&Record> {
if let Self::Record(v) = self {
Some(v)
} else {
None
}
}
pub(crate) fn typename(&self) -> String {
match self {
PlaixtVertex::Record { .. } => "Record".to_string(),
PlaixtVertex::Fields { name, .. } => name.clone(),
}
}
}
impl<'a> Adapter<'a> for PlaixtAdapter {
type Vertex = PlaixtVertex;
fn resolve_starting_vertices(
&self,
edge_name: &Arc<str>,
_parameters: &trustfall::provider::EdgeParameters,
_resolve_info: &trustfall::provider::ResolveInfo,
) -> trustfall::provider::VertexIterator<'a, Self::Vertex> {
trace!(?edge_name, "Resolving start vertex");
match edge_name.as_ref() {
"RecordsAll" => Box::new(self.records.clone().into_iter().map(PlaixtVertex::Record)),
_ => unreachable!(),
}
}
fn resolve_property<V: trustfall::provider::AsVertex<Self::Vertex> + 'a>(
&self,
contexts: trustfall::provider::ContextIterator<'a, V>,
type_name: &Arc<str>,
property_name: &Arc<str>,
_resolve_info: &trustfall::provider::ResolveInfo,
) -> trustfall::provider::ContextOutcomeIterator<'a, V, trustfall::FieldValue> {
match (type_name.as_ref(), property_name.as_ref()) {
(_, "__typename") => Box::new(contexts.map(|ctx| {
let value = match ctx.active_vertex() {
Some(_record) => _record.typename().into(),
None => FieldValue::Null,
};
(ctx, value)
})),
(_, "at") => resolve_property_with(
contexts,
field_property!(as_record, at, { at.to_string().into() }),
),
(_, "kind") => resolve_property_with(contexts, field_property!(as_record, kind)),
(name, field) => {
debug!(?name, ?field, "Asking for properties");
let field = field.to_string();
resolve_property_with(contexts, move |vertex| {
trace!(?vertex, ?field, "Getting property");
let fields = vertex.as_fields().unwrap();
match fields.get(&field).unwrap().clone() {
KdlValue::Bool(b) => FieldValue::Boolean(b),
KdlValue::Float(f) => FieldValue::Float64(f),
KdlValue::Null => FieldValue::Null,
KdlValue::Integer(i) => FieldValue::Int64(i.try_into().unwrap()),
KdlValue::String(s) => FieldValue::String(s.into()),
}
})
}
}
}
fn resolve_neighbors<V: trustfall::provider::AsVertex<Self::Vertex> + 'a>(
&self,
contexts: trustfall::provider::ContextIterator<'a, V>,
type_name: &Arc<str>,
edge_name: &Arc<str>,
_parameters: &trustfall::provider::EdgeParameters,
_resolve_info: &trustfall::provider::ResolveEdgeInfo,
) -> trustfall::provider::ContextOutcomeIterator<
'a,
V,
trustfall::provider::VertexIterator<'a, Self::Vertex>,
> {
trace!(?type_name, ?edge_name, "Resolving neighbors");
match edge_name.as_ref() {
"fields" => resolve_neighbors_with(contexts, |c| {
Box::new(
c.as_record()
.map(|r| PlaixtVertex::Fields {
name: format!("{}Fields", r.kind),
values: r.fields.clone(),
})
.into_iter(),
)
}),
_ => resolve_neighbors_with(contexts, |c| todo!()),
_ => unreachable!("Could not resolve {edge_name}"),
}
}
fn resolve_coercion<V: trustfall::provider::AsVertex<Self::Vertex> + 'a>(
&self,
contexts: trustfall::provider::ContextIterator<'a, V>,
type_name: &Arc<str>,
coerce_to_type: &Arc<str>,
_resolve_info: &trustfall::provider::ResolveInfo,
) -> trustfall::provider::ContextOutcomeIterator<'a, V, bool> {
debug!("Asking to coerce {type_name} into {coerce_to_type}");
let coerce_to_type = coerce_to_type.clone();
resolve_coercion_with(contexts, move |node| {
node.as_record()
.map(|r| r.kind == *coerce_to_type)
.unwrap_or(false)
})
}
}

View file

@ -16,8 +16,17 @@
};
};
outputs = { self, nixpkgs, crane, flake-utils, rust-overlay, ... }:
flake-utils.lib.eachDefaultSystem (system:
outputs =
{
self,
nixpkgs,
crane,
flake-utils,
rust-overlay,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs {
inherit system;
@ -25,29 +34,51 @@
};
rustTarget = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
unstableRustTarget = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override {
extensions = [ "rust-src" "miri" "rustfmt" ];
});
unstableRustTarget = pkgs.rust-bin.selectLatestNightlyWith (
toolchain:
toolchain.default.override {
extensions = [
"rust-src"
"miri"
"rustfmt"
];
}
);
craneLib = (crane.mkLib pkgs).overrideToolchain rustTarget;
unstableCraneLib = (crane.mkLib pkgs).overrideToolchain unstableRustTarget;
tomlInfo = craneLib.crateNameFromCargoToml { cargoToml = ./Cargo.toml; };
inherit (tomlInfo) pname version;
inherit (tomlInfo) version;
src = ./.;
rustfmt' = pkgs.writeShellScriptBin "rustfmt" ''
exec "${unstableRustTarget}/bin/rustfmt" "$@"
'';
cargoArtifacts = craneLib.buildDepsOnly {
inherit src;
cargoExtraArgs = "--all-features --all";
common = {
src = ./.;
buildInputs = [
pkgs.openssl
pkgs.pkg-config
];
};
plaixt = craneLib.buildPackage {
inherit cargoArtifacts src version;
cargoExtraArgs = "--all-features --all";
};
cargoArtifacts = craneLib.buildDepsOnly (
common
// {
cargoExtraArgs = "--all-features --all";
}
);
plaixt = craneLib.buildPackage (
common
// {
inherit cargoArtifacts version;
cargoExtraArgs = "--all-features --all";
}
);
in
rec {
@ -78,6 +109,8 @@
devShells.plaixt = pkgs.mkShell {
buildInputs = [ ];
inputsFrom = [ plaixt ];
nativeBuildInputs = [
rustfmt'
rustTarget