Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions avrotize/avrotocsharp.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ def __init__(self, base_namespace: str = '') -> None:
self.cbor_annotation = False
self.avro_annotation = False
self.protobuf_net_annotation = False
self.use_optional = False
self.use_ivalidatableobject = False
self.generated_types: Dict[str,str] = {}
self.generated_avro_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {}
self.type_dict: Dict[str, Dict] = {}
Expand Down Expand Up @@ -851,6 +853,30 @@ def write_to_file(self, namespace: str, name: str, definition: str):
file_content += definition
file.write(file_content)

def generate_option_class(self, output_dir: str) -> None:
""" Generates Option<T> class for optional properties when use_optional is enabled """
if not self.use_optional:
return # Not using Option<T> pattern

# Convert base namespace to PascalCase for consistency with other generated classes
namespace_pascal = pascal(self.base_namespace)

# Generate the Option class
option_definition = process_template(
"avrotocsharp/option.cs.jinja",
namespace=namespace_pascal
)

# Write to the same directory structure as other classes (using PascalCase path)
directory_path = os.path.join(
output_dir, os.path.join('src', namespace_pascal.replace('.', os.sep)))
if not os.path.exists(directory_path):
os.makedirs(directory_path, exist_ok=True)
option_file_path = os.path.join(directory_path, "Option.cs")

with open(option_file_path, 'w', encoding='utf-8') as option_file:
option_file.write(option_definition)

def generate_tests(self, output_dir: str) -> None:
""" Generates unit tests for all the generated C# classes and enums """
test_directory_path = os.path.join(output_dir, "test")
Expand Down Expand Up @@ -1100,6 +1126,10 @@ def convert_schema(self, schema: JsonNode, output_dir: str):
self.output_dir = output_dir
for avro_schema in (avs for avs in schema if isinstance(avs, dict)):
self.generate_class_or_enum(avro_schema, '')

# Generate Option<T> class if needed
self.generate_option_class(output_dir)

self.generate_tests(output_dir)

def convert(self, avro_schema_path: str, output_dir: str):
Expand All @@ -1121,7 +1151,9 @@ def convert_avro_to_csharp(
msgpack_annotation=False,
cbor_annotation=False,
avro_annotation=False,
protobuf_net_annotation=False
protobuf_net_annotation=False,
use_optional=False,
use_ivalidatableobject=False
):
"""Converts Avro schema to C# classes

Expand All @@ -1138,6 +1170,8 @@ def convert_avro_to_csharp(
cbor_annotation (bool, optional): Use Dahomey.Cbor annotations. Defaults to False.
avro_annotation (bool, optional): Use Avro annotations. Defaults to False.
protobuf_net_annotation (bool, optional): Use protobuf-net annotations. Defaults to False.
use_optional (bool, optional): Use Option<T> wrapper for optional properties. Defaults to False.
use_ivalidatableobject (bool, optional): Implement IValidatableObject interface. Defaults to False.
"""

if not base_namespace:
Expand All @@ -1152,6 +1186,8 @@ def convert_avro_to_csharp(
avrotocs.cbor_annotation = cbor_annotation
avrotocs.avro_annotation = avro_annotation
avrotocs.protobuf_net_annotation = protobuf_net_annotation
avrotocs.use_optional = use_optional
avrotocs.use_ivalidatableobject = use_ivalidatableobject
avrotocs.convert(avro_schema_path, cs_file_path)


Expand All @@ -1167,7 +1203,9 @@ def convert_avro_schema_to_csharp(
msgpack_annotation: bool = False,
cbor_annotation: bool = False,
avro_annotation: bool = False,
protobuf_net_annotation: bool = False
protobuf_net_annotation: bool = False,
use_optional: bool = False,
use_ivalidatableobject: bool = False
):
"""Converts Avro schema to C# classes

Expand All @@ -1184,6 +1222,8 @@ def convert_avro_schema_to_csharp(
cbor_annotation (bool, optional): Use Dahomey.Cbor annotations. Defaults to False.
avro_annotation (bool, optional): Use Avro annotations. Defaults to False.
protobuf_net_annotation (bool, optional): Use protobuf-net annotations. Defaults to False.
use_optional (bool, optional): Use Option<T> wrapper for optional properties. Defaults to False.
use_ivalidatableobject (bool, optional): Implement IValidatableObject interface. Defaults to False.
"""
avrotocs = AvroToCSharp(base_namespace)
avrotocs.project_name = project_name
Expand All @@ -1195,4 +1235,6 @@ def convert_avro_schema_to_csharp(
avrotocs.cbor_annotation = cbor_annotation
avrotocs.avro_annotation = avro_annotation
avrotocs.protobuf_net_annotation = protobuf_net_annotation
avrotocs.use_optional = use_optional
avrotocs.use_ivalidatableobject = use_ivalidatableobject
avrotocs.convert_schema(avro_schema, output_dir)
203 changes: 203 additions & 0 deletions avrotize/avrotocsharp/option.cs.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using System.Text.Json.Serialization;

{% if namespace %}
namespace {{ namespace }}
{
{% endif %}
{% set ind=4 if namespace else 0 %}
{% filter indent(width=ind, first=True) %}
/// <summary>
/// Represents an optional value wrapper that distinguishes between undefined, null, and a value.
/// Compatible with DotNext.Optional API.
/// </summary>
/// <typeparam name="T">The type of the value</typeparam>
public struct Option<T>
{
private T? _value;
private bool _isDefined;

/// <summary>
/// Gets a value indicating whether the optional value is defined (either null or a value).
/// </summary>
[JsonIgnore]
public readonly bool IsDefined => _isDefined;

/// <summary>
/// Gets a value indicating whether the optional value is undefined (not set).
/// </summary>
[JsonIgnore]
public readonly bool IsUndefined => !_isDefined;

/// <summary>
/// Gets a value indicating whether the optional value has a non-null value.
/// </summary>
[JsonIgnore]
public readonly bool HasValue => _isDefined && _value != null;

/// <summary>
/// Gets a value indicating whether the optional value is null (but defined).
/// </summary>
[JsonIgnore]
public readonly bool IsNull => _isDefined && _value == null;

/// <summary>
/// Gets whether the value has been explicitly set (either to null or a value).
/// Alias for IsDefined for backward compatibility.
/// </summary>
[JsonIgnore]
public readonly bool IsSet => _isDefined;

/// <summary>
/// Gets the underlying value. Throws if the value is undefined.
/// </summary>
[JsonIgnore]
public readonly T? Value
{
get
{
if (!_isDefined)
throw new InvalidOperationException("Optional value is not defined");
return _value;
}
}

/// <summary>
/// Creates an undefined Option
/// </summary>
public Option()
{
_value = default;
_isDefined = false;
}

/// <summary>
/// Creates an Option with a value (which may be null)
/// </summary>
/// <param name="value">The value to wrap</param>
public Option(T? value)
{
_value = value;
_isDefined = true;
}

/// <summary>
/// Implicitly converts a value to an Option
/// </summary>
public static implicit operator Option<T>(T? value)
{
return new Option<T>(value);
}

/// <summary>
/// Explicitly converts an Option to its value
/// </summary>
public static explicit operator T?(Option<T> option)
{
return option._value;
}

/// <summary>
/// Gets the value if defined, otherwise returns the default value
/// </summary>
public readonly T? ValueOrDefault => _isDefined ? _value : default;

/// <summary>
/// Gets the value if defined, otherwise returns the specified default value
/// </summary>
public readonly T Or(T defaultValue)
{
return _isDefined && _value != null ? _value : defaultValue;
}

/// <summary>
/// Gets the value if defined, otherwise returns the default value
/// </summary>
public readonly T? GetValueOrDefault(T? defaultValue = default)
{
return _isDefined ? _value : defaultValue;
}

/// <summary>
/// Tries to get the value if it is defined
/// </summary>
public readonly bool TryGet(out T? value)
{
value = _value;
return _isDefined;
}

/// <summary>
/// Returns a string representation of the Option
/// </summary>
public override readonly string ToString()
{
if (!_isDefined) return "<undefined>";
if (_value == null) return "null";
return _value.ToString() ?? string.Empty;
}

/// <summary>
/// Checks equality with another Option
/// </summary>
public override readonly bool Equals(object? obj)
{
if (obj is not Option<T> other) return false;
if (_isDefined != other._isDefined) return false;
if (!_isDefined) return true; // Both undefined
return EqualityComparer<T?>.Default.Equals(_value, other._value);
}

/// <summary>
/// Gets the hash code
/// </summary>
public override readonly int GetHashCode()
{
if (!_isDefined) return 0;
return HashCode.Combine(_isDefined, _value);
}
}

/// <summary>
/// JSON converter for Option&lt;T&gt; that handles serialization and deserialization
/// </summary>
public class OptionJsonConverter<T> : JsonConverter<Option<T>>
{
/// <inheritdoc/>
public override Option<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return new Option<T>(default(T));
}

var value = JsonSerializer.Deserialize<T>(ref reader, options);
return new Option<T>(value);
}

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, Option<T> value, JsonSerializerOptions options)
{
if (value.IsUndefined)
{
// Skip writing the property entirely if undefined
return;
}

if (value.IsNull)
{
writer.WriteNullValue();
}
else
{
JsonSerializer.Serialize(writer, value.Value, options);
}
}
}
{% endfilter %}
{% if namespace %}
}
{% endif %}
36 changes: 34 additions & 2 deletions avrotize/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -2061,7 +2061,9 @@
"msgpack_annotation": "args.msgpack_annotation",
"cbor_annotation": "args.cbor_annotation",
"pascal_properties": "args.pascal_properties",
"base_namespace": "args.namespace"
"base_namespace": "args.namespace",
"use_optional": "args.use_optional",
"use_ivalidatableobject": "args.use_ivalidatableobject"
}
},
"extensions": [
Expand Down Expand Up @@ -2148,6 +2150,20 @@
"help": "Use PascalCase properties",
"default": false,
"required": false
},
{
"name": "--use-optional",
"type": "bool",
"help": "Use Option<T> wrapper for optional properties",
"default": false,
"required": false
},
{
"name": "--use-ivalidatableobject",
"type": "bool",
"help": "Implement IValidatableObject interface for validation",
"default": false,
"required": false
}
],
"suggested_output_file_path": "{input_file_name}-cs",
Expand Down Expand Up @@ -2197,7 +2213,9 @@
"avro_annotation": "args.avro_annotation",
"pascal_properties": "args.pascal_properties",
"base_namespace": "args.namespace",
"project_name": "args.project_name"
"project_name": "args.project_name",
"use_optional": "args.use_optional",
"use_ivalidatableobject": "args.use_ivalidatableobject"
}
},
"extensions": [
Expand Down Expand Up @@ -2270,6 +2288,20 @@
"help": "Use PascalCase properties",
"default": false,
"required": false
},
{
"name": "--use-optional",
"type": "bool",
"help": "Use Option<T> wrapper for optional properties",
"default": false,
"required": false
},
{
"name": "--use-ivalidatableobject",
"type": "bool",
"help": "Implement IValidatableObject interface for validation",
"default": false,
"required": false
}
],
"suggested_output_file_path": "{input_file_name}-cs",
Expand Down
Loading
Loading