Skip to content

Commit

Permalink
Support function object types in Ocl
Browse files Browse the repository at this point in the history
  • Loading branch information
zentron committed Aug 6, 2023
1 parent 2abf077 commit 725e577
Show file tree
Hide file tree
Showing 12 changed files with 432 additions and 41 deletions.
71 changes: 53 additions & 18 deletions source/Ocl/Converters/OclConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Octopus.Ocl.Converters
{
public abstract class OclConverter : IOclConverter
{
/// <summary>
/// Returns true if the converter can be used for the provided type
/// </summary>
/// <param name="type">The model type being converted</param>
public abstract bool CanConvert(Type type);

public virtual IEnumerable<IOclElement> ToElements(OclConversionContext context, PropertyInfo? propertyInfo, object obj)
Expand All @@ -17,6 +22,13 @@ public virtual IEnumerable<IOclElement> ToElements(OclConversionContext context,
: Array.Empty<IOclElement>();
}

/// <summary>
/// Converts the provided object to an OCL root document.
/// </summary>
/// <param name="context"></param>
/// <param name="obj"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
public virtual OclDocument ToDocument(OclConversionContext context, object obj)
=> throw new NotSupportedException("This type does not support conversion to the OCL root document");

Expand All @@ -32,20 +44,25 @@ protected virtual string GetName(OclConversionContext context, PropertyInfo? pro
protected virtual IEnumerable<IOclElement> GetElements(object obj, IEnumerable<PropertyInfo> properties, OclConversionContext context)
{
var elements = from p in properties
from element in context.ToElements(p, p.GetValue(obj))
from element in PropertyToElements(obj, context, p)
orderby
element is OclBlock,
element.Name
select element;
return elements;
}

protected virtual IEnumerable<IOclElement> PropertyToElements(object obj, OclConversionContext context, PropertyInfo p)
=> context.ToElements(p, p.GetValue(obj));


protected virtual IReadOnlyList<IOclElement> SetProperties(
OclConversionContext context,
IEnumerable<IOclElement> elements,
object target,
IReadOnlyList<PropertyInfo> properties)
{

var notFound = new List<IOclElement>();
foreach (var element in elements)
{
Expand All @@ -68,52 +85,70 @@ protected virtual IReadOnlyList<IOclElement> SetProperties(
if (!propertyToSet.CanWrite)
throw new OclException($"The property '{propertyToSet.Name}' on '{target.GetType().Name}' does not have a setter");

propertyToSet.SetValue(target, CoerceValue(valueToSet, propertyToSet.PropertyType));
propertyToSet.SetValue(target, CoerceValue(context, valueToSet, propertyToSet.PropertyType));
}
}
}

return notFound;
}

object? CoerceValue(object? valueToSet, Type type)
object? CoerceValue(OclConversionContext context, object? sourceValue, Type targetType)
{
if (valueToSet is OclStringLiteral literal)
valueToSet = literal.Value;
if (sourceValue is OclStringLiteral literal)
sourceValue = literal.Value;

if (sourceValue is OclFunctionCall functionCall)
{
var result = context.GetFunctionCallFor(functionCall.Name).ToValue(functionCall);
return CoerceValue(context, result, targetType);
}

if (valueToSet == null)
if (sourceValue == null)
return null;

if (type.IsInstanceOfType(valueToSet))
return valueToSet;
if (targetType.IsInstanceOfType(sourceValue))
return sourceValue;

if (valueToSet is Dictionary<string, object?> dict)
if (sourceValue is Dictionary<string, object?> dict)
{
if (type.IsAssignableFrom(typeof(Dictionary<string, string>)))
return dict.ToDictionary(kvp => kvp.Key, kvp => (string?)CoerceValue(kvp.Value, typeof(string)));
if (targetType.IsAssignableFrom(typeof(Dictionary<string, string>)))
return dict.ToDictionary(kvp => kvp.Key, kvp => (string?)CoerceValue(context, kvp.Value, typeof(string)));

throw new OclException($"Could not coerce dictionary to {type.Name}. Only Dictionary<string, string> and Dictionary<string, object?> are supported.");
throw new OclException($"Could not coerce dictionary to {targetType.Name}. Only Dictionary<string, string> and Dictionary<string, object?> are supported.");
}

if (type == typeof(string) && valueToSet.GetType().IsPrimitive)
return valueToSet.ToString();
if (targetType == typeof(string))
{
if (sourceValue.GetType().IsPrimitive)
return sourceValue.ToString();

if (sourceValue is byte[] bytes)
return Encoding.UTF8.GetString(bytes);
}

object? FromArray<T>()
{
if (valueToSet is T[] array)
if (sourceValue is T[] array)
{
if (type == typeof(List<T>))
if (targetType == typeof(List<T>))
return array.ToList();
if (type == typeof(HashSet<T>))
if (targetType == typeof(HashSet<T>))
return array.ToHashSet();
}

return null;
}

return FromArray<string>() ?? FromArray<decimal>() ?? FromArray<int>() ?? throw new Exception($"Could not coerce value of type {valueToSet.GetType().Name} to {type.Name}");
return FromArray<string>() ?? FromArray<decimal>() ?? FromArray<int>() ?? throw new Exception($"Could not coerce value of type {sourceValue.GetType().Name} to {targetType.Name}");
}

/// <summary>
/// Get the properties for the given type.
/// TODO: The virtual accessor can probably be removed and replaced with a ShouldSerialize method that seems to be used.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
protected virtual IEnumerable<PropertyInfo> GetProperties(Type type)
{
var defaultProperties = type.GetDefaultMembers().OfType<PropertyInfo>();
Expand Down
14 changes: 14 additions & 0 deletions source/Ocl/FunctionCalls/IFunctionCall.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Reflection;

namespace Octopus.Ocl.FunctionCalls
{
public interface IFunctionCall
{
string Name {get;}
object? ToValue(OclFunctionCall functionCall);
OclFunctionCall? ToOclFunctionCall(object obj, PropertyInfo propertyInfo);

OclFunctionCall? ToOclFunctionCall(object[] arguments);
}
}
8 changes: 6 additions & 2 deletions source/Ocl/OclAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public object? Value
set
{
if (value != null && !IsSupportedValueType(value.GetType()))
throw new OclException($"The type {value.GetType().FullName} is not a support value type OCL attribute value");
throw new OclException($"The type {value.GetType().FullName} is not a supported value type OCL attribute value");
this.value = value;
}
}
Expand All @@ -64,9 +64,13 @@ bool IsNullableSupportedValueType()
IsObjectDictionary(type) ||
IsStringDictionary(type) ||
IsNullableSupportedValueType() ||
IsSupportedValueCollectionType(type);
IsSupportedValueCollectionType(type) ||
IsFunctionCall(type);
}

internal static bool IsFunctionCall(Type type)
=> typeof(OclFunctionCall).IsAssignableFrom(type);

internal static bool IsObjectDictionary(Type type)
=> typeof(IEnumerable<KeyValuePair<string, object?>>).IsAssignableFrom(type);

Expand Down
37 changes: 37 additions & 0 deletions source/Ocl/OclConversionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,32 @@
using System.Linq;
using System.Reflection;
using Octopus.Ocl.Converters;
using Octopus.Ocl.FunctionCalls;
using Octopus.Ocl.Namers;

namespace Octopus.Ocl
{
/*public class FileFunction: IFunctionCall
{
public static string FnName = "file";
public string Name { get; } = FnName;
public object? ToValue(OclFunctionCall functionCall)
=> throw new NotImplementedException();
public OclFunctionCall? ToOclFunctionCall(object obj, PropertyInfo propertyInfo)
=> throw new NotImplementedException();
public OclFunctionCall? ToOclFunctionCall(object[] arguments)
=> throw new NotImplementedException();
}*/

public class OclConversionContext
{
readonly IReadOnlyList<IOclConverter> converters;

readonly IReadOnlyList<IFunctionCall> functions;

public OclConversionContext(OclSerializerOptions options)
{
Expand All @@ -24,6 +43,12 @@ public OclConversionContext(OclSerializerOptions options)
new DefaultBlockOclConverter()
})
.ToArray();

functions = options.Functions.Concat(new IFunctionCall[]
{
//TODO: Add some built-in functions
}).ToArray();

Namer = options.Namer;
}

Expand All @@ -38,6 +63,18 @@ public IOclConverter GetConverterFor(Type type)
throw new Exception("Could not find a converter for " + type.FullName);
}

