swls-core 0.1.0

Core LSP infrastructure for the Semantic Web Language Server
Documentation
use std::{
    borrow::Cow,
    collections::{HashMap, HashSet},
};

use bevy_ecs::{prelude::*, world::CommandQueue};
use serde::Deserialize;
use tracing::{debug, error, info, span};

use sophia_api::{
    prelude::{Any, Dataset},
    quad::Quad,
    term::Term as _,
};

use crate::{
    lsp_types::{TextDocumentItem, Url},
    prelude::*,
    util::{fs::Fs, ns::owl},
};

use super::setup::FromPrefix;

#[derive(Deserialize, Debug)]
struct Version {
    #[serde(rename = "fileURL")]
    file_url: Option<String>,
    issued: chrono::DateTime<chrono::Utc>,
}

#[derive(Deserialize, Debug)]
struct Vocab {
    versions: Vec<Version>,
}

async fn extract_file_url(prefix: &str, client: &impl Client) -> Option<String> {
    let url = format!(
        "https://lov.linkeddata.es/dataset/lov/api/v2/vocabulary/info?vocab={}",
        prefix
    );
    match client.fetch(&url, &std::collections::HashMap::new()).await {
        Ok(resp) if resp.status == 200 => match serde_json::from_str::<Vocab>(&resp.body) {
            Ok(x) => {
                let versions: Vec<_> = x.versions.iter().flat_map(|x| &x.file_url).collect();
                debug!(
                    "Found lov response ({} versions) {:?}",
                    x.versions.len(),
                    versions
                );
                x.versions
                    .into_iter()
                    .flat_map(|x| x.file_url.map(|url| (url, x.issued)))
                    .max_by_key(|x| x.1)
                    .map(|x| x.0)
            }
            Err(e) => {
                error!("Deserialize failed ({}) {:?}", url, e);
                None
            }
        },
        Ok(resp) => {
            error!("Fetch ({}) failed status {}", url, resp.status);
            None
        }
        Err(e) => {
            error!("Fetch ({}) failed {:?}", url, e);
            None
        }
    }
}

pub fn open_imports<C: Client + Resource>(
    query: Query<(&Triples, &RopeC), Changed<Triples>>,
    mut opened: Local<HashSet<String>>,
    sender: Res<CommandSender>,
    fs: Res<Fs>,
    client: Res<C>,
) {
    for (triples, _) in &query {
        for object in triples
            .quads_matching(Any, [owl::imports], Any, Any)
            .flatten()
            .flat_map(|s| s.o().iri())
            .flat_map(|s| Url::parse(s.as_str()))
        {
            if opened.contains(object.as_str()) {
                continue;
            }
            opened.insert(object.as_str().to_string());

            let fs = fs.clone();
            let sender = sender.clone();
            let fut = async move {
                if let Some(content) = fs.0.read_file(&object).await {
                    spawn_document(object, content, &sender.0, |_, _| {});

                    let mut command_queue = CommandQueue::default();
                    command_queue.push(move |world: &mut World| {
                        world.run_schedule(SaveLabel);
                    });
                    let _ = sender.unbounded_send(command_queue);
                } else {
                    info!("No content found for {}", object);
                }
            };
            client.spawn(fut);
        }
    }
}

/// First of all, fetch the lov dataset information at url <https://lov.linkeddata.es/dataset/lov/api/v2/vocabulary/info?vocab=${prefix}>
/// Next, extract that json object into an object and find the latest dataset
pub fn fetch_lov_properties<C: Client + Resource>(
    sender: Res<CommandSender>,
    query: Query<
        &Prefixes,
        (
            Or<((Changed<Prefixes>, With<Open>), Changed<Open>)>,
        ),
    >,
    ontologies: Query<(Entity, &swls_lov::LocalPrefix)>,
    mut prefixes: Local<HashSet<String>>,
    client: Res<C>,
    fs: Res<Fs>,
) {
    for prefs in &query {
        for prefix in prefs.0.iter() {
            if !prefixes.contains(prefix.url.as_str()) {
                prefixes.insert(prefix.url.to_string());
                let mut found = false;
                for (_e, local) in ontologies
                    .iter()
                    .filter(|(_, x)| x.namespace == prefix.url.as_str())
                {
                    debug!(
                        "Local lov for Prefix {} {} is entry {} {} {} {}",
                        prefix.prefix,
                        prefix.url.as_str(),
                        local.name,
                        local.namespace,
                        local.location,
                        local.content.is_empty()
                    );

                    let label = match crate::lsp_types::Url::parse(&local.location) {
                        Ok(label) => label,
                        Err(e) => {
                            tracing::error!(
                                "Failed to parse defined prefix location {} {:?}",
                                local.location,
                                e
                            );
                            continue;
                        }
                    };

                    found = true;
                    let c = client.as_ref().clone();
                    let sender = sender.0.clone();
                    client.spawn(local_lov::<C>(local.clone(), label, sender, fs.clone(), c));
                }

                if !found {
                    if let Some(url) = fs.0.lov_url(prefix.url.as_str(), &prefix.prefix) {
                        debug!(
                            "Remote lov for prefix {} {}",
                            prefix.prefix,
                            prefix.url.as_str()
                        );
                        let sender = sender.0.clone();
                        let c = client.as_ref().clone();
                        client.spawn(fetch_lov(
                            prefix.clone(),
                            url.clone(),
                            c,
                            sender,
                            fs.clone(),
                        ));
                    }
                }
            }
        }
    }
}

