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

A strategy approach to implement Object -> HDoc mapping #5

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions source/Hcl/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly:InternalsVisibleTo("Tests")]
31 changes: 31 additions & 0 deletions source/Hcl/Converters/AttributeScanningHclChildElementStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Octopus.Hcl.Converters
{
public class AttributeScanningHclChildElementStrategy : IHclChildElementStrategy
{
public virtual IEnumerable<IHElement> GetElements(object obj, HclConversionContext context)
=> GetElements(
obj,
from p in obj.GetType().GetProperties()
where p.CanRead
where p.GetCustomAttribute<HclIgnoreAttribute>() == null
where p.GetCustomAttribute<HclLabelAttribute>() == null
select p,
context
);

protected virtual IEnumerable<IHElement> GetElements(object obj, IEnumerable<PropertyInfo> properties, HclConversionContext context)
{
var elements = from p in properties
let attr = p.GetCustomAttribute<HclElementAttribute>() ?? new HclElementAttribute()
from element in context.ToElements(attr.Name ?? p.Name, p.GetValue(obj))
orderby attr.Ordinal, element is HAttribute, element.Name
select element;
return elements;
}
}
}
23 changes: 23 additions & 0 deletions source/Hcl/Converters/AttributeScanningHclLabelStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Octopus.Hcl.Converters
{
public class AttributeScanningHclLabelStrategy : IHclLabelStrategy
{
public IEnumerable<string> GetLabels(object obj)
{
var labels = from p in obj.GetType().GetProperties()
where p.CanRead
let attr = p.GetCustomAttribute<HclLabelAttribute>()
where attr != null
orderby attr.Ordinal
let labelObj = p.GetValue(obj) ?? throw new HclException($"Labels cannot be null ({p.DeclaringType?.FullName}.{p.Name})")
let label = labelObj as string ?? throw new Exception($"Labels must be strings ({p.DeclaringType?.FullName}.{p.Name})")
select label;
return labels;
}
}
}
22 changes: 22 additions & 0 deletions source/Hcl/Converters/DefaultCollectionHclConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;

namespace Octopus.Hcl.Converters
{
public class DefaultCollectionHclConverter : IHclConverter
{
public bool CanConvert(Type type)
=> typeof(IEnumerable).IsAssignableFrom(type);

public IEnumerable<IHElement> Convert(HclConversionContext context, PropertyInfo property, object value)
{
var items = (IEnumerable)value;
foreach (var item in items)
if (item != null)
foreach (var element in context.ToElements(property, item))
yield return element;
}
}
}
24 changes: 24 additions & 0 deletions source/Hcl/Converters/HDocumentHclConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Octopus.Hcl.Converters
{
internal class HDocumentHclConverter
{
public HDocument Convert(object? obj, HclConversionContext context)
=> obj == null
? new HDocument()
: new HDocument(
GetElements(
obj,
from p in obj.GetType().GetProperties()
where p.CanRead
where p.GetCustomAttribute<HclIgnoreAttribute>() == null
select p,
context
)
);
}
}
38 changes: 38 additions & 0 deletions source/Hcl/Converters/HclConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;

namespace Octopus.Hcl.Converters
{
public class HclConverter : IHclConverter
{
private readonly IHclNamingStrategy namingStrategy;
private readonly IHclLabelStrategy hclLabelStrategy;
private readonly IHclChildElementStrategy hclChildElementStrategy;

public virtual bool CanConvert(Type type)
=> true;

public HclConverter(
IHclNamingStrategy namingStrategy,
IHclLabelStrategy hclLabelStrategy,
IHclChildElementStrategy hclChildElementStrategy
)
{
this.namingStrategy = namingStrategy;
this.hclLabelStrategy = hclLabelStrategy;
this.hclChildElementStrategy = hclChildElementStrategy;
}

public IEnumerable<IHElement> Convert(HclConversionContext context, string name, object obj)
=> new[]
{
HAttribute.IsSupportedValueType(obj.GetType())
? new HAttribute(namingStrategy.GetName(name, obj), obj)
: new HBlock(
namingStrategy.GetName(name, obj),
hclLabelStrategy.GetLabels(obj),
hclChildElementStrategy.GetElements(obj, context)
)
};
}
}
10 changes: 10 additions & 0 deletions source/Hcl/Converters/IHclChildElementStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;

namespace Octopus.Hcl.Converters
{
public interface IHclChildElementStrategy
{
IEnumerable<IHElement> GetElements(object obj, HclConversionContext context);
}
}
10 changes: 10 additions & 0 deletions source/Hcl/Converters/IHclLabelStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;

namespace Octopus.Hcl.Converters
{
public interface IHclLabelStrategy
{
IEnumerable<string> GetLabels(object obj);
}
}
10 changes: 10 additions & 0 deletions source/Hcl/Converters/IHclNamingStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using System.Reflection;

