Skip to content

Commit e466bbf

Browse files
authored
Extend Property struct to support examples and handle complex additionalProperties (#37)
* Add examples field to Property conversion Initializes the examples field as an empty vector when converting Attribute to schema::Property. This prepares the Property struct for future use of example values. * Handle object type for additionalProperties in schema Added custom deserializer to support 'additionalProperties' as an object in SchemaObject. Updated tests to cover this case and added support for 'examples' in Property struct. * Add JSON schema validation to CLI Introduces support for validating JSON schema files in the CLI by checking the input type and delegating to a new validate_from_json_schema function. Handles validation errors and logs results appropriately. * Remove whitespace from title during deserialization Added a custom deserializer for the SchemaObject title field to strip all whitespace characters during deserialization. Includes tests to verify whitespace removal for various input cases. * Change Property examples type to Vec<Value> Updated the Property struct so that the examples field is now a Vec<Value> instead of Vec<String>, allowing for more flexible example types in JSON schema properties. * Bump version to 0.2.7 in project files Updated version numbers to 0.2.7 in Cargo.toml, Cargo.lock, and pyproject.toml for the mdmodels and mdmodels_core projects.
2 parents b67ad92 + 322f5ed commit e466bbf

7 files changed

Lines changed: 189 additions & 7 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "mdmodels"
33
authors = ["Jan Range <jan.range@simtech.uni-stuttgart.de>"]
44
description = "A tool to generate models, code and schemas from markdown files"
5-
version = "0.2.6"
5+
version = "0.2.7"
66
edition = "2021"
77
license = "MIT"
88
repository = "https://github.com/FAIRChemistry/md-models"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "maturin"
44

55
[project]
66
name = "mdmodels_core"
7-
version = "0.2.6"
7+
version = "0.2.7"
88
description = "A tool to generate models, code and schemas from markdown files"
99
requires-python = ">=3.8"
1010
classifiers = [

src/bin/cli.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use colored::Colorize;
2626
use log::error;
2727
use mdmodels::{
2828
datamodel::DataModel,
29+
error::DataModelError,
2930
exporters::{render_jinja_template, Templates},
3031
json::validation::validate_json,
3132
linkml::export::serialize_linkml,
@@ -34,7 +35,13 @@ use mdmodels::{
3435
};
3536
use serde::{Deserialize, Serialize};
3637
use std::{
37-
collections::HashMap, error::Error, fmt::Display, fs, io::Write, path::PathBuf, str::FromStr,
38+
collections::HashMap,
39+
error::Error,
40+
fmt::Display,
41+
fs,
42+
io::Write,
43+
path::{Path, PathBuf},
44+
str::FromStr,
3845
};
3946

4047
/// Command-line interface for MD-Models CLI.
@@ -246,7 +253,13 @@ fn validate(args: ValidateArgs) -> Result<(), Box<dyn Error>> {
246253

247254
let path = resolve_input_path(&args.input);
248255

249-
match DataModel::from_markdown(&path) {
256+
if is_json_schema(&path)? {
257+
return validate_from_json_schema(&path);
258+
}
259+
260+
let model = DataModel::from_markdown(&path);
261+
262+
match model {
250263
Ok(_) => {
251264
print_validation_result(true);
252265
Ok(())
@@ -259,6 +272,26 @@ fn validate(args: ValidateArgs) -> Result<(), Box<dyn Error>> {
259272
}
260273
}
261274

275+
/// Validates a JSON schema file.
276+
///
277+
/// # Arguments
278+
///
279+
/// * `path` - Path to the JSON schema file.
280+
fn validate_from_json_schema(path: &Path) -> Result<(), Box<dyn Error>> {
281+
if let Err(err) = DataModel::from_json_schema(path) {
282+
match err {
283+
DataModelError::ValidationError(validator) => {
284+
validator.log_result();
285+
Err("Model is invalid".into())
286+
}
287+
_ => Err(err.into()),
288+
}
289+
} else {
290+
print_validation_result(true);
291+
Ok(())
292+
}
293+
}
294+
262295
/// Prints the result of the validation.
263296
///
264297
/// # Arguments

src/json/export.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,7 @@ impl TryFrom<&Attribute> for schema::Property {
616616
enum_values,
617617
any_of: None,
618618
all_of: None,
619+
examples: Vec::new(),
619620
})
620621
}
621622
}

src/json/import.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,4 +930,27 @@ mod tests {
930930
assert_eq!(clean_key("Test__Hello"), "TEST_HELLO");
931931
assert_eq!(clean_key("!Test"), "TEST");
932932
}
933+
934+
#[test]
935+
fn test_additional_properties_object() {
936+
let schema = json!({
937+
"title": "Test",
938+
"type": "object",
939+
"properties": {
940+
"test": {
941+
"type": "string"
942+
}
943+
},
944+
"additionalProperties": {
945+
"type": "string"
946+
}
947+
});
948+
949+
let schema: SchemaObject = serde_json::from_value(schema).unwrap();
950+
let data_model = DataModel::try_from(schema.clone()).unwrap();
951+
952+
assert!(schema.additional_properties);
953+
assert_eq!(data_model.objects.len(), 1);
954+
assert_eq!(data_model.objects[0].attributes.len(), 1);
955+
}
933956
}

src/json/schema.rs

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ pub struct SchemaObject {
5454
pub schema: Option<String>,
5555
#[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
5656
pub id: Option<String>,
57-
#[serde(default = "generate_unique_title")]
57+
#[serde(
58+
default = "generate_unique_title",
59+
deserialize_with = "deserialize_title_with_whitespace_removal"
60+
)]
5861
pub title: String,
5962
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
6063
pub dtype: Option<DataType>,
@@ -70,7 +73,11 @@ pub struct SchemaObject {
7073
pub definitions: BTreeMap<String, SchemaType>,
7174
#[serde(default)]
7275
pub required: Vec<String>,
73-
#[serde(rename = "additionalProperties", default = "default_false")]
76+
#[serde(
77+
rename = "additionalProperties",
78+
default = "default_false",
79+
deserialize_with = "deserialize_additional_properties"
80+
)]
7481
pub additional_properties: bool,
7582
}
7683

