Skip to content

Commit 3d620c4

Browse files
committed
Added GraphQL support #50
* added pub mod GraphQL * added juniper fork as a dependency * added GQL support to all public structures used in Report
1 parent ea178d5 commit 3d620c4

File tree

10 files changed

+215
-17
lines changed

10 files changed

+215
-17
lines changed

stackmuncher_lib/Cargo.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@ chrono = "0.4"
1616
tracing = { version = "0.1", features = ["log"] }
1717
encoding_rs_io = "0.1"
1818
encoding_rs = "0.8"
19-
uuid = { version = "0.8", features = ["v4"] }
19+
uuid = { version = "1.1.0", features = ["v4"] }
2020
tokio = { version = "1", features = ["full"] }
2121
sha-1 = "0.10"
2222
sha2 = "0.10"
2323
bs58 = "0.4"
2424
path-absolutize = "3.0"
2525
flate2 = "1.0"
2626
rust-embed = { version = "6", features = ["compression"] }
27+
# Temporarily running off a fork until https://github.com/graphql-rust/juniper/issues/1071 is resolved
28+
# juniper = { git = "https://github.com/graphql-rust/juniper.git" }
29+
juniper = { git = "https://github.com/rimutaka/juniper.git", branch = "impl-hashset-as-vec" }
2730

2831
[dev-dependencies]
2932
tracing-subscriber = "0.3"

stackmuncher_lib/src/contributor.rs

+16-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
use super::git::GitLogEntry;
2+
use crate::graphql::RustScalarValue;
3+
use juniper::GraphQLObject;
24
use serde::{Deserialize, Serialize};
35
use std::collections::{HashMap, HashSet};
46

