Skip to content

Commit b8b0490

Browse files
committed
Add header option to CLI
1 parent 3908873 commit b8b0490

File tree

3 files changed

+101
-1
lines changed

3 files changed

+101
-1
lines changed

graphql_client_cli/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ FLAGS:
2222
2323
OPTIONS:
2424
--authorization <authorization> Set the contents of the Authorizaiton header.
25+
--header <headers>... Specify custom headers. --header 'X-Name: Value'
2526
--output <output> Where to write the JSON for the introspected schema.
2627
2728
ARGS:

graphql_client_cli/src/introspect_schema.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use reqwest;
44
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE};
55
use serde_json;
66
use std::path::PathBuf;
7+
use std::str::FromStr;
78

89
#[derive(GraphQLQuery)]
910
#[graphql(
@@ -18,6 +19,7 @@ pub fn introspect_schema(
1819
location: &str,
1920
output: Option<PathBuf>,
2021
authorization: Option<String>,
22+
headers: Vec<Header>,
2123
) -> Result<(), failure::Error> {
2224
use std::io::Write;
2325

@@ -35,6 +37,14 @@ pub fn introspect_schema(
3537
let client = reqwest::Client::new();
3638

3739
let mut req_builder = client.post(location).headers(construct_headers());
40+
41+
for custom_header in headers {
42+
req_builder = req_builder.header(
43+
custom_header.name.as_str(),
44+
custom_header.value.as_str(),
45+
);
46+
}
47+
3848
if let Some(token) = authorization {
3949
req_builder = req_builder.bearer_auth(token.as_str());
4050
};
@@ -60,3 +70,82 @@ fn construct_headers() -> HeaderMap {
6070
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
6171
headers
6272
}
73+
74+
#[derive(Debug, PartialEq)]
75+
pub struct Header {
76+
name: String,
77+
value: String,
78+
}
79+
80+
impl FromStr for Header {
81+
type Err = failure::Error;
82+
83+
fn from_str(input: &str) -> Result<Self, Self::Err> {
84+
// error: colon required for name/value pair
85+
if ! input.contains(":") {
86+
return Err(format_err!("Invalid header input. A colon is required to separate the name and value. [{}]", input));
87+
}
88+
89+
// split on first colon and trim whitespace from name and value
90+
let name_value: Vec<&str> = input.splitn(2, ':').collect();
91+
let name = name_value[0].trim();
92+
let value = name_value[1].trim();
93+
94+
// error: field name must be
95+
if name.len() == 0 {
96+
return Err(format_err!("Invalid header input. Field name is required before colon. [{}]", input));
97+
}
98+
99+
// error: no whitespace in field name
100+
if name.split_whitespace().count() > 1 {
101+
return Err(format_err!("Invalid header input. Whitespace not allowed in field name. [{}]", input));
102+
}
103+
104+
Ok(Self{name: name.to_string(), value: value.to_string()})
105+
}
106+
}
107+
108+
#[cfg(test)]
109+
mod tests {
110+
use super::*;
111+
112+
#[test]
113+
fn it_errors_invalid_headers() {
114+
// https://tools.ietf.org/html/rfc7230#section-3.2
115+
116+
for input in vec![
117+
"X-Name Value", // error: colon required for name/value pair
118+
": Value", // error: field name must be
119+
"X Name: Value", // error: no whitespace in field name
120+
"X\tName: Value", // error: no whitespace in field name (tab)
121+
] {
122+
let header = Header::from_str(input);
123+
124+
assert!(header.is_err(), "Expected error: [{}]", input);
125+
}
126+
}
127+
128+
#[test]
129+
fn it_parses_valid_headers() {
130+
// https://tools.ietf.org/html/rfc7230#section-3.2
131+
132+
let expected1 = Header{name: "X-Name".to_string(), value: "Value".to_string()};
133+
let expected2 = Header{name: "X-Name".to_string(), value: "Value:".to_string()};
134+
135+
for (input, expected) in vec![
136+
("X-Name: Value", &expected1), // ideal
137+
("X-Name:Value", &expected1), // no optional whitespace
138+
("X-Name: Value ", &expected1), // with optional whitespace
139+
("X-Name:\tValue", &expected1), // with optional whitespace (tab)
140+
("X-Name: Value:", &expected2), // with colon in value
141+
// not allowed per RFC, but we'll forgive
142+
("X-Name : Value", &expected1),
143+
(" X-Name: Value", &expected1),
144+
] {
145+
let header = Header::from_str(input);
146+
147+
assert!(header.is_ok(), "Expected ok: [{}]", input);
148+
assert_eq!(header.unwrap(), *expected, "Expected equality: [{}]", input);
149+
}
150+
}
151+
}

graphql_client_cli/src/main.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ enum Cli {
3939
/// Set the contents of the Authorizaiton header.
4040
#[structopt(long = "authorization")]
4141
authorization: Option<String>,
42+
/// Specify custom headers.
43+
/// --header 'X-Name: Value'
44+
#[structopt(long = "header")]
45+
headers: Vec<introspect_schema::Header>,
4246
},
4347
#[structopt(name = "generate")]
4448
Generate {
@@ -85,7 +89,13 @@ fn main() -> Result<(), failure::Error> {
8589
schema_location,
8690
output,
8791
authorization,
88-
} => introspect_schema::introspect_schema(&schema_location, output, authorization),
92+
headers,
93+
} => introspect_schema::introspect_schema(
94+
&schema_location,
95+
output,
96+
authorization,
97+
headers,
98+
),
8999
Cli::Generate {
90100
additional_derives,
91101
deprecation_strategy,

0 commit comments

Comments
 (0)