Skip to main content

cratestack_parser/
entry.rs

1//! Public entry points: `parse_schema*`.
2//!
3//! Split out of `lib.rs` (cratestack#916) once every entry point that
4//! actually knows a path started tagging its returned [`SchemaError`]s with
5//! it via `SchemaError::with_file` — the extra couple of lines per function
6//! pushed `lib.rs` (already carrying the full module declaration list) past
7//! the 200-line ceiling.
8
9use std::path::Path;
10use std::sync::Arc;
11
12use crate::{SchemaError, parse, validate};
13
14/// File identity used when a caller parses a schema with no path to give.
15/// Prefer [`parse_schema_named`] whenever a path is known: it is what makes
16/// a rendered diagnostic name the file.
17pub const ANONYMOUS_SCHEMA: &str = "<schema>";
18
19pub fn parse_schema(source: &str) -> Result<cratestack_core::Schema, SchemaError> {
20    parse_schema_named(ANONYMOUS_SCHEMA, source)
21}
22
23pub fn parse_schema_named(
24    path: &str,
25    source: &str,
26) -> Result<cratestack_core::Schema, SchemaError> {
27    // Allocated inside `map_err` rather than up front: a successful parse —
28    // the overwhelmingly common case, and the one a proc macro hits on every
29    // expansion — should not heap-copy the whole schema text just to drop it.
30    let tag = |error: SchemaError| error.with_file(&Arc::from(path), &Arc::from(source));
31    let schema = parse::parse_schema_only(source).map_err(tag)?;
32    validate::validate_schema(path, source, &schema).map_err(tag)?;
33    Ok(schema)
34}
35
36/// Parse and validate, reporting **every** independent problem rather than
37/// only the first.
38///
39/// [`parse_schema_named`] stops at the first error, which is right for a
40/// compiler or a CLI: the build is failing either way, and one clear message
41/// beats a cascade. It is wrong for an editor, where stopping early means the
42/// author fixes one error, saves, and is handed the next — one round trip per
43/// mistake.
44///
45/// Semantics worth knowing:
46///
47/// * A **syntax** error still yields exactly one diagnostic. Parsing has no
48///   recovery, so there is no second error to report — everything after the
49///   failure is unparsed, not valid.
50/// * **Validation** errors are collected in stages, and a stage runs only when
51///   every earlier stage was clean. Several validators document that they
52///   assume an earlier one passed, and running them over already-rejected input
53///   produces cascades pointing at the wrong places. Within a stage, every
54///   declaration reports independently — three models each naming a type that
55///   does not exist produce three diagnostics, not three round trips.
56/// * The schema is returned only when there are no errors at all, matching
57///   [`parse_schema_named`].
58///
59/// The first element of the returned `Vec` is always the same error
60/// [`parse_schema_named`] would have returned; both go through one set of
61/// checks in one order, so they cannot drift apart.
62pub fn parse_schema_diagnostics(
63    path: &str,
64    source: &str,
65) -> (Option<cratestack_core::Schema>, Vec<SchemaError>) {
66    let file: Arc<str> = Arc::from(path);
67    let source_arc: Arc<str> = Arc::from(source);
68    let schema = match parse::parse_schema_only(source) {
69        Ok(schema) => schema,
70        Err(error) => return (None, vec![error.with_file(&file, &source_arc)]),
71    };
72    let errors: Vec<SchemaError> = validate::validate_schema_collecting(path, source, &schema)
73        .into_iter()
74        .map(|error| error.with_file(&file, &source_arc))
75        .collect();
76    if errors.is_empty() {
77        (Some(schema), Vec::new())
78    } else {
79        (None, errors)
80    }
81}
82
83/// Parse a `.cstack` source into a [`cratestack_core::Schema`] WITHOUT
84/// running [`validate::validate_schema`].
85///
86/// Prefer [`parse_schema`] for any new source — this exists for two
87/// legitimate cases where the validated pipeline understates what a
88/// `Schema` value can actually be:
89///
90/// 1. A committed `migrations/*/schema.snapshot.json` can predate a
91///    validation rule added later. `cratestack-cli`'s `migrate diff`
92///    deserializes that "previous" snapshot directly and never re-runs
93///    `validate_schema` on it (only the *new* side, parsed fresh from the
94///    `.cstack` source, goes through [`parse_schema_file`]) — so an emitter
95///    can still legitimately be handed a shape the current validator would
96///    reject at the source level.
97/// 2. Tests that deliberately exercise an emitter's rendering logic for
98///    such an already-invalid shape, to prove the emitter itself still
99///    behaves sanely if that shape arrives via (1) — see
100///    `cratestack-migrate`'s `emit::postgres::tests::enums` for an example
101///    (a list-valued enum column, rejected by cratestack#229/#236 at parse
102///    time, but still real input to the Postgres emitter via a pre-#236
103///    snapshot).
104pub fn parse_schema_unvalidated(source: &str) -> Result<cratestack_core::Schema, SchemaError> {
105    // Tagged like `parse_schema`: this entry point takes no path, so the
106    // placeholder is the honest answer — but the error still carries the
107    // source, so `render()` produces a real code frame instead of a bare
108    // message. Before this, the only public route to an untagged error
109    // rendered as 36 bytes with no file, line, or excerpt.
110    parse::parse_schema_only(source)
111        .map_err(|error| error.with_file(&Arc::from(ANONYMOUS_SCHEMA), &Arc::from(source)))
112}
113
114pub fn parse_schema_file(path: impl AsRef<Path>) -> Result<cratestack_core::Schema, SchemaError> {
115    let path = path.as_ref();
116    let display_path = path.display().to_string();
117    let source = std::fs::read_to_string(path).map_err(|error| {
118        SchemaError::new(
119            format!("failed to read schema file {display_path}: {error}"),
120            0..0,
121            1,
122        )
123        .with_file(&Arc::from(display_path.as_str()), &Arc::from(""))
124    })?;
125    parse_schema_named(&display_path, &source)
126}