use crate::{Container, Member, Output};
use anyhow::{bail, Result};
use heck::ToUpperCamelCase;
use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::{
JSONSchemaProps, JSONSchemaPropsOrArray, JSONSchemaPropsOrBool, JSON,
};
use std::collections::{BTreeMap, HashMap};
const IGNORED_KEYS: [&str; 3] = ["metadata", "apiVersion", "kind"];
pub fn analyze(schema: JSONSchemaProps, kind: &str) -> Result<Output> {
let mut res = vec![];
analyze_(&schema, "", kind, 0, &mut res)?;
Ok(Output(res))
}
fn analyze_(
schema: &JSONSchemaProps,
current: &str,
stack: &str,
level: u8,
results: &mut Vec<Container>,
) -> Result<()> {
let props = schema.properties.clone().unwrap_or_default();
let mut array_recurse_level: HashMap<String, u8> = Default::default();
if schema.type_.clone().unwrap_or_default() == "object" {
if let Some(JSONSchemaPropsOrBool::Schema(s)) = schema.additional_properties.as_ref() {
let dict_type = s.type_.clone().unwrap_or_default();
if let Some(extra_props) = &s.properties {
debug!("Generating map struct for {} (under {})", current, stack);
let c = extract_container(extra_props, stack, &mut array_recurse_level, level, schema)?;
results.push(c);
} else if !dict_type.is_empty() {
warn!("not generating type {} - using {} map", current, dict_type);
return Ok(()); }
} else {
debug!("Generating struct for {} (under {})", current, stack);
if props.is_empty() && schema.x_kubernetes_preserve_unknown_fields.unwrap_or(false) {
warn!("not generating type {} - using BTreeMap", current);
return Ok(());
}
let c = extract_container(&props, stack, &mut array_recurse_level, level, schema)?;
results.push(c);
}
}
let extras = if let Some(JSONSchemaPropsOrBool::Schema(s)) = schema.additional_properties.as_ref() {
let extra_props = s.properties.clone().unwrap_or_default();
find_containers(&extra_props, stack, &mut array_recurse_level, level, schema)?
} else {
find_containers(&props, stack, &mut array_recurse_level, level, schema)?
};
results.extend(extras);
Ok(())
}
fn find_containers(
props: &BTreeMap<String, JSONSchemaProps>,
stack: &str,
array_recurse_level: &mut HashMap<String, u8>,
level: u8,
schema: &JSONSchemaProps,
) -> Result<Vec<Container>> {
let mut results = vec![];
for (key, value) in props {
if level == 0 && IGNORED_KEYS.contains(&(key.as_ref())) {
debug!("not recursing into ignored {}", key); continue;
}
let next_key = key.to_upper_camel_case();
let next_stack = format!("{}{}", stack, next_key);
let value_type = value.type_.clone().unwrap_or_default();
match value_type.as_ref() {
"object" => {
let mut handled_inner = false;
if let Some(JSONSchemaPropsOrBool::Schema(s)) = &value.additional_properties {
let dict_type = s.type_.clone().unwrap_or_default();
if dict_type == "array" {
if let Some(JSONSchemaPropsOrArray::Schema(items)) = &s.as_ref().items {
debug!("..recursing into object member {}", key);
analyze_(items, &next_key, &next_stack, level + 1, &mut results)?;
handled_inner = true;
}
}
}
if !handled_inner {
analyze_(value, &next_key, &next_stack, level + 1, &mut results)?;
}
}
"array" => {
if let Some(recurse) = array_recurse_level.get(key).cloned() {
let mut inner = value.clone();
for _i in 0..recurse {
debug!("..recursing into props for {}", key);
if let Some(sub) = inner.items {
match sub {
JSONSchemaPropsOrArray::Schema(s) => {
inner = *s.clone();
}
_ => bail!("only handling single type in arrays"),
}
} else {
bail!("could not recurse into vec");
}
}
analyze_(&inner, &next_key, &next_stack, level + 1, &mut results)?;
}
}
"" => {
if value.x_kubernetes_int_or_string.is_some() {
debug!("..not recursing into IntOrString {}", key)
} else {
debug!("..not recursing into unknown empty type {}", key)
}
}
x => {
if let Some(en) = &value.enum_ {
let new_result = analyze_enum_properties(en, &next_stack, level, schema)?;
results.push(new_result);
} else {
debug!("..not recursing into {} ('{}' is not a container)", key, x)
}
}
}
}
Ok(results)
}
fn analyze_enum_properties(
items: &Vec<JSON>,
stack: &str,
level: u8,
schema: &JSONSchemaProps,
) -> Result<Container, anyhow::Error> {
let mut members = vec![];
debug!("analyzing enum {}", serde_json::to_string(&schema).unwrap());
for en in items {
debug!("got enum {:?}", en);
let name = match &en.0 {
serde_json::Value::String(name) => name.to_string(),
serde_json::Value::Number(val) => {
if !val.is_u64() {
bail!("enum member cannot have signed/floating discriminants");
}
val.to_string()
}
_ => bail!("not handling non-string/int enum outside oneOf block"),
};
let rust_type = "".to_string();
let member_doc = None;
debug!("with enum member {}", name);
members.push(Member {
type_: rust_type,
name: name.to_string(),
serde_annot: vec![],
extra_annot: vec![],
docs: member_doc,
})
}
Ok(Container {
name: stack.to_string(),
members,
level,
docs: schema.description.clone(),
is_enum: true,
})
}
fn extract_container(
props: &BTreeMap<String, JSONSchemaProps>,
stack: &str,
array_recurse_level: &mut HashMap<String, u8>,
level: u8,
schema: &JSONSchemaProps,
) -> Result<Container, anyhow::Error> {
let mut members = vec![];
let reqs = schema.required.clone().unwrap_or_default();
for (key, value) in props {
let value_type = value.type_.clone().unwrap_or_default();
let rust_type = match value_type.as_ref() {
"object" => {
let mut dict_key = None;
if let Some(additional) = &value.additional_properties {
dict_key = resolve_additional_properties(additional, stack, key, value)?;
} else if value.properties.is_none()
&& value.x_kubernetes_preserve_unknown_fields.unwrap_or(false)
{
dict_key = Some("serde_json::Value".into());
}
if let Some(dict) = dict_key {
format!("BTreeMap<String, {}>", dict)
} else {
format!("{}{}", stack, key.to_upper_camel_case())
}
}
"string" => {
if let Some(_en) = &value.enum_ {
trace!("got enum string: {}", serde_json::to_string(&schema).unwrap());
format!("{}{}", stack, key.to_upper_camel_case())
} else {
"String".to_string()
}
}
"boolean" => "bool".to_string(),
"date" => extract_date_type(value)?,
"number" => extract_number_type(value)?,
"integer" => extract_integer_type(value)?,
"array" => {
let (array_type, recurse_level) = array_recurse_for_type(value, stack, key, 1)?;
trace!("got array {} for {} in level {}", array_type, key, recurse_level);
array_recurse_level.insert(key.clone(), recurse_level);
array_type
}
"" => {
if value.x_kubernetes_int_or_string.is_some() {
"IntOrString".into()
} else if value.x_kubernetes_preserve_unknown_fields == Some(true) {
"HashMap<String, serde_json::Value>".into()
} else {
bail!("unknown empty dict type for {}", key)
}
}
x => bail!("unknown type {}", x),
};
let member_doc = value.description.clone();
if reqs.contains(key) {
debug!("with required member {} of type {}", key, &rust_type);
members.push(Member {
type_: rust_type,
name: key.to_string(),
serde_annot: vec![],
extra_annot: vec![],
docs: member_doc,
})
} else {
debug!("with optional member {} of type {}", key, rust_type);
members.push(Member {
type_: format!("Option<{}>", rust_type),
name: key.to_string(),
serde_annot: vec![
"default".into(),
"skip_serializing_if = \"Option::is_none\"".into(),
],
extra_annot: vec![],
docs: member_doc,
})
}
}
Ok(Container {
name: stack.to_string(),
members,
level,
docs: schema.description.clone(),
is_enum: false,
})
}
fn resolve_additional_properties(
additional: &JSONSchemaPropsOrBool,
stack: &str,
key: &str,
value: &JSONSchemaProps,
) -> Result<Option<String>, anyhow::Error> {
debug!("got additional: {}", serde_json::to_string(&additional)?);
let JSONSchemaPropsOrBool::Schema(s) = additional else {
return Ok(None);
};
let dict_type = s.type_.clone().unwrap_or_default();
let dict_key = match dict_type.as_ref() {
"string" => Some("String".into()),
"array" => {
let mut simple_inner = None;
if let Some(JSONSchemaPropsOrArray::Schema(ix)) = &s.items {
simple_inner = ix.type_.clone();
debug!("additional simple inner type: {:?}", simple_inner);
}
match simple_inner.as_deref() {
Some("string") => Some("String".into()),
Some("integer") => Some(extract_integer_type(s)?),
Some("date") => Some(extract_date_type(value)?),
Some("") => {
if s.x_kubernetes_int_or_string.is_some() {
Some("IntOrString".into())
} else {
bail!("unknown inner empty dict type for {}", key)
}
}
Some("object") => Some(format!("{}{}", stack, key.to_upper_camel_case())),
None => Some(format!("{}{}", stack, key.to_upper_camel_case())),
Some(x) => bail!("unknown inner empty dict type {} for {}", x, key),
}
}
"object" => {
Some(format!("{}{}", stack, key.to_upper_camel_case()))
}
"" => {
if s.x_kubernetes_int_or_string.is_some() {
Some("IntOrString".into())
} else {
bail!("unknown empty dict type for {}", key)
}
}
"boolean" => Some("bool".to_string()),
"integer" => Some(extract_integer_type(s)?),
x => Some(x.to_upper_camel_case()), };
Ok(dict_key)
}
fn array_recurse_for_type(
value: &JSONSchemaProps,
stack: &str,
key: &str,
level: u8,
) -> Result<(String, u8)> {
if let Some(items) = &value.items {
match items {
JSONSchemaPropsOrArray::Schema(s) => {
if s.type_.is_none() && s.x_kubernetes_preserve_unknown_fields == Some(true) {
return Ok(("Vec<HashMap<String, serde_json::Value>>".into(), level));
}
let inner_array_type = s.type_.clone().unwrap_or_default();
return match inner_array_type.as_ref() {
"object" => {
let mut dict_value = None;
if let Some(additional) = &s.additional_properties {
dict_value = resolve_additional_properties(additional, stack, key, s)?;
}
let vec_value = if let Some(dict_value) = dict_value {
format!("BTreeMap<String, {dict_value}>")
} else {
let structsuffix = key.to_upper_camel_case();
format!("{stack}{structsuffix}")
};
Ok((format!("Vec<{}>", vec_value), level))
}
"string" => Ok(("Vec<String>".into(), level)),
"boolean" => Ok(("Vec<bool>".into(), level)),
"date" => Ok((format!("Vec<{}>", extract_date_type(value)?), level)),
"number" => Ok((format!("Vec<{}>", extract_number_type(value)?), level)),
"integer" => Ok((format!("Vec<{}>", extract_integer_type(value)?), level)),
"array" => Ok(array_recurse_for_type(s, stack, key, level + 1)?),
unknown => {
bail!("unsupported recursive array type \"{unknown}\" for {key}")
}
};
}
_ => bail!("only support single schema in array {}", key),
}
} else {
bail!("missing items in array type")
}
}
fn extract_date_type(value: &JSONSchemaProps) -> Result<String> {
Ok(if let Some(f) = &value.format {
match f.as_ref() {
"date" => "NaiveDate".to_string(),
"date-time" => "DateTime<Utc>".to_string(),
x => {
bail!("unknown date {}", x);
}
}
} else {
"String".to_string()
})
}
fn extract_number_type(value: &JSONSchemaProps) -> Result<String> {
Ok(if let Some(f) = &value.format {
match f.as_ref() {
"float" => "f32".to_string(),
"double" => "f64".to_string(),
x => {
bail!("unknown number {}", x);
}
}
} else {
"f64".to_string()
})
}
fn extract_integer_type(value: &JSONSchemaProps) -> Result<String> {
Ok(if let Some(f) = &value.format {
match f.as_ref() {
"int8" => "i8".to_string(),
"int16" => "i16".to_string(),
"int32" => "i32".to_string(),
"int64" => "i64".to_string(),
"int128" => "i128".to_string(),
"uint8" => "u8".to_string(),
"uint16" => "u16".to_string(),
"uint32" => "u32".to_string(),
"uint64" => "u64".to_string(),
"uint128" => "u128".to_string(),
x => {
bail!("unknown integer {}", x);
}
}
} else {
"i64".to_string()
})
}
#[cfg(test)]
mod test {
use crate::analyze;
use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::JSONSchemaProps;
use std::sync::Once;
static START: Once = Once::new();
fn init() {
START.call_once(|| {
env_logger::init();
});
}
#[test]
fn map_of_struct() {
init();
let schema_str = r#"
description: AgentStatus defines the observed state of Agent
properties:
validationsInfo:
additionalProperties:
items:
properties:
id:
type: string
message:
type: string
status:
type: string
required:
- id
- message
- status
type: object
type: array
description: ValidationsInfo is a JSON-formatted string containing
the validation results for each validation id grouped by category
(network, hosts-data, etc.)
type: object
type: object
"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "Agent").unwrap().0;
let root = &structs[0];
assert_eq!(root.name, "Agent");
assert_eq!(root.level, 0);
let map = &root.members[0];
assert_eq!(map.name, "validationsInfo");
assert_eq!(map.type_, "Option<BTreeMap<String, AgentValidationsInfo>>");
let other = &structs[1];
assert_eq!(other.name, "AgentValidationsInfo");
assert_eq!(other.level, 1);
assert_eq!(other.members[0].name, "id");
assert_eq!(other.members[0].type_, "String");
assert_eq!(other.members[1].name, "message");
assert_eq!(other.members[1].type_, "String");
assert_eq!(other.members[2].name, "status");
assert_eq!(other.members[2].type_, "String");
}
#[test]
fn empty_preserve_unknown_fields() {
init();
let schema_str = r#"
description: |-
Identifies servers in the same namespace for which this authorization applies.
required:
- selector
properties:
selector:
description: A label query over servers on which this authorization
applies.
required:
- matchLabels
properties:
matchLabels:
type: object
x-kubernetes-preserve-unknown-fields: true
type: object
type: object
"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "Server").unwrap().0;
let root = &structs[0];
assert_eq!(root.name, "Server");
assert_eq!(root.level, 0);
let root_member = &root.members[0];
assert_eq!(root_member.name, "selector");
assert_eq!(root_member.type_, "ServerSelector");
let server_selector = &structs[1];
assert_eq!(server_selector.name, "ServerSelector");
assert_eq!(server_selector.level, 1);
let match_labels = &server_selector.members[0];
assert_eq!(match_labels.name, "matchLabels");
assert_eq!(match_labels.type_, "BTreeMap<String, serde_json::Value>");
}
#[test]
fn int_or_string() {
init();
let schema_str = r#"
properties:
port:
description: A port name or number. Must exist in a pod spec.
x-kubernetes-int-or-string: true
required:
- port
type: object
"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "Server").unwrap().0;
let root = &structs[0];
assert_eq!(root.name, "Server");
let member = &root.members[0];
assert_eq!(member.name, "port");
assert_eq!(member.type_, "IntOrString");
assert!(root.uses_int_or_string());
}
#[test]
fn boolean_in_additionals() {
init();
let schema_str = r#"
properties:
options:
additionalProperties:
type: boolean
type: object
patch:
type: string
type: object
"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "Options").unwrap().0;
println!("got {:?}", structs);
let root = &structs[0];
assert_eq!(root.name, "Options");
assert_eq!(root.level, 0);
assert_eq!(&root.members[0].name, "options");
assert_eq!(&root.members[0].type_, "Option<BTreeMap<String, bool>>");
}
#[test]
fn enum_string() {
init();
let schema_str = r#"
properties:
operator:
enum:
- In
- NotIn
- Exists
- DoesNotExist
type: string
required:
- operator
type: object
"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "MatchExpressions").unwrap().0;
println!("got {:?}", structs);
let root = &structs[0];
assert_eq!(root.name, "MatchExpressions");
assert_eq!(root.level, 0);
assert_eq!(&root.members[0].name, "operator");
assert_eq!(&root.members[0].type_, "MatchExpressionsOperator");
let op = &structs[1];
assert!(op.is_enum);
assert_eq!(op.name, "MatchExpressionsOperator");
assert_eq!(&op.members[0].name, "In");
assert_eq!(&op.members[0].type_, "");
assert_eq!(&op.members[1].name, "NotIn");
assert_eq!(&op.members[1].type_, "");
assert_eq!(&op.members[2].name, "Exists");
assert_eq!(&op.members[2].type_, "");
assert_eq!(&op.members[3].name, "DoesNotExist");
assert_eq!(&op.members[3].type_, "");
}
#[test]
fn enum_string_within_container() {
init();
let schema_str = r#"
description: Endpoint
properties:
relabelings:
items:
properties:
action:
default: replace
enum:
- replace
- keep
- drop
- hashmod
- labelmap
- labeldrop
- labelkeep
type: string
modulus:
format: int64
type: integer
type: object
type: array
type: object
"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "Endpoint").unwrap().0;
println!("got {:?}", structs);
let root = &structs[0];
assert_eq!(root.name, "Endpoint");
assert_eq!(root.level, 0);
assert_eq!(root.is_enum, false);
assert_eq!(&root.members[0].name, "relabelings");
assert_eq!(&root.members[0].type_, "Option<Vec<EndpointRelabelings>>");
let rel = &structs[1];
assert_eq!(rel.name, "EndpointRelabelings");
assert_eq!(rel.is_enum, false);
assert_eq!(&rel.members[0].name, "action");
assert_eq!(&rel.members[0].type_, "Option<EndpointRelabelingsAction>");
let act = &structs[2];
assert_eq!(act.name, "EndpointRelabelingsAction");
assert_eq!(act.is_enum, true);
assert_eq!(&act.members[0].name, "replace");
assert_eq!(&act.members[0].type_, "");
assert_eq!(&act.members[1].name, "keep");
assert_eq!(&act.members[1].type_, "");
assert_eq!(&act.members[2].name, "drop");
assert_eq!(&act.members[2].type_, "");
assert_eq!(&act.members[3].name, "hashmod");
assert_eq!(&act.members[3].type_, "");
}
#[test]
#[ignore] fn enum_oneof() {
init();
let schema_str = r#"
description: "Auto-generated derived type for ServerSpec via `CustomResource`"
properties:
spec:
properties:
podSelector:
oneOf:
- required:
- matchExpressions
- required:
- matchLabels
properties:
matchExpressions:
items:
properties:
key:
type: string
operator:
enum:
- In
- NotIn
- Exists
- DoesNotExists
type: string
values:
items:
type: string
nullable: true
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
type: object
type: object
required:
- podSelector
type: object
required:
- spec
title: Server
type: object"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "ServerSpec").unwrap().0;
println!("got {:?}", structs);
let root = &structs[0];
assert_eq!(root.name, "ServerSpec");
assert_eq!(root.level, 0);
let member = &root.members[0];
assert_eq!(member.name, "pod_selector");
assert_eq!(member.type_, "ServerPodSelector");
let ps = &structs[1]; assert_eq!(ps.name, "ServerPodSelector");
assert_eq!(ps.level, 1);
assert_eq!(&ps.members[0].name, "MatchExpressions");
assert_eq!(&ps.members[0].type_, "Vec<ServerPodSelectorMatchExpressions");
assert_eq!(&ps.members[1].name, "MatchLabels");
assert_eq!(&ps.members[1].type_, "BTreeMap<String, String>");
let me = &structs[2];
assert_eq!(me.name, "ServerPodSelectorMatchExpressions");
assert_eq!(me.level, 2);
assert_eq!(&me.members[0].name, "key");
assert_eq!(&me.members[0].type_, "String");
assert_eq!(&me.members[1].name, "operator");
assert_eq!(&me.members[1].type_, "ServerPodSelectorMatchExpressionsOperator");
assert_eq!(&me.members[2].name, "values");
assert_eq!(&me.members[2].type_, " Option<Vec<String>>");
let op = &structs[3];
assert_eq!(op.name, "ServerPodSelectorMatchExpressionsOperator");
assert_eq!(op.level, 3);
assert_eq!(&op.members[0].name, "In");
assert_eq!(&op.members[1].name, "In");
assert_eq!(&op.members[2].name, "In");
assert_eq!(&op.members[3].name, "In");
}
#[test]
fn service_monitor_params() {
init();
let schema_str = r#"
properties:
endpoints:
items:
description: Endpoint defines a scrapeable endpoint serving Prometheus
metrics.
properties:
params:
additionalProperties:
items:
type: string
type: array
description: Optional HTTP URL parameters
type: object
type: object
type: array
required:
- endpoints
type: object
"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "ServiceMonitor").unwrap().0;
println!("got {:?}", structs);
let root = &structs[0];
assert_eq!(root.name, "ServiceMonitor");
assert_eq!(root.level, 0);
let member = &root.members[0];
assert_eq!(member.name, "endpoints");
assert_eq!(member.type_, "Vec<ServiceMonitorEndpoints>");
let eps = &structs[1];
assert_eq!(eps.name, "ServiceMonitorEndpoints");
assert_eq!(eps.level, 1);
let member = &eps.members[0];
assert_eq!(member.name, "params");
assert_eq!(member.type_, "Option<BTreeMap<String, String>>");
}
#[test]
fn integer_handling_in_maps() {
init();
let schema_str = r#"
properties:
distribute:
description: 'Optional: only one of distribute, failover
or failoverPriority can be set.'
items:
properties:
from:
description: Originating locality, '/' separated
type: string
to:
additionalProperties:
type: integer
format: int32
description: Map of upstream localities to traffic
distribution weights.
type: object
type: object
type: array
type: object
"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "DestinationRule").unwrap().0;
let root = &structs[0];
assert_eq!(root.name, "DestinationRule");
let distmember = &root.members[0];
assert_eq!(distmember.name, "distribute");
assert_eq!(distmember.type_, "Option<Vec<DestinationRuleDistribute>>");
let ruledist = &structs[1];
assert_eq!(ruledist.name, "DestinationRuleDistribute");
let from = &ruledist.members[0];
let to = &ruledist.members[1];
assert_eq!(from.name, "from");
assert_eq!(to.name, "to");
assert_eq!(from.type_, "Option<String>");
assert_eq!(to.type_, "Option<BTreeMap<String, i32>>");
}
#[test]
#[ignore] fn top_level_enum_with_integers() {
init();
let schema_str = r#"
default: 302
enum:
- 301
- 302
type: integer
"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
println!("got schema {}", serde_yaml::to_string(&schema).unwrap());
let structs = analyze(schema, "StatusCode").unwrap().0;
println!("got {:?}", structs);
let root = &structs[0];
assert_eq!(root.name, "StatusCode");
assert_eq!(root.level, 0);
assert_eq!(root.is_enum, true);
assert_eq!(&root.members[0].name, "301");
assert_eq!(&root.members[0].name, "302");
assert_eq!(&root.members[0].type_, "");
}
#[test]
fn array_of_preserve_unknown_objects() {
init();
let schema_str = r#"
properties:
patchesStrategicMerge:
description: Strategic merge patches, defined as inline YAML objects.
items:
x-kubernetes-preserve-unknown-fields: true
type: array
type: object
"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "KustomizationSpec").unwrap().0;
println!("got {:?}", structs);
let root = &structs[0];
assert_eq!(root.name, "KustomizationSpec");
assert_eq!(root.level, 0);
assert_eq!(root.is_enum, false);
assert_eq!(&root.members[0].name, "patchesStrategicMerge");
assert_eq!(
&root.members[0].type_,
"Option<Vec<HashMap<String, serde_json::Value>>>"
);
}
#[test]
fn nested_properties_in_additional_properties() {
init();
let schema_str = r#"
properties:
jwtTokensByRole:
additionalProperties:
description: JWTTokens represents a list of JWT tokens
properties:
items:
items:
properties:
exp:
format: int64
type: integer
iat:
format: int64
type: integer
id:
type: string
required:
- iat
type: object
type: array
type: object
type: object
type: object"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "AppProjectStatus").unwrap().0;
println!("got {:?}", structs);
let root = &structs[0];
assert_eq!(root.name, "AppProjectStatus");
assert_eq!(root.level, 0);
assert_eq!(root.is_enum, false);
assert_eq!(&root.members[0].name, "jwtTokensByRole");
assert_eq!(
&root.members[0].type_,
"Option<BTreeMap<String, AppProjectStatusJwtTokensByRole>>"
);
let role = &structs[1];
assert_eq!(role.level, 1);
assert_eq!(role.name, "AppProjectStatusJwtTokensByRole");
assert_eq!(&role.members[0].name, "items");
let items = &structs[2];
assert_eq!(items.level, 2);
assert_eq!(items.name, "AppProjectStatusJwtTokensByRoleItems");
assert_eq!(&items.members[0].name, "exp");
assert_eq!(&items.members[1].name, "iat");
assert_eq!(&items.members[2].name, "id");
}
#[test]
fn underscore_to_camel_case() {
init();
let schema_str = r#"
properties:
validations_info:
type: object
type: object
"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "Agent").unwrap().0;
let root = &structs[0];
assert_eq!(root.name, "Agent");
assert_eq!(root.level, 0);
let map = &root.members[0];
assert_eq!(map.name, "validations_info");
assert_eq!(map.type_, "Option<AgentValidationsInfo>");
let other = &structs[1];
assert_eq!(other.name, "AgentValidationsInfo");
assert_eq!(other.level, 1);
}
#[test]
fn skipped_type_as_map_nested_in_array() {
init();
let schema_str = r#"
properties:
records:
items:
additionalProperties:
type: string
type: object
type: array
type: object
"#;
let schema: JSONSchemaProps = serde_yaml::from_str(schema_str).unwrap();
let structs = analyze(schema, "Geoip").unwrap().0;
assert_eq!(structs.len(), 1);
assert_eq!(structs[0].members.len(), 1);
assert_eq!(structs[0].members[0].name, "records");
assert_eq!(
structs[0].members[0].type_,
"Option<Vec<BTreeMap<String, String>>>"
);
}
}