From 616598bfb8b6e0d0f0cc5319a84c5e55f6001c6c Mon Sep 17 00:00:00 2001 From: Jan Range <30547301+JR-1991@users.noreply.github.com> Date: Wed, 29 Jan 2025 23:15:17 +0100 Subject: [PATCH 1/5] added new export targets --- src/exporters.rs | 115 +++++++++++- src/pipeline.rs | 45 +++++ templates/golang.jinja | 203 +++++++++++++++++++++ templates/graphql.jinja | 96 ++++++++++ templates/protobuf.jinja | 80 ++++++++ templates/python-dataclass.jinja | 24 +++ templates/python-macros.jinja | 3 + templates/python-pydantic-xml.jinja | 34 ++++ templates/python-pydantic.jinja | 31 ++++ templates/rust.jinja | 125 +++++++++++++ templates/typescript-zod.jinja | 125 +++++++++++++ templates/typescript.jinja | 4 +- templates/xml-schema.jinja | 8 +- tests/data/expected_golang.go | 86 +++++++++ tests/data/expected_graphql.graphql | 52 ++++++ tests/data/expected_internal_schema.json | 55 +++--- tests/data/expected_mkdocs.md | 6 +- tests/data/expected_proto.proto | 49 +++++ tests/data/expected_protobuf.proto | 49 +++++ tests/data/expected_pydantic.py | 32 ++++ tests/data/expected_python_dc.py | 25 +++ tests/data/expected_python_pydantic_xml.py | 38 +++- tests/data/expected_rust.rs | 79 ++++++++ tests/data/expected_typescript.ts | 9 +- tests/data/expected_typescript_zod.ts | 72 ++++++++ tests/data/expected_xml_schema.xsd | 5 +- tests/data/model.md | 8 +- 27 files changed, 1415 insertions(+), 43 deletions(-) create mode 100644 templates/golang.jinja create mode 100644 templates/graphql.jinja create mode 100644 templates/protobuf.jinja create mode 100644 templates/rust.jinja create mode 100644 templates/typescript-zod.jinja create mode 100644 tests/data/expected_golang.go create mode 100644 tests/data/expected_graphql.graphql create mode 100644 tests/data/expected_proto.proto create mode 100644 tests/data/expected_protobuf.proto create mode 100644 tests/data/expected_rust.rs create mode 100644 tests/data/expected_typescript_zod.ts diff --git a/src/exporters.rs b/src/exporters.rs index ed1ac17..eb531b7 100644 --- a/src/exporters.rs +++ b/src/exporters.rs @@ -25,6 +25,7 @@ use std::{collections::HashMap, error::Error, fmt::Display, str::FromStr}; use crate::{datamodel::DataModel, markdown::frontmatter::FrontMatter}; use clap::ValueEnum; +use convert_case::{Case, Casing}; use lazy_static::lazy_static; use minijinja::{context, Environment}; use textwrap::wrap; @@ -71,6 +72,19 @@ lazy_static! { m.insert("bytes".to_string(), "string".to_string()); m }; + + /// Maps MD-Models type names to GraphQL-specific type names. + static ref GRAPHQL_TYPE_MAPS: std::collections::HashMap = { + let mut m = std::collections::HashMap::new(); + m.insert("integer".to_string(), "Int".to_string()); + m.insert("number".to_string(), "Float".to_string()); + m.insert("float".to_string(), "Float".to_string()); + m.insert("boolean".to_string(), "Boolean".to_string()); + m.insert("string".to_string(), "String".to_string()); + m.insert("bytes".to_string(), "String".to_string()); + m.insert("date".to_string(), "String".to_string()); + m + }; } /// Enumeration of available templates. @@ -91,6 +105,11 @@ pub enum Templates { MkDocs, Internal, Typescript, + TypescriptZod, + Rust, + Protobuf, + Graphql, + Golang, } impl Display for Templates { @@ -109,6 +128,11 @@ impl Display for Templates { Templates::MkDocs => write!(f, "mk-docs"), Templates::Internal => write!(f, "internal"), Templates::Typescript => write!(f, "typescript"), + Templates::TypescriptZod => write!(f, "typescript-zod"), + Templates::Rust => write!(f, "rust"), + Templates::Protobuf => write!(f, "protobuf"), + Templates::Graphql => write!(f, "graphql"), + Templates::Golang => write!(f, "golang"), } } } @@ -132,6 +156,11 @@ impl FromStr for Templates { "mk-docs" => Ok(Templates::MkDocs), "internal" => Ok(Templates::Internal), "typescript" => Ok(Templates::Typescript), + "typescript-zod" => Ok(Templates::TypescriptZod), + "rust" => Ok(Templates::Rust), + "protobuf" => Ok(Templates::Protobuf), + "graphql" => Ok(Templates::Graphql), + "golang" => Ok(Templates::Golang), _ => { let err = format!("Invalid template type: {}", s); Err(err.into()) @@ -163,6 +192,7 @@ pub fn render_jinja_template( match template { Templates::XmlSchema => convert_model_types(model, &XSD_TYPE_MAPS), Templates::Typescript => convert_model_types(model, &TYPESCRIPT_TYPE_MAPS), + Templates::Graphql => convert_model_types(model, &GRAPHQL_TYPE_MAPS), Templates::Shacl | Templates::Shex => { convert_model_types(model, &SHACL_TYPE_MAPS); filter_objects_wo_terms(model); @@ -176,6 +206,8 @@ pub fn render_jinja_template( // Add custom functions to the Jinja environment env.add_function("wrap", wrap_text); + env.add_filter("pascal_case", pascal_case); + env.add_filter("snake_case", snake_case); // Get the appropriate template let template = match template { @@ -189,6 +221,11 @@ pub fn render_jinja_template( Templates::PythonPydanticXML => env.get_template("python-pydantic-xml.jinja")?, Templates::MkDocs => env.get_template("mkdocs.jinja")?, Templates::Typescript => env.get_template("typescript.jinja")?, + Templates::TypescriptZod => env.get_template("typescript-zod.jinja")?, + Templates::Rust => env.get_template("rust.jinja")?, + Templates::Protobuf => env.get_template("protobuf.jinja")?, + Templates::Graphql => env.get_template("graphql.jinja")?, + Templates::Golang => env.get_template("golang.jinja")?, _ => { panic!( "The template is not available as a Jinja Template and should not be used using the jinja exporter. @@ -234,7 +271,14 @@ pub fn render_jinja_template( /// # Returns /// /// A string with the wrapped text. -fn wrap_text(text: &str, width: usize, initial_offset: &str, offset: &str) -> String { +fn wrap_text( + text: &str, + width: usize, + initial_offset: &str, + offset: &str, + delimiter: Option<&str>, +) -> String { + let delimiter = delimiter.unwrap_or(""); // Remove multiple spaces let options = textwrap::Options::new(width) .initial_indent(initial_offset) @@ -242,7 +286,19 @@ fn wrap_text(text: &str, width: usize, initial_offset: &str, offset: &str) -> St .width(width) .break_words(false); - wrap(remove_multiple_spaces(text).as_str(), options).join("\n") + wrap(remove_multiple_spaces(text).as_str(), options).join(&format!("{delimiter}\n")) +} + +/// Filter use only for Jinja templates. +/// Converts a string to PascalCase. +fn pascal_case(s: String) -> String { + s.to_case(Case::Pascal) +} + +/// Filter use only for Jinja templates. +/// Converts a string to snake_case. +fn snake_case(s: String) -> String { + s.to_case(Case::Snake) } /// Removes leading and trailing whitespace and multiple spaces from a string. @@ -438,6 +494,17 @@ mod tests { assert_eq!(rendered, expected); } + #[test] + fn test_convert_to_typescript_zod() { + // Arrange + let rendered = build_and_convert(Templates::TypescriptZod); + + // Assert + let expected = fs::read_to_string("tests/data/expected_typescript_zod.ts") + .expect("Could not read expected file"); + assert_eq!(rendered, expected); + } + #[test] fn test_convert_to_pydantic() { // Arrange @@ -448,4 +515,48 @@ mod tests { .expect("Could not read expected file"); assert_eq!(rendered, expected); } + + #[test] + fn test_convert_to_graphql() { + // Arrange + let rendered = build_and_convert(Templates::Graphql); + + // Assert + let expected = fs::read_to_string("tests/data/expected_graphql.graphql") + .expect("Could not read expected file"); + assert_eq!(rendered, expected); + } + + #[test] + fn test_convert_to_golang() { + // Arrange + let rendered = build_and_convert(Templates::Golang); + + // Assert + let expected = fs::read_to_string("tests/data/expected_golang.go") + .expect("Could not read expected file"); + assert_eq!(rendered, expected); + } + + #[test] + fn test_convert_to_rust() { + // Arrange + let rendered = build_and_convert(Templates::Rust); + + // Assert + let expected = fs::read_to_string("tests/data/expected_rust.rs") + .expect("Could not read expected file"); + assert_eq!(rendered, expected); + } + + #[test] + fn test_convert_to_protobuf() { + // Arrange + let rendered = build_and_convert(Templates::Protobuf); + + // Assert + let expected = fs::read_to_string("tests/data/expected_protobuf.proto") + .expect("Could not read expected file"); + assert_eq!(rendered, expected); + } } diff --git a/src/pipeline.rs b/src/pipeline.rs index 36ec090..e50fa3b 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -214,6 +214,51 @@ pub fn process_pipeline(path: &PathBuf) -> Result<(), Box Some(&specs.config), )?; } + Templates::TypescriptZod => { + serialize_by_template( + &specs.out, + paths, + &merge_state, + &template, + Some(&specs.config), + )?; + } + Templates::Rust => { + serialize_by_template( + &specs.out, + paths, + &merge_state, + &template, + Some(&specs.config), + )?; + } + Templates::Golang => { + serialize_by_template( + &specs.out, + paths, + &merge_state, + &template, + Some(&specs.config), + )?; + } + Templates::Protobuf => { + serialize_by_template( + &specs.out, + paths, + &merge_state, + &template, + Some(&specs.config), + )?; + } + Templates::Graphql => { + serialize_by_template( + &specs.out, + paths, + &merge_state, + &template, + Some(&specs.config), + )?; + } Templates::MkDocs => { // If the template is not set to merge, then disable the navigation. if let MergeState::Merge = merge_state { diff --git a/templates/golang.jinja b/templates/golang.jinja new file mode 100644 index 0000000..26df91b --- /dev/null +++ b/templates/golang.jinja @@ -0,0 +1,203 @@ +{# Helper macros #} +{% macro is_multiple(attr) %} + {%- if attr.multiple -%}[]{% endif -%} +{% endmacro %} + +{% macro get_type(attr, parent_name) %} + {%- if attr.dtypes | length > 1 -%} + {{ parent_name }}{{ attr.name | capitalize }}Type + {%- elif attr.dtypes[0] in object_names -%} + {{ attr.dtypes[0] }} + {%- elif attr.dtypes[0] == "string" -%} + string + {%- elif attr.dtypes[0] == "float" or attr.dtypes[0] == "number" -%} + float64 + {%- elif attr.dtypes[0] == "integer" -%} + int64 + {%- elif attr.dtypes[0] == "boolean" -%} + bool + {%- else -%} + {{ attr.dtypes[0] }} + {%- endif -%} +{% endmacro %} + +{% macro wrap_type(attr, parent_name) %} + {%- if attr.multiple -%} + []{{ get_type(attr, parent_name) }} + {%- else -%} + {{ get_type(attr, parent_name) }} + {%- endif -%} +{% endmacro %} + +// Package {% if title -%}{{ title | lower }}{% else %}model{% endif %} contains Go struct definitions with JSON serialization. +// +// WARNING: This is an auto-generated file. +// Do not edit directly - any changes will be overwritten. + +package {% if title -%}{{ title | lower }}{% else %}model{% endif %} + +import ( + "encoding/json" + "fmt" +) + +// +// Type definitions +// +{% for object in objects %} +{%- if object.docstring %} +// {{ object.name }} {{ wrap(object.docstring, 70, "", "// ", None) }} +{%- endif %} +type {{ object.name }} struct { + {%- for attr in object.attributes %} + {%- if attr.docstring %} + // {{ wrap(attr.docstring, 70, "", " // ", None) }} + {%- endif %} + {{ attr.name | capitalize }} {{ wrap_type(attr, object.name) }} `json:"{{ attr.name }}{% if attr.required is false %},omitempty{% endif %}"` + {%- endfor %} +} +{% endfor %} + +{%- if enums | length > 0 %} +// +// Enum definitions +// +{%- for enum in enums %} + +{%- if enum.docstring %} + // {{ enum.name }} {{ wrap(enum.docstring, 70, "", " // ", None) }} +{%- endif %} +type {{ enum.name }} string + +const ( + {%- for key, value in enum.mappings | dictsort %} + {{ enum.name }}{{ key }} {{ enum.name }} = "{{ value }}" + {%- endfor %} +) +{%- endfor %} +{%- endif %} + +// +// Type definitions for attributes with multiple types +// +{%- for object in objects %} +{%- for attr in object.attributes %} + {%- if attr.dtypes | length > 1 %} + +// {{ object.name }}{{ attr.name | capitalize }}Type represents a union type that can hold any of the following types: +{%- for dtype in attr.dtypes %} +// - {{ dtype }} +{%- endfor %} +type {{ object.name }}{{ attr.name | capitalize }}Type struct { + {%- for dtype in attr.dtypes %} + {%- if dtype in object_names %} + Object {{ dtype }} + {%- elif dtype == "string" %} + String string + {%- elif dtype == "float" %} + Float float64 + {%- elif dtype == "integer" %} + Integer int64 + {%- elif dtype == "boolean" %} + Boolean bool + {%- else %} + {{ dtype | capitalize }} {{ dtype }} + {%- endif %} + {%- endfor %} +} + +// UnmarshalJSON implements custom JSON unmarshaling for {{ object.name }}{{ attr.name | capitalize }}Type +func (t *{{ object.name }}{{ attr.name | capitalize }}Type) UnmarshalJSON(data []byte) error { + // Reset existing values + {%- for dtype in attr.dtypes %} + {%- if dtype in object_names %} + t.Object = {{ dtype }}{} + {%- elif dtype == "string" %} + t.String = "" + {%- elif dtype == "float" %} + t.Float = 0 + {%- elif dtype == "integer" %} + t.Integer = 0 + {%- elif dtype == "boolean" %} + t.Boolean = false + {%- else %} + t.{{ dtype | capitalize }} = {{ dtype }}{} + {%- endif %} + {%- endfor %} + + {%- for dtype in attr.dtypes %} + {%- if dtype in object_names %} + var objectValue {{ dtype }} + if err := json.Unmarshal(data, &objectValue); err == nil { + t.Object = objectValue + return nil + } + {%- elif dtype == "string" %} + var stringValue string + if err := json.Unmarshal(data, &stringValue); err == nil { + t.String = stringValue + return nil + } + {%- elif dtype == "float" %} + var floatValue float64 + if err := json.Unmarshal(data, &floatValue); err == nil { + t.Float = floatValue + return nil + } + {%- elif dtype == "integer" %} + var intValue int64 + if err := json.Unmarshal(data, &intValue); err == nil { + t.Integer = intValue + return nil + } + {%- elif dtype == "boolean" %} + var boolValue bool + if err := json.Unmarshal(data, &boolValue); err == nil { + t.Boolean = boolValue + return nil + } + {%- else %} + var value {{ dtype }} + if err := json.Unmarshal(data, &value); err == nil { + t.{{ dtype | capitalize }} = value + return nil + } + {%- endif %} + {%- endfor %} + return fmt.Errorf("{{ object.name }}{{ attr.name | capitalize }}Type: data is neither {{ attr.dtypes | join(', ') }}") +} + +// MarshalJSON implements custom JSON marshaling for {{ object.name }}{{ attr.name | capitalize }}Type +func (t {{ object.name }}{{ attr.name | capitalize }}Type) MarshalJSON() ([]byte, error) { + {%- for dtype in attr.dtypes %} + {%- if dtype in object_names %} + if t.Object != nil { + return json.Marshal(t.Object) + } + {%- elif dtype == "string" %} + if t.String != nil { + return json.Marshal(*t.String) + } + {%- elif dtype == "float" %} + if t.Float != nil { + return json.Marshal(*t.Float) + } + {%- elif dtype == "integer" %} + if t.Integer != nil { + return json.Marshal(*t.Integer) + } + {%- elif dtype == "boolean" %} + if t.Boolean != nil { + return json.Marshal(*t.Boolean) + } + {%- else %} + if t.{{ dtype | capitalize }} != nil { + return json.Marshal(*t.{{ dtype | capitalize }}) + } + {%- endif %} + {%- endfor %} + return []byte("null"), nil +} + {%- endif %} + {%- endfor %} +{%- endfor %} \ No newline at end of file diff --git a/templates/graphql.jinja b/templates/graphql.jinja new file mode 100644 index 0000000..6063400 --- /dev/null +++ b/templates/graphql.jinja @@ -0,0 +1,96 @@ +# This file contains GraphQL type definitions. +# +# WARNING: This is an auto-generated file. +# Do not edit directly - any changes will be overwritten. + +{% macro get_type(attr) %} + {%- if attr.dtypes | length > 1 -%} + {%- for dtype in attr.dtypes -%} + {%- if dtype in object_names -%} + {{- dtype -}} + {%- else -%} + {{- dtype -}} + {%- endif -%} + {%- if not loop.last %} | {% endif -%} + {%- endfor -%} + {%- else -%} + {%- if attr.dtypes[0] in object_names -%} + {{- attr.dtypes[0] -}} + {%- else -%} + {{- attr.dtypes[0] -}} + {%- endif -%} + {%- endif -%} +{% endmacro %} + +{% macro wrap_type(dtype, attr) %} + {%- if attr.multiple -%} + [{{ dtype }}] + {%- else -%} + {{ dtype }} + {%- endif -%} + {%- if attr.required -%}!{%- endif -%} +{% endmacro %} + +# Scalar wrapper types +{%- for object in objects -%} + {%- for attr in object.attributes -%} + {%- if attr.dtypes | length > 1 -%} + {%- for dtype in attr.dtypes -%} + {%- if dtype not in object_names %} +type {{ dtype }}Value { + value: {{ dtype }}! +} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- endfor -%} +{%- endfor %} + +# Union type definitions +{%- for object in objects -%} + {%- for attr in object.attributes -%} + {%- if attr.dtypes | length > 1 %} +union {{ object.name }}{{ attr.name | capitalize }} = {% for dtype in attr.dtypes %}{% if dtype in object_names %}{{ dtype }}{% else %}{{ dtype }}Value{% endif %}{% if not loop.last %} | {% endif %}{% endfor %} + + {%- endif -%} + {%- endfor -%} +{%- endfor %} + +# {% if title %}{{ title }}{% else %}Model{% endif %} Type definitions +{%- for object in objects %} +type {{ object.name }} { + {%- for attr in object.attributes %} + {{ attr.name }}: {% if attr.dtypes | length > 1 -%} + {{ wrap_type(object.name + attr.name | capitalize, attr) }} + {%- else -%} + {{ wrap_type(get_type(attr), attr) }} + {%- endif -%} + {%- endfor %} +} +{% endfor %} + +{%- if enums | length > 0 %} +# {% if title %}{{ title }}{% else %}Model{% endif %} Enum definitions +{%- for enum in enums %} +enum {{ enum.name }} { + {%- for key, value in enum.mappings | dictsort %} + {{ key }} # {{ value }} + {%- endfor %} +} +{% endfor %} +{%- endif %} + +# Query type definitions +type Query { + {%- for object in objects %} + + # {{ object.name }} queries + {{ object.name | lower }}(id: ID!): {{ object.name }} + all{{ object.name }}s: [{{ object.name }}] + {%- for attr in object.attributes %} + {%- if not attr.multiple and attr.dtypes | length == 1 and attr.dtypes[0] not in object_names %} + {{ object.name | lower }}By{{ attr.name | capitalize }}({{ attr.name }}: {{ get_type(attr) }}): [{{ object.name }}] + {%- endif %} + {%- endfor %} + {%- endfor %} +} diff --git a/templates/protobuf.jinja b/templates/protobuf.jinja new file mode 100644 index 0000000..3ac7d68 --- /dev/null +++ b/templates/protobuf.jinja @@ -0,0 +1,80 @@ +/** + * This file contains Protocol Buffer message definitions. + * + * Protocol Buffers (protobuf) is Google's language-neutral, platform-neutral, + * extensible mechanism for serializing structured data. + * + * WARNING: This is an auto-generated file. + * Do not edit directly - any changes will be overwritten. + */ + +{% macro get_type(attr) %} + {%- if attr.dtypes | length > 1 -%} + OneOf{{ attr.name | pascal_case }} + {%- elif attr.dtypes[0] in object_names -%} + {{ attr.dtypes[0] }} + {%- elif attr.dtypes[0] in enum_names -%} + {{ attr.dtypes[0] }} + {%- elif attr.dtypes[0] == "string" -%} + string + {%- elif attr.dtypes[0] == "float" -%} + double + {%- elif attr.dtypes[0] == "int" -%} + int32 + {%- elif attr.dtypes[0] == "bool" -%} + bool + {%- else -%} + {{ attr.dtypes[0] }} + {%- endif -%} +{% endmacro %} + +{% macro field_rule(attr) %} + {%- if attr.multiple -%} + repeated + {%- elif attr.required is false -%} + optional + {%- endif -%} +{% endmacro %} + +syntax = "proto3"; + +package {% if title %}{{ title | lower }}{% else %}model{% endif %}; + +{%- if enums | length > 0 %} +// +// {% if title %}{{ title }}{% else %}Model{% endif %} Enum definitions +// +{%- for enum in enums %} +enum {{ enum.name }} { + {%- for key, value in enum.mappings | dictsort %} + {{ key }} = {{ loop.index0 }}; // {{ value }} + {%- endfor %} +} +{%- endfor %} +{% endif %} + +// +// {% if title %}{{ title }}{% else %}Model{% endif %} Message definitions +// +// OneOf type definitions for attributes with multiple types +{%- for object in objects %} +{%- for attr in object.attributes %} +{%- if attr.dtypes | length > 1 %} +message OneOf{{ attr.name | pascal_case }} { + oneof value { + {%- for dtype in attr.dtypes %} + {{ get_type({'dtypes': [dtype]}) }} {{ dtype | snake_case }}_value = {{ loop.index }}; + {%- endfor %} + } +} +{% endif %} +{%- endfor %} +message {{ object.name }} { + {%- for attr in object.attributes %} + {%- if attr.docstring %} + // {{ wrap(attr.docstring, 70, "", " // ", None) }} + {%- endif %} + {{ field_rule(attr) }}{%- if not attr.required %} {% endif -%}{{ get_type(attr) }} {{ attr.name | snake_case }} = {{ loop.index }}; + {%- endfor %} +} +{% endfor %} diff --git a/templates/python-dataclass.jinja b/templates/python-dataclass.jinja index 2180dff..5726d0f 100644 --- a/templates/python-dataclass.jinja +++ b/templates/python-dataclass.jinja @@ -1,3 +1,27 @@ +""" +This file contains dataclass definitions for data validation. + +Dataclasses are a built-in Python library that provides a way to define data models +with type hints and automatic serialization to JSON. + +Usage example: +```python +from my_model import MyModel + +# Validates data at runtime +my_model = MyModel(name="John", age=30) + +# Type-safe - my_model has correct type hints +print(my_model.name) +``` + +For more information see: +https://docs.python.org/3/library/dataclasses.html + +WARNING: This is an auto-generated file. +Do not edit directly - any changes will be overwritten. +""" + {# This macro determines whether a given attributes default is a string #} diff --git a/templates/python-macros.jinja b/templates/python-macros.jinja index 3a5f7ed..338e843 100644 --- a/templates/python-macros.jinja +++ b/templates/python-macros.jinja @@ -11,6 +11,9 @@ default_factory=list, {% endif -%} tag="{{ xml_tag(attr.xml) }}", + {% if attr.docstring | length > 0 -%} + description="""{{ wrap(attr.docstring, 80, "", " ", None ) }}""", + {% endif -%} json_schema_extra={{ create_options(attr.options, attr.term) }} ) {% endmacro %} diff --git a/templates/python-pydantic-xml.jinja b/templates/python-pydantic-xml.jinja index f9fbf81..b1a3002 100644 --- a/templates/python-pydantic-xml.jinja +++ b/templates/python-pydantic-xml.jinja @@ -1,3 +1,34 @@ +""" +This file contains Pydantic XML model definitions for data validation. + +Pydantic is a data validation library that uses Python type annotations. +It allows you to define data models with type hints that are validated +at runtime while providing static type checking. + +Usage example: +```python +from my_model import MyModel + +# Validates data at runtime +my_model = MyModel(name="John", age=30) + +# Type-safe - my_model has correct type hints +print(my_model.name) + +# Will raise error if validation fails +try: + MyModel(name="", age=30) +except ValidationError as e: + print(e) +``` + +For more information see: +https://pydantic-xml.readthedocs.io/en/latest/ + +WARNING: This is an auto-generated file. +Do not edit directly - any changes will be overwritten. +""" + {% import "python-macros.jinja" as utils %} ## This is a generated file. Do not modify it manually! @@ -7,6 +38,9 @@ from typing import Dict, List, Optional from uuid import uuid4 from datetime import date, datetime from xml.dom import minidom +{% if enums | length > 0 -%} +from enum import Enum +{%- endif %} from lxml.etree import _Element from pydantic import PrivateAttr, model_validator diff --git a/templates/python-pydantic.jinja b/templates/python-pydantic.jinja index 9e5caae..5fa1a08 100644 --- a/templates/python-pydantic.jinja +++ b/templates/python-pydantic.jinja @@ -1,3 +1,34 @@ +""" +This file contains Pydantic model definitions for data validation. + +Pydantic is a data validation library that uses Python type annotations. +It allows you to define data models with type hints that are validated +at runtime while providing static type checking. + +Usage example: +```python +from my_model import MyModel + +# Validates data at runtime +my_model = MyModel(name="John", age=30) + +# Type-safe - my_model has correct type hints +print(my_model.name) + +# Will raise error if validation fails +try: + MyModel(name="", age=30) +except ValidationError as e: + print(e) +``` + +For more information see: +https://docs.pydantic.dev/ + +WARNING: This is an auto-generated file. +Do not edit directly - any changes will be overwritten. +""" + {# This macro determines whether a given attributes default is a string #} diff --git a/templates/rust.jinja b/templates/rust.jinja new file mode 100644 index 0000000..21180e8 --- /dev/null +++ b/templates/rust.jinja @@ -0,0 +1,125 @@ +{# Helper macros #} +{% macro is_multiple(attr) %} + {%- if attr.multiple -%}Vec<{%- endif -%} +{% endmacro %} + +{% macro is_multiple_end(attr) %} + {%- if attr.multiple -%}>{%- endif -%} +{% endmacro %} + +{% macro get_type(attr, parent_name) %} + {%- if attr.dtypes | length > 1 -%} + {{ parent_name }}{{ attr.name | capitalize }}Type + {%- elif attr.dtypes[0] in object_names -%} + {{ attr.dtypes[0] }} + {%- elif attr.dtypes[0] == "string" -%} + String + {%- elif attr.dtypes[0] == "float" -%} + f64 + {%- elif attr.dtypes[0] == "integer" -%} + i64 + {%- elif attr.dtypes[0] == "boolean" -%} + bool + {%- else -%} + {{ attr.dtypes[0] }} + {%- endif -%} +{% endmacro %} + +{% macro wrap_type(attr, parent_name) %} + {%- if attr.required is false -%} + Option<{{ is_multiple(attr) }}{{ get_type(attr, parent_name) }}{{ is_multiple_end(attr) }}> + {%- else -%} + {{ is_multiple(attr) }}{{ get_type(attr, parent_name) }}{{ is_multiple_end(attr) }} + {%- endif -%} +{% endmacro %} + +//! This file contains Rust struct definitions with serde serialization. +//! +//! WARNING: This is an auto-generated file. +//! Do not edit directly - any changes will be overwritten. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// JSON-LD base types +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct JsonLdContext(pub HashMap); + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct JsonLd { + #[serde(rename = "@context", skip_serializing_if = "Option::is_none")] + pub context: Option, + #[serde(rename = "@id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "@type", skip_serializing_if = "Option::is_none")] + pub type_: Option, +} + +// +// {% if title %}{{ title }}{% else %}Model{% endif %} Type definitions +// +{%- for object in objects %} + +{%- if object.docstring %} +/// {{ wrap(object.docstring, 70, "", "/// ", None) }} +{%- endif %} +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct {{ object.name }} { + #[serde(flatten)] + pub json_ld: JsonLd, + {%- for attr in object.attributes %} + {%- if attr.docstring %} + /// {{ wrap(attr.docstring, 70, "", " /// ", None) }} + {%- endif %} + {%- if attr.required is false %} + #[serde(skip_serializing_if = "Option::is_none")] + {%- endif %} + pub {{ attr.name }}: {{ wrap_type(attr, object.name) }}, + {%- endfor %} +} +{% endfor %} + +{%- if enums | length > 0 %} +// +// {% if title %}{{ title }}{% else %}Model{% endif %} Enum definitions +// +{%- for enum in enums %} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum {{ enum.name }} { + {%- for key, value in enum.mappings | dictsort %} + #[serde(rename = "{{ value }}")] + {{ key }}, + {%- endfor %} +} +{% endfor %} +{%- endif %} + +// +// Enum definitions for attributes with multiple types +// +{%- for object in objects %} +{%- for attr in object.attributes %} +{%- if attr.dtypes | length > 1 %} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum {{ object.name }}{{ attr.name | capitalize }}Type { + {%- for dtype in attr.dtypes %} + {%- if dtype in object_names %} + Object({{ dtype }}), + {%- elif dtype == "string" %} + String(String), + {%- elif dtype == "float" %} + Float(f64), + {%- elif dtype == "integer" %} + Integer(i64), + {%- elif dtype == "boolean" %} + Boolean(bool), + {%- else %} + {{ dtype | capitalize }}({{ dtype }}), + {%- endif %} + {%- endfor %} +} +{%- endif %} +{%- endfor %} +{%- endfor %} diff --git a/templates/typescript-zod.jinja b/templates/typescript-zod.jinja new file mode 100644 index 0000000..0f43e2b --- /dev/null +++ b/templates/typescript-zod.jinja @@ -0,0 +1,125 @@ +/** + * This file contains Zod schema definitions for data validation. + * + * Zod is a TypeScript-first schema declaration and validation library. + * It allows you to create schemas that validate data at runtime while + * providing static type inference. + * + * Usage example: + * ```typescript + * import { TestSchema } from './schemas'; + * + * // Validates data at runtime + * const result = TestSchema.parse(data); + * + * // Type-safe - result has correct TypeScript types + * console.log(result.name); + * + * // Will throw error if validation fails + * try { + * TestSchema.parse(invalidData); + * } catch (err) { + * console.error(err); + * } + * ``` + * + * @see https://github.com/colinhacks/zod + * + * WARNING: This is an auto-generated file. + * Do not edit directly - any changes will be overwritten. + */ + + +{% macro is_multiple(attr) %} + {%- if attr.multiple -%}[]{%- endif -%} +{% endmacro %} + +{% macro get_type(attr) %} + {%- if attr.dtypes | length == 1 -%} + {%- if attr.dtypes[0] in object_names -%} + {{ attr.dtypes[0] }} + {%- else -%} + {{ attr.dtypes[0] }} + {%- endif -%} + {%- else -%} + union + {%- endif -%} +{% endmacro %} + +{# New macros for Zod schema types #} +{% macro zod_type(dtype, attr) %} + {%- if attr.dtypes | length > 1 -%} + z.union([ + {%- for type in attr.dtypes -%} + {%- if type in object_names -%} + {{ type }}Schema + {%- elif type in enum_names -%} + {{ type }}Schema + {%- elif type == "float" -%} + z.number() + {%- else -%} + z.{{ type | lower }}() + {%- endif -%} + {%- if not loop.last %}, {% endif -%} + {%- endfor -%} + ]) + {%- else -%} + {%- if dtype in object_names -%} + {{ dtype }}Schema + {%- elif dtype in enum_names -%} + {{ dtype }}Schema + {%- elif dtype == "float" -%} + z.number() + {%- else -%} + z.{{ dtype | lower }}() + {%- endif -%} + {%- endif -%} +{% endmacro %} + +{% macro wrap_zod_type(dtype, attr) %} + {%- if attr.multiple -%} + z.array({{ zod_type(dtype, attr) }}) + {%- elif attr.required is false -%} + {{ zod_type(dtype, attr) }}.nullable() + {%- else -%} + {{ zod_type(dtype, attr) }} + {%- endif -%} +{% endmacro %} + +{# Code structure starts here #} +import { z } from 'zod'; + +// JSON-LD Types +export const JsonLdContextSchema = z.record(z.any()); + +export const JsonLdSchema = z.object({ + '@context': JsonLdContextSchema.optional(), + '@id': z.string().optional(), + '@type': z.string().optional(), +}); + +// {% if title %}{{ title }}{% else %}Model{% endif %} Type definitions +{%- for object in objects %} +export const {{ object.name }}Schema = z.lazy(() => JsonLdSchema.extend({ + {%- for attr in object.attributes %} + {{ attr.name }}: {{ wrap_zod_type(get_type(attr), attr) }}{% if attr.docstring %}.describe(` + {{ wrap(attr.docstring, 70, "", " ", None) }} + `){% endif %}, + {%- endfor %} +})); + +export type {{ object.name }} = z.infer; +{% endfor %} + +{%- if enums | length > 0 %} +// {% if title %}{{ title }}{% else %}Model{% endif %} Enum definitions +{%- for enum in enums %} +export enum {{ enum.name }} { + {%- for key, value in enum.mappings | dictsort %} + {{ key }} = '{{ value }}', + {%- endfor %} +} + +export const {{ enum.name }}Schema = z.nativeEnum({{ enum.name }}); +{% endfor %} +{% endif %} diff --git a/templates/typescript.jinja b/templates/typescript.jinja index 8acfeda..539e7b9 100644 --- a/templates/typescript.jinja +++ b/templates/typescript.jinja @@ -81,7 +81,7 @@ export interface JsonLd { {% endif %} {%- for attr in object.attributes %} - * @param {{ attr.name }} {%- if attr.docstring %} - {{ wrap(attr.docstring, 70, "", " ") }}{%- endif %} + * @param {{ attr.name }} {%- if attr.docstring %} - {{ wrap(attr.docstring, 70, "", " ", None) }}{%- endif %} {%- endfor %} **/ export interface {{ object.name }} extends JsonLd { @@ -103,7 +103,7 @@ export const {{ object.name }}Codec = D.lazy("{{ object.name }}", () => D.struct {%- for enum in enums %} {%- if enum.docstring %} /** - * {{ wrap(enum.docstring, 70, " ", " ") }} + * {{ wrap(enum.docstring, 70, " ", " ", None) }} **/ {%- endif %} export enum {{ enum.name }} { diff --git a/templates/xml-schema.jinja b/templates/xml-schema.jinja index ad68d83..dddaef5 100644 --- a/templates/xml-schema.jinja +++ b/templates/xml-schema.jinja @@ -42,7 +42,7 @@ > - {{ attribute.docstring }} + {{ wrap(attribute.docstring, 70, "", " ", None) }} @@ -65,7 +65,7 @@ {%- if attribute.docstring | length > 0 %} - {{ attribute.docstring }} + {{ wrap(attribute.docstring, 70, "", " ", None) }} {%- endif %} @@ -82,7 +82,7 @@ - {{ attribute.docstring }} + {{ wrap(attribute.docstring, 70, "", " ", None) }} @@ -99,7 +99,7 @@ > - {{ attribute.docstring }} + {{ wrap(attribute.docstring, 70, "", " ", None) }} diff --git a/tests/data/expected_golang.go b/tests/data/expected_golang.go new file mode 100644 index 0000000..ec3f88a --- /dev/null +++ b/tests/data/expected_golang.go @@ -0,0 +1,86 @@ +// Package model contains Go struct definitions with JSON serialization. +// +// WARNING: This is an auto-generated file. +// Do not edit directly - any changes will be overwritten. + +package model + +import ( + "encoding/json" + "fmt" +) + +// +// Type definitions +// + +// Test Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do +// eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim +// ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut +// aliquip ex ea commodo consequat. +type Test struct { + // The name of the test. This is a unique identifier that helps track + // individual test cases across the system. It should be + // descriptive and follow the standard naming conventions. + Name string `json:"name"` + Number TestNumberType `json:"number,omitempty"` + Test2 []Test2 `json:"test2,omitempty"` + Ontology Ontology `json:"ontology,omitempty"` +} + +type Test2 struct { + Names []string `json:"names,omitempty"` + Number float64 `json:"number,omitempty"` +} + +// +// Enum definitions +// +type Ontology string + +const ( + OntologyECO Ontology = "https://www.evidenceontology.org/term/" + OntologyGO Ontology = "https://amigo.geneontology.org/amigo/term/" + OntologySIO Ontology = "http://semanticscience.org/resource/" +) + +// +// Type definitions for attributes with multiple types +// + +// TestNumberType represents a union type that can hold any of the following types: +// - float +// - string +type TestNumberType struct { + Float float64 + String string +} + +// UnmarshalJSON implements custom JSON unmarshaling for TestNumberType +func (t *TestNumberType) UnmarshalJSON(data []byte) error { + // Reset existing values + t.Float = 0 + t.String = "" + var floatValue float64 + if err := json.Unmarshal(data, &floatValue); err == nil { + t.Float = floatValue + return nil + } + var stringValue string + if err := json.Unmarshal(data, &stringValue); err == nil { + t.String = stringValue + return nil + } + return fmt.Errorf("TestNumberType: data is neither float, string") +} + +// MarshalJSON implements custom JSON marshaling for TestNumberType +func (t TestNumberType) MarshalJSON() ([]byte, error) { + if t.Float != nil { + return json.Marshal(*t.Float) + } + if t.String != nil { + return json.Marshal(*t.String) + } + return []byte("null"), nil +} \ No newline at end of file diff --git a/tests/data/expected_graphql.graphql b/tests/data/expected_graphql.graphql new file mode 100644 index 0000000..4e7163b --- /dev/null +++ b/tests/data/expected_graphql.graphql @@ -0,0 +1,52 @@ +# This file contains GraphQL type definitions. +# +# WARNING: This is an auto-generated file. +# Do not edit directly - any changes will be overwritten. + + +# Scalar wrapper types +type FloatValue { + value: Float! +} +type StringValue { + value: String! +} + +# Union type definitions +union TestNumber = FloatValue | StringValue + +# Model Type definitions +type Test { + name: String! + number: TestNumber + test2: [Test2] + ontology: Ontology +} + +type Test2 { + names: [String] + number: Float +} + +# Model Enum definitions +enum Ontology { + ECO # https://www.evidenceontology.org/term/ + GO # https://amigo.geneontology.org/amigo/term/ + SIO # http://semanticscience.org/resource/ +} + + +# Query type definitions +type Query { + + # Test queries + test(id: ID!): Test + allTests: [Test] + testByName(name: String): [Test] + testByOntology(ontology: Ontology): [Test] + + # Test2 queries + test2(id: ID!): Test2 + allTest2s: [Test2] + test2ByNumber(number: Float): [Test2] +} \ No newline at end of file diff --git a/tests/data/expected_internal_schema.json b/tests/data/expected_internal_schema.json index 12094fa..79934ea 100644 --- a/tests/data/expected_internal_schema.json +++ b/tests/data/expected_internal_schema.json @@ -10,7 +10,7 @@ "dtypes": [ "string" ], - "docstring": "The name of the test.", + "docstring": "The name of the test. This is a unique identifier that helps track individual test cases across the system. It should be descriptive and follow the standard naming conventions.", "options": [], "term": "schema:hello", "required": true, @@ -20,14 +20,14 @@ }, "is_enum": false, "position": { - "line": 13, + "line": 15, "column": { "start": 1, "end": 11 }, "offset": { - "start": 166, - "end": 458 + "start": 399, + "end": 864 } } }, @@ -36,7 +36,8 @@ "multiple": false, "is_id": false, "dtypes": [ - "float" + "float", + "string" ], "docstring": "", "options": [], @@ -49,14 +50,14 @@ }, "is_enum": false, "position": { - "line": 18, + "line": 22, "column": { "start": 1, "end": 9 }, "offset": { - "start": 275, - "end": 355 + "start": 673, + "end": 761 } } }, @@ -77,14 +78,14 @@ }, "is_enum": false, "position": { - "line": 23, + "line": 27, "column": { "start": 1, "end": 8 }, "offset": { - "start": 355, - "end": 427 + "start": 761, + "end": 833 } } }, @@ -105,19 +106,19 @@ }, "is_enum": true, "position": { - "line": 27, + "line": 31, "column": { "start": 1, "end": 11 }, "offset": { - "start": 427, - "end": 458 + "start": 833, + "end": 864 } } } ], - "docstring": "", + "docstring": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", "position": { "line": 11, "column": { @@ -150,14 +151,14 @@ }, "is_enum": false, "position": { - "line": 32, + "line": 36, "column": { "start": 1, "end": 8 }, "offset": { - "start": 469, - "end": 613 + "start": 875, + "end": 1019 } } }, @@ -183,28 +184,28 @@ }, "is_enum": false, "position": { - "line": 36, + "line": 40, "column": { "start": 1, "end": 9 }, "offset": { - "start": 533, - "end": 613 + "start": 939, + "end": 1019 } } } ], "docstring": "", "position": { - "line": 30, + "line": 34, "column": { "start": 1, "end": 10 }, "offset": { - "start": 458, - "end": 468 + "start": 864, + "end": 874 } } } @@ -219,14 +220,14 @@ }, "docstring": "", "position": { - "line": 45, + "line": 49, "column": { "start": 1, "end": 13 }, "offset": { - "start": 630, - "end": 643 + "start": 1036, + "end": 1049 } } } diff --git a/tests/data/expected_mkdocs.md b/tests/data/expected_mkdocs.md index 78d2ca6..7108f12 100644 --- a/tests/data/expected_mkdocs.md +++ b/tests/data/expected_mkdocs.md @@ -29,14 +29,14 @@ This page provides comprehensive information about the structure and components ### Test - +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. __name__* `string` -- The name of the test. +- The name of the test. This is a unique identifier that helps track individual test cases across the system. It should be descriptive and follow the standard naming conventions. -__number__ `float` +__number__ `float``string` - `Default`: 1.0 diff --git a/tests/data/expected_proto.proto b/tests/data/expected_proto.proto new file mode 100644 index 0000000..45f52c6 --- /dev/null +++ b/tests/data/expected_proto.proto @@ -0,0 +1,49 @@ +/** + * This file contains Protocol Buffer message definitions. + * + * Protocol Buffers (protobuf) is Google's language-neutral, platform-neutral, + * extensible mechanism for serializing structured data. + * + * WARNING: This is an auto-generated file. + * Do not edit directly - any changes will be overwritten. + */ + + +syntax = "proto3"; + +package model; +// +// Model Enum definitions +// +enum Ontology { + ECO = 0; // https://www.evidenceontology.org/term/ + GO = 1; // https://amigo.geneontology.org/amigo/term/ + SIO = 2; // http://semanticscience.org/resource/ +} + + +// +// Model Message definitions +// +// OneOf type definitions for attributes with multiple types +message OneOfNumber { + oneof value { + double float_value = 1; + string string_value = 2; + } +} + +message Test { + // The name of the test. This is a unique identifier that helps track + // individual test cases across the system. It should be descriptive + // and follow the standard naming conventions. + string name = 1; + optional OneOfNumber number = 2; + repeated Test2 test_2 = 3; + optional Ontology ontology = 4; +} + +message Test2 { + repeated string names = 1; + optional double number = 2; +} \ No newline at end of file diff --git a/tests/data/expected_protobuf.proto b/tests/data/expected_protobuf.proto new file mode 100644 index 0000000..45f52c6 --- /dev/null +++ b/tests/data/expected_protobuf.proto @@ -0,0 +1,49 @@ +/** + * This file contains Protocol Buffer message definitions. + * + * Protocol Buffers (protobuf) is Google's language-neutral, platform-neutral, + * extensible mechanism for serializing structured data. + * + * WARNING: This is an auto-generated file. + * Do not edit directly - any changes will be overwritten. + */ + + +syntax = "proto3"; + +package model; +// +// Model Enum definitions +// +enum Ontology { + ECO = 0; // https://www.evidenceontology.org/term/ + GO = 1; // https://amigo.geneontology.org/amigo/term/ + SIO = 2; // http://semanticscience.org/resource/ +} + + +// +// Model Message definitions +// +// OneOf type definitions for attributes with multiple types +message OneOfNumber { + oneof value { + double float_value = 1; + string string_value = 2; + } +} + +message Test { + // The name of the test. This is a unique identifier that helps track + // individual test cases across the system. It should be descriptive + // and follow the standard naming conventions. + string name = 1; + optional OneOfNumber number = 2; + repeated Test2 test_2 = 3; + optional Ontology ontology = 4; +} + +message Test2 { + repeated string names = 1; + optional double number = 2; +} \ No newline at end of file diff --git a/tests/data/expected_pydantic.py b/tests/data/expected_pydantic.py index 2688744..36884fd 100644 --- a/tests/data/expected_pydantic.py +++ b/tests/data/expected_pydantic.py @@ -1,3 +1,35 @@ +""" +This file contains Pydantic model definitions for data validation. + +Pydantic is a data validation library that uses Python type annotations. +It allows you to define data models with type hints that are validated +at runtime while providing static type checking. + +Usage example: +```python +from my_model import MyModel + +# Validates data at runtime +my_model = MyModel(name="John", age=30) + +# Type-safe - my_model has correct type hints +print(my_model.name) + +# Will raise error if validation fails +try: + MyModel(name="", age=30) +except ValidationError as e: + print(e) +``` + +For more information see: +https://docs.pydantic.dev/ + +WARNING: This is an auto-generated file. +Do not edit directly - any changes will be overwritten. +""" + + ## This is a generated file. Do not modify it manually! from __future__ import annotations diff --git a/tests/data/expected_python_dc.py b/tests/data/expected_python_dc.py index d4d6d8a..2e53ba0 100644 --- a/tests/data/expected_python_dc.py +++ b/tests/data/expected_python_dc.py @@ -1,3 +1,28 @@ +""" +This file contains dataclass definitions for data validation. + +Dataclasses are a built-in Python library that provides a way to define data models +with type hints and automatic serialization to JSON. + +Usage example: +```python +from my_model import MyModel + +# Validates data at runtime +my_model = MyModel(name="John", age=30) + +# Type-safe - my_model has correct type hints +print(my_model.name) +``` + +For more information see: +https://docs.python.org/3/library/dataclasses.html + +WARNING: This is an auto-generated file. +Do not edit directly - any changes will be overwritten. +""" + + ## This is a generated file. Do not modify it manually! from __future__ import annotations diff --git a/tests/data/expected_python_pydantic_xml.py b/tests/data/expected_python_pydantic_xml.py index b19f910..0cdd3ac 100644 --- a/tests/data/expected_python_pydantic_xml.py +++ b/tests/data/expected_python_pydantic_xml.py @@ -1,3 +1,35 @@ +""" +This file contains Pydantic XML model definitions for data validation. + +Pydantic is a data validation library that uses Python type annotations. +It allows you to define data models with type hints that are validated +at runtime while providing static type checking. + +Usage example: +```python +from my_model import MyModel + +# Validates data at runtime +my_model = MyModel(name="John", age=30) + +# Type-safe - my_model has correct type hints +print(my_model.name) + +# Will raise error if validation fails +try: + MyModel(name="", age=30) +except ValidationError as e: + print(e) +``` + +For more information see: +https://pydantic-xml.readthedocs.io/en/latest/ + +WARNING: This is an auto-generated file. +Do not edit directly - any changes will be overwritten. +""" + + ## This is a generated file. Do not modify it manually! from __future__ import annotations @@ -5,6 +37,7 @@ from uuid import uuid4 from datetime import date, datetime from xml.dom import minidom +from enum import Enum from lxml.etree import _Element from pydantic import PrivateAttr, model_validator @@ -17,10 +50,13 @@ class Test( ): name: str = attr( tag="name", + description="""The name of the test. This is a unique identifier that helps track individual + test cases across the system. It should be descriptive and follow + the standard naming conventions.""", json_schema_extra=dict(term = "schema:hello",) ) - number: Optional[float] = attr( + number: Union[None,float,str] = attr( default=1.0, tag="number", json_schema_extra=dict(term = "schema:one",) diff --git a/tests/data/expected_rust.rs b/tests/data/expected_rust.rs new file mode 100644 index 0000000..55bf656 --- /dev/null +++ b/tests/data/expected_rust.rs @@ -0,0 +1,79 @@ +//! This file contains Rust struct definitions with serde serialization. +//! +//! WARNING: This is an auto-generated file. +//! Do not edit directly - any changes will be overwritten. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// JSON-LD base types +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct JsonLdContext(pub HashMap); + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct JsonLd { + #[serde(rename = "@context", skip_serializing_if = "Option::is_none")] + pub context: Option, + #[serde(rename = "@id", skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(rename = "@type", skip_serializing_if = "Option::is_none")] + pub type_: Option, +} + +// +// Model Type definitions +// +/// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do +/// eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut +/// enim ad minim veniam, quis nostrud exercitation ullamco laboris +/// nisi ut aliquip ex ea commodo consequat. +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Test { + #[serde(flatten)] + pub json_ld: JsonLd, + /// The name of the test. This is a unique identifier that helps track + /// individual test cases across the system. It should be + /// descriptive and follow the standard naming conventions. + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub number: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub test2: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub ontology: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Test2 { + #[serde(flatten)] + pub json_ld: JsonLd, + #[serde(skip_serializing_if = "Option::is_none")] + pub names: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub number: Option, +} + +// +// Model Enum definitions +// + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Ontology { + #[serde(rename = "https://www.evidenceontology.org/term/")] + ECO, + #[serde(rename = "https://amigo.geneontology.org/amigo/term/")] + GO, + #[serde(rename = "http://semanticscience.org/resource/")] + SIO, +} + + +// +// Enum definitions for attributes with multiple types +// + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TestNumberType { + Float(f64), + String(String), +} \ No newline at end of file diff --git a/tests/data/expected_typescript.ts b/tests/data/expected_typescript.ts index 33473a1..16c9ee8 100644 --- a/tests/data/expected_typescript.ts +++ b/tests/data/expected_typescript.ts @@ -23,7 +23,14 @@ export interface JsonLd { // none Type definitions /** - * @param name - The name of the test. + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut + enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat. + + * @param name - The name of the test. This is a unique identifier that helps track + individual test cases across the system. It should be + descriptive and follow the standard naming conventions. * @param number * @param test2 * @param ontology diff --git a/tests/data/expected_typescript_zod.ts b/tests/data/expected_typescript_zod.ts new file mode 100644 index 0000000..494590c --- /dev/null +++ b/tests/data/expected_typescript_zod.ts @@ -0,0 +1,72 @@ +/** + * This file contains Zod schema definitions for data validation. + * + * Zod is a TypeScript-first schema declaration and validation library. + * It allows you to create schemas that validate data at runtime while + * providing static type inference. + * + * Usage example: + * ```typescript + * import { TestSchema } from './schemas'; + * + * // Validates data at runtime + * const result = TestSchema.parse(data); + * + * // Type-safe - result has correct TypeScript types + * console.log(result.name); + * + * // Will throw error if validation fails + * try { + * TestSchema.parse(invalidData); + * } catch (err) { + * console.error(err); + * } + * ``` + * + * @see https://github.com/colinhacks/zod + * + * WARNING: This is an auto-generated file. + * Do not edit directly - any changes will be overwritten. + */ + + +import { z } from 'zod'; + +// JSON-LD Types +export const JsonLdContextSchema = z.record(z.any()); + +export const JsonLdSchema = z.object({ + '@context': JsonLdContextSchema.optional(), + '@id': z.string().optional(), + '@type': z.string().optional(), +}); + +// Model Type definitions +export const TestSchema = z.lazy(() => JsonLdSchema.extend({ + name: z.string().describe(` + The name of the test. This is a unique identifier that helps track + individual test cases across the system. It should be descriptive + and follow the standard naming conventions. + `), + number: z.union([z.number(), z.string()]).nullable(), + test2: z.array(Test2Schema), + ontology: OntologySchema.nullable(), +})); + +export type Test = z.infer; + +export const Test2Schema = z.lazy(() => JsonLdSchema.extend({ + names: z.array(z.string()), + number: z.number().nullable(), +})); + +export type Test2 = z.infer; + +// Model Enum definitions +export enum Ontology { + ECO = 'https://www.evidenceontology.org/term/', + GO = 'https://amigo.geneontology.org/amigo/term/', + SIO = 'http://semanticscience.org/resource/', +} + +export const OntologySchema = z.nativeEnum(Ontology); \ No newline at end of file diff --git a/tests/data/expected_xml_schema.xsd b/tests/data/expected_xml_schema.xsd index 0f4a190..bb2b054 100644 --- a/tests/data/expected_xml_schema.xsd +++ b/tests/data/expected_xml_schema.xsd @@ -20,7 +20,10 @@ - The name of the test. + The name of the test. This is a unique identifier that helps track + individual test cases across the system. It should + be descriptive and follow the standard naming + conventions. diff --git a/tests/data/model.md b/tests/data/model.md index a04b995..e65479a 100644 --- a/tests/data/model.md +++ b/tests/data/model.md @@ -10,13 +10,17 @@ nsmap: ### Test +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + - __name__ - Type: Identifier - Term: schema:hello - - Description: The name of the test. + - Description: The name of the test. This is a unique identifier + that helps track individual test cases across the system. + It should be descriptive and follow the standard naming conventions. - XML: @name - number - - Type: float + - Type: float, string - Term: schema:one - XML: @number - Default: 1.0 From 21398e8e98462a5c860fc8e4d098f0d9cd0fa6b8 Mon Sep 17 00:00:00 2001 From: Jan Range <30547301+JR-1991@users.noreply.github.com> Date: Wed, 29 Jan 2025 23:15:36 +0100 Subject: [PATCH 2/5] change to public for external usage --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e23aafc..6b6832f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,9 +33,9 @@ pub mod exporters; pub mod pipeline; pub mod validation; -pub(crate) mod attribute; -pub(crate) mod object; -pub(crate) mod xmltype; +pub mod attribute; +pub mod object; +pub mod xmltype; pub mod prelude { pub use crate::datamodel::DataModel; @@ -51,7 +51,7 @@ pub mod json { } pub(crate) mod markdown { - pub(crate) mod frontmatter; + pub mod frontmatter; pub(crate) mod parser; pub(crate) mod position; } From a1b9a0cebf6201a8b3a93199bed39db3ee717137 Mon Sep 17 00:00:00 2001 From: Jan Range <30547301+JR-1991@users.noreply.github.com> Date: Wed, 29 Jan 2025 23:15:50 +0100 Subject: [PATCH 3/5] collect multi-lined descriptions --- src/markdown/parser.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/markdown/parser.rs b/src/markdown/parser.rs index b957bfa..b53901a 100644 --- a/src/markdown/parser.rs +++ b/src/markdown/parser.rs @@ -430,6 +430,12 @@ fn extract_attribute_options(iterator: &mut OffsetIter) -> Vec { let last_option = options.last_mut().unwrap(); *last_option = format!("{}[]", last_option); } + Event::Text(text) if text.to_string() != "]" => { + let last_option = options.last_mut().unwrap(); + if last_option.to_lowercase().contains("description:") { + *last_option = format!("{} {}", last_option.trim(), text); + } + } _ => {} } } From 8174e5eab5a19bfb4c2284dccaf7853465946dc8 Mon Sep 17 00:00:00 2001 From: Jan Range <30547301+JR-1991@users.noreply.github.com> Date: Wed, 29 Jan 2025 23:16:01 +0100 Subject: [PATCH 4/5] update list of export targets --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 539dea6..360ca23 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # MD-Models ![Crates.io Version](https://img.shields.io/crates/v/mdmodels) ![NPM Version](https://img.shields.io/npm/v/mdmodels-core) @@ -73,10 +72,19 @@ The following templates are available: - `python-dataclass`: Python dataclass implementation with JSON-LD support - `python-pydantic`: PyDantic implementation with JSON-LD support - `python-pydantic-xml`: PyDantic implementation with XML support +- `typescript`: TypeScript interface definitions with JSON-LD support +- `typescript-zod`: TypeScript Zod schema definitions +- `rust`: Rust struct definitions with serde support +- `golang`: Go struct definitions +- `protobuf`: Protocol Buffer schema definition +- `graphql`: GraphQL schema definition - `xml-schema`: XML schema definition - `json-schema`: JSON schema definition +- `json-schema-all`: Multiple JSON schema definitions (one per object) - `shacl`: SHACL shapes definition - `shex`: ShEx shapes definition +- `compact-markdown`: Compact markdown representation +- `mkdocs`: MkDocs documentation format ## Installation options From 73280ad19f388c190174daa73045fe6a0bcc5124 Mon Sep 17 00:00:00 2001 From: Jan Range <30547301+JR-1991@users.noreply.github.com> Date: Wed, 29 Jan 2025 23:16:28 +0100 Subject: [PATCH 5/5] bump feature version --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7cc12f7..a7e4706 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,12 +302,12 @@ checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "colored" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1158,7 +1158,7 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "mdmodels" -version = "0.1.8" +version = "0.2.0" dependencies = [ "assert_cmd", "clap", diff --git a/Cargo.toml b/Cargo.toml index 1cf0605..443c860 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "mdmodels" authors = ["Jan Range "] description = "A tool to generate models, code and schemas from markdown files" -version = "0.1.8" +version = "0.2.0" edition = "2021" license = "MIT" repository = "https://github.com/FAIRChemistry/md-models" diff --git a/pyproject.toml b/pyproject.toml index 68e7911..a13bfff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "mdmodels_core" -version = "0.1.8" +version = "0.2.0" description = "A tool to generate models, code and schemas from markdown files" requires-python = ">=3.8" classifiers = [