1use crate::{
2 api_model_config::Lang,
3 id::{prefix::IdPrefix, Id},
4 prelude::{shared::record_metadata::RecordMetadata, MongoStore, StringExt},
5 IntegrationOSError, InternalError,
6};
7use async_recursion::async_recursion;
8use bson::doc;
9use indexmap::IndexMap;
10use openapiv3::*;
11use semver::Version;
12use serde::{Deserialize, Serialize};
13use serde_json::{json, Map, Value};
14use std::{
15 collections::{HashMap, HashSet},
16 hash::Hash,
17 ops::Deref,
18};
19
20#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
21#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
22pub struct CommonModel {
23 #[serde(rename = "_id")]
24 pub id: Id,
25 pub name: String,
26 pub fields: Vec<Field>,
27 #[serde(default)]
28 pub sample: Value,
29 #[serde(default)]
30 pub primary: bool,
31 pub category: String,
32 #[serde(default)]
33 pub interface: HashMap<Lang, String>,
34 #[serde(flatten, default)]
35 pub record_metadata: RecordMetadata,
36}
37
38impl Hash for CommonModel {
39 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
40 self.id.hash(state);
41 }
42}
43
44pub enum TypeGenerationStrategy<'a> {
45 Cumulative {
49 visited_enums: &'a mut HashSet<Id>,
50 visited_common_models: &'a mut HashSet<Id>,
51 },
52 Unique,
55}
56
57#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
58pub struct UnsavedCommonModel {
59 pub name: String,
60 pub fields: Vec<Field<UnsavedCommonModel>>,
61 pub category: String,
62 #[serde(default)]
63 pub sample: Value,
64 #[serde(default)]
65 pub interface: HashMap<Lang, String>,
66 #[serde(default)]
67 pub primary: bool,
68}
69
70impl Default for CommonModel {
71 fn default() -> Self {
72 Self {
73 id: Id::new(IdPrefix::CommonModel, chrono::Utc::now()),
74 name: Default::default(),
75 fields: Default::default(),
76 sample: Default::default(),
77 primary: Default::default(),
78 category: Default::default(),
79 interface: Default::default(),
80 record_metadata: Default::default(),
81 }
82 }
83}
84
85#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Copy)]
86#[serde(rename_all = "kebab-case")]
87#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
88pub enum SchemaType {
89 Lax,
90 Strict,
91}
92
93#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
94#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
95pub struct Field<T = CommonModel> {
96 pub name: String,
97 #[serde(flatten)]
98 #[cfg_attr(feature = "dummy", dummy(default))]
99 pub datatype: DataType<T>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub description: Option<String>,
102 #[serde(default)]
103 pub required: bool,
104}
105
106impl Field {
107 fn is_expandable(&self) -> bool {
108 self.datatype.is_expandable()
109 }
110
111 fn is_primitive(&self) -> bool {
112 self.datatype.is_primitive()
113 }
114
115 fn is_enum_reference(&self) -> bool {
116 self.datatype.is_enum_reference()
117 }
118
119 fn is_enum_field(&self) -> bool {
120 self.datatype.is_enum_field()
121 }
122
123 fn as_rust_ref(&self) -> String {
124 format!(
125 "pub {}: Option<{}>",
126 replace_reserved_keyword(&self.name, Lang::Rust).snake_case(),
127 self.datatype.as_rust_ref(self.name.clone())
128 )
129 }
130
131 fn as_typescript_ref(&self) -> String {
132 format!(
133 "{}?: {}",
134 replace_reserved_keyword(&self.name, Lang::TypeScript).camel_case(),
135 self.datatype.as_typescript_ref(self.name.clone())
136 )
137 }
138
139 fn as_typescript_schema(&self, r#type: SchemaType) -> String {
140 format!(
141 "{}: {}",
142 replace_reserved_keyword(&self.name, Lang::TypeScript).camel_case(),
143 self.datatype
144 .as_typescript_schema(self.name.clone(), r#type)
145 )
146 }
147}
148
149#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
150#[serde(tag = "datatype")]
151pub enum DataType<T = CommonModel> {
152 #[default]
153 String,
154 Number,
155 Boolean,
156 Date,
157 Enum {
158 options: Option<Vec<String>>,
159 #[serde(default)]
160 reference: String,
161 },
162 Expandable(Expandable<T>),
163 Array {
164 #[serde(rename = "elementType")]
165 element_type: Box<DataType<T>>,
166 },
167 Unknown,
168}
169
170#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
171pub struct CommonEnum {
172 #[serde(rename = "_id")]
173 pub id: Id,
174 pub name: String,
175 pub options: Vec<String>,
176}
177
178fn replace_reserved_keyword(name: &str, lang: Lang) -> String {
179 match lang {
180 Lang::Rust => match name.to_lowercase().as_str() {
181 "type" => "r#type".to_owned(),
182 "enum" => "r#enum".to_owned(),
183 "struct" => "r#struct".to_owned(),
184 _ => name.to_owned(),
185 },
186 Lang::TypeScript => match name.to_lowercase().as_str() {
187 "type" => "type_".to_owned(),
188 "enum" => "enum_".to_owned(),
189 "interface" => "interface_".to_owned(),
190 _ => name.to_owned(),
191 },
192 _ => name.to_owned(),
193 }
194}
195
196impl CommonEnum {
197 pub fn as_rust_type(&self) -> String {
198 format!(
199 "pub enum {} {{ {} }}\n",
200 replace_reserved_keyword(&self.name, Lang::Rust)
201 .replace("::", "")
202 .pascal_case(),
203 self.options
204 .iter()
205 .map(|option| option.pascal_case())
206 .collect::<HashSet<String>>()
207 .into_iter()
208 .collect::<Vec<_>>()
209 .join(", ")
210 )
211 }
212
213 pub fn as_rust_schema(&self) -> String {
215 let name = replace_reserved_keyword(&self.name, Lang::Rust)
216 .replace("::", "")
217 .pascal_case();
218 let napi = format!("#[napi(string_enum = \"kebab-case\", js_name = {})]", name);
219 format!(
220 "{} pub enum {} {{ {} }}\n",
221 napi,
222 name,
223 self.options
224 .iter()
225 .map(|option| {
226 let option_name = option.pascal_case();
227 let option_value = if option.chars().all(char::is_uppercase) {
228 option.to_lowercase()
229 } else {
230 option.kebab_case()
231 };
232
233 let option_annotation = format!("#[napi(value = \"{}\")]", option_value);
234
235 format!("{} {}", option_annotation, option_name)
236 })
237 .collect::<HashSet<String>>()
238 .into_iter()
239 .collect::<Vec<_>>()
240 .join(", ")
241 )
242 }
243
244 pub fn as_typescript_type(&self) -> String {
245 format!(
247 "export const enum {} {{ {} }}\n",
248 replace_reserved_keyword(&self.name, Lang::TypeScript)
249 .replace("::", "")
250 .pascal_case(),
251 self.options
252 .iter()
253 .map(|option| {
254 let option_name = option.pascal_case();
255 let option_value = if option.chars().all(char::is_uppercase) {
256 option.to_lowercase()
257 } else {
258 option.kebab_case()
259 };
260
261 format!("{} = '{}'", option_name, option_value)
262 })
263 .collect::<HashSet<String>>()
264 .into_iter()
265 .collect::<Vec<_>>()
266 .join(", ")
267 )
268 }
269
270 pub fn as_typescript_schema(&self) -> String {
272 let name = replace_reserved_keyword(&self.name, Lang::TypeScript)
273 .replace("::", "")
274 .pascal_case();
275 let native_enum = format!(
276 "export enum {}Enum {{ {} }}\n",
277 name,
278 self.options
279 .iter()
280 .map(|option| {
281 let option_name = option.pascal_case();
282 let option_value = if option.chars().all(char::is_uppercase) {
283 option.to_lowercase()
284 } else {
285 option.kebab_case()
286 };
287
288 format!("{} = '{}'", option_name, option_value)
289 })
290 .collect::<HashSet<_>>()
291 .into_iter()
292 .collect::<Vec<_>>()
293 .join(", ")
294 );
295
296 let schema = format!(
297 "export const {} = Schema.Enums({}Enum)\n // __SEPARATOR__\n",
298 name, name
299 );
300
301 format!("{}\n{}", native_enum, schema)
302 }
303}
304
305impl DataType {
306 fn as_rust_ref(&self, e_name: String) -> String {
307 match self {
308 DataType::String => "String".into(),
309 DataType::Number => "f64".into(),
310 DataType::Boolean => "bool".into(),
311 DataType::Date => "String".into(),
312 DataType::Enum { reference, .. } => {
313 if reference.is_empty() {
314 e_name.pascal_case()
315 } else {
316 reference.into()
317 }
318 }
319 DataType::Expandable(expandable) => expandable.reference(),
320 DataType::Array { element_type } => {
321 let name = (*element_type).as_rust_ref(e_name);
322 format!("Vec<{}>", name)
323 }
324 DataType::Unknown => "serde_json::Value".into(),
325 }
326 }
327
328 fn as_typescript_ref(&self, enum_name: String) -> String {
329 match self {
330 DataType::String => "string".into(),
331 DataType::Number => "number".into(),
332 DataType::Boolean => "boolean".into(),
333 DataType::Date => "string".into(),
334 DataType::Enum { reference, .. } => {
335 if reference.is_empty() {
336 enum_name.pascal_case()
337 } else {
338 reference.into()
339 }
340 }
341 DataType::Expandable(expandable) => expandable.reference(),
342 DataType::Array { element_type } => {
343 let name = (*element_type).as_typescript_ref(enum_name);
344 format!("{}[]", name)
345 }
346 DataType::Unknown => "unknown".into(),
347 }
348 }
349
350 fn as_typescript_schema(&self, enum_name: String, r#type: SchemaType) -> String {
351 match self {
352 DataType::String => {
353 match r#type {
354 SchemaType::Lax => "Schema.optional(Schema.NullishOr(Schema.String))".into(),
355 SchemaType::Strict => "Schema.String".into()
356 }
357 },
358 DataType::Number => {
359 match r#type {
360 SchemaType::Lax => "Schema.optional(Schema.NullishOr(Schema.Number))".into(),
361 SchemaType::Strict => "Schema.Number".into()
362 } },
363 DataType::Boolean => {
364 match r#type {
365 SchemaType::Lax => "Schema.optional(Schema.NullishOr(Schema.Boolean))".into(),
366 SchemaType::Strict => "Schema.Boolean".into()
367 }
368
369 },
370 DataType::Date => {
371 match r#type {
372 SchemaType::Lax => "Schema.optional(Schema.NullishOr(Schema.String.pipe(Schema.filter((d) => !isNaN(new Date(d).getTime())))))".into(),
373 SchemaType::Strict => "Schema.String.pipe(Schema.filter((d) => !isNaN(new Date(d).getTime())))".into()
374 }
375 },
376 DataType::Enum { reference, .. } => {
377 match r#type {
378 SchemaType::Lax => {
379 if reference.is_empty() {
380 format!(
381 "Schema.optional(Schema.NullishOr({}))",
382 enum_name.pascal_case()
383 )
384 } else {
385 format!("Schema.optional(Schema.NullishOr({}))", reference)
386 }
387 },
388 SchemaType::Strict => {
389 if reference.is_empty() {
390 enum_name.pascal_case()
391 } else {
392 reference.into()
393 }
394 }
395 }
396
397 }
398 DataType::Expandable(expandable) => {
399 match r#type {
400 SchemaType::Lax => {
401 format!(
402 "Schema.optional(Schema.NullishOr({}))",
403 expandable.reference()
404 )
405 },
406 SchemaType::Strict => {
407 expandable.reference()
408 }
409 }
410 }
411 DataType::Array { element_type } => {
412 match r#type {
413 SchemaType::Lax => {
414 let name = (*element_type).as_typescript_schema(enum_name, r#type);
415
416 let refined = if name.starts_with("Schema.optional(") && name.ends_with(')') {
417 let without_optional = name.strip_prefix("Schema.optional(").unwrap_or(&name);
418
419 if without_optional.starts_with("Schema.NullishOr(") && without_optional.ends_with(')') {
420 let without_nullish = without_optional.strip_prefix("Schema.NullishOr(")
422 .unwrap_or(without_optional)
423 .strip_suffix(')')
424 .unwrap_or(without_optional);
425
426 without_nullish.strip_suffix(')').unwrap_or(without_nullish)
428 } else {
429 without_optional.strip_suffix(')').unwrap_or(without_optional)
431 }
432 } else {
433 &name
434 };
435
436
437 format!(
438 "Schema.optional(Schema.NullishOr(Schema.Array({})))",
439 refined
440 )
441 },
442 SchemaType::Strict => {
443 let name = (*element_type).as_typescript_schema(enum_name, r#type);
444 format!(
445 "Schema.NonEmptyArray({})",
446 name
447 )
448 }
449 }
450 }
451 DataType::Unknown => {
452 match r#type {
453 SchemaType::Lax => "Schema.optional(Schema.NullishOr(Schema.Unknown))".to_string(),
454 SchemaType::Strict => "Schema.Unknown".to_string()
455 }
456 }
457 }
458 }
459
460 pub fn schema(&self, format: Option<String>) -> ReferenceOr<Box<Schema>> {
461 match self {
462 DataType::String => ReferenceOr::Item(Box::new(Schema {
463 schema_data: Default::default(),
464 schema_kind: SchemaKind::Type(Type::String(StringType {
465 format: VariantOrUnknownOrEmpty::Unknown(format.unwrap_or_default()),
466 pattern: None,
467 ..Default::default()
468 })),
469 })),
470 DataType::Number => ReferenceOr::Item(Box::new(Schema {
471 schema_data: Default::default(),
472 schema_kind: SchemaKind::Type(Type::Number(NumberType {
473 format: VariantOrUnknownOrEmpty::Unknown(format.unwrap_or_default()),
474 ..Default::default()
475 })),
476 })),
477 DataType::Boolean => ReferenceOr::Item(Box::new(Schema {
478 schema_data: Default::default(),
479 schema_kind: SchemaKind::Type(Type::Boolean(BooleanType {
480 ..Default::default()
481 })),
482 })),
483 DataType::Date => ReferenceOr::Item(Box::new(Schema {
484 schema_data: Default::default(),
485 schema_kind: SchemaKind::Type(Type::String(StringType {
486 format: VariantOrUnknownOrEmpty::Unknown("date-time".to_string()),
487 ..Default::default()
488 })),
489 })),
490 DataType::Enum { options, reference } => match options {
491 Some(options) => ReferenceOr::Item(Box::new(Schema {
492 schema_data: Default::default(),
493 schema_kind: SchemaKind::Type(Type::String(StringType {
494 format: VariantOrUnknownOrEmpty::Unknown(format.unwrap_or_default()),
495 enumeration: options
496 .iter()
497 .map(|option| Some(option.to_owned()))
498 .collect(),
499 ..Default::default()
500 })),
501 })),
502 None => ReferenceOr::Reference {
503 reference: "#/components/schemas/".to_string() + reference,
504 },
505 },
506 DataType::Array { element_type } => ReferenceOr::Item(Box::new(Schema {
507 schema_data: Default::default(),
508 schema_kind: SchemaKind::Type(Type::Array(ArrayType {
509 items: Some(element_type.schema(format)),
510 min_items: None,
511 max_items: None,
512 unique_items: false,
513 })),
514 })),
515 DataType::Expandable(expandable) => match expandable {
516 Expandable::Expanded { model, .. } => ReferenceOr::Item(Box::new(Schema {
517 schema_data: Default::default(),
518 schema_kind: SchemaKind::Type(Type::Object(ObjectType {
519 properties: {
520 IndexMap::from_iter(
521 model
522 .fields
523 .iter()
524 .map(|field| (field.name.clone(), field.datatype.schema(None)))
525 .collect::<Vec<_>>(),
526 )
527 },
528 ..Default::default()
529 })),
530 })),
531 Expandable::Unexpanded { reference } => ReferenceOr::Reference {
532 reference: "#/components/schemas/".to_string() + reference,
533 },
534 _ => ReferenceOr::Item(Box::new(Schema {
535 schema_data: Default::default(),
536 schema_kind: SchemaKind::Type(Type::Object(Default::default())),
537 })),
538 },
539 DataType::Unknown => ReferenceOr::Item(Box::new(Schema {
540 schema_data: Default::default(),
541 schema_kind: SchemaKind::Type(Type::Object(Default::default())),
542 })),
543 }
544 }
545
546 fn is_enum_reference(&self) -> bool {
547 match self {
548 DataType::Enum { reference, .. } => !reference.is_empty(),
549 DataType::Array { element_type } => element_type.is_enum_reference(),
550 _ => false,
551 }
552 }
553
554 fn is_enum_field(&self) -> bool {
555 match self {
556 DataType::Enum { options, .. } => options.is_some(),
557 DataType::Array { element_type } => element_type.is_enum_field(),
558 _ => false,
559 }
560 }
561
562 fn is_expandable(&self) -> bool {
563 match self {
564 DataType::Expandable { .. } => true,
565 DataType::Array { element_type } => element_type.is_expandable(),
566 _ => false,
567 }
568 }
569
570 fn is_primitive(&self) -> bool {
571 match self {
572 DataType::String | DataType::Number | DataType::Boolean | DataType::Date => true,
573 DataType::Array { element_type } => element_type.is_primitive(),
574 _ => false,
575 }
576 }
577
578 pub fn to_name(&self) -> String {
579 match &self {
580 DataType::String => "String".to_owned(),
581 DataType::Number => "Number".to_owned(),
582 DataType::Boolean => "Boolean".to_owned(),
583 DataType::Date => "Date".to_owned(),
584 DataType::Enum { options, .. } => {
585 let options = options.as_ref().unwrap_or(&vec![]).join("|");
586 format!("Enum<{}>", options)
587 }
588 DataType::Expandable(expandable) => match expandable {
589 Expandable::Expanded { reference, .. } => {
590 format!("Expandable<{reference}>")
591 }
592 Expandable::Unexpanded { reference } => {
593 format!("Expandable<{reference}>")
594 }
595 Expandable::NotFound { reference } => format!("Expandable<{reference}>"),
596 },
597 DataType::Array { element_type } => {
598 let name = (*element_type).to_name();
599 format!("Array<{name}>")
600 }
601 DataType::Unknown => "unknown".into(),
602 }
603 }
604}
605
606#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
607#[serde(untagged)]
608pub enum Expandable<T = CommonModel> {
609 Expanded { reference: String, model: T },
610 Unexpanded { reference: String },
611 NotFound { reference: String },
612}
613
614impl<T> Expandable<T> {
615 pub fn reference(&self) -> String {
616 match self {
617 Expandable::Expanded { reference, .. } => reference.clone(),
618 Expandable::Unexpanded { reference } => reference.clone(),
619 Expandable::NotFound { reference } => reference.clone(),
620 }
621 }
622}
623
624impl From<UnsavedCommonModel> for CommonModel {
625 fn from(model: UnsavedCommonModel) -> Self {
626 Self {
627 id: Id::now(IdPrefix::CommonModel),
628 name: model.name,
629 fields: model.fields.into_iter().map(|f| f.into()).collect(),
630 sample: model.sample,
631 category: model.category,
632 primary: model.primary,
633 interface: model.interface,
634 record_metadata: Default::default(),
635 }
636 }
637}
638
639impl From<Field<UnsavedCommonModel>> for Field {
640 fn from(field: Field<UnsavedCommonModel>) -> Self {
641 Self {
642 name: field.name,
643 datatype: field.datatype.into(),
644 description: field.description,
645 required: field.required,
646 }
647 }
648}
649
650impl From<DataType<UnsavedCommonModel>> for DataType {
651 fn from(data_type: DataType<UnsavedCommonModel>) -> Self {
652 match data_type {
653 DataType::String => DataType::String,
654 DataType::Number => DataType::Number,
655 DataType::Boolean => DataType::Boolean,
656 DataType::Date => DataType::Date,
657 DataType::Enum { options, reference } => DataType::Enum { options, reference },
658 DataType::Expandable(e) => DataType::Expandable(e.into()),
659 DataType::Array { element_type } => DataType::Array {
660 element_type: Box::new(element_type.deref().clone().into()),
661 },
662 DataType::Unknown => DataType::Unknown,
663 }
664 }
665}
666
667impl From<Expandable<UnsavedCommonModel>> for Expandable {
668 fn from(expandable: Expandable<UnsavedCommonModel>) -> Self {
669 match expandable {
670 Expandable::Expanded { reference, model } => Expandable::Expanded {
671 reference,
672 model: model.into(),
673 },
674 Expandable::Unexpanded { reference } => Expandable::Unexpanded { reference },
675 Expandable::NotFound { reference } => Expandable::NotFound { reference },
676 }
677 }
678}
679
680impl CommonModel {
681 pub fn new(
682 name: String,
683 version: Version,
684 fields: Vec<Field>,
685 category: String,
686 sample: Value,
687 primary: bool,
688 interface: HashMap<Lang, String>,
689 ) -> Self {
690 let mut record = Self {
691 id: Id::new(IdPrefix::CommonModel, chrono::Utc::now()),
692 name,
693 fields,
694 primary,
695 sample,
696 category,
697 interface,
698 record_metadata: Default::default(),
699 };
700 record.record_metadata.version = version;
701 record
702 }
703
704 pub fn new_empty() -> Self {
705 Self {
706 id: Id::new(IdPrefix::CommonModel, chrono::Utc::now()),
707 ..Default::default()
708 }
709 }
710
711 pub fn reference(&self) -> Schema {
712 Schema {
713 schema_data: Default::default(),
714 schema_kind: SchemaKind::Type(Type::Object(ObjectType {
715 properties: self.schema(),
716 ..Default::default()
717 })),
718 }
719 }
720
721 pub fn generate_as(&self, lang: &Lang) -> String {
728 match lang {
729 Lang::Rust => self.as_rust_ref(),
730 Lang::TypeScript => self.as_typescript_ref(),
731 _ => unimplemented!(),
732 }
733 }
734
735 pub async fn generate_as_expanded<'a>(
746 &self,
747 lang: &Lang,
748 cm_store: &MongoStore<CommonModel>,
749 ce_store: &MongoStore<CommonEnum>,
750 strategy: TypeGenerationStrategy<'a>,
751 ) -> String {
752 match lang {
753 Lang::Rust => self.as_rust_expanded(cm_store, ce_store, strategy).await,
754 Lang::TypeScript => {
755 self.as_typescript_expanded(cm_store, ce_store, strategy)
756 .await
757 }
758 _ => unimplemented!(),
759 }
760 }
761
762 fn as_rust_ref(&self) -> String {
763 format!(
764 "pub struct {} {{ {} }}\n",
765 replace_reserved_keyword(&self.name, Lang::Rust)
766 .replace("::", "")
767 .pascal_case(),
768 self.fields
769 .iter()
770 .map(|field| field.as_rust_ref())
771 .collect::<HashSet<String>>()
772 .into_iter()
773 .collect::<Vec<_>>()
774 .join(",\n ")
775 )
776 }
777
778 fn as_typescript_schema(&self, r#type: SchemaType) -> String {
780 format!(
781 "export const {} = Schema.Struct({{ {} }}).annotations({{ title: '{}' }});\n",
782 replace_reserved_keyword(&self.name, Lang::TypeScript)
783 .replace("::", "")
784 .pascal_case(),
785 self.fields
786 .iter()
787 .map(|field| field.as_typescript_schema(r#type))
788 .collect::<HashSet<String>>()
789 .into_iter()
790 .collect::<Vec<_>>()
791 .join(",\n "),
792 self.name
793 )
794 }
795
796 fn as_typescript_ref(&self) -> String {
797 format!(
798 "export interface {} {{ {} }}\n",
799 replace_reserved_keyword(&self.name, Lang::TypeScript)
800 .replace("::", "")
801 .pascal_case(),
802 self.fields
803 .iter()
804 .map(|field| field.as_typescript_ref())
805 .collect::<HashSet<String>>()
806 .into_iter()
807 .collect::<Vec<_>>()
808 .join(";\n ")
809 )
810 }
811
812 pub async fn as_typescript_schema_expanded(
813 &self,
814 cm_store: &MongoStore<CommonModel>,
815 ce_store: &MongoStore<CommonEnum>,
816 r#type: SchemaType,
817 ) -> String {
818 let mut visited_enums = HashSet::new();
819 let mut visited_common_models = HashSet::new();
820
821 let enums = self
822 .fetch_all_enum_references(cm_store.clone(), ce_store.clone())
823 .await
824 .map(|enums| {
825 enums
826 .iter()
827 .filter_map(|enum_model| {
828 if visited_enums.contains(&enum_model.id) {
829 return None;
830 }
831
832 visited_enums.insert(enum_model.id);
833
834 Some(enum_model.as_typescript_schema())
835 })
836 .collect::<HashSet<String>>()
837 .into_iter()
838 .collect::<Vec<_>>()
839 })
840 .ok()
841 .unwrap_or_default()
842 .into_iter()
843 .collect::<Vec<_>>();
844
845 let children = self
846 .fetch_all_children_common_models(cm_store.clone())
847 .await
848 .ok()
849 .unwrap_or_default();
850
851 let children_types = children
852 .0
853 .into_values()
854 .filter_map(|child| {
855 if visited_common_models.contains(&child.id) {
856 return None;
857 }
858 visited_common_models.insert(child.id);
859
860 Some(child.as_typescript_schema(r#type))
861 })
862 .collect::<Vec<_>>()
863 .join("\n // __SEPARATOR__ \n");
864
865 let ce_types = enums.join("\n");
866
867 let cm_types = self.as_typescript_schema(r#type);
868
869 if visited_common_models.contains(&self.id) {
870 format!(
871 "// __SEPARATOR \n {}\n // __SEPARATOR__ \n {}",
872 ce_types, children_types
873 )
874 } else {
875 format!(
876 "// __SEPARATOR__ \n {}\n{}\n // __SEPARATOR__ \n{}",
877 ce_types, children_types, cm_types
878 )
879 }
880 }
881
882 async fn as_typescript_expanded<'a>(
894 &self,
895 cm_store: &MongoStore<CommonModel>,
896 ce_store: &MongoStore<CommonEnum>,
897 strategy: TypeGenerationStrategy<'a>,
898 ) -> String {
899 let mut long_lived_visited_enums = HashSet::new();
900 let mut long_lived_visited_common_models = HashSet::new();
901
902 let (visited_enums, visited_common_models) = match strategy {
903 TypeGenerationStrategy::Cumulative {
904 visited_enums,
905 visited_common_models,
906 } => (visited_enums, visited_common_models),
907 TypeGenerationStrategy::Unique => (
908 &mut long_lived_visited_enums,
909 &mut long_lived_visited_common_models,
910 ),
911 };
912
913 let enums = self
914 .fetch_all_enum_references(cm_store.clone(), ce_store.clone())
915 .await
916 .map(|enums| {
917 enums
918 .iter()
919 .filter_map(|enum_model| {
920 if visited_enums.contains(&enum_model.id) {
921 return None;
922 }
923
924 visited_enums.insert(enum_model.id);
925
926 Some(enum_model.as_typescript_type())
927 })
928 .collect::<HashSet<String>>()
929 .into_iter()
930 .collect::<Vec<_>>()
931 })
932 .ok()
933 .unwrap_or_default()
934 .into_iter()
935 .collect::<Vec<_>>();
936
937 let children = self
938 .fetch_all_children_common_models(cm_store.clone())
939 .await
940 .ok()
941 .unwrap_or_default();
942
943 let children_types = children
944 .0
945 .into_values()
946 .filter_map(|child| {
947 if visited_common_models.contains(&child.id) {
948 return None;
949 }
950 visited_common_models.insert(child.id);
951 Some(format!(
952 "export interface {} {{ {} }}\n",
953 replace_reserved_keyword(&child.name, Lang::TypeScript)
954 .replace("::", "")
955 .pascal_case(),
956 child
957 .fields
958 .iter()
959 .map(|field| field.as_typescript_ref())
960 .collect::<HashSet<String>>()
961 .into_iter()
962 .collect::<Vec<_>>()
963 .join(";\n ")
964 ))
965 })
966 .collect::<Vec<_>>()
967 .join("\n");
968
969 let ce_types = enums.join("\n");
970
971 let cm_types = format!(
972 "export interface {} {{ {} }}\n",
973 replace_reserved_keyword(&self.name, Lang::TypeScript)
974 .replace("::", "")
975 .pascal_case(),
976 self.fields
977 .iter()
978 .map(|field| field.as_typescript_ref())
979 .collect::<HashSet<String>>()
980 .into_iter()
981 .collect::<Vec<_>>()
982 .join(";\n ")
983 );
984
985 if visited_common_models.contains(&self.id) {
986 format!("{}\n{}", ce_types, children_types)
987 } else {
988 format!("{}\n{}\n{}", ce_types, children_types, cm_types)
989 }
990 }
991
992 async fn as_rust_expanded<'a>(
1004 &self,
1005 cm_store: &MongoStore<CommonModel>,
1006 ce_store: &MongoStore<CommonEnum>,
1007 strategy: TypeGenerationStrategy<'a>,
1008 ) -> String {
1009 let mut long_lived_visited_enums = HashSet::new();
1010 let mut long_lived_visited_common_models = HashSet::new();
1011
1012 let (visited_enums, visited_common_models) = match strategy {
1013 TypeGenerationStrategy::Cumulative {
1014 visited_enums,
1015 visited_common_models,
1016 } => (visited_enums, visited_common_models),
1017 TypeGenerationStrategy::Unique => (
1018 &mut long_lived_visited_enums,
1019 &mut long_lived_visited_common_models,
1020 ),
1021 };
1022
1023 let enums = self
1024 .fetch_all_enum_references(cm_store.clone(), ce_store.clone())
1025 .await
1026 .map(|enums| {
1027 enums
1028 .iter()
1029 .filter_map(|enum_model| {
1030 if visited_enums.contains(&enum_model.id) {
1031 return None;
1032 }
1033
1034 visited_enums.insert(enum_model.id);
1035 Some(enum_model.as_rust_type())
1036 })
1037 .collect::<HashSet<String>>()
1038 .into_iter()
1039 .collect::<Vec<_>>()
1040 })
1041 .ok()
1042 .unwrap_or_default()
1043 .into_iter()
1044 .collect::<Vec<_>>();
1045
1046 let children = self
1047 .fetch_all_children_common_models(cm_store.clone())
1048 .await
1049 .ok()
1050 .unwrap_or_default();
1051
1052 let children_types = children
1053 .0
1054 .into_values()
1055 .filter_map(|child| {
1056 if visited_common_models.contains(&child.id) {
1057 return None;
1058 }
1059 visited_common_models.insert(child.id);
1060 Some(format!(
1061 "pub struct {} {{ {} }}\n",
1062 replace_reserved_keyword(&child.name, Lang::Rust)
1063 .replace("::", "")
1064 .pascal_case(),
1065 child
1066 .fields
1067 .iter()
1068 .map(|field| field.as_rust_ref())
1069 .collect::<HashSet<String>>()
1070 .into_iter()
1071 .collect::<Vec<_>>()
1072 .join(",\n ")
1073 ))
1074 })
1075 .collect::<Vec<_>>()
1076 .join("\n");
1077
1078 let ce_types = enums.join("\n");
1079
1080 let cm_types = format!(
1081 "pub struct {} {{ {} }}\n",
1082 replace_reserved_keyword(&self.name, Lang::Rust)
1083 .replace("::", "")
1084 .pascal_case(),
1085 self.fields
1086 .iter()
1087 .map(|field| field.as_rust_ref())
1088 .collect::<HashSet<String>>()
1089 .into_iter()
1090 .collect::<Vec<_>>()
1091 .join(",\n ")
1092 );
1093
1094 if visited_common_models.contains(&self.id) {
1095 format!("{}\n{}", ce_types, children_types)
1096 } else {
1097 format!("{}\n{}\n{}", ce_types, children_types, cm_types)
1098 }
1099 }
1100
1101 fn schema(&self) -> IndexMap<String, ReferenceOr<Box<Schema>>> {
1102 self.fields
1103 .iter()
1104 .fold(IndexMap::new(), |mut index, field| {
1105 let schema = field.datatype.schema(Some(field.name.to_owned()));
1106
1107 index.insert(field.name.clone(), schema);
1108 index
1109 })
1110 }
1111
1112 pub fn request_body(&self, required: bool) -> RequestBody {
1113 let mut content = IndexMap::new();
1114 content.insert(
1115 "application/json".to_string(),
1116 MediaType {
1117 schema: Some(ReferenceOr::Reference {
1118 reference: "#/components/schemas/".to_owned() + self.name.as_str(),
1119 }),
1120 ..Default::default()
1121 },
1122 );
1123
1124 RequestBody {
1125 content,
1126 required,
1127 ..Default::default()
1128 }
1129 }
1130
1131 pub fn get_expandable_fields(&self) -> Vec<Field> {
1132 self.fields
1133 .iter()
1134 .filter(|field| field.is_expandable())
1135 .cloned()
1136 .collect()
1137 }
1138
1139 pub fn get_primitive_fields(&self) -> Vec<Field> {
1140 self.fields
1141 .iter()
1142 .filter(|field| field.is_primitive())
1143 .cloned()
1144 .collect()
1145 }
1146
1147 pub fn get_enum_references(&self) -> Vec<Field> {
1148 self.fields
1149 .iter()
1150 .filter(|field| field.is_enum_reference())
1151 .map(|field| {
1152 if let DataType::Array { element_type } = &field.datatype {
1153 Field {
1154 name: field.name.clone(),
1155 datatype: element_type.deref().clone(),
1156 description: field.description.clone(),
1157 required: field.required,
1158 }
1159 } else {
1160 field.clone()
1161 }
1162 })
1163 .collect()
1164 }
1165
1166 pub fn get_enum_fields(&self) -> Vec<Field> {
1167 self.fields
1168 .iter()
1169 .filter(|field| field.is_enum_field())
1170 .map(|field| {
1171 if let DataType::Array { element_type } = &field.datatype {
1172 Field {
1173 name: field.name.clone(),
1174 datatype: element_type.deref().clone(),
1175 description: field.description.clone(),
1176 required: field.required,
1177 }
1178 } else {
1179 field.clone()
1180 }
1181 })
1182 .collect()
1183 }
1184
1185 pub fn flatten(mut self) -> Vec<CommonModel> {
1186 let mut models = vec![];
1187 for field in &self.fields {
1188 match &field.datatype {
1189 DataType::Expandable(Expandable::Expanded { model, .. }) => {
1190 models.extend(model.clone().flatten());
1191 }
1192 DataType::Array { element_type } => {
1193 if let DataType::Expandable(Expandable::Expanded { model, .. }) =
1194 element_type.deref()
1195 {
1196 models.extend(model.clone().flatten());
1197 }
1198 }
1199 _ => {}
1200 }
1201 }
1202
1203 for field in self.fields.iter_mut() {
1204 match field.datatype {
1205 DataType::Expandable(Expandable::Expanded { ref reference, .. }) => {
1206 field.datatype = DataType::Expandable(Expandable::Unexpanded {
1207 reference: reference.clone(),
1208 })
1209 }
1210 DataType::Array { ref element_type } => {
1211 if let DataType::Expandable(Expandable::Expanded { ref reference, .. }) =
1212 element_type.deref()
1213 {
1214 field.datatype = DataType::Array {
1215 element_type: Box::new(DataType::Expandable(Expandable::Unexpanded {
1216 reference: reference.clone(),
1217 })),
1218 }
1219 }
1220 }
1221 _ => {}
1222 }
1223 }
1224
1225 models.push(self);
1226
1227 models
1228 }
1229
1230 pub async fn expand_all(
1231 &self,
1232 cm_store: MongoStore<CommonModel>,
1233 ce_store: MongoStore<CommonEnum>,
1234 ) -> Result<Self, IntegrationOSError> {
1235 const MAX_NESTING_LEVEL: u8 = 100; self.expand_all_recursive(cm_store, ce_store, MAX_NESTING_LEVEL)
1237 .await
1238 }
1239
1240 #[async_recursion]
1241 async fn expand_all_recursive(
1242 &self,
1243 cm_store: MongoStore<CommonModel>,
1244 ce_store: MongoStore<CommonEnum>,
1245 nesting: u8,
1246 ) -> Result<Self, IntegrationOSError> {
1247 if nesting == 0 {
1248 return Ok(self.clone()); }
1250
1251 let mut new_model = self.clone();
1252 let ts = self
1253 .generate_as_expanded(
1254 &Lang::TypeScript,
1255 &cm_store,
1256 &ce_store,
1257 TypeGenerationStrategy::Unique,
1258 )
1259 .await;
1260 let rust = self
1261 .generate_as_expanded(
1262 &Lang::Rust,
1263 &cm_store,
1264 &ce_store,
1265 TypeGenerationStrategy::Unique,
1266 )
1267 .await;
1268 let interface = HashMap::from_iter(vec![(Lang::Rust, rust), (Lang::TypeScript, ts)]);
1269 new_model.interface = interface;
1270 new_model.fields = Vec::new(); for field in &self.fields {
1273 match &field.datatype {
1274 DataType::Expandable(expandable) => {
1275 let expanded = expandable.expand(cm_store.clone()).await?;
1276 let expanded_field = Field {
1277 name: field.name.clone(),
1278 datatype: DataType::Expandable(expanded),
1279 required: field.required,
1280 description: field.description.clone(),
1281 };
1282
1283 match &expanded_field.datatype {
1284 DataType::Expandable(Expandable::Expanded { model, .. }) => {
1285 let recursively_expanded_model = model
1286 .expand_all_recursive(
1287 cm_store.clone(),
1288 ce_store.clone(),
1289 nesting - 1,
1290 )
1291 .await?;
1292 new_model.fields.push(Field {
1293 name: field.name.clone(),
1294 datatype: DataType::Expandable(Expandable::Expanded {
1295 reference: model.name.clone(),
1296 model: recursively_expanded_model,
1297 }),
1298 required: field.required,
1299 description: field.description.clone(),
1300 });
1301 }
1302 _ => {
1303 new_model.fields.push(expanded_field);
1304 }
1305 }
1306 }
1307 DataType::Array { element_type } => match &**element_type {
1308 DataType::Expandable(expandable) => {
1309 let mut expanded = expandable.expand(cm_store.clone()).await?;
1310 if let Expandable::Expanded { model, .. } = &expanded {
1311 let recursively_expanded_model = model
1312 .expand_all_recursive(
1313 cm_store.clone(),
1314 ce_store.clone(),
1315 nesting - 1,
1316 )
1317 .await?;
1318 expanded = Expandable::Expanded {
1319 reference: model.name.clone(),
1320 model: recursively_expanded_model,
1321 };
1322 }
1323 let expanded_field = Field {
1324 name: field.name.clone(),
1325 datatype: DataType::Array {
1326 element_type: Box::new(DataType::Expandable(expanded)),
1327 },
1328 required: field.required,
1329 description: field.description.clone(),
1330 };
1331 new_model.fields.push(expanded_field);
1332 }
1333 DataType::Enum { reference, .. } if !reference.is_empty() => {
1334 let enum_model = ce_store.get_one(doc! { "name": reference }).await?;
1335 if let Some(enum_model) = enum_model {
1336 new_model.fields.push(Field {
1337 name: field.name.clone(),
1338 datatype: DataType::Enum {
1339 options: Some(
1340 enum_model
1341 .options
1342 .iter()
1343 .map(|option| option.to_owned())
1344 .collect(),
1345 ),
1346 reference: reference.clone(),
1347 },
1348 required: field.required,
1349 description: field.description.clone(),
1350 });
1351 }
1352 }
1353 _ => {
1354 new_model.fields.push(field.clone());
1355 }
1356 },
1357 DataType::Enum { reference, .. } if !reference.is_empty() => {
1358 let enum_model = ce_store.get_one(doc! { "name": reference }).await?;
1359 if let Some(enum_model) = enum_model {
1360 new_model.fields.push(Field {
1361 name: field.name.clone(),
1362 datatype: DataType::Enum {
1363 options: Some(
1364 enum_model
1365 .options
1366 .iter()
1367 .map(|option| option.to_owned())
1368 .collect(),
1369 ),
1370 reference: reference.clone(),
1371 },
1372 required: field.required,
1373 description: field.description.clone(),
1374 });
1375 }
1376 }
1377 _ => {
1378 new_model.fields.push(field.clone());
1379 }
1380 }
1381 }
1382
1383 Ok(new_model)
1384 }
1385
1386 pub async fn fetch_all_enum_references(
1395 &self,
1396 cm_store: MongoStore<CommonModel>,
1397 ce_store: MongoStore<CommonEnum>,
1398 ) -> Result<Vec<CommonEnum>, IntegrationOSError> {
1399 let mut enum_references = self
1400 .get_enum_references()
1401 .into_iter()
1402 .filter_map(|x| match x.datatype {
1403 DataType::Enum { reference, .. } => Some(reference.pascal_case()),
1404 _ => None,
1405 })
1406 .collect::<HashSet<_>>();
1407
1408 let mut flat_enums = self
1409 .get_enum_fields()
1410 .into_iter()
1411 .filter_map(|e| match e.datatype {
1412 DataType::Enum { options, .. } => Some(CommonEnum {
1413 id: Id::now(IdPrefix::CommonEnum),
1414 name: e.name.pascal_case(),
1415 options: options.unwrap_or_default(),
1416 }),
1417 _ => None,
1418 })
1419 .collect::<HashSet<_>>();
1420
1421 for (_, child) in self
1422 .fetch_all_children_common_models(cm_store.clone())
1423 .await?
1424 .0
1425 {
1426 enum_references.extend(child.get_enum_references().into_iter().filter_map(|x| {
1427 match x.datatype {
1428 DataType::Enum { reference, .. } => Some(reference.pascal_case()),
1429 _ => None,
1430 }
1431 }));
1432
1433 let child_enums = child
1434 .get_enum_fields()
1435 .into_iter()
1436 .filter_map(|e| match e.datatype {
1437 DataType::Enum { options, .. } => Some(CommonEnum {
1438 id: Id::now(IdPrefix::CommonEnum),
1439 name: e.name.pascal_case(),
1440 options: options.unwrap_or_default(),
1441 }),
1442 _ => None,
1443 })
1444 .collect::<HashSet<_>>();
1445
1446 flat_enums.extend(child_enums);
1447 }
1448
1449 let enums = ce_store
1450 .get_many(
1451 Some(doc! {
1452 "name": {
1453 "$in": bson::to_bson(&enum_references).map_err(|e| InternalError::invalid_argument(&e.to_string(), Some("enum references")))?,
1454 }
1455 }),
1456 None,
1457 None,
1458 None,
1459 None,
1460 )
1461 .await?;
1462
1463 let enums = enums
1464 .into_iter()
1465 .chain(flat_enums.into_iter())
1466 .collect::<HashSet<_>>()
1467 .into_iter()
1468 .collect();
1469
1470 Ok(enums)
1471 }
1472
1473 pub async fn fetch_all_children_common_models(
1478 &self,
1479 store: MongoStore<CommonModel>,
1480 ) -> Result<(HashMap<String, CommonModel>, HashSet<String>), IntegrationOSError> {
1481 let mut map = HashMap::new();
1482 let mut queue = vec![self.clone()];
1483 let mut not_found = HashSet::new();
1484
1485 while !queue.is_empty() {
1486 let mut refs = HashSet::new();
1487
1488 while let Some(common_model) = queue.pop() {
1489 for field in &common_model.fields {
1490 let expandable = match &field.datatype {
1491 DataType::Array { element_type } => {
1492 let DataType::Expandable(expandable) = &**element_type else {
1493 continue;
1494 };
1495
1496 expandable
1497 }
1498 DataType::Expandable(expandable) => expandable,
1499 _ => {
1500 continue;
1501 }
1502 };
1503
1504 match expandable {
1505 Expandable::Expanded { model, .. } => {
1506 if map.contains_key(&model.name) {
1507 continue;
1508 }
1509 map.insert(model.name.clone(), model.clone());
1510 queue.push(model.clone());
1511 }
1512 Expandable::Unexpanded { reference } => {
1513 if map.contains_key(reference) {
1514 continue;
1515 }
1516 refs.insert(reference.clone());
1517 }
1518 _ => {
1519 continue;
1520 }
1521 };
1522 }
1523 }
1524
1525 let models = store
1526 .get_many(
1527 Some(doc! {
1528 "name": {
1529 "$in": bson::to_bson(&refs).map_err(|e| InternalError::invalid_argument(&e.to_string(), Some("model references")))?,
1530 }
1531 }),
1532 None,
1533 None,
1534 None,
1535 None,
1536 )
1537 .await?;
1538
1539 let not_found_refs: HashSet<String> = refs
1540 .difference(&models.iter().map(|model| model.name.clone()).collect())
1541 .cloned()
1542 .collect();
1543
1544 not_found.extend(not_found_refs);
1545
1546 for model in models {
1547 if map.contains_key(&model.name) {
1548 continue;
1549 }
1550 map.insert(model.name.clone(), model.clone());
1551 queue.push(model.clone());
1552 }
1553 }
1554 Ok((map, not_found))
1555 }
1556
1557 pub async fn get_all_common_models(
1558 store: MongoStore<CommonModel>,
1559 ) -> Result<Vec<String>, IntegrationOSError> {
1560 let docs = store
1561 .aggregate(vec![doc! {
1562 "$group": {
1563 "_id": "",
1564 "list": {"$addToSet": "$name"}
1565 }
1566 }])
1567 .await?;
1568
1569 let first_doc = docs.first().unwrap_or(&doc! {}).clone();
1570
1571 #[derive(Debug, Serialize, Deserialize)]
1572 struct AggregateResult {
1573 list: Vec<String>,
1574 }
1575 Ok(bson::from_document::<AggregateResult>(first_doc)
1576 .map_err(|e| {
1577 InternalError::invalid_argument(&e.to_string(), Some("common model names"))
1578 })?
1579 .list)
1580 }
1581
1582 pub fn to_flat_json(&self) -> Value {
1583 let mut map = Map::new();
1584
1585 for field in &self.fields {
1586 let name = field.datatype.to_name();
1587 map.insert(field.name.clone(), Value::String(name));
1588 }
1589
1590 json!({
1591 "name": self.name,
1592 "fields": Value::Object(map)
1593 })
1594 }
1595}
1596
1597impl Expandable {
1598 pub async fn expand(&self, store: MongoStore<CommonModel>) -> Result<Self, IntegrationOSError> {
1599 Ok(match self {
1600 Expandable::Unexpanded { reference } => {
1601 if let Some(model) = store.get_one(doc! { "name": &reference }).await? {
1602 Expandable::Expanded {
1603 reference: reference.clone(),
1604 model,
1605 }
1606 } else {
1607 Expandable::NotFound {
1608 reference: reference.clone(),
1609 }
1610 }
1611 }
1612 _ => self.clone(),
1613 })
1614 }
1615}
1616
1617#[cfg(test)]
1618mod tests {
1619 use super::*;
1620
1621 #[test]
1622 fn test_field_as_rust_ref_is_correct() {
1623 let field = Field {
1624 name: "name".to_string(),
1625 datatype: DataType::String,
1626 description: None,
1627 required: true,
1628 };
1629
1630 assert_eq!(field.as_rust_ref(), "pub name: Option<String>");
1631 }
1632
1633 #[test]
1634 fn test_data_type_as_rust_reference_is_correct() {
1635 let data_type = DataType::String;
1636 assert_eq!(data_type.as_rust_ref("String".into()), "String");
1637
1638 let data_type = DataType::Number;
1639 assert_eq!(data_type.as_rust_ref("String".into()), "f64");
1640
1641 let data_type = DataType::Boolean;
1642 assert_eq!(data_type.as_rust_ref("".into()), "bool");
1643
1644 let data_type = DataType::Date;
1645 assert_eq!(data_type.as_rust_ref("".into()), "String");
1646
1647 let data_type = DataType::Enum {
1648 options: Some(vec!["option1".to_string(), "option2".to_string()]),
1649 reference: "Reference".to_string(),
1650 };
1651 assert_eq!(data_type.as_rust_ref("".into()), "Reference");
1652
1653 let data_type = DataType::Expandable(Expandable::Unexpanded {
1654 reference: "Reference".to_string(),
1655 });
1656 assert_eq!(data_type.as_rust_ref("".into()), "Reference");
1657
1658 let data_type = DataType::Array {
1659 element_type: Box::new(DataType::String),
1660 };
1661 assert_eq!(data_type.as_rust_ref("String".into()), "Vec<String>");
1662 }
1663
1664 #[test]
1665 fn test_common_model_as_rust_struct_is_correct() {
1666 let common_model = CommonModel {
1667 id: Id::new(IdPrefix::CommonModel, chrono::Utc::now()),
1668 name: "Model".to_string(),
1669 fields: vec![
1670 Field {
1671 name: "name".to_string(),
1672 datatype: DataType::String,
1673 description: None,
1674 required: true,
1675 },
1676 Field {
1677 name: "age".to_string(),
1678 datatype: DataType::Number,
1679 description: None,
1680 required: true,
1681 },
1682 ],
1683 sample: json!({
1684 "name": "John Doe",
1685 "age": 25
1686 }),
1687 primary: true,
1688 category: "Category".to_string(),
1689 interface: Default::default(),
1690 record_metadata: Default::default(),
1691 };
1692
1693 let rust_struct = common_model.as_rust_ref();
1694 let typescript_interface = common_model.as_typescript_ref();
1695
1696 assert!(
1697 rust_struct.contains(
1698 "pub struct Model { pub age: Option<f64>,\n pub name: Option<String> }"
1699 ) || rust_struct.contains(
1700 "pub struct Model { pub name: Option<String>,\n pub age: Option<f64> }"
1701 )
1702 );
1703
1704 assert!(
1705 typescript_interface
1706 .contains("export interface Model { age?: number;\n name?: string }")
1707 || typescript_interface
1708 .contains("export interface Model { name?: string;\n age?: number }")
1709 );
1710 }
1711
1712 #[test]
1713 fn test_common_model_as_lax_schema_is_correct() {
1714 let common_model = CommonModel {
1715 id: Id::new(IdPrefix::CommonModel, chrono::Utc::now()),
1716 name: "Model".to_string(),
1717 fields: vec![
1718 Field {
1719 name: "name".to_string(),
1720 datatype: DataType::String,
1721 description: None,
1722 required: true,
1723 },
1724 Field {
1725 name: "age".to_string(),
1726 datatype: DataType::Number,
1727 description: None,
1728 required: true,
1729 },
1730 ],
1731 sample: json!({
1732 "name": "John Doe",
1733 "age": 25
1734 }),
1735 primary: true,
1736 category: "Category".to_string(),
1737 interface: Default::default(),
1738 record_metadata: Default::default(),
1739 };
1740
1741 let lax_schema = common_model.as_typescript_schema(SchemaType::Lax);
1742 assert!(
1743 lax_schema.contains(
1744 "export const Model = Schema.Struct({ age: Schema.optional(Schema.NullishOr(Schema.Number)),\n name: Schema.optional(Schema.NullishOr(Schema.String)) }).annotations({ title: 'Model' });\n") ||
1745 lax_schema.contains(
1746 "export const Model = Schema.Struct({ name: Schema.optional(Schema.NullishOr(Schema.String)),\n age: Schema.optional(Schema.NullishOr(Schema.Number)) }).annotations({ title: 'Model' });\n"
1747 )
1748 );
1749 }
1750
1751 #[test]
1752 fn test_common_model_as_strict_schema_is_correct() {
1753 let common_model = CommonModel {
1754 id: Id::new(IdPrefix::CommonModel, chrono::Utc::now()),
1755 name: "Model".to_string(),
1756 fields: vec![
1757 Field {
1758 name: "name".to_string(),
1759 datatype: DataType::String,
1760 description: None,
1761 required: true,
1762 },
1763 Field {
1764 name: "age".to_string(),
1765 datatype: DataType::Number,
1766 description: None,
1767 required: true,
1768 },
1769 ],
1770 sample: json!({
1771 "name": "John Doe",
1772 "age": 25
1773 }),
1774 primary: true,
1775 category: "Category".to_string(),
1776 interface: Default::default(),
1777 record_metadata: Default::default(),
1778 };
1779
1780 let strict_schema = common_model.as_typescript_schema(SchemaType::Strict);
1781 assert!(
1782 strict_schema.contains(
1783 "export const Model = Schema.Struct({ age: Schema.Number,\n name: Schema.String }).annotations({ title: 'Model' });\n") ||
1784 strict_schema.contains(
1785 "export const Model = Schema.Struct({ name: Schema.String,\n age: Schema.Number }).annotations({ title: 'Model' });\n"
1786 )
1787 );
1788 }
1789}