Skip to content

Commit

Permalink
refactor: reorganize the stef-compiler crate
Browse files Browse the repository at this point in the history
Shift several components around for better structure, expose all the
possible errors on the API and document any publicly visible types.
  • Loading branch information
dnaka91 committed Nov 2, 2023
1 parent c380ace commit 5d430be
Show file tree
Hide file tree
Showing 8 changed files with 550 additions and 391 deletions.
2 changes: 1 addition & 1 deletion crates/stef-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::{
path::{Path, PathBuf},
};

use miette::{NamedSource, Report};
use miette::Report;
use stef_parser::Schema;
use thiserror::Error;

Expand Down
192 changes: 25 additions & 167 deletions crates/stef-compiler/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,173 +1,31 @@
//! Compiler for `STEF` schema files.
//!
//! In this context, compiling means to validate schema files and resolve all used types within.
//! This allows any code generator to consider its input valid and not having to do extra validation
//! work on the schemas.
//!
//! # Example
//!
//! Compile a basic `STEF` schema:
//!
//! ```
//! let schema = stef_parser::Schema::parse("struct Sample(u32 @1)", None).unwrap();
//!
//! // Ensure the schema in itself is valid (for example no duplicate IDs, type or field names).
//! stef_compiler::validate_schema(&schema).unwrap();
//! // Resolve all types used in the schema, both in the schema itself and its submodules, and in
//! // potentially types from external schemas that are referenced in it.
//! stef_compiler::resolve_schemas(&[("test", &schema)]).unwrap();
//! ```

#![forbid(unsafe_code)]
#![deny(rust_2018_idioms, clippy::all)]
#![warn(clippy::pedantic)]
#![warn(missing_docs, clippy::pedantic)]
#![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)]

use std::{
error::Error,
fmt::{self, Display},
};

pub use ids::{DuplicateFieldId, DuplicateId, DuplicateVariantId};
use miette::{Diagnostic, NamedSource};
use stef_parser::{Definition, Schema};
use thiserror::Error;
pub use resolve::schemas as resolve_schemas;
pub use validate::schema as validate_schema;

use self::{
generics::InvalidGenericType,
names::{DuplicateFieldName, DuplicateName},
resolve::ResolveError,
};

mod generics;
mod highlight;
mod ids;
mod names;
mod resolve;

#[derive(Debug, Diagnostic, Error)]
pub enum ValidationError {
#[error("duplicate ID found")]
#[diagnostic(transparent)]
DuplicateId(#[from] DuplicateId),
#[error("duplicate name found")]
#[diagnostic(transparent)]
DuplicateName(#[from] DuplicateName),
#[error("invalid generic type found")]
#[diagnostic(transparent)]
InvalidGeneric(#[from] InvalidGenericType),
}

impl From<DuplicateFieldId> for ValidationError {
fn from(v: DuplicateFieldId) -> Self {
Self::DuplicateId(v.into())
}
}

impl From<DuplicateFieldName> for ValidationError {
fn from(v: DuplicateFieldName) -> Self {
Self::DuplicateName(v.into())
}
}

pub fn validate_schema(value: &Schema<'_>) -> Result<(), ValidationError> {
names::validate_names_in_module(&value.definitions)?;
value.definitions.iter().try_for_each(validate_definition)
}

fn validate_definition(value: &Definition<'_>) -> Result<(), ValidationError> {
match value {
Definition::Module(m) => {
names::validate_names_in_module(&m.definitions)?;
m.definitions.iter().try_for_each(validate_definition)?;
}
Definition::Struct(s) => {
ids::validate_struct_ids(s)?;
names::validate_struct_names(s)?;
generics::validate_struct_generics(s)?;
}
Definition::Enum(e) => {
ids::validate_enum_ids(e)?;
names::validate_enum_names(e)?;
generics::validate_enum_generics(e)?;
}
Definition::TypeAlias(_) | Definition::Const(_) | Definition::Import(_) => {}
}

Ok(())
}

#[derive(Debug)]
pub struct ResolutionError {
source_code: NamedSource,
cause: resolve::ResolveError,
}

impl Error for ResolutionError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.cause)
}
}

impl Display for ResolutionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("type resolution failed")
}
}

impl Diagnostic for ResolutionError {
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.cause.code()
}

fn severity(&self) -> Option<miette::Severity> {
self.cause.severity()
}

fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.cause.help()
}

fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.cause.url()
}

fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.source_code)
}

fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
self.cause.labels()
}

fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
self.cause.related()
}

fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.cause.diagnostic_source()
}
}

pub fn resolve_schemas(values: &[(&str, &Schema<'_>)]) -> Result<(), ResolutionError> {
let modules = values
.iter()
.map(|(name, schema)| (*name, resolve::resolve_types(name, schema)))
.collect::<Vec<_>>();

for (schema, module) in modules
.iter()
.enumerate()
.map(|(i, (_, module))| (values[i].1, module))
{
let mut missing = Vec::new();
resolve::resolve_module_types(module, &mut missing);

let imports =
resolve::resolve_module_imports(module, &modules).map_err(|e| ResolutionError {
source_code: NamedSource::new(
schema
.path
.as_ref()
.map_or_else(|| "<unknown>".to_owned(), |p| p.display().to_string()),
schema.source.to_owned(),
),
cause: ResolveError::Import(e),
})?;

for ty in missing {
resolve::resolve_type_remotely(ty, &imports).map_err(|e| ResolutionError {
source_code: NamedSource::new(
schema
.path
.as_ref()
.map_or_else(|| "<unknown>".to_owned(), |p| p.display().to_string()),
schema.source.to_owned(),
),
cause: e,
})?;
}
}

Ok(())
}
pub mod resolve;
pub mod validate;
Loading

0 comments on commit 5d430be

Please sign in to comment.