@@ -118,6 +125,8 @@ pub struct Property {
118125
pub all_of: Option<Vec<Item>>,
119126
#[serde(skip_serializing_if = "skip_empty", rename = "enum")]
120127
pub enum_values: Option<Vec<String>>,
128+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
129+
pub examples: Vec<Value>,
121130
}
122131

123132
#[derive(Debug, Deserialize, Variantly, Clone)]
@@ -356,6 +365,80 @@ fn default_false() -> bool {
356365
false
357366
}
358367

368+
fn deserialize_title_with_whitespace_removal<'de, D>(deserializer: D) -> Result<String, D::Error>
369+
where
370+
D: serde::Deserializer<'de>,
371+
{
372+
use serde::de::{self, Visitor};
373+
use std::fmt;
374+
375+
struct TitleVisitor;
376+
377+
impl<'de> Visitor<'de> for TitleVisitor {
378+
type Value = String;
379+
380+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
381+
formatter.write_str("a string")
382+
}
383+
384+
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
385+
where
386+
E: de::Error,
387+
{
388+
Ok(value.chars().filter(|c| !c.is_whitespace()).collect())
389+
}
390+
391+
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
392+
where
393+
E: de::Error,
394+
{
395+
Ok(value.chars().filter(|c| !c.is_whitespace()).collect())
396+
}
397+
}
398+
399+
deserializer.deserialize_str(TitleVisitor)
400+
}
401+
402+
fn deserialize_additional_properties<'de, D>(deserializer: D) -> Result<bool, D::Error>
403+
where
404+
D: serde::Deserializer<'de>,
405+
{
406+
use serde::de::{self, Visitor};
407+
use serde_json::Value;
408+
use std::fmt;
409+
410+
struct AdditionalPropertiesVisitor;
411+
412+
impl<'de> Visitor<'de> for AdditionalPropertiesVisitor {
413+
type Value = bool;
414+
415+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
416+
formatter.write_str("a boolean or an object")
417+
}
418+
419+
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
420+
where
421+
E: de::Error,
422+
{
423+
Ok(value)
424+
}
425+
426+
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
427+
where
428+
M: de::MapAccess<'de>,
429+
{
430+
// Consume all entries in the map to avoid "trailing characters" error
431+
while map.next_entry::<String, Value>()?.is_some() {
432+
// Just consume and discard
433+
}
434+
// Any object/map means additionalProperties = true
435+
Ok(true)
436+
}
437+
}
438+
439+
deserializer.deserialize_any(AdditionalPropertiesVisitor)
440+
}
441+
359442
#[cfg(test)]
360443
mod tests {
361444
use super::*;
@@ -371,4 +454,46 @@ mod tests {
371454
assert_eq!(DataType::from_str("object").unwrap(), DataType::Object);
372455
assert_eq!(DataType::from_str("array").unwrap(), DataType::Array);
373456
}
457+
458+
#[test]
459+
/// Tests that title deserialization removes all whitespaces from the title string.
460+
fn test_title_whitespace_removal() {
461+
use serde_json;
462+
463+
// Test with spaces, tabs, and newlines
464+
let json_with_whitespace = r#"
465+
{
466+
"title": "My Test Title",
467+
"type": "object",
468+
"properties": {}
469+
}
470+
"#;
471+
472+
let schema: SchemaObject = serde_json::from_str(json_with_whitespace).unwrap();
473+
assert_eq!(schema.title, "MyTestTitle");
474+
475+
// Test with various whitespace characters
476+
let json_with_various_whitespace = r#"
477+
{
478+
"title": " My\t\nTest\r Title ",
479+
"type": "object",
480+
"properties": {}
481+
}
482+
"#;
483+
484+
let schema2: SchemaObject = serde_json::from_str(json_with_various_whitespace).unwrap();
485+
assert_eq!(schema2.title, "MyTestTitle");
486+
487+
// Test with no whitespace (should remain unchanged)
488+
let json_no_whitespace = r#"
489+
{
490+
"title": "MyTitle",
491+
"type": "object",
492+
"properties": {}
493+
}
494+
"#;
495+
496+
let schema3: SchemaObject = serde_json::from_str(json_no_whitespace).unwrap();
497+
assert_eq!(schema3.title, "MyTitle");
498+
}
374499
}

0 commit comments

Comments
 (0)