-
Notifications
You must be signed in to change notification settings - Fork 46
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
It’s too easy to use a schema or document with errors #709
Comments
Sketch of a possible API: /// Immutable, validated. Execution would require this.
///
/// FIXME: keep this name? "Document" is made implicit
struct Executable {…}
/// Mutable, may or may not be valid
///
/// FIXME: some other name?
struct PartialExecutable {…}
/// Immutable, validated.
///
/// `Executable` and `PartialExecutable` require this
struct Schema {…}
/// Mutable, may or may not be valid
///
/// FIXME: some other name?
struct PartialSchema {…}
struct WithErrors<T> {
pub errors: DiagnosticList,
/// Some components may be omitted when `errors` has a corresponding diagnostic
pub document: T,
}
impl Schema {
/// If `Err`, `DiagnosticList` may contain any kind of error
pub fn parse_and_validate(
source_text: impl Into<String>,
path: impl AsRef<Path>,
) -> Result<Self, WithErrors<PartialSchema>> {…}
}
impl PartialSchema {
/// If `Err`, `DiagnosticList` may contain parse errors or/and build errors
pub fn parse(
source_text: impl Into<String>,
path: impl AsRef<Path>,
) -> Result<Self, WithErrors<Self>> {…}
/// If `Err`, `DiagnosticList` only contains validation errors
pub fn validate(self) -> Result<Schema, WithErrors<Self>> {…}
}
impl Executable {
/// If `Err`, `DiagnosticList` may contain any kind of error
pub fn parse_and_validate(
schema: &Schema,
source_text: impl Into<String>,
path: impl AsRef<Path>,
) -> Result<Self, WithErrors<PartialExecutable>> {…}
}
impl PartialExecutable {
/// If `Err`, `DiagnosticList` may contain parse errors or/and build errors
pub fn parse(
schema: &Schema,
source_text: impl Into<String>,
path: impl AsRef<Path>,
) -> Result<Self, WithErrors<Self>> {…}
/// If `Err`, `DiagnosticList` only contains validation errors
pub fn validate(self) -> Result<Executable, WithErrors<Self>> {…}
}
impl ast::Document {
/// If `Err`, `DiagnosticList` only contains parse errors
pub fn parse(
source_text: impl Into<String>,
path: impl AsRef<Path>,
) -> Result<Self, WithErrors<Self>> {…}
// FIXME: names of these four methods?
/// If `Err`, `DiagnosticList` only contains build errors
pub fn to_partial_schema(&self) -> Result<PartialSchema, WithErrors<PartialSchema>> {…}
/// If `Err`, `DiagnosticList` may contains build errors and/or validation errors
pub fn to_schema_validate(&self) -> Result<Schema, WithErrors<PartialSchema>> {…}
/// If `Err`, `DiagnosticList` only contains build errors
pub fn to_partial_executable(
&self,
schema: &Schema,
) -> Result<PartialExecutable, WithErrors<PartialExecutable>> {…}
/// If `Err`, `DiagnosticList` may contains build errors and/or validation errors
pub fn to_executable_validate(
&self,
schema: &Schema,
) -> Result<Executable, WithErrors<PartialExecutable>> {…}
} |
Also: impl Schema {
pub fn into_partial(self) -> PartialSchema {…}
}
impl Executable {
pub fn into_partial(self) -> PartialExecutable {…}
} |
Having all those different types is kinda confusing to me, why don't you do sth. like the following? Schema::parse(...) -> Result<Self, DiagnosticList> // parsing, including validation
Schema::parse_unchecked(...) -> Self // only parsing, as is the current behaviour of parse(...) and same thing for |
It’s possible, but would sacrifice encoding in the type system "this schema/document is valid" and "this function can only be called with a valid schema". Would things be easier to understand with a wrapper type? /// Mutable, may or may not be valid
struct Executable {…}
/// Mutable, may or may not be valid
struct Schema {…}
/// Immutable (`Arc::make_mut` not exposed), can only be obtained from `validate`, implements Deref
struct Valid<T>(Arc<T>);
struct WithErrors<T> {
pub errors: DiagnosticList,
pub document: T,
}
impl Schema {
fn parse_and_validate(self) -> Result<Valid<Self>, WithError<Self>>
fn parse(…) -> Result<Self, WithError<Self>>
fn validate(self) -> Result<Valid<Self>, WithError<Self>>
} |
Yes, the Valid-wrapper is way more intuitive. How are validation warnings handled? |
Hmm good point! I kinda forgot about those 😅 Like now, if validation finds both errors and warnings they’d end up in the same fn validate(self) -> Result<(Valid<Self>, DiagnosticList), (Self, DiagnosticList)> … but part of the motivation for the fn validate(self) -> Result<(Valid<Self>, DiagnosticList), WithErrors<Self>> Or, equivalent but maybe feels less awkward, rename fn validate(self) -> Result<WithDiagnostics<Valid<Self>>, WithDiagnostics<Self>> |
At this point I wonder: should warnings be part of |
I think there is a good argument for splitting them up as you currently have to filter out warnings and "advice" in the most common use cases. |
I agree with splitting them up. In the case of the mentioned WithDiagnostics-wrapper it would also be a little bit contradictory if you had an empty list. |
Concerning the Valid-wrapper, I think that it doesn't really solve the issue as you're still able to easily forget validation when using the unvalidated parse function because you have access to all functions of the Schema or Executable. |
Sorry this was unclear, but the idea is that |
ah then it's fine, didn't know you're working on an execution api, very cool |
By the way, the PR linked above contains a full execution engine but only to support introspection. The current plan is to keep it private at first, and later if someone is interested consider polishing it for other use cases. Some execution-related things like input variable coercion will likely be public though. |
This is breaking change that we should either make before 1.0 leaves beta, or decide not to make.
Status quo
Typical intended usage of apollo-compiler 1.0.0-beta.4 looks like this:
However it’s very easy to forget validation:
… and get a schema and document that look fine and contain some of the expected things, but in fact may have error and silently be missing other things. Specifically, two categories of errors can be encountered before running "full validation":
In either case an error is recorded and returned by
.validate()
. But in the second usage example they’ll be ignored and may cause unexpected results later, when they’re harder to debug.Full validation as well is a property that may be useful to encode in the Rust type system. Maybe even
ExecutableDocument
should only be created with a fully valid schema?Proposal
Change
parse
methods to return aResult
in order to force callers to handle the error case (even if withunwrap
).This result should not always be tied to that of full validation. Federation for example needs to parse subgraph schemas that are invalid per spec because their root
Query
type may be implicit. Federation code fills in the blanks by programmatically mutating aSchema
before calling validation.If each different state is a different Rust type we may need to pick names for up to 4 types just for schemas:
… and similarly for executable documents.
Unless
parse
runs full validation but only when it encounters a build or parse error? In which case both results could share aInvalidSchema
error type.The text was updated successfully, but these errors were encountered: