Skip to content

Commit

Permalink
[add] .array().of(schema)
Browse files Browse the repository at this point in the history
  • Loading branch information
igtm committed Oct 29, 2022
1 parent 2cc5a67 commit 7fb67f4
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 116 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "openapi-yup-generator"
version = "0.0.4"
version = "0.0.5"
edition = "2021"
authors = ["igtm"]
description = "CLI tool for generating yup definitions from openapi3.yaml"
Expand Down
94 changes: 78 additions & 16 deletions examples/yup-defs.js
Expand Up @@ -5,9 +5,20 @@
import { object, string, number, date, array, bool } from 'yup';

export const LightingSummary = object({
zones: array().optional(),
zoneStatus: array().optional(),
});
zones: array().of(object({
id: string().optional(),
name: string().optional(),
deviceId: number().integer().optional(),
deviceType: string().optional(),
zone: string().optional(),
})).optional(),
zoneStatus: array().of(object({
id: string().optional(),
name: string().optional(),
lastUpdate: date().optional(),
level: number().integer().optional(),
}).label('the status of the lighting zone.')).optional(),
}).label('ok');

export const LightingZone = object({
id: string().optional(),
Expand All @@ -22,28 +33,40 @@ export const LightingZoneStatus = object({
name: string().optional(),
lastUpdate: date().optional(),
level: number().integer().optional(),
});
}).label('the status of the lighting zone.');

export const TemperatureSummary = object({
zones: array().optional(),
zoneStatus: array().optional(),
});
zones: array().of(object({
id: number().integer().label('the unique identifier for the zone').required(),
name: string().required(),
inputPosition: number().integer().optional(),
outputPosition: number().integer().optional(),
zone: string().optional(),
}).label('a single temperature zone')).optional(),
zoneStatus: array().of(object({
id: string().label('the unique identifier for the zone').required(),
name: string().label('the name of the zone').optional(),
value: number().label('the temperature in the zone').required(),
units: string().label('the temperature units').optional(),
timestamp: date().label('the timestamp when the temperature was measured').required(),
}).label('status of a single zone')).optional(),
}).label('ok');

export const TemperatureZone = object({
id: number().integer().required(),
id: number().integer().label('the unique identifier for the zone').required(),
name: string().required(),
inputPosition: number().integer().optional(),
outputPosition: number().integer().optional(),
zone: string().optional(),
});
}).label('a single temperature zone');

export const TemperatueZoneStatus = object({
id: string().required(),
name: string().optional(),
value: number().required(),
units: string().optional(),
timestamp: date().required(),
});
id: string().label('the unique identifier for the zone').required(),
name: string().label('the name of the zone').optional(),
value: number().label('the temperature in the zone').required(),
units: string().label('the temperature units').optional(),
timestamp: date().label('the timestamp when the temperature was measured').required(),
}).label('status of a single zone');

