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}