Skip to content
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

feat: introspection SDL encoder #283

Merged
merged 28 commits into from
Mar 23, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
31c0887
feat: create an SDL encoder
lrlna Feb 8, 2021
03abd1c
sdl-encoder: create a Field struct
lrlna Feb 22, 2021
62150c4
feat(sdl-encoder): Fields can have FieldType
lrlna Feb 26, 2021
dae3c8b
feat(sdl-encoder): SDL can have an input object
lrlna Feb 26, 2021
a8d2ea9
feat(introspection): parse introspection schema
lrlna Mar 4, 2021
1942565
feat(introspection): parse introspection schema
lrlna Mar 4, 2021
c23437d
feat(sdl-encoder): add enum definition
lrlna Mar 4, 2021
f4afae0
feat(sdl-encoder): add scalar encoding
lrlna Mar 11, 2021
4cba068
chore(sdl-encoder): refactor for easier reading
lrlna Mar 11, 2021
79d3d0a
feat(sdl-encoder): add encoding for Directives.
lrlna Mar 15, 2021
1ecb149
chore: ofType can be recursive, as a treat
lrlna Mar 15, 2021
32a0fd5
feat(sdl-encoder): add encoding for Unions
lrlna Mar 16, 2021
620a44f
feat(sdl-encoder): encoder Interface types
lrlna Mar 16, 2021
d819a2c
feat(sdl-encoder): allow for deprecated fields
lrlna Mar 16, 2021
58df7f5
feat(sdl-encoder): add NonNull FieldType
lrlna Mar 17, 2021
9f5fbe5
feat(sdl-encoder): fields can have arguments
lrlna Mar 17, 2021
9cefc86
feat(introspection): exclude graphqltypes from SDL
lrlna Mar 18, 2021
4acd7f9
feat(sdl-encoder): directives can have arguments
lrlna Mar 18, 2021
5f04444
chore(sdl-encoder): seperate out possible values
lrlna Mar 18, 2021
7842198
feat(sdl-encoder): default values
lrlna Mar 18, 2021
267231b
fix(sdl-encoder): multiline descriptions on "\n"
lrlna Mar 18, 2021
81d0cc0
chore(rust-docs): document sdl_encoder
lrlna Mar 19, 2021
7eb7337
chore: align introspection with main branch
lrlna Mar 19, 2021
8217d82
chore: align naming conventions to the spec
lrlna Mar 22, 2021
85e7c85
feat(introspection): add header flag parser (#351)
JakeDawkins Mar 23, 2021
3003986
chore: introspection encoding integration tests
lrlna Mar 23, 2021
4bfd54b
feat(sdl-encoder): encode schema definition
lrlna Mar 23, 2021
b6e68d4
Merge branch 'main' into build_schema
lrlna Mar 23, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 126 additions & 60 deletions Cargo.lock

Large diffs are not rendered by default.

43 changes: 22 additions & 21 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
[package]
name = "rover"
version = "0.0.3"
authors = ["Apollo Developers <opensource@apollographql.com>"]
build = "build.rs"
categories = ["command-line-interface"]
description = """
Rover is a tool for working with the Apollo GraphQL Registry.
"""
documentation = "https://go.apollo.dev/r/docs"
repository = "https://github.com/apollographql/rover/"
readme = "README.md"
edition = "2018"
keywords = ["graphql", "cli", "apollo", "graph", "registry"]
categories = ["command-line-interface"]
license = "MIT"
build = "build.rs"
edition = "2018"
name = "rover"
readme = "README.md"
repository = "https://github.com/apollographql/rover/"
version = "0.0.3"

[[bin]]
name = "rover"
Expand All @@ -24,45 +24,46 @@ members = [".", "crates/*", "installers/binstall"]
[dependencies]

# workspace deps
binstall = { path = "./installers/binstall" }
houston = { path = "./crates/houston" }
robot-panic = { path = "./crates/robot-panic" }
rover-client = { path = "./crates/rover-client" }
sputnik = { path = "./crates/sputnik" }
timber = { path = "./crates/timber" }
binstall = {path = "./installers/binstall"}
houston = {path = "./crates/houston"}
robot-panic = {path = "./crates/robot-panic"}
rover-client = {path = "./crates/rover-client"}
sdl-encoder = {path = "./crates/sdl-encoder"}
sputnik = {path = "./crates/sputnik"}
timber = {path = "./crates/timber"}

# crates.io deps
ansi_term = "0.12.1"
anyhow = "1.0.38"
atty = "0.2.14"
ansi_term = "0.12.1"
billboard = {git = "https://github.com/EverlastingBugstopper/billboard.git", branch = "main"}
camino = "1.0.2"
billboard = { git = "https://github.com/EverlastingBugstopper/billboard.git", branch = "main" }
chrono = "0.4"
console = "0.14.0"
git2 = "0.13.17"
git-url-parse = "0.3.1"
git2 = "0.13.17"
heck = "0.3.2"
humantime = "2.1.0"
opener = "0.4.1"
os_info = "3.0"
prettytable-rs = "0.8.0"
regex = "1"
semver = "0.11"
serde = "1.0"
strsim = "0.10"
serde_json = "1.0"
strsim = "0.10"
structopt = "0.3.21"
toml = "0.5"
tracing = "0.1.22"
regex = "1"
url = "2.2.0"
semver = "0.11"
toml = "0.5"

[dev-dependencies]
assert_cmd = "1.0.1"
assert_fs = "1.0.0"
predicates = "1.0.5"
reqwest = "0.11.1"
rustversion = "1.0.4"
serial_test = "0.5.0"
predicates = "1.0.5"

[build-dependencies]
anyhow = "1"
Expand Down
20 changes: 11 additions & 9 deletions crates/rover-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
[package]
name = "rover-client"
description = "an http client for making graphql requests for the rover CLI"
version = "0.0.0"
authors = ["Apollo Developers <opensource@apollographql.com>"]
description = "an http client for making graphql requests for the rover CLI"
edition = "2018"
name = "rover-client"
version = "0.0.0"

[dependencies]

# workspace deps
houston = { path = "../houston" }
houston = {path = "../houston"}

# crates.io deps
anyhow = "1"
camino = "1"
chrono = "0.4"
graphql-parser = "0.3.0"
graphql_client = "0.9"
http = "0.2"
reqwest = { version = "0.11", features = ["json", "blocking", "native-tls-vendored"] }
reqwest = {version = "0.11", features = ["json", "blocking", "native-tls-vendored"]}
sdl-encoder = {path = "../sdl-encoder"}
serde = "1"
serde_json = "1"
thiserror = "1"
tracing = "0.1"
chrono = "0.4"
regex = "1"
regex = "1.4.5"

[build-dependencies]
camino = "1"
online = "0.2.2"
reqwest = { version = "0.11", features = ["blocking", "native-tls-vendored"] }
uuid = { version = "0.8", features = ["v4"] }
reqwest = {version = "0.11", features = ["blocking", "native-tls-vendored"]}
uuid = {version = "0.8", features = ["v4"]}
7 changes: 7 additions & 0 deletions crates/rover-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ pub enum RoverClientError {
msg: String,
},

/// Failed to parse Introspection Response coming from server.
#[error("{msg}")]
IntrospectionError {
/// Introspection Error coming from schema encoder.
msg: String,
},

/// Tried to build a [HeaderMap] with an invalid header name.
#[error("invalid header name")]
InvalidHeaderName(#[from] reqwest::header::InvalidHeaderName),
Expand Down
2 changes: 2 additions & 0 deletions crates/rover-client/src/introspection/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod schema;
pub use schema::Schema;
241 changes: 241 additions & 0 deletions crates/rover-client/src/introspection/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
//! Schema encoding module used to work with Introspection result.
//!
//! More information on Schema Definition language(SDL) can be found in [this
//! documentation](https://www.apollographql.com/docs/apollo-server/schema/schema/).
//!
use crate::query::graph::introspect;
use sdl_encoder::{
Directive, EnumDef, EnumValue, Field, InputField, InputObjectDef, InputValue, InterfaceDef,
ObjectDef, ScalarDef, Schema as SDL, Type_, UnionDef,
};
use serde::Deserialize;
use std::convert::TryFrom;

pub type FullTypeField = introspect::introspection_query::FullTypeFields;
pub type FullTypeInputField = introspect::introspection_query::FullTypeInputFields;
pub type FullTypeFieldArg = introspect::introspection_query::FullTypeFieldsArgs;
pub type IntrospectionResult = introspect::introspection_query::ResponseData;
pub type SchemaType = introspect::introspection_query::IntrospectionQuerySchemaTypes;
pub type SchemaDirective = introspect::introspection_query::IntrospectionQuerySchemaDirectives;
pub type __TypeKind = introspect::introspection_query::__TypeKind;

// Represents GraphQL types we will not be encoding to SDL.
const GRAPHQL_NAMED_TYPES: [&str; 12] = [
"__Schema",
"__Type",
"__TypeKind",
"__Field",
"__InputValue",
"__EnumValue",
"__DirectiveLocation",
"__Directive",
"Boolean",
"String",
"Int",
"ID",
];

// Represents GraphQL directives we will not be encoding to SDL.
const SPECIFIED_DIRECTIVES: [&str; 3] = ["skip", "include", "deprecated"];

/// A representation of a GraphQL Schema.
///
/// Contains Schema Types and Directives.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Schema {
types: Vec<SchemaType>,
directives: Vec<SchemaDirective>,
}

impl Schema {
/// Encode Schema into an SDL.
pub fn encode(self) -> String {
let mut sdl = SDL::new();

// Exclude GraphQL directives like 'skip' and 'include' before encoding directives.
self.directives
.into_iter()
.filter(|directive| !SPECIFIED_DIRECTIVES.contains(&directive.name.as_str()))
.for_each(|directive| Self::encode_directives(directive, &mut sdl));

// Exclude GraphQL named types like __Schema before encoding full type.
self.types
.into_iter()
.filter(|type_| match type_.full_type.name.as_deref() {
Some(name) => !GRAPHQL_NAMED_TYPES.contains(&name),
None => false,
})
.for_each(|type_| Self::encode_full_type(type_, &mut sdl));

sdl.finish()
}

fn encode_directives(directive: SchemaDirective, sdl: &mut SDL) {
let mut directive_ = Directive::new(directive.name);
directive_.description(directive.description);
for location in directive.locations {
// Location is of a __DirectiveLocation enum that doesn't implement
// Display (meaning we can't just do .to_string). This next line
// just forces it into a String with format! debug.
directive_.location(format!("{:?}", location));
}

sdl.directive(directive_)
}

fn encode_full_type(type_: SchemaType, sdl: &mut SDL) {
let ty = type_.full_type;

match ty.kind {
__TypeKind::OBJECT => {
let mut object_def = ObjectDef::new(ty.name.unwrap_or_else(String::new));
object_def.description(ty.description);
if let Some(interfaces) = ty.interfaces {
for interface in interfaces {
object_def.interface(interface.type_ref.name.unwrap_or_else(String::new));
}
}
if let Some(field) = ty.fields {
for f in field {
let field_def = Self::encode_field(f);
object_def.field(field_def);
}
sdl.object(object_def);
}
}
__TypeKind::INPUT_OBJECT => {
let mut input_def = InputObjectDef::new(ty.name.unwrap_or_else(String::new));
input_def.description(ty.description);
if let Some(field) = ty.input_fields {
for f in field {
let input_field_def = Self::encode_input_field(f);
input_def.field(input_field_def);
}
sdl.input(input_def);
}
}
__TypeKind::INTERFACE => {
let mut interface_def = InterfaceDef::new(ty.name.unwrap_or_else(String::new));
interface_def.description(ty.description);
if let Some(interfaces) = ty.interfaces {
for interface in interfaces {
interface_def
.interface(interface.type_ref.name.unwrap_or_else(String::new));
}
}
if let Some(field) = ty.fields {
for f in field {
let field_def = Self::encode_field(f);
interface_def.field(field_def);
}
sdl.interface(interface_def);
}
}
__TypeKind::SCALAR => {
let mut scalar_def = ScalarDef::new(ty.name.unwrap_or_else(String::new));
scalar_def.description(ty.description);
sdl.scalar(scalar_def);
}
__TypeKind::UNION => {
let mut union_def = UnionDef::new(ty.name.unwrap_or_else(String::new));
union_def.description(ty.description);
if let Some(possible_types) = ty.possible_types {
for possible_type in possible_types {
union_def.member(possible_type.type_ref.name.unwrap_or_else(String::new));
}
}
sdl.union(union_def);
}
__TypeKind::ENUM => {
let mut enum_def = EnumDef::new(ty.name.unwrap_or_else(String::new));
if let Some(enums) = ty.enum_values {
for enum_ in enums {
let mut enum_value = EnumValue::new(enum_.name);
enum_value.description(enum_.description);

if enum_.is_deprecated {
enum_value.deprecated(enum_.deprecation_reason);
}

enum_def.value(enum_value);
}
}
sdl.enum_(enum_def);
}
_ => (),
}
}

fn encode_field(field: FullTypeField) -> Field {
let ty = Self::encode_type(field.type_.type_ref);
let mut field_def = Field::new(field.name, ty);

for value in field.args {
let field_value = Self::encode_arg(value);
field_def.arg(field_value);
}

if field.is_deprecated {
field_def.deprecated(field.deprecation_reason);
}
field_def.description(field.description);
field_def
}

fn encode_input_field(field: FullTypeInputField) -> InputField {
let ty = Self::encode_type(field.input_value.type_.type_ref);
let mut field_def = InputField::new(field.input_value.name, ty);

field_def.default(field.input_value.default_value);
field_def.description(field.input_value.description);
field_def
}

fn encode_arg(value: FullTypeFieldArg) -> InputValue {
let ty = Self::encode_type(value.input_value.type_.type_ref);
let mut value_def = InputValue::new(value.input_value.name, ty);

value_def.default(value.input_value.default_value);
value_def.description(value.input_value.description);
value_def
}

fn encode_type(ty: impl introspect::OfType) -> Type_ {
use introspect::introspection_query::__TypeKind::*;
match ty.kind() {
SCALAR | OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT => Type_::NamedType {
name: ty.name().unwrap().to_string(),
},
NON_NULL => {
let ty = Self::encode_type(ty.of_type().unwrap());
Type_::NonNull { ty: Box::new(ty) }
}
LIST => {
let ty = Self::encode_type(ty.of_type().unwrap());
Type_::List { ty: Box::new(ty) }
}
Other(ty) => panic!("Unknown type: {}", ty),
}
}
}

impl TryFrom<IntrospectionResult> for Schema {
type Error = &'static str;

fn try_from(src: IntrospectionResult) -> Result<Self, Self::Error> {
match src.schema {
Some(s) => Ok(Self {
types: s.types,
directives: s.directives,
}),
None => Err("Schema not found in Introspection Result."),
}
}
}

#[cfg(test)]
mod tests {
#[test]
fn it_build_simple_schema() {}
Copy link
Member Author

@lrlna lrlna Mar 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is empty so far, unfortunately. I am thinking of a good way to be able to run 10 or so overall tests for overall schema encoding. The problem is that I need the actual Introspection type that I think I can literally only get by using and making queries to the graphql_client, which is really quite bulky. If I can't come up with anything smarter, that's what I'll end up doing, I think.

Help: If you can think of another clever way, please let me know!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't you manually execute introspection queries and store the results in files?
(With the old apollo CLI, you could get those by running something like apollo schema:download --endpoint=http://localhost:8080/graphql schema.json.) Or am I misunderstanding the issue?

Copy link
Member Author

@lrlna lrlna Mar 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you're suggesting is not possible as Schema is currently created for an IntrospectionResult. .json file will not work. A .json file would have worked if IntrospectionResult had a Serialize implementation, but this implementation doesn't exist and the following code does not work:

        let file = File::open("src/introspection/introspection.json")
            .expect("File should be able to be opened.");
        let result: IntrospectionResult =
            serde_json::from_reader(file).expect("File is not a proper JSON.");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand. Since executing an introspection query against a server would return the same JSON contents, and we are able to load those into an IntrospectionResult, why is this different?

}
3 changes: 3 additions & 0 deletions crates/rover-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ mod error;
/// Module related to constructing request headers.
pub mod headers;

/// Module related to building an SDL from an introspection response.
pub mod introspection;

/// Module for client related errors.
pub use error::RoverClientError;

Expand Down
Loading