Skip to main content

cratestack_parser/
lib.rs

1mod diagnostics;
2mod line_helpers;
3mod parse;
4mod relation_actions;
5mod relation_helpers;
6mod validate;
7
8#[cfg(test)]
9mod tests_attribute_spacing;
10#[cfg(test)]
11mod tests_basic;
12mod tests_builder_add_setter_collisions;
13#[cfg(test)]
14mod tests_builder_collisions;
15mod tests_builder_collisions_derived;
16#[cfg(test)]
17mod tests_client_method_collisions;
18#[cfg(test)]
19mod tests_computed;
20#[cfg(test)]
21mod tests_computed_params;
22#[cfg(test)]
23mod tests_docs;
24#[cfg(test)]
25mod tests_enums;
26#[cfg(test)]
27mod tests_extensions;
28#[cfg(test)]
29mod tests_field_attrs;
30#[cfg(test)]
31mod tests_list_arity;
32#[cfg(test)]
33mod tests_mixins;
34#[cfg(test)]
35mod tests_model_attrs;
36#[cfg(test)]
37mod tests_model_index;
38#[cfg(test)]
39mod tests_model_internal;
40#[cfg(test)]
41mod tests_model_unique;
42#[cfg(test)]
43mod tests_multi_error;
44mod tests_patch_touch_flag_collisions;
45#[cfg(test)]
46mod tests_procedure_handler_collisions;
47#[cfg(test)]
48mod tests_procedures;
49#[cfg(test)]
50mod tests_queries;
51#[cfg(test)]
52mod tests_queries_attributes;
53#[cfg(test)]
54mod tests_queries_rejections;
55#[cfg(test)]
56mod tests_queries_sql_body;
57#[cfg(test)]
58mod tests_queries_support;
59#[cfg(test)]
60mod tests_relation_actions;
61#[cfg(test)]
62mod tests_relations;
63#[cfg(test)]
64mod tests_relations_policy;
65#[cfg(test)]
66mod tests_reserved_keywords;
67#[cfg(test)]
68mod tests_snake_case_collisions;
69#[cfg(test)]
70mod tests_spatial;
71#[cfg(test)]
72mod tests_stream_attribute;
73#[cfg(test)]
74mod tests_transport;
75#[cfg(test)]
76mod tests_type_declaration_collisions;
77#[cfg(test)]
78mod tests_types;
79#[cfg(test)]
80mod tests_validators;
81#[cfg(test)]
82mod tests_vector;
83#[cfg(test)]
84mod tests_version;
85#[cfg(test)]
86mod tests_views;
87
88use std::path::Path;
89
90pub use diagnostics::SchemaError;
91
92/// Canonical scalar type names built into the `.cstack` language (e.g.
93/// `String`, `Int`, `Decimal`, ...), including `Page` (which is valid only
94/// as a procedure return type — see `validate::type_names::validate_type_ref`
95/// — not as a plain field type).
96///
97/// This is the same list `cratestack-lsp`'s autocompletion and the
98/// `cratestack-pg`/`cratestack-sqlite` emitter/decoder round-trip tests
99/// assert against, so a new builtin scalar only has to be added here once —
100/// see cratestack#232 for why that matters (this list had already silently
101/// drifted from the LSP's hand-copied one before this accessor existed).
102pub fn builtin_type_names() -> &'static [&'static str] {
103    validate::builtin_type_names()
104}
105
106#[cfg(test)]
107use relation_helpers::parse_relation_attribute;
108
109pub fn parse_schema(source: &str) -> Result<cratestack_core::Schema, SchemaError> {
110    parse_schema_named("<schema>", source)
111}
112
113pub fn parse_schema_named(
114    path: &str,
115    source: &str,
116) -> Result<cratestack_core::Schema, SchemaError> {
117    let schema = parse::parse_schema_only(source)?;
118    validate::validate_schema(path, source, &schema)?;
119    Ok(schema)
120}
121
122/// Parse and validate, reporting **every** independent problem rather than
123/// only the first.
124///
125/// [`parse_schema_named`] stops at the first error, which is right for a
126/// compiler or a CLI: the build is failing either way, and one clear message
127/// beats a cascade. It is wrong for an editor, where stopping early means the
128/// author fixes one error, saves, and is handed the next — one round trip per
129/// mistake.
130///
131/// Semantics worth knowing:
132///
133/// * A **syntax** error still yields exactly one diagnostic. Parsing has no
134///   recovery, so there is no second error to report — everything after the
135///   failure is unparsed, not valid.
136/// * **Validation** errors are collected in stages, and a stage runs only when
137///   every earlier stage was clean. Several validators document that they
138///   assume an earlier one passed, and running them over already-rejected input
139///   produces cascades pointing at the wrong places. Within a stage, every
140///   declaration reports independently — three models each naming a type that
141///   does not exist produce three diagnostics, not three round trips.
142/// * The schema is returned only when there are no errors at all, matching
143///   [`parse_schema_named`].
144///
145/// The first element of the returned `Vec` is always the same error
146/// [`parse_schema_named`] would have returned; both go through one set of
147/// checks in one order, so they cannot drift apart.
148pub fn parse_schema_diagnostics(
149    path: &str,
150    source: &str,
151) -> (Option<cratestack_core::Schema>, Vec<SchemaError>) {
152    let schema = match parse::parse_schema_only(source) {
153        Ok(schema) => schema,
154        Err(error) => return (None, vec![error]),
155    };
156    let errors = validate::validate_schema_collecting(path, source, &schema);
157    if errors.is_empty() {
158        (Some(schema), Vec::new())
159    } else {
160        (None, errors)
161    }
162}
163
164/// Parse a `.cstack` source into a [`cratestack_core::Schema`] WITHOUT
165/// running [`validate::validate_schema`].
166///
167/// Prefer [`parse_schema`] for any new source — this exists for two
168/// legitimate cases where the validated pipeline understates what a
169/// `Schema` value can actually be:
170///
171/// 1. A committed `migrations/*/schema.snapshot.json` can predate a
172///    validation rule added later. `cratestack-cli`'s `migrate diff`
173///    deserializes that "previous" snapshot directly and never re-runs
174///    `validate_schema` on it (only the *new* side, parsed fresh from the
175///    `.cstack` source, goes through [`parse_schema_file`]) — so an emitter
176///    can still legitimately be handed a shape the current validator would
177///    reject at the source level.
178/// 2. Tests that deliberately exercise an emitter's rendering logic for
179///    such an already-invalid shape, to prove the emitter itself still
180///    behaves sanely if that shape arrives via (1) — see
181///    `cratestack-migrate`'s `emit::postgres::tests::enums` for an example
182///    (a list-valued enum column, rejected by cratestack#229/#236 at parse
183///    time, but still real input to the Postgres emitter via a pre-#236
184///    snapshot).
185pub fn parse_schema_unvalidated(source: &str) -> Result<cratestack_core::Schema, SchemaError> {
186    parse::parse_schema_only(source)
187}
188
189pub fn parse_schema_file(path: impl AsRef<Path>) -> Result<cratestack_core::Schema, SchemaError> {
190    let path = path.as_ref();
191    let source = std::fs::read_to_string(path).map_err(|error| {
192        SchemaError::new(
193            format!("failed to read schema file {}: {error}", path.display()),
194            0..0,
195            1,
196        )
197    })?;
198    parse_schema_named(&path.display().to_string(), &source)
199}