export const ApiResponse = object({
code: number().integer().optional(),
Expand All @@ -63,7 +86,33 @@ export const DeviceState = object({
});

export const ForecastResponse = object({
values: array().optional(),
city: object({
id: number().integer().optional(),
name: string().optional(),
lat: number().optional(),
lon: number().optional(),
country: string().optional(),
}).optional(),
values: array().of(object({
date: date().optional(),
pressure: number().optional(),
humidity: number().integer().optional(),
windSpeed: number().optional(),
clouds: number().integer().optional(),
temperature: object({
low: number().optional(),
high: number().optional(),
morning: number().optional(),
day: number().optional(),
evening: number().optional(),
night: number().optional(),
}).optional(),
weather: object({
summary: string().optional(),
description: string().optional(),
icon: string().optional(),
}).optional(),
})).optional(),
});

export const Forecast = object({
Expand All @@ -72,6 +121,19 @@ export const Forecast = object({
humidity: number().integer().optional(),
windSpeed: number().optional(),
clouds: number().integer().optional(),
temperature: object({
low: number().optional(),
high: number().optional(),
morning: number().optional(),
day: number().optional(),
evening: number().optional(),
night: number().optional(),
}).optional(),
weather: object({
summary: string().optional(),
description: string().optional(),
icon: string().optional(),
}).optional(),
});

export const City = object({
Expand Down
202 changes: 104 additions & 98 deletions src/main.rs
Expand Up @@ -92,131 +92,137 @@ fn write_yup_defs(s: OpenAPI, config: &JsonObject, mut out_file: File) {

for a in &s.components {
for (schema_name, or) in a.schemas.iter() {
let scheme = resolve(&s, or).unwrap();
let schema = resolve(&s, or).unwrap();

if let SchemaKind::Type(any_schema_type) = &scheme.schema_kind {
if let Type::Object(any_schema_object_type) = any_schema_type {
str.push_str(&format!("export const {} = ", schema_name.to_case(Case::UpperCamel)));

str.push_str(&format!("export const {} = object({{\n", schema_name.to_case(Case::UpperCamel)));
str.push_str(&get_str_from_schema(&s, &config, &schema, indent_str_curry("".to_owned())));

str.push_str(&write_object_type(&s, config, any_schema_object_type, indent_str_curry(FIELD_INDENTS.to_owned())));

str.push_str(&format!("}});\n\n"));
}
}
str.push_str(&format!(";\n\n"));
}
}

out_file.write_all(str.as_bytes()).unwrap();

}

fn write_object_type(s: &OpenAPI, config: &JsonObject, object_type: &ObjectType, indent_str: impl Fn(String) -> String) -> String {
fn get_str_from_schema(s: &OpenAPI, config: &JsonObject, schema: &Schema, indent_str: impl Fn(String) -> String) -> String {
let mut str = "".to_owned();

for (prop_name, p) in object_type.properties.iter() {
if let SchemaKind::Type(any_schema_type2) = &schema.schema_kind {

match any_schema_type2 {
Type::Array(x) => {
str.push_str( "array()");
// of
if let Some(r) = &x.items {
if let Some(array_schema) = resolve(s, &r.to_owned().unbox()) {
str.push_str( ".of(");
str.push_str(&get_str_from_schema(&s, &config, &array_schema, indent_str));
str.push_str( ")");
}
}
// min/max
if let Some(minimum) = x.min_items {
str.push_str(&format!(".min({})", minimum));
}
if let Some(maximum) = x.max_items {
str.push_str(&format!(".max({})", maximum));
}
},
Type::Boolean{} => {
str.push_str("bool()");
},
Type::Integer(x) => {
str.push_str("number().integer()");
// min/max
if let Some(minimum) = x.minimum {
str.push_str(&format!(".min({})", minimum));
}
if let Some(maximum) = x.maximum {
str.push_str(&format!(".max({})", maximum));
}
},
Type::Number(x) => {
str.push_str("number()");
// min/max
if let Some(minimum) = x.minimum {
str.push_str(&format!(".min({})", minimum));
}
if let Some(maximum) = x.maximum {
str.push_str(&format!(".max({})", maximum));
}
},
Type::Object(x) => {
str.push_str("object({\n");
for (prop_name, p) in x.properties.iter() {

if let Some(any_schema_type2_item) = resolve(&s, &p.to_owned().unbox()) {
let nested_indent_str = &indent_str_curry(indent_str(FIELD_INDENTS.to_owned()));

if let SchemaKind::Type(any_schema_type2) = &any_schema_type2_item.schema_kind {
// prop
str.push_str(&nested_indent_str(format!("{}: ", prop_name.to_case(Case::Camel))));

if let Some(any_schema_type2_item) = resolve(&s, &p.to_owned().unbox()) {
str.push_str(&get_str_from_schema(&s, &config, &any_schema_type2_item, nested_indent_str));
}

match any_schema_type2 {
Type::Array(x) => {
str.push_str(&indent_str(format!("{}: {}", prop_name.to_case(Case::Camel), "array()")));
// TODO: .of(scheme)
// min/max
if let Some(minimum) = x.min_items {
str.push_str(&format!(".min({})", minimum));
}
if let Some(maximum) = x.max_items {
str.push_str(&format!(".max({})", maximum));
}
},
Type::Boolean{} => {
str.push_str(&indent_str(format!("{}: {}", prop_name.to_case(Case::Camel), "bool()")));
},
Type::Integer(x) => {
str.push_str(&indent_str(format!("{}: {}", prop_name.to_case(Case::Camel), "number().integer()")));
// min/max
if let Some(minimum) = x.minimum {
str.push_str(&format!(".min({})", minimum));
}
if let Some(maximum) = x.maximum {
str.push_str(&format!(".max({})", maximum));
}
},
Type::Number(x) => {
str.push_str(&indent_str(format!("{}: {}", prop_name.to_case(Case::Camel), "number()")));
// min/max
if let Some(minimum) = x.minimum {
str.push_str(&format!(".min({})", minimum));
}
if let Some(maximum) = x.maximum {
str.push_str(&format!(".max({})", maximum));
}
},
Type::Object(x) => {
str.push_str(&indent_str(format!("{}: {}", prop_name.to_case(Case::Camel), "object({\n")));
str.push_str(&write_object_type(&s, config, x, indent_str_curry(indent_str(FIELD_INDENTS.to_owned()))));
str.push_str(&indent_str("})".to_owned()));
},
Type::String(x) => {
let mut type_name = "string".to_owned();
// type
if let VariantOrUnknownOrEmpty::Item(fmt_item) = &x.format {
match fmt_item {
StringFormat::Date | StringFormat::DateTime => {
type_name = "date".to_owned();
},
_ => {},
}
}
str.push_str(&indent_str(format!("{}: {}()", prop_name.to_case(Case::Camel), type_name)));
// min/max
if let Some(minimum) = x.min_length {
str.push_str(&format!(".min({})", minimum));
}
if let Some(maximum) = x.max_length {
str.push_str(&format!(".max({})", maximum));
}
// matches (from pattern)
if let Some(pattern) = &x.pattern {
str.push_str(&format!(".matches(new RegExp(\"{}\"))", pattern));
}
// required/optional
if x.required.iter().any(|pn| pn == prop_name) {
str.push_str(".required()");
} else {
str.push_str(".optional()");
}

// format
if let VariantOrUnknownOrEmpty::Unknown(fmt_name) = &x.format {
if fmt_name == "email" {
str.push_str(&format!(".{}()", fmt_name));
}
}
},
// end
str.push_str(",\n");
}

// [optional] label (from description)
if let Some(_) = config.get_boolean("description_as_label") {
if let Some(description) = &any_schema_type2_item.schema_data.description {
str.push_str(&format!(".label('{}')", description));
str.push_str(&indent_str("})".to_owned()));
},
Type::String(x) => {
let mut type_name = "string".to_owned();
// type
if let VariantOrUnknownOrEmpty::Item(fmt_item) = &x.format {
match fmt_item {
StringFormat::Date | StringFormat::DateTime => {
type_name = "date".to_owned();
},
_ => {},
}
}
str.push_str(&format!("{}()", type_name));
// min/max
if let Some(minimum) = x.min_length {
str.push_str(&format!(".min({})", minimum));
}
if let Some(maximum) = x.max_length {
str.push_str(&format!(".max({})", maximum));
}
// matches (from pattern)
if let Some(pattern) = &x.pattern {
str.push_str(&format!(".matches(new RegExp(\"{}\"))", pattern));
}

// required/optional
if object_type.required.iter().any(|pn| pn == prop_name) {
str.push_str(".required()");
} else {
str.push_str(".optional()");
// format
if let VariantOrUnknownOrEmpty::Unknown(fmt_name) = &x.format {
if fmt_name == "email" {
str.push_str(&format!(".{}()", fmt_name));
}
}
},
}

// end
str.push_str(",\n");
// [optional] label (from description)
if let Some(_) = config.get_boolean("description_as_label") {
if let Some(description) = &schema.schema_data.description {
str.push_str(&format!(".label('{}')", description));
}
}

}

return str;
}


static RE_REF: Lazy<Regex> = Lazy::new(|| {
Regex::new("^(?P<source>[^#]*)#/components/(?P<type>[^/]+)/(?P<name>.+)$").unwrap()
});
Expand All @@ -243,6 +249,6 @@ fn resolve<'a>(s: &'a OpenAPI, ri: &'a ReferenceOr<Schema>) -> Option<&'a Schema

fn indent_str_curry(indent: String) -> impl Fn(String) -> String {
move |str: String| {
format!("{}{}", indent, str)
format!("{}{}", indent, str)
}
}
}

0 comments on commit 7fb67f4

Please sign in to comment.