7+
/// This type would normally be a `(String, String)` tuple, but GraphQL requires a custom implementation for that.
8+
/// On the other hand there is a default impl for `[T]`.
9+
///
10+
/// The serialized output looks the same for both: `["rimutaka","[email protected]"]`.
11+
pub type NameEmailPairType = [String; 2];
12+
513
/// A GIT author or committer. E.g. `Author: rimutaka <[email protected]>` from `git log`.
614
/// It contains extended info like what was committed, when, contact details.
7-
#[derive(Serialize, Deserialize, Debug, Clone)]
15+
#[derive(Serialize, Deserialize, Debug, Clone, GraphQLObject)]
16+
#[graphql(scalar = RustScalarValue)]
817
pub struct Contributor {
918
/// Email is the preferred ID, but it can be just the name if the email is missing, e.g. `[email protected]` for `Author: rimutaka <[email protected]>`
1019
///
@@ -13,7 +22,7 @@ pub struct Contributor {
1322
pub git_id: String,
1423
/// A list of possible identities as name/email pairs for extracting contact details and de-duplication.
1524
/// E.g. `Author: rimutaka <[email protected]> would be `rimutaka`/`[email protected]`.
16-
pub name_email_pairs: HashSet<(String, String)>,
25+
pub name_email_pairs: HashSet<NameEmailPairType>,
1726
/// The full SHA1 of the very last commit by this contributor. This bit should be retained for matching repositories on STM server.
1827
pub last_commit_sha1: String,
1928
/// The timestamp as EPOCH of the very last commit by this contributor.
@@ -32,7 +41,8 @@ pub struct Contributor {
3241
pub commits: Vec<u64>,
3342
}
3443

35-
#[derive(Serialize, Deserialize, Debug, Clone, Eq)]
44+
#[derive(Serialize, Deserialize, Debug, Clone, Eq, GraphQLObject)]
45+
#[graphql(scalar = RustScalarValue)]
3646
pub struct ContributorFile {
3747
/// The file name extracted from GIT, including the relative path, e.g. `myproject/src/main.rs`
3848
pub name: String,
@@ -86,7 +96,7 @@ impl Contributor {
8696
// this is a known contributor - merge with the existing one
8797
contributor
8898
.name_email_pairs
89-
.insert((commit.author_name_email.0, commit.author_name_email.1));
99+
.insert([commit.author_name_email.0, commit.author_name_email.1]);
90100

91101
// only the latest version of the file is of interest
92102
for file in commit.files {
@@ -102,8 +112,8 @@ impl Contributor {
102112
// it's a new contributor - add as-is
103113

104114
// add the identities as name/email pairs
105-
let mut name_email_pairs: HashSet<(String, String)> = HashSet::new();
106-
name_email_pairs.insert((commit.author_name_email.0, commit.author_name_email.1));
115+
let mut name_email_pairs: HashSet<NameEmailPairType> = HashSet::new();
116+
name_email_pairs.insert([commit.author_name_email.0, commit.author_name_email.1]);
107117

108118
// collect the list of touched files with the commit SHA1
109119
let mut touched_files: HashMap<String, (String, String, i64)> = HashMap::new();

stackmuncher_lib/src/graphql.rs

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
//! This module is needed to support the cloud-side of the project.
2+
//! It enables GraphQL support for core structures used on the client and server sides.
3+
4+
use juniper::{
5+
graphql_scalar,
6+
parser::{ParseError, ScalarToken, Token},
7+
serde::{de, Deserialize, Deserializer, Serialize},
8+
InputValue, ParseScalarResult, ScalarValue, Value,
9+
};
10+
use std::{convert::TryInto as _, fmt};
11+
12+
/// An extension to the standard GraphQL set of types to include Rust scalar values.
13+
/// Only the types used in this project are added to the list.
14+
/// ### About GraphQL scalars
15+
/// * https://graphql.org/learn/schema/#scalar-types
16+
/// * https://www.graphql-tools.com/docs/scalars#custom-scalars
17+
/// ### About extending the GraphQL scalars in Juniper
18+
/// * https://graphql-rust.github.io/juniper/master/types/scalars.html#custom-scalars
19+
/// * https://github.com/graphql-rust/juniper/issues/862
20+
#[derive(Clone, Debug, PartialEq, ScalarValue, Serialize)]
21+
#[serde(untagged)]
22+
pub enum RustScalarValue {
23+
/// A GraphQL scalar for i32
24+
#[value(as_float, as_int)]
25+
Int(i32),
26+
/// A custom scalar for u64. The value is serialized into JSON number and should not be more than 53 bits to fit into JS Number type:
27+
/// * Number.MAX_SAFE_INTEGER = 2^53 - 1 = 9_007_199_254_740_991
28+
/// * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
29+
/// JSON spec does not constrain integer values unless specified in the schema. 53 bits is sufficient for our purposes.
30+
U64(u64),
31+
/// A custom scalar for i64 used in EPOCH timestamps. Theoretically, the value should never be negative because all STM dates are post 1970.
32+
/// The value is serialized into JSON number and should not be more than 53 bits to fit into JS Number type:
33+
/// * Number.MIN_SAFE_INTEGER = -(2^53 - 1) = -9,007,199,254,740,991
34+
I64(i64),
35+
/// A GraphQL scalar for f64
36+
#[value(as_float)]
37+
Float(f64),
38+
/// A GraphQL scalar for String
39+
#[value(as_str, as_string, into_string)]
40+
String(String),
41+
/// A GraphQL scalar for bool
42+
#[value(as_bool)]
43+
Boolean(bool),
44+
}
45+
46+
impl<'de> Deserialize<'de> for RustScalarValue {
47+
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
48+
struct Visitor;
49+
50+
impl<'de> de::Visitor<'de> for Visitor {
51+
type Value = RustScalarValue;
52+
53+
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
54+
f.write_str("a valid input value")
55+
}
56+
57+
fn visit_bool<E: de::Error>(self, b: bool) -> Result<Self::Value, E> {
58+
Ok(RustScalarValue::Boolean(b))
59+
}
60+
61+
fn visit_i32<E: de::Error>(self, n: i32) -> Result<Self::Value, E> {
62+
Ok(RustScalarValue::Int(n))
63+
}
64+
65+
fn visit_u64<E: de::Error>(self, b: u64) -> Result<Self::Value, E> {
66+
if b <= u64::from(i32::MAX as u32) {
67+
self.visit_i32(b.try_into().unwrap())
68+
} else {
69+
Ok(RustScalarValue::U64(b))
70+
}
71+
}
72+
73+
fn visit_u32<E: de::Error>(self, n: u32) -> Result<Self::Value, E> {
74+
if n <= i32::MAX as u32 {
75+
self.visit_i32(n.try_into().unwrap())
76+
} else {
77+
self.visit_u64(n.into())
78+
}
79+
}
80+
81+
fn visit_i64<E: de::Error>(self, n: i64) -> Result<Self::Value, E> {
82+
if n <= i64::MAX as i64 {
83+
self.visit_i64(n.try_into().unwrap())
84+
} else {
85+
// Browser's `JSON.stringify()` serializes all numbers
86+
// having no fractional part as integers (no decimal point),
87+
// so we must parse large integers as floating point,
88+
// otherwise we would error on transferring large floating
89+
// point numbers.
90+
// TODO: Use `FloatToInt` conversion once stabilized:
91+
// https://github.com/rust-lang/rust/issues/67057
92+
Ok(RustScalarValue::Float(n as f64))
93+
}
94+
}
95+
96+
fn visit_f64<E: de::Error>(self, f: f64) -> Result<Self::Value, E> {
97+
Ok(RustScalarValue::Float(f))
98+
}
99+
100+
fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
101+
self.visit_string(s.into())
102+
}
103+
104+
fn visit_string<E: de::Error>(self, s: String) -> Result<Self::Value, E> {
105+
Ok(RustScalarValue::String(s))
106+
}
107+
}
108+
109+
de.deserialize_any(Visitor)
110+
}
111+
}
112+
113+
#[graphql_scalar(with = u64_scalar, scalar = RustScalarValue)]
114+
type U64 = u64;
115+
116+
mod u64_scalar {
117+
use super::*;
118+
119+
pub(super) fn to_output(v: &U64) -> Value<RustScalarValue> {
120+
Value::scalar(*v)
121+
}
122+
123+
pub(super) fn from_input(v: &InputValue<RustScalarValue>) -> Result<U64, String> {
124+
v.as_scalar_value::<u64>()
125+
.copied()
126+
.ok_or_else(|| format!("Expected `RustScalarValue::U64`, found: {}", v))
127+
}
128+
129+
pub(super) fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult<'_, RustScalarValue> {
130+
if let ScalarToken::Int(v) = value {
131+
v.parse()
132+
.map_err(|_| ParseError::UnexpectedToken(Token::Scalar(value)))
133+
.map(|s: u64| s.into())
134+
} else {
135+
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
136+
}
137+
}
138+
}
139+
140+
#[graphql_scalar(with = i64_scalar, scalar = RustScalarValue)]
141+
type I64 = i64;
142+
143+
mod i64_scalar {
144+
use super::*;
145+
146+
pub(super) fn to_output(v: &I64) -> Value<RustScalarValue> {
147+
Value::scalar(*v)
148+
}
149+
150+
pub(super) fn from_input(v: &InputValue<RustScalarValue>) -> Result<I64, String> {
151+
v.as_scalar_value::<i64>()
152+
.copied()
153+
.ok_or_else(|| format!("Expected `RustScalarValue::I64`, found: {}", v))
154+
}
155+
156+
pub(super) fn parse_token(value: ScalarToken<'_>) -> ParseScalarResult<'_, RustScalarValue> {
157+
if let ScalarToken::Int(v) = value {
158+
v.parse()
159+
.map_err(|_| ParseError::UnexpectedToken(Token::Scalar(value)))
160+
.map(|s: i64| s.into())
161+
} else {
162+
Err(ParseError::UnexpectedToken(Token::Scalar(value)))
163+
}
164+
}
165+
}

stackmuncher_lib/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub mod config;
1111
pub mod contributor;
1212
pub mod file_type;
1313
pub mod git;
14+
pub mod graphql;
1415
mod ignore_paths;
1516
pub mod muncher;
1617
pub mod processors;

stackmuncher_lib/src/report/commit_time_histo.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use super::Report;
2+
use crate::graphql::RustScalarValue;
23
use chrono::{self, Duration, TimeZone, Timelike, Utc};
4+
use juniper::GraphQLObject;
35
use serde::{Deserialize, Serialize};
46
use tracing::warn;
57

@@ -8,7 +10,8 @@ pub const RECENT_PERIOD_LENGTH_IN_DAYS: i64 = 365;
810

911
/// Number of commits or percentage of commits per UTC hour.
1012
/// The structure is skipped in JSON if all values are zero and is initialized to all zeros to have fewer Option<T> unwraps.
11-
#[derive(Serialize, Deserialize, Clone, Debug)]
13+
#[derive(Serialize, Deserialize, Clone, Debug, GraphQLObject)]
14+
#[graphql(scalar = RustScalarValue)]
1215
pub struct CommitTimeHistoHours {
1316
#[serde(skip_serializing_if = "CommitTimeHistoHours::is_zero", default = "u64::default")]
1417
pub h00: u64,
@@ -61,7 +64,8 @@ pub struct CommitTimeHistoHours {
6164
}
6265

6366
/// Contains members and methods related to commit time histogram
64-
#[derive(Serialize, Deserialize, Clone, Debug)]
67+
#[derive(Serialize, Deserialize, Clone, Debug, GraphQLObject)]
68+
#[graphql(scalar = RustScalarValue)]
6569
pub struct CommitTimeHisto {
6670
/// The sum of all commits included in `histogram_recent`. This value is used as the 100% of all recent commits.
6771
/// The value is populated once after all commits have been added.

stackmuncher_lib/src/report/kwc.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
use crate::graphql::RustScalarValue;
2+
use juniper::GraphQLObject;
13
use serde::{Deserialize, Serialize};
24
use std::collections::HashSet;
35
use tracing::{error, warn};
46

5-
#[derive(Debug, Serialize, Deserialize, Eq, Clone)]
7+
#[derive(Debug, Serialize, Deserialize, Eq, Clone, GraphQLObject)]
8+
#[graphql(scalar = RustScalarValue)]
69
pub struct KeywordCounter {
710
/// keyword
811
pub k: String,

stackmuncher_lib/src/report/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
pub mod commit_time_histo;
12
pub mod kwc;
23
pub mod overview;
34
pub mod report;
45
pub mod tech;
5-
pub mod commit_time_histo;
66

77
pub use overview::{ProjectReportOverview, TechOverview};
88
pub use report::Report;

stackmuncher_lib/src/report/overview.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
use super::tech::Tech;
2+
use crate::graphql::RustScalarValue;
23
use chrono::{DateTime, Datelike, Timelike, Utc};
4+
use juniper::GraphQLObject;
35
use serde::{Deserialize, Serialize};
46
use std::collections::{HashMap, HashSet};
57
use tracing::warn;
68

79
/// A very concise overview of a single Tech record
810
/// to show the share of the technology in the project
9-
#[derive(Serialize, Deserialize, Clone, Debug, Eq)]
11+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, GraphQLObject)]
12+
#[graphql(scalar = RustScalarValue)]
1013
pub struct TechOverview {
1114
/// The same as Tech.language
1215
pub language: String,
@@ -36,7 +39,8 @@ impl PartialEq for TechOverview {
3639

3740
/// An overview of an individual project report included in the combined report
3841
/// to avoid loading the full project report every time the combined report is looked at.
39-
#[derive(Serialize, Deserialize, Clone, Debug, Eq)]
42+
#[derive(Serialize, Deserialize, Clone, Debug, Eq, GraphQLObject)]
43+
#[graphql(scalar = RustScalarValue)]
4044
pub struct ProjectReportOverview {
4145
/// A human-readable project name. It should not be used as an ID.
4246
#[serde(default = "String::new")]

stackmuncher_lib/src/report/report.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ use super::commit_time_histo::CommitTimeHisto;
22
use super::kwc::{KeywordCounter, KeywordCounterSet};
33
use super::tech::{Tech, TechHistory};
44
use super::ProjectReportOverview;
5+
use crate::graphql::RustScalarValue;
56
use crate::utils::sha256::hash_str_to_sha256_as_base58;
67
use crate::{contributor::Contributor, git::GitLogEntry, utils};
78
use chrono::{DateTime, Utc};
89
use flate2::write::GzEncoder;
910
use flate2::Compression;
11+
use juniper::GraphQLObject;
1012
use path_absolutize::{self, Absolutize};
1113
use serde::{Deserialize, Serialize};
1214
use serde_json;
@@ -18,7 +20,8 @@ use tracing::{debug, error, info, warn};
1820

1921
/// Contains the number of elements per list to help with DB queries.
2022
/// The numbers are calculated once before saving the Report in the DB.
21-
#[derive(Serialize, Deserialize, Clone, Debug)]
23+
#[derive(Serialize, Deserialize, Clone, Debug, GraphQLObject)]
24+
#[graphql(scalar = RustScalarValue)]
2225
pub struct ListCounts {
2326
tech: u64,
2427
contributor_git_ids: u64,
@@ -33,7 +36,8 @@ pub struct ListCounts {
3336
keywords: u64,
3437
}
3538

36-
#[derive(Serialize, Deserialize, Clone, Debug)]
39+
#[derive(Serialize, Deserialize, Clone, Debug, GraphQLObject)]
40+
#[graphql(scalar = RustScalarValue)]
3741
#[serde(rename = "tech")]
3842
pub struct Report {
3943
/// The exact timestamp of the report generation in ISO3389 format.

stackmuncher_lib/src/report/tech.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
use super::kwc::{KeywordCounter, KeywordCounterSet};
2+
use crate::graphql::RustScalarValue;
3+
use juniper::GraphQLObject;
24
use regex::Regex;
35
use serde::{Deserialize, Serialize};
46
use std::collections::HashSet;
57
use tracing::{debug, trace, warn};
68

79
/// Contains time-range data for its parent Tech.
8-
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
10+
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, GraphQLObject)]
11+
#[graphql(scalar = RustScalarValue)]
912
#[serde(rename = "tech")]
1013
pub struct TechHistory {
1114
/// Number of months between the first and the last commit.
@@ -24,7 +27,8 @@ pub struct TechHistory {
2427
/// Any additions to this struct should be considered for clean up before submission to stackmuncher.com
2528
/// to avoid sending out any info that doesn't need to be sent.
2629
/// See https://github.com/stackmuncher/stm_app/issues/12
27-
#[derive(Serialize, Deserialize, Debug, Eq, Clone)]
30+
#[derive(Serialize, Deserialize, Debug, Eq, Clone, GraphQLObject)]
31+
#[graphql(scalar = RustScalarValue)]
2832
#[serde(rename = "tech")]
2933
pub struct Tech {
3034
/// The name of the file for individual file reports. Not present in combined tech reports.

0 commit comments

Comments
 (0)