diff --git a/source/Ocl/Converters/OclConverter.cs b/source/Ocl/Converters/OclConverter.cs
index 1af1bda..93f3ac2 100644
--- a/source/Ocl/Converters/OclConverter.cs
+++ b/source/Ocl/Converters/OclConverter.cs
@@ -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
{
+ ///
+ /// Returns true if the converter can be used for the provided type
+ ///
+ /// The model type being converted
public abstract bool CanConvert(Type type);
public virtual IEnumerable ToElements(OclConversionContext context, PropertyInfo? propertyInfo, object obj)
@@ -17,6 +22,13 @@ public virtual IEnumerable ToElements(OclConversionContext context,
: Array.Empty();
}
+ ///
+ /// Converts the provided object to an OCL root document.
+ ///
+ ///
+ ///
+ ///
+ ///
public virtual OclDocument ToDocument(OclConversionContext context, object obj)
=> throw new NotSupportedException("This type does not support conversion to the OCL root document");
@@ -32,7 +44,7 @@ protected virtual string GetName(OclConversionContext context, PropertyInfo? pro
protected virtual IEnumerable GetElements(object obj, IEnumerable 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
@@ -40,12 +52,17 @@ from element in context.ToElements(p, p.GetValue(obj))
return elements;
}
+ protected virtual IEnumerable PropertyToElements(object obj, OclConversionContext context, PropertyInfo p)
+ => context.ToElements(p, p.GetValue(obj));
+
+
protected virtual IReadOnlyList SetProperties(
OclConversionContext context,
IEnumerable elements,
object target,
IReadOnlyList properties)
{
+
var notFound = new List();
foreach (var element in elements)
{
@@ -68,7 +85,7 @@ protected virtual IReadOnlyList 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));
}
}
}
@@ -76,44 +93,74 @@ protected virtual IReadOnlyList SetProperties(
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).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 dict)
+ if (sourceValue is Dictionary dict)
{
- if (type.IsAssignableFrom(typeof(Dictionary)))
- return dict.ToDictionary(kvp => kvp.Key, kvp => (string?)CoerceValue(kvp.Value, typeof(string)));
+ if (targetType.IsAssignableFrom(typeof(Dictionary)))
+ 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 and Dictionary are supported.");
+ throw new OclException($"Could not coerce dictionary to {targetType.Name}. Only Dictionary and Dictionary 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()
{
- if (valueToSet is T[] array)
+ if (sourceValue is T[] array)
{
- if (type == typeof(List))
+ if (targetType == typeof(List))
return array.ToList();
- if (type == typeof(HashSet))
+ if (targetType == typeof(HashSet))
return array.ToHashSet();
}
return null;
}
- return FromArray() ?? FromArray() ?? FromArray() ?? throw new Exception($"Could not coerce value of type {valueToSet.GetType().Name} to {type.Name}");
+ return FromArray() ?? FromArray() ?? FromArray() ?? FromArray() ?? throw new Exception($"Could not coerce value of type {sourceValue.GetType().Name} to {targetType.Name}");
}
+ ///
+ /// 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.
+ ///
+ ///
+ ///
protected virtual IEnumerable GetProperties(Type type)
{
var defaultProperties = type.GetDefaultMembers().OfType();
diff --git a/source/Ocl/Converters/OclFunctionAttribute.cs b/source/Ocl/Converters/OclFunctionAttribute.cs
new file mode 100644
index 0000000..75c620a
--- /dev/null
+++ b/source/Ocl/Converters/OclFunctionAttribute.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Octopus.Ocl.Converters
+{
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class OclFunctionAttribute : Attribute
+ {
+ public OclFunctionAttribute(string name)
+ => Name = name;
+
+ ///
+ /// The name of the FunctionCall operation
+ ///
+ public string Name { get; }
+ }
+}
\ No newline at end of file
diff --git a/source/Ocl/FunctionCalls/Base64FunctionCall.cs b/source/Ocl/FunctionCalls/Base64FunctionCall.cs
new file mode 100644
index 0000000..6951ed8
--- /dev/null
+++ b/source/Ocl/FunctionCalls/Base64FunctionCall.cs
@@ -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 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 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");
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Ocl/FunctionCalls/IFunctionCall.cs b/source/Ocl/FunctionCalls/IFunctionCall.cs
new file mode 100644
index 0000000..3e3c3b1
--- /dev/null
+++ b/source/Ocl/FunctionCalls/IFunctionCall.cs
@@ -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 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 ToOclFunctionCall(object propertyValue);
+ }
+}
\ No newline at end of file
diff --git a/source/Ocl/Ocl.csproj b/source/Ocl/Ocl.csproj
index 5dbc7fb..3af29e3 100644
--- a/source/Ocl/Ocl.csproj
+++ b/source/Ocl/Ocl.csproj
@@ -10,6 +10,7 @@
netstandard2.1
enable
true
+ 9
diff --git a/source/Ocl/OclAttribute.cs b/source/Ocl/OclAttribute.cs
index d97a3ed..236344b 100644
--- a/source/Ocl/OclAttribute.cs
+++ b/source/Ocl/OclAttribute.cs
@@ -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;
}
}
@@ -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>).IsAssignableFrom(type);
diff --git a/source/Ocl/OclConversionContext.cs b/source/Ocl/OclConversionContext.cs
index 372b5f9..b58765e 100644
--- a/source/Ocl/OclConversionContext.cs
+++ b/source/Ocl/OclConversionContext.cs
@@ -3,6 +3,7 @@
using System.Linq;
using System.Reflection;
using Octopus.Ocl.Converters;
+using Octopus.Ocl.FunctionCalls;
using Octopus.Ocl.Namers;
namespace Octopus.Ocl
@@ -10,6 +11,8 @@ namespace Octopus.Ocl
public class OclConversionContext
{
readonly IReadOnlyList converters;
+
+ readonly IReadOnlyDictionary functions;
public OclConversionContext(OclSerializerOptions options)
{
@@ -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;
}
@@ -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 ToElements(PropertyInfo? propertyInfo, object? value)
{
if (value == null)
- return new IOclElement[0];
+ return Array.Empty();
+
+ 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 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);
diff --git a/source/Ocl/OclFunctionCall.cs b/source/Ocl/OclFunctionCall.cs
new file mode 100644
index 0000000..558a140
--- /dev/null
+++ b/source/Ocl/OclFunctionCall.cs
@@ -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 arguments;
+
+ public OclFunctionCall(string name, IEnumerable 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;
+ }
+ }
+
+ ///
+ /// The attribute value is given as an expression, which is retained literally for later evaluation by the calling application.
+ ///
+ public IEnumerable 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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Ocl/OclSerializerOptions.cs b/source/Ocl/OclSerializerOptions.cs
index 910f82d..f26461f 100644
--- a/source/Ocl/OclSerializerOptions.cs
+++ b/source/Ocl/OclSerializerOptions.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Octopus.Ocl.FunctionCalls;
using Octopus.Ocl.Namers;
namespace Octopus.Ocl
@@ -19,6 +20,7 @@ public class OclSerializerOptions
public int IndentDepth { get; set; } = 4;
public string DefaultHeredocTag { get; set; } = "EOT";
public List Converters { get; set; } = new List();
+ public List Functions { get; set; } = new List();
public IOclNamer Namer { get; set; } = new SnakeCaseOclNamer();
}
}
\ No newline at end of file
diff --git a/source/Ocl/OclWriter.cs b/source/Ocl/OclWriter.cs
index 25d6269..f0a3318 100644
--- a/source/Ocl/OclWriter.cs
+++ b/source/Ocl/OclWriter.cs
@@ -203,19 +203,16 @@ 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;
}
@@ -223,6 +220,27 @@ void WriteValue(object? value)
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)
diff --git a/source/Ocl/Parsing/OclParser.cs b/source/Ocl/Parsing/OclParser.cs
index 5dfa9a2..bcd5a23 100644
--- a/source/Ocl/Parsing/OclParser.cs
+++ b/source/Ocl/Parsing/OclParser.cs
@@ -14,7 +14,7 @@ static class OclParser
static readonly Parser BlockOpen = Parse.Char('{');
static readonly Parser BlockClose = Parse.Char('}');
- static readonly Parser Name =
+ static readonly Parser Identifier =
from name in Parse.Char(c => c == '_' || char.IsLetterOrDigit(c), "letter, digit, _")
.AtLeastOnce()
.Text()
@@ -69,7 +69,7 @@ from values in DecimalLiteral.DelimitedBy(Comma.Token())
from close in ArrayClose.Token()
select values.ToArray();
- static readonly Parser ArrayLiteral =
+ static readonly Parser Tuple =
EmptyArrayLiteral
.Or(QuotedStringArrayLiteral)
.Or(DecimalArrayLiteral)
@@ -79,6 +79,13 @@ from close in ArrayClose.Token()
DecimalLiteral.Select(d => (object)d)
.Or(IntegerLiteral.Select(d => (object)d));
+ static readonly Parser FunctionCall =
+ from identifier in Identifier.Token()
+ from funcOpen in Parse.Char('(')
+ from values in Literal.DelimitedBy(Comma.Token()).Optional()
+ from funcClose in Parse.Char(')')
+ select new OclFunctionCall(identifier.ToString(), values.GetOrDefault() ?? Array.Empty());
+
static readonly Parser Literal =
NullLiteral
.XOr(TrueLiteral.Select(d => (object)d))
@@ -86,30 +93,44 @@ from close in ArrayClose.Token()
.XOr(QuotedStringParser.QuotedStringLiteral)
.XOr(HeredocParser.Literal)
.XOr(NumberLiteral)
- .XOr(ArrayLiteral);
+ .XOr(Tuple);
static readonly Parser UnquotedDictionaryKey =
Parse.CharExcept(c => char.IsWhiteSpace(c) || c == '"', "Not whitespace or quotes")
.Many()
.Text();
- static readonly Parser> DictionaryEntry =
+ static readonly Parser> ObjectElem =
from key in UnquotedDictionaryKey.Or(QuotedStringParser.QuotedStringLiteral).SameLineToken()
from _ in Parse.Char('=')
- from value in Literal.SameLineToken()
+ from value in ExprTerm
select new KeyValuePair(key, value);
- static readonly Parser> Dictionary =
+ ///
+ /// See https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#collection-values
+ ///
+ static readonly Parser> @Object =
from open in BlockOpen.Token()
- from entries in DictionaryEntry.Token().Many()
+ from entries in ObjectElem.Token().Many()
from close in BlockClose.Token()
select new Dictionary(entries);
+
+ ///
+ /// See https://github.com/hashicorp/hcl/blob/hcl2/hclsyntax/spec.md#attribute-definitions
+ /// Currently only support values
+ ///
static readonly Parser Attribute =
- from name in Name.SameLineToken()
+ from name in Identifier.SameLineToken()
from _ in Parse.Char('=')
- from value in Dictionary.XOr(Literal).SameLineToken()
+ from value in ExprTerm
select new OclAttribute(name, value);
+
+ ///
+ /// See https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#expression-terms
+ ///
+ static readonly Parser ExprTerm =
+ Object.Or(FunctionCall).Or(Literal).SameLineToken();
static readonly Parser EmptyBlockBody =
from open in BlockOpen.SameLineToken()
@@ -124,7 +145,7 @@ from close in BlockClose.Token()
select children.ToArray();
static readonly Parser Block =
- from name in Name.SameLineToken()
+ from name in Identifier.SameLineToken()
from labels in QuotedStringParser.QuotedStringLiteral.SameLineToken().Many()
from children in EmptyBlockBody.Or(BlockBody).Token().Once()
select new OclBlock(name, labels.ToArray(), children.Single());
diff --git a/source/Tests/Functions/Base64FunctionCallFixture.cs b/source/Tests/Functions/Base64FunctionCallFixture.cs
new file mode 100644
index 0000000..d77f2a3
--- /dev/null
+++ b/source/Tests/Functions/Base64FunctionCallFixture.cs
@@ -0,0 +1,42 @@
+using System;
+using FluentAssertions;
+using NUnit.Framework;
+using Octopus.Ocl;
+using Octopus.Ocl.Converters;
+
+namespace Tests.Functions
+{
+ public class Base64FunctionCallFixture
+ {
+ [Test]
+ public void TwoWayFunctionIsReversible()
+ {
+ var obj = new TestObject()
+ {
+ WithFunctionAttribute = new byte[]{72, 101, 108, 108, 111},
+ WithoutFunctionAttribute = new byte[]{72, 101, 108, 108, 111}
+ };
+
+ var ocl = CreateSerializer().Serialize(obj);
+ ocl = @"with_function_attribute = base64decode(""SGVsbG8="")
+ without_function_attribute = [72, 101, 108, 108, 111]";
+
+ CreateSerializer().Deserialize(ocl)
+ .Should()
+ .BeEquivalentTo(obj);
+ }
+
+ OclSerializer CreateSerializer()
+ {
+ return new OclSerializer(new OclSerializerOptions());
+ }
+
+ class TestObject
+ {
+ [OclFunction("base64decode")]
+ public byte[] WithFunctionAttribute { get; set; } = Array.Empty();
+
+ public byte[] WithoutFunctionAttribute { get; set; } = Array.Empty();
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Tests/Functions/CustomFunctionFixture.cs b/source/Tests/Functions/CustomFunctionFixture.cs
new file mode 100644
index 0000000..16cf82d
--- /dev/null
+++ b/source/Tests/Functions/CustomFunctionFixture.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using FluentAssertions;
+using NUnit.Framework;
+using Octopus.Ocl;
+using Octopus.Ocl.Converters;
+using Octopus.Ocl.FunctionCalls;
+
+namespace Tests.Functions
+{
+ public class CustomFunctionFixture
+ {
+ [Test]
+ public void TwoWayFunctionIsReversible()
+ {
+ var car = new Car()
+ {
+ Name = "Hatchback",
+ Engine = new Engine()
+ {
+ TempC = 67
+ },
+ };
+
+ var ocl = CreateSerializer().Serialize(car);
+ ocl = @"name = ""Hatchback""
+
+engine {
+ temp_c = f2c(152.6)
+}";
+
+ CreateSerializer().Deserialize(ocl)
+ .Should()
+ .BeEquivalentTo(car);
+ }
+
+ [Test]
+ public void StringLookingLikeFunctionRemainsString()
+ {
+ var car = new Car()
+ {
+ Name = "f2c(152.6)"
+ };
+
+ var ocl = CreateSerializer().Serialize(car);
+ ocl.Should()
+ .BeEquivalentTo(@"name = ""f2c(152.6)""");
+
+ CreateSerializer().Deserialize(ocl)
+ .Should()
+ .BeEquivalentTo(car);
+ }
+
+
+ OclSerializer CreateSerializer()
+ {
+ return new OclSerializer(new OclSerializerOptions()
+ {
+ Converters = new List()
+ {
+ new EngineConverter()
+ },
+ Functions = new List()
+ {
+ new FahrenheitToCelsiusFunction()
+ }
+ });
+ }
+
+ class EngineConverter : DefaultBlockOclConverter
+ {
+ public override bool CanConvert(Type type)
+ => type == typeof(Engine);
+
+ protected override IEnumerable PropertyToElements(object obj, OclConversionContext context, PropertyInfo propertyInfo)
+ {
+ if (propertyInfo.Name == nameof(Engine.TempC))
+ {
+ return context.PropertyToOclFunction(propertyInfo.GetValue(obj), propertyInfo, FahrenheitToCelsiusFunction.FnName);
+ }
+
+ return base.PropertyToElements(obj, context, propertyInfo);
+ }
+ }
+
+ class FahrenheitToCelsiusFunction : IFunctionCall
+ {
+ public static readonly string FnName = "f2c";
+ public string Name => FnName;
+
+ public object? ToValue(IEnumerable arguments)
+ {
+ var val = arguments.FirstOrDefault();
+ if (val == null)
+ {
+ return null;
+ }
+
+ if (val == null || !double.TryParse(val.ToString(), out var fahrenheit))
+ {
+ throw new OclException("f2c function expecting a single double argument. Unable to parse value");
+ }
+
+ return (fahrenheit - 32) * 5 / 9;
+ }
+
+ public IEnumerable ToOclFunctionCall(object propertyValue)
+ {
+ if (!double.TryParse(propertyValue.ToString(), out var celsius))
+ {
+ throw new OclException("f2c function expecting a double argument. Unable to parse value");
+ }
+
+ var fahrenheit = (celsius * 9 / 5) + 32;
+ return new object?[] { fahrenheit };
+ }
+ }
+
+ class Car
+ {
+ public string? Name { get; set; }
+ public byte[]? Image { get; set; }
+ public Engine? Engine { get; set; }
+ }
+
+ class Engine
+ {
+ public double TempC { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Tests/Functions/FunctionCallFixture.cs b/source/Tests/Functions/FunctionCallFixture.cs
new file mode 100644
index 0000000..ec0d090
--- /dev/null
+++ b/source/Tests/Functions/FunctionCallFixture.cs
@@ -0,0 +1,34 @@
+using System;
+using FluentAssertions;
+using NUnit.Framework;
+using Octopus.Ocl;
+
+namespace Tests.Functions
+{
+ public class FunctionCallFixture
+ {
+ [Test]
+ public void UnknownFunctionThrows()
+ {
+ Action action = () =>
+ {
+ CreateSerializer()
+ .Deserialize(new OclDocument()
+ {
+ new OclAttribute("name", new OclFunctionCall("somefakefunction", new object?[] { 11, "zoom" }))
+ });
+ };
+ action.Should()
+ .Throw()
+ .WithMessage("Call to unknown function. There is no function named \"somefakefunction\"");
+ }
+
+ class TestObject
+ {
+ }
+ OclSerializer CreateSerializer()
+ {
+ return new OclSerializer(new OclSerializerOptions());
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/Tests/Parsing/FunctionCallParsingFixture.cs b/source/Tests/Parsing/FunctionCallParsingFixture.cs
new file mode 100644
index 0000000..7cf97e8
--- /dev/null
+++ b/source/Tests/Parsing/FunctionCallParsingFixture.cs
@@ -0,0 +1,60 @@
+using NUnit.Framework;
+using Octopus.Ocl;
+using Octopus.Ocl.Parsing;
+
+namespace Tests.Parsing
+{
+ public class FunctionCallParsingFixture
+ {
+ [Test]
+ public void WithSingleArgument()
+ => OclParser.Execute(@"Child = dateCalc(""today"")")
+ .Should()
+ .HaveChildrenExactly(
+ new OclAttribute("Child",
+ new OclFunctionCall("dateCalc", new[] { "today" }))
+ );
+
+ [Test]
+ public void WithNoArgument()
+ => OclParser.Execute(@"Child = dateCalc()")
+ .Should()
+ .HaveChildrenExactly(
+ new OclAttribute("Child",
+ new OclFunctionCall("dateCalc", new object?[] { }))
+ );
+
+ [Test]
+ public void WithMultipleArgument()
+ => OclParser.Execute(@"Child = dateCalc(12, ""cat"", null)")
+ .Should()
+ .HaveChildrenExactly(
+ new OclAttribute("Child",
+ new OclFunctionCall("dateCalc", new object?[] { 12, "cat", null }))
+ );
+
+ [Test]
+ public void WithinBlock()
+ => OclParser.Execute(@"
+ Parent {
+ Child = dateCalc()
+ }")
+ .Should()
+ .HaveChildrenExactly(new OclBlock("Parent")
+ {
+ new OclAttribute("Child",
+ new OclFunctionCall("dateCalc", new object?[] { }))
+ });
+
+
+ [Test]
+ public void WithSurroundingQuotes()
+ {
+ OclParser.Execute("Child = \"dateCalc()\"").Should()
+ .HaveChildrenExactly(
+ new OclAttribute("Child","dateCalc()")
+ );
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/source/Tests/RealLifeScenario/Implementation/PropertiesDictionaryOclConverter.cs b/source/Tests/RealLifeScenario/Implementation/PropertiesDictionaryOclConverter.cs
index 3a5d7a8..85786d2 100644
--- a/source/Tests/RealLifeScenario/Implementation/PropertiesDictionaryOclConverter.cs
+++ b/source/Tests/RealLifeScenario/Implementation/PropertiesDictionaryOclConverter.cs
@@ -25,7 +25,7 @@ public IEnumerable ToElements(OclConversionContext context, Propert
var stringDict = dict.ToDictionary(
kvp => kvp.Key,
- kvp => kvp.Value.Value
+ kvp => (object)kvp.Value.Value
);
yield return new OclAttribute("properties", stringDict);
diff --git a/source/Tests/RealLifeScenario/RealLifeScenarioFixtureBase.cs b/source/Tests/RealLifeScenario/RealLifeScenarioFixture.cs
similarity index 100%
rename from source/Tests/RealLifeScenario/RealLifeScenarioFixtureBase.cs
rename to source/Tests/RealLifeScenario/RealLifeScenarioFixture.cs