diff --git a/openapi-converter/clients_schema/src/lib.rs b/openapi-converter/clients_schema/src/lib.rs index ae790cbe33..15c3131345 100644 --- a/openapi-converter/clients_schema/src/lib.rs +++ b/openapi-converter/clients_schema/src/lib.rs @@ -750,7 +750,7 @@ impl Model { } pub fn type_registry(&self) -> TypeRegistry { - TypeRegistry::new(self) + TypeRegistry::new(&self.types) } } @@ -793,8 +793,8 @@ pub struct TypeRegistry<'a> { } impl<'a> TypeRegistry<'a> { - pub fn new(model: &Model) -> TypeRegistry { - let types = model.types.iter() + pub fn new(types_vec: &Vec) -> TypeRegistry { + let types = types_vec.iter() .map(|typedef| (typedef.name(), typedef)) .collect::>(); diff --git a/openapi-converter/clients_schema_to_openapi/src/components.rs b/openapi-converter/clients_schema_to_openapi/src/components.rs new file mode 100644 index 0000000000..29856c0788 --- /dev/null +++ b/openapi-converter/clients_schema_to_openapi/src/components.rs @@ -0,0 +1,44 @@ +use openapiv3::{Components, Parameter, ReferenceOr, RequestBody, Response, Schema, StatusCode}; +use clients_schema::{TypeDefinition, TypeName, TypeRegistry}; +use crate::schemas::SchemaName; + +pub struct TypesAndComponents<'a> { + pub types: TypeRegistry<'a>, + pub components: &'a mut Components, +} + +impl <'a> TypesAndComponents<'a> { + pub fn new(types_vec: &'a Vec, components: &'a mut Components) -> TypesAndComponents<'a> { + TypesAndComponents { + types: TypeRegistry::new(types_vec), + components, + } + } + + pub fn add_request_body(&mut self, endpoint: &str, body: RequestBody,) -> ReferenceOr { + self.components.request_bodies.insert(endpoint.to_string(), ReferenceOr::Item(body)); + ReferenceOr::Reference { + reference: format!("#/components/requestBodies/{}", endpoint) + } + } + + pub fn add_parameter(&mut self, endpoint: &str, param: Parameter) -> ReferenceOr { + let result = ReferenceOr::Reference { + reference: format!("#/components/parameters/{}#{}", endpoint, ¶m.parameter_data_ref().name) + }; + self.components.parameters.insert(format!("{}#{}", endpoint, ¶m.parameter_data_ref().name), ReferenceOr::Item(param)); + result + } + + pub fn add_response(&mut self, endpoint: &str, status: StatusCode, response: Response) -> ReferenceOr { + self.components.responses.insert(format!("{}#{}", endpoint, status), ReferenceOr::Item(response)); + ReferenceOr::Reference { + reference: format!("#/components/responses/{}#{}", endpoint, status) + } + } + + pub fn add_schema(&mut self, name: &TypeName, schema: ReferenceOr) -> ReferenceOr { + self.components.schemas.insert(name.schema_name(), schema); + name.schema_ref() + } +} diff --git a/openapi-converter/clients_schema_to_openapi/src/lib.rs b/openapi-converter/clients_schema_to_openapi/src/lib.rs index 9f059fc481..c825ccb36b 100644 --- a/openapi-converter/clients_schema_to_openapi/src/lib.rs +++ b/openapi-converter/clients_schema_to_openapi/src/lib.rs @@ -1,67 +1,91 @@ mod paths; mod schemas; +mod components; use std::io::Write; use std::path::Path; +use openapiv3::{Components, OpenAPI}; -use clients_schema::{Model, Property, TypeDefinition, TypeName, TypeRegistry, ValueOf}; +use clients_schema::{Endpoint, Model}; +use crate::components::TypesAndComponents; -pub fn convert_schema(path: impl AsRef, out: impl Write) -> anyhow::Result<()> { +pub fn convert_schema_file(path: impl AsRef, endpoint_filter: fn(e: &Endpoint) -> bool, out: impl Write) -> anyhow::Result<()> { let file = std::fs::File::open(path)?; - let model: Model = serde_json::from_reader(file)?; - let types = model.type_registry(); - - let mut openapi = openapiv3::OpenAPI::default(); - openapi.openapi = "3.1.0".into(); + let openapi = convert_schema(&model, endpoint_filter)?; - openapi.info = openapiv3::Info { - title: "Elasticsearch API".to_string(), - description: None, - terms_of_service: None, - contact: None, - license: license(&model), - version: "".to_string(), - extensions: Default::default(), - }; + serde_json::to_writer_pretty(out, &openapi)?; + Ok(()) +} - // Endpoints - let paths = paths::build_paths(&model.endpoints, &types)?; +pub fn convert_schema(model: &Model, endpoint_filter: fn(e: &Endpoint) -> bool) -> anyhow::Result { - openapi.paths = openapiv3::Paths { - paths: paths, + let mut openapi = OpenAPI { + openapi: "3.0.3".into(), + info: info(model), + servers: vec![], + paths: Default::default(), + components: Some(Components { + security_schemes: Default::default(), + // Filled from endpoints + responses: Default::default(), + // Filled from endpoints + // TODO: add common request parameters and common cat parameters? + parameters: Default::default(), + examples: Default::default(), + // Filled from endpoints + request_bodies: Default::default(), + headers: Default::default(), + // Filled with type definitions + schemas: Default::default(), + links: Default::default(), + callbacks: Default::default(), + extensions: Default::default(), + }), + security: None, + tags: vec![], + external_docs: None, extensions: Default::default(), }; - // Types - let components = openapiv3::Components { - security_schemes: Default::default(), - responses: Default::default(), - parameters: Default::default(), - examples: Default::default(), - request_bodies: Default::default(), - headers: Default::default(), - schemas: Default::default(), - links: Default::default(), - callbacks: Default::default(), - extensions: Default::default(), - }; + let mut tac = TypesAndComponents::new(&model.types, openapi.components.as_mut().unwrap()); - openapi.components = Some(components); + // Endpoints + for endpoint in model.endpoints.iter().filter(|e| endpoint_filter(e)) { + paths::add_endpoint(endpoint, &mut tac, &mut openapi.paths)?; + } + //let paths = paths::build_paths(model.endpoints, &types)?; - serde_json::to_writer_pretty(out, &openapi)?; - Ok(()) + //openapi.paths = openapiv3::Paths { + // paths: paths, + // extensions: Default::default(), + //}; + + Ok(openapi) } -fn license(model: &Model) -> Option { - if let Some(info) = &model.info { - Some(openapiv3::License { - name: info.license.name.clone(), - url: Some(info.license.url.clone()), - extensions: Default::default(), - }) +fn info(model: &Model) -> openapiv3::Info { + let (title, license) = if let Some(info) = &model.info { + ( + info.title.clone(), + Some(openapiv3::License { + name: info.license.name.clone(), + url: Some(info.license.url.clone()), + extensions: Default::default(), + }) + ) } else { - None + ("".to_string(), None) + }; + + openapiv3::Info { + title, + description: None, + terms_of_service: None, + contact: None, + license, + version: "".to_string(), // TODO + extensions: Default::default(), } } diff --git a/openapi-converter/clients_schema_to_openapi/src/main.rs b/openapi-converter/clients_schema_to_openapi/src/main.rs index 21935133df..fac749b699 100644 --- a/openapi-converter/clients_schema_to_openapi/src/main.rs +++ b/openapi-converter/clients_schema_to_openapi/src/main.rs @@ -1,7 +1,18 @@ -fn main() { - clients_schema_to_openapi::convert_schema( - "../../elasticsearch-specification/output/schema/schema.json", +use tracing::Level; +use tracing_subscriber::FmtSubscriber; + +fn main() -> anyhow::Result<()> { + + let subscriber = FmtSubscriber::builder() + .with_max_level(Level::TRACE) + .finish(); + tracing::subscriber::set_global_default(subscriber)?; + + clients_schema_to_openapi::convert_schema_file( + "../output/schema/schema-no-generics.json", + |e| e.name == "search", std::io::stdout() - ).unwrap(); + )?; + Ok(()) } diff --git a/openapi-converter/clients_schema_to_openapi/src/paths.rs b/openapi-converter/clients_schema_to_openapi/src/paths.rs index d21d0f2315..de4baa1f85 100644 --- a/openapi-converter/clients_schema_to_openapi/src/paths.rs +++ b/openapi-converter/clients_schema_to_openapi/src/paths.rs @@ -1,138 +1,226 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use anyhow::{anyhow, bail}; -use indexmap::IndexMap; -use openapiv3::{ExternalDocumentation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, PathStyle, QueryStyle, ReferenceOr, Schema}; -use regex::Regex; +use indexmap::indexmap; -use clients_schema::{Property, TypeDefinition, TypeName, TypeRegistry, ValueOf}; -use crate::schemas; +use openapiv3::{ExternalDocumentation, MediaType, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, Paths, PathStyle, QueryStyle, ReferenceOr, RequestBody, Response, Responses, StatusCode}; +use regex::Regex; +use clients_schema::Property; +use crate::components::TypesAndComponents; -pub fn build_paths(endpoints: &Vec, types: &TypeRegistry) - -> anyhow::Result>> { - let mut paths = IndexMap::new(); +/// Add an endpoint to the OpenAPI schema. This will result in the addition of a number of elements to the +/// openapi schema's `paths` and `components` sections. +/// +pub fn add_endpoint(endpoint: &clients_schema::Endpoint, tac: &mut TypesAndComponents, out: &mut Paths) -> anyhow::Result<()> { - for endpoint in endpoints { - if endpoint.request.is_none() || endpoint.response.is_none() { - tracing::warn!("Endpoint {} is missing either request or response", &endpoint.name); - continue; - } + if endpoint.request.is_none() || endpoint.response.is_none() { + tracing::warn!("Endpoint {} is missing either request or response", &endpoint.name); + return Ok(()); + } - let request = types.get_request(endpoint.request.as_ref().unwrap())?; + // Will we produce multiple paths? If true, we will register components for reuse across paths + let is_multipath = endpoint.urls.len() > 1 || endpoint.urls.iter().find(|u| u.methods.len() > 1).is_some(); + + let request = tac.types.get_request(endpoint.request.as_ref().unwrap())?; + + fn parameter_data(prop: &Property, tac: &mut TypesAndComponents) -> anyhow::Result { + Ok(ParameterData { + name: prop.name.clone(), + description: prop.description.clone(), + required: prop.required, + deprecated: Some(prop.deprecation.is_some()), + format: ParameterSchemaOrContent::Schema(tac.convert_value_of(&prop.typ)?), + example: None, + examples: Default::default(), + explode: None, // Defaults to simple, i.e. comma-separated values for arrays + extensions: Default::default(), + }) + } - fn find_property<'a>(name: &str, props: &'a Vec) -> anyhow::Result<&'a Property> { - props.iter() - .find(|p| p.name == name) - .ok_or_else(|| anyhow!("Property {} not found in request ", name)) + //----- Prepare path parameters + + let mut path_params = HashMap::new(); + for prop in request.path.iter() { + let parameter = Parameter::Path { + parameter_data: parameter_data(prop, tac)?, + // Simple (the default) maps array to comma-separated values, which is ES behavior + // See https://www.rfc-editor.org/rfc/rfc6570#section-3.2.2 + style: PathStyle::Simple, + }; + + // Reuse reference if multiple paths, and inline otherwise + path_params.insert(prop.name.clone(), if is_multipath { + tac.add_parameter(&endpoint.name, parameter) + } else { + ReferenceOr::Item(parameter) + }); + }; + + //----- Prepare query parameters + + let mut query_params = Vec::new(); + for prop in request.query.iter() { + let parameter = Parameter::Query { + parameter_data: parameter_data(prop, tac)?, + allow_reserved: false, + style: QueryStyle::Form, + allow_empty_value: None, + }; + + query_params.push(if is_multipath { + tac.add_parameter(&endpoint.name, parameter) + } else { + ReferenceOr::Item(parameter) + }); + }; + + //---- Prepare request body + + let request_body = tac.convert_request(request)?.map(|schema| { + let media = MediaType { + schema: Some(schema), + example: None, + examples: Default::default(), + encoding: Default::default(), + extensions: Default::default(), + }; + + let body = RequestBody { + description: None, + // FIXME: nd-json requests + content: indexmap!{ "application/json".to_string() => media }, + required: endpoint.request_body_required, + extensions: Default::default(), + }; + + if is_multipath { + tac.add_request_body(&endpoint.name, body) + } else { + ReferenceOr::Item(body) } - - fn parameter_data(prop: &Property) -> anyhow::Result { - Ok(ParameterData { - name: prop.name.clone(), - description: prop.description.clone(), - required: prop.required, - deprecated: Some(prop.deprecation.is_some()), - format: ParameterSchemaOrContent::Schema(schemas::for_value_of(&prop.typ)?), + }); + + //---- Prepare request responses + + // FIXME: buggy for responses with no body + // TODO: handle binary responses + let response_def = tac.types.get_resppnse(endpoint.response.as_ref().unwrap())?; + let response = Response { + description: "".to_string(), + headers: Default::default(), + content: indexmap! { + "application/json".to_string() => MediaType { + schema: tac.convert_response(response_def)?, example: None, examples: Default::default(), - explode: None, // Defaults to simple, i.e. comma-separated values for arrays + encoding: Default::default(), extensions: Default::default(), - }) - } - - for url_template in &endpoint.urls { - - //----- Path and query parameters - - let mut parameters = Vec::new(); - let mut operation_path = url_template.path.clone(); - - // Path parameters - - for path_variable in get_path_parameters(&operation_path) { - let parameter = Parameter::Path { - parameter_data: parameter_data(find_property(path_variable, &request.path)?)?, - // Simple (the default) maps array to comma-separated values, which is ES behavior - // See https://www.rfc-editor.org/rfc/rfc6570#section-3.2.2 - style: PathStyle::Simple, - }; - - parameters.push(ReferenceOr::Item(parameter)); } + }, + links: Default::default(), + extensions: Default::default(), + }; + let response = if is_multipath { + tac.add_response(&endpoint.name, StatusCode::Code(200), response) + } else { + ReferenceOr::Item(response) + }; + + let responses = indexmap! { + StatusCode::Code(200) => response + // TODO: add error responses + }; + + //---- Build a path for each url + method + let mut operation_counter = 0; + + for url_template in &endpoint.urls { + + // Path and query parameters + + let mut parameters = Vec::new(); + + for path_variable in get_path_parameters(&url_template.path) { + let parameter = path_params.get(path_variable) + .ok_or_else(|| anyhow!("Missing path parameter definition {} for endpoint {}", + path_variable, &endpoint.name) + )?; + parameters.push(parameter.clone()); + } - // Query parameters - - for query_prop in &request.query { - let parameter = Parameter::Query { - parameter_data: parameter_data(query_prop)?, - allow_reserved: false, - style: QueryStyle::Form, - allow_empty_value: None, - }; - - parameters.push(ReferenceOr::Item(parameter)); - } + parameters.append(&mut query_params.clone()); - // Add query parameter names to the path template - // See https://www.rfc-editor.org/rfc/rfc6570#section-3.2.8 - if !&request.query.is_empty() { - let params = &request.query.iter().map(|p| p.name.as_str()).collect::>().join(","); - operation_path = format!("{operation_path}{{?{params}}}"); - } - - // Create the operation, it will be repeated if we have several methods - // LATER: we could also register the operation as a component and reference it several times - let operation = openapiv3::Operation { - tags: vec![], - summary: None, - description: Some(endpoint.description.clone()), - external_docs: endpoint.doc_url.as_ref().map(|url| ExternalDocumentation { - description: None, - url: url.clone(), - extensions: Default::default(), - }), - operation_id: None, - parameters: parameters, - request_body: None, // TODO - responses: Default::default(), // TODO - deprecated: endpoint.deprecation.is_some(), - security: None, - servers: vec![], - extensions: Default::default(), // FIXME: translate availability? - }; - - let mut path = PathItem { - summary: None, + // Create the operation, it will be repeated if we have several methods + let operation = openapiv3::Operation { + tags: vec![], + summary: None, + description: Some(endpoint.description.clone()), + external_docs: endpoint.doc_url.as_ref().map(|url| ExternalDocumentation { description: None, - get: None, - put: None, - post: None, - delete: None, - options: None, - head: None, - patch: None, - trace: None, - servers: vec![], - parameters: vec![], + url: url.clone(), + extensions: Default::default(), + }), + operation_id: None, // set in clone_operation below with operation_counter + parameters, + request_body: request_body.clone(), + responses: Responses { + default: None, + responses: responses.clone(), extensions: Default::default(), - }; - - for method in &url_template.methods { - match method.as_str() { - "HEAD" => path.get = Some(operation.clone()), - "GET" => path.get = Some(operation.clone()), - "POST" => path.post = Some(operation.clone()), - "PUT" => path.put = Some(operation.clone()), - "DELETE" => path.put = Some(operation.clone()), - _ => bail!("Unsupported method: {}", method), - } + }, + deprecated: endpoint.deprecation.is_some(), + security: None, + servers: vec![], + extensions: Default::default(), // FIXME: translate availability? + }; + + let mut path = PathItem { + summary: None, + description: None, + get: None, + put: None, + post: None, + delete: None, + options: None, + head: None, + patch: None, + trace: None, + servers: vec![], + parameters: vec![], + extensions: Default::default(), + }; + + let mut clone_operation = || { + let mut clone = operation.clone(); + clone.operation_id = Some(format!("{}#{}", endpoint.name, operation_counter)); + operation_counter += 1; + Some(clone) + }; + + for method in &url_template.methods { + match method.as_str() { + "HEAD" => path.get = clone_operation(), + "GET" => path.get = clone_operation(), + "POST" => path.post = clone_operation(), + "PUT" => path.put = clone_operation(), + "DELETE" => path.put = clone_operation(), + _ => bail!("Unsupported method: {}", method), } + } - paths.insert(operation_path, ReferenceOr::Item(path)); + let mut operation_path = url_template.path.clone(); + + // Add query parameter names to the path template + // See https://www.rfc-editor.org/rfc/rfc6570#section-3.2.8 + if !&request.query.is_empty() { + let params = &request.query.iter().map(|p| p.name.as_str()).collect::>().join(","); + operation_path = format!("{operation_path}{{?{params}}}"); } + + out.paths.insert(operation_path, ReferenceOr::Item(path)); } - Ok(paths) + Ok(()) } fn get_path_parameters (template: &str) -> Vec<&str> { diff --git a/openapi-converter/clients_schema_to_openapi/src/schemas.rs b/openapi-converter/clients_schema_to_openapi/src/schemas.rs index 0cc7625a30..1ca6505150 100644 --- a/openapi-converter/clients_schema_to_openapi/src/schemas.rs +++ b/openapi-converter/clients_schema_to_openapi/src/schemas.rs @@ -1,7 +1,507 @@ -use openapiv3::{ReferenceOr, Schema}; -use clients_schema::ValueOf; +use anyhow::bail; +use indexmap::IndexMap; +use openapiv3::{AdditionalProperties, ArrayType, Discriminator, ExternalDocumentation, NumberType, ObjectType, ReferenceOr, Schema, SchemaData, SchemaKind, StringType, Type}; +use clients_schema::{Body, Enum, Interface, LiteralValueValue, PropertiesBody, Property, Request, Response, TypeAlias, TypeAliasVariants, TypeDefinition, TypeName, ValueOf}; -pub fn for_value_of(value_of: &ValueOf) -> anyhow::Result> { - // TODO - Ok(ReferenceOr::ref_("foo")) +use crate::components::TypesAndComponents; + +// A placeholder in components.schema to handle recursive types +const SCHEMA_PLACEHOLDER: ReferenceOr = ReferenceOr::Reference { + reference: String::new() +}; + +pub trait ReferenceOrBoxed { + fn boxed(self) -> ReferenceOr>; +} + +impl ReferenceOrBoxed for ReferenceOr { + fn boxed(self) -> ReferenceOr> { + match self { + ReferenceOr::Item(t) => ReferenceOr::Item(Box::new(t)), + ReferenceOr::Reference { reference } => ReferenceOr::Reference { reference }, + } + } +} + +pub trait SchemaName { + fn schema_name(&self) -> String; + fn schema_ref(&self) -> ReferenceOr; +} + +impl SchemaName for TypeName { + fn schema_name(&self) -> String { + format!("{}", self) + } + + fn schema_ref(&self) -> ReferenceOr { + ReferenceOr::Reference { + reference: format!("#/components/schemas/{}", self) + } + } +} + +pub trait IntoSchema { + fn into_schema(self) -> ReferenceOr; + fn into_schema_with_base(self, base: &clients_schema::BaseType) -> ReferenceOr where Self: Sized { + let mut result = self.into_schema(); + if let ReferenceOr::Item(ref mut schema) = &mut result { + fill_data_with_base(&mut schema.schema_data, base); + } + result + } + + fn into_schema_with_data_fn(self, f: fn (&mut SchemaData) -> ()) -> ReferenceOr where Self: Sized { + let mut result = self.into_schema(); + if let ReferenceOr::Item(ref mut schema) = &mut result { + f(&mut schema.schema_data); + } + result + } +} + +impl IntoSchema for SchemaKind { + fn into_schema(self) -> ReferenceOr { + ReferenceOr::Item(Schema { + schema_data: Default::default(), + schema_kind: self, + }) + } +} + +impl IntoSchema for Type { + fn into_schema(self) -> ReferenceOr { + ReferenceOr::Item(Schema { + schema_kind: SchemaKind::Type(self), + schema_data: Default::default(), + }) + } +} + +impl IntoSchema for ObjectType { + fn into_schema(self) -> ReferenceOr { + ReferenceOr::Item(Schema { + schema_kind: SchemaKind::Type(Type::Object(self)), + schema_data: Default::default(), + }) + } } + +impl <'a> TypesAndComponents<'a> { + + pub fn convert_value_of(&mut self, value_of: &ValueOf) -> anyhow::Result> { + + Ok(match value_of { + // + // Instance_of + ValueOf::InstanceOf(instance) => { + // Do not care about generics, we work with an expanded schema + self.for_type_name(&instance.typ)? + }, + + // + // Array + ValueOf::ArrayOf(array) => { + ReferenceOr::Item(Schema { + schema_data: Default::default(), + schema_kind: SchemaKind::Type(Type::Array(ArrayType { + items: Some(self.convert_value_of(&array.value)?.boxed()), + min_items: None, + max_items: None, + unique_items: false, + })) + }) + }, + + // + // Union + ValueOf::UnionOf(union) => { + let mut items = Vec::new(); + for item in &union.items { + items.push(self.convert_value_of(item)?) + } + + ReferenceOr::Item(Schema { + schema_data: Default::default(), + schema_kind: SchemaKind::OneOf { + one_of: items, + } + }) + }, + + // + // Dictionary + // See https://swagger.io/docs/specification/data-models/dictionaries/ + ValueOf::DictionaryOf(dict) => { + ObjectType { + properties: Default::default(), + required: vec![], + additional_properties: Some(AdditionalProperties::Schema(Box::new(self.convert_value_of(&dict.value)?))), + // Single key dictionaries have exactly one property + min_properties: if dict.single_key { Some(1) } else { None }, + max_properties: if dict.single_key { Some(1) } else { None }, + }.into_schema() + }, + + // + // User defined value + ValueOf::UserDefinedValue(_) => { + ReferenceOr::Item(Schema { + schema_data: Default::default(), + // FIXME: not the right way to represent an arbitrary value + schema_kind: SchemaKind::Type(Type::Object(ObjectType::default())) + }) + }, + + // + // Literal value + ValueOf::LiteralValue(literal) => { + let str_value = match &literal.value { + LiteralValueValue::String(s) => s.clone(), + LiteralValueValue::Number(n) => n.to_string(), + LiteralValueValue::Boolean(b) => b.to_string(), + }; + + ReferenceOr::Item(Schema { + // Note: the enclosing property will add "required: true" + schema_data: Default::default(), + schema_kind: SchemaKind::Type(Type::String(StringType { + format: Default::default(), + pattern: None, + enumeration: vec![Some(str_value)], + min_length: None, + max_length: None, + })), + }) + } + }) + } + + // + // Return the reference for a type name, registering it if needed + // + pub fn for_type_name(&mut self, type_name: &TypeName) -> anyhow::Result> { + let schema_name = type_name.schema_name(); + + if self.components.schemas.contains_key(&schema_name) { + // Has already been processed + return Ok(type_name.schema_ref()); + } + + // Builtin types + if type_name.namespace == "_builtins" { + return match type_name.name.as_str() { + "string" => { + Ok(Type::String(StringType { + format: Default::default(), + pattern: None, + enumeration: vec![], + min_length: None, + max_length: None, + }).into_schema()) + }, + "boolean" => { + Ok(Type::Boolean {}.into_schema()) + }, + "number" => { + Ok(Type::Number(NumberType::default()).into_schema()) + }, + "void" => { + // Empty object + Ok(ObjectType::default().into_schema()) + }, + "null" => { + // Note that there is no null type; instead, the nullable attribute is used as a modifier of the base type. + // https://swagger.io/docs/specification/data-models/data-types/ + // FIXME: null should be handled in unions by setting "nullable" to the resulting schema + + Ok(Type::String(StringType { + format: Default::default(), + pattern: None, + enumeration: vec![], + min_length: None, + max_length: None, + }).into_schema_with_data_fn(|data| { data.nullable = true; })) + } + _ => bail!("unknown builtin type: {}", type_name), + } + } + + if type_name.namespace == "_types" { + match type_name.name.as_str() { + "double" | "long" | "integer" | "float" => { + return Ok(Type::Number(NumberType::default()).into_schema()); + }, + _ => {}, + } + } + + // Store a placeholder, it will avoid infinite loops with recursive types + self.components.schemas.insert(schema_name, SCHEMA_PLACEHOLDER); + + let typedef = self.types.get(type_name)?; + use TypeDefinition::*; + let schema = match typedef { + // Request and response may not have a body (and a schema) and so have dedicated methods below + Request(_) => bail!("Requests should be handled using for_request"), + Response(_) => bail!("Responses should be handled using for_request"), + + Enum(enumm) => self.convert_enum(enumm)?, + Interface(itf) => self.convert_interface_definition(itf)?, + TypeAlias(alias) => self.convert_type_alias(alias)?, + }; + + Ok(self.add_schema(type_name, schema)) + } + + // Returns the schema, if any, for a request body + pub fn convert_request(&mut self, request: &Request) -> anyhow::Result>> { + self.for_body(&request.body) + } + + pub fn convert_response(&mut self, response: &Response) -> anyhow::Result>> { + self.for_body(&response.body) + } + + fn for_body(&mut self, body: &Body) -> anyhow::Result>> { + + let result = match body { + Body::NoBody(_) => None, + Body::Value(value_body) => Some(self.convert_value_of(&value_body.value)?), // TODO codegen_name? + Body::Properties(PropertiesBody { properties }) => { + Some(ObjectType { + properties: self.convert_properties(properties.iter())?, + required: self.required_properties(properties.iter()), + additional_properties: None, + min_properties: None, + max_properties: None, + }.into_schema()) + } + }; + + Ok(result) + } + + fn convert_property(&mut self, prop: &Property) -> anyhow::Result> { + let mut result = self.convert_value_of(&prop.typ)?; + if let ReferenceOr::Item(ref mut schema) = &mut result { + fill_data_with_prop(&mut schema.schema_data, prop); + } + Ok(result) + } + + fn convert_properties<'b> (&mut self, props: impl Iterator) -> anyhow::Result>>> { + let mut result = IndexMap::new(); + for prop in props { + result.insert(prop.name.clone(), self.convert_property(prop)?.boxed()); + } + Ok(result) + } + + fn required_properties<'b> (&mut self, props: impl Iterator) -> Vec { + props.filter_map(|prop| prop.required.then(|| prop.name.clone())).collect() + } + + // + // Register an interface definition and return the schema reference. + // + fn convert_interface_definition(&mut self, itf: &Interface) -> anyhow::Result> { + + let mut schema = if let Some(container) = &itf.variants { + // TODO: interface definition container.non_exhaustive + let _non_exhaustive = container.non_exhaustive; + + // Split container properties and variants + let container_props = itf.properties.iter().filter(|p| p.container_property).collect::>(); + let variant_props = itf.properties.iter().filter(|p| !p.container_property).collect::>(); + + // A container is represented by an object will all optional properties and exactly one that + // needs to be set. + let mut schema = ObjectType { + properties: self.convert_properties(variant_props.iter().map(|p| *p))?, + required: vec![], + additional_properties: None, + min_properties: Some(1), + max_properties: Some(1), + }.into_schema(); + + if !container_props.is_empty() { + // Create a schema for the container property, and group it in an "allOf" with variants + let container_props_schema = ObjectType { + properties: self.convert_properties(container_props.iter().map(|p| *p))?, + required: self.required_properties(container_props.iter().map(|p| *p)), + additional_properties: None, + min_properties: None, + max_properties: None, + }.into_schema(); + + schema = SchemaKind::AllOf { + all_of: vec![container_props_schema, schema], + }.into_schema(); + } + + fill_schema_with_base(&mut schema, &itf.base); + schema + + } else { + let schema = ObjectType { + properties: self.convert_properties(itf.properties.iter())?, + required: self.required_properties(itf.properties.iter()), + additional_properties: None, + min_properties: None, + max_properties: None, + }.into_schema(); + + schema + }; + + // Inheritance + if let Some(inherit) = &itf.inherits { + schema = SchemaKind::AllOf { + all_of: vec![self.for_type_name(&inherit.typ)?, schema], + }.into_schema(); + } + + // Behaviors + for bh in &itf.implements { + match bh.typ.name.as_str() { + name @ ("AdditionalProperty" | "AdditionalProperties") => { + let single = name == "AdditionalProperty"; + let value_schema = self.convert_value_of(&bh.generics[1])?; + + schema = ObjectType { + properties: Default::default(), + required: vec![], + additional_properties: Some(AdditionalProperties::Schema(Box::new(value_schema))), + min_properties: if single { Some(1) } else { None }, + max_properties: if single { Some(1) } else { None }, + }.into_schema(); + } + _ => bail!("Unknown behavior {}", &bh.typ) + } + } + Ok(schema) + // FIXME: implements + } + + // + // Register a type alias and return the schema reference. + // + fn convert_type_alias(&mut self, alias: &TypeAlias) -> anyhow::Result> { + + let mut schema = self.convert_value_of(&alias.typ)?; + + // Add docs, etc. + if let ReferenceOr::Item(ref mut schema) = &mut schema { + schema.schema_data = convert_base_type(&alias.base); + } + + match &alias.variants { + None => {}, + Some(TypeAliasVariants::ExternalTag(_tag)) => { + // TODO: typed-keys: add an extension to identify it? + }, + Some(TypeAliasVariants::InternalTag(tag)) => { + if let ReferenceOr::Item(ref mut schema) = &mut schema { + // TODO: add tag.default_tag as an extension + schema.schema_data.discriminator = Some(Discriminator { + property_name: tag.tag.clone(), + mapping: Default::default(), + extensions: Default::default(), + }); + } + }, + }; + + Ok(schema) + } + + // + // Register an enumeration and return the schema reference. + // + fn convert_enum(&mut self, enumm: &Enum) -> anyhow::Result> { + + // TODO: enum.is_open + + let enum_values = enumm.members.iter().map(|m| { + Some(m.name.clone()) + }).collect::>(); + + let schema = ReferenceOr::Item(Schema { + schema_data: convert_base_type(&enumm.base), + schema_kind: SchemaKind::Type(Type::String(StringType { + format: Default::default(), + pattern: None, + enumeration: enum_values, + min_length: None, + max_length: None, + })), + }); + + Ok(schema) + } +} + +// +// Convert common type information. +// +fn convert_base_type(base: &clients_schema::BaseType) -> SchemaData { + let mut result = SchemaData::default(); + fill_data_with_base(&mut result, base); + return result; +} + +fn fill_schema_with_base(schema: &mut ReferenceOr, base: &clients_schema::BaseType) { + if let ReferenceOr::Item(ref mut schema) = schema { + fill_data_with_base(&mut schema.schema_data, base); + } +} + +fn fill_data_with_base(data: &mut SchemaData, base: &clients_schema::BaseType) { + // SchemaData { + // nullable: false, + // read_only: false, + // write_only: false, + // deprecated: false, + // external_docs: Default::default(), + // example: None, + // title: None, + // description: base.description.clone(), + // discriminator: None, + // default: None, + // extensions: Default::default(), + // } + + let external_docs = base.doc_url.as_ref().map(|url| ExternalDocumentation { + description: None, + url: url.clone(), + extensions: Default::default(), + }); + + data.external_docs = external_docs; + data.deprecated = base.deprecation.is_some(); + data.description = base.description.clone(); + // TODO: base.deprecation as extension + // TODO: base.spec_location as extension? + // TODO: base.doc_id as extension + // TODO: base.variant_name as extension? (used for external_variants) + // TODO: base.codegen_names as extension? +} + +fn fill_data_with_prop(data: &mut SchemaData, prop: &Property) { + let external_docs = prop.doc_url.as_ref().map(|url| ExternalDocumentation { + description: None, + url: url.clone(), + extensions: Default::default(), + }); + data.external_docs = external_docs; + data.deprecated = prop.deprecation.is_some(); + data.description = prop.description.clone(); + // TODO: prop.aliases as extensions + // TODO: prop.server_default as extension + // TODO: prop.availability as extension + // TODO: prop.doc_id as extension (new representation of since and stability) + // TODO: prop.es_quirk as extension? + // TODO: prop.codegen_name as extension? + // TODO: prop.deprecation as extension +} +