Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/libgraphql-core-v1/src/error_note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use crate::span::Span;
/// defined, or where a conflicting field exists)
///
/// This is the schema-layer analogue of
/// [`libgraphql_parser::GraphQLErrorNote`](libgraphql_parser::GraphQLErrorNote)
/// [`libgraphql_parser::GraphQLErrorNote`]
/// in the parser crate. The key difference is that spans here
/// are [`libgraphql_core::Span`]s (deferred byte-offset + source map ID) rather
/// are [`Span`]s (deferred byte-offset + source map ID) rather
/// than pre-resolved `SourceSpan`s, since schema errors may
/// reference locations across multiple source files loaded at
/// different times.
Expand Down
148 changes: 130 additions & 18 deletions crates/libgraphql-core-v1/src/schema/schema_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ use indexmap::IndexMap;
use libgraphql_parser::ast;
use libgraphql_parser::ByteSpan;
use libgraphql_parser::GraphQLErrorNoteKind;
use std::path::Path;
use std::path::PathBuf;

/// Accumulates GraphQL type definitions, directive definitions,
/// and schema metadata, then validates and produces an immutable
Expand Down Expand Up @@ -409,9 +411,113 @@ impl SchemaBuilder {
/// errors are collected into the returned `Err` variant
/// with their original parser spans translated to our
/// [`Span`] type.
///
/// The registered source map carries no source label; when
/// loading multiple sources, prefer
/// [`load_str_with_label()`](Self::load_str_with_label) so
/// diagnostics can identify which source a location refers
/// to.
pub fn load_str(
&mut self,
source: &str,
) -> Result<&mut Self, Vec<SchemaBuildError>> {
self.load_str_impl(source, /* label = */ None)
}

/// Like [`load_str()`](Self::load_str), but labels the
/// registered [`SchemaSourceMap`] with `label` (typically
/// the path the source text was read from).
///
/// The label is stored as the source map's
/// [`file_path()`](SchemaSourceMap::file_path) and is
/// surfaced by diagnostics that resolve spans from this
/// source, which is especially useful when a schema is
/// assembled from multiple source strings.
///
/// # Example
///
/// ```rust
/// # use libgraphql_core_v1 as libgraphql_core;
/// use libgraphql_core::schema::SchemaBuilder;
/// use std::path::Path;
///
/// let mut builder = SchemaBuilder::new();
/// builder.load_str_with_label(
/// "type Query { hello: String }",
/// "schemas/main.graphql",
/// ).unwrap();
/// let schema = builder.build().unwrap();
///
/// assert_eq!(
/// schema.source_maps()[1].file_path(),
/// Some(Path::new("schemas/main.graphql")),
/// );
/// ```
pub fn load_str_with_label(
&mut self,
source: &str,
label: impl AsRef<Path>,
) -> Result<&mut Self, Vec<SchemaBuildError>> {
self.load_str_impl(source, Some(label.as_ref().to_path_buf()))
}

/// Convenience: creates a new `SchemaBuilder` and loads
/// `source` into it in one step.
///
/// Equivalent to [`SchemaBuilder::new()`](Self::new)
/// followed by [`load_str()`](Self::load_str). Additional
/// sources can still be loaded into the returned builder
/// before calling [`build()`](Self::build).
///
/// The error type mirrors `load_str`'s
/// (`Vec<SchemaBuildError>`, the load-phase shape) rather than
/// [`build()`](Self::build)'s `SchemaErrors` — `from_str` is a
/// load-phase API; only `build*` methods return `SchemaErrors`.
///
/// # Example
///
/// ```rust
/// # use libgraphql_core_v1 as libgraphql_core;
/// use libgraphql_core::schema::SchemaBuilder;
///
/// let builder = SchemaBuilder::from_str(
/// "type Query { hello: String }",
/// ).unwrap();
/// let schema = builder.build().unwrap();
///
/// assert!(schema.object_type("Query").is_some());
/// ```
// An inherent `from_str` is intentional (mirroring the other
// string entry points and the v0 API) rather than a
// `std::str::FromStr` impl, which would force callers to
// import the trait for a builder-construction convenience.
#[allow(clippy::should_implement_trait)]
pub fn from_str(source: &str) -> Result<Self, Vec<SchemaBuildError>> {
let mut builder = Self::new();
builder.load_str(source)?;
Ok(builder)
}

/// Like [`from_str()`](Self::from_str), but labels the
/// registered [`SchemaSourceMap`] with `label` (see
/// [`load_str_with_label()`](Self::load_str_with_label)).
pub fn from_str_with_label(
source: &str,
label: impl AsRef<Path>,
) -> Result<Self, Vec<SchemaBuildError>> {
let mut builder = Self::new();
builder.load_str_with_label(source, label)?;
Ok(builder)
}

/// Shared implementation for the `load_str` family:
/// registers a [`SchemaSourceMap`] (labeled with `label`,
/// if provided) for `source`, parses it, and loads all
/// definitions into this builder.
fn load_str_impl(
&mut self,
source: &str,
label: Option<PathBuf>,
) -> Result<&mut Self, Vec<SchemaBuildError>> {
let parse_result =
libgraphql_parser::parse_schema(source);
Expand All @@ -434,7 +540,7 @@ impl SchemaBuilder {
},
};
self.source_maps.push(
SchemaSourceMap::from_source(source, None),
SchemaSourceMap::from_source(source, label),
);

// Report parse-level errors with proper spans
Expand Down Expand Up @@ -791,11 +897,11 @@ impl SchemaBuilder {
/// 3. **Empty type checks** -- rejects object/interface types
/// with no fields, unions with no members, and enums with
/// no values.
/// 4. **Type-system validators** -- runs
/// [`ObjectOrInterfaceTypeValidator`],
/// [`UnionTypeValidator`],
/// [`InputObjectTypeValidator`], and
/// [`validate_directive_definitions`] to enforce
/// 4. **Type-system validators** -- runs the (internal)
/// `ObjectOrInterfaceTypeValidator`,
/// `UnionTypeValidator`,
/// `InputObjectTypeValidator`, and
/// `validate_directive_definitions` passes to enforce
/// cross-type reference rules.
///
/// See [Schema](https://spec.graphql.org/September2025/#sec-Schema).
Expand Down Expand Up @@ -1024,9 +1130,24 @@ impl SchemaBuilder {
pub fn build_from_str(
source: &str,
) -> Result<Schema, SchemaErrors> {
let mut sb = Self::new();
sb.load_str(source).map_err(SchemaErrors::new)?;
sb.build()
Self::from_str(source)
.map_err(SchemaErrors::new)
.and_then(Self::build)
}

/// Like [`build_from_str()`](Self::build_from_str), but
/// labels the registered [`SchemaSourceMap`] with `label`
/// (see [`load_str_with_label()`](Self::load_str_with_label)).
// TODO: SchemaErrors wraps Vec<SchemaBuildError> which is
// large. Consider boxing once error strategy is finalized.
#[allow(clippy::result_large_err)]
pub fn build_from_str_with_label(
source: &str,
label: impl AsRef<Path>,
) -> Result<Schema, SchemaErrors> {
Self::from_str_with_label(source, label)
.map_err(SchemaErrors::new)
.and_then(Self::build)
}

// ---------------------------------------------------------
Expand Down Expand Up @@ -1107,15 +1228,6 @@ impl SchemaBuilder {
pub(crate) fn errors(&self) -> &[SchemaBuildError] {
&self.errors
}

/// Returns the mutation root type name binding (for test
/// inspection).
// TODO: Remove #[allow(dead_code)] once mutation root type
// tests are added or build() consumes this field.
#[allow(dead_code)]
pub(crate) fn mutation_type_name(&self) -> Option<&(TypeName, Span)> {
self.mutation_type_name.as_ref()
}
}

// ---------------------------------------------------------
Expand Down
46 changes: 46 additions & 0 deletions crates/libgraphql-core-v1/src/schema/schema_def.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::names::DirectiveName;
use crate::names::TypeName;
use crate::schema_source_map::LineCol;
use crate::schema_source_map::SchemaSourceMap;
use crate::span::Span;
use crate::types::DirectiveDefinition;
use crate::types::EnumType;
use crate::types::GraphQLType;
Expand Down Expand Up @@ -265,4 +267,48 @@ impl Schema {
pub fn source_maps(&self) -> &[SchemaSourceMap] {
&self.source_maps
}

// ---------------------------------------------------------
// Span resolution
// ---------------------------------------------------------

/// Resolves the start of a **schema-originated** [`Span`] to
/// a 0-based [`LineCol`] position using this schema's stored
/// source maps.
///
/// # Accepted span domain
///
/// This method only accepts spans that originated from this
/// `Schema` — i.e. spans found on its types, fields,
/// directive definitions, and the errors produced while
/// building it. A [`Span`]'s `source_map_id` is an index
/// that is only meaningful relative to the artifact that
/// produced it, so passing a span from any *other* artifact
/// (an operation, an executable document, or a different
/// `Schema`) yields `None` or a plausible-but-wrong
/// location.
///
/// # Return value
///
/// - `Some(LineCol)` for spans whose `source_map_id` is in
/// range for this schema. Spans on built-in definitions
/// (e.g. the `Boolean` scalar or `@skip` directive, which
/// have no user-authored source) resolve against the
/// synthetic built-in source map to line `0`, column `0`.
/// - `None` when the span's `source_map_id` is out of range
/// for this schema.
///
/// Because `Schema` does not retain source text, the
/// returned [`LineCol`]'s `col_utf8` always equals its
/// `col_linestart_byte_offset` (exact for ASCII source; a
/// byte-based approximation when the line contains
/// multi-byte characters).
pub fn resolve_span(&self, span: Span) -> Option<LineCol> {
let source_map = self.source_maps
.get(span.source_map_id.0 as usize)?;
Some(source_map.resolve_offset(
span.byte_span.start,
/* source = */ None,
))
}
}
1 change: 1 addition & 0 deletions crates/libgraphql-core-v1/src/schema/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod schema_build_error_tests;
mod schema_builder_extension_tests;
mod schema_builder_tests;
mod schema_def_tests;
mod schema_errors_tests;
mod type_validation_error_tests;
Loading
Loading