namespace Octopus.Hcl.Converters
{
public interface IHclNamingStrategy
{
string GetName(PropertyInfo property, object value);
}
}
59 changes: 59 additions & 0 deletions source/Hcl/Converters/PropertyNameNamingStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Octopus.Hcl.Converters
{
public interface IHclNamingStrategy
{
string GetName(PropertyInfo property, object obj);
}

public class BlockHclConverter : HclConverter
{
public override bool CanConvert(Type type)
=> !HAttribute.IsSupportedValueType(type);

protected override IHElement ConvertInternal(HclConversionContext context, string name, object obj)
=> new HBlock(
GetName(name, obj),
GetLabels(obj),
GetElements(obj, context)
);

protected virtual IEnumerable<string> GetLabels(object obj)
{
var labels = from p in obj.GetType().GetProperties()
where p.CanRead
let attr = p.GetCustomAttribute<HclLabelAttribute>()
where attr != null
orderby attr.Ordinal
let labelObj = p.GetValue(obj) ?? throw new HclException($"Labels cannot be null ({p.DeclaringType?.FullName}.{p.Name})")
let label = labelObj as string ?? throw new Exception($"Labels must be strings ({p.DeclaringType?.FullName}.{p.Name})")
select label;
return labels;
}

protected virtual IEnumerable<IHElement> GetElements(object obj, HclConversionContext context)
=> GetElements(
obj,
from p in obj.GetType().GetProperties()
where p.CanRead
where p.GetCustomAttribute<HclIgnoreAttribute>() == null
where p.GetCustomAttribute<HclLabelAttribute>() == null
select p,
context
);

protected virtual IEnumerable<IHElement> GetElements(object obj, IEnumerable<PropertyInfo> properties, HclConversionContext context)
{
var elements = from p in properties
let attr = p.GetCustomAttribute<HclElementAttribute>() ?? new HclElementAttribute()
from element in context.ToElements(attr.Name ?? p.Name, p.GetValue(obj))
orderby attr.Ordinal, element is HAttribute, element.Name
select element;
return elements;
}
}
}
25 changes: 24 additions & 1 deletion source/Hcl/HAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;

namespace Octopus.Hcl
{
Expand All @@ -12,6 +14,7 @@ namespace Octopus.Hcl
public class HAttribute : IHElement
{
private string name;
private object? value;

public HAttribute(string name, object? value)
{
Expand All @@ -33,6 +36,26 @@ public string Name
/// <remarks>
/// The attribute value is given as an expression, which is retained literally for later evaluation by the calling application.
/// </remarks>
public object? Value { get; set; }
public object? Value
{
get => value;
set
{
if (value != null && !IsSupportedValueType(value.GetType()))
throw new ArgumentException($"The type {value.GetType().FullName} is not a support value type HCL attribute");
this.value = value;
}
}

public static bool IsSupportedValueType(Type type)
=> type.IsPrimitive ||
type == typeof(string) ||
type == typeof(decimal) ||
type == typeof(HclStringLiteral) ||
IsSupportedValueCollectionType(type);

internal static bool IsSupportedValueCollectionType(Type type)
=> (type.IsArray && type.GetArrayRank() == 1 && IsSupportedValueType(type.GetElementType()!)) ||
(typeof(IEnumerable).IsAssignableFrom(type) && type.IsGenericType && type.GenericTypeArguments.Length == 1 && IsSupportedValueType(type.GenericTypeArguments[0]));
}
}
15 changes: 14 additions & 1 deletion source/Hcl/HBlock.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Octopus.Hcl
{
Expand All @@ -16,8 +17,20 @@ public class HBlock : HBody, IHElement
private string name;

public HBlock(string name)
: this(name, Array.Empty<string>())
{
}

public HBlock(string name, IEnumerable<string> labels)
: this(name, labels, Array.Empty<IHElement>())
{
}

public HBlock(string name, IEnumerable<string> labels, IEnumerable<IHElement> elements)
: base(elements)
{
this.name = Name = name; // Make the compiler happy
Labels = labels.ToList();
}

public string Name
Expand All @@ -34,6 +47,6 @@ public string Name
/// <remarks>
/// Block labels can either be quoted literal strings or naked identifiers.
/// </remarks>
public List<string> Labels { get; } = new List<string>();
public List<string> Labels { get; }
}
}
13 changes: 12 additions & 1 deletion source/Hcl/HBody.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace Octopus.Hcl
{
Expand All @@ -12,7 +13,17 @@ namespace Octopus.Hcl
/// </remarks>
public class HBody : IEnumerable<IHElement>
{
private readonly List<IHElement> elements = new List<IHElement>();
private readonly List<IHElement> elements;

public HBody()
{
elements = new List<IHElement>();
}

public HBody(IEnumerable<IHElement> elements)
{
this.elements = elements.ToList();
}

public IEnumerator<IHElement> GetEnumerator()
=> elements.GetEnumerator();
Expand Down
11 changes: 11 additions & 0 deletions source/Hcl/HDocument.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Octopus.Hcl
{
public class HDocument : HBody
{
public HDocument()
{

}

public HDocument(IEnumerable<IHElement> elements)
: base(elements)
{
}
}
}
Loading