use crate::parser::Request;
use std::collections::BTreeMap;
use super::{convert, names, prepare_model_data, utils};
pub fn render_schema_file(requests: &[Request]) -> String {
let model_data = prepare_model_data::prepare_model_data(requests);
let identifiers = names::build_model_identifiers(&model_data.names);
let methods = group_requests_by_method(requests);
let mut out = String::new();
out.push_str("// This file is generated by kayto. Do not edit manually.\n\n");
render_model_types(&mut out, &model_data, &identifiers);
render_schemas_interface(&mut out, &model_data, &identifiers);
render_endpoints_namespace(&mut out, &methods);
out
}
fn render_model_types(
out: &mut String,
model_data: &prepare_model_data::ModelData,
identifiers: &BTreeMap<String, String>,
) {
for model_name in &model_data.names {
let ident = identifiers
.get(model_name)
.expect("type identifier must exist for each model name");
let ts_type = match model_data.definitions.get(model_name) {
Some(schema_type) => convert::schema_to_ts(schema_type),
None => "unknown".to_string(),
};
out.push_str(&format!("export type {ident} = {ts_type};\n\n"));
}
}
fn render_schemas_interface(
out: &mut String,
model_data: &prepare_model_data::ModelData,
identifiers: &BTreeMap<String, String>,
) {
out.push_str("export interface Schemas {\n");
for model_name in &model_data.names {
let ident = identifiers
.get(model_name)
.expect("type identifier must exist for each model name");
out.push_str(&format!(" {}: {ident};\n", utils::ts_quote(model_name)));
}
out.push_str("}\n\n");
}
fn render_endpoints_namespace(
out: &mut String,
methods: &BTreeMap<String, BTreeMap<String, &Request>>,
) {
out.push_str("export interface Endpoints {\n");
for (method, path_map) in methods {
out.push_str(&format!(" {}: {{\n", utils::ts_quote(method)));
for (path, req) in path_map {
render_endpoint_entry(out, method, path, req);
}
out.push_str(" };\n");
}
out.push_str("}\n");
}
fn render_endpoint_entry(out: &mut String, method: &str, path: &str, req: &Request) {
out.push_str(&format!(" {}: {{\n", utils::ts_quote(path)));
out.push_str(&format!(" path: {};\n", utils::ts_quote(&req.path)));
out.push_str(&format!(" method: {};\n", utils::ts_quote(method)));
if let Some(operation_id) = &req.operation_id {
out.push_str(&format!(
" operationId: {};\n",
utils::ts_quote(operation_id)
));
}
match convert::params_to_ts(req) {
Some(params_type) => out.push_str(&format!(
" params: {};\n",
utils::indent_inline(¶ms_type, " ")
)),
None => out.push_str(" params?: never;\n"),
}
match req.body.as_ref() {
Some(body) => {
let body_type = convert::parsed_response_to_ts(body);
out.push_str(&format!(
" body: {};\n",
utils::indent_inline(&body_type, " ")
));
}
None => out.push_str(" body?: never;\n"),
}
match convert::responses_to_ts(req) {
Some(responses_type) => out.push_str(&format!(
" responses: {};\n",
utils::indent_inline(&responses_type, " ")
)),
None => out.push_str(" responses?: never;\n"),
}
out.push_str(" };\n");
}
fn group_requests_by_method(requests: &[Request]) -> BTreeMap<String, BTreeMap<String, &Request>> {
let mut methods: BTreeMap<String, BTreeMap<String, &Request>> = BTreeMap::new();
for req in requests {
methods
.entry(req.method.to_lowercase())
.or_default()
.insert(req.path.clone(), req);
}
methods
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::{ParsedResponse, Request, SchemaType};
use crate::{parser, spec, test_fixtures};
use std::collections::BTreeMap;
fn fixture_requests() -> Vec<Request> {
let mut responses = BTreeMap::new();
responses.insert(
200,
ParsedResponse {
schema_type: Some(SchemaType::Ref("Item".to_string())),
schema_name: None,
},
);
vec![Request {
path: "/items".to_string(),
method: "get".to_string(),
operation_id: Some("listItems".to_string()),
params: None,
body: None,
responses: Some(responses),
}]
}
#[test]
fn renders_expected_schema_snapshot() {
let output = render_schema_file(&fixture_requests());
let expected = r#"// This file is generated by kayto. Do not edit manually.
export type Item = unknown;
export interface Schemas {
"Item": Item;
}
export interface Endpoints {
"get": {
"/items": {
path: "/items";
method: "get";
operationId: "listItems";
params?: never;
body?: never;
responses: {
200: Schemas["Item"];
};
};
};
}
"#;
assert_eq!(output, expected);
}
#[test]
fn renders_reserved_method_as_quoted_key() {
let mut responses = BTreeMap::new();
responses.insert(
204,
ParsedResponse {
schema_type: None,
schema_name: None,
},
);
let requests = vec![Request {
path: "/items/{id}".to_string(),
method: "delete".to_string(),
operation_id: Some("deleteItem".to_string()),
params: None,
body: None,
responses: Some(responses),
}];
let output = render_schema_file(&requests);
assert!(output.contains("\"delete\": {"));
assert!(output.contains("method: \"delete\";"));
}
fn parse_requests_from_json(input: &str) -> Vec<Request> {
let openapi: spec::OpenAPI = serde_json::from_str(input).expect("valid OpenAPI json");
let parsed = parser::parse(&openapi).expect("parser should return output");
assert!(
parsed.issues.is_empty(),
"expected no parser issues, got: {:#?}",
parsed.issues
);
parsed.requests
}
#[test]
fn renders_combinators_end_to_end_snapshot() {
let requests = parse_requests_from_json(test_fixtures::COMBINATORS_OPENAPI_JSON);
let output = render_schema_file(&requests);
let expected = r#"// This file is generated by kayto. Do not edit manually.
export type BasePet = {
"id"?: number;
};
export type CatKind = ("cat") | ("tiger");
export type CreatePetRequest = {
"filter"?: Schemas["PetFilter"];
"kind"?: Schemas["CatKind"];
"name": string;
};
export type PetDetails = (Schemas["BasePet"]) & ({
"name": string;
});
export type PetFilter = (string) | (number);
export interface Schemas {
"BasePet": BasePet;
"CatKind": CatKind;
"CreatePetRequest": CreatePetRequest;
"PetDetails": PetDetails;
"PetFilter": PetFilter;
}
export interface Endpoints {
"get": {
"/base-pet": {
path: "/base-pet";
method: "get";
operationId: "getBasePet";
params?: never;
body?: never;
responses: {
200: Schemas["BasePet"];
};
};
"/filters": {
path: "/filters";
method: "get";
operationId: "getPetFilter";
params?: never;
body?: never;
responses: {
200: Schemas["PetFilter"];
};
};
"/kinds": {
path: "/kinds";
method: "get";
operationId: "getCatKind";
params?: never;
body?: never;
responses: {
200: Schemas["CatKind"];
};
};
};
"post": {
"/pets": {
path: "/pets";
method: "post";
operationId: "createPet";
params?: never;
body: Schemas["CreatePetRequest"];
responses: {
201: Schemas["PetDetails"];
};
};
};
}
"#;
assert_eq!(output, expected);
}
}