1use std::collections::BTreeSet;
28use std::fs;
29use std::path::Path;
30
31use serde::{Deserialize, Serialize};
32
33use crate::admin::{Admin, AdminField, FieldType};
34use crate::error::Error;
35
36pub const SCHEMA_VERSION: u32 = 2;
44
45pub const VALID_TYPE_NAMES: &[&str] = &["i32", "i64", "String", "bool", "DateTime"];
50
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct Schema {
60 pub version: u32,
61 pub rustio_version: String,
62 pub models: Vec<SchemaModel>,
63}
64
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct SchemaModel {
68 pub name: String,
69 pub table: String,
70 pub admin_name: String,
71 pub display_name: String,
72 pub singular_name: String,
73 pub fields: Vec<SchemaField>,
74 pub relations: Vec<SchemaRelation>,
77 #[serde(default)]
81 pub core: bool,
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85#[serde(deny_unknown_fields)]
86pub struct SchemaField {
87 pub name: String,
88 #[serde(rename = "type")]
89 pub ty: String,
90 pub nullable: bool,
91 pub editable: bool,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub relation: Option<Relation>,
99}
100
101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122#[serde(deny_unknown_fields)]
123pub struct Relation {
124 pub model: String,
126 pub field: String,
130 pub kind: RelationKind,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub display_field: Option<String>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub required: Option<bool>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub on_delete: Option<String>,
153}
154
155#[non_exhaustive]
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum RelationKind {
162 BelongsTo,
163 HasMany,
164}
165
166impl RelationKind {
167 pub fn as_str(self) -> &'static str {
168 match self {
169 RelationKind::BelongsTo => "belongs_to",
170 RelationKind::HasMany => "has_many",
171 }
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181#[serde(deny_unknown_fields)]
182pub struct SchemaRelation {
183 pub kind: String,
184 pub to: String,
185 pub via: String,
186}
187
188#[non_exhaustive]
191#[derive(Debug, Clone, PartialEq)]
192pub enum SchemaError {
193 VersionMismatch { found: u32, expected: u32 },
195 DuplicateModel(String),
197 DuplicateField { model: String, field: String },
199 InvalidType {
201 model: String,
202 field: String,
203 ty: String,
204 },
205 UnknownRelationTarget { from: String, to: String },
207 EmptyIdentifier(&'static str),
210 Parse(String),
212}
213
214impl std::fmt::Display for SchemaError {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 match self {
217 Self::VersionMismatch { found, expected } => write!(
218 f,
219 "schema version mismatch: found {found}, expected {expected}"
220 ),
221 Self::DuplicateModel(name) => write!(f, "duplicate model `{name}`"),
222 Self::DuplicateField { model, field } => {
223 write!(f, "duplicate field `{field}` in model `{model}`")
224 }
225 Self::InvalidType { model, field, ty } => write!(
226 f,
227 "field `{model}.{field}` has invalid type `{ty}` (valid: {valid})",
228 valid = VALID_TYPE_NAMES.join(", "),
229 ),
230 Self::UnknownRelationTarget { from, to } => {
231 write!(f, "relation from `{from}` targets unknown model `{to}`")
232 }
233 Self::EmptyIdentifier(which) => write!(f, "empty {which}"),
234 Self::Parse(msg) => write!(f, "schema parse error: {msg}"),
235 }
236 }
237}
238
239impl std::error::Error for SchemaError {}
240
241impl From<SchemaError> for Error {
242 fn from(e: SchemaError) -> Self {
243 Error::Internal(e.to_string())
244 }
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
250pub struct IncomingRelation {
251 pub from_model: String,
253 pub from_field: String,
255 pub to_model: String,
257 pub kind: RelationKind,
260}
261
262impl Schema {
263 pub fn relation_for(&self, model: &str, field: &str) -> Option<&Relation> {
269 self.models
270 .iter()
271 .find(|m| m.name == model)?
272 .fields
273 .iter()
274 .find(|f| f.name == field)?
275 .relation
276 .as_ref()
277 }
278
279 pub fn incoming_relations(&self, model: &str) -> Vec<IncomingRelation> {
284 let mut out: Vec<IncomingRelation> = Vec::new();
285 for m in &self.models {
286 for f in &m.fields {
287 if let Some(rel) = &f.relation {
288 if rel.model == model && matches!(rel.kind, RelationKind::BelongsTo) {
289 out.push(IncomingRelation {
290 from_model: m.name.clone(),
291 from_field: f.name.clone(),
292 to_model: model.to_string(),
293 kind: RelationKind::HasMany,
294 });
295 }
296 }
297 }
298 }
299 out
300 }
301
302 pub fn from_admin(admin: &Admin) -> Self {
311 let mut models: Vec<SchemaModel> = admin
312 .entries()
313 .iter()
314 .map(SchemaModel::from_entry)
315 .collect();
316 models.sort_by(|a, b| a.name.cmp(&b.name));
317 Self {
318 version: SCHEMA_VERSION,
319 rustio_version: env!("CARGO_PKG_VERSION").to_string(),
320 models,
321 }
322 }
323
324 pub fn validate(&self) -> Result<(), SchemaError> {
329 if self.version != SCHEMA_VERSION {
330 return Err(SchemaError::VersionMismatch {
331 found: self.version,
332 expected: SCHEMA_VERSION,
333 });
334 }
335
336 let mut model_names: BTreeSet<&str> = BTreeSet::new();
337 for model in &self.models {
338 if model.name.is_empty() {
339 return Err(SchemaError::EmptyIdentifier("model name"));
340 }
341 if model.table.is_empty() {
342 return Err(SchemaError::EmptyIdentifier("model table"));
343 }
344 if !model_names.insert(model.name.as_str()) {
345 return Err(SchemaError::DuplicateModel(model.name.clone()));
346 }
347 }
348
349 let valid_types: BTreeSet<&str> = VALID_TYPE_NAMES.iter().copied().collect();
350
351 for model in &self.models {
352 let mut field_names: BTreeSet<&str> = BTreeSet::new();
353 for field in &model.fields {
354 if field.name.is_empty() {
355 return Err(SchemaError::EmptyIdentifier("field name"));
356 }
357 if !field_names.insert(field.name.as_str()) {
358 return Err(SchemaError::DuplicateField {
359 model: model.name.clone(),
360 field: field.name.clone(),
361 });
362 }
363 if !valid_types.contains(field.ty.as_str()) {
364 return Err(SchemaError::InvalidType {
365 model: model.name.clone(),
366 field: field.name.clone(),
367 ty: field.ty.clone(),
368 });
369 }
370 }
371
372 for relation in &model.relations {
373 if !model_names.contains(relation.to.as_str()) {
374 return Err(SchemaError::UnknownRelationTarget {
375 from: model.name.clone(),
376 to: relation.to.clone(),
377 });
378 }
379 }
380 }
381
382 Ok(())
383 }
384
385 pub fn parse(json: &str) -> Result<Self, SchemaError> {
390 let schema: Schema =
391 serde_json::from_str(json).map_err(|e| SchemaError::Parse(e.to_string()))?;
392 schema.validate()?;
393 Ok(schema)
394 }
395
396 pub fn to_pretty_json(&self) -> Result<String, Error> {
401 let mut out =
402 serde_json::to_string_pretty(self).map_err(|e| Error::Internal(e.to_string()))?;
403 out.push('\n');
404 Ok(out)
405 }
406
407 pub fn write_to(&self, path: &Path) -> Result<(), Error> {
411 self.validate()?;
412 let json = self.to_pretty_json()?;
413 let tmp = path.with_extension("json.tmp");
414 let _ = fs::remove_file(&tmp);
418 fs::write(&tmp, json).map_err(|e| Error::Internal(e.to_string()))?;
419 if let Err(e) = fs::rename(&tmp, path) {
420 let _ = fs::remove_file(&tmp);
423 return Err(Error::Internal(e.to_string()));
424 }
425 Ok(())
426 }
427}
428
429impl SchemaModel {
430 fn from_entry(entry: &crate::admin::AdminEntry) -> Self {
431 let mut fields: Vec<SchemaField> = entry
432 .fields
433 .iter()
434 .map(SchemaField::from_admin_field)
435 .collect();
436 fields.sort_by(|a, b| a.name.cmp(&b.name));
437 Self {
438 name: entry.singular_name.to_string(),
439 table: entry.table.to_string(),
440 admin_name: entry.admin_name.to_string(),
441 display_name: entry.display_name.to_string(),
442 singular_name: entry.singular_name.to_string(),
443 fields,
444 relations: Vec::new(),
445 core: entry.core,
446 }
447 }
448}
449
450impl SchemaField {
451 fn from_admin_field(f: &AdminField) -> Self {
452 let relation = f.relation.as_ref().map(|r| Relation {
458 model: r.target_model.to_string(),
459 field: "id".to_string(),
460 kind: RelationKind::BelongsTo,
461 display_field: r.display_field.map(|s| s.to_string()),
462 required: None,
466 on_delete: None,
467 });
468 Self {
469 name: f.name.to_string(),
470 ty: field_type_name(f.field_type).to_string(),
475 nullable: f.field_type.nullable(),
476 editable: f.editable,
477 relation,
478 }
479 }
480}
481
482pub(crate) fn field_type_name(ty: FieldType) -> &'static str {
492 match ty {
496 FieldType::I32 => "i32",
497 FieldType::I64 => "i64",
498 FieldType::String => "String",
499 FieldType::Bool => "bool",
500 FieldType::DateTime | FieldType::OptionalDateTime => "DateTime",
501 FieldType::OptionalI64 => "i64",
502 FieldType::OptionalString => "String",
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use crate::admin::{Admin, AdminField, AdminModel, FieldType};
510 use crate::http::FormData;
511 use crate::error::Error;
512 use crate::orm::{Model, Row, Value};
513
514 struct Post;
515
516 impl Model for Post {
517 const TABLE: &'static str = "posts";
518 const COLUMNS: &'static [&'static str] = &["id", "title", "published_at"];
519 const INSERT_COLUMNS: &'static [&'static str] = &["title", "published_at"];
520 fn id(&self) -> i64 {
521 0
522 }
523 fn from_row(_: Row<'_>) -> Result<Self, Error> {
524 unimplemented!()
525 }
526 fn insert_values(&self) -> Vec<Value> {
527 Vec::new()
528 }
529 }
530
531 impl AdminModel for Post {
538 const ADMIN_NAME: &'static str = "posts";
539 const DISPLAY_NAME: &'static str = "Posts";
540 const SINGULAR_NAME: &'static str = "Post";
541 const FIELDS: &'static [AdminField] = &[
542 AdminField {
543 name: "id",
544 label: "id",
545 field_type: FieldType::I64,
546 editable: false,
547 relation: None,
548 choices: None,
549 },
550 AdminField {
551 name: "title",
552 label: "title",
553 field_type: FieldType::String,
554 editable: true,
555 relation: None,
556 choices: None,
557 },
558 AdminField {
559 name: "published_at",
560 label: "published_at",
561 field_type: FieldType::OptionalDateTime,
562 editable: true,
563 relation: None,
564 choices: None,
565 },
566 ];
567 fn display_values(&self) -> Vec<(String, String)> {
568 Vec::new()
569 }
570 fn from_form(_: &FormData) -> std::result::Result<Self, Vec<String>> {
571 unimplemented!()
572 }
573 fn object_label(&self) -> String {
574 "Post".into()
575 }
576 fn id(&self) -> i64 {
577 0
578 }
579 fn values_to_update(&self) -> Vec<(&'static str, Value)> {
580 Vec::new()
581 }
582 }
583
584 struct Book;
588
589 impl Model for Book {
590 const TABLE: &'static str = "books";
591 const COLUMNS: &'static [&'static str] = &["id", "title"];
592 const INSERT_COLUMNS: &'static [&'static str] = &["title"];
593 fn id(&self) -> i64 {
594 0
595 }
596 fn from_row(_: Row<'_>) -> Result<Self, Error> {
597 unimplemented!()
598 }
599 fn insert_values(&self) -> Vec<Value> {
600 Vec::new()
601 }
602 }
603
604 impl AdminModel for Book {
605 const ADMIN_NAME: &'static str = "books";
606 const DISPLAY_NAME: &'static str = "Books";
607 const SINGULAR_NAME: &'static str = "Book";
608 const FIELDS: &'static [AdminField] = &[
609 AdminField {
610 name: "id",
611 label: "id",
612 field_type: FieldType::I64,
613 editable: false,
614 relation: None,
615 choices: None,
616 },
617 AdminField {
618 name: "title",
619 label: "title",
620 field_type: FieldType::String,
621 editable: true,
622 relation: None,
623 choices: None,
624 },
625 ];
626 fn display_values(&self) -> Vec<(String, String)> {
627 Vec::new()
628 }
629 fn from_form(_: &FormData) -> std::result::Result<Self, Vec<String>> {
630 unimplemented!()
631 }
632 fn object_label(&self) -> String {
633 "Book".into()
634 }
635 fn id(&self) -> i64 {
636 0
637 }
638 fn values_to_update(&self) -> Vec<(&'static str, Value)> {
639 Vec::new()
640 }
641 }
642
643 fn find<'a>(schema: &'a Schema, name: &str) -> &'a SchemaModel {
647 schema
648 .models
649 .iter()
650 .find(|m| m.name == name)
651 .unwrap_or_else(|| panic!("no model named `{name}` in schema"))
652 }
653
654 #[test]
655 fn schema_reflects_admin_registry() {
656 let admin = Admin::new().model::<Post>();
657 let schema = Schema::from_admin(&admin);
658
659 assert_eq!(schema.version, SCHEMA_VERSION);
660 assert_eq!(schema.models.len(), 2);
662
663 let m = find(&schema, "Post");
664 assert_eq!(m.table, "posts");
665 assert_eq!(m.admin_name, "posts");
666 assert_eq!(m.display_name, "Posts");
667 assert_eq!(m.singular_name, "Post");
668 assert_eq!(m.fields.len(), 3);
669 assert!(m.relations.is_empty());
670 assert!(!m.core, "user models must not be marked core");
671
672 let title = m.fields.iter().find(|f| f.name == "title").unwrap();
673 assert_eq!(title.ty, "String");
674 assert!(!title.nullable);
675 assert!(title.editable);
676
677 let pub_at = m.fields.iter().find(|f| f.name == "published_at").unwrap();
678 assert_eq!(pub_at.ty, "DateTime");
679 assert!(pub_at.nullable);
680 assert!(pub_at.editable);
681 }
682
683 #[test]
684 fn core_user_model_is_always_present() {
685 let schema = Schema::from_admin(&Admin::new());
689 let user = find(&schema, "User");
690 assert!(user.core, "User must be flagged as a core model");
691 assert_eq!(user.table, "rustio_users");
692 let pw = user
693 .fields
694 .iter()
695 .find(|f| f.name == "password_hash")
696 .unwrap();
697 assert!(
698 !pw.editable,
699 "password_hash must never be exposed as editable via admin"
700 );
701 let created_at = user.fields.iter().find(|f| f.name == "created_at").unwrap();
704 assert_eq!(created_at.ty, "DateTime");
705 assert!(!created_at.editable);
706 }
707
708 #[test]
709 fn schema_fields_are_sorted_by_name() {
710 let schema = Schema::from_admin(&Admin::new().model::<Post>());
714 let post = find(&schema, "Post");
715 let names: Vec<&str> = post.fields.iter().map(|f| f.name.as_str()).collect();
716 assert_eq!(names, vec!["id", "published_at", "title"]);
717 }
718
719 #[test]
720 fn schema_models_are_sorted_by_name() {
721 let schema = Schema::from_admin(&Admin::new().model::<Post>().model::<Book>());
724 let names: Vec<&str> = schema.models.iter().map(|m| m.name.as_str()).collect();
725 assert_eq!(names, vec!["Book", "Post", "User"]);
726 }
727
728 #[test]
729 fn to_pretty_json_round_trips() {
730 let schema = Schema::from_admin(&Admin::new().model::<Post>());
731 let json = schema.to_pretty_json().unwrap();
732 let parsed = Schema::parse(&json).unwrap();
733 assert_eq!(parsed, schema);
734 }
735
736 #[test]
737 fn to_pretty_json_ends_with_newline() {
738 let schema = Schema::from_admin(&Admin::new().model::<Post>());
739 let json = schema.to_pretty_json().unwrap();
740 assert!(json.ends_with('\n'), "schema JSON must end with newline");
741 }
742
743 #[test]
744 fn same_registry_produces_identical_bytes() {
745 let a = Schema::from_admin(&Admin::new().model::<Post>().model::<Book>())
749 .to_pretty_json()
750 .unwrap();
751 let b = Schema::from_admin(&Admin::new().model::<Post>().model::<Book>())
752 .to_pretty_json()
753 .unwrap();
754 assert_eq!(a, b);
755 }
756
757 #[test]
765 fn schema_snapshot_is_byte_for_byte_stable() {
766 let schema = Schema::from_admin(&Admin::new().model::<Post>());
771 let actual = schema.to_pretty_json().unwrap();
772
773 let expected = format!(
774 r#"{{
775 "version": {sv},
776 "rustio_version": "{rv}",
777 "models": [
778 {{
779 "name": "Post",
780 "table": "posts",
781 "admin_name": "posts",
782 "display_name": "Posts",
783 "singular_name": "Post",
784 "fields": [
785 {{
786 "name": "id",
787 "type": "i64",
788 "nullable": false,
789 "editable": false
790 }},
791 {{
792 "name": "published_at",
793 "type": "DateTime",
794 "nullable": true,
795 "editable": true
796 }},
797 {{
798 "name": "title",
799 "type": "String",
800 "nullable": false,
801 "editable": true
802 }}
803 ],
804 "relations": [],
805 "core": false
806 }},
807 {{
808 "name": "User",
809 "table": "rustio_users",
810 "admin_name": "users",
811 "display_name": "Users",
812 "singular_name": "User",
813 "fields": [
814 {{
815 "name": "created_at",
816 "type": "DateTime",
817 "nullable": false,
818 "editable": false
819 }},
820 {{
821 "name": "email",
822 "type": "String",
823 "nullable": false,
824 "editable": true
825 }},
826 {{
827 "name": "id",
828 "type": "i64",
829 "nullable": false,
830 "editable": false
831 }},
832 {{
833 "name": "is_active",
834 "type": "bool",
835 "nullable": false,
836 "editable": true
837 }},
838 {{
839 "name": "password_hash",
840 "type": "String",
841 "nullable": false,
842 "editable": false
843 }},
844 {{
845 "name": "role",
846 "type": "String",
847 "nullable": false,
848 "editable": true
849 }}
850 ],
851 "relations": [],
852 "core": true
853 }}
854 ]
855}}
856"#,
857 rv = env!("CARGO_PKG_VERSION"),
858 sv = SCHEMA_VERSION,
859 );
860
861 assert_eq!(actual, expected);
862 }
863
864 #[test]
865 fn validate_accepts_clean_schema() {
866 let schema = Schema::from_admin(&Admin::new().model::<Post>().model::<Book>());
867 assert_eq!(schema.validate(), Ok(()));
868 }
869
870 #[test]
871 fn validate_rejects_version_mismatch() {
872 let mut schema = Schema::from_admin(&Admin::new().model::<Post>());
873 schema.version = 999;
874 assert_eq!(
875 schema.validate(),
876 Err(SchemaError::VersionMismatch {
877 found: 999,
878 expected: SCHEMA_VERSION
879 })
880 );
881 }
882
883 #[test]
884 fn validate_rejects_duplicate_models() {
885 let mut schema = Schema::from_admin(&Admin::new().model::<Post>());
886 let post = find(&schema, "Post").clone();
887 schema.models.push(post);
888 assert_eq!(
889 schema.validate(),
890 Err(SchemaError::DuplicateModel("Post".to_string()))
891 );
892 }
893
894 #[test]
895 fn validate_rejects_duplicate_fields() {
896 let mut schema = Schema::from_admin(&Admin::new().model::<Post>());
897 let post_idx = schema.models.iter().position(|m| m.name == "Post").unwrap();
898 let dup = schema.models[post_idx].fields[0].clone();
899 schema.models[post_idx].fields.push(dup);
900 assert_eq!(
901 schema.validate(),
902 Err(SchemaError::DuplicateField {
903 model: "Post".to_string(),
904 field: "id".to_string(),
905 })
906 );
907 }
908
909 #[test]
910 fn validate_rejects_unknown_type() {
911 let mut schema = Schema::from_admin(&Admin::new().model::<Post>());
912 let post_idx = schema.models.iter().position(|m| m.name == "Post").unwrap();
913 schema.models[post_idx].fields[0].ty = "HyperFloat128".to_string();
914 assert_eq!(
915 schema.validate(),
916 Err(SchemaError::InvalidType {
917 model: "Post".to_string(),
918 field: "id".to_string(),
919 ty: "HyperFloat128".to_string(),
920 })
921 );
922 }
923
924 #[test]
925 fn validate_rejects_dangling_relation() {
926 let mut schema = Schema::from_admin(&Admin::new().model::<Post>());
927 let post_idx = schema.models.iter().position(|m| m.name == "Post").unwrap();
928 schema.models[post_idx].relations.push(SchemaRelation {
929 kind: "belongs_to".to_string(),
930 to: "Ghost".to_string(),
931 via: "ghost_id".to_string(),
932 });
933 assert_eq!(
934 schema.validate(),
935 Err(SchemaError::UnknownRelationTarget {
936 from: "Post".to_string(),
937 to: "Ghost".to_string(),
938 })
939 );
940 }
941
942 #[test]
943 fn validate_accepts_self_referencing_relation() {
944 let mut schema = Schema::from_admin(&Admin::new().model::<Post>());
947 let post_idx = schema.models.iter().position(|m| m.name == "Post").unwrap();
948 schema.models[post_idx].relations.push(SchemaRelation {
949 kind: "belongs_to".to_string(),
950 to: "Post".to_string(),
951 via: "parent_id".to_string(),
952 });
953 assert_eq!(schema.validate(), Ok(()));
954 }
955
956 #[test]
957 fn parse_rejects_unknown_top_level_field() {
958 let bad = r#"{
959 "version": 1,
960 "rustio_version": "0.4.0",
961 "models": [],
962 "something_extra": true
963 }"#;
964 let result = Schema::parse(bad);
965 assert!(
966 matches!(result, Err(SchemaError::Parse(_))),
967 "unknown fields must be rejected, got: {:?}",
968 result
969 );
970 }
971
972 #[test]
973 fn parse_rejects_missing_required_field() {
974 let bad = r#"{
976 "version": 1,
977 "models": []
978 }"#;
979 let result = Schema::parse(bad);
980 assert!(
981 matches!(result, Err(SchemaError::Parse(_))),
982 "missing fields must be rejected"
983 );
984 }
985
986 #[test]
987 fn parse_rejects_version_mismatch() {
988 let bad = r#"{
989 "version": 999,
990 "rustio_version": "0.4.0",
991 "models": []
992 }"#;
993 let err = Schema::parse(bad).unwrap_err();
994 assert!(matches!(err, SchemaError::VersionMismatch { .. }));
995 }
996
997 #[test]
998 fn write_to_is_atomic_no_tmp_left_behind() {
999 let tmp_dir = std::env::temp_dir().join(format!(
1000 "rustio-schema-write-{}-{}",
1001 std::process::id(),
1002 std::time::SystemTime::now()
1003 .duration_since(std::time::UNIX_EPOCH)
1004 .unwrap()
1005 .as_nanos()
1006 ));
1007 std::fs::create_dir_all(&tmp_dir).unwrap();
1008 let target = tmp_dir.join("rustio.schema.json");
1009
1010 let schema = Schema::from_admin(&Admin::new().model::<Post>());
1011 schema.write_to(&target).unwrap();
1012
1013 let bytes = std::fs::read_to_string(&target).unwrap();
1015 let parsed = Schema::parse(&bytes).unwrap();
1016 assert_eq!(parsed, schema);
1017
1018 assert!(!tmp_dir.join("rustio.schema.tmp").exists());
1021 assert!(!tmp_dir.join("rustio.schema.json.tmp").exists());
1022
1023 std::fs::remove_dir_all(&tmp_dir).ok();
1024 }
1025}