-
Notifications
You must be signed in to change notification settings - Fork 257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rust implementation of the core spec #326
Conversation
sorry about the file movement noise y'all. If you want to hide all the moved files in this PR, you can run this snippet in the dev console: document.querySelectorAll('a[title^="stargate"]').forEach(e => e.closest('.file').remove()) (there are moved files because I put |
This is ready for review |
@queerviolet Since we already agreed to hoist the Could we cherry pick I think, just |
… and create the Bounds trait to get bounds if desired.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello!
I had a chance to look through this today finally. I hope some of these comments are helpful. Please let me know if I can clarify anything, or you wanted more specific feedback anywhere in this code!
One overarching comment I have is to run this with cargo clippy
, as there are a few warnings it's currently printing for this crate (and I can't review it better than clippy for those parts anyways)
using/src/schema.rs
Outdated
/// displaying the error. | ||
/// | ||
/// Returns the empty string if this error does not reference any `Request`s. | ||
fn requests(&self) -> String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would implement a Display
for SchemaError
in this case, something like this:
impl fmt::Display for SchemaError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
let formatting_logic = // what you have already written below in your code
write!(fmt, "{}", formatting_logic)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, derive(thiserror::Error)
is taking care of implementing fmt::Display
. We're using that for errors everywhere else, so I kindof wanted to maintain that here (it also looks nice). But specifically for the OverlappingPrefix
case, I wanted to list all the overlapping requests as part of the error, which requires a bit more implementation than I wanted to put into the #[error(...)]
attribute, so we call out to this fn
using/src/version.rs
Outdated
} | ||
} | ||
|
||
/// Return true iff this Version satisfies the `required` version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
smol typo
/// Return true iff this Version satisfies the `required` version | |
/// Return true if this Version satisfies the `required` version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, that's intentional, "iff" = "if and only if"
but i can spell that out
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ohhhhh definitely didn't know that! ++ on spelling it out then.
using/src/version.rs
Outdated
use regex::Regex; | ||
use thiserror::Error; | ||
|
||
/// Versions are a (major, minor) u64 pair. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this can't use the semver
crate because graphql versions are not technically semver
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we don't have patch versions. I thought about maybe hacking it (appending a patch version of 0
always), but the logic is simple enough that I'm not sure it's worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah yea! this approach seems good then (i think semver implementation is pretty much what you have here, plus the extra five lines for minor
anyways)
using/src/schema.rs
Outdated
Ok(machined) | ||
} | ||
|
||
/// Validate that no two `Request`s are using the same `prefix`, removing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You do not need a doc comment here (///) since this is not a public function (just // works)
using/src/schema.rs
Outdated
} | ||
} | ||
|
||
/// Drain all items from a `Vec<T>` which match `pred` and collect the results |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does not need to be a doc comment
using/src/activations.rs
Outdated
/// matching implementations were available. | ||
// type Activation<'schema, 'impls, T> = (&'schema Request, Find<'impls, T>); | ||
|
||
impl<'a> Schema<'a> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason for having this not part of schema.rs
? It's definitely not standard, and from what I understand of this PR I don't see a reason for it to be separate, but maybe I am missing something!
using/src/activations.rs
Outdated
/// # Ok::<(), GraphQLParseError>(()) | ||
/// ``` | ||
pub fn activations<'impls, T>( | ||
&'impls self, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With impl
being a reserved word, it's a bit weird to have 'impls
as a name for a lifetime. Would definitely use something else (I do understand it's trying to describe a lifetime of Implementations, which is just very close to impl
hah)
using/src/request.rs
Outdated
|
||
use crate::spec::{Spec, SpecParseError}; | ||
|
||
/// Requests contain a `spec`, the `prefix` requested for that spec (which |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using/src/schema.rs
Outdated
@@ -0,0 +1,693 @@ | |||
//! A GraphQL schema referencing one or more specifications |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PTAL @lrlna @trevor-scheer @lrlna, great cow suggestion, I've implemented your changes. As I've been working with this library in #305, I've found that it would indeed be very helpful to be able to easily define specs as top-level consts. This is really nice! |
@queerviolet in regards to your first question:
My answer here depends on how often you expect to use impl Default for Spec {
fn default() -> Self {
identity: String::From("https://lib.apollo.dev/core"),
name: String::from("core"),
version: Version(0, 1),
}
} TL;DR: for only internal library changes, P.S. So very sorry it took me so long to get back to you 🙈 |
d5f6ed9
to
373ab32
Compare
Most of all, its worth noting that the code coverage results are not surfaced _anywhere_ right now as of this commit and there are no thresholds that hold us accountable for meeting or regressing on coverage requirements. However, the motivating factor in this removal is an error in the CI because of linker issues with profiling mechanisms that don't exist in WASM code. Put simply, this is not able to provide coverage for the code that we trust today. We absolutely do WANT code coverage, but the link to the [Stackoverflow post] in the comment within the code that introduced this current implementation (which this commit removes) states a very real obstacle and a very relevant issue. The introduction of this was arguably a bit premature given the relative infancy of the Rust code coverage tooling in the ecosystem and we never agreed on how we wanted to configure this. Hope is on the way (see attached), but for now let's just count on the fact that Rust's compiler is pretty good and Clippy (which we also have enabled on this repository) already points out dead-code and omissions to best practices. Alternatively, let's actually discuss how we want to configure this to really surface code coverage problems in the code we have in a meaningful way, not just running code coverage in CI for the sake of running code coverage in CI but not giving any meaningful output that indicates if it was coverage win or loss or anything.) Ref: https://blog.rust-lang.org/inside-rust/2020/11/12/source-based-code-coverage.html [Stackoverflow post]: https://stackoverflow.com/questions/61989746/how-do-i-use-zprofile-to-generate-code-coverage-for-a-nostd-kernel)
PTAL, I think this is ready to go? |
|
||
/// Versions are a (major, minor) u64 pair. | ||
/// | ||
/// Versions implement `PartialOrd` and `Ord`, which orders them by major and then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As with the TypeScript version, I'm not sure what the purpose of defining ordering (esp Ord) on versions is when satisfaction is the only semantically interesting property. Is this required in order to use Versions in some data structure?
... it looks like the answer is that we intend to use Versions as a key for a BTreeMap which more or less wants keys to be Ord. OK! So in this case we are defining PartialOrd/Ord less as an order that is deeply semantically meaningful to the application domain and more as something that lets us use them in certain data structures?
/// *cannot* satisfy a request for `Version(1, 9)`. To check for version compatibility, | ||
/// use [the `satisfies` method](#satisfies). | ||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] | ||
pub struct Version(pub u64, pub u64); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not a Rust expert but it seems like it would be nice to use named fields here.
I am scared of a world where versions need 64 bits but I'm sure u64 is fine...
/// ``` | ||
pub fn parse(input: &str) -> Result<Version, VersionParseError> { | ||
lazy_static! { | ||
static ref VERSION_RE: Regex = Regex::new(r#"^v(\d+)\.(\d+)$"#).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As in the TS version, this allows leading zeros which the spec bans. I do see that there's an explicit test for this below but I don't really think that the initial implementations of a spec's parser need to be lax in their parsing? The nice thing about banning leading zeroes is that it means each version has a unique serialization and you can compare them as strings directly if you'd like. Why not be strict?
You can replace each (\d+)
with (0|[1-9]\d*)
which is pretty legible I think...
} | ||
|
||
#[test] | ||
fn it_errors_on_invalid_specifiers() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested additional test cases: v1.2.3
, v1
, 1.2
@@ -0,0 +1,100 @@ | |||
//! Spec url handling | |||
//! | |||
//! `Spec`s are parsed from URL strings and extract the spec's: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming concern from apollographql/core-schema-js#2 (comment) applies here as well, though the fact that this is a struct rather than some kind of trait or whatever does make it a bit more clear to me.
NoSchemas, | ||
|
||
/// No reference to the core spec found | ||
#[error("no reference to the core spec found")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
technically this is more like no reference to an understood version of the core spec is found. maybe the error can actually show the identity and version (or just the URL) that is understood?
/// by the document. One will be selected, the others | ||
/// will generate these errors. | ||
#[error("{} extra using spec ignored", .0)] | ||
ExtraUsing(Pos, Version), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unused (since you use find_map
)
} | ||
|
||
impl SchemaError { | ||
/// Collect `Request`s referenced by this error and join their position, urls, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Search whole project for "request" and rename to "feature"
.iter() | ||
.map(|req| { | ||
format!( | ||
" {pos}: {url}/{ver}", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The as
seems relevant to this error
#[derive(Debug, Clone, PartialEq, Eq)] | ||
pub struct Spec { | ||
pub identity: Cow<'static, str>, | ||
pub name: Cow<'static, str>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I worry it might be a little too easy to get confused between Spec.name
and Feature.name
and accidentally use the wrong one, writing some code that works as long as you don't use as
. How important is it that there be a stand-alone name
field on spec
? It looks like the only time we ever read it is when we produce a Feature
so I think we could just validate that the name is valid at Spec::parse
time (and save name
or its start index into a private field?) but make actually getting the name later be a method or something?
We can re-open this if we need to. |
Define a new crate,
core-schema
, which provides Rust support for the current draft@core
directive.The intention here is to provide (1) a crate we can use in the query planner to process composed schema directives (2) a reference implementation of the spec which can be ported to other languages if needed.
DONE:
core-schema
as per the specTODO: