Skip to content

Commit b851d85

Browse files
authored
Merge pull request #237 from travismiller/add-header-option-to-cli
Add header option to CLI
2 parents 3908873 + a0af426 commit b851d85

File tree

3 files changed

+111
-1
lines changed

3 files changed

+111
-1
lines changed

graphql_client_cli/README.md

+1
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

+104
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,11 @@ 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(custom_header.name.as_str(), custom_header.value.as_str());
43+
}
44+
3845
if let Some(token) = authorization {
3946
req_builder = req_builder.bearer_auth(token.as_str());
4047
};
@@ -60,3 +67,100 @@ fn construct_headers() -> HeaderMap {
6067
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
6168
headers
6269
}
70+
71+
#[derive(Debug, PartialEq)]
72+
pub struct Header {
73+
name: String,
74+
value: String,
75+
}
76+
77+
impl FromStr for Header {
78+
type Err = failure::Error;
79+
80+
fn from_str(input: &str) -> Result<Self, Self::Err> {
81+
// error: colon required for name/value pair
82+
if !input.contains(":") {
83+
return Err(format_err!(
84+
"Invalid header input. A colon is required to separate the name and value. [{}]",
85+
input
86+
));
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!(
97+
"Invalid header input. Field name is required before colon. [{}]",
98+
input
99+
));
100+
}
101+
102+
// error: no whitespace in field name
103+
if name.split_whitespace().count() > 1 {
104+
return Err(format_err!(
105+
"Invalid header input. Whitespace not allowed in field name. [{}]",
106+
input
107+
));
108+
}
109+
110+
Ok(Self {
111+
name: name.to_string(),
112+
value: value.to_string(),
113+
})
114+
}
115+
}
116+
117+
#[cfg(test)]
118+
mod tests {
119+
use super::*;
120+
121+
#[test]
122+
fn it_errors_invalid_headers() {
123+
// https://tools.ietf.org/html/rfc7230#section-3.2
124+
125+
for input in vec![
126+
"X-Name Value", // error: colon required for name/value pair
127+
": Value", // error: field name must be
128+
"X Name: Value", // error: no whitespace in field name
129+
"X\tName: Value", // error: no whitespace in field name (tab)
130+
] {
131+
let header = Header::from_str(input);
132+
133+
assert!(header.is_err(), "Expected error: [{}]", input);
134+
}
135+
}
136+
137+
#[test]
138+
fn it_parses_valid_headers() {
139+
// https://tools.ietf.org/html/rfc7230#section-3.2
140+
141+
let expected1 = Header {
142+
name: "X-Name".to_string(),
143+
value: "Value".to_string(),
144+
};
145+
let expected2 = Header {
146+
name: "X-Name".to_string(),
147+
value: "Value:".to_string(),
148+
};
149+
150+
for (input, expected) in vec![
151+
("X-Name: Value", &expected1), // ideal
152+
("X-Name:Value", &expected1), // no optional whitespace
153+
("X-Name: Value ", &expected1), // with optional whitespace
154+
("X-Name:\tValue", &expected1), // with optional whitespace (tab)
155+
("X-Name: Value:", &expected2), // with colon in value
156+
// not allowed per RFC, but we'll forgive
157+
("X-Name : Value", &expected1),
158+
(" X-Name: Value", &expected1),
159+
] {
160+
let header = Header::from_str(input);
161+
162+
assert!(header.is_ok(), "Expected ok: [{}]", input);
163+
assert_eq!(header.unwrap(), *expected, "Expected equality: [{}]", input);
164+
}
165+
}
166+
}

graphql_client_cli/src/main.rs

+6-1
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,8 @@ 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(&schema_location, output, authorization, headers),
8994
Cli::Generate {
9095
additional_derives,
9196
deprecation_strategy,

0 commit comments

Comments
 (0)