Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support function object types in Ocl #132

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
83 changes: 65 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,82 @@ 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.Arguments);
return CoerceValue(context, result, targetType);
}

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

if (sourceValue is int[] array)
{
if (typeof(IEnumerable<byte>).IsAssignableFrom(targetType))
sourceValue = array.Select(i => (byte)i).ToArray();
}

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);
}

if (targetType == typeof(int))
{
if (sourceValue is decimal sd && sd == Decimal.Truncate(sd))
return (int)sd;
}

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>() ?? FromArray<byte>() ?? 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
16 changes: 16 additions & 0 deletions source/Ocl/Converters/OclFunctionAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;

namespace Octopus.Ocl.Converters
{
[AttributeUsage(AttributeTargets.Property)]
public sealed class OclFunctionAttribute : Attribute
{
public OclFunctionAttribute(string name)
=> Name = name;

/// <summary>
/// The name of the FunctionCall operation
/// </summary>
public string Name { get; }
}
}
37 changes: 37 additions & 0 deletions source/Ocl/FunctionCalls/Base64FunctionCall.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Octopus.Ocl.FunctionCalls
{
public class Base64DecodeFunctionCall : IFunctionCall
{
public string Name => "base64decode";

public object? ToValue(IEnumerable<object?> arguments)
{
var val = arguments.FirstOrDefault();
if (val == null)
{
return null;
}

if (val is not string valString)
{
throw new OclException($"The {Name} OCL function expects a single double argument. Unable to parse value");
}
return Convert.FromBase64String(valString);
}

public IEnumerable<object?> ToOclFunctionCall(object propertyValue)
{
if (propertyValue is Byte[] bytes)
{
var fahrenheit = Convert.ToBase64String(bytes);
return new object?[] { fahrenheit };
}

throw new InvalidOperationException($"The {Name} OCL function currently only supports byte arrays");
}
}
}
17 changes: 17 additions & 0 deletions source/Ocl/FunctionCalls/IFunctionCall.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;

namespace Octopus.Ocl.FunctionCalls
{
public interface IFunctionCall
{
string Name {get;}

// Called during deserialization when converting from the OCL representation to a single property value.
object? ToValue(IEnumerable<object?> arguments);

// Called during serialization and allows for a single object value to be represented by the function call
// as being defined with non or many arguments.
IEnumerable<object?> ToOclFunctionCall(object propertyValue);
}
}
1 change: 1 addition & 0 deletions source/Ocl/Ocl.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<LangVersion>9</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
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
43 changes: 42 additions & 1 deletion source/Ocl/OclConversionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
using System.Linq;
using System.Reflection;
using Octopus.Ocl.Converters;
using Octopus.Ocl.FunctionCalls;
using Octopus.Ocl.Namers;

namespace Octopus.Ocl
{
public class OclConversionContext
{
readonly IReadOnlyList<IOclConverter> converters;

readonly IReadOnlyDictionary<string, IFunctionCall> functions;

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

functions = options.Functions.Concat(new IFunctionCall[]
{
new Base64DecodeFunctionCall()
}).ToDictionary(func => func.Name, func => func);

Namer = options.Namer;
}

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

public IFunctionCall GetFunctionCallFor(string name)
{
if(!functions.TryGetValue(name, out var fnCall)) {
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)
return new IOclElement[0];
return Array.Empty<IOclElement>();

if (propertyInfo != null
&& propertyInfo.GetCustomAttribute(typeof(OclFunctionAttribute)) is OclFunctionAttribute oclFunctionAttribute
&& !string.IsNullOrEmpty(oclFunctionAttribute.Name))
{
return PropertyToOclFunction(value, propertyInfo, oclFunctionAttribute.Name);
}

return GetConverterFor(value.GetType())
.ToElements(this, propertyInfo, value);
}

internal IEnumerable<IOclElement> PropertyToOclFunction(object? propertyValue, PropertyInfo propertyInfo, string oclFunctionName)
{
object? attributeValue = null;

if (propertyValue != null)
{
var convertedValues = GetFunctionCallFor(oclFunctionName).ToOclFunctionCall(propertyValue);
attributeValue = new OclFunctionCall(oclFunctionName, convertedValues);
}

return new IOclElement[] { new OclAttribute(Namer.GetName(propertyInfo), attributeValue) };


}

internal object? FromElement(Type type, IOclElement element, object? getCurrentValue)
=> GetConverterFor(type)
.FromElement(this, type, element, getCurrentValue);
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;
}
}
}
}
Loading