Skip to content

Commit

Permalink
Generic JSON body support in rust-server (#1523)
Browse files Browse the repository at this point in the history
* Ensure that generic JSON bodies are correctly autogenerated

Generates valid Rust for an arbitrary JSON response body of type `object` with no other schema.
  • Loading branch information
mthebridge authored and bjgill committed Dec 4, 2018
1 parent 8795bff commit 4930f75
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,12 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
rsp.vendorExtensions.put("producesPlainText", true);
} else {
rsp.vendorExtensions.put("producesJson", true);
// If the data type is just "object", then ensure that the Rust data type
// is "serde_json::Value". This allows us to define APIs that
// can return arbitrary JSON bodies.
if (rsp.dataType.equals("object")) {
rsp.dataType = "serde_json::Value";
}
}

Schema response = (Schema) rsp.schema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,38 @@ paths:
/file_response:
get:
summary: Get a file
produces:
- application/json
responses:
200:
description: Success
schema:
type: file
/raw_json:
get:
summary: Get an arbitrary JSON blob.
responses:
200:
description: Success
schema:
type: object
# Requests with arbitrary JSON currently fail.
# post:
# summary: Send an arbitrary JSON blob
# consumes:
# - application/json
# parameters:
# - in: body
# name: body
# required: true
# schema:
# type: object
# responses:
# 200:
# description: Success
# schema:
# type: string