public IFunctionCall GetFunctionCallFor(string name)
{
var fnCall = functions.FirstOrDefault(f => f.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (fnCall == null)
{
throw new OclException($"Call to unknown function. "
+ $"There is no function named \"{name}\"");
}

return fnCall;
}

internal IEnumerable<IOclElement> ToElements(PropertyInfo? propertyInfo, object? value)
{
if (value == null)
Expand Down
52 changes: 52 additions & 0 deletions source/Ocl/OclFunctionCall.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Octopus.Ocl
{
[DebuggerDisplay("{Name}({Arguments})", Name = "OclFunctionCall")]
public class OclFunctionCall : IOclElement
{
string name;
IEnumerable<object?> arguments;

public OclFunctionCall(string name, IEnumerable<object?> arguments)
{
this.name = Name = name; // Make the compiler happy
this.arguments = arguments;
}

public string Name
{
get => name;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new OclException("FunctionCalls must have an identifier name");
name = value;
}
}

/// <remarks>
/// The attribute value is given as an expression, which is retained literally for later evaluation by the calling application.
/// </remarks>
public IEnumerable<object?> Arguments
{
get => arguments;
set
{
var invalidArg = arguments.Where(a => a != null && !OclAttribute.IsSupportedValueType(a.GetType()))
.Select(t => t?.GetType().FullName).Distinct().ToArray();
if(invalidArg.Any())
{
var msg = (invalidArg.Length == 1) ?
$"The type {invalidArg} is not a supported value type for an OCL function call argument" :
$"The types {string.Join(',', invalidArg)} are not a supported value types for an OCL function call argument";
throw new OclException(msg);
}

this.arguments = value;
}
}
}
}
2 changes: 2 additions & 0 deletions source/Ocl/OclSerializerOptions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Octopus.Ocl.FunctionCalls;
using Octopus.Ocl.Namers;

namespace Octopus.Ocl
Expand All @@ -19,6 +20,7 @@ public class OclSerializerOptions
public int IndentDepth { get; set; } = 4;
public string DefaultHeredocTag { get; set; } = "EOT";
public List<IOclConverter> Converters { get; set; } = new List<IOclConverter>();
public List<IFunctionCall> Functions { get; set; } = new List<IFunctionCall>();
public IOclNamer Namer { get; set; } = new SnakeCaseOclNamer();
}
}
38 changes: 28 additions & 10 deletions source/Ocl/OclWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,26 +203,44 @@ void WriteValue(object? value)
return;
}

if(OclAttribute.IsFunctionCall(valueType))
{
WriteValue((OclFunctionCall)value);
return;
}

if (OclAttribute.IsSupportedValueCollectionType(valueType))
{
var enumerable = (IEnumerable)value;
writer.Write('[');
var isFirst = true;
foreach (var item in enumerable)
{
if (!isFirst)
writer.Write(", ");
isFirst = false;
WriteValue(item);
}

Write((IEnumerable)value);
writer.Write(']');
return;
}

throw new InvalidOperationException($"The type {value.GetType().FullName} is not a valid attribute value and can not be serialized");
}

void Write(IEnumerable enumerable)
{

var isFirst = true;
foreach (var item in enumerable)
{
if (!isFirst)
writer.Write(", ");
isFirst = false;
WriteValue(item);
}
}

void WriteValue(OclFunctionCall functionCall)
{
writer.Write(functionCall.Name);
writer.Write("(");
Write((IEnumerable)functionCall.Arguments);
writer.Write(")");
}

void WriteValue(OclStringLiteral literal)
{
if (literal.Format == OclStringLiteralFormat.SingleLine)
Expand Down
Loading

0 comments on commit 725e577

Please sign in to comment.