Skip to content

Commit

Permalink
Expose hint levels
Browse files Browse the repository at this point in the history
Composition hints in federation 2 are generated with a "level" (which
can currently be one of of "DEBUG", "INFO" or "WARN"). This commit
modify harmonizer to include those levels in the output and the type
definitions to decode them, and include them in the `supergraph` output
for future use by `rover`.

Fixes apollographql#102
  • Loading branch information
pcmanus committed Oct 10, 2022
1 parent 6545394 commit ea35322
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 10 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion apollo-federation-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ config = ["camino", "log", "thiserror", "serde_yaml", "url", "serde_with"]
[dependencies]
# config and build dependencies
serde = { version = "1", features = ["derive"] }
regex = "1"
lazy_static = "1.4.0"

# config-only dependencies
camino = { version = "1", features = [ "serde1" ], optional = true }
Expand All @@ -34,4 +36,4 @@ serde_json = { version = "1", optional = true }

[dev-dependencies]
assert_fs = "1"
serde_json = "1"
serde_json = "1"
91 changes: 86 additions & 5 deletions apollo-federation-types/src/build/hint.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,76 @@
use serde::{Deserialize, Serialize};
use regex::Regex;
use lazy_static::lazy_static;

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct BuildHintLevel {
/// Value of the hint level. Higher values correspond to more "important" hints.
pub value: u16,

/// Readable name of the hint level
pub name: String,
}

impl BuildHintLevel {
pub fn warn() -> Self { Self { value: 60, name: String::from("WARN") } }
pub fn info() -> Self { Self { value: 40, name: String::from("INFO") } }
pub fn debug() -> Self { Self { value: 20, name: String::from("DEBUG") } }
}


/// BuildHint contains helpful information that pertains to a build
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct BuildHint {
/// The message of the hint
/// This will usually be formatted as "[<hint code>] <message details>" and the
/// `BuildHint::extract_code_and_message` method can be used to extract the components of this
/// message.
pub message: String,

/// The level of the hint
// We should always get a level out of recent harmonizer, but older one will not have it and we
// default to "INFO".
#[serde(default="BuildHintLevel::info")]
pub level: BuildHintLevel,

/// Other untyped JSON included in the build hint.
#[serde(flatten)]
pub other: crate::UncaughtJson,
}

impl BuildHint {
pub fn new(message: String) -> Self {
pub fn new(message: String, level: BuildHintLevel) -> Self {
Self {
message,
level,
other: crate::UncaughtJson::new(),
}
}

pub fn debug(message: String) -> Self {
Self::new(message, BuildHintLevel::debug())
}

pub fn info(message: String) -> Self {
Self::new(message, BuildHintLevel::info())
}

pub fn warn(message: String) -> Self {
Self::new(message, BuildHintLevel::warn())
}

/// Extracts the underlying code and "raw" message of the hint.
pub fn extract_code_and_message(&self) -> (String, String) {
lazy_static! {
static ref RE: Regex = Regex::new(r"^\[(\w+)\] (.+)").unwrap();
}
let maybe_captures = RE.captures(&self.message);
if let Some(captures) = maybe_captures {
(captures.get(1).unwrap().as_str().to_string(), captures.get(2).unwrap().as_str().to_string())
} else {
(String::from("UNKNOWN"), self.message.clone())
}
}
}

#[cfg(test)]
Expand All @@ -29,16 +82,24 @@ mod tests {
#[test]
fn it_can_serialize() {
let msg = "hint".to_string();
let expected_json = json!({ "message": &msg });
let actual_json = serde_json::to_value(&BuildHint::new(msg)).unwrap();
let expected_json = json!({"level": { "value": 40, "name": "INFO"}, "message": &msg });
let actual_json = serde_json::to_value(&BuildHint::info(msg)).unwrap();
assert_eq!(expected_json, actual_json)
}

#[test]
fn it_can_deserialize() {
let msg = "hint".to_string();
let actual_struct = serde_json::from_str(&json!({"level": { "value": 20, "name": "DEBUG"}, "message": &msg }).to_string()).unwrap();
let expected_struct = BuildHint::debug(msg);
assert_eq!(expected_struct, actual_struct);
}

#[test]
fn it_can_deserialize_without_levels() {
let msg = "hint".to_string();
let actual_struct = serde_json::from_str(&json!({ "message": &msg }).to_string()).unwrap();
let expected_struct = BuildHint::new(msg);
let expected_struct = BuildHint::info(msg);
assert_eq!(expected_struct, actual_struct);
}

Expand All @@ -51,10 +112,30 @@ mod tests {
&json!({ "message": &msg, &unexpected_key: &unexpected_value }).to_string(),
)
.unwrap();
let mut expected_struct = BuildHint::new(msg);
let mut expected_struct = BuildHint::info(msg);
expected_struct
.other
.insert(unexpected_key, Value::String(unexpected_value));
assert_eq!(expected_struct, actual_struct);
}

#[test]
fn it_extracts_code_and_message() {
let hint = BuildHint::info("[MY_CODE] Some message".to_string());
let (actual_code, actual_message) = hint.extract_code_and_message();
let expected_code = "MY_CODE".to_string();
let expected_message = "Some message".to_string();
assert_eq!(expected_code, actual_code);
assert_eq!(expected_message, actual_message);
}

#[test]
fn it_handle_extracting_code_and_message_with_unknown_code() {
let hint = BuildHint::info("Some message without code".to_string());
let (actual_code, actual_message) = hint.extract_code_and_message();
let expected_code = "UNKNOWN".to_string();
let expected_message = "Some message without code".to_string();
assert_eq!(expected_code, actual_code);
assert_eq!(expected_message, actual_message);
}
}
6 changes: 3 additions & 3 deletions apollo-federation-types/src/build/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ mod tests {
let sdl = "my-sdl".to_string();
let hint_one = "hint-one".to_string();
let hint_two = "hint-two".to_string();
let expected_json = json!({"supergraphSdl": &sdl, "hints": [{"message": &hint_one}, {"message": &hint_two}]});
let expected_json = json!({"supergraphSdl": &sdl, "hints": [{"level": { "value": 40, "name": "INFO"}, "message": &hint_one}, {"level": { "value": 60, "name": "WARN"}, "message": &hint_two}]});
let actual_json = serde_json::to_value(&BuildOutput::new_with_hints(
sdl.to_string(),
vec![BuildHint::new(hint_one), BuildHint::new(hint_two)],
vec![BuildHint::info(hint_one), BuildHint::warn(hint_two)],
))
.unwrap();
assert_eq!(expected_json, actual_json)
Expand All @@ -81,7 +81,7 @@ mod tests {
.unwrap();
let expected_struct = BuildOutput::new_with_hints(
sdl,
vec![BuildHint::new(hint_one), BuildHint::new(hint_two)],
vec![BuildHint::info(hint_one), BuildHint::info(hint_two)],
);

assert_eq!(expected_struct, actual_struct)
Expand Down
2 changes: 2 additions & 0 deletions federation-2/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion federation-2/harmonizer/deno/do_compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ try {
let hints = [];
if (composed.hints) {
composed.hints.map((composed_hint) => {
hints.push({ message: composed_hint.toString() });
hints.push({
message: composed_hint.toString(),
level: composed_hint.definition.level,
});
});
}
done(
Expand Down

0 comments on commit ea35322

Please sign in to comment.