From b5798cf1cadfd057a14a94c9aa9890594f565857 Mon Sep 17 00:00:00 2001 From: Dominik Nakamura Date: Sun, 7 Jan 2024 13:40:50 +0900 Subject: [PATCH] feat: introduce Mabo project files Adding dedicated `Mabo.toml` files allows to define the root of projects as well as kicking off the start of project metadata and future packaging. This can later extend into an ecosystem that allows to distribute schema file collections and consume them through a dependency management system. --- Cargo.lock | 106 ++++++++- Cargo.toml | 2 + crates/mabo-build/Cargo.toml | 2 +- crates/mabo-build/src/lib.rs | 53 ++--- crates/mabo-compiler/Cargo.toml | 4 +- crates/mabo-lsp/Cargo.toml | 6 +- crates/mabo-lsp/src/handlers/mod.rs | 35 ++- crates/mabo-lsp/src/logging.rs | 4 + crates/mabo-playground/Mabo.toml | 9 + crates/mabo-playground/build.rs | 5 +- crates/mabo-project/Cargo.toml | 21 ++ crates/mabo-project/src/de.rs | 13 ++ crates/mabo-project/src/lib.rs | 211 ++++++++++++++++++ vscode-extension/.vscodeignore | 1 + vscode-extension/package.json | 14 +- vscode-extension/schemas/mabo.json | 66 ++++++ vscode-extension/schemas/mabo.yaml | 62 +++++ .../syntaxes/mabo.tmLanguage.json | 2 +- .../syntaxes/mabo.tmLanguage.yaml | 3 +- 19 files changed, 543 insertions(+), 76 deletions(-) create mode 100644 crates/mabo-playground/Mabo.toml create mode 100644 crates/mabo-project/Cargo.toml create mode 100644 crates/mabo-project/src/de.rs create mode 100644 crates/mabo-project/src/lib.rs create mode 100644 vscode-extension/schemas/mabo.json create mode 100644 vscode-extension/schemas/mabo.yaml diff --git a/Cargo.lock b/Cargo.lock index 4703681..5569b7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,6 +426,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "erased-serde" version = "0.4.2" @@ -500,6 +506,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "heck" version = "0.4.1" @@ -533,9 +545,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" dependencies = [ "crossbeam-deque", "globset", @@ -553,6 +565,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "indoc" version = "2.0.4" @@ -729,10 +751,10 @@ dependencies = [ name = "mabo-build" version = "0.1.0" dependencies = [ - "glob", "insta", "mabo-compiler", "mabo-parser", + "mabo-project", "miette", "prettyplease", "proc-macro2", @@ -809,7 +831,6 @@ dependencies = [ "anyhow", "clap", "directories", - "ignore", "line-index", "log", "lsp-server", @@ -817,6 +838,7 @@ dependencies = [ "mabo-compiler", "mabo-meta", "mabo-parser", + "mabo-project", "ouroboros", "parking_lot", "ropey", @@ -852,6 +874,18 @@ dependencies = [ "mabo-build", ] +[[package]] +name = "mabo-project" +version = "0.1.0" +dependencies = [ + "globset", + "ignore", + "serde", + "spdx", + "thiserror", + "toml", +] + [[package]] name = "memchr" version = "2.7.1" @@ -1221,18 +1255,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", @@ -1261,9 +1295,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.110" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -1281,6 +1315,15 @@ dependencies = [ "syn 2.0.47", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "similar" version = "2.4.0" @@ -1309,6 +1352,15 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "spdx" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bde1398b09b9f93fc2fc9b9da86e362693e999d3a54a8ac47a99a5a73f638b" +dependencies = [ + "smallvec", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -1568,6 +1620,40 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "typed-arena" version = "2.0.2" diff --git a/Cargo.toml b/Cargo.toml index fe260c1..01fc592 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,8 @@ mimalloc = "0.1.39" owo-colors = { version = "3.5.0", features = ["supports-colors"] } proc-macro2 = { version = "1.0.75", default-features = false } quote = { version = "1.0.35", default-features = false } +serde = { version = "1.0.195", features = ["derive"] } +serde_json = "1.0.111" syn = "2.0.47" thiserror = "1.0.56" diff --git a/crates/mabo-build/Cargo.toml b/crates/mabo-build/Cargo.toml index e0d77be..59eee8a 100644 --- a/crates/mabo-build/Cargo.toml +++ b/crates/mabo-build/Cargo.toml @@ -10,13 +10,13 @@ repository.workspace = true license.workspace = true [dependencies] -glob.workspace = true miette = { workspace = true, features = ["fancy-no-backtrace"] } prettyplease = "0.2.16" proc-macro2.workspace = true quote.workspace = true mabo-compiler = { path = "../mabo-compiler" } mabo-parser = { path = "../mabo-parser" } +mabo-project = { path = "../mabo-project" } syn.workspace = true thiserror.workspace = true diff --git a/crates/mabo-build/src/lib.rs b/crates/mabo-build/src/lib.rs index 9f2983b..3953ee5 100644 --- a/crates/mabo-build/src/lib.rs +++ b/crates/mabo-build/src/lib.rs @@ -1,6 +1,6 @@ //! Code generator crate for Rust projects that can be used in `build.rs` build scripts. -use std::{convert::AsRef, env, fmt::Debug, fs, path::PathBuf}; +use std::{env, fmt::Debug, fs, path::PathBuf}; use mabo_parser::Schema; use miette::Report; @@ -19,25 +19,12 @@ pub type Result = std::result::Result; /// Errors that can happen when generating Rust source code from Mabo schema files. #[derive(Error)] pub enum Error { + /// Failed to load the Mabo project. + #[error("failed to load the Mabo project")] + LoadProject(#[source] mabo_project::Error), /// The required OUT_DIR env var doesn't exist. #[error("missing OUT_DIR environment variable")] NoOutDir, - /// One of the user-provided glob patterns is invalid. - #[error("failed to parse the glob pattern {glob:?}")] - Pattern { - /// Source error of the problem. - #[source] - source: glob::PatternError, - /// The problematic pattern. - glob: String, - }, - /// Failed to iterate over the matching files for a pattern. - #[error("failed to read files of a glob pattern")] - Glob { - /// Source error of the problem. - #[source] - source: glob::GlobError, - }, /// The file name resulting from a glob pattern didn't produce a usable file path. #[error("failed to get the file name from a found file path")] NoFileName, @@ -100,7 +87,17 @@ pub enum Error { impl Debug for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self}") + use std::error::Error; + + write!(f, "{self}")?; + + let mut source = self.source(); + while let Some(inner) = source { + write!(f, "\n-> {inner}")?; + source = inner.source(); + } + + Ok(()) } } @@ -141,9 +138,10 @@ impl Compiler { /// /// Will return an `Err` if any of the various cases happen, which are described in the /// [`Error`] type. - pub fn compile(&self, schemas: &[impl AsRef]) -> Result<()> { + pub fn compile(&self, manifest_dir: &str) -> Result<()> { init_miette(); + let project = mabo_project::load(manifest_dir).map_err(Error::LoadProject)?; let out_dir = PathBuf::from(env::var_os("OUT_DIR").ok_or(Error::NoOutDir)?).join("mabo"); fs::create_dir_all(&out_dir).map_err(|source| Error::Create { @@ -154,20 +152,13 @@ impl Compiler { let mut inputs = Vec::new(); let mut validated = Vec::new(); - for schema in schemas.iter().map(AsRef::as_ref) { - for schema in glob::glob(schema).map_err(|source| Error::Pattern { + for path in project.files { + let input = fs::read_to_string(&path).map_err(|source| Error::Read { source, - glob: schema.to_owned(), - })? { - let path = schema.map_err(|e| Error::Glob { source: e })?; - - let input = fs::read_to_string(&path).map_err(|source| Error::Read { - source, - file: path.clone(), - })?; + file: path.clone(), + })?; - inputs.push((path, input)); - } + inputs.push((path, input)); } for (path, input) in &inputs { diff --git a/crates/mabo-compiler/Cargo.toml b/crates/mabo-compiler/Cargo.toml index b001eac..46ec69d 100644 --- a/crates/mabo-compiler/Cargo.toml +++ b/crates/mabo-compiler/Cargo.toml @@ -13,8 +13,8 @@ license.workspace = true miette.workspace = true owo-colors.workspace = true schemars = { version = "0.8.16", optional = true } -serde = { version = "1.0.194", features = ["derive"], optional = true } -serde_json = { version = "1.0.110", optional = true } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } mabo-parser = { path = "../mabo-parser" } thiserror.workspace = true diff --git a/crates/mabo-lsp/Cargo.toml b/crates/mabo-lsp/Cargo.toml index a8f9532..ff99fc4 100644 --- a/crates/mabo-lsp/Cargo.toml +++ b/crates/mabo-lsp/Cargo.toml @@ -13,7 +13,6 @@ license.workspace = true anyhow.workspace = true clap.workspace = true directories = "5.0.1" -ignore = "0.4.21" line-index = "0.1.1" log = { version = "0.4.20", features = ["kv_unstable_std", "std"] } lsp-server = "0.7.6" @@ -21,11 +20,12 @@ lsp-types = { version = "0.95.0", features = ["proposed"] } ouroboros = "0.18.2" parking_lot = "0.12.1" ropey = "1.6.1" -serde = { version = "1.0.194", features = ["derive"] } -serde_json = "1.0.110" +serde.workspace = true +serde_json.workspace = true mabo-compiler = { path = "../mabo-compiler" } mabo-meta = { path = "../mabo-meta" } mabo-parser = { path = "../mabo-parser" } +mabo-project = { path = "../mabo-project" } time = { version = "0.3.31", features = ["formatting", "local-offset", "macros"] } [lints] diff --git a/crates/mabo-lsp/src/handlers/mod.rs b/crates/mabo-lsp/src/handlers/mod.rs index 37f2beb..675fa97 100644 --- a/crates/mabo-lsp/src/handlers/mod.rs +++ b/crates/mabo-lsp/src/handlers/mod.rs @@ -34,25 +34,22 @@ pub fn initialize( ) -> Result { log::trace!("{params:#?}"); - if let Some(root) = params.root_uri { - for path in ignore::Walk::new(root.path()) { - let Ok(path) = path else { continue }; - - if path.file_type().is_some_and(|ty| ty.is_file()) - && path.path().extension().is_some_and(|ext| ext == "mabo") - { - let Ok(text) = std::fs::read_to_string(path.path()) else { - error!(path = as_debug!(path.path()); "failed reading file content"); - continue; - }; - - let Ok(uri) = Url::from_file_path(path.path()) else { - error!(path = as_debug!(path.path()); "failed parsing file path as URI"); - continue; - }; - - state.files.insert(uri.clone(), create_file(uri, text)); - } + if let Some(projects) = params + .root_uri + .and_then(|root| mabo_project::discover(root.path()).ok()) + { + for path in projects.into_iter().flat_map(|project| project.files) { + let Ok(text) = std::fs::read_to_string(&path) else { + error!(path = as_debug!(path); "failed reading file content"); + continue; + }; + + let Ok(uri) = Url::from_file_path(&path) else { + error!(path = as_debug!(path); "failed parsing file path as URI"); + continue; + }; + + state.files.insert(uri.clone(), create_file(uri, text)); } } diff --git a/crates/mabo-lsp/src/logging.rs b/crates/mabo-lsp/src/logging.rs index f14a374..5eb4ecc 100644 --- a/crates/mabo-lsp/src/logging.rs +++ b/crates/mabo-lsp/src/logging.rs @@ -174,6 +174,10 @@ struct FileLogger { impl FileLogger { fn new(file: PathBuf, offset: UtcOffset) -> Result { + if let Some(parent) = file.parent() { + std::fs::create_dir_all(parent)?; + } + Ok(Self { file: File::options().create(true).append(true).open(file)?.into(), offset, diff --git a/crates/mabo-playground/Mabo.toml b/crates/mabo-playground/Mabo.toml new file mode 100644 index 0000000..54980ef --- /dev/null +++ b/crates/mabo-playground/Mabo.toml @@ -0,0 +1,9 @@ +[package] +name = "playground" +files = [ + "src/evolution.mabo", + "src/sample.mabo", + "schemas/*.mabo", + "src/other.mabo", + "src/second.mabo", +] diff --git a/crates/mabo-playground/build.rs b/crates/mabo-playground/build.rs index b0a96f1..eeeb775 100644 --- a/crates/mabo-playground/build.rs +++ b/crates/mabo-playground/build.rs @@ -1,8 +1,5 @@ #![allow(missing_docs)] fn main() -> mabo_build::Result<()> { - let compiler = mabo_build::Compiler::default(); - compiler.compile(&["src/evolution.mabo", "src/sample.mabo"])?; - compiler.compile(&["schemas/*.mabo", "src/other.mabo", "src/second.mabo"])?; - Ok(()) + mabo_build::Compiler::default().compile(env!("CARGO_MANIFEST_DIR")) } diff --git a/crates/mabo-project/Cargo.toml b/crates/mabo-project/Cargo.toml new file mode 100644 index 0000000..d6f996f --- /dev/null +++ b/crates/mabo-project/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "mabo-project" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +globset = "0.4.14" +ignore = "0.4.22" +serde.workspace = true +spdx = "0.10.3" +thiserror.workspace = true +toml = "0.8.8" + +[lints] +workspace = true diff --git a/crates/mabo-project/src/de.rs b/crates/mabo-project/src/de.rs new file mode 100644 index 0000000..8cc3750 --- /dev/null +++ b/crates/mabo-project/src/de.rs @@ -0,0 +1,13 @@ +use std::borrow::Cow; + +use serde::de::{Deserialize, Deserializer, Error}; + +pub fn spdx_expression_opt<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + D::Error: Error, +{ + let raw = Option::>::deserialize(deserializer)?; + raw.map(|value| spdx::Expression::parse(&value).map_err(D::Error::custom)) + .transpose() +} diff --git a/crates/mabo-project/src/lib.rs b/crates/mabo-project/src/lib.rs new file mode 100644 index 0000000..bf3c546 --- /dev/null +++ b/crates/mabo-project/src/lib.rs @@ -0,0 +1,211 @@ +//! Loading and resolution of `Mabo.toml` project files. +//! +//! This crate defines the structs for the contents of project files, as well as common resolution +//! logic like finding all projects in a folder or resolving schema search patterns into absolute +//! file paths. + +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use globset::{GlobBuilder, GlobSetBuilder}; +use ignore::{Walk, WalkBuilder}; +use serde::Deserialize; + +mod de; + +/// Shorthand for the standard result type, that defaults to the crate level's [`Error`] type. +pub type Result = std::result::Result; + +/// Errors that can happen when loading Mabo projects. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// One of the glob patterns is invalid. + #[error("failed to parse the glob pattern {glob:?}")] + Pattern { + /// Source error of the problem. + #[source] + source: globset::Error, + /// The problematic pattern. + glob: String, + }, + /// The glob patterns are valid but couldn't be combined into a single set. + #[error("failed to combine the glob patterns into a set")] + PatternSet(#[source] globset::Error), + /// Failed to iterate over the matching files for a pattern. + #[error("failed to walk over the file tree")] + Walk(#[source] ignore::Error), + /// Failed to read a Mabo project file. + #[error("failed reading project file at {file:?}")] + Read { + /// Source error of the problem. + #[source] + source: std::io::Error, + /// The problematic file. + file: PathBuf, + }, + /// Failed to parse a Mabo project file. + #[error("failed parsing project file {file:?}")] + Parse { + /// Source error of the problem. + #[source] + source: Box, + /// The problematic schema file. + file: PathBuf, + }, + /// Failed to strip the base path from a found schema file. + #[error("failed to turn an absolute path into a relative one")] + StripPrefix(#[source] std::path::StripPrefixError), +} + +/// Parsed `Mabo.toml` project file. +#[derive(Debug, Deserialize)] +pub struct ProjectFile { + /// The package that defines the content of this project. + pub package: Package, +} + +/// Single named collection of schema files that form a package. +#[derive(Debug, Deserialize)] +pub struct Package { + /// The package name is an identifier used to refer to the package. + pub name: String, + /// The description is a short description about the package. + pub description: Option, + /// The license defines the software license that this package is released under. It can be a + /// single [SPDX](https://spdx.dev/) license, or multiple combined with `AND` and `OR` into an + /// expression. + /// + /// See the [SPDX Specification](https://spdx.github.io/spdx-spec/v2.3/) for more details about + /// the exact expression syntax. + /// + /// ## Example + /// + /// ```toml + /// [package] + /// # ... + /// license = "MIT OR Apache-2.0" + /// ``` + #[serde(default, deserialize_with = "de::spdx_expression_opt")] + pub license: Option, + /// List of files that make up the schema package. These are not regular file paths but glob + /// patterns, meaning that file trees can be defined in a consise way like `schemas/**/*.mabo`. + /// + /// Regardless of the [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) defined + /// the final file list is always filtered by the `.mabo` file extension. + pub files: Vec, +} + +/// Single project that was loaded from a `Mabo.toml` file and all files and additional information +/// that comes with it. +#[derive(Debug)] +pub struct Project { + /// Parsed content of the `Mabo.toml` project file. + pub project_file: ProjectFile, + /// Resolved final list of files to process. + pub files: Vec, +} + +/// Search through a project folder and load all possible Mabo project contained within. +/// +/// # Errors +/// +/// Will return `Err` in case of any I/O failure, missing files or an invalid project file format. +pub fn discover(base: impl AsRef) -> Result> { + let pattern = GlobBuilder::new("**/Mabo.toml") + .literal_separator(true) + .build() + .map_err(|source| Error::Pattern { + source, + glob: "**/Mabo.toml".to_owned(), + })? + .compile_matcher(); + + Walk::new(base) + .filter_map(Result::ok) + .filter(|f| f.file_type().map_or(false, |ty| ty.is_file()) && pattern.is_match(f.path())) + .filter_map(|file| { + file.path() + .parent() + .map(|base| load_project(base, file.path())) + }) + .collect() +} + +/// Load a single `Mabo.toml` project, if it's known that there is only a single one possible. +/// +/// This is usually the case when loaded from a single project of a programming language, like a +/// Rust project with a `build.rs` build script. +/// +/// # Errors +/// +/// Will return `Err` in case of any I/O failure, missing files or an invalid project file format. +pub fn load(base: impl AsRef) -> Result { + let base = base.as_ref(); + let file = base.join("Mabo.toml"); + + load_project(base, &file) +} + +fn load_project(base: &Path, file: &Path) -> Result { + let project_file = fs::read_to_string(file).map_err(|source| Error::Read { + source, + file: file.to_owned(), + })?; + let project_file = + toml::from_str::(&project_file).map_err(|source| Error::Parse { + source: source.into(), + file: file.to_owned(), + })?; + + let files = collect_files(base, &project_file.package.files)?; + + Ok(Project { + project_file, + files, + }) +} + +fn collect_files(base: &Path, patterns: &[String]) -> Result> { + let walk = WalkBuilder::new(base) + .follow_links(true) + .same_file_system(true) + .skip_stdout(true) + .build(); + + let patterns = patterns + .iter() + .map(|pattern| { + GlobBuilder::new(pattern) + .literal_separator(true) + .empty_alternates(true) + .build() + .map_err(|source| Error::Pattern { + source, + glob: pattern.to_owned(), + }) + }) + .try_fold(GlobSetBuilder::new(), |mut set, pattern| { + set.add(pattern?); + Ok(set) + })? + .build() + .map_err(Error::PatternSet)?; + + let mut files = Vec::new(); + + for entry in walk { + let entry = entry.map_err(Error::Walk)?; + let path = entry + .path() + .strip_prefix(base) + .map_err(Error::StripPrefix)?; + + if patterns.is_match(path) && path.extension().map_or(false, |ext| ext == "mabo") { + files.push(entry.into_path()); + } + } + + Ok(files) +} diff --git a/vscode-extension/.vscodeignore b/vscode-extension/.vscodeignore index 4f95ec3..24b8980 100644 --- a/vscode-extension/.vscodeignore +++ b/vscode-extension/.vscodeignore @@ -1,5 +1,6 @@ ../ out/ src/ +schemas/*.yaml syntaxes/*.yaml .gitignore diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 4b24c13..eab9917 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -21,8 +21,7 @@ { "id": "mabo", "aliases": [ - "Mabo", - "mabo" + "Mabo" ], "extensions": [ ".mabo" @@ -42,7 +41,7 @@ } ], "configuration": { - "title": "Strongly Typed Encoding Format", + "title": "Mabo", "properties": { "mabo.maxNumberOfProblems": { "scope": "resource", @@ -108,6 +107,12 @@ ] } } + ], + "tomlValidation": [ + { + "fileMatch": "Mabo.toml", + "url": "https://raw.githubusercontent.com/dnaka91/mabo/main/vscode-extension/schemas/mabo.json" + } ] }, "vsce": { @@ -118,8 +123,9 @@ "watch": "bun run esbuild --watch", "build": "bun run esbuild --minify", "lint": "biome check --apply src/**/*.ts", + "schemas": "js-yaml schemas/mabo.yaml > schemas/mabo.json", "syntaxes": "js-yaml syntaxes/mabo.tmLanguage.yaml > syntaxes/mabo.tmLanguage.json", - "vscode:prepublish": "bun run syntaxes && bun run build", + "vscode:prepublish": "bun run schemas && bun run syntaxes && bun run build", "package": "vsce package" }, "dependencies": { diff --git a/vscode-extension/schemas/mabo.json b/vscode-extension/schemas/mabo.json new file mode 100644 index 0000000..3c971f8 --- /dev/null +++ b/vscode-extension/schemas/mabo.json @@ -0,0 +1,66 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Mabo.toml", + "description": "A schema for Mabo.toml.", + "type": "object", + "properties": { + "package": { + "$ref": "#/definitions/Package" + } + }, + "definitions": { + "Package": { + "title": "Package", + "description": "The only fields required by Cargo are [`name`](https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field) and\n[`version`](https://doc.rust-lang.org/cargo/reference/manifest.html#the-version-field). If publishing to a registry, the registry may\nrequire additional fields. See the notes below and [the publishing chapter](https://doc.rust-lang.org/cargo/reference/publishing.html) for requirements for publishing to [crates.io](https://crates.io/).\n", + "type": "object", + "required": [ + "name", + "files" + ], + "properties": { + "name": { + "description": "The package name is an identifier used to refer to the package. It is used\nwhen listed as a dependency in another package, and as the default name of\ninferred lib and bin targets.\n\nThe name must use only [alphanumeric](https://doc.rust-lang.org/std/primitive.char.html#method.is_alphanumeric) characters or `-` or `_`, and cannot be empty.\nNote that [`cargo new`](https://doc.rust-lang.org/cargo/commands/cargo-new.html) and [`cargo init`](https://doc.rust-lang.org/cargo/commands/cargo-init.html) impose some additional restrictions on\nthe package name, such as enforcing that it is a valid Rust identifier and not\na keyword. [crates.io](https://crates.io) imposes even more restrictions, such as\nenforcing only ASCII characters, not a reserved name, not a special Windows\nname such as \"nul\", is not too long, etc.\n", + "type": "string" + }, + "authors": { + "$ref": "#/definitions/Authors" + }, + "description": { + "$ref": "#/definitions/Description" + }, + "license": { + "$ref": "#/definitions/License" + }, + "readme": { + "$ref": "#/definitions/Readme" + } + } + }, + "Readme": { + "title": "Readme", + "description": "The `readme` field should be the path to a file in the package root (relative\nto this `Cargo.toml`) that contains general information about the package.\nThis file will be transferred to the registry when you publish. [crates.io](https://crates.io)\nwill interpret it as Markdown and render it on the crate's page.\n\n```toml\n[package]\n# ...\nreadme = \"README.md\"\n```\n\nIf no value is specified for this field, and a file named `README.md`,\n`README.txt` or `README` exists in the package root, then the name of that\nfile will be used. You can suppress this behavior by setting this field to\n`false`. If the field is set to `true`, a default value of `README.md` will\nbe assumed.\n", + "type": "string" + }, + "Authors": { + "title": "Authors", + "description": "The `authors` field lists people or organizations that are considered the\n\"authors\" of the package. The exact meaning is open to interpretation — it may\nlist the original or primary authors, current maintainers, or owners of the\npackage. These names will be listed on the crate's page on\n[crates.io](https://crates.io). An optional email address may be included within angled\nbrackets at the end of each author.\n\n> **Note**: [crates.io](https://crates.io) requires at least one author to be listed.\n", + "type": "array", + "items": [ + { + "type": "string", + "description": "The `authors` field lists people or organizations that are considered the\n\"authors\" of the package. The exact meaning is open to interpretation — it may\nlist the original or primary authors, current maintainers, or owners of the\npackage. These names will be listed on the crate's page on\n[crates.io](https://crates.io). An optional email address may be included within angled\nbrackets at the end of each author.\n\n> **Note**: [crates.io](https://crates.io) requires at least one author to be listed.\n" + } + ] + }, + "Description": { + "title": "Description", + "description": "The description is a short blurb about the package. [crates.io](https://crates.io) will display\nthis with your package. This should be plain text (not Markdown).\n\n```toml\n[package]\n# ...\ndescription = \"A short description of my package\"\n```\n\n> **Note**: [crates.io](https://crates.io) requires the `description` to be set.\n", + "type": "string" + }, + "License": { + "title": "License", + "description": "The `license` field contains the name of the software license that the package\nis released under.\n\n[crates.io](https://crates.io/) interprets the `license` field as an [SPDX 2.1 license\nexpression](https://spdx.org/spdx-specification-21-web-version#h.jxpfx0ykyb60). The name must be a known license\nfrom the [SPDX license list 3.6](https://github.com/spdx/license-list-data/tree/v3.6). Parentheses are not\ncurrently supported. See the [SPDX site](https://spdx.org/license-list) for more information.\n\nSPDX license expressions support AND and OR operators to combine multiple\nlicenses.\n\n```toml\n[package]\n# ...\nlicense = \"MIT OR Apache-2.0\"\n```\n\nUsing `OR` indicates the user may choose either license. Using `AND` indicates\nthe user must comply with both licenses simultaneously. The `WITH` operator\nindicates a license with a special exception. Some examples:\n\n* `MIT OR Apache-2.0`\n* `LGPL-2.1 AND MIT AND BSD-2-Clause`\n* `GPL-2.0+ WITH Bison-exception-2.2`\n\nIf a package is using a nonstandard license, then the `license-file` field may\nbe specified in lieu of the `license` field.\n", + "type": "string" + } + } +} diff --git a/vscode-extension/schemas/mabo.yaml b/vscode-extension/schemas/mabo.yaml new file mode 100644 index 0000000..f5c2b8b --- /dev/null +++ b/vscode-extension/schemas/mabo.yaml @@ -0,0 +1,62 @@ +# yaml-language-server: $schema=http://json-schema.org/draft-07/schema# +$schema: http://json-schema.org/draft-07/schema# +title: Mabo.toml +description: A schema for Mabo.toml. +type: object +properties: + package: + $ref: "#/definitions/Package" +definitions: + Package: + title: Package + description: Single named collection of schema files that form a package. + type: object + required: + - name + - files + properties: + name: + $ref: "#/definitions/Name" + description: + $ref: "#/definitions/Description" + license: + $ref: "#/definitions/License" + files: + $ref: "#/definitions/Files" + Name: + title: Name + description: The package name is an identifier used to refer to the package. + type: string + Description: + title: Description + description: The description is a short description about the package. + type: string + License: + title: License + description: | + The license defines the software license that this package is released under. It can be a single + [SPDX](https://spdx.dev/) license, or multiple combined with `AND` and `OR` into an expression. + + See the [SPDX Specification](https://spdx.github.io/spdx-spec/v2.3/) for more details about the exact expression + syntax. + + ## Example + + ```toml + [package] + # ... + license = "MIT OR Apache-2.0" + ``` + type: string + Files: + title: Files + description: &filesDesc | + List of files that make up the schema package. These are not regular file paths but glob patterns, meaning that + file trees can be defined in a consise way like `schemas/**/*.mabo`. + + Regardless of the [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) defined the final file list is + always filtered by the `.mabo` file extension. + type: array + items: + - type: string + description: *filesDesc diff --git a/vscode-extension/syntaxes/mabo.tmLanguage.json b/vscode-extension/syntaxes/mabo.tmLanguage.json index 89c1e26..1ec76f3 100644 --- a/vscode-extension/syntaxes/mabo.tmLanguage.json +++ b/vscode-extension/syntaxes/mabo.tmLanguage.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "$schema": "https://json.schemastore.org/tmlanguage.json", "name": "Mabo Schema", "scopeName": "source.mabo", "fileTypes": [ diff --git a/vscode-extension/syntaxes/mabo.tmLanguage.yaml b/vscode-extension/syntaxes/mabo.tmLanguage.yaml index 7cbb760..8da6818 100644 --- a/vscode-extension/syntaxes/mabo.tmLanguage.yaml +++ b/vscode-extension/syntaxes/mabo.tmLanguage.yaml @@ -1,4 +1,5 @@ -$schema: "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json" +# yaml-language-server: $schema=https://json.schemastore.org/tmlanguage.json +$schema: https://json.schemastore.org/tmlanguage.json name: "Mabo Schema" scopeName: source.mabo fileTypes: [mabo]