type Sender = futures::channel::mpsc::UnboundedSender<CommandQueue>;

pub fn spawn_document(
    url: Url,
    content: String,
    sender: &Sender,
    extra: impl FnOnce(Entity, &mut World) -> () + Send + Sync + 'static,
) {
    let mut command_queue = CommandQueue::default();
    let item = TextDocumentItem {
        version: 1,
        uri: url.clone(),
        language_id: String::from("turtle"),
        text: String::new(),
    };

    let spawn = spawn_or_insert(
        url.clone(),
        (
            RopeC(ropey::Rope::from_str(&content)),
            Source(content.clone()),
            Label(url.clone()),
            Wrapped(item),
            Types(HashMap::new()),
        ),
        Some("turtle".into()),
        (),
    );

    command_queue.push(move |world: &mut World| {
        let span = span!(tracing::Level::INFO, "span lov");
        let _enter = span.enter();
        let e = spawn(world);

        extra(e, world);

        world.run_schedule(ParseLabel);
    });

    let _ = sender.unbounded_send(command_queue);
}

fn extra_from_lov<C: Client + Resource>(
    from: FromPrefix,
    content: String,
    url: Url,
    fs: Fs,
) -> impl FnOnce(Entity, &mut World) + Send + Sync + 'static {
    move |e, world| {
        world.entity_mut(e).insert(from);

        let client = world.resource::<C>();
        client.spawn(async move {
            fs.0.write_file(&url, &content).await;
        });
    }
}

pub(super) async fn fetch_lov_body<C: Client + Resource>(prefix: &str, c: C) -> Option<String> {
    if let Some(url) = extract_file_url(&prefix, &c).await {
        match c.fetch(&url, &std::collections::HashMap::new()).await {
            Ok(resp) if resp.status == 200 => return Some(resp.body),
            Ok(resp) => {
                error!("Fetch ({}) failed status {}", url, resp.status);
            }
            Err(e) => {
                error!("Fetch ({}) failed {:?}", url, e);
            }
        }
    }
    None
}

async fn fetch_lov<C: Client + Resource>(prefix: Prefix, label: Url, c: C, sender: Sender, fs: Fs) {
    if let Some(body) = fetch_lov_body(&prefix.prefix, c).await {
        let extra = extra_from_lov::<C>(FromPrefix(prefix), body.clone(), label.clone(), fs);
        spawn_document(label, body, &sender, extra);
    }
}

async fn local_lov<C: Client + Resource>(
    local: swls_lov::LocalPrefix,
    label: Url,
    sender: Sender,
    fs: Fs,
    c: C,
) {
    info!(
        "Using local {} {} Label {}",
        local.name,
        local.namespace,
        label.as_str()
    );
    let content = if local.content.is_empty() {
        info!("Fetching from LOV {}", local.name);
        if let Some(body) = fetch_lov_body(&local.name, c).await {
            Cow::Owned(body)
        } else {
            return;
        }
    } else {
        local.content
    };

    let from = FromPrefix(Prefix {
        prefix: local.name.to_string(),
        url: Url::parse(&local.namespace).unwrap(),
    });

    let extra = extra_from_lov::<C>(from, content.to_string(), label.clone(), fs);
    spawn_document(label, content.to_string(), &sender, extra);
}