parameters:
nested_response:
name: nested_response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ cargo run --example client DummyGet
cargo run --example client DummyPut
cargo run --example client FileResponseGet
cargo run --example client HtmlPost
cargo run --example client RawJsonGet
```

### HTTPS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,22 @@ paths:
responses:
200:
content:
'*/*':
application/json:
schema:
format: binary
type: string
description: Success
summary: Get a file
/raw_json:
get:
responses:
200:
content:
'*/*':
schema:
type: object
description: Success
summary: Get an arbitrary JSON blob.
components:
requestBodies:
nested_response:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ use rust_server_test::{ApiNoContext, ContextWrapperExt,
DummyGetResponse,
DummyPutResponse,
FileResponseGetResponse,
HtmlPostResponse
HtmlPostResponse,
RawJsonGetResponse
};
use clap::{App, Arg};

Expand All @@ -35,6 +36,7 @@ fn main() {
"DummyPut",
"FileResponseGet",
"HtmlPost",
"RawJsonGet",
])
.required(true)
.index(1))
Expand Down Expand Up @@ -95,6 +97,11 @@ fn main() {
println!("{:?} (X-Span-ID: {:?})", result, (client.context() as &Has<XSpanIdString>).get().clone());
},

Some("RawJsonGet") => {
let result = core.run(client.raw_json_get());
println!("{:?} (X-Span-ID: {:?})", result, (client.context() as &Has<XSpanIdString>).get().clone());
},

_ => {
panic!("Invalid operation provided")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use rust_server_test::{Api, ApiError,
DummyGetResponse,
DummyPutResponse,
FileResponseGetResponse,
HtmlPostResponse
HtmlPostResponse,
RawJsonGetResponse
};
use rust_server_test::models;

Expand Down Expand Up @@ -59,4 +60,11 @@ impl<C> Api<C> for Server<C> where C: Has<XSpanIdString>{
Box::new(futures::failed("Generic failure".into()))
}

/// Get an arbitrary JSON blob.
fn raw_json_get(&self, context: &C) -> Box<Future<Item=RawJsonGetResponse, Error=ApiError>> {
let context = context.clone();
println!("raw_json_get() - X-Span-ID: {:?}", context.get().0.clone());
Box::new(futures::failed("Generic failure".into()))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ use {Api,
DummyGetResponse,
DummyPutResponse,
FileResponseGetResponse,
HtmlPostResponse
HtmlPostResponse,
RawJsonGetResponse
};
use models;

Expand Down Expand Up @@ -508,6 +509,73 @@ if let Some(body) = body {

}

fn raw_json_get(&self, context: &C) -> Box<Future<Item=RawJsonGetResponse, Error=ApiError>> {


let uri = format!(
"{}/raw_json",
self.base_path
);

let uri = match Uri::from_str(&uri) {
Ok(uri) => uri,
Err(err) => return Box::new(futures::done(Err(ApiError(format!("Unable to build URI: {}", err))))),
};

let mut request = hyper::Request::new(hyper::Method::Get, uri);



request.headers_mut().set(XSpanId((context as &Has<XSpanIdString>).get().0.clone()));


Box::new(self.client_service.call(request)
.map_err(|e| ApiError(format!("No response received: {}", e)))
.and_then(|mut response| {
match response.status().as_u16() {
200 => {
let body = response.body();
Box::new(
body
.concat2()
.map_err(|e| ApiError(format!("Failed to read response: {}", e)))
.and_then(|body| str::from_utf8(&body)
.map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))
.and_then(|body|

serde_json::from_str::<serde_json::Value>(body)
.map_err(|e| e.into())

))
.map(move |body|
RawJsonGetResponse::Success(body)
)
) as Box<Future<Item=_, Error=_>>
},
code => {
let headers = response.headers().clone();
Box::new(response.body()
.take(100)
.concat2()
.then(move |body|
future::err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}",
code,
headers,
match body {
Ok(ref body) => match str::from_utf8(body) {
Ok(body) => Cow::from(body),
Err(e) => Cow::from(format!("<Body was not UTF8: {:?}>", e)),
},
Err(e) => Cow::from(format!("<Failed to read body: {}>", e)),
})))
)
) as Box<Future<Item=_, Error=_>>
}
}
}))

}

}

#[derive(Debug)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ pub enum HtmlPostResponse {
Success ( String ) ,
}

#[derive(Debug, PartialEq)]
pub enum RawJsonGetResponse {
/// Success
Success ( serde_json::Value ) ,
}


/// API
pub trait Api<C> {
Expand All @@ -79,6 +85,9 @@ pub trait Api<C> {
/// Test HTML handling
fn html_post(&self, body: String, context: &C) -> Box<Future<Item=HtmlPostResponse, Error=ApiError>>;

/// Get an arbitrary JSON blob.
fn raw_json_get(&self, context: &C) -> Box<Future<Item=RawJsonGetResponse, Error=ApiError>>;

}

/// API without a `Context`
Expand All @@ -96,6 +105,9 @@ pub trait ApiNoContext {
/// Test HTML handling
fn html_post(&self, body: String) -> Box<Future<Item=HtmlPostResponse, Error=ApiError>>;

/// Get an arbitrary JSON blob.
fn raw_json_get(&self) -> Box<Future<Item=RawJsonGetResponse, Error=ApiError>>;

}

/// Trait to extend an API to make it easy to bind it to a context.
Expand Down Expand Up @@ -132,6 +144,11 @@ impl<'a, T: Api<C>, C> ApiNoContext for ContextWrapper<'a, T, C> {
self.api().html_post(body, &self.context())
}

/// Get an arbitrary JSON blob.
fn raw_json_get(&self) -> Box<Future<Item=RawJsonGetResponse, Error=ApiError>> {
self.api().raw_json_get(&self.context())
}

}

#[cfg(feature = "client")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ pub mod responses {
// The macro is called per-operation to beat the recursion limit
/// Create Mime objects for the response content types for FileResponseGet
lazy_static! {
pub static ref FILE_RESPONSE_GET_SUCCESS: Mime = "*/*".parse().unwrap();
pub static ref FILE_RESPONSE_GET_SUCCESS: Mime = "application/json".parse().unwrap();
}
/// Create Mime objects for the response content types for HtmlPost
lazy_static! {
pub static ref HTML_POST_SUCCESS: Mime = "text/html".parse().unwrap();
}
/// Create Mime objects for the response content types for RawJsonGet
lazy_static! {
pub static ref RAW_JSON_GET_SUCCESS: Mime = "*/*".parse().unwrap();
}

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ use {Api,
DummyGetResponse,
DummyPutResponse,
FileResponseGetResponse,
HtmlPostResponse
HtmlPostResponse,
RawJsonGetResponse
};
#[allow(unused_imports)]
use models;
Expand All @@ -56,12 +57,14 @@ mod paths {
pub static ref GLOBAL_REGEX_SET: regex::RegexSet = regex::RegexSet::new(&[
r"^/dummy$",
r"^/file_response$",
r"^/html$"
r"^/html$",
r"^/raw_json$"
]).unwrap();
}
pub static ID_DUMMY: usize = 0;
pub static ID_FILE_RESPONSE: usize = 1;
pub static ID_HTML: usize = 2;
pub static ID_RAW_JSON: usize = 3;
}

pub struct NewService<T, C> {
Expand Down Expand Up @@ -377,6 +380,60 @@ where
},


// RawJsonGet - GET /raw_json
&hyper::Method::Get if path.matched(paths::ID_RAW_JSON) => {







Box::new({
{{

Box::new(api_impl.raw_json_get(&context)
.then(move |result| {
let mut response = Response::new();
response.headers_mut().set(XSpanId((&context as &Has<XSpanIdString>).get().0.to_string()));

match result {
Ok(rsp) => match rsp {
RawJsonGetResponse::Success

(body)


=> {
response.set_status(StatusCode::try_from(200).unwrap());

response.headers_mut().set(ContentType(mimetypes::responses::RAW_JSON_GET_SUCCESS.clone()));


let body = serde_json::to_string(&body).expect("impossible to fail to serialize");

response.set_body(body);
},
},
Err(_) => {
// Application code returned an error. This should not happen, as the implementation should
// return a valid response.
response.set_status(StatusCode::InternalServerError);
response.set_body("An internal error occurred");
},
}

future::ok(response)
}
))

}}
}) as Box<Future<Item=Response, Error=Error>>


},


_ => Box::new(future::ok(Response::new().with_status(StatusCode::NotFound))) as Box<Future<Item=Response, Error=Error>>,
}
}
Expand Down Expand Up @@ -410,6 +467,9 @@ impl RequestParser for ApiRequestParser {

// HtmlPost - POST /html
&hyper::Method::Post if path.matched(paths::ID_HTML) => Ok("HtmlPost"),

// RawJsonGet - GET /raw_json
&hyper::Method::Get if path.matched(paths::ID_RAW_JSON) => Ok("RawJsonGet"),
_ => Err(()),
}
}
Expand Down

0 comments on commit 4930f75

Please sign in to comment.