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]