use serde_json::{Value as Json, json};
fn assert_roundtrips(value: Json) {
let toml_str = tomljson::to_string(&value).expect("encode succeeded");
let back = tomljson::from_str(&toml_str).expect("decode succeeded");
assert_eq!(back, value, "round-trip mismatch\n--- toml ---\n{toml_str}");
}
fn assert_decodes_to(toml_str: &str, expected: Json) {
let back = tomljson::from_str(toml_str).expect("decode succeeded");
assert_eq!(
back, expected,
"hand-written TOML decoded to wrong JSON\n--- toml ---\n{toml_str}"
);
}
#[test]
fn numeric_constraints() {
assert_roundtrips(json!({
"type": "number",
"minimum": 0,
"maximum": 100,
"exclusiveMinimum": -1
}));
}
#[test]
fn string_constraints_with_pattern() {
assert_roundtrips(json!({
"type": "string",
"minLength": 1,
"maxLength": 64,
"pattern": "^[a-z][a-z0-9_-]*$"
}));
}
#[test]
fn homogeneous_string_enum() {
assert_roundtrips(json!({
"enum": ["draft", "published", "archived"]
}));
}
#[test]
fn mixed_types_enum_no_null() {
assert_roundtrips(json!({
"enum": [1, "two", true]
}));
}
#[test]
fn const_null_at_scalar_position() {
assert_roundtrips(json!({ "const": null }));
}
#[test]
fn default_with_null_nested_in_object_and_array() {
assert_roundtrips(json!({
"type": "object",
"default": {
"color": null,
"tags": ["a", null, "b"]
}
}));
}
#[test]
fn array_with_items_schema() {
assert_roundtrips(json!({
"type": "array",
"items": { "type": "string", "minLength": 1 }
}));
}
#[test]
fn prefix_items_tuple_form() {
assert_roundtrips(json!({
"type": "array",
"prefixItems": [
{ "type": "string" },
{ "type": "integer" }
],
"items": false
}));
}
#[test]
fn any_of_all_of_not_combined() {
assert_roundtrips(json!({
"allOf": [
{ "type": "object" },
{ "required": ["id"] }
],
"anyOf": [
{ "properties": { "kind": { "const": "a" } } },
{ "properties": { "kind": { "const": "b" } } }
],
"not": { "required": ["deprecated"] }
}));
}
#[test]
fn if_then_else() {
assert_roundtrips(json!({
"type": "object",
"properties": { "kind": { "type": "string" } },
"if": { "properties": { "kind": { "const": "premium" } } },
"then": { "required": ["billing"] },
"else": { "required": ["email"] }
}));
}
#[test]
fn x_mdvs_extensions_flat_form() {
assert_roundtrips(json!({
"type": "string",
"x-mdvs-allowed": ["**/*.md"],
"x-mdvs-required": ["posts/**/*.md"]
}));
}
#[test]
fn number_fidelity_int_vs_float_preserved() {
assert_roundtrips(json!({ "enum": [1, 1.0, 1.5] }));
}
#[test]
fn realistic_composite_schema() {
assert_roundtrips(json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://mdvs.dev/schema/post.json",
"type": "object",
"properties": {
"title": { "type": "string", "minLength": 1, "maxLength": 200 },
"status": { "enum": ["draft", "published", "archived"] },
"author": { "$ref": "#/$defs/person" },
"tags": {
"type": "array",
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
"minItems": 0,
"maxItems": 16
},
"rating": {
"oneOf": [
{ "type": "number", "minimum": 0, "maximum": 5 },
{ "type": "null" }
]
}
},
"required": ["title", "status"],
"$defs": {
"person": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "pattern": "^[^@]+@[^@]+$" }
},
"required": ["name"]
}
},
"x-mdvs-allowed": ["posts/**/*.md"],
"x-mdvs-required": ["posts/**/*.md"]
}));
}
#[test]
fn canonical_hand_written_toml_decodes_to_composite() {
let canonical = r##"
"$schema" = "https://json-schema.org/draft/2020-12/schema"
"$id" = "https://mdvs.dev/schema/post.json"
type = "object"
required = ["title", "status"]
"x-mdvs-allowed" = ["posts/**/*.md"]
"x-mdvs-required" = ["posts/**/*.md"]
[properties.title]
type = "string"
minLength = 1
maxLength = 200
[properties.status]
enum = ["draft", "published", "archived"]
[properties.author]
"$ref" = "#/$defs/person"
[properties.tags]
type = "array"
minItems = 0
maxItems = 16
[properties.tags.items]
type = "string"
pattern = "^[a-z][a-z0-9-]*$"
[properties.rating]
[[properties.rating.oneOf]]
type = "number"
minimum = 0
maximum = 5
[[properties.rating.oneOf]]
type = "null"
["$defs".person]
type = "object"
required = ["name"]
["$defs".person.properties.name]
type = "string"
["$defs".person.properties.email]
type = "string"
pattern = "^[^@]+@[^@]+$"
"##;
let expected = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://mdvs.dev/schema/post.json",
"type": "object",
"required": ["title", "status"],
"x-mdvs-allowed": ["posts/**/*.md"],
"x-mdvs-required": ["posts/**/*.md"],
"properties": {
"title": { "type": "string", "minLength": 1, "maxLength": 200 },
"status": { "enum": ["draft", "published", "archived"] },
"author": { "$ref": "#/$defs/person" },
"tags": {
"type": "array",
"minItems": 0,
"maxItems": 16,
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }
},
"rating": {
"oneOf": [
{ "type": "number", "minimum": 0, "maximum": 5 },
{ "type": "null" }
]
}
},
"$defs": {
"person": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "pattern": "^[^@]+@[^@]+$" }
}
}
}
});
assert_decodes_to(canonical, expected);
}
#[test]
fn strings_shaped_like_toml_literals_stay_strings() {
assert_roundtrips(json!({
"enum": ["true", "false", "42", "1.5", "2026-05-04", "inf", "nan"]
}));
}
#[test]
fn unquoted_toml_date_decodes_to_string() {
let toml_str = r#"
type = "string"
default = 2026-05-04
"#;
assert_decodes_to(
toml_str,
json!({ "type": "string", "default": "2026-05-04" }),
);
}
#[test]
fn unquoted_toml_local_time_decodes_to_string() {
let toml_str = "default = 09:30:00\n";
assert_decodes_to(toml_str, json!({ "default": "09:30:00" }));
}
#[test]
fn empty_string_value() {
assert_roundtrips(json!({ "default": "", "const": "" }));
}